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 initial comparison running on my development box (will benchmark on my Seeedstudio EdgeBox RPi 200.) was roughly what I was expecting though the SkaiSharp 2560×1440 mean duration was a bit odd. I think that the difference in the amount of memory allocated is because SkaiSharp’s memory is allocated by the native code. Both benchmarks need some refactoring to improve repeatability on my different platforms.
These benchmarks should be treated as indicative not authoritative
The second prototype of “transforming” telemetry data used C# code complied and binary cached on demand. The HiveMQClient based application subscribes to topics (devices publishing environmental measurements) and then republishes them to multiple topics.
public class messageTransformer : IMessageTransformer
{
public MQTT5PublishMessage[] Transform(MQTT5PublishMessage message)
{
if (message.Payload is null)
{
return [];
}
var payload = Encoding.UTF8.GetString(message.Payload);
// Simple transformations: convert to both upper and lower case
var toLower = new MQTT5PublishMessage
{
Topic = message.Topic,
Payload = Encoding.UTF8.GetBytes(payload.ToLower()),
QoS = QualityOfService.AtLeastOnceDelivery
};
var toUpper = new MQTT5PublishMessage
{
Topic = message.Topic,
Payload = Encoding.UTF8.GetBytes(payload.ToUpper()),
QoS = QualityOfService.AtLeastOnceDelivery
};
return [toLower, toUpper];
}
}
The sample C# code (LowerUpper.cs) implements the IMessageTransformer interface and republishes both lower and upper case versions of the message.
Once the transformer had been loaded then compiled there was no noticeable difference between the application, loaded from constant string, and loaded from external file versions.
private static void OnMessageReceived(object? sender, HiveMQtt.Client.Events.OnMessageReceivedEventArgs e)
{
HiveMQClient client = (HiveMQClient)sender!;
Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} HiveMQ.receive start");
Console.WriteLine($" Topic:{e.PublishMessage.Topic} QoS:{e.PublishMessage.QoS} Payload:{e.PublishMessage.PayloadAsString}");
Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} HiveMQ.Publish start");
foreach (string topic in _applicationSettings.PublishTopics.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
e.PublishMessage.Topic = string.Format(topic, _applicationSettings.ClientId);
var transformer = _scriptEngine.GetTransformer();
if (transformer is null)
{
Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} Transformer is null");
return;
}
var transformedMessages = transformer.Transform(e.PublishMessage);
if (transformedMessages is null)
{
Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} Transformer returned null");
return;
}
if (transformedMessages.Length == 0)
{
Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} Transformer returned no messages");
return;
}
foreach (MQTT5PublishMessage message in transformer.Transform(e.PublishMessage))
{
if (message is null)
{
Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} Transformer message is null");
continue;
}
try
{
Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} Topic:{e.PublishMessage.Topic} HiveMQ Publish start ");
var resultPublish = client.PublishAsync(message).GetAwaiter().GetResult();
Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} Published:{resultPublish.QoS1ReasonCode} {resultPublish.QoS2ReasonCode}");
}
catch (Exception ex)
{
Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} HiveMQ Publish exception {ex.Message}");
}
}
}
Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} HiveMQ.Receive finish");
}
The MQTTX application subscribed to topics that devices (XiaoTandHandCO2A, XiaoTandHandCO2B etc.) and the simulated bridge (in my case DESKTOP-EN0QGL0) published.
The first prototype of “transforming” telemetry data used C# code complied with the application. The HiveMQClient based application subscribes to topics (devices publishing environmental measurements) and then republishes them to multiple topics.
The MQTTX application subscribed to topics that devices (XiaoTandHandCO2A, XiaoTandHandCO2B etc.) and the simulated bridge (DESKTOP-EN0QGL0) published.
The second prototype “transforms” the telemetry message payloads with C# code that is compiled (with CSScript) as the application starts. The application subscribes to the topics which devices publish (environmental measurements), transforms the payloads, and then republishes the transformed messages to “bridge” topics
private static void OnMessageReceived(object? sender, HiveMQtt.Client.Events.OnMessageReceivedEventArgs e)
{
HiveMQClient client = (HiveMQClient)sender!;
Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} HiveMQ.receive start");
Console.WriteLine($" Topic:{e.PublishMessage.Topic} QoS:{e.PublishMessage.QoS} Payload:{e.PublishMessage.PayloadAsString}");
Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} HiveMQ.Publish start");
foreach (string topic in _applicationSettings.PublishTopics.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
e.PublishMessage.Topic = string.Format(topic, _applicationSettings.ClientId);
foreach (MQTT5PublishMessage message in _evaluator.Transform(e.PublishMessage))
{
Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} Topic:{e.PublishMessage.Topic} HiveMQ Publish start ");
var resultPublish = client.PublishAsync(message).GetAwaiter().GetResult();
Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} Published:{resultPublish.QoS1ReasonCode} {resultPublish.QoS2ReasonCode}");
}
}
Console.WriteLine($"{DateTime.UtcNow:yy-MM-dd HH:mm:ss:fff} HiveMQ.Receive finish");
}
// This code is compiled as the application starts up, it implements the IMessageTransformer interface
const string sampleTransformCode = @"
using System.Text;
using HiveMQtt.MQTT5.Types;
public class messageTransformer : devMobile.IoT.MqttTransformer.CSScriptLoopback.IMessageTransformer
{
public MQTT5PublishMessage[] Transform(MQTT5PublishMessage message)
{
// Example: echo the payload to a new topic
var payload = Encoding.UTF8.GetString(message.Payload);
// Simple transformations: convert to uppercase or lowercase
var toLower = new MQTT5PublishMessage
{
Topic = message.Topic,
Payload = Encoding.UTF8.GetBytes(payload.ToLower()),
QoS = QualityOfService.AtLeastOnceDelivery
};
var toUpper = new MQTT5PublishMessage
{
Topic = message.Topic,
Payload = Encoding.UTF8.GetBytes(payload.ToUpper()),
QoS = QualityOfService.AtLeastOnceDelivery
};
return new[] { toLower, toUpper };
}
}";
The sample C# code implements the IMessageTransformer interface and republishes lower and upper case versions of the message.
public interface IMessageTransformer
{
public MQTT5PublishMessage[] Transform(MQTT5PublishMessage mqttPublishMessage);
}
...
_evaluator = CSScript.Evaluator.LoadCode<IMessageTransformer>(sampleTransformCode);
The SwarmSpace, and Myriota gateways both use an interface based approach to process uplink and downlink messages. Future versions will include support for isolating processing so that a rogue script can’t crash the application or reference unapproved assemblies
This sensor has an operating voltage of 3.6-30V/DC so it can be powered by the 5V output of a RS485 Breakout Board for Seeed Studio XIAO (SKU 113991354). The red wire is for powering the breakout and device with a 12V power supply so was tied back so it didn’t touch any of the other electronics.
HardwareSerial RS485Serial(1);
ModbusMaster node;
// -----------------------------
// RS485 Pin Assignments (Corrected)
// -----------------------------
const int RS485_RX = 6; // UART1 RX
const int RS485_TX = 5; // UART1 TX
const int RS485_EN = D2;
// Sensor/Modbus parameters (from datasheet)
#define MODBUS_SLAVE_ID 0x2A
#define REG_TEMPERATURE 0x0000
#define REG_HUMIDITY 0x0001
#define REG_DEWPOINT 0x0002
// Forward declarations for ModbusMaster callbacks
void preTransmission();
void postTransmission();
void setup() {
Serial.begin(9600);
delay(5000);
Serial.println("ModbusMaster: Seeed SKU101990882 starting");
// Wait for the hardware serial to be ready
while (!Serial)
;
Serial.println("Serial done");
pinMode(RS485_EN, OUTPUT);
digitalWrite(RS485_EN, LOW); // Start in RX mode
// Datasheet: 9600 baud, 8N1
RS485Serial.begin(9600, SERIAL_8N1, RS485_RX, RS485_TX);
while (!RS485Serial)
;
Serial.println("RS485 done");
// Tie ModbusMaster to the UART we just configured
node.begin(MODBUS_SLAVE_ID, RS485Serial);
// Register callbacks for half-duplex direction control
node.preTransmission(preTransmission);
node.postTransmission(postTransmission);
}
...
void loop() {
float temperature;
uint16_t humidity;
uint16_t dewPoint;
uint8_t result = node.readInputRegisters(0x0000, 3);
if (result == node.ku8MBSuccess) {
// --- Read Temperature ---
uint16_t rawTemperature = node.getResponseBuffer(REG_TEMPERATURE);
temperature = (int16_t)rawTemperature / 100.0;
// --- Read Humidity ---
humidity = node.getResponseBuffer(REG_HUMIDITY);
humidity = humidity / 100;
// --- Read DewPoint ---
dewPoint = node.getResponseBuffer(REG_DEWPOINT);
dewPoint = dewPoint / 100;
Serial.printf("Temperature: %.1f°C Humidity: %u%%RH Dewpoint: %u°C\n", temperature, humidity, dewPoint);
} else {
Serial.printf("Modbus error: %d\n", result);
}
delay(60000);
}
The Arduino ModbusMaster based application worked first time I forgot to scale the dewpoint.
After hours of fail trying to get nanoMQ TCP bridge running on my Windows11 development system it was time to walk away. I ran nanoMQ with different log levels but “nng_dialer_create failed 9” was the initial error message displayed.
The setup looked good…
bridges.mqtt.MyBridgeDeviceID {
## Azure Event Grid MQTT broker endpoint
server = "tls+mqtt-tcp://xxxx.newzealandnorth-1.ts.eventgrid.azure.net:8883"
proto_ver = 5
clientid = "MyBridgeDeviceID"
username = "MyBridgeDeviceID"
clean_start = true
keepalive = "60s"
## TLS client certificate authentication
ssl = {
# key_password = ""
keyfile = "certificates/MyBridgeDeviceID.key"
certfile = "certificates/MyBridgeDeviceID.crt"
cacertfile = "certificates/xxxx.crt"
}
## ------------------------------------------------------------
## Topic forwarding (NanoMQ → Azure Event Grid)
## ------------------------------------------------------------
## These are the topics your device publishes locally.
## They will be forwarded upstream to Event Grid.
##
forwards = [xxxx]
## ------------------------------------------------------------
## Topic subscription (Azure Event Grid → NanoMQ)
## ------------------------------------------------------------
## This is the topic your device subscribes to from Event Grid.
subscription = [xxxx]
}
Most of my applications have focused on telemetry but I had been thinking about local control for solutions that have to run disconnected. In “real-world” deployments connectivity to Azure EventGrid MQTT Broker isn’t 100% reliable (also delay and jitter issues) which are an issue for control at the edge.
Over Christmas I read an article about the Internet of Vehicles(IoV) which got me thinking about “edge brokers”. In “real-world” deployments connectivity to Azure EventGrid MQTT Broker would not 100% reliable so I have been looking at lightweight edge brokers.
NanoMQ is a Linux Foundation Edge project. It is ultra-lightweight with a footprint of less than 200Kb when deployed with the minimum feature set, is cross platform (POSIX), has MQTT V5 support, bridging, and message persistence. The support for secure deployment is very limited.
Eclipse Mosquitto is another open-source message broker that implements the MQTT versions 5.0, 3.1.1 and 3.1. It is lightweight and is suitable for use on all devices from low power single board computers to full servers.
HiveMQ Edge is an MQTT V5, V3.1.1 and V3.1 compliant, with protocol adapters which connect various industrial communication protocols and integrate several database engines, bidirectional connections to enterprise brokers. For disconnected scenarios messages are stored on disk and published once online. There are opensource(community support) and commercial versions (additional features and support). The open-source version does not include offline persistence.