Search code examples
c#unity-game-engineazure-iot-hubsignalr.clientazure-digital-twins

unity not receive signalr broadcast


This digital twins example works fine for me but when I adapt it to an IoT device (mxchip), the Unity signalr client does not receive broadcast messages.

The only change to the example is that I sent actual IoT data instead of running the example's telemetry simulator.

I can see the device telemetry received by azure and the function app finally broadcasts via signalr the data but the signalr client in Unity does not pick it up. Unity signalr client does not throw any error either.

Unity signalr client connection and disconnection subscriptions (rService.OnConnected and rService.OnDisconnected) to signalr fire correctly. However I do not see the signalr client receive the message and therefore fire the relevant subscribed events (rService.OnTelemetryMessage and rService.OnPropertyMessage at Unity's client ADTDataHandlerr.cs).

I note I have triple checked the Target for signalr is indeed what is expected by the client at receipt of the message (PropertyMessage or TelemetryMessage).

Can someone please point me what I may be missing please?

My Client:

using Microsoft.AspNetCore.SignalR.Client;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using UnityEngine;
using static System.Net.WebRequestMethods;

public class MachineSignalRService
{
public HubConnection connection;

public event Action<string> OnConnected;

public event Action<string> OnTelemetryMessage;

public event Action<MachinePropertyMessage> OnPropertyMessage;

public event Action OnDisconnected;

MachineSignalRService()
{
    if (connection != null)
    {
        connection.StopAsync();
        connection = null;
    }
}

public async Task StartAsync(string url)
{
    UnityEngine.Debug.Log("url to start SingalR Service:" + url);

    connection = new HubConnectionBuilder()
       .WithUrl(url)
       .Build();

    connection.On<MachinePropertyMessage>("PropertyMessage", (message) =>
    {
        UnityEngine.Debug.Log("Received property data " + message);
        OnPropertyMessage?.Invoke(message);
    });
    connection.On<string>("TelemetryMessage", (message) =>
    {
        UnityEngine.Debug.Log("Received telemetry data " + message);
        OnTelemetryMessage?.Invoke(message);
    });
    
    await connection.StartAsync();
    
    OnConnected?.Invoke(connection.State.ToString());

    connection.Closed += async (error) =>
    {
        OnDisconnected?.Invoke();
        await connection.StartAsync();
    };
}
}

The Data Handler object in the scene:

using Microsoft.Unity;
using System;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;

public class ADTDataHandler : MonoBehaviour
{
private MachineSignalRService rService;

public string url = "";
public MachinePartsData machinePartsData;
public MachinePartGameEvent machinePartPropertyMessageReceived;

private void Start()
{
    this.RunSafeVoid(CreateServiceAsync);
}

private void OnDestroy()
{
    if (rService != null)
    {
        rService.OnConnected -= HandleConnected;
        rService.OnTelemetryMessage -= HandleTelemetryMessage;
        rService.OnDisconnected -= HandleDisconnected;
        rService.OnPropertyMessage -= HandlePropertyMessage;
    }
}

/// <summary>
/// Received a message from SignalR. Note, this message is received on a background thread.
/// </summary>
/// <param name="message">
/// The message.
/// </param>
//private void HandleTelemetryMessage(TelemetryMachineMessage message)
//private void HandleTelemetryMessage(TelemetryMachineMessage message)
private void HandleTelemetryMessage(string message)
{
    Debug.Log("Received Telemetry Message:" + message);
    // Finally update Unity GameObjects, but this must be done on the Unity Main thread.
    /*UnityDispatcher.InvokeOnAppThread(() =>
    {
        foreach (MachinePartScriptableObject part in machinePartsData.partData)
        {
            if (part.machinePartData.PartId == message.PartId)
            {
                part.UpdateData(CreateNewMachinePartData(message));
                return;
            }
        }
    });*/
}

/// <summary>
/// Construct the MachinePart Data received from SignalR
/// </summary>
/// <param name="message">Telemetry data</param>
/// <returns>Data values of machien parts</returns>
private MachinePartData CreateNewMachinePartData(TelemetryMachineMessage message)
{
    bool successParse = Int32.TryParse(message.TimeInterval, out int thisTimeInterval);
    thisTimeInterval = !successParse ? -1 : thisTimeInterval;

    MachinePartData data = new MachinePartData
    {
        PartId = message.PartId,
        TimeInterval = thisTimeInterval,
        EventDescription = message.Description,
        EventCode = message.Code,
        Temperature = message.Temperature,
        Humidity = message.Humidity,
        Pressure = message.Pressure,
        MagnetometerX = message.MagnetometerX,
        MagnetometerY = message.MagnetometerY,
        MagnetometerZ = message.MagnetometerZ,
        AccelerometerX = message.AccelerometerX,
        AccelerometerY = message.AccelerometerY,
        AccelerometerZ = message.AccelerometerZ,
        GyroscopeX = message.GyroscopeX,
        GyroscopeY = message.GyroscopeY,
        GyroscopeZ = message.GyroscopeZ,
    };

    return data;
}

/// <summary>
/// Received a Property message from SignalR. Note, this message is received on a background thread.
/// </summary>
/// <param name="message">
/// The message
/// </param>
private void HandlePropertyMessage(MachinePropertyMessage message)
{
    Debug.Log("Received Telemetry Message:" + message);

    UnityDispatcher.InvokeOnAppThread(() =>
    {
        var matchingMachineParts = machinePartsData.machineParts.Where(t => t.Key.machinePartData.PartId == message.PartId);
        if (!matchingMachineParts.Any())
        {
            Debug.LogWarning($"MachinePart {message.PartId} was not found in the Site Data.");
            return;
        }
        var machinePartScriptableObject = matchingMachineParts.First().Key;
        machinePartScriptableObject.machinePartMetaData.Alert = message.Alert;
        machinePartPropertyMessageReceived.Raise(machinePartScriptableObject);
    });
}

private Task CreateServiceAsync()
{
    rService = new MachineSignalRService();
    rService.OnConnected += HandleConnected;
    rService.OnDisconnected += HandleDisconnected;
    rService.OnTelemetryMessage += HandleTelemetryMessage;
    rService.OnPropertyMessage += HandlePropertyMessage;

    return rService.StartAsync(url);
}

private void HandleConnected(string obj)
{
    Debug.Log("Connected:" + obj.ToString());
}

private void HandleDisconnected()
{
    Debug.Log("Disconnected");
}
}

The Server Function apps negotiating the connection and broadcasting messages:

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.EventGrid.Models;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Extensions.EventGrid;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;

namespace SignalRFunctions
{
public static class SignalRFunctions
{
    public static string partId;
    public static string timeInterval;
    public static string description;
    public static int code;
    public static double Temperature;
    public static double Humidity = 0.0D;
    public static double Pressure;

    public static double MagnetometerX;
    public static double MagnetometerY;
    public static double MagnetometerZ;

    public static double AccelerometerX;
    public static double AccelerometerY;
    public static double AccelerometerZ;

    public static double GyroscopeX;
    public static double GyroscopeY;
    public static double GyroscopeZ;

    public static bool alert;
    public static bool ledState;

    [FunctionName("negotiate")]
    public static SignalRConnectionInfo GetSignalRInfo(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest req,
        [SignalRConnectionInfo(HubName = "dttelemetry")] SignalRConnectionInfo connectionInfo, ILogger log)
    {
        log.LogInformation("Negotiate - url:" + connectionInfo.Url + " access token"+connectionInfo.AccessToken);
        return connectionInfo;
    }

    [FunctionName("broadcast")]
    public static Task SendMessage(
        [EventGridTrigger] EventGridEvent eventGridEvent,
        [SignalR(HubName = "dttelemetry")] IAsyncCollector<SignalRMessage> signalRMessages,
        ILogger log)
    {
        JObject eventGridData = (JObject)JsonConvert.DeserializeObject(eventGridEvent.Data.ToString());
        log.LogInformation("broadcast data :" + eventGridData);
        if (eventGridEvent.EventType.Contains("telemetry"))
        {
            var data = eventGridData.SelectToken("data");

            var telemetryMessage = new Dictionary<object, object>();
            foreach (JProperty property in data.Children())
            {
                //log.LogInformation("broadcast log at telemetry type:"+property.Name + " - " + property.Value);
                telemetryMessage.Add(property.Name, property.Value);
            }

            try
            {
                log.LogInformation("Success adding broadcast target with args:" + telemetryMessage);
                return signalRMessages.AddAsync(
                    new SignalRMessage
                    {
                        Target = "TelemetryMessage",
                        Arguments = new[] { telemetryMessage }
                    });
            }
            catch (Exception e)
            {
                log.LogInformation("Failed to send broadcast telemetry data" + e);
            }
            return null;
        }
        else
        {
            try
            {
                log.LogInformation("broadcast log not telemetry type:");

                //partId = eventGridEvent.Subject;
                partId = "MachinePart1";

                var data = eventGridData.SelectToken("data");
                var patch = data.SelectToken("patch");
                foreach (JToken token in patch)
                {
                    if (token["path"].ToString() == "/Alert")
                    {
                        alert = token["value"].ToObject<bool>();
                    }
                    if (token["path"].ToString() == "/ledState")
                    {
                        ledState = token["value"].ToObject<bool>();
                    }
                }

                log.LogInformation($"SingalRFunction - setting alert to: {alert}");
                log.LogInformation($"SingalRFunction - setting ledState to: {ledState}");
                var property = new Dictionary<object, object>
                {
                    {"PartID", partId },
                    {"Alert", alert },
                    {"LedState", ledState}
                };
                return signalRMessages.AddAsync(
                    new SignalRMessage
                    {
                        Target = "PropertyMessage",
                        Arguments = new[] { property }
                    });
            }
            catch (Exception e)
            {
                log.LogInformation("Exception at SingalRFunction: " + e.Message);
                return null;
            }
        }

    }
}

}

Azure broadcast data Logs:

2022-09-07T13:47:07.878 [Information] Executing 'broadcast' (Reason='EventGrid trigger fired at 2022-09-07T13:47:07.8784905+00:00', Id=xxx)

2022-09-07T13:47:07.879 [Information] broadcast data :{"data": {"ID": "MachinePart1","TimeInterval": null,"Temperature": null,"Humidity": null,"Pressure": null,"MagnetometerX": null,"MagnetometerY": null,"MagnetometerZ": null,"AccelerometerX": null,"AccelerometerY": null,"AccelerometerZ": null,"GyroscopeX": 0,"GyroscopeY": -70,"GyroscopeZ": 0},"dataschema": "dtmi:digitaltwins:Basic:MachinePart;1","contenttype": "application/json","traceparent": "xxx"}

2022-09-07T13:47:07.879 [Information] broadcast methodMicrosoft.Azure.WebJobs.Extensions.SignalRService.SignalRAsyncCollector`1[Microsoft.Azure.WebJobs.Extensions.SignalRService.SignalRMessage]

2022-09-07T13:47:07.879 [Information] broadcast log at telemetry type:ID - MachinePart1

2022-09-07T13:47:07.879 [Information] broadcast log at telemetry type:TimeInterval -

2022-09-07T13:47:07.879 [Information] broadcast log at telemetry type:Temperature -

2022-09-07T13:47:07.879 [Information] broadcast log at telemetry type:Humidity -

2022-09-07T13:47:07.879 [Information] broadcast log at telemetry type:Pressure -

2022-09-07T13:47:07.879 [Information] broadcast log at telemetry type:MagnetometerX -

2022-09-07T13:47:07.879 [Information] broadcast log at telemetry type:MagnetometerY -

2022-09-07T13:47:07.879 [Information] broadcast log at telemetry type:MagnetometerZ -

2022-09-07T13:47:07.879 [Information] broadcast log at telemetry type:AccelerometerX -

2022-09-07T13:47:07.879 [Information] broadcast log at telemetry type:AccelerometerY -

2022-09-07T13:47:07.880 [Information] broadcast log at telemetry type:AccelerometerZ -

2022-09-07T13:47:07.880 [Information] broadcast log at telemetry type:GyroscopeX - 0

2022-09-07T13:47:07.880 [Information] broadcast log at telemetry type:GyroscopeY - -70

2022-09-07T13:47:07.880 [Information] broadcast log at telemetry type:GyroscopeZ - 0

2022-09-07T13:47:07.894 [Information] Executed 'broadcast' (Succeeded, Id=xxxx, Duration=16ms)13:47:07.879

Solution

  • problem was the incoming data wasn't of the type expected by the function and could not deserialize, setting the data to be received to string made this clear.