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
This sample client is an Wilderness Labs Meadow with a Sensiron SHT31 Temperature & humidity sensor (supported by meadow foundation), and a generic nRF24L01 device connected with jumper cables.
After sorting out power to the SHT31 (I had to push the jumper cable further into the back of the jumper cable plug). I could see temperature and humidity values getting uploaded to Adafruit.IO.
Visual Studio 2019 debug output
Adafruit.IO “automagically” provisions new feeds which is helpful when building a proof of concept (PoC)
Adafruit.IO feed with default feed IDs
I then modified the feed configuration to give it a user friendly name.
Application Insights logging with message unpackingApplication Insights logging message payload
Then in the last log entry the decoded message payload
/*
Copyright ® 2020 Feb devMobile Software, All Rights Reserved
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
Default URL for triggering event grid function in the local environment.
http://localhost:7071/runtime/webhooks/EventGrid?functionName=functionname
*/
namespace EventGridProcessorAzureIotHub
{
using System;
using System.IO;
using System.Reflection;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.EventGrid.Models;
using Microsoft.Azure.WebJobs.Extensions.EventGrid;
using log4net;
using log4net.Config;
using Newtonsoft.Json;
public static class Telemetry
{
[FunctionName("Telemetry")]
public static void Run([EventGridTrigger]Microsoft.Azure.EventGrid.Models.EventGridEvent eventGridEvent, ExecutionContext executionContext )//, TelemetryClient telemetryClient)
{
ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly());
XmlConfigurator.Configure(logRepository, new FileInfo(Path.Combine(executionContext.FunctionAppDirectory, "log4net.config")));
log.Info($"eventGridEvent.Data-{eventGridEvent}");
log.Info($"eventGridEvent.Data.ToString()-{eventGridEvent.Data.ToString()}");
IotHubDeviceTelemetryEventData iOThubDeviceTelemetryEventData = (IotHubDeviceTelemetryEventData)JsonConvert.DeserializeObject(eventGridEvent.Data.ToString(), typeof(IotHubDeviceTelemetryEventData));
log.Info($"iOThubDeviceTelemetryEventData.Body.ToString()-{iOThubDeviceTelemetryEventData.Body.ToString()}");
byte[] base64EncodedBytes = System.Convert.FromBase64String(iOThubDeviceTelemetryEventData.Body.ToString());
log.Info($"System.Text.Encoding.UTF8.GetString(-{System.Text.Encoding.UTF8.GetString(base64EncodedBytes)}");
}
}
}
Overall it took roughly half a page of code (mainly generated by a tool) to unpack and log the contents of an Azure IoT Hub EventGrid payload to Application Insights.
The size of the packets sent and the total device data appeared to map pretty well but I was also interested in the Transport Layer Security (TLS) and Messaging Queuing Telemetry Transport (MQTT) overheads.
Azure IoT Hub Metrics
To get an idea of the overheads I fired up LiveTcpUdpWatch by Nirsoft and noted down the traffic measure on port 8883.
Conenction LiveTcpUdpWatch main screen
Launching the MQTTNet client sending every 30 seconds resulted in traffic like this
So it looks like my very rough numbers are close to the numbers discussed in the above article. I need to explore the impact of keep-alive messages and other background operations.
I did notice that the .DeviceConnected and .DeviceDisconnected events did take a while to arrive. When I started the field gateway application on the Windows 10 IoT Core device I would get several DeviceTelemetry events before the DeviceConnected event arrived.
I was using Advanced Message Queueing Protocol (AMQP) so I modified the configuration file so I could try all the available options.
C# TransportType enumeration
namespace Microsoft.Azure.Devices.Client
{
//
// Summary:
// Transport types supported by DeviceClient - AMQP/TCP, HTTP 1.1, MQTT/TCP, AMQP/WS,
// MQTT/WS
public enum TransportType
{
//
// Summary:
// Advanced Message Queuing Protocol transport. Try Amqp over TCP first and fallback
// to Amqp over WebSocket if that fails
Amqp = 0,
//
// Summary:
// HyperText Transfer Protocol version 1 transport.
Http1 = 1,
//
// Summary:
// Advanced Message Queuing Protocol transport over WebSocket only.
Amqp_WebSocket_Only = 2,
//
// Summary:
// Advanced Message Queuing Protocol transport over native TCP only
Amqp_Tcp_Only = 3,
//
// Summary:
// Message Queuing Telemetry Transport. Try Mqtt over TCP first and fallback to
// Mqtt over WebSocket if that fails
Mqtt = 4,
//
// Summary:
// Message Queuing Telemetry Transport over Websocket only.
Mqtt_WebSocket_Only = 5,
//
// Summary:
// Message Queuing Telemetry Transport over native TCP only
Mqtt_Tcp_Only = 6
}
}
The first telemetry data arrived 00:57:18, the DeviceConnected arrived 01:01:28 so approximately a 4 minute delay, the DeviceDisconnected arrived within a minute of me shutting the device down.
The first telemetry data arrived 04:16:48, the DeviceConnected arrived 04:20:39 so approximately a 4 minute delay, the DeviceDisconnected arrived within a minute of me shutting the device down.
The first telemetry data arrived 04:05:36, DeviceConnected arrived 04:09:52 so approximately a 4 minute delay, the DeviceDisconnected arrived within a minute of me shutting the device down.
HTTP
I waited for 20 minutes and there wasn’t a DeviceConnected message which I sort of expected as HTTP is a connectionless protocol.
The first telemetry data arrived 01:11:33, the DeviceConnected arrived 01:11:25 so they arrived in order and within 10 seconds, the DeviceDisconnected arrived within a 15 seconds of me shutting the device down.
The first telemetry data arrived 04:42:15, the DeviceConnected arrived 04:42:06 so they arrived in order and within 10 seconds, the DeviceDisconnected arrived within a 20 seconds of me shutting device down.
The first telemetry data arrived 04:36:08, the DeviceConnected arrived 04:36:03 so they arrived in order and within 10 seconds, the DeviceDisconnected arrived within a 30 seconds of me shutting device down.
Summary
My LoRa sensors nodes are sending data roughly every minute which reduces the precision of the times.
It looks like for AMQP based messaging it can take 4-5 minutes for a Devices.DeviceConnected message to arrive, for based MQTT messaging it’s 5-10 seconds.
I have one an Azure IoT HubLoRa Telemetry Field Gateway running in my office and I wanted to process the data collected by the sensors around my property without using a Software as a Service(SaaS) Internet of Things (IoT) package.
Rather than lots of screen grabs of my configuration steps I figured people reading this series of posts would be able to figure the details out themselves.
I downloaded the JSON configuration file template from my Windows 10 device (which is created on first startup after installation) and configured the Azure IoT Hub connection string.
I then uploaded this to my Windows 10 IoT Core device and restarted the Azure IoT Hub Field gateway so it picked up the new settings.
I could then see on the device messages from sensor nodes being unpacked and uploaded to my Azure IoT Hub.
ETW logging on device
In the Azure IoT Hub metrics I graphed the number of devices connected and the number of telemetry messages sent and could see my device connect then start uploading telemetry.
Azure IoT Hub metrics
One of my customers uses Azure Event Grid for application integration and I wanted to explore using it in an IoT solution. The first step was to create an Event Grid Domain.
To confirm my event subscriptions were successful I previously found the “simplest” approach was to use an Azure storage queue endpoint. I had to create an Azure Storage Account with two Azure Storage Queues one for device connectivity (.DeviceConnected & .DeviceDisconnected) events and the other for device telemetry (.DeviceTelemetry) events.
I created a couple of other subscriptions so I could compare the different Event schemas (Event Grid Schema & Cloud Event Schema v1.0). At this stage I didn’t configure any Filters or Additional Features.
Azure IoT Hub Telemetry Event Metrics
I use Cerebrate Cerculean for monitoring and managing a couple of other customer projects so I used it to inspect the messages in the storage queues.
Without writing any code (I will script the configuration) I could upload sensor data to an Azure IoT Hub, subscribe to a selection of events the Azure IoT Hub publishes and then inspect them in an Azure Storage Queue.
I did notice that the .DeviceConnected and .DeviceDisconnected events did take a while to arrive. When I started the field gateway application on the device I would get several DeviceTelemetry events before the DeviceConnected event arrived.
I need to test the expiry of my SAS Tokens some more especially with the client running on my development machine (NZT which is currently UTC+13) and in Azure (UTC timezone)
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)
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.
The application logs debugging information to the Windows 10 IoT Core ETW logging Microsoft-Windows-Diagnostics-LoggingChannel
MQTT LoRa Gateway with Azure IoT Hub plug-in
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.
This post has been edited (2019-11-24) my original assumption about how DateTime.Kind unspecified was handled were incorrect.
As I was testing my Azure MQTT Test Client I noticed some oddness with MQTT connection timeouts and this got me wondering about token expiry times. So, I went searching again and found thisAzure IoT Hub specific sample code
This code worked first time and was more flexible than mine which was a bonus. Though while running my MQTTNet based client I noticed the connection would drop after approximately 10mins (EDIT this was probably an unrelated networking issue).
A long time ago (25 years) I had issues sharing a Unix time value between an applications written with Borland C and Microsoft Visual C which made me wonder about Unix epoch base offsets.
So to test my theory I built a Unix epoch test harness console application
EDIT: I now think the UtcNow to “unspecified” kind mathematics was being handled correctly. I have updated the code to use the DateTime.UnixEpoch constant so the code is more readable.
I need to test the expiry of my SAS Tokens some more especially with the client running on my development machine (NZT which is currently UTC+13) and in Azure (UTC timezone)
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;
}
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.
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.