Maduino LoRa Air Temperature and Soil Moisture

This is a demo MakerFabs Maduino LoRa Radio 868MHz client (based on Maduino LoRa 868MHz example) that uploads telemetry data to my Windows 10 IoT Core on Raspberry PI AdaFruit.IO and Azure IoT Hub field gateways.

The code is available on github

Sample hardware
Azure IoT Central data visualisation

The Maduino device in the picture is a custom version with an onboard Microchip ATSHA204 crypto and authentication chip (currently only use for the unique 72 bit serial number) and a voltage divider connected to the analog pin A6 to monitor the battery voltage.

There are compile time options ATSHA204 & BATTERY_VOLTAGE_MONITOR which can be used to selectively enable this functionality.

I use the Arduino lowpower library to aggressively sleep the device between measurements

// Adjust the delay so period is close to desired sec as possible, first do 8sec chunks. 
  int delayCounter = SensorUploadDelay / 8 ;
  for( int i = 0 ; i < delayCounter ; i++ )
  {
     LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);  
  }
  
  // Then to 4 sec chunk
  delayCounter =  ( SensorUploadDelay % 8 ) / 4;
  for( int i = 0 ; i < delayCounter ; i++ )
  {
     LowPower.powerDown(SLEEP_4S, ADC_OFF, BOD_OFF);  
  }

  // Then to 2 sec chunk
  delayCounter =  ( SensorUploadDelay % 4 ) / 2 ;
  for( int i = 0 ; i < delayCounter ; i++ )
  {
     LowPower.powerDown(SLEEP_2S, ADC_OFF, BOD_OFF);  
  }

  // Then to 1 sec chunk
  delayCounter =  ( SensorUploadDelay % 2 ) ;
  for( int i = 0 ; i < delayCounter ; i++ )
  {
     LowPower.powerDown(SLEEP_1S, ADC_OFF, BOD_OFF);  
  }
}

I use a spare digital PIN for powering the soil moisture probe so it can be powered down when not in use. I have included a short delay after powering up the device to allow the reading to settle.

  // Turn on soil mosture sensor, take reading then turn off to save power
  digitalWrite(SoilMoistureSensorEnablePin, HIGH);
  delay(SoilMoistureSensorEnableDelay);
  int soilMoistureADCValue = analogRead(SoilMoistureSensorPin);
  digitalWrite(SoilMoistureSensorEnablePin, LOW);
  int soilMoisture = map(soilMoistureADCValue,SoilMoistureSensorMinimum,SoilMoistureSensorMaximum, SoilMoistureValueMinimum, SoilMoistureValueMaximum); 
  PayloadAdd( "s", soilMoisture, false);

Bill of materials (Prices Nov 2019)

  • Maduino LoRa Radion (868MHz) 18.90
  • SHT20 I2C Temperature & Humidity Sensor (Waterproof Probe) USD22.50
  • Pinotech SoilWatch 10 – Soil moisture sensor USD23
  • Elecrow 1 Watt solar panel with wires USD3.80
  • 500 mAh LI-Ion battery

The software could easily be modified to support additional sensors.

RFM69 shield library Part2

Register Dump

Next step was to dump all registers (0x00 RegFifo thru 0x4F RegTemp2) of the RFM69HCW device.

/*
    Copyright ® 2019 May 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

	 RegFiFo 0x00 thru RegPacketConfig2 0x3D

*/
namespace devMobile.IoT.Rfm69Hcw.RegisterScan
{
	using System;
	using System.Diagnostics;
	using System.Threading.Tasks;
	using Windows.ApplicationModel.Background;
	using Windows.Devices.Spi;

	public sealed class Rfm69HcwDevice
	{
		private SpiDevice rfm69Hcw;

		public Rfm69HcwDevice(int chipSelectPin)
		{
			SpiController spiController = SpiController.GetDefaultAsync().AsTask().GetAwaiter().GetResult();
			var settings = new SpiConnectionSettings(chipSelectPin)
			{
				ClockFrequency = 500000,
				Mode = SpiMode.Mode0,
			};

			rfm69Hcw = spiController.GetDevice(settings);
		}

		public Byte RegisterReadByte(byte registerAddress)
		{
			byte[] writeBuffer = new byte[] { registerAddress };
			byte[] readBuffer = new byte[1];
			Debug.Assert(rfm69Hcw != null);

			rfm69Hcw.TransferSequential(writeBuffer, readBuffer);

			return readBuffer[0];
		}
	}

	public sealed class StartupTask : IBackgroundTask
	{
		private const int ChipSelectLine = 0;
		private Rfm69HcwDevice rfm69HcwDevice = new Rfm69HcwDevice(ChipSelectLine);

		public void Run(IBackgroundTaskInstance taskInstance)
		{

			while (true)
			{
				for (byte registerIndex = 0; registerIndex <= 0x4F; registerIndex++)
				{
					byte registerValue = rfm69HcwDevice.RegisterReadByte(registerIndex);

					Debug.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", registerIndex, registerValue, Convert.ToString(registerValue, 2).PadLeft(8, '0'));
				}

				Task.Delay(10000).Wait();
			}
		}
	}
}

I checked a selection of values from the Debug output and they matched the defaults in the datasheet e.g. 0x07-RegFrfMsb 0xE4, 0x08-RegFrfMid 0xC0, 0x09-RegFrfLsb 0x00, 0x2C-RegPreamble 0x0, 0x2D-RegPreamble 0x03 and 0x4E-RegTemp1 0x01.

Register 0x00 - Value 0X00 - Bits 00000000
Register 0x01 - Value 0X04 - Bits 00000100
Register 0x02 - Value 0X00 - Bits 00000000
Register 0x03 - Value 0X1a - Bits 00011010
Register 0x04 - Value 0X0b - Bits 00001011
Register 0x05 - Value 0X00 - Bits 00000000
Register 0x06 - Value 0X52 - Bits 01010010
Register 0x07 - Value 0Xe4 - Bits 11100100
Register 0x08 - Value 0Xc0 - Bits 11000000
Register 0x09 - Value 0X00 - Bits 00000000
Register 0x0a - Value 0X41 - Bits 01000001
Register 0x0b - Value 0X40 - Bits 01000000
Register 0x0c - Value 0X02 - Bits 00000010
Register 0x0d - Value 0X92 - Bits 10010010
Register 0x0e - Value 0Xf5 - Bits 11110101
Register 0x0f - Value 0X20 - Bits 00100000
Register 0x10 - Value 0X24 - Bits 00100100
Register 0x11 - Value 0X9f - Bits 10011111
Register 0x12 - Value 0X09 - Bits 00001001
Register 0x13 - Value 0X1a - Bits 00011010
Register 0x14 - Value 0X40 - Bits 01000000
Register 0x15 - Value 0Xb0 - Bits 10110000
Register 0x16 - Value 0X7b - Bits 01111011
Register 0x17 - Value 0X9b - Bits 10011011
Register 0x18 - Value 0X08 - Bits 00001000
Register 0x19 - Value 0X86 - Bits 10000110
Register 0x1a - Value 0X8a - Bits 10001010
Register 0x1b - Value 0X40 - Bits 01000000
Register 0x1c - Value 0X80 - Bits 10000000
Register 0x1d - Value 0X06 - Bits 00000110
Register 0x1e - Value 0X10 - Bits 00010000
Register 0x1f - Value 0X00 - Bits 00000000
Register 0x20 - Value 0X00 - Bits 00000000
Register 0x21 - Value 0X00 - Bits 00000000
Register 0x22 - Value 0X00 - Bits 00000000
Register 0x23 - Value 0X02 - Bits 00000010
Register 0x24 - Value 0Xff - Bits 11111111
Register 0x25 - Value 0X00 - Bits 00000000
Register 0x26 - Value 0X05 - Bits 00000101
Register 0x27 - Value 0X80 - Bits 10000000
Register 0x28 - Value 0X00 - Bits 00000000
Register 0x29 - Value 0Xff - Bits 11111111
Register 0x2a - Value 0X00 - Bits 00000000
Register 0x2b - Value 0X00 - Bits 00000000
Register 0x2c - Value 0X00 - Bits 00000000
Register 0x2d - Value 0X03 - Bits 00000011
Register 0x2e - Value 0X98 - Bits 10011000
Register 0x2f - Value 0X00 - Bits 00000000
Register 0x30 - Value 0X00 - Bits 00000000
Register 0x31 - Value 0X00 - Bits 00000000
Register 0x32 - Value 0X00 - Bits 00000000
Register 0x33 - Value 0X00 - Bits 00000000
Register 0x34 - Value 0X00 - Bits 00000000
Register 0x35 - Value 0X00 - Bits 00000000
Register 0x36 - Value 0X00 - Bits 00000000
Register 0x37 - Value 0X10 - Bits 00010000
Register 0x38 - Value 0X40 - Bits 01000000
Register 0x39 - Value 0X00 - Bits 00000000
Register 0x3a - Value 0X00 - Bits 00000000
Register 0x3b - Value 0X00 - Bits 00000000
Register 0x3c - Value 0X0f - Bits 00001111
Register 0x3d - Value 0X02 - Bits 00000010
Register 0x3e - Value 0X00 - Bits 00000000
Register 0x3f - Value 0X00 - Bits 00000000
Register 0x40 - Value 0X00 - Bits 00000000
Register 0x41 - Value 0X00 - Bits 00000000
Register 0x42 - Value 0X00 - Bits 00000000
Register 0x43 - Value 0X00 - Bits 00000000
Register 0x44 - Value 0X00 - Bits 00000000
Register 0x45 - Value 0X00 - Bits 00000000
Register 0x46 - Value 0X00 - Bits 00000000
Register 0x47 - Value 0X00 - Bits 00000000
Register 0x48 - Value 0X00 - Bits 00000000
Register 0x49 - Value 0X00 - Bits 00000000
Register 0x4a - Value 0X00 - Bits 00000000
Register 0x4b - Value 0X00 - Bits 00000000
Register 0x4c - Value 0X00 - Bits 00000000
Register 0x4d - Value 0X00 - Bits 00000000
Register 0x4e - Value 0X01 - Bits 00000001
Register 0x4f - Value 0X00 - Bits 00000000

RFM9X.IoTCore Adafruit LoRa Radio Bonnet support

The RFM9X chip select line on the Adafruit LoRa Radio Bonnet 868 or 915MHz with OLED RFM95W is connected to pin 26(CS1), the reset line to pin 22(GPIO25) and the interrupt line to pin 15(GPIO22).

When I ran the RFM9XLoRaDeviceClient from my RFM9X.IoTCore library with the following configuration

#if ADAFRUIT_RADIO_BONNET
	private const byte ResetLine = 25;
	private const byte InterruptLine = 22;
	private Rfm9XDevice rfm9XDevice = new Rfm9XDevice(ChipSelectPin.CS1, ResetLine, InterruptLine);
#endif

public void Run(IBackgroundTaskInstance taskInstance)
{
	rfm9XDevice.Initialise(Frequency, paBoost: true, rxPayloadCrcOn : true);
#if DEBUG
	rfm9XDevice.RegisterDump();
#endif
	rfm9XDevice.OnReceive += Rfm9XDevice_OnReceive;
#if ADDRESSED_MESSAGES_PAYLOAD
	rfm9XDevice.Receive(UTF8Encoding.UTF8.GetBytes(Environment.MachineName));
#else
	rfm9XDevice.Receive();
#endif
	rfm9XDevice.OnTransmit += Rfm9XDevice_OnTransmit;

	Task.Delay(10000).Wait();

	while (true)
	{
		string messageText = string.Format("Hello from {0} ! {1}", Environment.MachineName, MessageCount);
		MessageCount -= 1;

		byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
		Debug.WriteLine("{0:HH:mm:ss}-TX {1} byte message {2}", DateTime.Now, messageBytes.Length, messageText);
#if ADDRESSED_MESSAGES_PAYLOAD
		this.rfm9XDevice.Send(UTF8Encoding.UTF8.GetBytes("AddressHere"), messageBytes);
#else
		this.rfm9XDevice.Send(messageBytes);
#endif
		Task.Delay(10000).Wait();
	}
}
#endif

I could see messages being sent and received in the debug output

Register 0x3e - Value 0X00 - Bits 00000000
Register 0x3f - Value 0X00 - Bits 00000000
Register 0x40 - Value 0X00 - Bits 00000000
Register 0x41 - Value 0X00 - Bits 00000000
Register 0x42 - Value 0X12 - Bits 00010010
...
The thread 0xec4 has exited with code 0 (0x0).
The thread 0x868 has exited with code 0 (0x0).
22:21:47-RX PacketSnr 9.8 Packet RSSI -80dBm RSSI -122dBm = 59 byte message "�LoRaIoT1Maduino2at 62.8,ah 77,wsa 1,wsg 3,wd 34.88,r 0.00,"
22:21:52-TX 31 byte message Hello from AdaFruitIOLoRa ! 255
22:21:52-TX Done
The thread 0xbf8 has exited with code 0 (0x0).
The program '[3380] backgroundTaskHost.exe' has exited with code -1 (0xffffffff).

Next step modify my Adafruit IO and Azure IoT Hub/Central field gateways.

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.

Windows 10 IoT Core Field Gateways “less is more”

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

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

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

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

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

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

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

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

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

Wisen Whisper Node – LoRa 915 MHz Payload Addressing Client

This is a demo Wizen Whisper NodeLoRa client (based on one of the examples from Arduino-LoRa) that uploads telemetry data to my Windows 10 IoT Core on Raspberry PI field gateway proof of concept(PoC).

The Wisen Bitbucket repository had sample code based on the RadioHead library which was useful for port numbers. This device family supports 433MHz, 868MHz & 915Mz modules. Wisen has other RFM69 based devices as well.

Bill of materials (Prices Sep 2018)

  • Wizen Whisper Node LoRa (433, 868 or 900 MHz) AUD27.90
  • Seeedstudio Temperature and Humidity Sensor Pro USD11.50
  • Seeedstudio 4 pin Male Jumper to Grove 4 pin Conversion Cable USD2.90

The code is pretty basic, it reads a value from the Seeedstudio temperature and humidity sensor, then packs the payload and sets the necessary RFM9X/SX127X LoRa module configuration. It has no power conservation, advanced wireless configuration etc.

I needed to use jumpers to connect my device up

WisenPatch20180924

WisenLoRa20180924

/*
  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               // include libraries
#include
#include
const int csPin = 10;          // LoRa radio chip select
const int resetPin = 7;       // LoRa radio reset
const int irqPin = 2;         // change for your board; must be a hardware interrupt pin

// Field gateway configuration
const char FieldGatewayAddress[] = "LoRaIoT1";
const float FieldGatewayFrequency =  915000000.0;
const byte FieldGatewaySyncWord = 0x12 ;

// Payload configuration
const int PayloadSizeMaximum = 64 ;
byte payload[PayloadSizeMaximum] = "";
const byte SensorReadingSeperator = ',' ;

// Manual serial number configuration
const char DeviceId[] = {"Wisen01"};

const int LoopSleepDelaySeconds = 10 ;

void setup() {
  Serial.begin(9600);
  //while (!Serial);

  Serial.println("LoRa Setup");

  // override the default CS, reset, and IRQ pins (optional)
  LoRa.setPins(csPin, resetPin, irqPin);// set CS, reset, IRQ pin

  if (!LoRa.begin(FieldGatewayFrequency))
  {
    Serial.println("LoRa init failed. Check your connections.");
    while (true);
  }

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

  //LoRa.dumpRegisters(Serial);
  Serial.println("LoRa Setup done.");

  // 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()
{
  int payloadLength = 0 ;
  float temperature ;
  float humidity ;

  Serial.println("Loop called");
  memset(payload, 0, sizeof(payload));

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

  // Copy the "To" address into payload
  memcpy(&payload[payloadLength], FieldGatewayAddress, strlen(FieldGatewayAddress));
  payloadLength += strlen(FieldGatewayAddress) ;

  // Copy the "From" into payload
  memcpy(&payload[payloadLength], DeviceId, strlen(DeviceId));
  payloadLength += strlen(DeviceId) ;

  // Read the temperature and humidity values then display nicely
  temperature = TH02.ReadTemperature();
  humidity = TH02.ReadHumidity();

  Serial.print("T:");
  Serial.print( temperature, 1 ) ;
  Serial.print( "C" ) ;

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

  // Copy the temperature into the payload
  payload[ payloadLength] = 't';
  payloadLength += 1 ;
  payload[ payloadLength] = ' ';
  payloadLength += 1 ;
  payloadLength += strlen( dtostrf(temperature, -1, 1, (char*)&payload[payloadLength]));
  payload[ payloadLength] = SensorReadingSeperator;
  payloadLength += sizeof(SensorReadingSeperator) ;

  // Copy the humidity into the payload
  payload[ payloadLength] = 'h';
  payloadLength += 1 ;
  payload[ payloadLength] = ' ';
  payloadLength += 1 ;
  payloadLength += strlen( dtostrf(humidity, -1, 0, (char *)&payload[payloadLength]));  

  // display info about payload then send it (No ACK) with LoRa unlike nRF24L01
  Serial.print( "RFM9X/SX127X Payload length:");
  Serial.print( payloadLength );
  Serial.println( " bytes" );

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

  Serial.println("Loop done");

  delay(LoopSleepDelaySeconds * 1000l);
}

In the debug output window the messages from the device looked like this

20:38:30-RX From Wisen01 PacketSnr 10.0 Packet RSSI -48dBm RSSI -104dBm = 11 byte message "t 22.2,h 91"
Sensor Wisen01t Value 22.2
Sensor Wisen01h Value 91
AzureIoTHubClient SendEventAsync start
AzureIoTHubClient SendEventAsync finish
The thread 0x14c0 has exited with code 0 (0x0).
The thread 0x788 has exited with code 0 (0x0).
20:38:40-RX From Wisen01 PacketSnr 9.8 Packet RSSI -48dBm RSSI -103dBm = 11 byte message "t 22.5,h 91"
Sensor Wisen01t Value 22.5
Sensor Wisen01h Value 91
AzureIoTHubClient SendEventAsync start
AzureIoTHubClient SendEventAsync finish
The thread 0x1124 has exited with code 0 (0x0).
The thread 0x129c has exited with code 0 (0x0).
20:38:50-RX From Wisen01 PacketSnr 9.3 Packet RSSI -47dBm RSSI -96dBm = 11 byte message "t 22.7,h 91"
Sensor Wisen01t Value 22.7
Sensor Wisen01h Value 91
AzureIoTHubClient SendEventAsync start
AzureIoTHubClient SendEventAsync finish
The thread 0x664 has exited with code 0 (0x0).

Then at my Azure IoT hub the data stream looked like this

WisenTalkLoRaAzureIoTHub

Adafruit Feather M0 RFM95 LoRa Radio Payload Addressing Client

This is a demo AdaFruit Feather Mo0 Radio with LoRa Radio Module client (based on one of the examples from Arduino-LoRa) that uploads telemetry data to my Windows 10 IoT Core on Raspberry PI field gateway proof of concept(PoC).

The Adafruit learn site had sample code based on the RadioHead library which was useful. This device supports 868MHz & 915Mz, there is are other Arduino 32u4 and 433MHz devices available.

Bill of materials (Prices Sep 2018)

  • Adafruit Feather M0 RFM95 LoRa Radio (433 or 900 MHz) USD34.95
  • Seeedstudio Temperature and Humidity Sensor Pro USD11.50
  • Seeedstudio 4 pin Male Jumper to Grove 4 pin Conversion Cable USD2.90

The code is pretty basic, it reads a value from the Seeedstudio temperature and humidity sensor, then packs the payload and sets the necessary RFM9X/SX127X LoRa module configuration, has no power conservation, advanced wireless configuration etc. I had to add an extra include from the dstrtof function

AdaFruitM0Feather

/*
  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
#include
#include
#include
const int csPin = 8;          // LoRa radio chip select
const int resetPin = 4;       // LoRa radio reset
const int irqPin = 3;         // change for your board; must be a hardware interrupt pin

// Field gateway configuration
const char FieldGatewayAddress[] = "LoRaIoT1";
const float FieldGatewayFrequency =  915000000.0;
const byte FieldGatewaySyncWord = 0x12 ;

// Payload configuration
const int PayloadSizeMaximum = 64 ;
byte payload[PayloadSizeMaximum] = "";
const byte SensorReadingSeperator = ',' ;

// Manual serial number configuration
const char DeviceId[] = {"AdafruitM0"};

const int LoopSleepDelaySeconds = 10 ;

void setup() {
  Serial.begin(9600);
  //while (!Serial);

  Serial.println("LoRa Setup");

  // override the default CS, reset, and IRQ pins (optional)
  LoRa.setPins(csPin, resetPin, irqPin);// set CS, reset, IRQ pin

  if (!LoRa.begin(FieldGatewayFrequency))
  {
    Serial.println("LoRa init failed. Check your connections.");
    while (true);
  }

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

  //LoRa.dumpRegisters(Serial);
  Serial.println("LoRa Setup done.");

  // 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()
{
  int payloadLength = 0 ;
  float temperature ;
  float humidity ;

  Serial.println("Loop called");
  memset(payload, 0, sizeof(payload));

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

  // Copy the "To" address into payload
  memcpy(&payload[payloadLength], FieldGatewayAddress, strlen(FieldGatewayAddress));
  payloadLength += strlen(FieldGatewayAddress) ;

  // Copy the "From" into payload
  memcpy(&payload[payloadLength], DeviceId, strlen(DeviceId));
  payloadLength += strlen(DeviceId) ;

  // Read the temperature and humidity values then display nicely
  temperature = TH02.ReadTemperature();
  humidity = TH02.ReadHumidity();

  Serial.print("T:");
  Serial.print( temperature, 1 ) ;
  Serial.print( "C" ) ;

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

  // Copy the temperature into the payload
  payload[ payloadLength] = 't';
  payloadLength += 1 ;
  payload[ payloadLength] = ' ';
  payloadLength += 1 ;
  payloadLength += strlen( dtostrf(temperature, -1, 1, (char*)&payload[payloadLength]));
  payload[ payloadLength] = SensorReadingSeperator;
  payloadLength += sizeof(SensorReadingSeperator) ;

  // Copy the humidity into the payload
  payload[ payloadLength] = 'h';
  payloadLength += 1 ;
  payload[ payloadLength] = ' ';
  payloadLength += 1 ;
  payloadLength += strlen( dtostrf(humidity, -1, 0, (char *)&payload[payloadLength]));  

  // display info about payload then send it (No ACK) with LoRa unlike nRF24L01
  Serial.print( "RFM9X/SX127X Payload length:");
  Serial.print( payloadLength );
  Serial.println( " bytes" );

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

  Serial.println("Loop done");

  delay(LoopSleepDelaySeconds * 1000l);
}

In the Arduino debug monitor the messages from the device looked like this

Loop done
Loop called
T:17.5C H:94%
RFM9X/SX127X Payload length:30 bytes
Loop done
Loop called
T:17.5C H:95%
RFM9X/SX127X Payload length:30 bytes
Loop done
Loop called
T:17.6C H:95%
RFM9X/SX127X Payload length:30 bytes
Loop done

In the debug output window the messages from the device looked like this

The thread 0xf9c has exited with code 0 (0x0).
The thread 0x8ac has exited with code 0 (0x0).
09:53:53-RX From AdafruitM0 PacketSnr 10.3 Packet RSSI -51dBm RSSI -103dBm = 11 byte message "t 17.8,h 93"
Sensor AdafruitM0t Value 17.8
Sensor AdafruitM0h Value 93
AzureIoTHubClient SendEventAsync start
AzureIoTHubClient SendEventAsync finish
The thread 0x704 has exited with code 0 (0x0).
The thread 0xad4 has exited with code 0 (0x0).
09:54:03-RX From AdafruitM0 PacketSnr 9.8 Packet RSSI -52dBm RSSI -101dBm = 11 byte message "t 17.8,h 93"
Sensor AdafruitM0t Value 17.8
Sensor AdafruitM0h Value 93
AzureIoTHubClient SendEventAsync start
AzureIoTHubClient SendEventAsync finish
The thread 0x1084 has exited with code 0 (0x0).
The thread 0xa2c has exited with code 0 (0x0).
09:54:14-RX From AdafruitM0 PacketSnr 9.8 Packet RSSI -54dBm RSSI -102dBm = 11 byte message "t 17.7,h 93"
Sensor AdafruitM0t Value 17.7
Sensor AdafruitM0h Value 93
AzureIoTHubClient SendEventAsync start
AzureIoTHubClient SendEventAsync finish
The thread 0x1720 has exited with code 0 (0x0).

10:00:06-RX From AdafruitM0 PacketSnr 9.3 Packet RSSI -52dBm RSSI -100dBm = 12 byte message "t 184.0,h 91"
Sensor AdafruitM0t Value 184.0
Sensor AdafruitM0h Value 91
AzureIoTHubClient SendEventAsync start
AzureIoTHubClient SendEventAsync finish
The thread 0x15d4 has exited with code 0 (0x0).
The thread 0x15f4 has exited with code 0 (0x0).
10:00:19-RX From AdafruitM0 PacketSnr 10.0 Packet RSSI -48dBm RSSI -102dBm = 12 byte message "t 180.9,h 94"
Sensor AdafruitM0t Value 180.9
Sensor AdafruitM0h Value 94
AzureIoTHubClient SendEventAsync start
AzureIoTHubClient SendEventAsync finish
The thread 0xabc has exited with code 0 (0x0).
The thread 0x2e4 has exited with code 0 (0x0).
10:00:29-RX From AdafruitM0 PacketSnr 9.8 Packet RSSI -49dBm RSSI -102dBm = 12 byte message "t -50.0,h 94"
Sensor AdafruitM0t Value -50.0
Sensor AdafruitM0h Value 94
AzureIoTHubClient SendEventAsync start
AzureIoTHubClient SendEventAsync finish
The thread 0x950 has exited with code 0 (0x0).
The thread 0x145c has exited with code 0 (0x0).
The thread 0x176c has exited with code 0 (0x0).
10:00:39-RX From AdafruitM0 PacketSnr 9.5 Packet RSSI -50dBm RSSI -102dBm = 11 byte message "t 17.5,h 94"
Sensor AdafruitM0t Value 17.5
Sensor AdafruitM0h Value 94
AzureIoTHubClient SendEventAsync start
AzureIoTHubClient SendEventAsync finish
The thread 0x165c has exited with code 0 (0x0).
The thread 0x6e8 has exited with code 0 (0x0).
10:00:49-RX From AdafruitM0 PacketSnr 9.8 Packet RSSI -59dBm RSSI -100dBm = 11 byte message "t 17.5,h 95"
Sensor AdafruitM0t Value 17.5
Sensor AdafruitM0h Value 95
AzureIoTHubClient SendEventAsync start
AzureIoTHubClient SendEventAsync finish
The thread 0x1334 has exited with code 0 (0x0).
The thread 0x14d0 has exited with code 0 (0x0).
10:00:59-RX From AdafruitM0 PacketSnr 9.5 Packet RSSI -66dBm RSSI -102dBm = 11 byte message "t 17.6,h 95"
Sensor AdafruitM0t Value 17.6
Sensor AdafruitM0h Value 95
AzureIoTHubClient SendEventAsync start
AzureIoTHubClient SendEventAsync finish

Then at my Azure IoT hub the data stream looked like this
AdaFruitM0LoRaFeatherIoTHub

To reduce power consumption I would disconnect/remove the light emitting diode(LED)