.NET nanoFramework SHT20 library on Github

The full source code (just need to do readme) of my .NET nanoFramework Sensirion SHT20 temperature and humidity(Waterproof) library is now available on GitHub. I have tested the library and sample application with Sparkfun Thing Plus and ST Micro STM32F7691 Discovery devices. (I can validate on more platform configurations if there is interest).

Important: make sure you setup the I2C pins especially on ESP32 Devices before creating the I2cDevice,

SHT20 +STM32F769 Discovery test rig

The .NET nanoFramework device libraries use a TryGet… pattern to retrieve sensor value, this library throws an exception if reading a sensor value fails. I’m not certain which approach is “better” as reading Sensirion SHT20 temperature and humidity(Waterproof) has never failed The only time reading a value failed was when I unplugged the device which I think is “exceptional”.

//---------------------------------------------------------------------------------
// Copyright (c) March 2023, 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.
//
// nanoff --target ST_STM32F769I_DISCOVERY --update 
// nanoff --platform ESP32 --serialport COM7 --update
//
//---------------------------------------------------------------------------------
#define ST_STM32F769I_DISCOVERY 
//#define  SPARKFUN_ESP32_THING_PLUS
namespace devMobile.IoT.Device.Sht20
{
    using System;
    using System.Device.I2c;
    using System.Threading;

#if SPARKFUN_ESP32_THING_PLUS
    using nanoFramework.Hardware.Esp32;
#endif

    class Program
    {
        static void Main(string[] args)
        {
            const int busId = 1;

            Thread.Sleep(5000);

#if SPARKFUN_ESP32_THING_PLUS
            Configuration.SetPinFunction(Gpio.IO23, DeviceFunction.I2C1_DATA);
            Configuration.SetPinFunction(Gpio.IO22, DeviceFunction.I2C1_CLOCK);
#endif

            I2cConnectionSettings i2cConnectionSettings = new(busId, Sht20.DefaultI2cAddress);

            using I2cDevice i2cDevice = I2cDevice.Create(i2cConnectionSettings);
            {
                using (Sht20 sht20 = new Sht20(i2cDevice))
                {
                    sht20.Reset();

                    while (true)
                    {
                        double temperature = sht20.Temperature();
                        double humidity = sht20.Humidity();
#if HEATER_ON_OFF
					    sht20.HeaterOn();
					    Console.WriteLine($"{DateTime.Now:HH:mm:ss} HeaterOn:{sht20.IsHeaterOn()}");
#endif
                        Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss} Temperature:{temperature:F1}°C Humidity:{humidity:F0}% HeaterOn:{sht20.IsHeaterOn()}");
#if HEATER_ON_OFF
					    sht20.HeaterOff();
					    Console.WriteLine($"{DateTime.Now:HH:mm:ss} HeaterOn:{sht20.IsHeaterOn()}");
#endif
                        Thread.Sleep(1000);
                    }
                }
            }
        }
    }
}

I’m going to soak test the library for a week to check that is working okay, then most probably refactor the code so it can be added to the nanoFramework IoT.Device Library repository.

.NET nanoFramework SHT20 Basic connectivity

A couple of years ago I wrote a .NET Core library for the Sensirion SHT20 temperature and humidity(Waterproof) sensor from DFRobot. This .NET nanoFramework version was “inspired” by the .NET Core library version, though I have added some message validation functionality.

DF Robot SHT20 Waterproof sensor

My test setup is a simple .NET nanoFramework console application running on an STM32F7691 Discovery board.

Discovery STM32F769 + SHT20 Testrig

The SH20DeviceI2C application has lots of magic numbers from the SHT20 datasheet and was just a tool for exploring how the sensor works.

 public static void Main()
{
    I2cConnectionSettings i2cConnectionSettings = new(1, 0x40);

    // i2cDevice.Dispose in final program
    I2cDevice i2cDevice = I2cDevice.Create(i2cConnectionSettings);

    while (true)
    {
        byte[] readBuffer = new byte[3] { 0, 0, 0 };

        // First temperature
        i2cDevice.WriteByte(0xF3);

        //Thread.Sleep(50); // no go -46.8
        //Thread.Sleep(60);
        Thread.Sleep(70);
        //Thread.Sleep(90);
        //Thread.Sleep(110);

        i2cDevice.Read(readBuffer);

        ushort temperatureRaw = (ushort)(readBuffer[0] << 8);
        temperatureRaw += readBuffer[1];

        //Debug.WriteLine($"Raw {temperatureRaw}");

        double temperature = temperatureRaw * (175.72 / 65536.0) - 46.85;

        // Then read the Humidity
        i2cDevice.WriteByte(0xF5);

        //Thread.Sleep(50);  
        //Thread.Sleep(60);  
        Thread.Sleep(70);  
        //Thread.Sleep(90);  
        //Thread.Sleep(110);   
                
        i2cDevice.Read(readBuffer);

        ushort humidityRaw = (ushort)(readBuffer[0] << 8);
        humidityRaw += readBuffer[1];

        //Debug.WriteLine($"Raw {humidityRaw}");

        double humidity = humidityRaw * (125.0 / 65536.0) - 6.0;

        //Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss} Temperature:{temperature:F1}°C");
        //Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss} Humidity:{humidity:F0}%");
        Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss} Temperature:{temperature:F1}°C Humidity:{humidity:F0}%");

        Thread.Sleep(1000);
    }
}

While tinkering with the sensor I found that having a short delay between initiating the temperature reading (TemperatureNoHold = 0xF3 was used so as not to hang up the I2C bus) and reading the value was important.

Temperature value without Thread.Sleep

When I ran the application without a Thread.Sleep(70) the temperature and/or humidity the values were incorrect and sometimes quite random.

Temperature value with Thread.Sleep(70)
Humidity value without Thread.Sleep
Humidity value with Thread.Sleep(70)
Temperature and Humidity values with Thread.Sleep(70)

The .NET Core library didn’t validate the message payload Cyclic Redundancy Check (CRC) so I have added that in this version

void CheckCrc(byte[] bytes, byte bytesLen, byte checksum)
{
    var crc = 0;

    for (var i = 0; i < bytesLen; i++)
    {
        crc ^= bytes[i];
        for (var bit = 8; bit > 0; --bit)
        {
            crc = ((crc & 0x80) == 0x80) ? ((crc << 1) ^ CrcPolynomial) : (crc << 1);
        }
    }

    if (crc != checksum)
    {
        throw new Exception("CRC Error");
    }
}

The CheckCrc is called in Temperature and Humidity methods.

public double Temperature()
{
    byte[] readBuffer = new byte[3] { 0, 0, 0 };
    if (_i2cDevice == null)
    {
        throw new ArgumentNullException(nameof(_i2cDevice));
    }

    _i2cDevice.WriteByte(TemperatureNoHold);

    Thread.Sleep(ReadingWaitmSec);

    _i2cDevice.Read(readBuffer);

    CheckCrc(readBuffer, 2, readBuffer[2]);

    ushort temperatureRaw = (ushort)(readBuffer[0] << 8);
    temperatureRaw += readBuffer[1];

    double temperature = temperatureRaw * (175.72 / 65536.0) - 46.85;

    return temperature;
}

I’m going to soak test the library for a week to check that is working okay, then refactor the code so it can be added to the nanoFramework IoT.Device Library repository.

Sensirion SHT 20 library for .NET Core 5.0

As part of project I needed to connect a Sensirion SHT20 driver to a.NET Core 5 application running on a Raspberry Pi so I wrote this library. For initial testing I used a DF Robot Waterproof SHT20 temperature and humidity sensor, Seeedstudio Gove Base Hat, Grove Screw Terminal, and a Grove – Universal 4 Pin Buckled 5cm Cable.

Sensirion SHT20 connected to Raspberry PI3

I have included sample application in the Github repository to show how to use the library

namespace devMobile.IoT.NetCore.Sensirion
{
	using System;
	using System.Device.I2c;
	using System.Threading;

	class Program
	{
		static void Main(string[] args)
		{
			// bus id on the raspberry pi 3
			const int busId = 1;

			I2cConnectionSettings i2cConnectionSettings = new(busId, Sht20.DefaultI2cAddress);

			using I2cDevice i2cDevice = I2cDevice.Create(i2cConnectionSettings);

			using (Sht20 sht20 = new Sht20(i2cDevice))
			{
				sht20.Reset();

				while (true)
				{
					double temperature = sht20.Temperature();
					double humidity = sht20.Humidity();

#if HEATER_ON_OFF
					sht20.HeaterOn();
					Console.WriteLine($"{DateTime.Now:HH:mm:ss} HeaterOn:{sht20.IsHeaterOn()}");
#endif
					Console.WriteLine($"{DateTime.Now:HH:mm:ss} Temperature:{temperature:F1}°C Humidity:{humidity:F0}% HeaterOn:{sht20.IsHeaterOn()}");
#if HEATER_ON_OFF
					sht20.HeaterOff();
					Console.WriteLine($"{DateTime.Now:HH:mm:ss} HeaterOn:{sht20.IsHeaterOn()}");
#endif

					Thread.Sleep(1000);
				}
			}
		}
	}
}

The Sensiron SHT20 has a heater which is intended to be used for functionality diagnosis – relative humidity drops upon rising temperature. The heater consumes about 5.5mW and provides a temperature increase of about 0.5 – 1.5°C.

Beware when the device is soft reset the heater bit is not cleared.

TTI V3 Gateway provisioning Dragino LHT65 Uplink

This very long post is about how to connect a Dragino LHT65 Temperature and Humidity sensor to Azure IoT Central using my TTI/TTN V3Azure IoT Connector and the Digital Twin Definition Language (DTDL).

Dragino LHT65 temperature and Humidity sensor

The first step was to add an application(dragino-lht65) in my The Things Industries(TTI) tenant

TTI/TTN application for my Dragino LHT65 devices
Adding devMobile as a collaborator on the new application
TTI Application API Key configuration

The new Application API Key used by the MQTTnet managed client only needs to have write downlink and read uplink traffic enabled.

FTDI Adapter and modified LHT64 cable

So I could reliably connect to my LHT65 devices to configure them I modified a programming cable so I could use it with a spare FTDI adaptor without jumper wires. Todo this I used a small jewelers screwdriver to “pop” out the VCC cable and move the transmit data line.

After entering the device password and checking the firmware version I used the AT+CFG command to display the device settings

AT+CFG: Print all configurations

[334428]***** UpLinkCounter= 0 *****
[334430]TX on freq 923200000 Hz at DR 2
[334804]txDone
[339807]RX on freq 923200000 Hz at DR 2
[339868]rxTimeOut
[340807]RX on freq 923200000 Hz at DR 2
[340868]rxTimeOut

Correct Password

Stop Tx events,Please wait for all configurations to print
Printf all config...
AT+DEUI=a8 .. .. .. .. .. .. d6
AT+DADDR=01......D6

AT+APPKEY=9d .. .. .. .. .. .. .. .. .. .. .. .. .. .. 2e
AT+NWKSKEY=f6 .. .. .. .. .. .. .. .. .. .. .. .. .. .. 69
AT+APPSKEY=4c 35 .. .. .. .. .. .. .. .. .. .. .. .. .. 3d
AT+APPEUI=a0 .. .. .. .. .. .. 00
AT+ADR=1
AT+TXP=0
AT+DR=0
AT+DCS=0
AT+PNM=1
AT+RX2FQ=923200000
AT+RX2DR=2
AT+RX1DL=1000
AT+RX2DL=2000
AT+JN1DL=5000
AT+JN2DL=6000
AT+NJM=1
AT+NWKID=00 00 00 00
AT+FCU=0
AT+FCD=0
AT+CLASS=A
AT+NJS=0
AT+RECVB=0:
AT+RECV=0:
AT+VER=v1.7 AS923

AT+CFM=0
AT+CFS=0
AT+SNR=0
AT+RSSI=0
AT+TDC=1200000
AT+PORT=2
AT+PWORD=123456
AT+CHS=0
AT+DATE=21/3/26 07:49:15
AT+SLEEP=0
AT+EXT=4,2
AT+RTP=20
AT+BAT=3120
AT+WMOD=0
AT+ARTEMP=-40,125
AT+CITEMP=1
Start Tx events OK


[399287]***** UpLinkCounter= 0 *****

[399289]TX on freq 923400000 Hz at DR 2

[399663]txDone

[404666]RX on freq 923400000 Hz at DR 2

[404726]rxTimeOut

[405666]RX on freq 923200000 Hz at DR 2

[405726]rxTimeOut

I copied the AppEUI and DevEUI for use on the TI Dragino LHT65 Register end device form provided by the TTI/TTN.

TTYI/TTN Dragino LHT65 Register end device

The Dragino LHT65 uses the DeviceEUI as the DeviceID which meant I had todo more redaction in my TTI/TTN and Azure Application Insights screen captures. The rules around the re-use of EndDevice ID were a pain in the arse(PITA) in my development focused tenant.

Dragino LHT 65 Device uplink payload formatter

The connector supports both uplink and downlink messages with JSON encoded payloads. The Dragino LHT65 has a vendor supplied formatter which is automatically configured when an EndDevice is created. The EndDevice formatter configuration can also be overridden at the Application level in the app.settings.json file.

Device Live Data Uplink Data Payload

Once an EndDevice is configured in TTI/TTN I usually use the “Live data Uplink Payload” to work out the decoded payload JSON property names and data types.

LHT65 Uplink only Azure IoT Central Device Template
LHT65 Device Template View Identity

For Azure IoT Central “automagic” provisioning the DTDLModelId has to be copied from the Azure IoT Central Template into the TTI/TTN EndDevice or app.settings.json file application configuration.

LHT65 Device Template copy DTDL @ID
TTI EndDevice configuring the DTDLV2 @ID at the device level

Configuring the DTDLV2 @ID at the TTI application level in the app.settings.json file

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Debug"
    },
    "ApplicationInsights": {
      "LogLevel": {
        "Default": "Debug"
      }
    }
  },

  "ProgramSettings": {
    "Applications": {
      "application1": {
        "AzureSettings": {
          "DeviceProvisioningServiceSettings": {
            "IdScope": "0ne...DD9",
            "GroupEnrollmentKey": "eFR...w=="
          }
        },
        "DTDLModelId": "dtmi:ttnv3connectorclient:FezduinoWisnodeV14x8;4",
        "MQTTAccessKey": "NNSXS.HCY...RYQ",
        "DeviceIntegrationDefault": false,
        "MethodSettings": {
          "Reboot": {
            "Port": 21,
            "Confirmed": true,
            "Priority": "normal",
            "Queue": "push"
          },
          "value_0": {
            "Port": 30,
            "Confirmed": true,
            "Priority": "normal",
            "Queue": "push"
          },
          "value_1": {
            "Port": 30,
            "Confirmed": true,
            "Priority": "normal",
            "Queue": "push"
          },
          "TemperatureOOBAlertMinimumAndMaximum": {
            "Port": 30,
            "Confirmed": true,
            "Priority": "normal",
            "Queue": "push"
          }
        }
      },
      "seeeduinolorawan": {
        "AzureSettings": {
          "DeviceProvisioningServiceSettings": {
            "IdScope": "0ne...DD9",
            "GroupEnrollmentKey": "AtN...g=="
          },
        },
        "DTDLModelId": "dtmi:ttnv3connectorclient:SeeeduinoLoRaWAN4cz;1",
        "MQTTAccessKey": "NNSXS.V44...42A",
        "DeviceIntegrationDefault": true,
        "DevicePageSize": 10
      },
      "dragino-lht65": {
        "AzureSettings": {
          "DeviceProvisioningServiceSettings": {
            "IdScope": "0ne...DD9",
            "GroupEnrollmentKey": "SLB...w=="
          }
        },
        "DTDLModelId": "dtmi:ttnv3connectorclient:DraginoLHT656w6;1",
        "MQTTAccessKey": "NNSXS.RIJ...NZQ",
        "DeviceIntegrationDefault": true,
        "DevicePageSize": 10
      }
    },
    "TheThingsIndustries": {
      "MqttServerName": "eu1.cloud.thethings.industries",
      "MqttClientId": "MQTTClient",
      "MqttAutoReconnectDelay": "00:00:05",
      "Tenant": "...-test",
      "ApiBaseUrl": "https://...-test.eu1.cloud.thethings.industries/api/v3",
      "ApiKey": "NNSXS.NR7...ZSA",
      "Collaborator": "devmobile",
      "DevicePageSize": 10,
      "DeviceIntegrationDefault": true
    }
  }
}

The Azure Device Provisioning Service(DPS) is configured at the TTI application level in the app.settings.json file. The IDScope and one of the Primary or Secondary Shared Access Signature(SAS) keys should be copied into DeviceProvisioningServiceSettings of an Application in the app.settings.json file. I usually set the “Automatically connect devices in this group” flag as part of the “automagic” provisioning process.

Azure IoT Central Group Enrollment Key
Then device templates need to be mapped to an Enrollment Group then Device Group.

For testing the connector application can be run locally with diagnostic information displayed in the application console window as it “automagically’ provisions devices and uploads telemetry data.

Connector application Diagnostics
Azure IoT Central Device list before my LHT65 device is “automagically” provisioned
Azure IoT Central Device list after my LHT65 device is “automagically” provisioned

One a device has been provisioned I check on the raw data display that all the fields I configured have been mapped correctly.

Azure IoT Central raw data display

I then created a dashboard to display the telemetry data from the LHT65 sensors.

Azure IoT Central dashboard displaying LHT65 temperature, humidity and battery voltage graphs.

The dashboard also has a few KPI displays which highlighted an issue which occurs a couple of times a month with the LHT65 onboard temperature sensor values (327.7°). I have connected Dragino technical support and have also been unable to find a way to remove the current an/or filter out future aberrant values.

Azure Application Insights logging

I also noticed that the formatting of the DeviceEUI values in the Application Insights logging was incorrect after trying to search for one of my Seeedstudio LoRaWAN device with its DeviceEUI.

MATH131 Numerical methods was useful

Back in 1986 in my second first year at the University of Canterbury I did “MATH131 Numerical Methods” which was a year of looking at why mathematics in FORTRAN, C, and Pascal sometimes didn’t return the result you were expecting…

While testing my GHI Electronics TinyCLR2 RAK Wireless RAK811 LoRaWAN client I noticed the temperature numbers didn’t quite match…

Visual Studio 2019 Debug output window
The Things Network Device Application Data tab

I have implemented my own Low Power Payload encoder in C# based on the sample Mbed C code

My translation of that code to C#

public void TemperatureAdd(byte channel, float celsius)
{
   if ((index + TemperatureSize) > buffer.Length)
   {
      throw new ApplicationException("TemperatureAdd insufficent buffer capacity");
   }

   short val = (short)(celsius * 10);

   buffer[index++] = channel;
   buffer[index++] = (byte)DataType.Temperature;
   buffer[index++] = (byte)(val >> 8);
   buffer[index++] = (byte)val;
}

After looking at the code I think the issues was most probably due to the representation of the constant 10(int32), 10.0(double), and 10.0f(single) . To confirm my theory I modified the client to send the temperature with the calculation done with three different constants.

Visual Studio 2019 Debug output window
The Things Network(TTN) Message Queue Telemetry Transport(MQTT) client

After some trial and error I settled on this C# code for my decoder

public void TemperatureAdd(byte channel, float celsius)
{
   if ((index + TemperatureSize) > buffer.Length)
   {
      throw new ApplicationException("TemperatureAdd insufficent buffer capacity");
   }

   short val = (short)(celsius * 10.0f);

   buffer[index++] = channel;
   buffer[index++] = (byte)DataType.Temperature;
   buffer[index++] = (byte)(val >> 8);
   buffer[index++] = (byte)val;
}

I don’t think this is specifically an issue with the TinyCLR V2 just with number type used for the constant.

Meadow LoRa Radio 915 MHz Payload Addressing client

This is a demo Wilderness Labs Meadow client that uploads temperature and humidity data to my Azure IoT Hubs/Central, AdaFruit.IO or MQTT on Raspberry PI field gateways.

Bill of materials (Prices Jan 2020).

//---------------------------------------------------------------------------------
// 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 sensor
Meadow sensor data in Field Gateway ETW logging
Meadow Sensor data in Azure IoT Central

Armtronix IA005 SX1276 loRa node

A month ago I ordered a pair of IA005: SX1276 Lora node STM32F103 devices from the Armtronix shop on Tindie for evaluation. At USD18 each these devices were competitively priced and I was interested in trialling another maple like device.

Bill of materials (Prices as at December 2019)

  • IA005 SX1276 loRa node USD36 (USD18 each)
  • Grove – Temperature&Humidity Sensor USD11.5
  • 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).

15:40:56.207 -> LoRa Setup done.
15:40:56.207 -> TH02 setup start
15:40:56.307 -> TH02 setup done
15:40:56.307 -> PayloadHeader- To len:8 From len:11 Header len:19
15:40:56.354 -> Setup done
15:40:56.354 -> 
15:40:56.354 -> Loop called
15:40:56.354 -> PayloadReset- To len:8 From len:11 Header len:19
15:40:56.408 -> T:23.9C 
15:40:56.408 -> PayloadAdd-float SensorId:T Len:1 Value:23.9 payloadLen:20 payloadLen:27
15:40:56.508 -> H:70% 
15:40:56.508 -> PayloadAdd-float SensorId:H Len:1 Value:70 payloadLen:27 payloadLen:32
15:40:56.608 -> RFM9X/SX127X Payload len:32 bytes
15:40:56.655 -> Loop done
15:40:56.655 -> 
15:41:26.647 -> Loop called
15:41:26.647 -> PayloadReset- To len:8 From len:11 Header len:19
15:41:26.684 -> T:24.0C 
15:41:26.730 -> PayloadAdd-float SensorId:T Len:1 Value:24.0 payloadLen:20 payloadLen:27
15:41:26.784 -> H:69% 
15:41:26.784 -> PayloadAdd-float SensorId:H Len:1 Value:69 payloadLen:27 payloadLen:32
15:41:26.884 -> RFM9X/SX127X Payload len:32 bytes
15:41:26.931 -> Loop done
15:41:26.931 -> 
15:41:56.904 -> Loop called
15:41:56.904 -> PayloadReset- To len:8 From len:11 Header len:19
15:41:56.948 -> T:24.1C 
15:41:56.982 -> PayloadAdd-float SensorId:T Len:1 Value:24.1 payloadLen:20 payloadLen:27
15:41:57.054 -> H:69% 
15:41:57.054 -> PayloadAdd-float SensorId:H Len:1 Value:69 payloadLen:27 payloadLen:32
15:41:57.157 -> RFM9X/SX127X Payload len:32 bytes
15:41:57.191 -> Loop done
15:41:57.191 -> 
15:42:27.211 -> Loop called
15:42:27.211 -> PayloadReset- To len:8 From len:11 Header len:19
15:42:27.258 -> T:24.1C 
15:42:27.258 -> PayloadAdd-float SensorId:T Len:1 Value:24.1 payloadLen:20 payloadLen:27
15:42:27.343 -> H:69% 
15:42:27.343 -> PayloadAdd-float SensorId:H Len:1 Value:69 payloadLen:27 payloadLen:32
15:42:27.427 -> RFM9X/SX127X Payload len:32 bytes
15:42:27.481 -> Loop done
15:42:27.481 -> 
15:42:57.504 -> Loop called
15:42:57.504 -> PayloadReset- To len:8 From len:11 Header len:19
15:42:57.504 -> T:24.1C 
15:42:57.550 -> PayloadAdd-float SensorId:T Len:1 Value:24.1 payloadLen:20 payloadLen:27
15:42:57.604 -> H:68% 
15:42:57.604 -> PayloadAdd-float SensorId:H Len:1 Value:68 payloadLen:27 payloadLen:32
15:42:57.704 -> RFM9X/SX127X Payload len:32 bytes
15:42:57.755 -> Loop done
15:42:57.755 -> 

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.

DF Robot Temperature & Humidity Sensor(SHT20) trial

In preparation for a project to build weather stations to place at a couple of local schools I purchased a DF Robot SHT20 Temperature & Humidity Sensor for evaluation.

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 test my setup I installed the DFRobot Arduino SHT20 library and downloaded a demo application to my device.

I started with my Easy Sensors Arduino Nano Radio Shield RFM69/95 Payload Addressing client and modified it to use the SHT20 sensor.

/*
  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

  DF Robot SHT20 Temperature & Humidity sensor   https://www.dfrobot.com/wiki/index.php/SHT20_I2C_Temperature_%26_Humidity_Sensor_(Waterproof_Probe)_SKU:_SEN0227  

  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 ufl to SMA connector
    3M command adhesive strips to hold battery & device in place
   
*/
#include <stdlib.h>
#include <DFRobot_SHT20.h>
#include <LoRa.h>
#include <sha204_library.h>

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

#define UNITS_HUMIDITY "%"
#define UNITS_TEMPERATURE "°c"

// 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] = {""};

// SHT20 Air temperature and humidity sensor
DFRobot_SHT20 sht20;

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("SHT20 setup start");  
  sht20.initSHT20();
  delay(100);
  sht20.checkSHT20();    
  Serial.println("SHT20 setup done");

  PayloadHeader((byte *)FieldGatewayAddress,strlen(FieldGatewayAddress), deviceSerialNumber, DeviceSerialNumberLength);

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


void loop()
{
  unsigned long currentMilliseconds = millis();  
  float temperature ;
  float humidity ;

  Serial.println("Loop called");

  PayloadReset();  

  humidity = sht20.readHumidity();          
  PayloadAdd( "h", humidity, 0, false);

  temperature = sht20.readTemperature();               
  PayloadAdd( "t", temperature, 1, false);
  
  #ifdef DEBUG_TEMPERATURE_AND_HUMIDITY  
    Serial.print("H:");
    Serial.print( humidity, 0 ) ;
    Serial.print( UNITS_HUMIDITY ) ;
    Serial.print("T:");
    Serial.print( temperature, 1 ) ;
    Serial.println( UNITS_TEMPERATURE ) ;
  #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("-");
    }
  }
}    

The code is available on GitHub.

20:52:09.656 -> Setup called
20:52:09.690 -> Field gateway:LoRaIoT1 Frequency:915000000MHz SyncWord:18
20:52:09.794 -> SNo:01-23-21-61-D6-D1-F5-86-EE
20:52:09.828 -> LoRa setup start
20:52:09.828 -> LoRa Setup done.
20:52:09.862 -> SHT20 setup start
20:52:09.932 -> End of battery: no
20:52:09.932 -> Heater enabled: no
20:52:09.965 -> Disable OTP reload: yes
20:52:09.999 -> SHT20 setup done
20:52:09.999 -> Setup done
20:52:09.999 -> 
20:52:09.999 -> Loop called
20:52:10.067 -> H:60%T:20.0°c
20:52:10.136 -> Loop done
20:52:10.136 -> 
20:53:09.915 -> Loop called
20:53:10.019 -> H:61%T:20.5°c
20:53:10.088 -> Loop done
20:53:10.088 -> 

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.

Bill of materials (prices as at August 2019)

  • Seeeduino Nano USD6.90
  • Grove Screw Terminal USD2.90
  • DF Robot SHT20 I2C Temperature & Humidity Sensor USD22.50
  • EasySensors Arduino Nano radio shield RFM95 USD15.00

STM32 Blue Pill LoRaWAN node

A few weeks ago I ordered an STM32 Blue Pill LoRaWAN node from the M2M Shop on Tindie for evaluation. I have bought a few M2M client devices including a Low power LoRaWan Node Model A328, and Low power LoRaWan Node Model B1284 for projects and they have worked well. This one looked interesting as I had never used a maple like device before.

Bill of materials (Prices as at July 2019)

  • STM32 Blue Pill LoRaWAN node USD21
  • Grove – Temperature&Humidity Sensor USD11.5
  • 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.

14:03:56.946 -> Setup called
14:03:56.946 -> LoRa setup start
14:03:56.946 -> LoRa setup done.
14:03:56.946 -> TH02 setup
14:03:57.046 -> TH02 Setup done
14:03:57.046 -> Setup done
14:03:57.115 -> Humidity: 76 %	Temperature: 18.9 *C
14:03:57.182 -> Loop done
14:08:57.226 -> Humidity: 74 %	Temperature: 18.7 *C
14:08:57.295 -> Loop done
14:13:57.360 -> Humidity: 76 %	Temperature: 18.3 *C
14:13:57.430 -> Loop done
14:18:57.475 -> Humidity: 74 %	Temperature: 18.2 *C
14:18:57.544 -> Loop done
14:23:57.593 -> Humidity: 70 %	Temperature: 17.8 *C
14:23:57.662 -> Loop done
14:28:57.733 -> Humidity: 71 %	Temperature: 17.8 *C
14:28:57.802 -> Loop done
14:33:57.883 -> Humidity: 73 %	Temperature: 17.9 *C
14:33:57.952 -> Loop done
14:38:57.997 -> Humidity: 73 %	Temperature: 18.0 *C
14:38:58.066 -> Loop done
14:43:58.138 -> Humidity: 73 %	Temperature: 18.1 *C
14:43:58.208 -> Loop done
14:48:58.262 -> Humidity: 73 %	Temperature: 18.3 *C
14:48:58.331 -> Loop done
14:53:58.374 -> Humidity: 73 %	Temperature: 18.2 *C
14:53:58.444 -> Loop done
14:58:58.509 -> Humidity: 73 %	Temperature: 18.3 *C
14:58:58.578 -> Loop done
15:03:58.624 -> Humidity: 65 %	Temperature: 16.5 *C
15:03:58.694 -> Loop done
15:08:58.766 -> Humidity: 71 %	Temperature: 18.8 *C
15:08:58.836 -> Loop done
15:13:58.893 -> Humidity: 75 %	Temperature: 19.1 *C
15:13:58.963 -> Loop done

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.

Nexus LoRa Radio 915 MHz Payload Addressing client

This is a demo Ingenuity Micro Nexus client (based on the Netduino example for my RFM9XLoRaNetMF library) that uploads temperature and humidity data to my Azure IoT Hubs/Central or AdaFruit.IO on Raspberry PI field gateways

Bill of materials (Prices June 2019).

// <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 Sensors
Nexus Sensor data in Azure IoT Hub Field Gateway ETW Logging
Nexus temperature & humidity data displayed in Azure IoT Central