Azure IoT Hub SAS Tokens revisited

A long time ago I wrote a post about uploading telemetry data to an Azure Event Hub from a Netduino 3 Wifi using HTTPS. To send messages to the EventHub I had to create a valid SAS Token which took a surprising amount of effort because of the reduced text encoding/decoding and cryptographic functionality available in .NET Micro Framework v4.3 (NetMF)

// Create a SAS token for a specified scope. SAS tokens are described in http://msdn.microsoft.com/en-us/library/windowsazure/dn170477.aspx.
private static string CreateSasToken(string uri, string keyName, string key)
{
   // Set token lifetime to 20 minutes. When supplying a device with a token, you might want to use a longer expiration time.
   uint tokenExpirationTime = GetExpiry(20 * 60);
 
   string stringToSign = HttpUtility.UrlEncode(uri) + "\n" + tokenExpirationTime;
 
   var hmac = SHA.computeHMAC_SHA256(Encoding.UTF8.GetBytes(key), Encoding.UTF8.GetBytes(stringToSign));
   string signature = Convert.ToBase64String(hmac);
 
   signature = Base64NetMf42ToRfc4648(signature);
 
   string token = "SharedAccessSignature sr=" + HttpUtility.UrlEncode(uri) + "&sig=" + HttpUtility.UrlEncode(signature) + "&se=" + tokenExpirationTime.ToString() + "&skn=" + keyName;
 
   return token;
}
 
private static string Base64NetMf42ToRfc4648(string base64netMf)
{
   var base64Rfc = string.Empty;
 
   for (var i = 0; i < base64netMf.Length; i++)
   {
      if (base64netMf[i] == '!')
      {
         base64Rfc += '+';
      }
      else if (base64netMf[i] == '*')
      {
         base64Rfc += '/';
      }
      else
      {
         base64Rfc += base64netMf[i];
      }
   }
   return base64Rfc;
}
 
static uint GetExpiry(uint tokenLifetimeInSeconds)
{
   const long ticksPerSecond = 1000000000 / 100; // 1 tick = 100 nano seconds
 
   DateTime origin = new DateTime(1970, 1, 1, 0, 0, 0, 0);
   TimeSpan diff = DateTime.Now.ToUniversalTime() - origin;
 
   return ((uint)(diff.Ticks / ticksPerSecond)) + tokenLifetimeInSeconds;
}

Initially for testing my Azure MQTT Test Client I manually generated the SAS tokens using Azure Device Explorer but figured it would be better if the application generated them.

An initial search lead to this article about how to generate a SAS token for an Azure Event Hub in multiple languages. For my first attempt I “copied and paste” the code sample for C# (I also wasn’t certain what to put in the KeyName parameter) and it didn’t work.

private static string createToken(string resourceUri, string keyName, string key)
{
    TimeSpan sinceEpoch = DateTime.UtcNow - new DateTime(1970, 1, 1);
    var week = 60 * 60 * 24 * 7;
    var expiry = Convert.ToString((int)sinceEpoch.TotalSeconds + week);
    string stringToSign = HttpUtility.UrlEncode(resourceUri) + "\n" + expiry;
    HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key));
    var signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));
    var sasToken = String.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}", HttpUtility.UrlEncode(resourceUri), HttpUtility.UrlEncode(signature), expiry, keyName);
    return sasToken;
}

By comparing the Device Explorer and C# generated SAS keys I worked out the keyName parameter was unnecessary so I removed.

private static string createToken(string resourceUri, string key)
{
    TimeSpan sinceEpoch = DateTime.UtcNow - new DateTime(1970, 1, 1);
    var week = 60 * 60 * 24 * 7;
    var expiry = Convert.ToString((int)sinceEpoch.TotalSeconds + week);
    string stringToSign = HttpUtility.UrlEncode(resourceUri) + "\n" + expiry;
    HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key));
    var signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));
    var sasToken = String.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}", HttpUtility.UrlEncode(resourceUri), HttpUtility.UrlEncode(signature), expiry);
    return sasToken;
}

The shared SAS token now looked closer to what I was expecting but the MQTTNet ConnectAsync was failing with an authentication exception. After looking at the Device Explorer SAS Key code, my .NetMF implementation and the code for the IoT Hub SDK I noticed the encoding for the HMAC Key was different. Encoding.UTF8.GetBytes vs. Convert.FromBase64String.

 private static string createToken(string resourceUri,string key, TimeSpan ttl)
      {
         TimeSpan afterEpoch = DateTime.UtcNow.Add( ttl ) - new DateTime(1970, 1, 1);

         string expiry = afterEpoch.TotalSeconds.ToString("F0");
         string stringToSign = HttpUtility.UrlEncode(resourceUri) + "\n" + expiry;
         HMACSHA256 hmac = new HMACSHA256(Convert.FromBase64String(key));
         string signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));
         return  String.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}", HttpUtility.UrlEncode(resourceUri), HttpUtility.UrlEncode(signature), expiry);
      }

This approach appears to work reliably in my test harness.

MQTTnet client with new SAS Key Generator

User beware DIY Crypto often ends badly

Azure IoT Hub with MQTTnet

As I’m testing my Message Queue Telemetry Transport(MQTT) LoRa gateway I’m building a proof of concept(PoC) .Net core console application for each IoT platform I would like to support.

This PoC was to confirm that my device could connect to the Microsoft Azure IoT Hub MQTT API then format topics and payloads correctly.

Azure IoT Hub MQTT Console Client

I had tried with a couple of different MQTT libraries from micro controllers and embedded devices without success. With the benefit of hindsight (plus this article) I think I had the SAS key format wrong.

The Azure IoT Hub MQTT broker requires only a server name (fully resolved CName), device ID and SAS Key.

   class Program
   {
      private static IMqttClient mqttClient = null;
      private static IMqttClientOptions mqttOptions = null;
      private static string server;
      private static string username;
      private static string password;
      private static string clientId;
      private static string topicD2C;
      private static string topicC2D;

      static void Main(string[] args)
      {
         MqttFactory factory = new MqttFactory();
         mqttClient = factory.CreateMqttClient();

         if (args.Length != 3)
         {
            Console.WriteLine("[AzureIoTHubHostName] [deviceID] [SASKey]");
            Console.WriteLine("Press <enter> to exit");
            Console.ReadLine();
            return;
         }

         server = args[0];
         clientId = args[1];
         sasKey= args[2];

         username = $"{server}/{clientId}/api-version=2018-06-30";
         topicD2C = $"devices/{clientId}/messages/events/";
         topicC2D = $"devices/{clientId}/messages/devicebound/#";

         Console.WriteLine($"MQTT Server:{server} Username:{username} ClientID:{clientId}");

         mqttOptions = new MqttClientOptionsBuilder()
            .WithTcpServer(server, 8883)
            .WithCredentials(username, sasKey)
            .WithClientId(clientId)
            .WithTls()
            .Build();

         mqttClient.UseDisconnectedHandler(new MqttClientDisconnectedHandlerDelegate(e => MqttClient_Disconnected(e)));
         mqttClient.UseApplicationMessageReceivedHandler(new MqttApplicationMessageReceivedHandlerDelegate(e => MqttClient_ApplicationMessageReceived(e)));
         mqttClient.ConnectAsync(mqttOptions).Wait();

         mqttClient.SubscribeAsync(topicC2D, MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce).GetAwaiter().GetResult();

         while (true)
         {
            JObject payloadJObject = new JObject();

            payloadJObject.Add("OfficeTemperature", "22." + DateTime.UtcNow.Millisecond.ToString());
            payloadJObject.Add("OfficeHumidity", (DateTime.UtcNow.Second + 40).ToString());

            string payload = JsonConvert.SerializeObject(payloadJObject);
            Console.WriteLine($"Topic:{topicD2C} Payload:{payload}");

            var message = new MqttApplicationMessageBuilder()
               .WithTopic(topicD2C)
               .WithPayload(payload)
               .WithAtLeastOnceQoS()
            .Build();

            Console.WriteLine("PublishAsync start");
            mqttClient.PublishAsync(message).Wait();
            Console.WriteLine("PublishAsync finish");

            Thread.Sleep(30100);
         }
      }

      private static void MqttClient_ApplicationMessageReceived(MqttApplicationMessageReceivedEventArgs e)
      {
         Console.WriteLine($"ClientId:{e.ClientId} Topic:{e.ApplicationMessage.Topic} Payload:{e.ApplicationMessage.ConvertPayloadToString()}");
      }

      private static async void MqttClient_Disconnected(MqttClientDisconnectedEventArgs e)
      {
         Debug.WriteLine("Disconnected");
         await Task.Delay(TimeSpan.FromSeconds(5));

         try
         {
            await mqttClient.ConnectAsync(mqttOptions);
         }
         catch (Exception ex)
         {
            Debug.WriteLine("Reconnect failed {0}", ex.Message);
         }
      }
   }

Overall the initial configuration went smoothly after I figured out the required Quality of Service (QoS) settings, and the SAS Key format.

Using the approach described in the Microsoft documentation I manually generated the SAS Key.(In my Netduino samples I have code for generating a SAS Key in my HTTPS Azure IoT Hub Client)

Azure Device Explorer Device Management
Azure Device Explorer SAS Key Generator

Once I had the configuration correct I could see telemetry from the device and send it messages.

Azure Device Explorer Data View

In a future post I will upload data to the Azure IoT Central for display. Then explore using a “module” attached to a device which maybe useful for my field gateway.

Thingspeak with MQTTnet

As I’m testing my Message Queue Telemetry Transport(MQTT) LoRa gateway I’m building a proof of concept(PoC) .Net core console application for each IoT platform I would like to support.

This PoC was to confirm that I could connect to the Thingspeak MQTT API then format topics and payloads correctly.

MQTT Console Client

The Thingspeak MQTT broker, username, password, and client ID, Channel, writeAPIKey, ClientID, channelData, are the minimum command line options required. The channelSubscribe, readAPIkey and field are optional.

   class Program
   {
      private static IMqttClient mqttClient = null;
      private static IMqttClientOptions mqttOptions = null;
      private static string server;
      private static string username;
      private static string password;
      private static string writeApiKey;
      private static string clientId;
      private static string channelData;
      private static string channelSubscribe;
      private static string readApiKey;
      private static string field;

      static void Main(string[] args)
      {
         MqttFactory factory = new MqttFactory();
         mqttClient = factory.CreateMqttClient();

         if ((args.Length != 6) && (args.Length != 8) && (args.Length != 9))
         {
            Console.WriteLine("[MQTT Server] [UserName] [Password] [WriteAPIKey] [ClientID] [Channel]");
            Console.WriteLine("[MQTT Server] [UserName] [Password] [WriteAPIKey] [ClientID] [Channel] [channelSubscribe] [ReadApiKey]");
            Console.WriteLine("[MQTT Server] [UserName] [Password] [WriteAPIKey] [ClientID] [Channel] [channelSubscribe] [ReadApiKey] [Field]");
            Console.WriteLine("Press <enter> to exit");
            Console.ReadLine();
            return;
         }

         server = args[0];
         username = args[1];
         password = args[2];
         writeApiKey = args[3];
         clientId = args[4];
         channelData = args[5];

         if (args.Length == 6)
         {
            Console.WriteLine($"MQTT Server:{server} Username:{username} ClientID:{clientId} ChannelData:{channelData}");
         }

         if (args.Length == 8)
         {
            channelSubscribe = args[6];
            readApiKey = args[7];
            Console.WriteLine($"MQTT Server:{server} Username:{username} ClientID:{clientId} ChannelData:{channelData} ChannelSubscribe:{channelSubscribe}");
         }

         if (args.Length == 9)
         {
            channelSubscribe = args[6];
            readApiKey = args[7];
            field = args[8];
            Console.WriteLine($"MQTT Server:{server} Username:{username} ClientID:{clientId} ChannelData:{channelData} ChannelSubscribe:{channelSubscribe} Field:{field}");
         }

         mqttOptions = new MqttClientOptionsBuilder()
            .WithTcpServer(server)
            .WithCredentials(username, password)
            .WithClientId(clientId)
            .WithTls()
            .Build();

         mqttClient.ConnectAsync(mqttOptions).Wait();

         if (args.Length == 8)
         {
            string topic = $"channels/{channelSubscribe}/subscribe/fields/+/{readApiKey}";
            
            Console.WriteLine($"Subscribe Topic:{topic}");

            mqttClient.SubscribeAsync(topic, MQTTnet.Protocol.MqttQualityOfServiceLevel.AtMostOnce).Wait(); 
            mqttClient.UseApplicationMessageReceivedHandler(new MqttApplicationMessageReceivedHandlerDelegate(e => MqttClient_ApplicationMessageReceived(e)));
         }
         if (args.Length == 9)
         {
            string topic = $"channels/{channelSubscribe}/subscribe/fields/{field}/{readApiKey}";

            Console.WriteLine($"Subscribe Topic:{topic}");

            mqttClient.SubscribeAsync(topic, MQTTnet.Protocol.MqttQualityOfServiceLevel.AtMostOnce).Wait();
            mqttClient.UseApplicationMessageReceivedHandler(new MqttApplicationMessageReceivedHandlerDelegate(e => MqttClient_ApplicationMessageReceived(e)));
         }

         mqttClient.UseDisconnectedHandler(new MqttClientDisconnectedHandlerDelegate(e => MqttClient_Disconnected(e)));

         string topicTemperatureData = $"channels/{channelData}/publish/{writeApiKey}";

         Console.WriteLine();

         while (true)
         {
            string value = "field1=22." + DateTime.UtcNow.Millisecond.ToString() + "&field2=60." + DateTime.UtcNow.Millisecond.ToString();
            Console.WriteLine($"Publish Topic {topicTemperatureData}  Value {value}");

            var message = new MqttApplicationMessageBuilder()
               .WithTopic(topicTemperatureData)
               .WithPayload(value)
               .WithQualityOfServiceLevel(MQTTnet.Protocol.MqttQualityOfServiceLevel.AtMostOnce)
//               .WithRetainFlag()
            .Build();

            Console.WriteLine("PublishAsync start");
            mqttClient.PublishAsync(message).Wait();
            Console.WriteLine("PublishAsync finish");
            Console.WriteLine();

            Thread.Sleep(30100);
         }
      }

      private static void MqttClient_ApplicationMessageReceived(MqttApplicationMessageReceivedEventArgs e)
      {
         Console.WriteLine($"ApplicationMessageReceived ClientId:{e.ClientId} Topic:{e.ApplicationMessage.Topic} Qos:{e.ApplicationMessage.QualityOfServiceLevel} Payload:{e.ApplicationMessage.ConvertPayloadToString()}");
         Console.WriteLine();
      }

      private static async void MqttClient_Disconnected(MqttClientDisconnectedEventArgs e)
      {
         Debug.WriteLine("Disconnected");
         await Task.Delay(TimeSpan.FromSeconds(5));

         try
         {
            await mqttClient.ConnectAsync(mqttOptions);
         }
         catch (Exception ex)
         {
            Debug.WriteLine("Reconnect failed {0}", ex.Message);
         }
      }
   }
Channel configuration

The ThingSpeak channel setup has many attributes (link to an external site, link to a GitHub repository, fields for elevation, latitude, longitude etc. ) but only 8 data fields which seems a bit limiting (some of my sensor nodes report more than 8 values).

Dashboard

The Thingspeak dashboard configuration was fairly simple, though the maximum of eight sequentially numbered of inputs (Fields) might require some mapping code.

Overall the initial configuration went smoothly after I figured out the required Quality of Service (QoS) settings, retain flag usage and the different APIKeys on the publish vs. subscribe topics.

I have not explored the advanced analysis enabled by the tight integration with MATLAB which could be quite an advantage for applications requiring that sort of functionality.

ubidots MQTT LoRa Field Gateway

Back in April I started working on an MQTT LoRa Field gateway which was going to support a selection of different Software as a service(SaaS) Internet of Things(IoT) platforms.

After a long pause in development I have a working ubidots client and have 3 proof of concept (PoC) integrations for Adafruit.IO, AskSensors, and Losant. I am also working on Azure IoT Hub, Azure IoT Central. The first iteration is focused on Device to Cloud (D2C) messaging in the next iteration I will add Cloud to Device where viable(C2D).

My applications use a lightweight, easy to implemented protocol which is intended for hobbyist and educational use rather than commercial applications (I have been working on a more secure version as yet another side project)

I have a number of sample Arduino with Dragino LoRa Shield for Arduino, MakerFabs Maduino, Dragino LoRa Mini Dev, M2M Low power Node and Netduino with Elecrow LoRa RFM95 Shield etc. clients. These work with both my platform specific (Adafruit.IO, Azure IoT Central) gateways and protocol specific field gateways.

Ubidots dashboard

When the application is first started it creates a minimal configuration file which should be downloaded, the missing information filled out, then uploaded using the File explorer in the Windows device portal.

{
  "MQTTUserName": "Ubidots generated usname here",
  "MQTTPassword": "NotVerySecure",
  "MQTTClientID": "MQTTLoRaGateway",
  "MQTTServer": "industrial.api.ubidots.com",
  "Address": "LoRaIoT1",
  "Frequency": 915000000.0,
  "MessageHandlerAssembly": "Mqtt.IoTCore.FieldGateway.LoRa.Ubidots",
  "PlatformSpecificConfiguration": ""
}

The application logs debugging information to the Windows 10 IoT Core ETW logging Microsoft-Windows-Diagnostics-LoggingChannel

MQTT LoRa Field Gateway with ubidots plugin generated telemetry
ubidots device management
ubidot managment

The message handler uploads all values in an inbound messages in one MQTT message using the ubidots MQTT message format

async void IMessageHandler.Rfm9XOnReceive(object sender, Rfm9XDevice.OnDataReceivedEventArgs e)
{
	LoggingFields processReceiveLoggingFields = new LoggingFields();
	JObject telemetryDataPoint = new JObject();
	char[] sensorReadingSeparators = { ',' };
	char[] sensorIdAndValueSeparators = { ' ' };

	processReceiveLoggingFields.AddString("PacketSNR", e.PacketSnr.ToString("F1"));
	processReceiveLoggingFields.AddInt32("PacketRSSI", e.PacketRssi);
	processReceiveLoggingFields.AddInt32("RSSI", e.Rssi);

	string addressBcdText = BitConverter.ToString(e.Address);
	processReceiveLoggingFields.AddInt32("DeviceAddressLength", e.Address.Length);
	processReceiveLoggingFields.AddString("DeviceAddressBCD", addressBcdText);

	string messageText;
	try
	{
		messageText = UTF8Encoding.UTF8.GetString(e.Data);
		processReceiveLoggingFields.AddString("MessageText", messageText);
	}
	catch (Exception ex)
	{
		processReceiveLoggingFields.AddString("Exception", ex.ToString());
		this.Logging.LogEvent("PayloadProcess failure converting payload to text", processReceiveLoggingFields, LoggingLevel.Warning);
		return;
	}

	// Chop up the CSV text
	string[] sensorReadings = messageText.Split(sensorReadingSeparators, StringSplitOptions.RemoveEmptyEntries);
	if (sensorReadings.Length < 1)
	{
		this.Logging.LogEvent("PayloadProcess payload contains no sensor readings", processReceiveLoggingFields, LoggingLevel.Warning);
		return;
	}

	// Chop up each sensor read into an ID & value
	foreach (string sensorReading in sensorReadings)
	{
		string[] sensorIdAndValue = sensorReading.Split(sensorIdAndValueSeparators, StringSplitOptions.RemoveEmptyEntries);
		// Check that there is an id & value
		if (sensorIdAndValue.Length != 2)
		{
			this.Logging.LogEvent("PayloadProcess payload invalid format", processReceiveLoggingFields, LoggingLevel.Warning);
			return;
		}

		string sensorId = sensorIdAndValue[0];
		string value = sensorIdAndValue[1];

		telemetryDataPoint.Add(addressBcdText + sensorId, Convert.ToDouble(value));
	}
	processReceiveLoggingFields.AddString("MQTTClientId", MqttClient.Options.ClientId);

	string stateTopic = string.Format(stateTopicFormat, MqttClient.Options.ClientId);

	try
	{
		var message = new MqttApplicationMessageBuilder()
			.WithTopic(stateTopic)
			.WithPayload(JsonConvert.SerializeObject(telemetryDataPoint))
			.WithAtLeastOnceQoS()
			.Build();
		Debug.WriteLine(" {0:HH:mm:ss} MQTT Client PublishAsync start", DateTime.UtcNow);
		await MqttClient.PublishAsync(message);
		Debug.WriteLine(" {0:HH:mm:ss} MQTT Client PublishAsync finish", DateTime.UtcNow);

		this.Logging.LogEvent("PublishAsync Ubidots payload", processReceiveLoggingFields, LoggingLevel.Information);
	}
	catch (Exception ex)
	{
		processReceiveLoggingFields.AddString("Exception", ex.ToString());
		this.Logging.LogEvent("PublishAsync Ubidots payload", processReceiveLoggingFields, LoggingLevel.Error);
	}
}

The “automagic” provisioning of feeds does make setting up small scale systems easier, though I’m not certain how well it would scale.

Some of the fields weren’t obviously editable e.g.”ÄPI Label” in device configuration which I only discovered by clicking on them..

The limitations of the free account meant I couldn’t evaluate ubidots in much depth but what was available appeared to be robust and reliable (Nov 2019).

Adafruit.IO MQTT LoRa Field Gateway

Back in April I started working on an MQTT LoRa Field gateway which was going to support a selection of different Software as a service(SaaS) Internet of Things(IoT) platforms.

After a long pause in development I have a working AdaFruit.IO client and have 3 proof of concept (PoC) integrations for AskSensors, Losant and Ubidots. I am also working on Azure IoT Hub, Azure IoT Central clients. The first iteration is focused on Device to Cloud (D2C) messaging in the next iteration I will add Cloud to Device where viable(C2D).

My applications use a lightweight, easy to implemented protocol which is intended for hobbyist and educational use rather than commercial applications (I have been working on a more secure version as yet another side project)

I have a number of sample Arduino with Dragino LoRa Shield for Arduino, MakerFabs Maduino, Dragino LoRa Mini Dev, M2M Low power Node and Netduino with Elecrow LoRa RFM95 Shield etc. clients. These work with both my platform specific (Adafruit.IO, Azure IoT Central) gateways and protocol specific field gateways.

Maduino client dashboard

When the application is first started it creates a minimal configuration file which should be downloaded, the missing information filled out, then uploaded using the File explorer in the Windows device portal.

{
  "MQTTUserName": "AdaFruitIO user",
  "MQTTPassword": "AIO Key",
  "MQTTClientID": "MQTTLoRaGateway",
  "MQTTServer": "io.adafruit.com",
  "Address": "LoRaIoT1",
  "Frequency": 915000000.0,
  "MessageHandlerAssembly": "Mqtt.IoTCore.FieldGateway.LoRa.Adafruit",
  "PlatformSpecificConfiguration": "mqttloragateway"
}

The application logs debugging information to the Windows 10 IoT Core ETW logging Microsoft-Windows-Diagnostics-LoggingChannel

MQTT LoRa Gateway with Adafruit.IO plug-in

The SaaS platform specific interface has gained an additional parameter for platform specific configuration.

namespace devMobile.Mqtt.IoTCore.FieldGateway
{
	using System;
	using Windows.Foundation.Diagnostics;

	using devMobile.IoT.Rfm9x;
	using MQTTnet;
	using MQTTnet.Client;

	public interface IMessageHandler
	{
		void Initialise(LoggingChannel logging, IMqttClient mqttClient, Rfm9XDevice rfm9XDevice,string platformSpecificConfiguration);

		void Rfm9XOnReceive(object sender, Rfm9XDevice.OnDataReceivedEventArgs e);

		void MqttApplicationMessageReceived(object sender, MqttApplicationMessageReceivedEventArgs e);

		void Rfm9xOnTransmit(object sender, Rfm9XDevice.OnDataTransmitedEventArgs e);
	}
}

This is used for the AdaFruit.IO GroupName so Adafruit.IO feed values are not all in a single group.

public class MessageHandler : IMessageHandler
	{
		private LoggingChannel Logging { get; set; }
		private IMqttClient MqttClient { get; set; }
		private Rfm9XDevice Rfm9XDevice { get; set; }
      private string PlatformSpecificConfiguration { get; set; }


      void IMessageHandler.Initialise(LoggingChannel logging, IMqttClient mqttClient, Rfm9XDevice rfm9XDevice, string platformSpecificConfiguration)
		{
			LoggingFields processInitialiseLoggingFields = new LoggingFields();

			this.Logging = logging;
			this.MqttClient = mqttClient;
			this.Rfm9XDevice = rfm9XDevice;
			this.PlatformSpecificConfiguration = platformSpecificConfiguration;
		}

		async void IMessageHandler.Rfm9XOnReceive(object sender, Rfm9XDevice.OnDataReceivedEventArgs e)
		{
			LoggingFields processReceiveLoggingFields = new LoggingFields();

			processReceiveLoggingFields.AddString("PacketSNR", e.PacketSnr.ToString("F1"));
			processReceiveLoggingFields.AddInt32("PacketRSSI", e.PacketRssi);
			processReceiveLoggingFields.AddInt32("RSSI", e.Rssi);

			string addressBcdText = BitConverter.ToString(e.Address);
			processReceiveLoggingFields.AddInt32("DeviceAddressLength", e.Address.Length);
			processReceiveLoggingFields.AddString("DeviceAddressBCD", addressBcdText);

			string payloadBcdText = BitConverter.ToString(e.Data);
			processReceiveLoggingFields.AddInt32("PayloadLength", e.Data.Length);
			processReceiveLoggingFields.AddString("DeviceAddressBCD", payloadBcdText);

			this.Logging.LogEvent("Rfm9XOnReceive", processReceiveLoggingFields, LoggingLevel.Information);
		}

		void IMessageHandler.MqttApplicationMessageReceived(object sender, MqttApplicationMessageReceivedEventArgs e)
		{
			LoggingFields processReceiveLoggingFields = new LoggingFields();

			processReceiveLoggingFields.AddString("ClientId", e.ClientId);
#if DEBUG
			processReceiveLoggingFields.AddString("Payload", e.ApplicationMessage.ConvertPayloadToString());
#endif
			processReceiveLoggingFields.AddString("QualityOfServiceLevel", e.ApplicationMessage.QualityOfServiceLevel.ToString());
			processReceiveLoggingFields.AddBoolean("Retain", e.ApplicationMessage.Retain);
			processReceiveLoggingFields.AddString("Topic", e.ApplicationMessage.Topic);

			this.Logging.LogEvent("MqttApplicationMessageReceived topic not processed", processReceiveLoggingFields, LoggingLevel.Error);
		}

		void IMessageHandler.Rfm9xOnTransmit(object sender, Rfm9XDevice.OnDataTransmitedEventArgs e)
		{
			this.Logging.LogMessage("Rfm9xOnTransmit", LoggingLevel.Information);
		}
	}
Adafruit.IO Group for a single field gateway
Group Setup

The message handler uploads all values in an inbound messages in one MQTT message using the AdaFruit.IO Group Feed format.

      async void IMessageHandler.Rfm9XOnReceive(object sender, Rfm9XDevice.OnDataReceivedEventArgs e)
      {
         LoggingFields processReceiveLoggingFields = new LoggingFields();
         char[] sensorReadingSeparators = { ',' };
         char[] sensorIdAndValueSeparators = { ' ' };

         processReceiveLoggingFields.AddString("PacketSNR", e.PacketSnr.ToString("F1"));
         processReceiveLoggingFields.AddInt32("PacketRSSI", e.PacketRssi);
         processReceiveLoggingFields.AddInt32("RSSI", e.Rssi);

         string addressBcdText = BitConverter.ToString(e.Address);
         processReceiveLoggingFields.AddInt32("DeviceAddressLength", e.Address.Length);
         processReceiveLoggingFields.AddString("DeviceAddressBCD", addressBcdText);

         string messageText;
         try
         {
            messageText = UTF8Encoding.UTF8.GetString(e.Data);
            processReceiveLoggingFields.AddString("MessageText", messageText);
         }
         catch (Exception ex)
         {
            processReceiveLoggingFields.AddString("Exception", ex.ToString());
            this.Logging.LogEvent("PayloadProcess failure converting payload to text", processReceiveLoggingFields, LoggingLevel.Warning);
            return;
         }

         // Chop up the CSV text
         string[] sensorReadings = messageText.Split(sensorReadingSeparators, StringSplitOptions.RemoveEmptyEntries);
         if (sensorReadings.Length < 1)
         {
            this.Logging.LogEvent("PayloadProcess payload contains no sensor readings", processReceiveLoggingFields, LoggingLevel.Warning);
            return;
         }

         JObject payloadJObject = new JObject();

         JObject feeds = new JObject();

         // Chop up each sensor read into an ID & value
         foreach (string sensorReading in sensorReadings)
         {
            string[] sensorIdAndValue = sensorReading.Split(sensorIdAndValueSeparators, StringSplitOptions.RemoveEmptyEntries);

            // Check that there is an id & value
            if (sensorIdAndValue.Length != 2)
            {
               this.Logging.LogEvent("PayloadProcess payload invalid format", processReceiveLoggingFields, LoggingLevel.Warning);
               return;
            }

            string sensorId = string.Concat(addressBcdText, sensorIdAndValue[0]);
            string value = sensorIdAndValue[1];

            feeds.Add(sensorId.ToLower(), value);
         }
         payloadJObject.Add("feeds", feeds);

         string topic = $"{MqttClient.Options.Credentials.Username}/groups/{PlatformSpecificConfiguration}";

         try
         {
            var message = new MqttApplicationMessageBuilder()
               .WithTopic(topic)
               .WithPayload(JsonConvert.SerializeObject(payloadJObject))
               .WithAtLeastOnceQoS()
               .Build();
            Debug.WriteLine(" {0:HH:mm:ss} MQTT Client PublishAsync start", DateTime.UtcNow);
            await MqttClient.PublishAsync(message);
            Debug.WriteLine(" {0:HH:mm:ss} MQTT Client PublishAsync finish", DateTime.UtcNow);

            this.Logging.LogEvent("PublishAsync Adafruit payload", processReceiveLoggingFields, LoggingLevel.Information);
         }
         catch (Exception ex)
         {
            processReceiveLoggingFields.AddString("Exception", ex.ToString());
            this.Logging.LogEvent("PublishAsync Adafruit payload", processReceiveLoggingFields, LoggingLevel.Error);
         }
      }

The casing of User names (Must match exactly) and Group/Feed names (must be lower case) tripped me up yet again. The “automagic” provisioning of feeds does make setting up small scale systems easier, though I’m not certain how well it would scale.

Adafruit MQTT Cloud to Device Messaging

After getting MQ Telemetry Transport (MQTT) Device to Cloud (D2C) messaging working for AdaFruit.IO I have also got Cloud to Device (C2D) messaging working as well.

The MQTT broker, username, API key, client ID, optional group name (to keep MQTT aligned with REST API terminology), command topic and feed name are command line options.

The Adafruit IO MQTT documentation suggests an approach for naming topics which allows a bit more structure for feed (D2C and C2D) names than the REST API (which only does D2C).

class Program
{
	private static IMqttClient mqttClient = null;
	private static IMqttClientOptions mqttOptions = null;
	private static string server;
	private static string username;
	private static string password;
	private static string clientId;
	private static string commandTopic;
	private static string groupname;
	private static string feedname;

	static void Main(string[] args)
	{
		MqttFactory factory = new MqttFactory();
		mqttClient = factory.CreateMqttClient();

		if ((args.Length != 6) && (args.Length != 7))
		{
			Console.WriteLine("[MQTT Server] [UserName] [Password] [ClientID] [CommandTopic] [GroupName] [FeedName]");
			Console.WriteLine("[MQTT Server] [UserName] [Password] [ClientID] [CommandTopic] [FeedName]");
			Console.WriteLine("Press <enter> to exit");
			Console.ReadLine();
			return;
		}

		server = args[0];
		username = args[1];
		password = args[2];
		clientId = args[3];
		commandTopic = args[4];
		if (args.Length == 6)
		{
			feedname = args[5].ToLower();
			Console.WriteLine($"MQTT Server:{server} Username:{username} ClientID:{clientId} CommandTopic:{commandTopic} Feedname:{feedname}");
		}

		if (args.Length == 7)
		{
			groupname = args[5].ToLower();
			feedname = args[6].ToLower();
			Console.WriteLine($"MQTT Server:{server} Username:{username} ClientID:{clientId} CommandTopic:{commandTopic} Groupname:{groupname} Feedname:{feedname}");
		}

		mqttOptions = new MqttClientOptionsBuilder()
			.WithTcpServer(server)
			.WithCredentials(username, password)
			.WithClientId(clientId)
			.WithTls()
			.Build();

		mqttClient.Disconnected += MqttClient_Disconnected;
		mqttClient.ConnectAsync(mqttOptions).Wait();
		mqttClient.ApplicationMessageReceived += MqttClient_ApplicationMessageReceived;

		// Adafruit.IO format for topics which are called feeds
		string topic = string.Empty;

		if (args.Length == 6)
		{
			topic = $"{username}/feeds/{feedname}";				
		}

		if (args.Length == 7)
		{
			topic = $"{username}/feeds/{groupname}.{feedname}";
		}

		mqttClient.SubscribeAsync(commandTopic, MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce).GetAwaiter().GetResult();

		while (true)
		{
			string value = "22." + DateTime.UtcNow.Millisecond.ToString();
			Console.WriteLine($"Topic:{topic} Value:{value}");

			var message = new MqttApplicationMessageBuilder()
				.WithTopic(topic)
				.WithPayload(value)
				.WithQualityOfServiceLevel(MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce)
			.WithRetainFlag()
			.Build();

			Console.WriteLine("PublishAsync start");
			mqttClient.PublishAsync(message).Wait();
			Console.WriteLine("PublishAsync finish");

			Thread.Sleep(30100);
		}
	}

	private static void MqttClient_ApplicationMessageReceived(object sender, MqttApplicationMessageReceivedEventArgs e)
	{
		Console.WriteLine($"ClientId:{e.ClientId} Topic:{e.ApplicationMessage.Topic} Payload:{e.ApplicationMessage.ConvertPayloadToString()}");
	}

	private static async void MqttClient_Disconnected(object sender, MqttClientDisconnectedEventArgs e)
	{
		Debug.WriteLine("Disconnected");
		await Task.Delay(TimeSpan.FromSeconds(5));

		try
		{
			await mqttClient.ConnectAsync(mqttOptions);
		}
		catch (Exception ex)
		{
			Debug.WriteLine("Reconnect failed {0}", ex.Message);
		}
	}
}

I configured a slider on the dashboard for my home called “setpoint” (yet again I was tripped up “automatically” camel casing the name because I’m a C# developer) which my MQTT client subscribed to.

AdaFruit.IO Home monitoring dashboard
setpoint feed configuration

After figuring out the format of the command topic I found that when the slider was moved the MQTT client subscription event fired reliably.

AdaFruit Client showing the setpoint value change notifications

Overall the process went pretty well, though the manual configuration of the subscriptions to AdaFruit.IO feeds could become a bit of a problem at scale.

Ask Sensors with MQTTnet

After a 6 month pause I’m back working on my Message Queue Telemetry Transport(MQTT) LoRa gateway.

As I’m testing my Message Queue Telemetry Transport(MQTT) LoRa gateway I’m building a proof of concept(PoC) .Net core console application for each IoT platform I would like to support.

This PoC was to confirm that I could connect to the Ask Sensors MQTT API then format topics and payloads correctly.

Console test application

The MQTT broker, username, password, and client ID are command line options.

class Program
{
	private static IMqttClient mqttClient = null;
	private static IMqttClientOptions mqttOptions = null;
	private static string server;
	private static string username;
	private static string apiKey;
	private static string clientID;

	static void Main(string[] args)
	{
		MqttFactory factory = new MqttFactory();
		mqttClient = factory.CreateMqttClient();
		bool heatPumpOn = false;

		if (args.Length != 4)
		{
			Console.WriteLine("[MQTT Server] [UserName] [APIKey] [ClientID]");
			Console.WriteLine("Press <enter> to exit");
			Console.ReadLine();
			return;
		}

		server = args[0];
		username = args[1];
		apiKey = args[2];
		clientID = args[3];

		Console.WriteLine($"MQTT Server:{server} Username:{username} ClientID:{clientID}");

		mqttOptions = new MqttClientOptionsBuilder()
			.WithTcpServer(server)
			.WithCredentials(username, "")
			.WithClientId(clientID)
			//.WithTls() // This is a bit of a worry
			.Build();

		mqttClient.ApplicationMessageReceived += MqttClient_ApplicationMessageReceived;
		mqttClient.Disconnected += MqttClient_Disconnected;
		mqttClient.ConnectAsync(mqttOptions).Wait();

		// AskSensors formatted client state update topic
		string stateTopic = $"{username}/{apiKey}";

		while (true)
		{
			string payloadText;
			double temperature = 22.0 + (DateTime.UtcNow.Millisecond / 1000.0);
			double humidity = 50 + (DateTime.UtcNow.Millisecond / 100.0);
			double speed = 10 + (DateTime.UtcNow.Millisecond / 100.0);
			Console.WriteLine($"Topic:{stateTopic} Temperature:{temperature:0.00} Humidity:{humidity:0} HeatPumpOn:{heatPumpOn}");

			// First JSON attempt didn't work
			payloadText = @"{""Humidity"":55}";

            // Second attempt worked
            payloadText = $"module1=22";

            // Third attempt with "real" values injected
            payloadText = $"module1={temperature}&m2={humidity}";

            var message = new MqttApplicationMessageBuilder()
					.WithTopic(stateTopic)
					.WithPayload(payloadText)
					.WithQualityOfServiceLevel(global::MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce)
				   .WithExactlyOnceQoS()
				   //.WithAtLeastOnceQoS()
				   //.WithRetainFlag()
				   .Build();

				Console.WriteLine("PublishAsync start");
				mqttClient.PublishAsync(message).Wait();
				Console.WriteLine("PublishAsync finish");

				Thread.Sleep(30100);
			}
		}

	private static async void MqttClient_Disconnected(object sender, MqttClientDisconnectedEventArgs e)
	{
		Debug.WriteLine("Disconnected");
		await Task.Delay(TimeSpan.FromSeconds(5));

		try
		{
			await mqttClient.ConnectAsync(mqttOptions);
		}
		catch (Exception ex)
		{
			Debug.WriteLine("Reconnect failed {0}", ex.Message);
		}
	}

The Ask Sensors screen designer has 8 different types of Graph (line, bar, scatter, gauge, table, binary. digital, map)

Ask sensors dashboard configuration was fairly simple, though sequential numbering of inputs (modules) might require some mapping code.

Overall the initial configuration went smoothly after I figured out the payload format (not JSON), though the functionality (of a free subscription) did appear to be quite limited.

Since I first started building my MQTT gateway there have been several breaking updates to the MQTTNet API which so I will have to refresh all the applications in my solution.

Re-reading the SX1231 datasheet

While trying to debug a deadlock in my RFM69 library I noticed in the logging that I was getting a receive interrupt while putting bytes in the FIFO for transmission.

Register 0x49 - Value 0X00 - Bits 00000000
Register 0x4a - Value 0X00 - Bits 00000000
Register 0x4b - Value 0X00 - Bits 00000000
Register 0x4c - Value 0X00 - Bits 00000000
Register 0x4d - Value 0X00 - Bits 00000000
...
22:58:47.192 Received To 0X22 a 33 byte message hello world RFM69-915-02 10-58-47 CRC Ok True
The thread 0x6a8 has exited with code 0 (0x0).
22:58:48.334 Send-hello world RFM69-915-01 10-58-48
22:58:48.351 Send-Done
22:58:48.388 Received To 0X22 a 33 byte message hello world RFM69-915-02 10-58-52 CRC Ok True
22:58:48.462 Transmit-Done
The thread 0xde4 has exited with code 0 (0x0).
22:58:53.427 Send-hello world RFM69-915-01 10-58-53
22:58:53.445 Send-Done
22:58:53.556 Transmit-Done
22:58:57.382 Received To 0X22 a 33 byte message hello world RFM69-915-02 10-58-57 CRC Ok True
The thread 0x17c has exited with code 0 (0x0).

After re-reading the RFM69CW/RFM69HCW module datasheet (based on the Semtech SX1231/SX1231H) I realised my code for loading the FIFO could be more efficient.

SX1231 Register map FIFO

Based on the timing diagram I could remove the loop which loads bytes in the FIFO for transmission.

The changes in my RFM69 library weren’t huge but I think they are also applicable to the RFM9X LoRa library as well

public void SendMessage(byte address, byte[] messageBytes)
{
#region Guard conditions
#endregion

	lock (Rfm9XRegFifoLock)
	{
		SetMode(RegOpModeMode.StandBy);

		if (PacketFormat == RegPacketConfig1PacketFormat.VariableLength)
		{
			RegisterManager.WriteByte((byte)Registers.RegFifo, (byte)(messageBytes.Length + 1)); // Additional byte for address 
		}

		RegisterManager.WriteByte((byte)Registers.RegFifo, address);

		foreach (byte b in messageBytes)
		{
				this.RegisterManager.WriteByte((byte)Registers.RegFifo, b);
		}

		SetMode(RegOpModeMode.Transmit);
	}
}

After modifications

public void SendMessage(byte address, byte[] messageBytes)
{
#region Guard conditions
...
#endregion

	SetDioPinMapping(dio0Mapping: Dio0Mapping.TransmitPacketSent);

	lock (Rfm9XRegFifoLock)
	{
		SetMode(RegOpModeMode.StandBy);

		if (PacketFormat == RegPacketConfig1PacketFormat.VariableLength)
		{
			RegisterManager.WriteByte((byte)Registers.RegFifo, (byte)(messageBytes.Length + 1)); // Additional byte for address 
		}

		RegisterManager.WriteByte((byte)Registers.RegFifo, address);

		this.RegisterManager.Write((byte)Registers.RegFifo, messageBytes);
	}
	SetMode(RegOpModeMode.Transmit);
}

After stress testing with several client devices this appears to have reduced the scope of a receive interrupt to occur while a packet was being loaded into the FIFO for transmission. I now need to dig deeper into the timing around entering different operational modes and selectively enabling event interrupts.

iwanders/plainRFM69 revisited

After problems with interleaved interrupt handling in my Windows 10 IoT Core client I figured the AutoMode used by the plainRFM69 library might be worth investigation. My first Arduino client was based on the plainRFM69 library but had Interoperability issues.

For this attempt I also started with the minimal sample and modified the code to send and receive text messages.

/*
    Copyright (c) 2014, Ivor Wanders, Bryn Lewis 2019
    MIT License, see the LICENSE.md file in the root folder.
*/

#include <SPI.h>
#include <plainRFM69.h>

// slave select pin.
#define SLAVE_SELECT_PIN 10

// connected to the reset pin of the RFM69.
#define RESET_PIN 9

// tie this pin down on the receiver.
#define SENDER_DETECT_PIN A0

const uint8_t tx_buffer[] = "ABCDEFGHIJKLMNOPQRSTURWXYZ1234567890";
//const uint8_t tx_buffer[] = "abcdefghijklmnopqrstuvwxyz1234567890";
uint8_t rx_buffer[sizeof(tx_buffer)] = "";

plainRFM69 rfm = plainRFM69(SLAVE_SELECT_PIN);


void sender() {

  uint32_t start_time = millis();

  uint32_t counter = 1; // the counter which we are going to send.

  while (true) {
    rfm.poll(); // run poll as often as possible.

    if (!rfm.canSend()) {
      continue; // sending is not possible, already sending.
    }

    if ((millis() - start_time) > 1000) { // every 500 ms.
      start_time = millis();

      // be a little bit verbose.
      Serial.print("Send:"); Serial.println(counter);

      // send the number of bytes equal to that set with setPacketLength.
      // read those bytes from memory where counter starts.
      rfm.sendVariable(tx_buffer, counter);

      counter++; // increase the counter.

      if ( counter > strlen(tx_buffer))
      {
        counter = 1;
      }
    }
  }
}

void receiver() {
  uint32_t counter = 0; // to count the messages.

  while (true) {

    rfm.poll(); // poll as often as possible.

    while (rfm.available())
    {
      uint8_t len = rfm.read(rx_buffer); // read the packet into the new_counter.

      // print verbose output.
      Serial.print("Packet Len:");
      Serial.print( len );
      Serial.print(" : ");
      Serial.println((char*)rx_buffer);
    }
  }
}

void setup() {
  Serial.begin(9600);
  SPI.begin();

  bareRFM69::reset(RESET_PIN); // sent the RFM69 a hard-reset.

  //rfm.setRecommended(); // set recommended paramters in RFM69.
  rfm.setPacketType(true, false); // set the used packet type.

  rfm.setBufferSize(2);   // set the internal buffer size.
  rfm.setPacketLength(sizeof(rx_buffer)); // set the packet length.

  rfm.setFrequency((uint32_t)909560000); // set the frequency.

  rfm.setLNA(RFM69_LNA_IMP_200OHM, RFM69_LNA_GAIN_AGC_LOOP);

  // p71, 3 preamble bytes.
  rfm.setPreambleSize(16);

  // p71, 4 bytes sync of 0x01, only start listening when sync is matched.
  //uint8_t syncthing[] = {0xaa, 0x2d, 0xd4};
  uint8_t syncthing[] = {0xd4, 0x2d, 0xaa};
  rfm.setSyncConfig(true, false, sizeof(syncthing), 0);
  rfm.setSyncValue(&syncthing, sizeof(syncthing));

  rfm.dumpRegisters(Serial);

  // baudrate is default, 4800 bps now.

  rfm.receive();
  // set it to receiving mode.

  pinMode(SENDER_DETECT_PIN, INPUT_PULLUP);
  delay(5);
}

void loop() {
  if (digitalRead(SENDER_DETECT_PIN) == LOW) {
    Serial.println("Going Receiver!");
    receiver();
    // this function never returns and contains an infinite loop.
  } else {
    Serial.println("Going sender!");
    sender();
    // idem.
  }
}

I took the list register values and loaded them into a Excel spreadsheet alongside the values from my Windows 10 IoT Core application

17:35:03.044 -> 0x0: 0x0
17:35:03.078 -> 0x1: 0x4
17:35:03.078 -> 0x2: 0x0
17:35:03.078 -> 0x3: 0x1A
17:35:03.112 -> 0x4: 0xB
17:35:03.112 -> 0x5: 0x0
17:35:03.112 -> 0x6: 0x52
17:35:03.146 -> 0x7: 0xE3
17:35:03.146 -> 0x8: 0x63
17:35:03.146 -> 0x9: 0xD7
17:35:03.180 -> 0xA: 0x41
17:35:03.180 -> 0xB: 0x40
17:35:03.180 -> 0xC: 0x2
17:35:03.215 -> 0xD: 0x92
17:35:03.215 -> 0xE: 0xF5
17:35:03.249 -> 0xF: 0x20
17:35:03.249 -> 0x10: 0x24
17:35:03.249 -> 0x11: 0x9F
17:35:03.282 -> 0x12: 0x9
17:35:03.282 -> 0x13: 0x1A
17:35:03.282 -> 0x14: 0x40
17:35:03.317 -> 0x15: 0xB0
17:35:03.317 -> 0x16: 0x7B
17:35:03.317 -> 0x17: 0x9B
17:35:03.317 -> 0x18: 0x88
17:35:03.351 -> 0x19: 0x86
17:35:03.351 -> 0x1A: 0x8A
17:35:03.384 -> 0x1B: 0x40
17:35:03.384 -> 0x1C: 0x80
17:35:03.384 -> 0x1D: 0x6
17:35:03.418 -> 0x1E: 0x10
17:35:03.418 -> 0x1F: 0x0
17:35:03.452 -> 0x20: 0x0
17:35:03.452 -> 0x21: 0x0
17:35:03.452 -> 0x22: 0x0
17:35:03.487 -> 0x23: 0x2
17:35:03.487 -> 0x24: 0xFF
17:35:03.487 -> 0x25: 0x0
17:35:03.521 -> 0x26: 0x5
17:35:03.521 -> 0x27: 0x80
17:35:03.521 -> 0x28: 0x0
17:35:03.556 -> 0x29: 0xFF
17:35:03.556 -> 0x2A: 0x0
17:35:03.556 -> 0x2B: 0x0
17:35:03.556 -> 0x2C: 0x0
17:35:03.590 -> 0x2D: 0x10
17:35:03.590 -> 0x2E: 0x90
17:35:03.624 -> 0x2F: 0xAA
17:35:03.624 -> 0x30: 0x2D
17:35:03.624 -> 0x31: 0xD4
17:35:03.659 -> 0x32: 0x0
17:35:03.659 -> 0x33: 0x0
17:35:03.659 -> 0x34: 0x0
17:35:03.693 -> 0x35: 0x0
17:35:03.693 -> 0x36: 0x0
17:35:03.728 -> 0x37: 0xD0
17:35:03.728 -> 0x38: 0x25
17:35:03.728 -> 0x39: 0x0
17:35:03.761 -> 0x3A: 0x0
17:35:03.761 -> 0x3B: 0x0
17:35:03.761 -> 0x3C: 0x1
17:35:03.795 -> 0x3D: 0x0
17:35:03.795 -> Going sender!
17:35:04.725 -> Send:1

Arduino RFM69HCW Client in receive mode

First thing I noticed was the order of the three sync byes (Registers 0x2F, 0x30, 0x31) was reversed. I then modified the run method in the Windows 10 code so the registers settings on both devices matched. (I removed the PlainRFM69 SetRecommended call so as many of the default options as possible were used).

public void Run(IBackgroundTaskInstance taskInstance)
{
	byte[] syncValues = { 0xAA, 0x2D, 0xD4 };
	byte[] aesKeyValues = { 0x0, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0X0E, 0X0F };

	try
	{
		rfm69Device.Initialise(Rfm69HcwDevice.RegOpModeMode.StandBy
										,frequency: 909560000.0 
										,dio0Mapping: Rfm69HcwDevice.Dio0Mapping.ReceiveCrcOk
										,preambleSize: 16												
										,syncValues: syncValues
										,packetFormat: Rfm69HcwDevice.RegPacketConfig1PacketFormat.VariableLength
										,packetDcFree: Rfm69HcwDevice.RegPacketConfig1DcFree.Whitening
										,autoRestartRx: false
										//,addressNode: 0x22
										//,addressbroadcast: 0x99
										//,aesKey: aesKeyValues
										);

		rfm69Device.OnReceive += Rfm69Device_OnReceive;
		rfm69Device.OnTransmit += Rfm69Device_OnTransmit;

		rfm69Device.RegisterDump();
		rfm69Device.SetMode(Rfm69HcwDevice.RegOpModeMode.Receive);


		while (true)
		{
			if (true)
			{
				string message = $"hello world {Environment.MachineName} {DateTime.Now:hh-mm-ss}";

				byte[] messageBuffer = UTF8Encoding.UTF8.GetBytes(message);

				Debug.WriteLine("{0:HH:mm:ss.fff} Send-{1}", DateTime.Now, message);
				//rfm69Device.SendMessage( 0x11, messageBuffer);
				rfm69Device.SendMessage(messageBuffer);

				Debug.WriteLine("{0:HH:mm:ss.fff} Send-Done", DateTime.Now);

				Task.Delay(5000).Wait();
			}
			else
			{
				Debug.Write(".");
				Task.Delay(1000).Wait();
			}
		}
	}
	catch (Exception ex)
	{
		Debug.WriteLine(ex.Message);
	}
}

I also found an error with the declaration of the RegPacketConfig1DcFree enumeration (Whitening = 0b0100000 vs. Whitening = 0b01000000) which wouldn’t have helped.

public enum RegPacketConfig1DcFree : byte
{
	None = 0b00000000,
	Manchester = 0b00100000,
	Whitening = 0b01000000,
	Reserved = 0b01100000,
}
const RegPacketConfig1DcFree RegPacketConfig1DcFreeDefault = RegPacketConfig1DcFree.None;

I could then reliably sent messages to and receive messages from my Arduino Nano Radio Shield RFM69/95 device

Register 0x4c - Value 0X00 - Bits 00000000
Register 0x4d - Value 0X00 - Bits 00000000
...
17:55:53.559 Received 1 byte message A CRC Ok True
.17:55:54.441 Received 2 byte message AB CRC Ok True
.17:55:55.444 Received 3 byte message ABC CRC Ok True
.17:55:56.447 Received 4 byte message ABCD CRC Ok True
.17:55:57.449 Received 5 byte message ABCDE CRC Ok True
.17:55:58.453 Received 6 byte message ABCDEF CRC Ok True
The thread 0x578 has exited with code 0 (0x0).
.17:55:59.622 Received 7 byte message ABCDEFG CRC Ok True
.17:56:00.457 Received 8 byte message ABCDEFGH CRC Ok True
.17:56:01.460 Received 9 byte message ABCDEFGHI CRC Ok True
.17:56:02.463 Received 10 byte message ABCDEFGHIJ CRC Ok True
..17:56:03.955 Received 11 byte message ABCDEFGHIJK CRC Ok True
17:56:04.583 Received 12 byte message ABCDEFGHIJKL CRC Ok True

I did some investigation into that the plainRMF69 code and found the ReadMultiple and WriteMuliple methods reverse the byte order

void bareRFM69::writeMultiple(uint8_t reg, void* data, uint8_t len){
    SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0));  // gain control of SPI bus
    this->chipSelect(true); // assert chip select
    SPI.transfer(RFM69_WRITE_REG_MASK | (reg & RFM69_READ_REG_MASK)); 
    uint8_t* r = reinterpret_cast<uint8_t*>(data);
    for (uint8_t i=0; i < len ; i++){
        SPI.transfer(r[len - i - 1]);
    }
    this->chipSelect(false);// deassert chip select
    SPI.endTransaction();    // release the SPI bus
}

void bareRFM69::readMultiple(uint8_t reg, void* data, uint8_t len){
    SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0));  // gain control of SPI bus
    this->chipSelect(true); // assert chip select
    
    SPI.transfer((reg % RFM69_READ_REG_MASK));
    uint8_t* r = reinterpret_cast<uint8_t*>(data);
    for (uint8_t i=0; i < len ; i++){
        r[len - i - 1] = SPI.transfer(0);
    }
    this->chipSelect(false);// deassert chip select
    SPI.endTransaction();    // release the SPI bus
}

I won’t be able to use interrupt AutoMode clients with the EasySensors shields as the DIO2 pin is not connected but on the AdaFruit RFM69HCW Radio Bonnet 433MHz or 915MHz it is connected to GPIO24.

RFM69 hat library lockups and corruptions

While doing yet more stress testing I noticed a couple of odd message go past and a long pause every so often when sending a message in the Visual Studio output window.

I have two Arduino devices sending addressed messages every (both individual and broadcast) to the Adafruit RFM69 HCW Radio Bonnet, on my two Windows 10 IoT Core devices every 100mSec. At the same time the windows 10 devices are sending each other a message every 5 seconds.

To help spot the pauses I added some code to mark any events where there was a significant gap. In this case ” is ASCII character for 0x22 the device address

21:10:30.746 Received To 34 a 23 byte message Hello World ---0x22:236 CRC Ok True
21:10:30.918 Received To 153 a 23 byte message Hello World ---0x99:236 CRC Ok True
21:10:31.399 Received To 34 a 23 byte message Hello World ---0x22:237 CRC Ok True
21:10:31.568 Send-hello world RFM69-915-01 09-10-31
21:10:31.580 Send-Done
21:10:31.592 Received To 34 a 33 byte message """"""""""""""""""""""""""""""""" CRC Ok True
RC-------------------------------------------
21:10:32.052 Received To 34 a 23 byte message Hello World ---0x22:238 CRC Ok True
21:10:32.225 Received To 153 a 23 byte message Hello World ---0x99:238 CRC Ok True
21:10:32.705 Received To 34 a 23 byte message Hello World ---0x22:239 CRC Ok True

There were also still some corrupted messages

21:10:30.746 Received To 34 a 23 byte message Hello World ---0x22:236 CRC Ok True
21:10:30.918 Received To 153 a 23 byte message Hello World ---0x99:236 CRC Ok True
21:10:31.399 Received To 34 a 23 byte message Hello World ---0x22:237 CRC Ok True
21:10:31.568 Send-hello world RFM69-915-01 09-10-31
21:10:31.580 Send-Done
21:10:31.592 Received To 34 a 33 byte message """"""""""""""""""""""""""""""""" CRC Ok True
RC-------------------------------------------
21:10:32.052 Received To 34 a 23 byte message Hello World ---0x22:238 CRC Ok True
21:10:32.225 Received To 153 a 23 byte message Hello World ---0x99:238 CRC Ok True
21:10:32.705 Received To 34 a 23 byte message Hello World ---0x22:239 CRC Ok True

It looks like if the base station receives a message as it is about to send a message the Rfm69Device_OnTransmit never gets called.

It also looks like every so often the transmitter gets stuck on one of Windows 10 devices effectively jamming the frequency.

Transmit stuck on
16:12:10.193 Received To 34 a 22 byte message Hello World ---0x22:65 CRC Ok True
16:12:10.360 Received To 153 a 22 byte message Hello World ---0x99:65 CRC Ok True
16:12:10.831 Received To 34 a 22 byte message Hello World ---0x22:66 CRC Ok True
16:12:10.998 Received To 153 a 22 byte message Hello World ---0x99:66 CRC Ok True
The thread 0x570 has exited with code 0 (0x0).
16:12:11.484 Send-hello world RFM69-915-01 04-12-11
16:12:11.494 Received To 34 a 22 byte message Hello World ---0x22:67 CRC Ok True
16:12:11.504 Send-Done
The thread 0x3a8 has exited with code 0 (0x0).
16:12:16.554 Send-hello world RFM69-915-01 04-12-16
16:12:16.566 Send-Done
16:12:16.660 Transmit-Done
T--------------------------------------------
16:12:16.736 Received To 153 a 22 byte message Hello World ---0x99:75 CRC Ok True
16:12:17.206 Received To 34 a 22 byte message Hello World ---0x22:76 CRC Ok True
16:12:17.374 Received To 153 a 22 byte message Hello World ---0x99:76 CRC Ok True
16:12:18.011 Received To 153 a 22 byte message Hello World ---0x99:77 CRC Ok True


Transmit stuck 
16:12:07.591 Transmit-Done
16:12:07.880 Received To 153 a 23 byte message Hello World ---0x99:137 CRC Ok True
16:12:08.533 Received To 153 a 23 byte message Hello World ---0x99:138 CRC Ok True
16:12:08.839 Received To 17 a 24 byte message Hello World ----0x11:139 CRC Ok True
16:12:09.186 Received To 153 a 23 byte message Hello World ---0x99:139 CRC Ok True
16:12:09.493 Received To 17 a 24 byte message Hello World ----0x11:140 CRC Ok True
16:12:10.799 Received To 17 a 24 byte message Hello World ----0x11:142 CRC Ok True
The thread 0xc8 has exited with code 0 (0x0).
16:12:12.567 Send-hello world RFM69-915-02 04-12-12
16:12:12.589 Send-Done
16:12:12.681 Transmit-Done
16:12:16.510 Received To 17 a 33 byte message hello world RFM69-915-01 04-12-16 CRC Ok True
16:12:16.576 Received To 153 a 22 byte message Hello World ---0x99:75 CRC Ok True
16:12:17.025 Received To 153 a 23 byte message Hello World ---0x99:151 CRC Ok True
16:12:17.214 Received To 153 a 22 byte message Hello World ---0x99:76 CRC Ok True
16:12:17.331 Received To 17 a 24 byte message Hello World ----0x11:152 CRC Ok True
The thread 0xfa0 has exited with code 0 (0x0).
16:12:17.661 Send-hello world RFM69-915-02 04-12-17
16:12:17.680 Send-Done
16:12:17.772 Transmit-Done
16:12:17.851 Received To 153 a 22 byte message Hello World ---0x99:77 CRC Ok True
16:12:18.331 Received To 153 a 23 byte message Hello World ---0x99:153 CRC Ok True
16:12:18.489 Received To 153 a 22 byte message Hello World ---0x99:78 CRC Ok True
16:12:18.638 Received To 17 a 24 byte message Hello World ----0x11:154 CRC Ok True
16:12:18.985 Received To 153 a 23 byte message Hello World ---0x99:154 CRC Ok True
16:12:19.291 Received To 17 a 24 byte message Hello World ----0x11:155 CRC Ok True
16:12:19.638 Received To 153 a 23 byte message Hello World ---0x99:155 CRC Ok True
16:12:19.944 Received To 17 a 24 byte message Hello World ----0x11:156 CRC Ok True
16:12:20.291 Received To 153 a 23 byte message Hello World ---0x99:156 CRC Ok True
16:12:20.597 Received To 17 a 24 byte message Hello World ----0x11:157 CRC Ok True

Then as rfm69Device.SetMode(Rfm69HcwDevice.RegOpModeMode.Receive) hasn’t been called no messages are received until another message is sent.

It looks like a timing issue around access to the message fifo (I have that in a critical section) so I need todo some more debugging. Maybe purging the receive buffer

byte regPacketConfig2 = RegisterManager.ReadByte((byte)Rfm69HcwDevice.Registers.RegPacketConfig2);
			regPacketConfig2 |= 0b00000100;
			RegisterManager.WriteByte((byte)Rfm69HcwDevice.Registers.RegPacketConfig2, regPacketConfig2);

The adfruit.io RFM69 shield has DIO2 which can be used for automode operation which might remove some of the synchronisation issues I am encountering.