SparkFun Pro RF – LoRa, 915MHz Payload Addressing Client

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

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

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

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

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

  http://www.devmobile.co.nz

*/
#include <stdlib.h>

#include <LoRa.h>
#include <avr/dtostrf.h>
#include "DHT.h"

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

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

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

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

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

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

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

DHT dht(DHTPIN, DHTTYPE);


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

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

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

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

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

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

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


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

  SerialUSB.println("Loop called");

  PayloadReset();

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

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

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

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

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


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

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

  payloadLength = 0 ;

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

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

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


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

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

  memcpy( &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
  SerialUSB.print( " payloadLength:");
  SerialUSB.print( payloadLength);
  SerialUSB.println( );
#endif
}


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

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

  memcpy( &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
  SerialUSB.print( " payloadLength:");
  SerialUSB.print( payloadLength);
  SerialUSB.println( );
#endif
}


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

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

  memcpy( &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
  SerialUSB.print( " payloadLength:");
  SerialUSB.print( payloadLength);
  SerialUSB.println( );
#endif
}


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

  payloadLength = addressesLength + 1;

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

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