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.
I now have a working Azure IoT Hub plug-in (Azure IoT Central support as planned as well) with the first iteration focused on Device to Cloud (D2C) messaging. In a future iteration I will add Cloud to Device messaging(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 Hub/Central) gateways and protocol specific field gateways.
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": "YourIoTHubHub.azure-devices.net/MQTTLoRa915MHz/api-version=2018-06-30",
"MQTTPassword": "SharedAccessSignature sr=YourIoTHubHub.azure-devices.net%2Fdevices%2FMQTTLoRa915MHz&sig=123456789012345678901234567890123456789012345%3D&se=1574673583",
"MQTTClientID": "MQTTLoRa915MHz",
"MQTTServer": "YourIoTHubHub.azure-devices.net",
"Address": "LoRaIoT1",
"Frequency": 915000000.0,
"MessageHandlerAssembly": "Mqtt.IoTCore.FieldGateway.LoRa.AzureIoTHub",
"PlatformSpecificConfiguration": ""
}
The application logs debugging information to the Windows 10 IoT Core ETW logging Microsoft-Windows-Diagnostics-LoggingChannel
The message handler uploads all values in an inbound messages in one MQTT message.
namespace devMobile.Mqtt.IoTCore.FieldGateway
{
using System;
using System.Diagnostics;
using System.Text;
using Windows.Foundation.Diagnostics;
using devMobile.IoT.Rfm9x;
using MQTTnet;
using MQTTnet.Client;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
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(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 = $"devices/{MqttClient.Options.ClientId}/messages/events/";
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 Azure IoTHub payload", processReceiveLoggingFields, LoggingLevel.Information);
}
catch (Exception ex)
{
processReceiveLoggingFields.AddString("Exception", ex.ToString());
this.Logging.LogEvent("PublishAsync Azure IoTHub payload", processReceiveLoggingFields, LoggingLevel.Error);
}
}
void IMessageHandler.MqttApplicationMessageReceived(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(Rfm9XDevice.OnDataTransmitedEventArgs e)
{
}
}
}
The formatting of the username and generation of password are password are a bit awkward and will be fixed in a future refactoring. Along with regenerating the SAS connection token just before it is due to expire.