Random wanderings through Microsoft Azure esp. PaaS plumbing, the IoT bits, AI on Micro controllers, AI on Edge Devices, .NET nanoFramework, .NET Core on *nix and ML.NET+ONNX
The first message sent shortly after I powered up the device had the latitude and longitude of Null Island
The Asset Tracker UserApplicationId is 65002 and the payload is similar to the Swarm Eval Kit. I created some message payloads (location of Christchurch Cathedral) for testing.
namespace PayloadFormatter // Additional namespace for shortening interface when usage in formatter code
{
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
public interface IFormatterUplink
{
public JObject Evaluate(IDictionary<string, string> properties, uint organisationId, uint deviceId, byte deviceType, ushort userApplicationId, JObject payloadJson, string payloadText, byte[] payloadBytes);
}
public interface IFormatterDownlink
{
public byte[] Evaluate(IDictionary<string, string> properties, uint organisationId, uint deviceId, byte deviceType, ushort userApplicationId, JObject payloadJson, string payloadText, byte[] payloadBytes);
}
}
The definitions of the uplink & downlink payload formatter evaluator interfaces have been updated and shifted to a new project.
Visual Studio 2022 Solution with payloadformatter maintenance application
I built a console application to help with developing and debugging uplink or downlink formatters. The application has a number of command line parameters which specify the formatter to be used, UserApplicationId, OrganizationId, DeviceType etc.
public class CommandLineOptions
{
[Option('d', "Direction", Required = true, HelpText = "Test Uplink or DownLink formatter")]
public string Direction { get; set; }
[Option('p', "filename", HelpText = "Uplink or Downlink Payload file name")]
public string PayloadFilename { get; set; } = string.Empty;
[Option('o', "OrganisationId", Required = true, HelpText = "Organisation unique identifier")]
public uint OrganizationId { get; set; }
[Option('i', "DeviceId", Required = true, HelpText = "Device unique identitifer")]
public uint DeviceId { get; set; }
[Option('t', "DeviceType", Required = true, HelpText = "Device type number")]
public byte DeviceType { get; set; }
[Option('u', "UserApplicationId", Required = true, HelpText = "User Application Id")]
public ushort UserApplicationId { get; set; }
[Option('h', "SwarmHiveReceivedAtUtc", HelpText = "Swarm Hive received at time UTC")]
public DateTime? SwarmHiveReceivedAtUtc { get; set; }
[Option('w', "UplinkWebHookReceivedAtUtc", HelpText = "Webhook received at time UTC")]
public DateTime? UplinkWebHookReceivedAtUtc { get; set; }
[Option('s', "Status", HelpText = "Uplink local file system file name")]
public byte? Status { get; set; }
[Option('c', "Client", HelpText = "Uplink local file system file name")]
public string Client { get; set; }
}
The downlink formatter (similar approach for uplink) loads the sample file as an array of bytes, then tries to convert it to text, and finally to JSON. Then the formatter code is “compiled” and the executed with the file payload and command line parameters.
private static async Task DownlinkFormatterCore(CommandLineOptions options)
{
Dictionary<string, string> properties = new Dictionary<string, string>();
string formatterFolder = Path.Combine(Environment.CurrentDirectory, "downlink");
Console.WriteLine($"Downlink- uplinkFormatterFolder: {formatterFolder}");
string formatterFile = Path.Combine(formatterFolder, $"{options.UserApplicationId}.cs");
Console.WriteLine($"Downlink- UserApplicationId: {options.UserApplicationId}");
Console.WriteLine($"Downlink- Payload formatter file: {formatterFile}");
PayloadFormatter.IFormatterDownlink evalulator;
try
{
evalulator = CSScript.Evaluator.LoadFile<PayloadFormatter.IFormatterDownlink>(formatterFile);
}
catch (CSScriptLib.CompilerException cex)
{
Console.Write($"Loading or compiling file:{formatterFile} failed Exception:{cex}");
return;
}
string payloadFilename = Path.Combine(formatterFolder, options.PayloadFilename);
Console.WriteLine($"Downlink- payloadFilename:{payloadFilename}");
byte[] uplinkBytes;
try
{
uplinkBytes = File.ReadAllBytes(payloadFilename);
}
catch (DirectoryNotFoundException dex)
{
Console.WriteLine($"Uplink payload filename directory {formatterFolder} not found:{dex}");
return;
}
catch (FileNotFoundException fnfex)
{
Console.WriteLine($"Uplink payload filename {payloadFilename} not found:{fnfex}");
return;
}
catch (FormatException fex)
{
Console.WriteLine($"Uplink payload file invalid format {payloadFilename} not found:{fex}");
return;
}
// See if payload can be converted to a string
string uplinkText = string.Empty;
try
{
uplinkText = Encoding.UTF8.GetString(uplinkBytes);
}
catch (FormatException fex)
{
Console.WriteLine("Encoding.UTF8.GetString failed:{0}", fex.Message);
}
// See if payload can be converted to JSON
JObject uplinkJson;
try
{
uplinkJson = JObject.Parse(uplinkText);
}
catch (JsonReaderException jrex)
{
Console.WriteLine("JObject.Parse failed Exception:{1}", jrex);
uplinkJson = new JObject();
}
Console.WriteLine("Properties");
foreach (var property in properties)
{
Console.WriteLine($"{property.Key}:{property.Value}");
}
// Transform the byte and optional text and JSON payload
Byte[] payload;
try
{
payload = evalulator.Evaluate(properties, options.OrganizationId, options.DeviceId, options.DeviceType, options.UserApplicationId, uplinkJson, uplinkText, uplinkBytes);
}
catch (Exception ex)
{
Console.WriteLine($"evalulatorUplink.Evaluate failed Exception:{ex}");
return;
}
Console.WriteLine("Payload");
Console.WriteLine(Convert.ToBase64String(payload));
}
The sample JSON payload is what would be sent by Azure IoT Central to a device to configure the fan speed
Azure IoT Central M138 Breakout device template with the Fan Status command selected
{
"FanStatus": 2
}
If the downlink payload formatter is compiled and executes successfully the Base64 representation output is displayed
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
public class FormatterDownlink : PayloadFormatter.IFormatterDownlink
{
public byte[] Evaluate(IDictionary<string, string> properties, uint organisationId, uint deviceId, byte deviceType, ushort userApplicationId, JObject payloadJson, string payloadText, byte[] payloadBytes)
{
byte? status = payloadJson.Value<byte?>("FanStatus");
if ( status.HasValue )
{
return new byte[] { status.Value };
}
return new byte[]{};
}
}
If the downlink payload formatter syntax is incorrect e.g. { status.Value ; }; an error message with the line and column is displayed.
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
public class FormatterDownlink : PayloadFormatter.IFormatterDownlink
{
public byte[] Evaluate(IDictionary<string, string> properties, uint organisationId, uint deviceId, byte deviceType, ushort userApplicationId, JObject payloadJson, string payloadText, byte[] payloadBytes)
{
byte? status = payloadJson.Value<byte?>("FanStatus");
if ( status.HasValue )
{
return new byte[] { status.Value ; };
}
return new byte[]{};
}
}
If the downlink payload formatter syntax is correct but execution fails (in the example code division by zero) an error message is displayed.
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
public class FormatterDownlink : PayloadFormatter.IFormatterDownlink
{
public byte[] Evaluate(IDictionary<string, string> properties, uint organisationId, uint deviceId, byte deviceType, ushort userApplicationId, JObject payloadJson, string payloadText, byte[] payloadBytes)
{
byte? status = payloadJson.Value<byte?>("FanStatus");
if ( status.HasValue )
{
int divideByZero = 10;
divideByZero = divideByZero / 0;
return new byte[] { status.Value };
}
return new byte[]{};
}
}
Swarm Space Bumble hive classes in Visual Studio 2022
My SwarmSpaceAzureIoTConnector project only needed to login, get a list of devices and send messages so all the additional functionality was never going to be used. The method to send a message didn’t work, the class used for the payload (UserMessage) appears to be wrong.
"post": {
"tags": [ "messages" ],
"summary": "POST user messages",
"description": "<p>This endpoint submits a JSON formatted UserMessage object for delivery to a Swarm device. A JSON object is returned with a newly assigned <code>packetId</code> and <code>status</code> of<code>OK</code> on success, or <code>ERROR</code> (with a description of the error) on failure.</p><p>The current user must have access to the <code>userApplicationId</code> and <code>device</code> given inside the UserMessage JSON. The device must also have the ability to receive messages from the Hive (\"two-way communication\") enabled. If these conditions are not met, a response with status code 403 (Forbidden) will be returned.</p><p>Note that the <code>data</code> field is the <b>Base64-encoded</b> version of the data to be sent. This allows the sending of binary, as well as text, data.</p>",
"operationId": "addApplicationMessage",
"requestBody": {
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserMessage" } } },
"required": true
},
"responses": {
"401": {
"description": "Unauthorized",
"content": { "*/*": { "schema": { "$ref": "#/components/schemas/ApiError" } } }
},
"403": {
"description": "Forbidden",
"content": { "*/*": { "schema": { "$ref": "#/components/schemas/ApiError" } } }
},
"400": {
"description": "Bad Request",
"content": { "*/*": { "schema": { "$ref": "#/components/schemas/ApiError" } } }
},
"200": {
"description": "OK",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/PacketPostReturn" } } }
}
}
}
},
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.17.0.0 (NJsonSchema v10.8.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class UserMessage
{
/// <summary>
/// Swarm packet ID
/// </summary>
[Newtonsoft.Json.JsonProperty("packetId", Required = Newtonsoft.Json.Required.Always)]
public long PacketId { get; set; }
/// <summary>
/// Swarm message ID. There may be multiple messages for a single message ID. A message ID represents an intent to send a message, but there may be multiple Swarm packets that are required to fulfill that intent. For example, if a Hive -> device message fails to reach its destination, automatic retry attempts to send that message will have the same message ID.
/// </summary>
[Newtonsoft.Json.JsonProperty("messageId", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public long MessageId { get; set; }
/// <summary>
/// Swarm device type
/// </summary>
[Newtonsoft.Json.JsonProperty("deviceType", Required = Newtonsoft.Json.Required.Always)]
public int DeviceType { get; set; }
/// <summary>
/// Swarm device ID
/// </summary>
[Newtonsoft.Json.JsonProperty("deviceId", Required = Newtonsoft.Json.Required.Always)]
public int DeviceId { get; set; }
/// <summary>
/// Swarm device name
/// </summary>
[Newtonsoft.Json.JsonProperty("deviceName", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string DeviceName { get; set; }
/// <summary>
/// Direction of message
/// </summary>
[Newtonsoft.Json.JsonProperty("direction", Required = Newtonsoft.Json.Required.Always)]
public int Direction { get; set; }
/// <summary>
/// Message data type, always = 6
/// </summary>
[Newtonsoft.Json.JsonProperty("dataType", Required = Newtonsoft.Json.Required.Always)]
public int DataType { get; set; }
/// <summary>
/// Application ID
/// </summary>
[Newtonsoft.Json.JsonProperty("userApplicationId", Required = Newtonsoft.Json.Required.Always)]
public int UserApplicationId { get; set; }
/// <summary>
/// Organization ID
/// </summary>
[Newtonsoft.Json.JsonProperty("organizationId", Required = Newtonsoft.Json.Required.Always)]
public int OrganizationId { get; set; }
/// <summary>
/// Length of data (in bytes) before base64 encoding
/// </summary>
[Newtonsoft.Json.JsonProperty("len", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public int Len { get; set; }
/// <summary>
/// Base64 encoded data string
/// </summary>
[Newtonsoft.Json.JsonProperty("data", Required = Newtonsoft.Json.Required.Always)]
[System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)]
public byte[] Data { get; set; }
/// <summary>
/// Swarm packet ID of acknowledging packet from device
/// </summary>
[Newtonsoft.Json.JsonProperty("ackPacketId", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public long AckPacketId { get; set; }
/// <summary>
/// Message status. Possible values:
/// <br/>0 = incoming message (from a device)
/// <br/>1 = outgoing message (to a device)
/// <br/>2 = incoming message, acknowledged as seen by customer. OR a outgoing message packet is on groundstation
/// <br/>3 = outgoing message, packet is on satellite
/// <br/>-1 = error
/// <br/>-3 = failed to deliver, retrying
/// <br/>-4 = failed to deliver, will not re-attempt
/// <br/>
/// </summary>
[Newtonsoft.Json.JsonProperty("status", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public int Status { get; set; }
/// <summary>
/// Time that the message was received by the Hive
/// </summary>
[Newtonsoft.Json.JsonProperty("hiveRxTime", Required = Newtonsoft.Json.Required.Always)]
[System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)]
public System.DateTimeOffset HiveRxTime { get; set; }
private System.Collections.Generic.IDictionary<string, object> _additionalProperties;
[Newtonsoft.Json.JsonExtensionData]
public System.Collections.Generic.IDictionary<string, object> AdditionalProperties
{
get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary<string, object>()); }
set { _additionalProperties = value; }
}
}
After several attempts I gave up and have rebuilt the required Bumble bee hive integration with RestSharp
public async Task SendAsync(uint organisationId, uint deviceId, byte deviceType, ushort userApplicationId, byte[] data, CancellationToken cancellationToken)
{
await TokenRefresh(cancellationToken);
_logger.LogInformation("SendAsync: OrganizationId:{0} DeviceType:{1} DeviceId:{2} UserApplicationId:{3} Data:{4} Enabled:{5}", organisationId, deviceType, deviceId, userApplicationId, Convert.ToBase64String(data), _bumblebeeHiveSettings.DownlinkEnabled);
Models.MessageSendRequest message = new Models.MessageSendRequest()
{
OrganizationId = (int)organisationId,
DeviceType = deviceType,
DeviceId = (int)deviceId,
UserApplicationId = userApplicationId,
Data = data,
};
RestClientOptions restClientOptions = new RestClientOptions()
{
BaseUrl = new Uri(_bumblebeeHiveSettings.BaseUrl),
ThrowOnAnyError = true,
};
using (RestClient client = new RestClient(restClientOptions))
{
RestRequest request = new RestRequest("api/v1/messages", Method.Post);
request.AddBody(message);
request.AddHeader("Authorization", $"bearer {_token}");
// To save the limited monthly allocation of mesages downlinks can be disabled
if (_bumblebeeHiveSettings.DownlinkEnabled)
{
var response = await client.PostAsync<Models.MessageSendResponse>(request, cancellationToken);
_logger.LogInformation("SendAsync-Result:{Status} PacketId:{PacketId}", response.Status, response.PacketId);
}
}
}
The application now has a StartUpService which loads the Azure DeviceClient cache (Lazy Cache) in the background as the application starts up. If an uplink message is received from a SwarmDevice before, it has been loaded by the FunctionsStartup the DeviceClient information is cached and another connection to the Azure IoT Hub is not established.
I’m looking at building a webby user interface where users an interactivity list, create, edit, delete formatters with syntax highlighter support, and the executing the formatter with sample payloads.
Swarm Space Azure IoT Connector Identity Translation Gateway Architecture
This approach uses most of the existing building blocks, and that’s it no more changes.
I initially tried building a cache loader with BackgroundService so that the DeviceClient cache would start loading as the application started but interdependencies became problem.
public partial class Connector
{
[Function("BumblebeeHiveCacheRefresh")]
public async Task<IActionResult> BumblebeeHiveCacheRefreshRun([HttpTrigger(AuthorizationLevel.Function, "get")] CancellationToken cancellationToken)
{
_logger.LogInformation("BumblebeeHiveCacheRefresh start");
await _swarmSpaceBumblebeeHive.Login(cancellationToken);
foreach (SwarmSpace.BumblebeeHiveClient.Device device in await _swarmSpaceBumblebeeHive.DeviceListAsync(cancellationToken))
{
_logger.LogInformation("BumblebeeHiveCacheRefresh DeviceId:{DeviceId} DeviceName:{DeviceName}", device.DeviceId, device.DeviceName);
Models.AzureIoTDeviceClientContext context = new Models.AzureIoTDeviceClientContext()
{
// TODO seems a bit odd getting this from application settings
OrganisationId = _applicationSettings.OrganisationId,
//UserApplicationId = device.UserApplicationId, deprecated
DeviceType = (byte)device.DeviceType,
DeviceId = (uint)device.DeviceId,
};
switch (_azureIoTSettings.ApplicationType)
{
case Models.ApplicationType.AzureIotHub:
switch (_azureIoTSettings.AzureIotHub.ConnectionType)
{
case Models.AzureIotHubConnectionType.DeviceConnectionString:
await _azureDeviceClientCache.GetOrAddAsync<DeviceClient>(device.DeviceId.ToString(), (ICacheEntry x) => AzureIoTHubDeviceConnectionStringConnectAsync(device.DeviceId.ToString(), context));
break;
case Models.AzureIotHubConnectionType.DeviceProvisioningService:
await _azureDeviceClientCache.GetOrAddAsync<DeviceClient>(device.DeviceId.ToString(), (ICacheEntry x) => AzureIoTHubDeviceProvisioningServiceConnectAsync(device.DeviceId.ToString(), context, _azureIoTSettings.AzureIotHub.DeviceProvisioningService));
break;
default:
_logger.LogError("Azure IoT Hub ConnectionType unknown {0}", _azureIoTSettings.AzureIotHub.ConnectionType);
throw new NotImplementedException("AzureIoT Hub unsupported ConnectionType");
}
break;
case Models.ApplicationType.AzureIoTCentral:
await _azureDeviceClientCache.GetOrAddAsync<DeviceClient>(device.DeviceId.ToString(), (ICacheEntry x) => AzureIoTHubDeviceProvisioningServiceConnectAsync(device.DeviceId.ToString(), context, _azureIoTSettings.AzureIoTCentral.DeviceProvisioningService));
break;
default:
_logger.LogError("AzureIoT application type unknown {0}", _azureIoTSettings.ApplicationType);
throw new NotImplementedException("AzureIoT unsupported ApplicationType");
}
}
_logger.LogInformation("BumblebeeHiveCacheRefresh finish");
return new OkResult();
}
}
In the short-term loading the cache with a call to an Azure HTTPTrigger Function works but may timeout issues. When I ran the connector with my 100’s of devices simulator the function timed out every so often.
There are now separate Data Transfer Objects(DTO) for the uplink and queue message payloads mainly, because the UplinkPayloadQueueDto has additional fields for the client (based on the x-api-key) and when the webhook was called.
public class UplinkPayloadQueueDto
{
public ulong PacketId { get; set; }
public byte DeviceType { get; set; }
public uint DeviceId { get; set; }
public ushort UserApplicationId { get; set; }
public uint OrganizationId { get; set; }
public string Data { get; set; } = string.Empty;
public byte Length { get; set; }
public int Status { get; set; }
public DateTime SwarmHiveReceivedAtUtc { get; set; }
public DateTime UplinkWebHookReceivedAtUtc { get; set; }
public string Client { get; set; } = string.Empty;
}
public class UplinkPayloadWebDto
{
public ulong PacketId { get; set; }
public byte DeviceType { get; set; }
public uint DeviceId { get; set; }
public ushort UserApplicationId { get; set; }
public uint OrganizationId { get; set; }
public string Data { get; set; } = string.Empty;
[Range(Constants.PayloadLengthMinimum, Constants.PayloadLengthMaximum)]
public byte Len { get; set; }
public int Status { get; set; }
public DateTime HiveRxTime { get; set; }
}
I did consider using AutoMapper to copy the values from the UplinkPayloadWebDto to the UplinkPayloadQueueDto but the additional complexity/configuration required for one mapping wasn’t worth it.
The UplinkController has a single POST method, which has a JSON payload(FromBody) and a single header (FromHeader) “x-api-key” which is to secure the method and identify the caller.
[HttpPost]
public async Task<IActionResult> Post([FromHeader(Name = "x-api-key")] string xApiKeyValue, [FromBody] Models.UplinkPayloadWebDto payloadWeb)
{
if (!_applicationSettings.XApiKeys.TryGetValue(xApiKeyValue, out string apiKeyName))
{
_logger.LogWarning("Authentication unsuccessful X-API-KEY value:{xApiKeyValue}", xApiKeyValue);
return this.Unauthorized("Unauthorized client");
}
_logger.LogInformation("Authentication successful X-API-KEY value:{apiKeyName}", apiKeyName);
// Could of used AutoMapper but didn't seem worth it for one place
Models.UplinkPayloadQueueDto payloadQueue = new()
{
PacketId = payloadWeb.PacketId,
DeviceType = payloadWeb.DeviceType,
DeviceId = payloadWeb.DeviceId,
UserApplicationId = payloadWeb.UserApplicationId,
OrganizationId = payloadWeb.OrganizationId,
Data = payloadWeb.Data,
Length = payloadWeb.Len,
Status = payloadWeb.Status,
SwarmHiveReceivedAtUtc = payloadWeb.HiveRxTime,
UplinkWebHookReceivedAtUtc = DateTime.UtcNow,
Client = apiKeyName,
};
_logger.LogInformation("SendAsync queue name:{QueueName}", _applicationSettings.QueueName);
QueueClient queueClient = _queueServiceClient.GetQueueClient(_applicationSettings.QueueName);
await queueClient.SendMessageAsync(Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(payloadQueue)));
return this.Ok();
}
This post could have been much longer with more screen grabs and code snippets, so this is the “highlights package”. This post took a lot longer than I expected as building, testing locally, then deploying the different implementations was time consuming.
Swarm Space Connector Functions Projects
I built the projects to investigate the different options taking into account reliability, robustness, amount of code, performance (I think slow startup could be a problem). The code is very “plain” I used the default options, no copyright notices, default formatting, context sensitive error messages were used to add any required “using” statements, libraries etc.
The desktop emulator hosting the six functions
I also deployed the Azure Functions and ASP .NET CoreWebAPI application to check there were no difference (beyond performance) in the way they worked. I included a “default” function (generated by the new project wizard) for reference while I was building the others.
The function application with six functions in deployed to Azure
namespace WebhookHttpTrigger
{
public static class TypedAutomagic
{
[FunctionName("TypedAutomagic")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] UplinkPayload payload,
ILogger log)
{
log.LogInformation($"C# HTTP trigger function typed TypedAutomagic UplinkPayload processed a request PacketId:{payload.PacketId}");
return new OkObjectResult("Hello, This HTTP triggered automagic function executed successfully.");
}
}
}
Successful execution of TypedAutomagic function
The “TypedAutomagic” implementation also detected when the JSON property values in the payload couldn’t be deserialised successfully, but if the hiveRxTime was invalid the value was set to 1/1/0001 12:00:00 am.
namespace WebhookHttpTrigger
{
public static class TypedDeserializeObjectAnnotations
{
[FunctionName("TypedDeserializeObjectAnnotations")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] string httpPayload,
ILogger log)
{
UplinkPayload uplinkPayload;
try
{
uplinkPayload = JsonConvert.DeserializeObject<UplinkPayload>(httpPayload);
}
catch (Exception ex)
{
log.LogWarning(ex, "JsonConvert.DeserializeObject failed");
return new BadRequestObjectResult(ex.Message);
}
var context = new ValidationContext(uplinkPayload, serviceProvider: null, items: null);
var results = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(uplinkPayload, context, results,true);
if (!isValid)
{
log.LogWarning("Validator.TryValidateObject failed results:{results}", results);
return new BadRequestObjectResult(results);
}
log.LogInformation($"C# HTTP trigger function typed DeserializeObject UplinkPayload processed a request PacketId:{uplinkPayload.PacketId}");
return new OkObjectResult("Hello, This HTTP triggered DeserializeObject function executed successfully.");
}
}
}
I built an ASP .NET CoreWebAPI version with two uplink method implementations, one which used dependency injection (DI) and the other that didn’t. I also added code to validate the deserialisation of HiveRxTimeUtc.
...
[HttpPost]
public async Task<IActionResult> Post([FromBody] UplinkPayload payload)
{
// Check that the post data is good
if (!this.ModelState.IsValid)
{
_logger.LogWarning("QueuedController validation failed {0}", this.ModelState.ToString());
return this.BadRequest(this.ModelState);
}
if ( payload.HiveRxTimeUtc == DateTime.MinValue)
{
_logger.LogWarning("HiveRxTimeUtc validation failed");
return this.BadRequest();
}
try
{
QueueClient queueClient = new QueueClient(_configuration.GetConnectionString("AzureWebApi"), "uplink");
//await queueClient.CreateIfNotExistsAsync();
await queueClient.SendMessageAsync(Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(payload)));
}
catch (Exception ex)
{
_logger.LogError(ex,"Unable to open/create queue or send message", ex);
return this.Problem("Unable to open queue (creating if it doesn't exist) or send message", statusCode: 500, title: "Uplink payload not sent");
}
return this.Ok();
}
Swarm Space Azure IoT Connector Identity Translation Gateway Architecture
The new approach uses most of the existing building blocks but adds an Azure HTTP Trigger which receives the Swarm Space Bumble bee hive Webhook Delivery Method calls and writes them to an Azure Storage Queue.
Swarm Space Bumble bee hive Web Hook Delivery method
The uplink and downlink formatters are now called asynchronously so they have limited impact on the overall performance of the application.
Azure IoT Central with consecutive duplicate PacketIds
Then I started to pay more attention and noticed that duplicate PacketIds could be interleaved
Azure IoT Central with interleaved duplicate PacketIds
Shortly after noticing the interleaved PacketIds I checked the Delivery Method and found there were message delivery timeouts.
Swarm Space Delivery with method timeouts
In Azure Application Insights I could see that the UplinkController was taking up to 15 seconds to execute which was longer than the bumblebee hive delivery timeout.
In Telerik Fiddler I could see calls to the UplinkController taking 16 seconds to execute. (I did see 30+ seconds)
Telerik Fiddler showing duration of Uplink controller calls
To see if the problem was loading CS-Script I added code to load a simple function as the application started. After averaging the duration over many executions there was little difference in the duration.
public interface IApplication
{
public DateTime Startup(DateTime utcNow);
}
...
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await Task.Yield();
_logger.LogInformation("StartUpService.ExecuteAsync start");
// Force the loading and startup of CS Script evaluator
dynamic application = CSScript.Evaluator
.LoadCode(
@"using System;
public class Application : IApplication
{
public DateTime Startup(DateTime utcNow)
{
return utcNow;
}
}");
DateTime result = application.Startup(DateTime.UtcNow);
try
{
await _swarmSpaceBumblebeeHive.Login(cancellationToken);
await _azureIoTDeviceClientCache.Load(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "StartUpService.ExecuteAsync error");
throw;
}
_logger.LogInformation("StartUpService.ExecuteAsync finish");
}
using System;
using System.Globalization;
using System.Text;
using Microsoft.Azure.Devices.Client;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
public class FormatterUplink : PayloadFormatter.IFormatterUplink
{
public Message Evaluate(int organisationId, int deviceId, int deviceType, int userApplicationId, JObject telemetryEvent, JObject payloadJson, string payloadText, byte[] payloadBytes)
{
if ((payloadText != "") && (payloadJson != null))
{
JObject location = new JObject();
location.Add("lat", payloadJson.GetValue("lt"));
location.Add("lon", payloadJson.GetValue("ln"));
location.Add("alt", payloadJson.GetValue("a"));
telemetryEvent.Add("DeviceLocation", location);
}
Message ioTHubmessage = new Message(Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(telemetryEvent)));
ioTHubmessage.Properties.Add("iothub-creation-time-utc", DateTimeOffset.FromUnixTimeSeconds((long)payloadJson.GetValue("d")).ToString("s", CultureInfo.InvariantCulture));
return ioTHubmessage;
}
}
I then added code to load the most complex uplink and downlink formatters as the application started. There was a significant reduction in the UplinkController execution durations, but it could still take more than 30 seconds.
I then added detailed telemetry to the code and found that the duration (also variability) was a combination of Azure IoT Device Provisoning Service(DPS) registration, Azure IoT Hub connection establishment, CS-Script payload formatter loading/compilation/execution, application startup tasks and message uploading durations.
After much experimentation It looks like that “synchronously” calling the payload processing code from the Uplink controller is not a viable approach as the Swarm Space Bumblebee hive calls regularly timeout resulting in duplicate messages.
https://json2csharp.com/
// Root myDeserializedClass = JsonConvert.DeserializeObject<Root>(myJsonResponse);
public class Root
{
public int packetId { get; set; }
public int deviceType { get; set; }
public int deviceId { get; set; }
public int userApplicationId { get; set; }
public int organizationId { get; set; }
public string data { get; set; }
public int len { get; set; }
public int status { get; set; }
public DateTime hiveRxTime { get; set; }
}
*/
public class UplinkPayload
{
[JsonProperty("packetId")]
public int PacketId { get; set; }
[JsonProperty("deviceType")]
public int DeviceType { get; set; }
[JsonProperty("deviceId")]
public int DeviceId { get; set; }
[JsonProperty("userApplicationId")]
public int UserApplicationId { get; set; }
[JsonProperty("organizationId")]
public int OrganizationId { get; set; }
[JsonProperty("data")]
[JsonRequired]
public string Data { get; set; }
[JsonProperty("len")]
public int Len { get; set; }
[JsonProperty("status")]
public int Status { get; set; }
[JsonProperty("hiveRxTime")]
public DateTime HiveRxTime { get; set; }
}
This class is used to “automagically” deserialise Delivery Webhook payloads. There is also some additional payload validation which discards test messages (not certain this is a good idea) etc.
//---------------------------------------------------------------------------------
// Copyright (c) December 2022, devMobile Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//---------------------------------------------------------------------------------
namespace devMobile.IoT.SwarmSpace.AzureIoT.Connector.Controllers
{
using System.Globalization;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Devices.Client;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
[ApiController]
[Route("api/[controller]")]
public class UplinkController : ControllerBase
{
private readonly ILogger<UplinkController> _logger;
private readonly IAzureIoTDeviceClientCache _azureIoTDeviceClientCache;
public UplinkController(ILogger<UplinkController> logger, IAzureIoTDeviceClientCache azureIoTDeviceClientCache)
{
_logger = logger;
_azureIoTDeviceClientCache = azureIoTDeviceClientCache;
}
[HttpPost]
public async Task<IActionResult> Uplink([FromBody] Models.UplinkPayload payload)
{
DeviceClient deviceClient;
_logger.LogDebug("Payload {0}", JsonConvert.SerializeObject(payload, Formatting.Indented));
if (payload.PacketId == 0)
{
_logger.LogWarning("Uplink-payload simulated DeviceId:{DeviceId}", payload.DeviceId);
return this.Ok();
}
if ((payload.UserApplicationId < Constants.UserApplicationIdMinimum) || (payload.UserApplicationId > Constants.UserApplicationIdMaximum))
{
_logger.LogWarning("Uplink-payload invalid User Application Id:{UserApplicationId}", payload.UserApplicationId);
return this.BadRequest($"Invalid User Application Id {payload.UserApplicationId}");
}
if ((payload.Len < Constants.PayloadLengthMinimum) || string.IsNullOrEmpty(payload.Data))
{
_logger.LogWarning("Uplink-payload.Data is empty PacketId:{PacketId}", payload.PacketId);
return this.Ok("payload.Data is empty");
}
Models.AzureIoTDeviceClientContext context = new Models.AzureIoTDeviceClientContext()
{
OrganisationId = payload.OrganizationId,
UserApplicationId = payload.UserApplicationId,
DeviceType = payload.DeviceType,
DeviceId = payload.DeviceId,
};
deviceClient = await _azureIoTDeviceClientCache.GetOrAddAsync(payload.DeviceId.ToString(), context);
JObject telemetryEvent = new JObject
{
{ "packetId", payload.PacketId},
{ "deviceType" , payload.DeviceType},
{ "DeviceID", payload.DeviceId },
{ "organizationId", payload.OrganizationId },
{ "ApplicationId", payload.UserApplicationId},
{ "ReceivedAtUtc", payload.HiveRxTime.ToString("s", CultureInfo.InvariantCulture) },
{ "DataLength", payload.Len },
{ "Data", payload.Data },
{ "Status", payload.Status },
};
// Send the message to Azure IoT Hub
using (Message ioTHubmessage = new Message(Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(telemetryEvent))))
{
// Ensure the displayed time is the acquired time rather than the uploaded time.
ioTHubmessage.Properties.Add("iothub-creation-time-utc", payload.HiveRxTime.ToString("s", CultureInfo.InvariantCulture));
ioTHubmessage.Properties.Add("OrganizationId", payload.OrganizationId.ToString());
ioTHubmessage.Properties.Add("ApplicationId", payload.UserApplicationId.ToString());
ioTHubmessage.Properties.Add("DeviceId", payload.DeviceId.ToString());
ioTHubmessage.Properties.Add("deviceType", payload.DeviceType.ToString());
await deviceClient.SendEventAsync(ioTHubmessage);
_logger.LogInformation("Uplink-DeviceID:{deviceId} SendEventAsync success", payload.DeviceId);
}
return this.Ok();
}
}
}
The webhook was configured to “acknowledge messages on successful delivery”. I then checked my Delivery Method configuration with a couple of “Test” messages.
My Swarm Space Eval Kit arrived md-week and after some issues with jumper settings it started reporting position and status information.