Windows 10 IoT Core TPM SAS Token Expiry

This is for people who were searching for why the SAS token issued by the TPM on their Windows 10 IoT Core device is expiring much quicker than expected or might have noticed that something isn’t quite right with the “validity” period. (as at early May 2019). If you want to “follow along at home” the code I used is available on GitHub.

I found the SAS key was expiring in roughly 5 minutes and the validity period in the configuration didn’t appear to have any effect on how long the SAS token was valid.

10:04:16 Application started
...
10:04:27 SAS token needs renewing
10:04:30 SAS token renewed 
 10:04:30.984 AzureIoTHubClient SendEventAsync starting
 10:04:36.709 AzureIoTHubClient SendEventAsync starting
The thread 0x1464 has exited with code 0 (0x0).
 10:04:37.808 AzureIoTHubClient SendEventAsync finished
 10:04:37.808 AzureIoTHubClient SendEventAsync finished
The thread 0xb88 has exited with code 0 (0x0).
The thread 0x1208 has exited with code 0 (0x0).
The thread 0x448 has exited with code 0 (0x0).
The thread 0x540 has exited with code 0 (0x0).
 10:04:46.763 AzureIoTHubClient SendEventAsync starting
 10:04:47.051 AzureIoTHubClient SendEventAsync finished
The thread 0x10d8 has exited with code 0 (0x0).
The thread 0x6e0 has exited with code 0 (0x0).
The thread 0xf7c has exited with code 0 (0x0).
 10:04:56.808 AzureIoTHubClient SendEventAsync starting
 10:04:57.103 AzureIoTHubClient SendEventAsync finished
The thread 0xb8c has exited with code 0 (0x0).
The thread 0xc60 has exited with code 0 (0x0).
 10:05:06.784 AzureIoTHubClient SendEventAsync starting
 10:05:07.057 AzureIoTHubClient SendEventAsync finished
...
The thread 0x4f4 has exited with code 0 (0x0).
The thread 0xe10 has exited with code 0 (0x0).
The thread 0x3c8 has exited with code 0 (0x0).
 10:09:06.773 AzureIoTHubClient SendEventAsync starting
 10:09:07.044 AzureIoTHubClient SendEventAsync finished
The thread 0xf70 has exited with code 0 (0x0).
The thread 0x1214 has exited with code 0 (0x0).
 10:09:16.819 AzureIoTHubClient SendEventAsync starting
 10:09:17.104 AzureIoTHubClient SendEventAsync finished
The thread 0x1358 has exited with code 0 (0x0).
The thread 0x400 has exited with code 0 (0x0).
 10:09:26.802 AzureIoTHubClient SendEventAsync starting
 10:09:27.064 AzureIoTHubClient SendEventAsync finished
The thread 0x920 has exited with code 0 (0x0).
The thread 0x1684 has exited with code 0 (0x0).
The thread 0x4ec has exited with code 0 (0x0).
 10:09:36.759 AzureIoTHubClient SendEventAsync starting
'backgroundTaskHost.exe' (CoreCLR: CoreCLR_UWP_Domain): Loaded 'C:\Data\Programs\WindowsApps\Microsoft.NET.CoreFramework.Debug.2.2_2.2.27505.2_arm__8wekyb3d8bbwe\System.Net.Requests.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
'backgroundTaskHost.exe' (CoreCLR: CoreCLR_UWP_Domain): Loaded 'C:\Data\Programs\WindowsApps\Microsoft.NET.CoreFramework.Debug.2.2_2.2.27505.2_arm__8wekyb3d8bbwe\System.Net.WebSockets.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Sending payload to AzureIoTHub failed:CONNECT failed: RefusedNotAuthorized

I went and looked at the NuGet package details and it seemed a bit old.

I have the RedGate Reflector plugin installed on my development box so I quickly disassembled the Microsoft.Devices.TPM assembly to see what was going on. The Reflector code is pretty readable and it wouldn’t take much “refactoring” to get it looking like “human” generated code.

public string GetSASToken(uint validity = 0xe10)
{
    string deviceId = this.GetDeviceId();
    string hostName = this.GetHostName();
    long num = (DateTime.get_Now().ToUniversalTime().ToFileTime() / 0x98_9680L) - 0x2_b610_9100L;
    string str3 = "";
    if ((hostName.Length > 0) && (deviceId.Length > 0))
    {
        object[] objArray1 = new object[] { hostName, "/devices/", deviceId, "\n", (long) num };
        byte[] bytes = new UTF8Encoding().GetBytes(string.Concat((object[]) objArray1));
        byte[] buffer2 = this.SignHmac(bytes);
        if (buffer2.Length != 0)
        {
            string str5 = this.AzureUrlEncode(Convert.ToBase64String(buffer2));
            object[] objArray2 = new object[] { "SharedAccessSignature sr=", hostName, "/devices/", deviceId, "&sig=", str5, "&se=", (long) num };
            str3 = string.Concat((object[]) objArray2);
        }
    }
    return str3;
}

The validity parameter appears to not used. Below is the current code from the Azure IoT CSharp SDK on GitHub repository and they are different, the validity is used.

public string GetSASToken(uint validity = 3600)
{
   const long WINDOWS_TICKS_PER_SEC = 10000000;
   const long EPOCH_DIFFERNECE = 11644473600;
   string deviceId = GetDeviceId();
   string hostName = GetHostName();
   long expirationTime = (DateTime.Now.ToUniversalTime().ToFileTime() / WINDOWS_TICKS_PER_SEC) - EPOCH_DIFFERNECE;
   expirationTime += validity;
   string sasToken = "";
   if ((hostName.Length > 0) && (deviceId.Length > 0))
   {
      // Encode the message to sign with the TPM
      UTF8Encoding utf8 = new UTF8Encoding();
      string tokenContent = hostName + "/devices/" + deviceId + "\n" + expirationTime;
      Byte[] encodedBytes = utf8.GetBytes(tokenContent);

      // Sign the message
      Byte[] hmac = SignHmac(encodedBytes);

      // if we got a signature foramt it
      if (hmac.Length > 0)
      {
         // Encode the output and assemble the connection string
         string hmacString = AzureUrlEncode(System.Convert.ToBase64String(hmac));
         sasToken = "SharedAccessSignature sr=" + hostName + "/devices/" + deviceId + "&sig=" + hmacString + "&se=" + expirationTime;
         }
   }
   return sasToken;
}

I went back and look at the Github history and it looks like a patch was applied after the NuGet packages were released in May 2016.

If you read from the TPM and get nothing make sure you’re using the right TPM slot number and have “System Management” checked in the capabilities tab of the application manifest.

I’m still not certain the validity is being applied correctly and will dig into in a future post.

Azure IOT Hub nRF24L01 Windows 10 IoT Core Field Gateway with BorosRF2

A couple of BorosRF2 Dual nRF24L01 Hats arrived earlier in the week. After some testing with my nRF24L01 Test application I have added compile-time configuration options for the two nRF24L01 sockets to my Azure IoT Hub nRF24L01 Field Gateway.

Boros RF2 with Dual nRF24L01 devices
public sealed class StartupTask : IBackgroundTask
{
   private const string ConfigurationFilename = "config.json";

   private const byte MessageHeaderPosition = 0;
   private const byte MessageHeaderLength = 1;

   // nRF24 Hardware interface configuration
#if CEECH_NRF24L01P_SHIELD
   private const byte RF24ModuleChipEnablePin = 25;
   private const byte RF24ModuleChipSelectPin = 0;
   private const byte RF24ModuleInterruptPin = 17;
#endif

#if BOROS_RF2_SHIELD_RADIO_0
   private const byte RF24ModuleChipEnablePin = 24;
   private const byte RF24ModuleChipSelectPin = 0;
   private const byte RF24ModuleInterruptPin = 27;
#endif

#if BOROS_RF2_SHIELD_RADIO_1
   private const byte RF24ModuleChipEnablePin = 25;
   private const byte RF24ModuleChipSelectPin = 1;
   private const byte RF24ModuleInterruptPin = 22;
#endif

private readonly LoggingChannel logging = new LoggingChannel("devMobile Azure IotHub nRF24L01 Field Gateway", null, new Guid("4bd2826e-54a1-4ba9-bf63-92b73ea1ac4a"));
private readonly RF24 rf24 = new RF24();

This version supports one nRF24L01 device socket active at a time.

Enabling both nRF24L01 device sockets broke outbound message routing in a prototype branch with cloud to device(C2D) messaging support. This functionality is part of an Over The Air(OTA) device provisioning implementation I’m working o.

Windows 10 IoT Core Field Gateways “less is more”

After looking back at the technical support interactions for my Azure IoT Hubs Windows 10 IoT Core Field Gateway & AdaFruit.IO LoRa Windows 10 IoT Core Field Gateway I think removing a “feature” might make it easier for first time users.

In an early version of the software I used to provide a sample configuration JSON file in the associated GitHub repository. Users had to download this file to a computer, update it with their Azure IOT Hub or Azure IoT Central connection string or AdafruitIO APIKey , frequency and device address, then upload to the field gateway.

In a later version of the software I added code which created an empty configuration file with defaults for all settings, many of which were a distraction as the majority of users would never change them.

More settings meant there was more scope for users to change settings which broke the device samples and the gateway.

I have removed the code to generate the full configuration file (starting with Azure IOT Hub field gateway) and included a sample configuration file with the minimum required settings in the GitHub repositories and installers.

I am assuming that if a user wants to change advanced settings they can look at the code and/or documentation and figure out the setting names and valid values.

The new sample configuration file for a Azure IoT Hub telemetry only gateway is

{
  "AzureIoTHubDeviceConnectionString": "Azure IOT Hub connection string",
  "AzureIoTHubTransportType": "amqp",
  "SensorIDIsDeviceIDSensorID": false,
  "Address": "Device address",
  "Frequency": 915000000.0
}

The prebuilt installers available on GitHub post version 1.0.13.0 (Azure IoT Hub) and 1.0.5.0 (Adafruit.IO) will implement this model.

SparkFun Pro RF – LoRa, 915MHz Payload Addressing Client

Last week a package arrived from NiceGear with a SparkFun Pro RF – LoRa, 915MHz and some cables. With this gear I have built yet another client for my Azure IoT Hub and AdaFruit.IOLoRa Field Gateways.

Now that the device is running well, I’ll look at reducing power consumption and splitting the the payload packing code into a library. Also noticed an extra “,” on the end of a message so need to come up with a better way of doing the payload packing.

/*
  Copyright ® 2018 December devMobile Software, All Rights Reserved

  THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
  KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
  IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
  PURPOSE.

  You can do what you want with this code, acknowledgment would be nice.

  http://www.devmobile.co.nz

*/
#include <stdlib.h&gt;

#include <LoRa.h&gt;
#include <avr/dtostrf.h&gt;
#include "DHT.h"

#define DEBUG
//#define DEBUG_TELEMETRY
//#define DEBUG_LORA

// LoRa field gateway configuration (these settings must match your field gateway)
const char FieldGatewayAddress[] = {"LoRaIoT1"};
const char DeviceAddress[] = {"SparkFunX1"};
const float FieldGatewayFrequency =  915000000.0;
const byte FieldGatewaySyncWord = 0x12 ;

// Payload configuration
const int InterruptPin = 12;
const int ChipSelectPin = 6;

// LoRa radio payload configuration
const byte SensorIdValueSeperator = ' ' ;
const byte SensorReadingSeperator = ',' ;
const int LoopSleepDelaySeconds = 10 ;

const byte PayloadSizeMaximum = 64 ;
byte payload[PayloadSizeMaximum];
byte payloadLength = 0 ;

#define DHTPIN 4     // what digital pin we're connected to

// Uncomment whatever type you're using!
//#define DHTTYPE DHT11   // DHT 11
#define DHTTYPE DHT22   // DHT 22  (AM2302), AM2321
//#define DHTTYPE DHT21   // DHT 21 (AM2301)

DHT dht(DHTPIN, DHTTYPE);


void setup()
{
  SerialUSB.begin(9600);
#ifdef DEBUG
  while (!SerialUSB);
#endif
  SerialUSB.println("Setup called");

  SerialUSB.println("LoRa setup start");
  
  // override the default chip select and reset pins
  LoRa.setPins(InterruptPin, ChipSelectPin); 
  if (!LoRa.begin(FieldGatewayFrequency))
  {
    SerialUSB.println("LoRa begin failed");
    while (true); // Drop into endless loop requiring restart
  }

  // Need to do this so field gateways pays attention to messsages from this device
  LoRa.enableCrc();
  LoRa.setSyncWord(FieldGatewaySyncWord);

#ifdef DEBUG_LORA
  LoRa.dumpRegisters(SerialUSB);
#endif
  SerialUSB.println("LoRa Setup done.");

  // Configure the Seeedstudio TH02 temperature &amp; humidity sensor
  SerialUSB.println("DHT setup start");
  dht.begin();
  delay(100);
  SerialUSB.println("DHT setup done");

  PayloadHeader((byte*)FieldGatewayAddress,strlen(FieldGatewayAddress), (byte*)DeviceAddress, strlen(DeviceAddress));

  SerialUSB.println("Setup done");
  SerialUSB.println();
}


void loop()
{
  float temperature = 17.2;
  float humidity = 75.0;

  SerialUSB.println("Loop called");

  PayloadReset();

  // Read the temperature &amp; humidity &amp; battery voltage values then display nicely
  temperature = dht.readTemperature();
  humidity = dht.readHumidity();
  if (isnan(humidity) || isnan(temperature)) 
  {
    SerialUSB.println("Failed to read from DHT sensor!");
    return;
  } 
   
  SerialUSB.print("T:");
  SerialUSB.print( temperature, 1 ) ;
  SerialUSB.println( "C " ) ;
  PayloadAdd( "T", temperature, 1);

  SerialUSB.print("H:");
  SerialUSB.print( humidity, 0 ) ;
  SerialUSB.println( "% " ) ;
  PayloadAdd( "H", humidity, 0) ;

#ifdef DEBUG_TELEMETRY
  SerialUSB.println();
  SerialUSB.print( "RFM9X/SX127X Payload length:");
  SerialUSB.print( payloadLength );
  SerialUSB.println( " bytes" );
#endif

  LoRa.beginPacket();
  LoRa.write( payload, payloadLength );
  LoRa.endPacket();

  SerialUSB.println("Loop done");
  SerialUSB.println();
  delay(LoopSleepDelaySeconds * 1000l);
}


void PayloadHeader( byte *to, byte toAddressLength, byte *from, byte fromAddressLength)
{
  byte addressesLength = toAddressLength + fromAddressLength ;

#ifdef DEBUG_TELEMETRY
  SerialUSB.println("PayloadHeader- ");
  SerialUSB.print( "To Address len:");
  SerialUSB.print( toAddressLength );
  SerialUSB.print( " From Address len:");
  SerialUSB.print( fromAddressLength );
  SerialUSB.print( " Addresses length:");
  SerialUSB.print( addressesLength );
  SerialUSB.println( );
#endif

  payloadLength = 0 ;

  // prepare the payload header with "To" Address length (top nibble) and "From" address length (bottom nibble)
  payload[payloadLength] = (toAddressLength << 4) | fromAddressLength ;
  payloadLength += 1;

  // Copy the "To" address into payload
  memcpy(&amp;payload[payloadLength], to, toAddressLength);
  payloadLength += toAddressLength ;

  // Copy the "From" into payload
  memcpy(&amp;payload[payloadLength], from, fromAddressLength);
  payloadLength += fromAddressLength ;
}


void PayloadAdd( char *sensorId, float value, byte decimalPlaces)
{
  byte sensorIdLength = strlen( sensorId ) ;

#ifdef DEBUG_TELEMETRY
  SerialUSB.println("PayloadAdd-float ");
  SerialUSB.print( "SensorId:");
  SerialUSB.print( sensorId );
  SerialUSB.print( " sensorIdLen:");
  SerialUSB.print( sensorIdLength );
  SerialUSB.print( " Value:");
  SerialUSB.print( value, decimalPlaces );
  SerialUSB.print( " payloadLength:");
  SerialUSB.print( payloadLength);
#endif

  memcpy( &amp;payload[payloadLength], sensorId,  sensorIdLength) ;
  payloadLength += sensorIdLength ;
  payload[ payloadLength] = SensorIdValueSeperator;
  payloadLength += 1 ;
  payloadLength += strlen( dtostrf(value, -1, decimalPlaces, (char *)&amp;payload[payloadLength]));
  payload[ payloadLength] = SensorReadingSeperator;
  payloadLength += 1 ;
  
#ifdef DEBUG_TELEMETRY
  SerialUSB.print( " payloadLength:");
  SerialUSB.print( payloadLength);
  SerialUSB.println( );
#endif
}


void PayloadAdd( char *sensorId, int value )
{
  byte sensorIdLength = strlen( sensorId ) ;

#ifdef DEBUG_TELEMETRY
  SerialUSB.println("PayloadAdd-int ");
  SerialUSB.print( "SensorId:");
  SerialUSB.print( sensorId );
  SerialUSB.print( " sensorIdLen:");
  SerialUSB.print( sensorIdLength );
  SerialUSB.print( " Value:");
  SerialUSB.print( value );
  SerialUSB.print( " payloadLength:");
  SerialUSB.print( payloadLength);
#endif  

  memcpy( &amp;payload[payloadLength], sensorId,  sensorIdLength) ;
  payloadLength += sensorIdLength ;
  payload[ payloadLength] = SensorIdValueSeperator;
  payloadLength += 1 ;
  payloadLength += strlen( itoa( value,(char *)&amp;payload[payloadLength],10));
  payload[ payloadLength] = SensorReadingSeperator;
  payloadLength += 1 ;
  
#ifdef DEBUG_TELEMETRY
  SerialUSB.print( " payloadLength:");
  SerialUSB.print( payloadLength);
  SerialUSB.println( );
#endif
}


void PayloadAdd( char *sensorId, unsigned int value )
{
  byte sensorIdLength = strlen( sensorId ) ;

#ifdef DEBUG_TELEMETRY
  SerialUSB.println("PayloadAdd-unsigned int ");
  SerialUSB.print( "SensorId:");
  SerialUSB.print( sensorId );
  SerialUSB.print( " sensorIdLen:");
  SerialUSB.print( sensorIdLength );
  SerialUSB.print( " Value:");
  SerialUSB.print( value );
  SerialUSB.print( " payloadLength:");
  SerialUSB.print( payloadLength);
#endif  

  memcpy( &amp;payload[payloadLength], sensorId,  sensorIdLength) ;
  payloadLength += sensorIdLength ;
  payload[ payloadLength] = SensorIdValueSeperator;
  payloadLength += 1 ;
  payloadLength += strlen( utoa( value,(char *)&amp;payload[payloadLength],10));
  payload[ payloadLength] = SensorReadingSeperator;
  payloadLength += 1 ;

#ifdef DEBUG_TELEMETRY
  SerialUSB.print( " payloadLength:");
  SerialUSB.print( payloadLength);
  SerialUSB.println( );
#endif
}


void PayloadReset()
{
  byte fromAddressLength = payload[0] &amp; 0xf ;
  byte toAddressLength = payload[0] &gt;&gt; 4 ;
  byte addressesLength = toAddressLength + fromAddressLength ;

  payloadLength = addressesLength + 1;

#ifdef DEBUG_TELEMETRY
  SerialUSB.println("PayloadReset- ");
  SerialUSB.print( "To Address len:");
  SerialUSB.print( toAddressLength );
  SerialUSB.print( " From Address len:");
  SerialUSB.print( fromAddressLength );
  SerialUSB.print( " Addresses length:");
  SerialUSB.print( addressesLength );
  SerialUSB.println( );
#endif
}
  • SparkFun Pro RF – LoRa, 915MHz USD29.95 NZD49
  • Grove – Temperature & Humidity Sensor Pro (AM2302) USD9.90
  • Seeedstudio 4 pin Male Jumper to Grove 4 pin Conversion Cable USD2.90

Moteino M0 Payload Addressing client

Last week a package arrived from LowPowerLab with some Moteino0 devices and accessories . With this gear I have built yet another client for my Azure IoT Hub and AdaFruit.IOLoRa Field Gateways.

It took me a while longer that usual to get the Motenio working as the sketch setup call appeared to hang in DEBUG builds.

After staring at the code for a while I noticed that I hadn’t changed LoRa.dumpRegisters method parameter from Serial to SerialUSB. A couple of hours lost due to a dumb typo by me.

Now that the device is running well, I’ll look at reducing power consumption and splitting the the payload packing code into a library.

/*
  Copyright ® 2018 November devMobile Software, All Rights Reserved

  THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
  KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
  IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
  PURPOSE.

  You can do what you want with this code, acknowledgment would be nice.

  http://www.devmobile.co.nz

*/
#include <stdlib.h&gt;
#include <avr/dtostrf.h&gt;
#include <LoRa.h&gt;
#include <TH02_dev.h&gt;

//#define DEBUG
//#define DEBUG_TELEMETRY
//#define DEBUG_LORA

// LoRa field gateway configuration (these settings must match your field gateway)
const char FieldGatewayAddress[] = {"LoRaIoT1"};
const char DeviceAddress[] = {"Moteino01"};
const float FieldGatewayFrequency =  915000000.0;
const byte FieldGatewaySyncWord = 0x12 ;

// Payload configuration
const int ChipSelectPin = A2;
const int InterruptPin = 9;
const int ResetPin = -1;

// LoRa radio payload configuration
const byte SensorIdValueSeperator = ' ' ;
const byte SensorReadingSeperator = ',' ;
const int LoopSleepDelaySeconds = 10 ;

const byte PayloadSizeMaximum = 64 ;
byte payload[PayloadSizeMaximum];
byte payloadLength = 0 ;


void setup()
{
  SerialUSB.begin(9600);
#ifdef DEBUG
  while (!SerialUSB);
#endif
  SerialUSB.println("Setup called");

  SerialUSB.println("LoRa setup start");
  
  // override the default chip select and reset pins
  LoRa.setPins( ChipSelectPin, ResetPin, InterruptPin ); 
  if (!LoRa.begin(FieldGatewayFrequency))
  {
    SerialUSB.println("LoRa begin failed");
    while (true); // Drop into endless loop requiring restart
  }

  // Need to do this so field gateways pays attention to messsages from this device
  LoRa.enableCrc();
  LoRa.setSyncWord(FieldGatewaySyncWord);

#ifdef DEBUG_LORA
  LoRa.dumpRegisters(USBSerial);
#endif
  SerialUSB.println("LoRa Setup done.");

  // Configure the Seeedstudio TH02 temperature &amp; humidity sensor
  SerialUSB.println("TH02 setup start");
  TH02.begin();
  delay(100);
  SerialUSB.println("TH02 setup done");

  PayloadHeader((byte*)FieldGatewayAddress,strlen(FieldGatewayAddress), (byte*)DeviceAddress, strlen(DeviceAddress));

  SerialUSB.println("Setup done");
  SerialUSB.println();
}


void loop()
{
  float temperature ;
  float humidity ;

  SerialUSB.println("Loop called");

  PayloadReset();

  // Read the temperature &amp; humidity &amp; battery voltage values then display nicely
  temperature = TH02.ReadTemperature();
  SerialUSB.print("T:");
  SerialUSB.print( temperature, 1 ) ;
  SerialUSB.println( "C " ) ;

  PayloadAdd( "T", temperature, 1);

  humidity = TH02.ReadHumidity();
  SerialUSB.print("H:");
  SerialUSB.print( humidity, 0 ) ;
  SerialUSB.println( "% " ) ;

  PayloadAdd( "H", humidity, 0) ;

#ifdef DEBUG_TELEMETRY
  SerialUSB.println();
  SerialUSB.print( "RFM9X/SX127X Payload length:");
  SerialUSB.print( payloadLength );
  SerialUSB.println( " bytes" );
#endif

  LoRa.beginPacket();
  LoRa.write( payload, payloadLength );
  LoRa.endPacket();

  SerialUSB.println("Loop done");
  SerialUSB.println();
  delay(LoopSleepDelaySeconds * 1000l);
}


void PayloadHeader( byte *to, byte toAddressLength, byte *from, byte fromAddressLength)
{
  byte addressesLength = toAddressLength + fromAddressLength ;

#ifdef DEBUG_TELEMETRY
  SerialUSB.println("PayloadHeader- ");
  SerialUSB.print( "To Address len:");
  SerialUSB.print( toAddressLength );
  SerialUSB.print( " From Address len:");
  SerialUSB.print( fromAddressLength );
  SerialUSB.print( " Addresses length:");
  SerialUSB.print( addressesLength );
  SerialUSB.println( );
#endif

  payloadLength = 0 ;

  // prepare the payload header with "To" Address length (top nibble) and "From" address length (bottom nibble)
  payload[payloadLength] = (toAddressLength << 4) | fromAddressLength ;
  payloadLength += 1;

  // Copy the "To" address into payload
  memcpy(&amp;payload[payloadLength], to, toAddressLength);
  payloadLength += toAddressLength ;

  // Copy the "From" into payload
  memcpy(&amp;payload[payloadLength], from, fromAddressLength);
  payloadLength += fromAddressLength ;
}


void PayloadAdd( const char *sensorId, float value, byte decimalPlaces)
{
  byte sensorIdLength = strlen( sensorId ) ;

#ifdef DEBUG_TELEMETRY
  SerialUSB.println("PayloadAdd-float ");
  SerialUSB.print( "SensorId:");
  SerialUSB.print( sensorId );
  SerialUSB.print( " sensorIdLen:");
  SerialUSB.print( sensorIdLength );
  SerialUSB.print( " Value:");
  SerialUSB.print( value, decimalPlaces );
  SerialUSB.print( " payloadLength:");
  SerialUSB.print( payloadLength);
#endif

  memcpy( &amp;payload[payloadLength], sensorId,  sensorIdLength) ;
  payloadLength += sensorIdLength ;
  payload[ payloadLength] = SensorIdValueSeperator;
  payloadLength += 1 ;
  payloadLength += strlen( dtostrf(value, -1, decimalPlaces, (char *)&amp;payload[payloadLength]));
  payload[ payloadLength] = SensorReadingSeperator;
  payloadLength += 1 ;
  
#ifdef DEBUG_TELEMETRY
  SerialUSB.print( " payloadLength:");
  SerialUSB.print( payloadLength);
  SerialUSB.println( );
#endif
}


void PayloadAdd( const char *sensorId, int value )
{
  byte sensorIdLength = strlen( sensorId ) ;

#ifdef DEBUG_TELEMETRY
  SerialUSB.println("PayloadAdd-int ");
  SerialUSB.print( "SensorId:");
  SerialUSB.print( sensorId );
  SerialUSB.print( " sensorIdLen:");
  SerialUSB.print( sensorIdLength );
  SerialUSB.print( " Value:");
  SerialUSB.print( value );
  SerialUSB.print( " payloadLength:");
  SerialUSB.print( payloadLength);
#endif  

  memcpy( &amp;payload[payloadLength], sensorId,  sensorIdLength) ;
  payloadLength += sensorIdLength ;
  payload[ payloadLength] = SensorIdValueSeperator;
  payloadLength += 1 ;
  payloadLength += strlen( itoa( value,(char *)&amp;payload[payloadLength],10));
  payload[ payloadLength] = SensorReadingSeperator;
  payloadLength += 1 ;
  
#ifdef DEBUG_TELEMETRY
  SerialUSB.print( " payloadLength:");
  SerialUSB.print( payloadLength);
  SerialUSB.println( );
#endif
}


void PayloadAdd( const char *sensorId, unsigned int value )
{
  byte sensorIdLength = strlen( sensorId ) ;

#ifdef DEBUG_TELEMETRY
  SerialUSB.println("PayloadAdd-unsigned int ");
  SerialUSB.print( "SensorId:");
  SerialUSB.print( sensorId );
  SerialUSB.print( " sensorIdLen:");
  SerialUSB.print( sensorIdLength );
  SerialUSB.print( " Value:");
  SerialUSB.print( value );
  SerialUSB.print( " payloadLength:");
  SerialUSB.print( payloadLength);
#endif  

  memcpy( &amp;payload[payloadLength], sensorId,  sensorIdLength) ;
  payloadLength += sensorIdLength ;
  payload[ payloadLength] = SensorIdValueSeperator;
  payloadLength += 1 ;
  payloadLength += strlen( utoa( value,(char *)&amp;payload[payloadLength],10));
  payload[ payloadLength] = SensorReadingSeperator;
  payloadLength += 1 ;

#ifdef DEBUG_TELEMETRY
  SerialUSB.print( " payloadLength:");
  SerialUSB.print( payloadLength);
  SerialUSB.println( );
#endif
}


void PayloadReset()
{
  byte fromAddressLength = payload[0] &amp; 0xf ;
  byte toAddressLength = payload[0] &gt;&gt; 4 ;
  byte addressesLength = toAddressLength + fromAddressLength ;

  payloadLength = addressesLength + 1;

#ifdef DEBUG_TELEMETRY
  SerialUSB.println("PayloadReset- ");
  SerialUSB.print( "To Address len:");
  SerialUSB.print( toAddressLength );
  SerialUSB.print( " From Address len:");
  SerialUSB.print( fromAddressLength );
  SerialUSB.print( " Addresses length:");
  SerialUSB.print( addressesLength );
  SerialUSB.println( );
#endif
}
Azure IoT Central

Bill of materials (prices as at November 2018)

  • Moteino M0 USD34.95
  • Seeedstudio Temperature and Humidity Sensor Pro USD11.50
  • Seeedstudio 4 pin Male Jumper to Grove 4 pin Conversion Cable USD2.90

Arduino MKR1300 WAN Payload Addressing client

Last week a package arrived from SeeedStudio with some Arduino devices and Grove shields. With this gear I have built yet another client for my Azure IoT Hub and AdaFruit.IO  LoRa Field Gateways.

For my application I directly access the on-board Semtech SX127X chip by passing the Murata CMWX1ZZABZ functionality. To do this I (November 2018) I had to upgrade the device firmware using the Arduino updater.

Arduino MKR1300 WAN device with Grove Shield & patch antenna

The application is a modified version of my Arduino code with additional debugging support and payload formatting functionality.

/*
  Copyright ® 2018 November devMobile Software, All Rights Reserved

  THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
  KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
  IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
  PURPOSE.

  You can do what you want with this code, acknowledgment would be nice.

  http://www.devmobile.co.nz

*/
#include <stdlib.h&gt;
#include <avr/dtostrf.h&gt;
#include <LoRa.h&gt;
#include <TH02_dev.h&gt;

#define DEBUG
//#define DEBUG_TELEMETRY
//#define DEBUG_LORA

// LoRa field gateway configuration (these settings must match your field gateway)
const char FieldGatewayAddress[] = {"LoRaIoT1"};
const char DeviceAddress[] = {"MKR1300LoRa1"};
const float FieldGatewayFrequency =  915000000.0;
const byte FieldGatewaySyncWord = 0x12 ;

// Payload configuration
const int InterruptPin = LORA_IRQ_DUMB;
const int ChipSelectPin = 6;
const int ResetPin = 1;

// LoRa radio payload configuration
const byte SensorIdValueSeperator = ' ' ;
const byte SensorReadingSeperator = ',' ;
const int LoopSleepDelaySeconds = 60 ;

const byte PayloadSizeMaximum = 64 ;
byte payload[PayloadSizeMaximum];
byte payloadLength = 0 ;


void setup()
{
  Serial.begin(9600);
#ifdef DEBUG
  while (!Serial);
#endif
  Serial.println("Setup called");

  Serial.println("LoRa setup start");
  
  // override the default chip select and reset pins
  LoRa.setPins(InterruptPin, ChipSelectPin, ResetPin); 
  if (!LoRa.begin(FieldGatewayFrequency))
  {
    Serial.println("LoRa begin failed");
    while (true); // Drop into endless loop requiring restart
  }

  // Need to do this so field gateways pays attention to messsages from this device
  LoRa.enableCrc();
  LoRa.setSyncWord(FieldGatewaySyncWord);

#ifdef DEBUG_LORA
  LoRa.dumpRegisters(Serial);
#endif
  Serial.println("LoRa Setup done.");

  // Configure the Seeedstudio TH02 temperature &amp; humidity sensor
  Serial.println("TH02 setup start");
  TH02.begin();
  delay(100);
  Serial.println("TH02 setup done");

  PayloadHeader((byte*)FieldGatewayAddress,strlen(FieldGatewayAddress), (byte*)DeviceAddress, strlen(DeviceAddress));

  Serial.println("Setup done");
  Serial.println();
}


void loop()
{
  float temperature ;
  float humidity ;

  Serial.println("Loop called");

  PayloadReset();

  // Read the temperature &amp; humidity &amp; battery voltage values then display nicely
  temperature = TH02.ReadTemperature();
  Serial.print("T:");
  Serial.print( temperature, 1 ) ;
  Serial.println( "C " ) ;

  PayloadAdd( "T", temperature, 1);

  humidity = TH02.ReadHumidity();
  Serial.print("H:");
  Serial.print( humidity, 0 ) ;
  Serial.println( "% " ) ;

  PayloadAdd( "H", humidity, 0) ;

#ifdef DEBUG_TELEMETRY
  Serial.println();
  Serial.print( "RFM9X/SX127X Payload length:");
  Serial.print( payloadLength );
  Serial.println( " bytes" );
#endif

  LoRa.beginPacket();
  LoRa.write( payload, payloadLength );
  LoRa.endPacket();

  Serial.println("Loop done");
  Serial.println();
  delay(LoopSleepDelaySeconds * 1000l);
}


void PayloadHeader( byte *to, byte toAddressLength, byte *from, byte fromAddressLength)
{
  byte addressesLength = toAddressLength + fromAddressLength ;

#ifdef DEBUG_TELEMETRY
  Serial.println("PayloadHeader- ");
  Serial.print( "To Address len:");
  Serial.print( toAddressLength );
  Serial.print( " From Address len:");
  Serial.print( fromAddressLength );
  Serial.print( " Addresses length:");
  Serial.print( addressesLength );
  Serial.println( );
#endif

  payloadLength = 0 ;

  // prepare the payload header with "To" Address length (top nibble) and "From" address length (bottom nibble)
  payload[payloadLength] = (toAddressLength << 4) | fromAddressLength ;
  payloadLength += 1;

  // Copy the "To" address into payload
  memcpy(&amp;payload[payloadLength], to, toAddressLength);
  payloadLength += toAddressLength ;

  // Copy the "From" into payload
  memcpy(&amp;payload[payloadLength], from, fromAddressLength);
  payloadLength += fromAddressLength ;
}


void PayloadAdd( char *sensorId, float value, byte decimalPlaces)
{
  byte sensorIdLength = strlen( sensorId ) ;

#ifdef DEBUG_TELEMETRY
  Serial.println("PayloadAdd-float ");
  Serial.print( "SensorId:");
  Serial.print( sensorId );
  Serial.print( " sensorIdLen:");
  Serial.print( sensorIdLength );
  Serial.print( " Value:");
  Serial.print( value, decimalPlaces );
  Serial.print( " payloadLength:");
  Serial.print( payloadLength);
#endif

  memcpy( &amp;payload[payloadLength], sensorId,  sensorIdLength) ;
  payloadLength += sensorIdLength ;
  payload[ payloadLength] = SensorIdValueSeperator;
  payloadLength += 1 ;
  payloadLength += strlen( dtostrf(value, -1, decimalPlaces, (char *)&amp;payload[payloadLength]));
  payload[ payloadLength] = SensorReadingSeperator;
  payloadLength += 1 ;
  
#ifdef DEBUG_TELEMETRY
  Serial.print( " payloadLength:");
  Serial.print( payloadLength);
  Serial.println( );
#endif
}


void PayloadAdd( char *sensorId, int value )
{
  byte sensorIdLength = strlen( sensorId ) ;

#ifdef DEBUG_TELEMETRY
  Serial.println("PayloadAdd-int ");
  Serial.print( "SensorId:");
  Serial.print( sensorId );
  Serial.print( " sensorIdLen:");
  Serial.print( sensorIdLength );
  Serial.print( " Value:");
  Serial.print( value );
  Serial.print( " payloadLength:");
  Serial.print( payloadLength);
#endif  

  memcpy( &amp;payload[payloadLength], sensorId,  sensorIdLength) ;
  payloadLength += sensorIdLength ;
  payload[ payloadLength] = SensorIdValueSeperator;
  payloadLength += 1 ;
  payloadLength += strlen( itoa( value,(char *)&amp;payload[payloadLength],10));
  payload[ payloadLength] = SensorReadingSeperator;
  payloadLength += 1 ;
  
#ifdef DEBUG_TELEMETRY
  Serial.print( " payloadLength:");
  Serial.print( payloadLength);
  Serial.println( );
#endif
}


void PayloadAdd( char *sensorId, unsigned int value )
{
  byte sensorIdLength = strlen( sensorId ) ;

#ifdef DEBUG_TELEMETRY
  Serial.println("PayloadAdd-unsigned int ");
  Serial.print( "SensorId:");
  Serial.print( sensorId );
  Serial.print( " sensorIdLen:");
  Serial.print( sensorIdLength );
  Serial.print( " Value:");
  Serial.print( value );
  Serial.print( " payloadLength:");
  Serial.print( payloadLength);
#endif  

  memcpy( &amp;payload[payloadLength], sensorId,  sensorIdLength) ;
  payloadLength += sensorIdLength ;
  payload[ payloadLength] = SensorIdValueSeperator;
  payloadLength += 1 ;
  payloadLength += strlen( utoa( value,(char *)&amp;payload[payloadLength],10));
  payload[ payloadLength] = SensorReadingSeperator;
  payloadLength += 1 ;

#ifdef DEBUG_TELEMETRY
  Serial.print( " payloadLength:");
  Serial.print( payloadLength);
  Serial.println( );
#endif
}


void PayloadReset()
{
  byte fromAddressLength = payload[0] &amp; 0xf ;
  byte toAddressLength = payload[0] &gt;&gt; 4 ;
  byte addressesLength = toAddressLength + fromAddressLength ;

  payloadLength = addressesLength + 1;

#ifdef DEBUG_TELEMETRY
  Serial.println("PayloadReset- ");
  Serial.print( "To Address len:");
  Serial.print( toAddressLength );
  Serial.print( " From Address len:");
  Serial.print( fromAddressLength );
  Serial.print( " Addresses length:");
  Serial.print( addressesLength );
  Serial.println( );
#endif
}

After updating the firmware configuring the data to display in Azure IoT Central (or AdaFruit.IO) took minimal time.

Arduino MKR 1300 Data in Azure IoT Central

Bill of materials (Prices as at Nov 2018)

  • Arduino MKR WAN 1300 USD39.80
  • Arduino MKR Connection Carrier (Grove Compatible) USD22.80
  • Grove Temperature & Humidity Sensor USD11.50

So far the battery life is looking pretty good considering all I have done is used Delay to stop the loop method for 60 seconds.

Next steps are to see if I can retrieve a unique identifier from the Murata firmware and improve battery life by hibernating the processor etc.

Azure IoT Hubs LoRa Field Gateway BCD Addressing

After some testing with more client devices, especially the Easy Sensors Arduino Nano radio shield RFM69/95 or NRF24L01+ I have decided to move to non text addresses for devices and the LoRa field gateway.

THIS IS A BREAKING CHANGE

The unique identifier provided by the SHA204A crypto and authentication chip on the EasySensors shield highlighted this issue. The Binary Coded Decimal(BCD) version of the 72 bit identifier was too long to fit in the from address.

My Arduino MKR1300 sample code has some helper functions to populate the message header, add values, and prepare the message payload for reuse.

On the server side I have added code to log the build version and Raspbery PI shield type

// Log the Application build, shield information etc.
LoggingFields appllicationBuildInformation = new LoggingFields();

#if DRAGINO
   appllicationBuildInformation.AddString("Shield", "DraginoLoRaGPSHat");
#endif

...

#if UPUTRONICS_RPIPLUS_CS1
   appllicationBuildInformation.AddString("Shield", "UputronicsPiPlusLoRaExpansionBoardCS1");
#endif
appllicationBuildInformation.AddString("Timezone", TimeZoneSettings.CurrentTimeZoneDisplayName);
appllicationBuildInformation.AddString("OSVersion", Environment.OSVersion.VersionString);
appllicationBuildInformation.AddString("MachineName", Environment.MachineName);

// This is from the application manifest 
Package package = Package.Current;
PackageId packageId = package.Id;
PackageVersion version = packageId.Version;

appllicationBuildInformation.AddString("ApplicationVersion", string.Format($"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}"));
this.logging.LogEvent("Application starting", appllicationBuildInformation, LoggingLevel.Information);

Then when the message payload is populated the from address byte array is converted to BCD

private async void Rfm9XDevice_OnReceive(object sender, Rfm9XDevice.OnDataReceivedEventArgs e)
{
   string addressBcdText;
   string messageBcdText;
   string messageText = "";
   char[] sensorReadingSeparators = new char[] { ',' };
   char[] sensorIdAndValueSeparators = new char[] { ' ' };

   addressBcdText = BitConverter.ToString(e.Address);
   messageBcdText = BitConverter.ToString(e.Data);
   try
   {
      messageText = UTF8Encoding.UTF8.GetString(e.Data);
   }
   catch (Exception)
   {
      this.logging.LogMessage("Failure converting payload to text", 
   LoggingLevel.Error);
      return;
   }


#if DEBUG
   Debug.WriteLine(@"{0:HH:mm:ss}-RX From {1} PacketSnr {2:0.0} Packet 
   RSSI {3}dBm RSSI {4}dBm = {5} byte message ""{6}""", DateTime.Now, 
   messageBcdText, e.PacketSnr, e.PacketRssi, e.Rssi, e.Data.Length, 
   messageText);
#endif
   LoggingFields messagePayload = new LoggingFields();
   messagePayload.AddInt32("AddressLength", e.Address.Length);
   messagePayload.AddString("Address-BCD", addressBcdText);
   messagePayload.AddInt32("Message-Length", e.Data.Length);
   messagePayload.AddString("Message-BCD", messageBcdText);
   messagePayload.AddString("Message-Unicode", messageText);
   messagePayload.AddDouble("Packet SNR", e.PacketSnr);
   messagePayload.AddInt32("Packet RSSI", e.PacketRssi);
   messagePayload.AddInt32("RSSI", e.Rssi);
   this.logging.LogEvent("Message Data", messagePayload, LoggingLevel.Verbose);

//...

   JObject telemetryDataPoint = new JObject(); // This could be simplified but for field gateway will use this style
   LoggingFields sensorData = new LoggingFields();

   telemetryDataPoint.Add("DeviceID", addressBcdText);
   sensorData.AddString("DeviceID", addressBcdText);
   telemetryDataPoint.Add("PacketSNR", e.PacketSnr.ToString("F1"));
   sensorData.AddString("PacketSNR", e.PacketSnr.ToString("F1"));
   telemetryDataPoint.Add("PacketRSSI", e.PacketRssi);
   sensorData.AddInt32("PacketRSSI", e.PacketRssi);
   telemetryDataPoint.Add("RSSI", e.Rssi);
   sensorData.AddInt32("RSSI", e.Rssi);

   //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.LogMessage("Sensor reading invalid format", LoggingLevel.Warning);
         return;
      }

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

      try
      {
         if (this.applicationSettings.SensorIDIsDeviceIDSensorID)
         {
            // Construct the sensor ID from SensordeviceID & Value ID
            telemetryDataPoint.Add(string.Format("{0}{1}", addressBcdText, sensorId), value);

            sensorData.AddString(string.Format("{0}{1}", addressBcdText, sensorId), value);
            Debug.WriteLine(" Sensor {0}{1} Value {2}", addressBcdText, sensorId, value);
         }
         else
         {
            telemetryDataPoint.Add(sensorId, value);
            sensorData.AddString(sensorId, value);
            Debug.WriteLine(" Device {0} Sensor {1} Value {2}", addressBcdText, sensorId, value);
         }
      }
      catch (Exception ex)
      {
         this.logging.LogMessage("Sensor reading invalid JSON format " + ex.Message, LoggingLevel.Warning);
         return;
      }
   }

  this.logging.LogEvent("Sensor readings", sensorData, LoggingLevel.Information);

   try
   {
      using (Message message = new Message(Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(telemetryDataPoint))))
      {
         Debug.WriteLine(" AzureIoTHubClient SendEventAsync start");
         await this.azureIoTHubClient.SendEventAsync(message);
         Debug.WriteLine(" AzureIoTHubClient SendEventAsync finish");
      }
   }
   catch (Exception ex)
   {
      this.logging.LogMessage("AzureIoTHubClient SendEventAsync failed " + ex.Message, LoggingLevel.Error);
   }
}

This does mean longer field names but I usually copy n paste them from the Arduino serial monitor of the Event Tracing For Windows (ETW) logging.

Azure IoT Hub LoRa Field Gateway ETW Logging