Random wanderings through Microsoft Azure esp. PaaS plumbing, the IoT bits, AI on Micro controllers, AI on Edge Devices, .NET nanoFramework, .NET Core on *nix and ML.NET+ONNX
When I initially deployed the RAK4200LoRaWANDeviceClient the RAK4200LoRaWAN-NetNF library failed in the OtaaInitialise method. I think this was caused by the “at+set_config=lora:work_mode:0” command rebooting the RAK4200 Module. I have commented out the code but may move it to a standalone method if required.
// Set the Working mode to LoRaWAN, not/never going todo P2P with this library.
#if DIAGNOSTICS
Debug.WriteLine($" {DateTime.UtcNow:hh:mm:ss} at+set_config=lora:work_mode:0");
#endif
Result result = SendCommand("Initialization OK", "at+set_config=lora:work_mode:0", CommandTimeoutDefault);
if (result != Result.Success)
{
#if DIAGNOSTICS
Debug.WriteLine($" {DateTime.UtcNow:hh:mm:ss} at+set_config=lora:work_mode:0 failed {result}");
#endif
return result;
}
I think it would be reasonable to assume that the device is in the correct mode (the default after a reset to factory) on startup so I removed the LoRa® network work mode configuration code.
// Set the Working mode to LoRaWAN, not/never going todo P2P with this library.
#if DIAGNOSTICS
Debug.WriteLine($" {DateTime.UtcNow:hh:mm:ss} AT+NWM=1");
#endif
Result result = SendCommand("Current Work Mode: LoRaWAN.", "AT+NWM=1", CommandTimeoutDefault);
if (result != Result.Success)
{
#if DIAGNOSTICS
Debug.WriteLine($" {DateTime.UtcNow:hh:mm:ss} AT+NWM=1 failed {result}");
#endif
return result;
}
I think it would be reasonable to assume that the device is in the correct mode (the default after a reset to factory) on startup so I removed the LoRa® network work mode configuration code.
Visual Studio Debug output for RAK3172LoRaWANDeviceClient minimal configuration
public static void Main()
{
Result result;
Debug.WriteLine("devMobile.IoT.RAK3172LoRaWANDeviceClient starting");
try
{
// set GPIO functions for COM2 (this is UART1 on ESP32)
#if ESP32_WROOM
Configuration.SetPinFunction(Gpio.IO17, DeviceFunction.COM2_TX);
Configuration.SetPinFunction(Gpio.IO16, DeviceFunction.COM2_RX);
#endif
Debug.Write("Ports:");
foreach (string port in SerialPort.GetPortNames())
{
Debug.Write($" {port}");
}
Debug.WriteLine("");
using (Rak3172LoRaWanDevice device = new Rak3172LoRaWanDevice())
{
result = device.Initialise(SerialPortId, 115200, Parity.None, 8, StopBits.One);
if (result != Result.Success)
{
Debug.WriteLine($"Initialise failed {result}");
return;
}
MessageSendTimer = new Timer(SendMessageTimerCallback, device, Timeout.Infinite, Timeout.Infinite);
device.OnJoinCompletion += OnJoinCompletionHandler;
device.OnReceiveMessage += OnReceiveMessageHandler;
#if CONFIRMED
device.OnMessageConfirmation += OnMessageConfirmationHandler;
#endif
#if FACTORY_RESET
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} FactoryReset");
result = device.FactoryReset();
if (result != Result.Success)
{
Debug.WriteLine($"FactoryReset failed {result}");
return;
}
#endif
#if DEVICE_DEVEUI_SET
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Device EUI");
result = device.DeviceEui(Config.devEui);
if (result != Result.Success)
{
Debug.WriteLine($"DeviceEUI set failed {result}");
return;
}
#endif
#if REGION_SET
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Region{Band}");
result = device.Band(Band);
if (result != Result.Success)
{
Debug.WriteLine($"Band on failed {result}");
return;
}
#endif
#if ADR_SET
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} ADR On");
result = device.AdrOn();
if (result != Result.Success)
{
Debug.WriteLine($"ADR on failed {result}");
return;
}
#endif
#if CONFIRMED
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Confirmed");
result = device.UplinkMessageConfirmationOn();
if (result != Result.Success)
{
Debug.WriteLine($"Confirm on failed {result}");
return;
}
#endif
#if UNCONFIRMED
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Unconfirmed");
result = device.UplinkMessageConfirmationOff();
if (result != Result.Success)
{
Debug.WriteLine($"Confirm off failed {result}");
return;
}
#endif
#if OTAA
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} OTAA");
result = device.OtaaInitialise(Config.JoinEui, Config.AppKey);
if (result != Result.Success)
{
Debug.WriteLine($"OTAA Initialise failed {result}");
return;
}
#endif
#if ABP
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} ABP");
result = device.AbpInitialise(Config.DevAddress, Config.NwksKey, Config.AppsKey);
if (result != Result.Success)
{
Debug.WriteLine($"ABP Initialise failed {result}");
return;
}
#endif
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Join start Timeout:{JoinTimeOut:hh:mm:ss}");
result = device.Join(JoinTimeOut);
if (result != Result.Success)
{
Debug.WriteLine($"Join failed {result}");
return;
}
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Join started");
Thread.Sleep(Timeout.Infinite);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
One of the major differences between the RAK4200 and RAK3127 libraries is the way a LoRaWAN network join is handled. The RAK4200 library Join method blocks until it succeeds of fails, the RAK3172 library Join method returns immediately then an EventHandler is called with the result.
I modified the NetworkJoinOTAA sample(based on the asynchronous version of BreakOutSerial) to send the required sequence of AT commands and displays the responses.
namespace devMobile.IoT.LoRaWAN.nanoFramework.RAK3172
{
using System;
using System.Diagnostics;
using System.IO.Ports;
using System.Threading;
#if ESP32_WROOM
using global::nanoFramework.Hardware.Esp32; //need NuGet nanoFramework.Hardware.Esp32
#endif
public class Program
{
#if ESP32_WROOM
private const string SerialPortId = "COM2";
#endif
#if ST_STM32F769I_DISCOVERY
private const string SerialPortId = "COM6";
#endif
private const string DevEui = "...";
private const string DevAddress = "...";
private const string NwksKey = "...";
private const string AppsKey = "...";
private const byte MessagePort = 1;
private const string Payload = "A0EEE456D02AFF4AB8BAFD58101D2A2A"; // Hello LoRaWAN
public static void Main()
{
Debug.WriteLine("devMobile.IoT.LoRaWAN.nanoFramework.RAK3172.NetworkJoinABP starting");
try
{
// set GPIO functions for COM2 (this is UART1 on ESP32)
#if ESP32_WROOM
Configuration.SetPinFunction(Gpio.IO17, DeviceFunction.COM2_TX);
Configuration.SetPinFunction(Gpio.IO16, DeviceFunction.COM2_RX);
#endif
Debug.Write("Ports:");
foreach (string port in SerialPort.GetPortNames())
{
Debug.Write($" {port}");
}
Debug.WriteLine("");
using (SerialPort serialPort = new SerialPort(SerialPortId))
{
// set parameters
serialPort.BaudRate = 115200;
serialPort.Parity = Parity.None;
serialPort.DataBits = 8;
serialPort.StopBits = StopBits.One;
serialPort.Handshake = Handshake.None;
serialPort.NewLine = "\r\n";
serialPort.ReadTimeout = 1000;
serialPort.DataReceived += SerialDevice_DataReceived;
serialPort.Open();
serialPort.WatchChar = '\n';
serialPort.ReadExisting(); // Running at 115K2 this was necessary
// Set the Device EUI
Console.WriteLine("Set Device EUI");
serialPort.WriteLine($"AT+DEVEUI={DevEui}");
// Set the Working mode to LoRaWAN
Console.WriteLine("Set Work mode");
serialPort.WriteLine("AT+NWM=1");
// Set the Region to AS923
Console.WriteLine("Set Region");
serialPort.WriteLine("AT+BAND=8");
// Set the JoinMode
Console.WriteLine("Set Join mode");
serialPort.WriteLine("AT+NJM=0");
// Set the DevAddress
Console.WriteLine("Set Device Address");
serialPort.WriteLine($"AT+DEVADDR={DevAddress}");
// Set the Network Session Key
Console.WriteLine("Set NwksKey");
serialPort.WriteLine($"AT+NWKSKEY={NwksKey}");
// Set the Application Session Key
Console.WriteLine("Set AppsKey");
serialPort.WriteLine($"AT+APPSKEY={AppsKey}");
// Set the Confirm flag
Console.WriteLine("Set Confirm off");
serialPort.WriteLine("AT+CFM=0");
// Join the network
Console.WriteLine("Start Join");
serialPort.WriteLine("AT+JOIN=1:0:10:2");
// Wait for the +EVT:JOINED
while (true)
{
Console.WriteLine("Sending");
serialPort.WriteLine($"AT+SEND={MessagePort}:{Payload}");
Thread.Sleep(300000);
}
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
private static void SerialDevice_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
SerialPort serialPort = (SerialPort)sender;
switch (e.EventType)
{
case SerialData.Chars:
break;
case SerialData.WatchChar:
string response = serialPort.ReadExisting();
Debug.Write(response);
break;
default:
Debug.Assert(false, $"e.EventType {e.EventType} unknown");
break;
}
}
}
}
The NetworkJoinABP application assumes that all of the AT commands succeed.
TTN Console live data tab connection process
Visual Studio Output windows displaying connection process and a D2C message
TTN Console live data tab connection process with a couple of D2C messages
Visual Studio Output windows displaying connection process and a couple of C2D messages
I then ran the RAK4200LoRaWANDeviceClient with DEVICE_DEVEUI_SET (devEui from label on the device), OTAA to configure the AppEui and AppKey and the device connected to The Things Network on the second attempt (typo in the DevEui).
public static void Main()
{
Result result;
Debug.WriteLine("devMobile.IoT.RAK4200LoRaWANDeviceClient starting");
try
{
// set GPIO functions for COM2 (this is UART1 on ESP32)
#if ESP32_WROOM
Configuration.SetPinFunction(Gpio.IO16, DeviceFunction.COM2_TX);
Configuration.SetPinFunction(Gpio.IO17, DeviceFunction.COM2_RX);
#endif
Debug.Write("Ports:");
foreach (string port in SerialPort.GetPortNames())
{
Debug.Write($" {port}");
}
Debug.WriteLine("");
using (Rak4200LoRaWanDevice device = new Rak4200LoRaWanDevice())
{
result = device.Initialise(SerialPortId, 9600, Parity.None, 8, StopBits.One);
if (result != Result.Success)
{
Debug.WriteLine($"Initialise failed {result}");
return;
}
#if CONFIRMED
device.OnMessageConfirmation += OnMessageConfirmationHandler;
#endif
device.OnReceiveMessage += OnReceiveMessageHandler;
#if FACTORY_RESET
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} FactoryReset");
result = device.FactoryReset();
if (result != Result.Success)
{
Debug.WriteLine($"FactoryReset failed {result}");
return;
}
#endif
#if DEVICE_DEVEUI_SET
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Device EUI");
result = device.DeviceEui(Config.devEui);
if (result != Result.Success)
{
Debug.WriteLine($"ADR on failed {result}");
return;
}
#endif
#if REGION_SET
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Region{Region}");
result = device.Region(Region);
if (result != Result.Success)
{
Debug.WriteLine($"Region on failed {result}");
return;
}
#endif
#if ADR_SET
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} ADR On");
result = device.AdrOn();
if (result != Result.Success)
{
Debug.WriteLine($"ADR on failed {result}");
return;
}
#endif
#if CONFIRMED
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Confirmed");
result = device.UplinkMessageConfirmationOn();
if (result != Result.Success)
{
Debug.WriteLine($"Confirm on failed {result}");
return;
}
#endif
#if UNCONFIRMED
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Unconfirmed");
result = device.UplinkMessageConfirmationOff();
if (result != Result.Success)
{
Debug.WriteLine($"Confirm off failed {result}");
return;
}
#endif
#if OTAA
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} OTAA");
result = device.OtaaInitialise(Config.JoinEui, Config.AppKey);
if (result != Result.Success)
{
Debug.WriteLine($"OTAA Initialise failed {result}");
return;
}
#endif
#if ABP
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} ABP");
result = device.AbpInitialise(Config.DevAddress, Config.NwksKey, Config.AppsKey);
if (result != Result.Success)
{
Debug.WriteLine($"ABP Initialise failed {result}");
return;
}
#endif
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Join start Timeout:{JoinTimeOut:hh:mm:ss}");
result = device.Join(JoinTimeOut);
if (result != Result.Success)
{
Debug.WriteLine($"Join failed {result}");
return;
}
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Join finish");
while (true)
{
#if PAYLOAD_BCD
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Send Timeout:{SendTimeout:hh:mm:ss} port:{MessagePort} payload BCD:{PayloadBcd}");
result = device.Send(MessagePort, PayloadBcd, SendTimeout);
#endif
#if PAYLOAD_BYTES
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Send Timeout:{SendTimeout:hh:mm:ss} port:{MessagePort} payload Bytes:{BitConverter.ToString(PayloadBytes)}");
result = device.Send(MessagePort, PayloadBytes, SendTimeout);
#endif
if (result != Result.Success)
{
Debug.WriteLine($"Send failed {result}");
}
Thread.Sleep(new TimeSpan(0, 5, 0));
}
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
Visual Studio Debug output for RAK4200LoRaWANDeviceClient minimal configuration connection
The Things Network “Live Data” for RAK4200LoRaWANDeviceClient minimal configuration connection
One of my client’s products has a configuration mode (button pressed as device starts) which enables a serial port (headers on board + FTDI module) for in field configuration of the onboard RAK4200 module.
The thread '' (0x2) has exited with code 0 (0x0).
devMobile.IoT.LoRaWAN.nanoFramework.RAK4200 BreakoutSerial starting
0 TX:at+get_config=lora:status bytes:25--------------------------------
OK Work Mode: LoRaWAN
Region: AS923
MulticastEnable: false
DutycycleEnable: false
Send_repeat_cnt: 0
Join_mode: OTAA
DevEui: 6..............9
AppEui: 7..............4
AppKey: A.............................9
Class: A
Joined Network:false
IsConfirm: unconfirm
AdrEnable: true
EnableRepeaterSupport: false
RX2_CHANNEL_FREQUENCY: 923200000, RX2_CHANNEL_DR:2
RX_WINDOW_DURATION: 3000ms
RECEIVE_DELAY_1: 1000ms
RECEIVE_DELAY_2: 2000ms
JOIN_ACCEPT_DELAY_1: 5000ms
JOIN_ACCEPT_DELAY_2: 6000ms
Current Datarate: 5
Primeval Datarate: 5
ChannelsTxPower: 0
UpLinkCounter: 0
DownLinkCounter: 0
I then reset the RAK4200 device to “factory defaults”
The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.IoT.LoRaWAN.nanoFramework.RAK4200 BreakoutSerial starting
Ports: COM5 COM6
0 TX:at+set_config=lora:default_parameters bytes:37--------------------------------
OK
The testrig would no longer connect as the device and network settings were invalid.
The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.IoT.LoRaWAN.nanoFramework.RAK4200 BreakoutSerial starting
Ports: COM5 COM6
0 TX:at+get_config=lora:status bytes:25--------------------------------
OK Work Mode: LoRaWAN
Region: AS923
MulticastEnable: false
DutycycleEnable: false
Send_repeat_cnt: 0
Join_mode: OTAA
DevEui: 0000000000000000
AppEui: 0000000000000000
AppKey: 00000000000000000000000000000000
Class: A
Joined Network:false
IsConfirm: unconfirm
AdrEnable: true
EnableRepeaterSupport: false
RX2_CHANNEL_FREQUENCY: 923200000, RX2_CHANNEL_DR:2
RX_WINDOW_DURATION: 3000ms
RECEIVE_DELAY_1: 1000ms
RECEIVE_DELAY_2: 2000ms
JOIN_ACCEPT_DELAY_1: 5000ms
JOIN_ACCEPT_DELAY_2: 6000ms
Current Datarate: 5
Primeval Datarate: 5
ChannelsTxPower: 0
UpLinkCounter: 0
DownLinkCounter: 0
To “restore” the device configuration I ran the RAK4200LoRaWANDeviceClient application with DEVICE_DEVEUI_SET, OTAA, UNCONFIRMED, REGION_SET and ADR_SET defined.
The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.IoT.LoRaWAN.nanoFramework.RAK4200 BreakoutSerial starting
Ports: COM2 COM3
0 TX:at+get_config=lora:status bytes:25--------------------------------
OK Work Mode: LoRaWAN
Region: AS923
MulticastEnable: false
DutycycleEnable: false
Send_repeat_cnt: 0
Join_mode: OTAA
DevEui: 6..............9
AppEui: 7.............4
AppKey: A.............................9
Class: A
Joined Network:false
IsConfirm: unconfirm
AdrEnable: true
EnableRepeaterSupport: false
RX2_CHANNEL_FREQUENCY: 923200000, RX2_CHANNEL_DR:2
RX_WINDOW_DURATION: 3000ms
RECEIVE_DELAY_1: 1000ms
RECEIVE_DELAY_2: 2000ms
JOIN_ACCEPT_DELAY_1: 5000ms
JOIN_ACCEPT_DELAY_2: 6000ms
Current Datarate: 5
Primeval Datarate: 5
ChannelsTxPower: 0
UpLinkCounter: 0
DownLinkCounter: 0
The testrig would then successfully connect to The Things Network. When the testrig was power cycled the device the configuration was retained.
I modified the NetworkJoinOTAA sample(based on the asynchronous version of BreakOutSerial) to send the required sequence of AT commands and display the responses.
namespace devMobile.IoT.LoRaWAN.nanoFramework.RAK4200
{
using System;
using System.Diagnostics;
using System.IO.Ports;
using System.Text;
using System.Threading;
#if ESP32_WROOM
using global::nanoFramework.Hardware.Esp32; ///need NuGet nanoFramework.Hardware.Esp32
#endif
public class Program
{
#if ST_STM32F769I_DISCOVERY
private const string SerialPortId = "COM6";
#endif
#if ESP32_WROOM
private const string SerialPortId = "COM2";
#endif
private const string DevEui = "...";
private const string AppEui = "...";
private const string AppKey = "...";
private const byte MessagePort = 1;
private const string Payload = "01020304"; // Is AQIDBA==
public static void Main()
{
string response;
Debug.WriteLine("devMobile.IoT.Rak4200.NetworkJoinOTAA starting");
try
{
#if ESP32_WROOM
Configuration.SetPinFunction(Gpio.IO16, DeviceFunction.COM2_TX);
Configuration.SetPinFunction(Gpio.IO17, DeviceFunction.COM2_RX);
#endif
Debug.Write("Ports:");
foreach (string port in SerialPort.GetPortNames())
{
Debug.Write($" {port}");
}
Debug.WriteLine("");
using (SerialPort serialDevice = new SerialPort(SerialPortId))
{
// set parameters
serialDevice.BaudRate = 9600;
//_SerialPort.BaudRate = 115200;
serialDevice.Parity = Parity.None;
serialDevice.StopBits = StopBits.One;
serialDevice.Handshake = Handshake.None;
serialDevice.DataBits = 8;
serialDevice.ReadTimeout = 10000;
//serialDevice.ReadBufferSize = 128;
//serialDevice.ReadBufferSize = 256;
serialDevice.ReadBufferSize = 512;
//serialDevice.ReadBufferSize = 1024;
serialDevice.NewLine = "\r\n";
serialDevice.DataReceived += SerialDevice_DataReceived;
serialDevice.Open();
serialDevice.WatchChar = '\n';
// clear out the RX buffer
serialDevice.ReadExisting();
response = serialDevice.ReadExisting();
Debug.WriteLine($"Response :{response.Trim()} bytes:{response.Length}");
Thread.Sleep(500);
// Set the Working mode to LoRaWAN
Debug.WriteLine("lora:work_mode:0");
serialDevice.WriteLine("at+set_config=lora:work_mode:0");
Thread.Sleep(1500);
// Set the JoinMode
Debug.WriteLine("lora:join_mode");
serialDevice.WriteLine("at+set_config=lora:join_mode:0");
Thread.Sleep(500);
// Set the Class
Debug.WriteLine("lora:class");
serialDevice.WriteLine("at+set_config=lora:class:0");
Thread.Sleep(500);
// Set the Region to AS923
Debug.WriteLine("lora:region");
serialDevice.WriteLine("at+set_config=lora:region:AS923");
Thread.Sleep(500);
// Set the devEUI
Debug.WriteLine("lora:dev_eui");
serialDevice.WriteLine($"at+set_config=lora:dev_eui:{DevEui}");
Thread.Sleep(500);
// Set the appEUI
Debug.WriteLine("lora:app_eui");
serialDevice.WriteLine($"at+set_config=lora:app_eui:{AppEui}");
Thread.Sleep(500);
// Set the appKey
Debug.WriteLine("lora:app_key");
serialDevice.WriteLine($"at+set_config=lora:app_key:{AppKey}");
Thread.Sleep(500);
// Set the Confirm flag
Debug.WriteLine("lora:confirm");
serialDevice.WriteLine("at+set_config=lora:confirm:0");
Thread.Sleep(500);
Debug.WriteLine("lora:adr");
serialDevice.WriteLine("at+set_config=lora:adr:1");
Thread.Sleep(500);
// Join the network
Debug.WriteLine("at+join");
serialDevice.WriteLine("at+join");
Thread.Sleep(10000);
byte counter = 1;
while (true)
{
// Send the BCD messages
string payload = $"{Payload}{counter:X2}";
Debug.WriteLine($"at+send=lora:{MessagePort}:{payload}");
serialDevice.WriteLine($"at+send=lora:{MessagePort}:{payload}");
counter += 1;
Thread.Sleep(300000);
}
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
private static void SerialDevice_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
SerialPort serialPort = (SerialPort)sender;
string response;
switch (e.EventType)
{
case SerialData.Chars:
break;
case SerialData.WatchChar:
response = serialPort.ReadExisting();
Debug.Write(response);
break;
default:
Debug.Assert(false, $"e.EventType {e.EventType} unknown");
break;
}
}
}
}
The NetworkJoinABP application assumes that all of the AT commands succeed.
TTN Console live data tab connection process
Visual Studio Output windows displaying connection process and a D2C message
In the new code a Thread reads lines of text from the SerialPort and processes them, checking for command responses, failures and downlink messages.
Unlike most of the devices I have worked with the RAK811 Join and Send commands are synchronous so return once the process has completed. The RAK811 responses also have quite a few empty, null prefixed or null suffixed lines which is a bit odd.
public void SerialPortProcessor()
{
string line;
while (CommandProcessResponses)
{
try
{
#if DIAGNOSTICS
Debug.WriteLine($" {DateTime.UtcNow:hh:mm:ss} ReadLine before");
#endif
line = SerialDevice.ReadLine().Trim('\0').Trim();
#if DIAGNOSTICS
Debug.WriteLine($" {DateTime.UtcNow:hh:mm:ss} ReadLine after:{line}");
#endif
// consume empty lines
if (String.IsNullOrWhiteSpace(line))
{
continue;
}
// Consume the response from set work mode
if (line.StartsWith("?LoRa (R)") || line.StartsWith("RAK811 ") || line.StartsWith("UART1 ") || line.StartsWith("UART3 ") || line.StartsWith("LoRa work mode"))
{
continue;
}
// See if device successfully joined network
if (line.StartsWith("OK Join Success"))
{
OnJoinCompletion?.Invoke(true);
CommandResponseExpectedEvent.Set();
continue;
}
if (line.StartsWith("at+recv="))
{
string[] payloadFields = line.Split("=,:".ToCharArray());
byte port = byte.Parse(payloadFields[1]);
int rssi = int.Parse(payloadFields[2]);
int snr = int.Parse(payloadFields[3]);
int length = int.Parse(payloadFields[4]);
if (this.OnMessageConfirmation != null)
{
OnMessageConfirmation?.Invoke(rssi, snr);
}
if (length > 0)
{
string payload = payloadFields[5];
if (this.OnReceiveMessage != null)
{
OnReceiveMessage.Invoke(port, rssi, snr, payload);
}
}
continue;
}
switch (line)
{
case "OK":
case "Initialization OK":
case "OK Wake Up":
case "OK Sleep":
CommandResult = Result.Success;
break;
case "ERROR: 1":
CommandResult = Result.ATCommandUnsuported;
break;
case "ERROR: 2":
CommandResult = Result.ATCommandInvalidParameter;
break;
case "ERROR: 3": //There is an error when reading or writing flash.
case "ERROR: 4": //There is an error when reading or writing through IIC.
CommandResult = Result.ErrorReadingOrWritingFlash;
break;
case "ERROR: 5": //There is an error when sending through UART
CommandResult = Result.ATCommandInvalidParameter;
break;
case "ERROR: 41": //The BLE works in an invalid state, so that it can’t be operated.
CommandResult = Result.ResponseInvalid;
break;
case "ERROR: 80":
CommandResult = Result.LoRaBusy;
break;
case "ERROR: 81":
CommandResult = Result.LoRaServiceIsUnknown;
break;
case "ERROR: 82":
CommandResult = Result.LoRaParameterInvalid;
break;
case "ERROR: 83":
CommandResult = Result.LoRaFrequencyInvalid;
break;
case "ERROR: 84":
CommandResult = Result.LoRaDataRateInvalid;
break;
case "ERROR: 85":
CommandResult = Result.LoRaFrequencyAndDataRateInvalid;
break;
case "ERROR: 86":
CommandResult = Result.LoRaDeviceNotJoinedNetwork;
break;
case "ERROR: 87":
CommandResult = Result.LoRaPacketToLong;
break;
case "ERROR: 88":
CommandResult = Result.LoRaServiceIsClosedByServer;
break;
case "ERROR: 89":
CommandResult = Result.LoRaRegionUnsupported;
break;
case "ERROR: 90":
CommandResult = Result.LoRaDutyCycleRestricted;
break;
case "ERROR: 91":
CommandResult = Result.LoRaNoValidChannelFound;
break;
case "ERROR: 92":
CommandResult = Result.LoRaNoFreeChannelFound;
break;
case "ERROR: 93":
CommandResult = Result.StatusIsError;
break;
case "ERROR: 94":
CommandResult = Result.LoRaTransmitTimeout;
break;
case "ERROR: 95":
CommandResult = Result.LoRaRX1Timeout;
break;
case "ERROR: 96":
CommandResult = Result.LoRaRX2Timeout;
break;
case "ERROR: 97":
CommandResult = Result.LoRaRX1ReceiveError;
break;
case "ERROR: 98":
CommandResult = Result.LoRaRX2ReceiveError;
break;
case "ERROR: 99":
CommandResult = Result.LoRaJoinFailed;
break;
case "ERROR: 100":
CommandResult = Result.LoRaDownlinkRepeated;
break;
case "ERROR: 101":
CommandResult = Result.LoRaPayloadSizeNotValidForDataRate;
break;
case "ERROR: 102":
CommandResult = Result.LoRaTooManyDownlinkFramesLost;
break;
case "ERROR: 103":
CommandResult = Result.LoRaAddressFail;
break;
case "ERROR: 104":
CommandResult = Result.LoRaMicVerifyError;
break;
default:
CommandResult = Result.ResponseInvalid;
break;
}
}
catch (TimeoutException)
{
// Intentionally ignored, not certain this is a good idea
}
CommandResponseExpectedEvent.Set();
}
}
After a lot of testing I think my thread based approach works reliably. Initially, I was having some signal strength issues because I had forgotten to configure the external antenna. I need to add some validation to the metrics and payload field unpacking (though I’m not certain what todo if they are the wrong format).
//---------------------------------------------------------------------------------
// Copyright (c) September 2021, 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.
//
// Must have one of following options defined in the project\build definitions
// PAYLOAD_BCD or PAYLOAD_BYTES
// OTAA or ABP
//
// Optional definitions
// CONFIRMED For confirmed messages
// DEVEUI_SET
//
//---------------------------------------------------------------------------------
namespace devMobile.IoT.LoRaWAN.NetCore.RAK3172
{
using System;
using System.IO.Ports;
using System.Threading;
public class Program
{
private const string SerialPortId = "/dev/ttyS0";
private const LoRaClass Class = LoRaClass.A;
private const string Band = "8-1";
private const byte MessagePort = 10;
private static readonly TimeSpan MessageSendTimerDue = new TimeSpan(0, 0, 15);
private static readonly TimeSpan MessageSendTimerPeriod = new TimeSpan(0, 5, 0);
private static Timer MessageSendTimer ;
private const int JoinRetryAttempts = 2;
private const int JoinRetryIntervalSeconds = 10;
#if PAYLOAD_BCD
private const string PayloadBcd = "48656c6c6f204c6f526157414e"; // Hello LoRaWAN in BCD
#endif
#if PAYLOAD_BYTES
private static readonly byte[] PayloadBytes = { 0x48, 0x65 , 0x6c, 0x6c, 0x6f, 0x20, 0x4c, 0x6f, 0x52, 0x61, 0x57, 0x41, 0x4e}; // Hello LoRaWAN in bytes
#endif
public static void Main()
{
Result result;
Console.WriteLine("devMobile.IoT.LoRaWAN.NetCore.RAK3172 RAK3712LoRaWANDeviceClient starting");
Console.WriteLine($"Serial ports:{String.Join(",", SerialPort.GetPortNames())}");
try
{
using (Rak3172LoRaWanDevice device = new Rak3172LoRaWanDevice())
{
result = device.Initialise(SerialPortId, 9600, Parity.None, 8, StopBits.One);
if (result != Result.Success)
{
Console.WriteLine($"Initialise failed {result}");
return;
}
MessageSendTimer = new Timer(SendMessageTimerCallback, device,Timeout.Infinite, Timeout.Infinite);
device.OnJoinCompletion += OnJoinCompletionHandler;
device.OnReceiveMessage += OnReceiveMessageHandler;
#if CONFIRMED
device.OnMessageConfirmation += OnMessageConfirmationHandler;
#endif
#if DEVEUI_SET
Console.WriteLine($"{DateTime.UtcNow:hh:mm:ss} DevEUI {Config.devEui}");
result = device.DeviceEui(Config.devEui);
if (result != Result.Success)
{
Console.WriteLine($"DevEUI failed {result}");
return;
}
#endif
Console.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Class {Class}");
result = device.Class(Class);
if (result != Result.Success)
{
Console.WriteLine($"Class failed {result}");
return;
}
Console.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Band {Band}");
result = device.Band(Band);
if (result != Result.Success)
{
Console.WriteLine($"Region failed {result}");
return;
}
Console.WriteLine($"{DateTime.UtcNow:hh:mm:ss} ADR On");
result = device.AdrOn();
if (result != Result.Success)
{
Console.WriteLine($"ADR on failed {result}");
return;
}
#if CONFIRMED
Console.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Confirmed");
result = device.UplinkMessageConfirmationOn();
if (result != Result.Success)
{
Console.WriteLine($"Confirm on failed {result}");
return;
}
#else
Console.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Unconfirmed");
result = device.UplinkMessageConfirmationOff();
if (result != Result.Success)
{
Console.WriteLine($"Confirm off failed {result}");
return;
}
#endif
#if OTAA
Console.WriteLine($"{DateTime.UtcNow:hh:mm:ss} OTAA");
result = device.OtaaInitialise(Config.JoinEui, Config.AppKey);
if (result != Result.Success)
{
Console.WriteLine($"OTAA Initialise failed {result}");
return;
}
#endif
#if ABP
Console.WriteLine($"{DateTime.UtcNow:hh:mm:ss} ABP");
result = device.AbpInitialise(Config.DevAddress, Config.NwksKey, Config.AppsKey);
if (result != Result.Success)
{
Console.WriteLine($"ABP Initialise failed {result}");
return;
}
#endif
Console.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Join start");
result = device.Join(JoinRetryAttempts, JoinRetryIntervalSeconds);
if (result != Result.Success)
{
Console.WriteLine($"Join failed {result}");
return;
}
Console.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Join started");
Thread.Sleep(Timeout.Infinite);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
private static void OnJoinCompletionHandler(bool result)
{
Console.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Join finished:{result}");
if (result)
{
MessageSendTimer.Change(MessageSendTimerDue, MessageSendTimerPeriod);
}
}
private static void SendMessageTimerCallback(object state)
{
Rak3172LoRaWanDevice device = (Rak3172LoRaWanDevice)state;
#if PAYLOAD_BCD
Console.WriteLine($"{DateTime.UtcNow:hh:mm:ss} port:{MessagePort} payload BCD:{PayloadBcd}");
Result result = device.Send(MessagePort, PayloadBcd );
#endif
#if PAYLOAD_BYTES
Console.WriteLine($"{DateTime.UtcNow:hh:mm:ss} port:{MessagePort} payload bytes:{Rak3172LoRaWanDevice.BytesToBcd(PayloadBytes)}");
Result result = device.Send(MessagePort, PayloadBytes);
#endif
if (result != Result.Success)
{
Console.WriteLine($"Send failed {result}");
}
}
#if CONFIRMED
private static void OnMessageConfirmationHandler()
{
Console.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Send successful");
}
#endif
private static void OnReceiveMessageHandler(byte port, int rssi, int snr, string payload)
{
byte[] payloadBytes = Rak3172LoRaWanDevice.HexToByes(payload); // Done this way so both conversion methods tested
Console.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Receive Message RSSI:{rssi} SNR:{snr} Port:{port} Payload:{payload} PayLoadBytes:{BitConverter.ToString(payloadBytes)}");
}
}
}
I have added XML Documentation comments which will need some rework, after I have “soak tested” the code for at least a week.
I have also added a method so the DevEUI can be set (intended for use after device firmware has been updated), fixed up my mistake with Binary Coded Decimal(BCD) vs. Hexadecimal strings.
I will also go back and apply the “learnings” from this refactoring to my other LoRaWAN module and platform libraries
Raspberry Pi3 with Grove Base Hat and RAK3172 Breakout (using UART2)
After some experimentation in the BreakOutSerial project I decided to reimplement the RAK3172 command processing. In the new code a Thread reads lines of text from the SerialPort and processes them. I have replaced the Join and Send(Confirmed) methods with ones that block only while the command are sent to the RAK3172. Then, when completed the OnJoinCompletion or OnMessagesConfirmation event handlers are called.
private Result SendCommand(string command)
{
if (command == null)
{
throw new ArgumentNullException(nameof(command));
}
if (command == string.Empty)
{
throw new ArgumentException($"command cannot be empty", nameof(command));
}
serialDevice.WriteLine(command);
this.CommandResponseExpectedEvent.Reset();
if (!this.CommandResponseExpectedEvent.WaitOne(CommandTimeoutDefaultmSec, false))
{
return Result.Timeout;
}
return CommandResult;
}
private void SerialPortProcessor()
{
string line;
while (CommandProcessResponses)
{
try
{
#if DIAGNOSTICS
Debug.WriteLine($" {DateTime.UtcNow:hh:mm:ss} ReadLine before");
#endif
line = serialDevice.ReadLine();
#if DIAGNOSTICS
Debug.WriteLine($" {DateTime.UtcNow:hh:mm:ss} ReadLine after:{line}");
#endif
// See if device successfully joined network
if (line.StartsWith("+EVT:JOINED"))
{
OnJoinCompletion?.Invoke(true);
continue;
}
// See if device failed ot join network
if (line.StartsWith("+EVT:JOIN FAILED"))
{
OnJoinCompletion?.Invoke(false);
continue;
}
// Applicable only if confirmed messages enabled
if (line.StartsWith("+EVT:SEND CONFIRMED OK"))
{
OnMessageConfirmation?.Invoke();
continue;
}
// Check for A/B/C downlink message
if (line.StartsWith("+EVT:RX_1") || line.StartsWith("+EVT:RX_2") || line.StartsWith("+EVT:RX_3") || line.StartsWith("+EVT:RX_C"))
{
// TODO beef up validation, nto certain what todo if borked
string[] metricsFields= line.Split(' ', ',');
int rssi = int.Parse(metricsFields[3]);
int snr = int.Parse(metricsFields[6]);
line = serialDevice.ReadLine();
#if DIAGNOSTICS
Debug.WriteLine($" {DateTime.UtcNow:HH:mm:ss} UNICAST :{line}");
#endif
line = serialDevice.ReadLine();
#if DIAGNOSTICS
Debug.WriteLine($" {DateTime.UtcNow:HH:mm:ss} Payload:{line}");
#endif
// TODO beef up validation, nto certain what todo if borked
string[] payloadFields = line.Split(':');
byte port = byte.Parse(payloadFields[1]);
string payload = payloadFields[2];
OnReceiveMessage?.Invoke(port, rssi, snr, payload);
continue;
}
#if DIAGNOSTICS
Debug.WriteLine($" {DateTime.UtcNow:hh:mm:ss} ReadLine Result");
#endif
line = serialDevice.ReadLine();
#if DIAGNOSTICS
Debug.WriteLine($" {DateTime.UtcNow:hh:mm:ss} ReadLine Result:{line}");
#endif
switch (line)
{
case "OK":
CommandResult = Result.Success;
break;
case "AT_ERROR":
CommandResult = Result.AtError;
break;
case "AT_PARAM_ERROR":
CommandResult = Result.ParameterError;
break;
case "AT_BUSY_ERROR":
CommandResult = Result.BusyError;
break;
case "AT_TEST_PARAM_OVERFLOW":
CommandResult = Result.ParameterOverflow;
break;
case "AT_NO_NETWORK_JOINED":
CommandResult = Result.NotJoined;
break;
case "AT_RX_ERROR":
CommandResult = Result.ReceiveError;
break;
case "AT_DUTYCYLE_RESTRICTED":
CommandResult = Result.DutyCycleRestricted;
break;
default:
CommandResult = Result.Undefined;
break;
}
CommandResponseExpectedEvent.Set();
}
catch (TimeoutException)
{
// Intentionally ignored, not certain this is a good idea
}
}
}
After a lot of testing I think my thread based approach works reliably. I also had to modify the code to shutdown the command processor thread and free any non managed resources.
/// <summary>
/// Ensures unmanaged serial port and thread resources are released in a "responsible" manner.
/// </summary>
public void Dispose()
{
CommandProcessResponses = false;
if (CommandResponsesProcessorThread != null)
{
CommandResponsesProcessorThread.Join();
CommandResponsesProcessorThread = null;
}
if (serialDevice != null)
{
serialDevice.Dispose();
serialDevice = null;
}
}
I need to add some validation to the metrics and payload field unpacking (though I’m not certain what todo if they are the wrong format) and review the handling of multi-line event messages.