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
//---------------------------------------------------------------------------------
// Copyright (c) January 2020, devMobile Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//---------------------------------------------------------------------------------
namespace devMobile.IoT.FieldGateway.Client
{
using System;
using System.Text;
using System.Threading;
using devMobile.IoT.Rfm9x;
using Meadow;
using Meadow.Devices;
using Meadow.Foundation.Leds;
using Meadow.Foundation.Sensors.Atmospheric;
using Meadow.Hardware;
using Meadow.Peripherals.Leds;
public class MeadowClient : App<F7Micro, MeadowClient>
{
private const double Frequency = 915000000.0;
private readonly byte[] fieldGatewayAddress = Encoding.UTF8.GetBytes("LoRaIoT1");
private readonly byte[] deviceAddress = Encoding.UTF8.GetBytes("Meadow");
private readonly Rfm9XDevice rfm9XDevice;
private readonly TimeSpan periodTime = new TimeSpan(0, 0, 60);
private readonly Sht31D sensor;
private readonly ILed Led;
public MeadowClient()
{
Led = new Led(Device, Device.Pins.OnboardLedGreen);
try
{
sensor = new Sht31D(Device.CreateI2cBus());
ISpiBus spiBus = Device.CreateSpiBus(500);
rfm9XDevice = new Rfm9XDevice(Device, spiBus, Device.Pins.D09, Device.Pins.D10, Device.Pins.D12);
rfm9XDevice.Initialise(Frequency, paBoost: true, rxPayloadCrcOn: true);
#if DEBUG
rfm9XDevice.RegisterDump();
#endif
rfm9XDevice.OnReceive += Rfm9XDevice_OnReceive;
rfm9XDevice.Receive(deviceAddress);
rfm9XDevice.OnTransmit += Rfm9XDevice_OnTransmit;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
while (true)
{
sensor.Update();
Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-TX T:{sensor.Temperature:0.0}C H:{sensor.Humidity:0}%");
string payload = $"t {sensor.Temperature:0.0},h {sensor.Humidity:0}";
Led.IsOn = true;
rfm9XDevice.Send(fieldGatewayAddress, Encoding.UTF8.GetBytes(payload));
Thread.Sleep(periodTime);
}
}
private void Rfm9XDevice_OnReceive(object sender, Rfm9XDevice.OnDataReceivedEventArgs e)
{
try
{
string addressText = UTF8Encoding.UTF8.GetString(e.Address);
string addressHex = BitConverter.ToString(e.Address);
string messageText = UTF8Encoding.UTF8.GetString(e.Data);
Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-RX PacketSnr {e.PacketSnr:0.0} Packet RSSI {e.PacketRssi}dBm RSSI {e.Rssi}dBm = {e.Data.Length} byte message {messageText}");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
private void Rfm9XDevice_OnTransmit(object sender, Rfm9XDevice.OnDataTransmitedEventArgs e)
{
Led.IsOn = false;
Console.WriteLine("{0:HH:mm:ss}-TX Done", DateTime.Now);
}
}
}
The Meadow platform is a work in progress (Jan 2020) so I haven’t put any effort into minimising power consumption but will revisit this in a future post.
Meadow device with Seeedstudio SHT31 temperature & humidity sensorMeadow sensor data in Field Gateway ETW loggingMeadow Sensor data in Azure IoT Central
Grove – 4 pin Female Jumper to Grove 4 pin Conversion Cable USD3.90
Armtronix device with Seeedstudio temperature & humidity sensor
I used a modified version of my Arduino client code which worked after I got the pins sorted and the female jumper sockets in the right order.
/*
Copyright ® 2019 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>
#include <LoRa.h>
#include <TH02_dev.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[] = {"ArmTronix01"};
const float FieldGatewayFrequency = 915000000.0;
const byte FieldGatewaySyncWord = 0x12 ;
// Payload configuration
const int ChipSelectPin = PA4;
const int InterruptPin = PA11;
const int ResetPin = PC13;
// LoRa radio payload configuration
const byte SensorIdValueSeperator = ' ' ;
const byte SensorReadingSeperator = ',' ;
const int LoopSleepDelaySeconds = 30 ;
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(ChipSelectPin, ResetPin, InterruptPin);
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 & 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 & humidity & 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.print( "RFM9X/SX127X Payload len:");
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.print("PayloadHeader- ");
Serial.print( "To len:");
Serial.print( toAddressLength );
Serial.print( " From len:");
Serial.print( fromAddressLength );
Serial.print( " Header len:");
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(&payload[payloadLength], to, toAddressLength);
payloadLength += toAddressLength ;
// Copy the "From" into payload
memcpy(&payload[payloadLength], from, fromAddressLength);
payloadLength += fromAddressLength ;
}
void PayloadAdd( const char *sensorId, float value, byte decimalPlaces)
{
byte sensorIdLength = strlen( sensorId ) ;
#ifdef DEBUG_TELEMETRY
Serial.print("PayloadAdd-float ");
Serial.print( "SensorId:");
Serial.print( sensorId );
Serial.print( " Len:");
Serial.print( sensorIdLength );
Serial.print( " Value:");
Serial.print( value, decimalPlaces );
Serial.print( " payloadLen:");
Serial.print( payloadLength);
#endif
memcpy( &payload[payloadLength], sensorId, sensorIdLength) ;
payloadLength += sensorIdLength ;
payload[ payloadLength] = SensorIdValueSeperator;
payloadLength += 1 ;
payloadLength += strlen( dtostrf(value, -1, decimalPlaces, (char *)&payload[payloadLength]));
payload[ payloadLength] = SensorReadingSeperator;
payloadLength += 1 ;
#ifdef DEBUG_TELEMETRY
Serial.print( " payloadLen:");
Serial.println( payloadLength);
#endif
}
void PayloadAdd( const char *sensorId, int value )
{
byte sensorIdLength = strlen( sensorId ) ;
#ifdef DEBUG_TELEMETRY
Serial.print("PayloadAdd-int ");
Serial.print( "SensorId:");
Serial.print( sensorId );
Serial.print( " Len:");
Serial.print( sensorIdLength );
Serial.print( " Value:");
Serial.print( value );
Serial.print( " payloadLen:");
Serial.print( payloadLength);
#endif
memcpy( &payload[payloadLength], sensorId, sensorIdLength) ;
payloadLength += sensorIdLength ;
payload[ payloadLength] = SensorIdValueSeperator;
payloadLength += 1 ;
payloadLength += strlen( itoa( value,(char *)&payload[payloadLength],10));
payload[ payloadLength] = SensorReadingSeperator;
payloadLength += 1 ;
#ifdef DEBUG_TELEMETRY
Serial.print( " payloadLen:");
Serial.println( payloadLength);
#endif
}
void PayloadAdd( const char *sensorId, unsigned int value )
{
byte sensorIdLength = strlen( sensorId ) ;
#ifdef DEBUG_TELEMETRY
Serial.print("PayloadAdd-unsigned int ");
Serial.print( "SensorId:");
Serial.print( sensorId );
Serial.print( " Len:");
Serial.print( sensorIdLength );
Serial.print( " Value:");
Serial.print( value );
Serial.print( " payloadLen:");
Serial.print( payloadLength);
#endif
memcpy( &payload[payloadLength], sensorId, sensorIdLength) ;
payloadLength += sensorIdLength ;
payload[ payloadLength] = SensorIdValueSeperator;
payloadLength += 1 ;
payloadLength += strlen( utoa( value,(char *)&payload[payloadLength],10));
payload[ payloadLength] = SensorReadingSeperator;
payloadLength += 1 ;
#ifdef DEBUG_TELEMETRY
Serial.print( " payloadLen:");
Serial.println( payloadLength);
#endif
}
void PayloadReset()
{
byte fromAddressLength = payload[0] & 0xf ;
byte toAddressLength = payload[0] >> 4 ;
byte addressesLength = toAddressLength + fromAddressLength ;
payloadLength = addressesLength + 1;
#ifdef DEBUG_TELEMETRY
Serial.print("PayloadReset- ");
Serial.print( "To len:");
Serial.print( toAddressLength );
Serial.print( " From len:");
Serial.print( fromAddressLength );
Serial.print( " Header len:");
Serial.println( addressesLength );
#endif
}
To get the application to download I had to configure the board in the Arduino IDE
Then change the jumpers
Initially I had some problems deploying my software because I hadn’t followed the instructions (the wiki everyone referred to appeared to be offline) and run the installation batch file (New dev machine since my previous maple based project).
I configured the device to upload to my Azure IoT Hub/Azure IoT Central gateway and it has been running reliably for a couple of days.
Azure IoT Central temperature and humidity values
Initially I had some configuration problems but I contacted Armtronix support and they promptly provided a couple of updated links for product and device documentation.
Seeeduino Nano easySensors shield and Grove VOC & eCO2 Sensor
Seeeduino Nano devices have a single on-board I2C socket which meant I didn’t need a Grove Shield for Arduino Nano which reduced the size and cost of the sensor node.
My first attempt failed with an issues accessing an Analog port to read the serial number from the Microchip ATSHA204 security chip. After looking at the Seeed SGP30 library source code (based on Sensiron samples) I think the my Nano device was running out of memory. I then searched for other Arduino compatible SGP30 libraries and rebuilt he application with the one from Sparkfun,
/*
Copyright ® 2019 August 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
Seeedstudio Grove - VOC and eCO2 Gas Sensor (SGP30)
https://www.seeedstudio.com/Grove-VOC-and-eCO2-Gas-Sensor-SGP30-p-3071.html
Seeeduino Nano
https://www.seeedstudio.com/Seeeduino-Nano-p-4111.html
Polycarbonate enclosure approx 3.5" x 4.5"
2 x Cable glands
1 x Grommet to seal SMA antenna connector
3M command adhesive strips to hold battery & device in place
*/
#include <stdlib.h>
#include "SparkFun_SGP30_Arduino_Library.h"
#include <LoRa.h>
#include <sha204_library.h>
//#define DEBUG
//#define DEBUG_TELEMETRY
//#define DEBUG_LORA
#define DEBUG_VOC_AND_CO2
#define UNITS_VOC "ppb"
#define UNITS_CO2 "ppm"
// LoRa field gateway configuration (these settings must match your field gateway)
const byte DeviceAddressMaximumLength = 15 ;
const char FieldGatewayAddress[] = {"LoRaIoT1"};
const float FieldGatewayFrequency = 915000000.0;
const byte FieldGatewaySyncWord = 0x12 ;
// Payload configuration
const int ChipSelectPin = 10;
const int ResetPin = 9;
const int InterruptPin = 2;
// LoRa radio payload configuration
const byte SensorIdValueSeperator = ' ' ;
const byte SensorReadingSeperator = ',' ;
const unsigned long SensorUploadDelay = 60000;
// ATSHA204 secure authentication, validation with crypto and hashing (currently only using for unique serial number)
const byte Atsha204Port = A3;
atsha204Class sha204(Atsha204Port);
const byte DeviceSerialNumberLength = 9 ;
byte deviceSerialNumber[DeviceSerialNumberLength] = {""};
SGP30 mySensor; //create an object of the SGP30 class
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.print("Field gateway:");
Serial.print(FieldGatewayAddress ) ;
Serial.print(" Frequency:");
Serial.print( FieldGatewayFrequency,0 ) ;
Serial.print("MHz SyncWord:");
Serial.print( FieldGatewaySyncWord ) ;
Serial.println();
// Retrieve the serial number then display it nicely
if(sha204.getSerialNumber(deviceSerialNumber))
{
Serial.println("sha204.getSerialNumber failed");
while (true); // Drop into endless loop requiring restart
}
Serial.print("SNo:");
DisplayHex( deviceSerialNumber, DeviceSerialNumberLength);
Serial.println();
Serial.println("LoRa setup start");
// override the default chip select and reset pins
LoRa.setPins(ChipSelectPin, ResetPin, InterruptPin);
if (!LoRa.begin(FieldGatewayFrequency))
{
Serial.println("LoRa begin failed");
while (true); // Drop into endless loop requiring restart
}
// Need to do this so field gateway 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 DF Robot SHT20, temperature & humidity sensor
Serial.println("SGP30 setup start");
Wire.begin();
if(mySensor.begin() == false)
{
Serial.println("SQP-30 initialisation failed");
while (true); // Drop into endless loop requiring restart
}
mySensor.initAirQuality();
delay(1000);
Serial.println("SGP30 setup done");
PayloadHeader((byte *)FieldGatewayAddress,strlen(FieldGatewayAddress), deviceSerialNumber, DeviceSerialNumberLength);
Serial.println("Setup done");
Serial.println();
}
void loop()
{
unsigned long currentMilliseconds = millis();
Serial.println("Loop called");
mySensor.measureAirQuality();
PayloadReset();
PayloadAdd( "v", mySensor.TVOC, false);
PayloadAdd( "c", mySensor.CO2, false);
#ifdef DEBUG_VOC_AND_CO2
Serial.print("VoC:");
Serial.print( mySensor.TVOC ) ;
Serial.print( UNITS_VOC ) ;
Serial.print(" Co2:");
Serial.print( mySensor.CO2 ) ;
Serial.println( UNITS_CO2 ) ;
#endif
#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(SensorUploadDelay - (millis() - currentMilliseconds ));
}
void PayloadHeader( const byte *to, byte toAddressLength, const byte *from, byte fromAddressLength)
{
byte addressesLength = toAddressLength + fromAddressLength ;
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(&payload[payloadLength], to, toAddressLength);
payloadLength += toAddressLength ;
// Copy the "From" into payload
memcpy(&payload[payloadLength], from, fromAddressLength);
payloadLength += fromAddressLength ;
}
void PayloadAdd( const char *sensorId, float value, byte decimalPlaces, bool last)
{
byte sensorIdLength = strlen( sensorId ) ;
memcpy( &payload[payloadLength], sensorId, sensorIdLength) ;
payloadLength += sensorIdLength ;
payload[ payloadLength] = SensorIdValueSeperator;
payloadLength += 1 ;
payloadLength += strlen( dtostrf(value, -1, decimalPlaces, (char *)&payload[payloadLength]));
if (!last)
{
payload[ payloadLength] = SensorReadingSeperator;
payloadLength += 1 ;
}
#ifdef DEBUG_TELEMETRY
Serial.print("PayloadAdd float-payloadLength:");
Serial.print( payloadLength);
Serial.println( );
#endif
}
void PayloadAdd( char *sensorId, int value, bool last )
{
byte sensorIdLength = strlen(sensorId) ;
memcpy(&payload[payloadLength], sensorId, sensorIdLength) ;
payloadLength += sensorIdLength ;
payload[ payloadLength] = SensorIdValueSeperator;
payloadLength += 1 ;
payloadLength += strlen(itoa( value,(char *)&payload[payloadLength],10));
if (!last)
{
payload[ payloadLength] = SensorReadingSeperator;
payloadLength += 1 ;
}
#ifdef DEBUG_TELEMETRY
Serial.print("PayloadAdd int-payloadLength:" );
Serial.print(payloadLength);
Serial.println( );
#endif
}
void PayloadAdd( char *sensorId, unsigned int value, bool last )
{
byte sensorIdLength = strlen(sensorId) ;
memcpy(&payload[payloadLength], sensorId, sensorIdLength) ;
payloadLength += sensorIdLength ;
payload[ payloadLength] = SensorIdValueSeperator;
payloadLength += 1 ;
payloadLength += strlen(utoa( value,(char *)&payload[payloadLength],10));
if (!last)
{
payload[ payloadLength] = SensorReadingSeperator;
payloadLength += 1 ;
}
#ifdef DEBUG_TELEMETRY
Serial.print("PayloadAdd uint-payloadLength:");
Serial.print(payloadLength);
Serial.println( );
#endif
}
void PayloadReset()
{
byte fromAddressLength = payload[0] & 0xf ;
byte toAddressLength = payload[0] >> 4 ;
payloadLength = toAddressLength + fromAddressLength + 1;
}
void DisplayHex( byte *byteArray, byte length)
{
for (int i = 0; i < length ; i++)
{
// Add a leading zero
if ( byteArray[i] < 16)
{
Serial.print("0");
}
Serial.print(byteArray[i], HEX);
if ( i < (length-1)) // Don't put a - after last digit
{
Serial.print("-");
}
}
}
To configure the device in Azure IoT Central (similar process for Adafruit.IO, working on support for losant, and ubidots) I copied the SNo: from the Arduino development tool logging window and appended c for the CO2 parts per million (ppm), v for VOC parts per billion (ppb) unique serial number from the ATSHA204A chip. (N.B. pay attention to the case of the field names they are case sensitive)
Azure IoT Central configuration
Overall the performance of the VoC sensor data is looking pretty positive, the eCO2 readings need some further investigation as they track the VOC levels. The large spike in the graph below is me putting an open vivid marker on my desk near the sensor.
Seeeduino Nano, EasySensors Shield & DF Robot Sensor test rig
The Seeeduino Nano devices I’m testing have a single on-board I2C socket which meant I didn’t need a Grove Shield for Arduino Nano which reduced the size and cost of the sensor node.
To configure the device in Azure IoT Central (similar process for Adafruit.IO, working on support for losant,and ubidots I copied the SNo: from the Arduino development tool logging window and appended p10 for PM 1 value, p25 for PM2.5 value and p100 for PM10 value to the unique serial number from the ATSHA204A chip. (N.B. pay attention to the case of the field names they are case sensitive)
When I moved the sensor indoors it appeared to take a while to warm up and after a while the metal body still felt cold. The sensor element is surrounded by quite a bit of protective packaging for outdoors use and I that would have a bit more thermal inertia the than the lightweight indoor enclosure.
It would be good to run the sensor alongside a calibrated temperature & humidity sensor to see how accurate and responsive it is.
The Seeeduino Nano devices I’m testing have a single on-board I2C socket which meant I didn’t need a Grove Shield for Arduino Nano which reduced the size and cost of the sensor node.
After looking at the demo application I stripped out the checksum code and threw the rest away. In my test harness I have extracted only the PM1.0/PM2.5/PM10.0 (concentration CF=1, Standard particulate) in μg/ m3 values from the sensor response payload.
/*
Copyright ® 2019 August 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>
#include <LoRa.h>
#include <sha204_library.h>
#include "Seeed_HM330X.h"
//#define DEBUG
//#define DEBUG_TELEMETRY
//#define DEBUG_LORA
const byte SensorPayloadLength = 28 ;
const byte SensorPayloadBufferSize = 29 ;
const byte SensorPayloadPM1_0Position = 4;
const byte SensorPayloadPM2_5Position = 6;
const byte SensorPayloadPM10_0Position = 8;
HM330X sensor;
byte SensorPayload[SensorPayloadBufferSize];
// LoRa field gateway configuration (these settings must match your field gateway)
const byte DeviceAddressMaximumLength = 15 ;
const char FieldGatewayAddress[] = {"LoRaIoT1"};
const float FieldGatewayFrequency = 915000000.0;
const byte FieldGatewaySyncWord = 0x12 ;
// Payload configuration
const int ChipSelectPin = 10;
const int ResetPin = 9;
const int InterruptPin = 2;
// LoRa radio payload configuration
const byte SensorIdValueSeperator = ' ' ;
const byte SensorReadingSeperator = ',' ;
const unsigned long SensorUploadDelay = 60000;
// ATSHA204 secure authentication, validation with crypto and hashing (currently only using for unique serial number)
const byte Atsha204Port = A3;
atsha204Class sha204(Atsha204Port);
const byte DeviceSerialNumberLength = 9 ;
byte deviceSerialNumber[DeviceSerialNumberLength] = {""};
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.print("Field gateway:");
Serial.print(FieldGatewayAddress ) ;
Serial.print(" Frequency:");
Serial.print( FieldGatewayFrequency,0 ) ;
Serial.print("MHz SyncWord:");
Serial.print( FieldGatewaySyncWord ) ;
Serial.println();
// Retrieve the serial number then display it nicely
if(sha204.getSerialNumber(deviceSerialNumber))
{
Serial.println("sha204.getSerialNumber failed");
while (true); // Drop into endless loop requiring restart
}
Serial.print("SNo:");
DisplayHex( deviceSerialNumber, DeviceSerialNumberLength);
Serial.println();
Serial.println("LoRa setup start");
// override the default chip select and reset pins
LoRa.setPins(ChipSelectPin, ResetPin, InterruptPin);
if (!LoRa.begin(FieldGatewayFrequency))
{
Serial.println("LoRa begin failed");
while (true); // Drop into endless loop requiring restart
}
// Need to do this so field gateway 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 CO2, temperature & humidity sensor
Serial.println("HM3301 setup start");
if(sensor.init())
{
Serial.println("HM3301 init failed");
while (true); // Drop into endless loop requiring restart
}
delay(100);
Serial.println("HM3301 setup done");
PayloadHeader((byte *)FieldGatewayAddress,strlen(FieldGatewayAddress), deviceSerialNumber, DeviceSerialNumberLength);
Serial.println("Setup done");
Serial.println();
}
void loop()
{
unsigned long currentMilliseconds = millis();
byte sum=0;
short pm1_0 ;
short pm2_5 ;
short pm10_0 ;
Serial.println("Loop called");
if(sensor.read_sensor_value(SensorPayload,SensorPayloadBufferSize) == NO_ERROR)
{
// Calculate then validate the payload "checksum"
for(int i=0;i<SensorPayloadLength;i++)
{
sum+=SensorPayload[i];
}
if(sum!=SensorPayload[SensorPayloadLength])
{
Serial.println("Invalid checksum");
return;
}
PayloadReset();
pm1_0 = (u16)SensorPayload[SensorPayloadPM1_0Position]<<8|SensorPayload[SensorPayloadPM1_0Position+1];
Serial.print("PM1.5: ");
Serial.print(pm1_0);
Serial.println("ug/m3 ") ;
PayloadAdd( "P10", pm1_0, false);
pm2_5 = (u16)SensorPayload[SensorPayloadPM2_5Position]<<8|SensorPayload[SensorPayloadPM2_5Position+1];
Serial.print("PM2.5: ");
Serial.print(pm2_5);
Serial.println("ug/m3 ") ;
PayloadAdd( "P25", pm2_5, 1, false);
pm10_0 = (u16)SensorPayload[SensorPayloadPM10_0Position]<<8|SensorPayload[SensorPayloadPM10_0Position+1];
Serial.print("PM10.0: ");
Serial.print(pm10_0);
Serial.println("ug/m3 ");
PayloadAdd( "P100", pm10_0, 0, true) ;
#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(SensorUploadDelay - (millis() - currentMilliseconds ));
}
void PayloadHeader( const byte *to, byte toAddressLength, const byte *from, byte fromAddressLength)
{
byte addressesLength = toAddressLength + fromAddressLength ;
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(&payload[payloadLength], to, toAddressLength);
payloadLength += toAddressLength ;
// Copy the "From" into payload
memcpy(&payload[payloadLength], from, fromAddressLength);
payloadLength += fromAddressLength ;
}
void PayloadAdd( const char *sensorId, float value, byte decimalPlaces, bool last)
{
byte sensorIdLength = strlen( sensorId ) ;
memcpy( &payload[payloadLength], sensorId, sensorIdLength) ;
payloadLength += sensorIdLength ;
payload[ payloadLength] = SensorIdValueSeperator;
payloadLength += 1 ;
payloadLength += strlen( dtostrf(value, -1, decimalPlaces, (char *)&payload[payloadLength]));
if (!last)
{
payload[ payloadLength] = SensorReadingSeperator;
payloadLength += 1 ;
}
#ifdef DEBUG_TELEMETRY
Serial.print("PayloadAdd float-payloadLength:");
Serial.print( payloadLength);
Serial.println( );
#endif
}
void PayloadAdd( char *sensorId, int value, bool last )
{
byte sensorIdLength = strlen(sensorId) ;
memcpy(&payload[payloadLength], sensorId, sensorIdLength) ;
payloadLength += sensorIdLength ;
payload[ payloadLength] = SensorIdValueSeperator;
payloadLength += 1 ;
payloadLength += strlen(itoa( value,(char *)&payload[payloadLength],10));
if (!last)
{
payload[ payloadLength] = SensorReadingSeperator;
payloadLength += 1 ;
}
#ifdef DEBUG_TELEMETRY
Serial.print("PayloadAdd int-payloadLength:" );
Serial.print(payloadLength);
Serial.println( );
#endif
}
void PayloadAdd( char *sensorId, unsigned int value, bool last )
{
byte sensorIdLength = strlen(sensorId) ;
memcpy(&payload[payloadLength], sensorId, sensorIdLength) ;
payloadLength += sensorIdLength ;
payload[ payloadLength] = SensorIdValueSeperator;
payloadLength += 1 ;
payloadLength += strlen(utoa( value,(char *)&payload[payloadLength],10));
if (!last)
{
payload[ payloadLength] = SensorReadingSeperator;
payloadLength += 1 ;
}
#ifdef DEBUG_TELEMETRY
Serial.print("PayloadAdd uint-payloadLength:");
Serial.print(payloadLength);
Serial.println( );
#endif
}
void PayloadReset()
{
byte fromAddressLength = payload[0] & 0xf ;
byte toAddressLength = payload[0] >> 4 ;
payloadLength = toAddressLength + fromAddressLength + 1;
}
void DisplayHex( byte *byteArray, byte length)
{
for (int i = 0; i < length ; i++)
{
// Add a leading zero
if ( byteArray[i] < 16)
{
Serial.print("0");
}
Serial.print(byteArray[i], HEX);
if ( i < (length-1)) // Don't put a - after last digit
{
Serial.print("-");
}
}
}
To configure the device in Azure IoT Central (similar process for Adafruit.IO, working on support for losant, and ubidots) I copied the SNo: from the Arduino development tool logging window and appended p10 for PM 1 value, p25 for PM2.5 value and p100 for PM10 value to the unique serial number from the ATSHA204A chip. (N.B. pay attention to the case of the field names they are case sensitive)
Azure IoT Central telemetry configuration
The rapidly settled into a narrow range of readings, but spiked when I took left it outside (winter in New Zealand) and the values spiked when food was being cooked in the kitchen which is next door to my office.
It would be good to run the sensor alongside a professional particulates monitor so the values could be compared and used to adjust the readings of the Grove sensor if necessary.
Hour of PM1, PM2.5 & PM10 readings in my office early eveningCO2 and particulates values while outside on my deck from 10:30pm to 11:30pm
Seeeduino Nano devices have a single on-board I2C socket which meant I didn’t need a Grove Shield for Arduino Nano which reduced the size and cost of the sensor node.
I downloaded the seeedstudio wiki example calibration code, compiled and uploaded it to one of my Seeeduino Nano devices. When activated for the first time a period of minimum 7 days is needed so that the sensor algorithm can find its initial parameter set. During this period the sensor has to be exposed to fresh air for at least 1 hour every day.
During the calibration process I put the device in my garage and left the big door open for at least an hour every day. Once the sensor was calibrated I bought it inside at put it on the bookcase in my office.
To configure the device in Azure IoT Central (similar process for Adafruit.IO, working on support for losant, and ubidots) I copied the SNo: from the Arduino development tool logging window and appended c for the CO2 parts per million (ppm), h for the humidity % and t for the temperature °C to the unique serial number from the ATSHA204A chip. (N.B. pay attention to the case of the field names they are case sensitive)
Azure IoT Central telemetry configuration
Overall the performance of the sensor is looking pretty positive, the CO2 levels fluctuate in a acceptable range (based on office occupancy), and the temperature + humidity readings track quite closely to the other two sensor nodes in my office. The only issue so far is my lack of USB-C cables to power the devices in the field
CO2, Humidity and Temperature in my office for a day
Grove – 4 pin Female Jumper to Grove 4 pin Conversion Cable USD3.90
The two sockets on the main board aren’t Grove compatible so I used the 4 pin female to Grove 4 pin conversion cable to connect the temperature and humidity sensor.
STM32 Blue Pill LoRaWAN node test rig
I used a modified version of my Arduino client code which worked after I got the pin reset pin sorted and the female sockets in the right order.
/*
Copyright ® 2019 July 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.
Adapted from LoRa Duplex communication with Sync Word
Sends temperature & humidity data from Seeedstudio
https://www.seeedstudio.com/Grove-Temperature-Humidity-Sensor-High-Accuracy-Min-p-1921.html
To my Windows 10 IoT Core RFM 9X library
https://blog.devmobile.co.nz/2018/09/03/rfm9x-iotcore-payload-addressing/
*/
#include <itoa.h>
#include <SPI.h>
#include <LoRa.h>
#include <TH02_dev.h>
#define DEBUG
//#define DEBUG_TELEMETRY
//#define DEBUG_LORA
// LoRa field gateway configuration (these settings must match your field gateway)
const char DeviceAddress[] = {"BLUEPILL"};
// Azure IoT Hub FieldGateway
const char FieldGatewayAddress[] = {"LoRaIoT1"};
const float FieldGatewayFrequency = 915000000.0;
const byte FieldGatewaySyncWord = 0x12 ;
// Bluepill hardware configuration
const int ChipSelectPin = PA4;
const int InterruptPin = PA0;
const int ResetPin = -1;
// LoRa radio payload configuration
const byte SensorIdValueSeperator = ' ' ;
const byte SensorReadingSeperator = ',' ;
const byte PayloadSizeMaximum = 64 ;
byte payload[PayloadSizeMaximum];
byte payloadLength = 0 ;
const int LoopDelaySeconds = 300 ;
// Sensor configuration
const char SensorIdTemperature[] = {"t"};
const char SensorIdHumidity[] = {"h"};
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(ChipSelectPin, ResetPin, InterruptPin);
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.");
PayloadHeader((byte*)FieldGatewayAddress, strlen(FieldGatewayAddress), (byte*)DeviceAddress, strlen(DeviceAddress));
// Configure the Seeedstudio TH02 temperature & humidity sensor
Serial.println("TH02 setup");
TH02.begin();
delay(100);
Serial.println("TH02 Setup done");
Serial.println("Setup done");
}
void loop() {
// read the value from the sensor:
double temperature = TH02.ReadTemperature();
double humidity = TH02.ReadHumidity();
Serial.print("Humidity: ");
Serial.print(humidity, 0);
Serial.print(" %\t");
Serial.print("Temperature: ");
Serial.print(temperature, 1);
Serial.println(" *C");
PayloadReset();
PayloadAdd(SensorIdHumidity, humidity, 0) ;
PayloadAdd(SensorIdTemperature, temperature, 1) ;
LoRa.beginPacket();
LoRa.write(payload, payloadLength);
LoRa.endPacket();
Serial.println("Loop done");
delay(LoopDelaySeconds * 1000);
}
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(&payload[payloadLength], to, toAddressLength);
payloadLength += toAddressLength ;
// Copy the "From" into payload
memcpy(&payload[payloadLength], from, fromAddressLength);
payloadLength += fromAddressLength ;
}
void PayloadAdd( const 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( &payload[payloadLength], sensorId, sensorIdLength) ;
payloadLength += sensorIdLength ;
payload[ payloadLength] = SensorIdValueSeperator;
payloadLength += 1 ;
payloadLength += strlen( dtostrf(value, -1, decimalPlaces, (char *)&payload[payloadLength]));
payload[ payloadLength] = SensorReadingSeperator;
payloadLength += 1 ;
#ifdef DEBUG_TELEMETRY
Serial.print( " payloadLength:");
Serial.print( payloadLength);
Serial.println( );
#endif
}
void PayloadAdd( const 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( &payload[payloadLength], sensorId, sensorIdLength) ;
payloadLength += sensorIdLength ;
payload[ payloadLength] = SensorIdValueSeperator;
payloadLength += 1 ;
payloadLength += strlen( itoa( value, (char *)&payload[payloadLength], 10));
payload[ payloadLength] = SensorReadingSeperator;
payloadLength += 1 ;
#ifdef DEBUG_TELEMETRY
Serial.print( " payloadLength:");
Serial.print( payloadLength);
Serial.println( );
#endif
}
void PayloadAdd( const 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( &payload[payloadLength], sensorId, sensorIdLength) ;
payloadLength += sensorIdLength ;
payload[ payloadLength] = SensorIdValueSeperator;
payloadLength += 1 ;
payloadLength += strlen( utoa( value, (char *)&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] & 0xf ;
byte toAddressLength = payload[0] >> 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
}
To get the application to compile I also had to include itoa.h rather than stdlib.h.
maple_loader v0.1
Resetting to bootloader via DTR pulse
[Reset via USB Serial Failed! Did you select the right serial port?]
Searching for DFU device [1EAF:0003]...
Assuming the board is in perpetual bootloader mode and continuing to attempt dfu programming...
dfu-util - (C) 2007-2008 by OpenMoko Inc.
Initially I had some problems deploying my software because I hadn’t followed the instructions and run the installation batch file.
I configured the device to upload to my Azure IoT Hub/Azure IoT Central gateway and after getting the device name configuration right it has been running reliably for a couple of days
Azure IoT Central Temperature and humidity
The device was sitting outside on the deck and rapid increase in temperature is me bringing it inside.
// <copyright file="client.cs" company="devMobile Software">
// Copyright ® 2019 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"
//
// </copyright>
namespace devMobile.IoT.Nexus.FieldGateway
{
using System;
using System.Text;
using System.Threading;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using devMobile.IoT.NetMF.ISM;
using devMobile.NetMF.Sensor;
using IngenuityMicro.Nexus;
class NexusClient
{
private Rfm9XDevice rfm9XDevice;
private readonly TimeSpan dueTime = new TimeSpan(0, 0, 15);
private readonly TimeSpan periodTime = new TimeSpan(0, 0, 60);
private readonly SiliconLabsSI7005 sensor = new SiliconLabsSI7005();
private readonly Led _led = new Led();
private readonly byte[] fieldGatewayAddress = Encoding.UTF8.GetBytes("LoRaIoT1");
private readonly byte[] deviceAddress = Encoding.UTF8.GetBytes("Nexus915");
public NexusClient()
{
rfm9XDevice = new Rfm9XDevice(SPI.SPI_module.SPI3, (Cpu.Pin)28, (Cpu.Pin)15, (Cpu.Pin)26);
_led.Set(0, 0, 0);
}
public void Run()
{
rfm9XDevice.Initialise(frequency: 915000000, paBoost: true, rxPayloadCrcOn: true);
rfm9XDevice.Receive(deviceAddress);
rfm9XDevice.OnDataReceived += rfm9XDevice_OnDataReceived;
rfm9XDevice.OnTransmit += rfm9XDevice_OnTransmit;
Timer humidityAndtemperatureUpdates = new Timer(HumidityAndTemperatureTimerProc, null, dueTime, periodTime);
Thread.Sleep(Timeout.Infinite);
}
private void HumidityAndTemperatureTimerProc(object state)
{
_led.Set(0, 128, 0);
double humidity = sensor.Humidity();
double temperature = sensor.Temperature();
Debug.Print(DateTime.UtcNow.ToString("hh:mm:ss") + " H:" + humidity.ToString("F1") + " T:" + temperature.ToString("F1"));
rfm9XDevice.Send(fieldGatewayAddress, Encoding.UTF8.GetBytes("t " + temperature.ToString("F1") + ",H " + humidity.ToString("F0")));
}
void rfm9XDevice_OnTransmit()
{
_led.Set(0, 0, 0);
Debug.Print("Transmit-Done");
}
void rfm9XDevice_OnDataReceived(byte[] address, float packetSnr, int packetRssi, int rssi, byte[] data)
{
try
{
string messageText = new string(UTF8Encoding.UTF8.GetChars(data));
string addressText = new string(UTF8Encoding.UTF8.GetChars(address));
Debug.Print(DateTime.UtcNow.ToString("HH:MM:ss") + "-Rfm9X PacketSnr " + packetSnr.ToString("F1") + " Packet RSSI " + packetRssi + "dBm RSSI " + rssi + "dBm = " + data.Length + " byte message " + @"""" + messageText + @"""");
}
catch (Exception ex)
{
Debug.Print(ex.Message);
}
}
}
}
Overall the development process was good with no modifications to my RFM9X.NetMF library or SI7005 library (bar removing a Netduino I2C work around) required
Nexus device with Seeedstudio Temperature & Humidity SensorsNexus Sensor data in Azure IoT Hub Field Gateway ETW LoggingNexus temperature & humidity data displayed in Azure IoT Central
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.
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.