TinyCLR OS LoRa library Part4

Transmit Basic

I had a couple of Armtronix IA005 SX1276 loRa nodes sitting on my desk from a recent post so I used one of them running a modified version of the Arduino LoRa library LoRaSetSyncWord example to receive messages from my device.

Armtronix and FEZT18-N TinyCLR testrig
/*
  LoRa Duplex communication with Sync Word
 
  Sends a message every half second, and polls continually
  for new incoming messages. Sets the LoRa radio's Sync Word.
 
  Spreading factor is basically the radio's network ID. Radios with different
  Sync Words will not receive each other's transmissions. This is one way you
  can filter out radios you want to ignore, without making an addressing scheme.
 
  See the Semtech datasheet, http://www.semtech.com/images/datasheet/sx1276.pdf
  for more on Sync Word.
 
  created 28 April 2017
  by Tom Igoe
*/
#include <stdlib.h>
#include <LoRa.h>
const int csPin = PA4;          // LoRa radio chip select
const int resetPin = PC13;       // LoRa radio reset
const int irqPin = PA11;         // change for your board; must be a hardware interrupt pin
 
byte msgCount = 0;            // count of outgoing messages
int interval = 2000;          // interval between sends
long lastSendTime = 0;        // time of last packet send
 
void setup() {
  Serial.begin(9600);                   // initialize serial
  while (!Serial);
 
  Serial.println("LoRa Duplex - Set sync word");
 
  // override the default CS, reset, and IRQ pins (optional)
  LoRa.setPins(csPin, resetPin, irqPin);// set CS, reset, IRQ pin
 
  if (!LoRa.begin(915E6)) {             // initialize ratio at 915 MHz
    Serial.println("LoRa init failed. Check your connections.");
    while (true);                       // if failed, do nothing
  }
 
  LoRa.setSyncWord(0x12);           // ranges from 0-0xFF, default 0x34, see API docs
 
  LoRa.dumpRegisters(Serial);
  Serial.println("LoRa init succeeded.");
}
 
void loop() {
  if (millis() - lastSendTime > interval) {
    String message = "HeLoRa World! ";   // send a message
    message += msgCount;
    sendMessage(message);
    Serial.println("Sending " + message);
    lastSendTime = millis();            // timestamp the message
    interval = random(1000) + 10000;    // 10-11 seconds
    msgCount++;
  }
 
  // parse for a packet, and call onReceive with the result:
  onReceive(LoRa.parsePacket());
}
 
void sendMessage(String outgoing) {
  LoRa.beginPacket();                   // start packet
  LoRa.print(outgoing);                 // add payload
  LoRa.endPacket();                     // finish packet and send it
  msgCount++;                           // increment message ID
}
 
void onReceive(int packetSize) {
  if (packetSize == 0) return;          // if there's no packet, return
 
  // read packet header bytes:
  String incoming = "";
 
  while (LoRa.available()) {
    incoming += (char)LoRa.read();
  }
 
  Serial.println("Message: " + incoming);
  Serial.println("RSSI: " + String(LoRa.packetRssi()));
  Serial.println("Snr: " + String(LoRa.packetSnr()));
  Serial.println();
}

The FEZT-18N application

   class Program
   {
      static void Main()
      {
         Rfm9XDevice rfm9XDevice = new Rfm9XDevice(FEZ.GpioPin.D10, FEZ.GpioPin.D9);
         int SendCount = 0;

         // Put device into LoRa + Sleep mode
         rfm9XDevice.RegisterWriteByte(0x01, 0b10000000); // RegOpMode 

         // Set the frequency to 915MHz
         byte[] frequencyWriteBytes = { 0xE4, 0xC0, 0x00 }; // RegFrMsb, RegFrMid, RegFrLsb
         rfm9XDevice.RegisterWrite(0x06, frequencyWriteBytes);

         // More power PA Boost
         rfm9XDevice.RegisterWriteByte(0x09, 0b10000000); // RegPaConfig

         while (true)
         {
            rfm9XDevice.RegisterWriteByte(0x0E, 0x0); // RegFifoTxBaseAddress 

            // Set the Register Fifo address pointer
            rfm9XDevice.RegisterWriteByte(0x0D, 0x0); // RegFifoAddrPtr 

            string messageText = $"Hello LoRa {SendCount += 1}!";
               
            // load the message into the fifo
            byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
            rfm9XDevice.RegisterWrite(0x0, messageBytes); // RegFifo

            // Set the length of the message in the fifo
            rfm9XDevice.RegisterWriteByte(0x22, (byte)messageBytes.Length); // RegPayloadLength

            Debug.WriteLine($"Sending {messageBytes.Length} bytes message {messageText}");
            /// Set the mode to LoRa + Transmit
            rfm9XDevice.RegisterWriteByte(0x01, 0b10000011); // RegOpMode 

            // Wait until send done, no timeouts in PoC
            Debug.WriteLine("Send-wait");
            byte IrqFlags = rfm9XDevice.RegisterReadByte(0x12); // RegIrqFlags
            while ((IrqFlags & 0b00001000) == 0)  // wait until TxDone cleared
            {
               Thread.Sleep(10);
               IrqFlags = rfm9XDevice.RegisterReadByte(0x12); // RegIrqFlags
               Debug.WriteLine(".");
            }
            rfm9XDevice.RegisterWriteByte(0x12, 0b00001000); // clear TxDone bit
            Debug.WriteLine("Send-Done");

            Thread.Sleep(30000);
         }
      }

When I ran the FEZ application in Visual Studio

'GHIElectronics.TinyCLR.VisualStudio.ProjectSystem.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.TinyCLR\TransmitBasic\bin\Debug\pe\..\GHIElectronics.TinyCLR.Native.dll'
'GHIElectronics.TinyCLR.VisualStudio.ProjectSystem.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.TinyCLR\TransmitBasic\bin\Debug\pe\..\GHIElectronics.TinyCLR.Devices.Gpio.dll'
'GHIElectronics.TinyCLR.VisualStudio.ProjectSystem.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.TinyCLR\TransmitBasic\bin\Debug\pe\..\GHIElectronics.TinyCLR.Devices.Spi.dll'
'GHIElectronics.TinyCLR.VisualStudio.ProjectSystem.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.TinyCLR\TransmitBasic\bin\Debug\pe\..\TransmitBasic.exe', Symbols loaded.
The thread '<No Name>' (0x2) has exited with code 0 (0x0).
Sending 13 bytes message Hello LoRa 1!
Send-wait
.
.
.
.
Send-Done
Sending 13 bytes message Hello LoRa 2!
Send-wait
.
.
.
.
Send-Done
Sending 13 bytes message Hello LoRa 3!
Send-wait
.
.
.
.
Send-Done
Sending 13 bytes message Hello LoRa 4!
Send-wait
.
.
.
.
Send-Done
Sending 13 bytes message Hello LoRa 5!
Send-wait
.
.
.
.
Send-Done

I could the see the messages arriving at the Armtronix device in the Arduino monitor.

19:22:28.445 -> Message: Hello LoRa 1!
19:22:28.445 -> RSSI: -36
19:22:28.479 -> Snr: 9.50
19:22:28.479 ->
19:22:29.362 -> Sending HeLoRa World! 48
19:22:40.390 -> Sending HeLoRa World! 50
19:22:50.654 -> Sending HeLoRa World! 52
19:22:58.484 -> Message: Hello LoRa 2!
19:22:58.484 -> RSSI: -36
19:22:58.553 -> Snr: 9.25
19:22:58.553 ->
19:23:01.254 -> Sending HeLoRa World! 54
19:23:11.873 -> Sending HeLoRa World! 56
19:23:22.103 -> Sending HeLoRa World! 58
19:23:28.558 -> Message: Hello LoRa 3!
19:23:28.558 -> RSSI: -31
19:23:28.592 -> Snr: 9.75
19:23:28.592 ->
19:23:32.307 -> Sending HeLoRa World! 60
19:23:42.929 -> Sending HeLoRa World! 62
19:23:53.392 -> Sending HeLoRa World! 64
19:23:58.622 -> Message: Hello LoRa 4!
19:23:58.622 -> RSSI: -36
19:23:58.655 -> Snr: 9.75
19:23:58.655 ->
19:24:03.629 -> Sending HeLoRa World! 66
19:24:14.299 -> Sending HeLoRa World! 68
19:24:24.989 -> Sending HeLoRa World! 70

This PoC code is getting a bit nasty with magic numbers and no error checking. The next step is getting a basic packet receive working…

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.