Adafruit LoRa Radio Bonnet with OLED – RadioFruit

Today a package arrived from Adafruit which contained an Adafruit LoRa Radio Bonnet 868 or 915MHz with OLED RFM95W.

The shield has a small OLED screen and 3 buttons connected to General Purpose Input Output(GPIO) pins.

The first step was to check the pin assignments of the 3 buttons.

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

	 Adafruit documentation page
	 https://learn.adafruit.com/adafruit-radio-bonnets/pinouts

    Button 1: GPIO 5 
    Button 2: GPIO 6
    Button 3: GPIO 12 

 */
namespace devMobile.IoT.Rfm9x.AdafruitButtons
{
	using System;
	using System.Diagnostics;
	using Windows.ApplicationModel.Background;
	using Windows.Devices.Gpio;

	public sealed class StartupTask : IBackgroundTask
    {
		private BackgroundTaskDeferral backgroundTaskDeferral = null;
		private GpioPin InterruptGpioPin1 = null;
		private GpioPin InterruptGpioPin2 = null;
		private GpioPin InterruptGpioPin3 = null;
		private const int InterruptPinNumber1 = 5;
		private const int InterruptPinNumber2 = 6;
		private const int InterruptPinNumber3 = 12;
		private readonly TimeSpan debounceTimeout = new TimeSpan(0, 0, 15);


		public void Run(IBackgroundTaskInstance taskInstance)
        {
			Debug.WriteLine("Application startup");

			try
			{
				GpioController gpioController = GpioController.GetDefault();

				InterruptGpioPin1 = gpioController.OpenPin(InterruptPinNumber1);
				InterruptGpioPin1.SetDriveMode(GpioPinDriveMode.InputPullUp);
				InterruptGpioPin1.ValueChanged += InterruptGpioPin_ValueChanged; ;

				InterruptGpioPin2 = gpioController.OpenPin(InterruptPinNumber2);
				InterruptGpioPin2.SetDriveMode(GpioPinDriveMode.InputPullUp);
				InterruptGpioPin2.ValueChanged += InterruptGpioPin_ValueChanged; ;

				InterruptGpioPin3 = gpioController.OpenPin(InterruptPinNumber3);
				InterruptGpioPin3.SetDriveMode(GpioPinDriveMode.InputPullUp);
				InterruptGpioPin3.ValueChanged += InterruptGpioPin_ValueChanged; ;

				Debug.WriteLine("Digital Input Interrupt configuration success");
			}
			catch (Exception ex)
			{
				Debug.WriteLine($"Digital Input Interrupt configuration failed " + ex.Message);
				return;
			}

			//enable task to continue running in background
			backgroundTaskDeferral = taskInstance.GetDeferral();
		}

		private void InterruptGpioPin_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs args)
		{
			Debug.WriteLine($"Digital Input Interrupt {sender.PinNumber} triggered {args.Edge}");
		}
	}
}

When I ran the application it produced the following output when I pressed the three buttons (left->right) which confirmed I had the correct GPIO pins configuration.

Application startup
'backgroundTaskHost.exe' (CoreCLR: CoreCLR_UWP_Domain): Loaded 'C:\Data\Programs\WindowsApps\Microsoft.NET.CoreFramework.Debug.2.2_2.2.27129.1_arm__8wekyb3d8bbwe\System.Runtime.WindowsRuntime.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Digital Input Interrupt configuration success
Digital Input Interrupt 5 triggered FallingEdge
Digital Input Interrupt 5 triggered RisingEdge
Digital Input Interrupt 6 triggered FallingEdge
Digital Input Interrupt 6 triggered RisingEdge
Digital Input Interrupt 12 triggered FallingEdge
Digital Input Interrupt 12 triggered RisingEdge

The next step was to get the Serial Peripheral Interface (SPI) interface for the module working.

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

	 Adafruit documentation page
	 https://learn.adafruit.com/adafruit-radio-bonnets/pinouts

	 CS : CE1
	 RST : GPIO25
	 IRQ : GPIO22 (DIO0)
	 Unused : GPIO23 (DIO1)
	 Unused : GPIO24 (DIO2)
 */
namespace devMobile.IoT.Rfm9x.AdafruitSPI
{
	using System;
	using System.Diagnostics;
	using System.Threading;
	using Windows.ApplicationModel.Background;
	using Windows.Devices.Spi;

	public sealed class StartupTask : IBackgroundTask
	{
		public void Run(IBackgroundTaskInstance taskInstance)
		{
			SpiController spiController = SpiController.GetDefaultAsync().AsTask().GetAwaiter().GetResult();
			var settings = new SpiConnectionSettings(1)
			{
				ClockFrequency = 500000,
				Mode = SpiMode.Mode0,   // From SemTech docs pg 80 CPOL=0, CPHA=0
			};

			SpiDevice Device = spiController.GetDevice(settings);

			while (true)
			{
				byte[] writeBuffer = new byte[] { 0x42 }; // RegVersion
				byte[] readBuffer = new byte[1];

				Device.TransferSequential(writeBuffer, readBuffer);

				byte registerValue = readBuffer[0];
				Debug.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", 0x42, registerValue, Convert.ToString(registerValue, 2).PadLeft(8, '0'));

				Thread.Sleep(10000);
			}
		}
	}
}

The output confirm the code worked

'backgroundTaskHost.exe' (CoreCLR: CoreCLR_UWP_Domain): Loaded 'C:\Data\Programs\WindowsApps\Microsoft.NET.CoreFramework.Debug.2.2_2.2.27129.1_arm__8wekyb3d8bbwe\System.Threading.Thread.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Register 0x42 - Value 0X12 - Bits 00010010
Register 0x42 - Value 0X12 - Bits 00010010

The next step is to build support for this shield into my RFM9X.IoTCore library and get the OLED working.

Grove Base Hat for Raspberry PI Windows 10 IoT Core

After some experimentation I have a proof of concept Windows 10 IoT Core library for accessing the Analog to Digital Convertor (ADC) on a Grove Base Hat for Raspberry PI.

I can read the raw, voltage & % values just fine but the Version number isn’t quite what I expected. In the python sample code I can see the register numbers etc.

def __init__(self, address=0x04):
self.address = address
self.bus = grove.i2c.Bus()

def read_raw(self, channel):
addr = 0x10 + channel
return self.read_register(addr)

# read input voltage (mV)
def read_voltage(self, channel):
addr = 0x20 + channel
return self.read_register(addr)

# input voltage / output voltage (%)
def read(self, channel):
addr = 0x30 + channel
return self.read_register(addr)

@property
def name(self):
id = self.read_register(0x0)
if id == RPI_HAT_PID:
return RPI_HAT_NAME
elif id == RPI_ZERO_HAT_PID:
return RPI_ZERO_HAT_NAME

@property
def version(self):
return self.read_register(0x3)

When I read register 0x3 to get the version info the value changes randomly. Format = register num, byte value, word value

0,4,4 1,134,10374 2,2,2 3,82,79 4,0,0 5,0,0 6,0,0 7,0,0 8,0,0 9,0,0 10,0,0 11,0,0 12,0,0 13,0,0 14,0,0 15,0,0 
0,4,4 1,134,10374 2,2,2 3,86,69 4,0,0 5,0,0 6,0,0 7,0,0 8,0,0 9,0,0 10,0,0 11,0,0 12,0,0 13,0,0 14,0,0 15,0,0 
0,4,4 1,134,10374 2,2,2 3,32,66 4,0,0 5,0,0 6,0,0 7,0,0 8,0,0 9,0,0 10,0,0 11,0,0 12,0,0 13,0,0 14,0,0 15,0,0 

It looks like register 1 or 2 (134/10374 or 2/2) might contain the device version information.

The code is available on GitHub here. Next time I purchase some gear from Seeedstudio I’ll include a Grove Base Hat For Raspberry PI Zero and extend the software so they work as well.

public sealed class StartupTask : IBackgroundTask
{
   private ThreadPoolTimer timer;
   private BackgroundTaskDeferral deferral;
   AnalogPorts analogPorts = new AnalogPorts();

   public void Run(IBackgroundTaskInstance taskInstance)
   {
      deferral = taskInstance.GetDeferral();

      analogPorts.Initialise();

      byte version = analogPorts.Version();
      Debug.WriteLine($"Version {version}");

      double powerSupplyVoltage = analogPorts.PowerSupplyVoltage();
      Debug.WriteLine($"Power supply voltage {powerSupplyVoltage}v");

      timer = ThreadPoolTimer.CreatePeriodicTimer(AnalogPorts, TimeSpan.FromSeconds(5));
   }

   void AnalogPorts(ThreadPoolTimer timer)
   {
      try
      {
         ushort valueRaw;
         valueRaw = analogPorts.ReadRaw(AnalogPorts.AnalogPort.A0);
         Debug.WriteLine($"A0 Raw {valueRaw}");

         double valueVoltage;
         valueVoltage = analogPorts.ReadVoltage(AnalogPorts.AnalogPort.A0);
         Debug.WriteLine($"A0 {valueVoltage}v");

         double value;
         value = analogPorts.Read(AnalogPorts.AnalogPort.A0);
         Debug.WriteLine($"A0 {value}");
      }
      catch (Exception ex)
      {
         Debug.WriteLine($"AnalogPorts Read failed {ex.Message}");
      }
   }
}

Grove Base Hat for Raspberry PI Investigation

For a couple of projects I had been using the Dexter industries GrovePI+ and the Grove Base Hat for Raspberry PI looked like a cheaper alternative for many applications, but it lacked Windows 10 IoT Core support.

My first project was to build a Inter Integrated Circuit(I2C) device scanner to check that the Grove Base Hat STM32 MCU I2C client implementation on a “played nice” with Windows 10 IoT core.

My Visual Studio 2017 project (I2C Device Scanner) scans all the valid 7bit I2C addresses and in the debug output displayed the two “found” devices, a Grove- 3 Axis Accelerometer(+-16G) (ADXL345) and the Grove Base Hat for Raspberry PI.

backgroundTaskHost.exe' (CoreCLR: CoreCLR_UWP_Domain): Loaded 'C:\Data\Users\DefaultAccount\AppData\Local\DevelopmentFiles\I2CDeviceScanner-uwpVS.Debug_ARM.Bryn.Lewis\System.Diagnostics.Debug.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\Users\DefaultAccount\AppData\Local\DevelopmentFiles\I2CDeviceScanner-uwpVS.Debug_ARM.Bryn.Lewis\System.Linq.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Exception thrown: 'System.IO.FileNotFoundException' in devMobile.Windows10IoTCore.I2CDeviceScanner.winmd
WinRT information: Slave address was not acknowledged.
.......
Exception thrown: 'System.IO.FileNotFoundException' in devMobile.Windows10IoTCore.I2CDeviceScanner.winmd
WinRT information: Slave address was not acknowledged.

I2C Controller \\?\ACPI#MSFT8000#1#{a11ee3c6-8421-4202-a3e7-b91ff90188e4}\I2C1 has 2 devices
Address 0x4
Address 0x53
Raspberry PI with Grove Base Hat & ADXL345 & Rotary angle sensor
Raspberry PI with Grove Base Hat I2C test rig

The next step was to confirm I could read the device ID of the ADXL345 and the Grove Base Hat for RaspberryPI. I had to figure out the Grove Base Hat for RaspberryPI from the Seeedstudio Python code.

I2CDevicePinger ADXL345 Debug output

...
'backgroundTaskHost.exe' (CoreCLR: CoreCLR_UWP_Domain): Loaded 'C:\Data\Users\DefaultAccount\AppData\Local\DevelopmentFiles\I2CDevicePinger-uwpVS.Debug_ARM.Bryn.Lewis\System.Diagnostics.Debug.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
DeviceID 0XE5

The DeviceID for the ADXL345 matched the DEVID in the device datasheet.

I2CDevicePinger Debug output

'backgroundTaskHost.exe' (CoreCLR: CoreCLR_UWP_Domain): Loaded 'C:\Data\Users\DefaultAccount\AppData\Local\DevelopmentFiles\I2CDevicePinger-uwpVS.Debug_ARM.Bryn.Lewis\System.Diagnostics.Debug.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
DeviceID 0X4

The DeviceID for the Grove Base Hat for RaspberryPI matched

RPI_HAT_PID = 0x0004 in the Python code.

The last test application reads the raw value of the specified analog input

public async void Run(IBackgroundTaskInstance taskInstance)
{
   string aqs = I2cDevice.GetDeviceSelector();
   DeviceInformationCollection I2CBusControllers = await DeviceInformation.FindAllAsync(aqs);

   if (I2CBusControllers.Count != 1)
   {
      Debug.WriteLine("Unexpect number of I2C bus controllers found");
      return;
   }

   I2cConnectionSettings settings = new I2cConnectionSettings(0x04)
   {
      BusSpeed = I2cBusSpeed.StandardMode,
      SharingMode = I2cSharingMode.Shared,
   };

   using (I2cDevice device = I2cDevice.FromIdAsync(I2CBusControllers[0].Id, settings).AsTask().GetAwaiter().GetResult())
   {
      try
      {
         ushort value = 0;
         // From the Seeedstudio python
	 // 0x10 ~ 0x17: ADC raw data
	 // 0x20 ~ 0x27: input voltage
         // 0x29: output voltage (Grove power supply voltage)
         // 0x30 ~ 0x37: input voltage / output voltage						
         do
	 {
            byte[] writeBuffer = new byte[1] { 0x10 };
            byte[] readBuffer = new byte[2] { 0, 0 };

            device.WriteRead(writeBuffer, readBuffer);
            value = BitConverter.ToUInt16(readBuffer, 0);

            Debug.WriteLine($"Value {value}");

            Task.Delay(1000).GetAwaiter().GetResult();
         }
         while (value != 0);
      }
      Catch (Exception ex)
      {
         Debug.WriteLine(ex.Message);
      }
   }
}

GroveBaseHatRPIRegisterReader Debug output

'backgroundTaskHost.exe' (CoreCLR: CoreCLR_UWP_Domain): Loaded 'C:\Data\Users\DefaultAccount\AppData\Local\DevelopmentFiles\GroveBaseHatRPIRegisterReader-uwpVS.Debug_ARM.Bryn.Lewis\System.Diagnostics.Debug.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Value 3685
Value 3685
Value 3688
Value 3681
Value 3681
Value 3688
Value 3688
Value 3683

The output changed when I adjusted the rotary angle sensor (0-4095) which confirmed I could reliably read the Analog input values.

The code for my test harness applications is available on github, the next step is to build a library for the Grove Base Hat for RaspberryPI

Adafruit.IO LoRa Field Gateway BCD Addressing

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

THIS IS A BREAKING CHANGE

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

My later Arduino based sample clients have some helper functions to populate the message header, add values, and prepare the message payload for reuse.

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

// Log the Application build, shield information etc.
LoggingFields appllicationBuildInformation = new LoggingFields();
#if DRAGINO
   appllicationBuildInformation.AddString("Shield", "DraginoLoRaGPSHat");
#endif
…
#if UPUTRONICS_RPIPLUS_CS1
   appllicationBuildInformation.AddString("Shield", "UputronicsPiPlusLoRaExpansionBoardCS1");
#endif
appllicationBuildInformation.AddString("Timezone", TimeZoneSettings.CurrentTimeZoneDisplayName);
appllicationBuildInformation.AddString("OSVersion", Environment.OSVersion.VersionString);
appllicationBuildInformation.AddString("MachineName", Environment.MachineName);

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

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

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

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

   addressBcdText = BitConverter.ToString(e.Address);

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

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

			
   // Check the address is not to short/long 
   if (e.Address.Length < AddressLengthMinimum)
   {
      this.loggingChannel.LogMessage("From address too short", LoggingLevel.Warning);
      return;
   }

   if (e.Address.Length > MessageLengthMaximum)
   {
      this.loggingChannel.LogMessage("From address too long", LoggingLevel.Warning);
      return;
   }

   // Check the payload is not too short/long 
   if (e.Data.Length < MessageLengthMinimum)
   {
      this.loggingChannel.LogMessage("Message too short to contain any data", LoggingLevel.Warning);
      return;
   }

   if (e.Data.Length > MessageLengthMaximum)
   {
      this.loggingChannel.LogMessage("Message too long to contain valid data", LoggingLevel.Warning);
      return;
   }

   // Adafruit IO is case sensitive & only does lower case ?
   string deviceId = addressBcdText.ToLower();

   // Chop up the CSV text payload
   string[] sensorReadings = messageText.Split(sensorReadingSeparator, StringSplitOptions.RemoveEmptyEntries);
   if (sensorReadings.Length == 0)
   {
      this.loggingChannel.LogMessage("Payload contains no sensor readings", LoggingLevel.Warning);
      return;
   }

   Group_feed_data groupFeedData = new Group_feed_data();

   LoggingFields sensorData = new LoggingFields();
   sensorData.AddString("DeviceID", deviceId);

   // Chop up each sensor reading into an ID & value
   foreach (string sensorReading in sensorReadings)
   {
      string[] sensorIdAndValue = sensorReading.Split(sensorIdAndValueSeparator, StringSplitOptions.RemoveEmptyEntries);

      // Check that there is an id & value
      if (sensorIdAndValue.Length != 2)
      {
         this.loggingChannel.LogMessage("Sensor reading invalid format", LoggingLevel.Warning);
         return;
      }

      string sensorId = sensorIdAndValue[0].ToLower();
      string value = sensorIdAndValue[1];

      // Construct the sensor ID from SensordeviceID & Value ID
      groupFeedData.Feeds.Add(new Anonymous2() { Key = string.Format("{0}{1}", deviceId, sensorId), Value = value });

      sensorData.AddString(sensorId, value);

      Debug.WriteLine(" Sensor {0}{1} Value {2}", deviceId, sensorId, value);
   }
   this.loggingChannel.LogEvent("Sensor readings", sensorData, LoggingLevel.Verbose);

   try
   {
      Debug.WriteLine(" CreateGroupDataAsync start");
      await this.adaFruitIOClient.CreateGroupDataAsync(this.applicationSettings.AdaFruitIOUserName,
this.applicationSettings.AdaFruitIOGroupName.ToLower(), groupFeedData);
      Debug.WriteLine(" CreateGroupDataAsync finish");
   }
   catch (Exception ex)
   {
      Debug.WriteLine(" CreateGroupDataAsync failed {0}", ex.Message);
				this.loggingChannel.LogMessage("CreateGroupDataAsync failed " + ex.Message, LoggingLevel.Error);
   }
}
AfaFruit.IO Data Display

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

AdaFruit.IO Field gateway ETW Logging

Azure IoT Hubs LoRa Field Gateway BCD Addressing

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

THIS IS A BREAKING CHANGE

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

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

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

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

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

...

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

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

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

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

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

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


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

//...

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

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

   //Chop up each sensor read into an ID & value
   foreach (string sensorReading in sensorReadings)
   {
      string[] sensorIdAndValue = sensorReading.Split(sensorIdAndValueSeparators, StringSplitOptions.RemoveEmptyEntries);

      // Check that there is an id & value
      if (sensorIdAndValue.Length != 2)
      {
         this.logging.LogMessage("Sensor reading invalid format", LoggingLevel.Warning);
         return;
      }

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

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

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

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

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

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

Azure IoT Hub LoRa Field Gateway ETW Logging

IoT.Net LoRa Radio 915 MHz Payload Addressing client

This is a demo ingenuity micro IoT.Net client (based on one of the examples in my RFM9XLoRaNetMF library) that uploads telemetry data to my Windows 10 IoT Core on Raspberry PI field gateway. 

Thought the silk screen says RFM69 this is a prototype running an RFM95 module.

iotnetlora.jpg

Bill of materials (Prices Sep 2018)

  • IoT.Net device (Beta tester will add price when available)

The device has an onboard MCP9808 temperature sensor which kept the BoM really short. I have had to make some modifications to my RFM9XLoRaNetMF library as the IoT.Net device uses a different SPI port. The code for this devices and the changes will be uploaded to GitHub in the next couple of days.

//---------------------------------------------------------------------------------
// Copyright (c) Sept 2018, 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.
// git remote add origin https://github.com/KiwiBryn/FieldGateway.LoRa.IoTNetClient.git
// git push -u origin master
//---------------------------------------------------------------------------------
namespace devMobile.IoT.IoTNet.FieldGateway
{
	using System;
	using System.Text;
	using System.Threading;
	using Microsoft.SPOT;
	using Microsoft.SPOT.Hardware;
	using devMobile.IoT.NetMF.ISM;
	using IngenuityMicro.Sensors;

	class IoTNetClient
	{
		private readonly Rfm9XDevice rfm9XDevice;
		private readonly TimeSpan dueTime = new TimeSpan(0, 0, 10);
		private readonly TimeSpan periodTime = new TimeSpan(0, 0, 30);
		private readonly MCP9808 mcp9808 = new MCP9808();
		private readonly OutputPort _led = new OutputPort((Cpu.Pin)16 + 8, false);
		private readonly byte[] fieldGatewayAddress = Encoding.UTF8.GetBytes("LoRaIoT1");
		private readonly byte[] deviceAddress = Encoding.UTF8.GetBytes("IoTNet1");

		public IoTNetClient()
		{
			rfm9XDevice = new Rfm9XDevice( SPI.SPI_module.SPI3, (Cpu.Pin)16 + 9, (Cpu.Pin)5, (Cpu.Pin)4);
		}

		public void Run()
		{
			rfm9XDevice.Initialise(frequency: 915000000, paBoost: true, rxPayloadCrcOn: true);
			rfm9XDevice.Receive(deviceAddress);

			rfm9XDevice.OnDataReceived += rfm9XDevice_OnDataReceived;
			rfm9XDevice.OnTransmit += rfm9XDevice_OnTransmit;

			Timer temperatureUpdates = new Timer(TemperatureTimerProc, null, dueTime, periodTime);

			Thread.Sleep(Timeout.Infinite);
		}

		private void TemperatureTimerProc(object state)
		{
			_led.Write(true);

			double temperature = mcp9808.ReadTempInC();

			Debug.Print(DateTime.UtcNow.ToString("hh:mm:ss") + "  T:" + temperature.ToString("F1"));

			rfm9XDevice.Send(fieldGatewayAddress, Encoding.UTF8.GetBytes("t " + temperature.ToString("F1")));

			_led.Write(true);
		}

		void rfm9XDevice_OnTransmit()
		{
			Debug.Print("Transmit-Done");
			_led.Write(false);
		}

		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);
			}
		}
	}
}
}

.Net Framework debug output Field Gateway

22:55:39-RX From IoTNet1 PacketSnr 9.5 Packet RSSI -50dBm RSSI -110dBm = 6 byte message "t 23.6"
 Sensor IoTNet1t Value 23.6
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish
The thread 0xbec has exited with code 0 (0x0).
The thread 0xbb4 has exited with code 0 (0x0).
The thread 0xa0c has exited with code 0 (0x0).
The thread 0x13c has exited with code 0 (0x0).
22:56:09-RX From IoTNet1 PacketSnr 9.3 Packet RSSI -44dBm RSSI -102dBm = 6 byte message "t 23.8"
 Sensor IoTNet1t Value 23.8
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish

A small footprint, battery powered .NetMF 4.4 LoRa device designed and made in New Zealand with Visual Studio 2017 support is great.

Netduino LoRa Radio 433/868/915 MHz Payload Addressing client

This is a demo Netduino client (based on one of the examples in my RFM9XLoRaNetMF library) that uploads telemetry data to my Windows 10 IoT Core on Raspberry PI field gateway proof of concept(PoC).

Bill of materials (Prices Sep 2018)

//---------------------------------------------------------------------------------
// Copyright (c) 2017, 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.Netduino.FieldGateway
{
   using System;
   using System.Text;
   using System.Threading;
   using Microsoft.SPOT;
   using Microsoft.SPOT.Hardware;
   using SecretLabs.NETMF.Hardware.Netduino;
   using devMobile.IoT.NetMF.ISM;
   using devMobile.NetMF.Sensor;

   class NetduinoClient
   {
      Rfm9XDevice rfm9XDevice;
      private readonly TimeSpan dueTime = new TimeSpan(0, 0, 15);
      private readonly TimeSpan periodTime = new TimeSpan(0, 0, 300);
      private readonly SiliconLabsSI7005 sensor = new SiliconLabsSI7005();
      private readonly OutputPort _led = new OutputPort(Pins.ONBOARD_LED, false);
      private readonly byte[] fieldGatewayAddress = Encoding.UTF8.GetBytes("LoRaIoT1");
      private readonly byte[] deviceAddress = Encoding.UTF8.GetBytes("Netduino1");

      public NetduinoClient()
      {
         rfm9XDevice = new Rfm9XDevice(Pins.GPIO_PIN_D10, Pins.GPIO_PIN_D9, Pins.GPIO_PIN_D2);
      }

      public void Run()
      {
         //rfm9XDevice.Initialise(frequency: 915000000, paBoost: true, rxPayloadCrcOn: true);
         rfm9XDevice.Initialise(frequency: 433000000, 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.Write(true);

         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")));

         _led.Write(true);
      }

      void rfm9XDevice_OnTransmit()
      {
         Debug.Print("Transmit-Done");
         _led.Write(false);
      }

      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);
         }
      }
   }
}

The code is available on GitHub
FieldGatewayNetduinoLoRaElecrow915
Elecrow shield
FieldGatewayNetduinoLoRaDragino915
Dragino shield
FieldGatewayNetduinLoRaMakerFabs433
MakerFabs shield
Net Micro Framework debug output from device

The thread '' (0x2) has exited with code 0 (0x0).
12:00:18 H:96.9 T:19.6
Transmit-Done
12:05:17 H:95.1 T:20.1
Transmit-Done

.Net Framework debug output Field Gateway

The thread 0x1550 has exited with code 0 (0x0).
21:21:49-RX From Netduino1 PacketSnr 9.5 Packet RSSI -40dBm RSSI -107dBm = 11 byte message "t 19.6,H 97"
 Sensor Netduino1t Value 19.6
 Sensor Netduino1H Value 97
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish
...
21:26:49-RX From Netduino1 PacketSnr 9.5 Packet RSSI -33dBm RSSI -103dBm = 11 byte message "t 20.1,H 95"
 Sensor Netduino1t Value 20.1
 Sensor Netduino1H Value 95
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish
The thread 0xfbc has exited with code 0 (0x0).

Then in my Azure IoT Hub

AzureIOTHubExplorerScreenGrab20180917