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
The first step was to bend the crimp connector locks back using a very small screwdriver.
Then split the heat-shrink and cable outer, so the individual cables were longer.
The crimp connector at the of each wire had to be “modified” by trimming them with a pair of wire cutters (just in front of the crimped section) so that they could be inserted into the breakout board connectors.
I couldn’t figure out why my code was failing so I gave up. Then the following day the application worked which was really odd. I then fired up my original Arduino PubSubClient library based application to do some testing and went to make a coffee. The application was failing to connect as XiaoTandHandCO2A and even when the bug was fixed (last will setup configuration commented out) it still wouldn’t connect.
Accidentally the configuration was changed XiaoTandHandCO2B and the application worked which was unexpected. I still couldn’t figure out why my code was failing XiaoTandHandCO2A so I gave up.
The following morning the XiaoTandHandCO2A configuration worked which was a bit odd. My best guess is that after a number of failed attempts the device is “disabled” for a while (but this is not displayed in the client configuration) and the following morning enough time had passed for the device to be “re-enabled”.
For testing I usually split my constants & secrets into two files, constants has the certificate chains for the Azure Event Grid MQTT Broker and secrets has certificate chains for devices and the Azure EventGrid MQTT Broker. I did consider have a secrets.h file for each device or splitting the device intermediate certificates, but as I was debugging and testing, it was just easier to duplicate them.
// constants.h
#pragma once
const uint16_t MQTT_PORT = 8883;
// This certificate is used to authenticate the host
static const char CA_ROOT_PEM[] PROGMEM = R"PEM(
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
)PEM";
To make deployments easier I have some software which stores the certificates in an optional “secure element” like the Microchip ATECC608C(The Seeedstudio Edgebox100 documentation refers to an optional Microchip ATECC680A but these are “not recommended for new designs”). This isn’t a great solution but deployments easier and is “better” than a string constant in the binary.
Still couldn’t figure out why my code was failing so I turned up logging to 11 and noticed a couple of messages which didn’t make sense. The device was connecting than disconnecting which indicated a another problem. As part of the Message Queue Telemetry Transport(MQTT) specification there is a “feature” Last Will and Testament(LWT) which a client can configure so that the MQTT broker sends a message to a topic if the device disconnects unexpectedly.
I was looking at the code and noticed that LWT was being used and that the topic didn’t exist in my Azure Event Grid MQTT Broker namespace. When the LWT configuration was commented out the application worked.
Paying close attention to the logging I noticed the “Subscribing to ssl/mqtts” followed by “Subscribe request sent”
I checked the sample application and found that if the connect was successful the application would then try and subscribe to a topic that didn’t exist.
The library doesn’t support client authentication with certificates, so I added two methods setClientCert and setClientKey to the esp-mqtt-arduino.h and esp-mqtt-arduino.cpp files
I tried increasing the log levels to get more debugging information, adding delays on startup to make it easier to see what was going on, trying different options of protocol support.
Over the last week or so I have been trying to get an Arduino application working which supports Message Queue Telemetry Transport(MQTT) properties which were added in V5. This specification was released March 2019 so I figured it would be commonly supported. In the Arduino ecosystem I was wrong and there is going to be a series of pastes about my “epic fail”.
The post is not about WolfMQTT but what I learnt about configuring it (and other) libraries for Arduino.
I wanted to set the message properties (for reasons) and in the .NET nanoFramework (and other .NET libraries) it wasn’t a problem.
while (true)
{
Console.WriteLine("MQTT publish message start...");
var payload = new MessagePayload() { ClientID = Secrets.MQTT_CLIENTID, Sequence = sequenceNumber++ };
string jsonPayload = JsonSerializer.SerializeObject(payload);
var result = mqttClient.Publish(topicPublish, Encoding.UTF8.GetBytes(jsonPayload), "application/json; charset=utf-8", null);
Debug.WriteLine($"MQTT published ({result}): {jsonPayload}");
Thread.Sleep(60000);
}
One of the options suggested by copilot was wolfMQTT(which uses an open/closed source model) but I found there was not an Arduino library for of this product. This was not unexpected due to the target audience of their products (but I did find there is a wolfSSL prebuilt library)
There were instructions on how to “re-organise” the source files into and Arduino friendly library
I don’t have an “easy” way of running bash on my dev box so I tried to do it manually (first of a series of mistakes).
#!/bin/sh
# this script will reformat the wolfSSL source code to be compatible with
# an Arduino project
# run as bash ./wolfssl-arduino.sh
DIR=${PWD##*/}
if [ "$DIR" = "ARDUINO" ]; then
rm -rf wolfMQTT
mkdir wolfMQTT
cp ../../src/*.c ./wolfMQTT
mkdir wolfMQTT/wolfmqtt
cp ../../wolfmqtt/*.h ./wolfMQTT/wolfmqtt
echo "/* Generated wolfMQTT header file for Arduino */" >> ./wolfMQTT/wolfMQTT.h
echo "#include <wolfmqtt/mqtt_client.h>" >> ./wolfMQTT/wolfMQTT.h
else
echo "ERROR: You must be in the IDE/ARDUINO directory to run this script"
fi
I copied the files into the specified folder tree and my compilation failed. I used a minimalist application to debug the compilation error.
I went back to the script file and realised that has missed creating a header file
I used a text editor to create the file which I saved into the src folder
I also checked that I had the same structure as wolfssl
The compilation was still failing so I turned on the Arduino verbose compiler output
After compilations I went back through the compiler output look for clues.
After a couple of failed compilations I paid attention to the error message.
I had the “case” of the header file name wrong, so I changed it to wolfMQTT.h
The compile then worked and the code executed.
After several hours of “fail” I now understand that case is important for header file names in Arduino(maybe due to Unix origins of some of the tools used). The naming of header file is also important so the library can be discovered.
Over the last couple of weekends I had been trying to get a repeatable process for extracting the X509 certificate information in the correct structure so my Arduino application could connect to Azure Event Grid. The first step was to get the certificate chain for my Azure Event Grid MQTT Broker with openssl
The CN: CN=DigiCert Global Root G3 and the wildcard CN=*.eventgrid.azure.net certificates were “concatenated” in the constants header file which is included in the main program file. The format of the certificate chain is described in the comments. Avoid blank lines, “rogue” spaces or other formatting as these may cause the WiFiClientSecureMbed TLS implementation to fail.
After a hard reset the WiFiClientSecure connect failed because the device time had not been initialised so the device/server time offset was too large (see rfc9325)
STM32F429 Discovery+ Dragino LoRa shield with Armtronix device
The code now works on STM32F429 Discovery and ESP32 WROOM platforms. (manual update nanoFramework.Hardware.Esp32 NuGet reference required)
Sparkfun LoRa Gateway 1 Channel schematic
One disadvantage of the SparkFun device is that the reset pin on the SX127X doesn’t appear to be connected to the ESP32 so I can’t factory reset the device in code.
//#define ST_STM32F429I_DISCOVERY //nanoff --target ST_STM32F429I_DISCOVERY --update
#define ESP32_WROOM_32_LORA_1_CHANNEL //nanoff --target ESP32_WROOM_32 --serialport COM4 --update
namespace devMobile.IoT.Rfm9x.TransmitBasic
{
using System;
using System.Text;
using System.Threading;
using Windows.Devices.Gpio;
using Windows.Devices.Spi;
#if ESP32_WROOM_32_LORA_1_CHANNEL
using nanoFramework.Hardware.Esp32;
#endif
public sealed class Rfm9XDevice
{
private SpiDevice rfm9XLoraModem;
private const byte RegisterAddressReadMask = 0X7f;
private const byte RegisterAddressWriteMask = 0x80;
public Rfm9XDevice(string spiPort, int chipSelectPin, int resetPin)
{
var settings = new SpiConnectionSettings(chipSelectPin)
{
ClockFrequency = 1000000,
//DataBitLength = 8,
Mode = SpiMode.Mode0,// From SemTech docs pg 80 CPOL=0, CPHA=0
SharingMode = SpiSharingMode.Shared,
};
rfm9XLoraModem = SpiDevice.FromId(spiPort, settings);
// Factory reset pin configuration
GpioController gpioController = GpioController.GetDefault();
GpioPin resetGpioPin = gpioController.OpenPin(resetPin);
resetGpioPin.SetDriveMode(GpioPinDriveMode.Output);
resetGpioPin.Write(GpioPinValue.Low);
Thread.Sleep(10);
resetGpioPin.Write(GpioPinValue.High);
Thread.Sleep(10);
}
public Rfm9XDevice(string spiPort, int chipSelectPin)
{
var settings = new SpiConnectionSettings(chipSelectPin)
{
ClockFrequency = 1000000,
Mode = SpiMode.Mode0,// From SemTech docs pg 80 CPOL=0, CPHA=0
SharingMode = SpiSharingMode.Shared,
};
rfm9XLoraModem = SpiDevice.FromId(spiPort, settings);
}
public Byte RegisterReadByte(byte registerAddress)
{
byte[] writeBuffer = new byte[] { registerAddress &= RegisterAddressReadMask, 0x0 };
byte[] readBuffer = new byte[writeBuffer.Length];
rfm9XLoraModem.TransferFullDuplex(writeBuffer, readBuffer);
return readBuffer[1];
}
public ushort RegisterReadWord(byte address)
{
byte[] writeBuffer = new byte[] { address &= RegisterAddressReadMask, 0x0, 0x0 };
byte[] readBuffer = new byte[writeBuffer.Length];
rfm9XLoraModem.TransferFullDuplex(writeBuffer, readBuffer);
return (ushort)(readBuffer[2] + (readBuffer[1] << 8));
}
public byte[] RegisterRead(byte address, int length)
{
byte[] writeBuffer = new byte[length + 1];
byte[] readBuffer = new byte[writeBuffer.Length];
byte[] repyBuffer = new byte[length];
writeBuffer[0] = address &= RegisterAddressReadMask;
rfm9XLoraModem.TransferFullDuplex(writeBuffer, readBuffer);
Array.Copy(readBuffer, 1, repyBuffer, 0, length);
return repyBuffer;
}
public void RegisterWriteByte(byte address, byte value)
{
byte[] writeBuffer = new byte[] { address |= RegisterAddressWriteMask, value };
byte[] readBuffer = new byte[writeBuffer.Length];
rfm9XLoraModem.TransferFullDuplex(writeBuffer, readBuffer);
}
public void RegisterWriteWord(byte address, ushort value)
{
byte[] valueBytes = BitConverter.GetBytes(value);
byte[] writeBuffer = new byte[] { address |= RegisterAddressWriteMask, valueBytes[0], valueBytes[1] };
byte[] readBuffer = new byte[writeBuffer.Length];
rfm9XLoraModem.TransferFullDuplex(writeBuffer,readBuffer);
}
public void RegisterWrite(byte address, byte[] bytes)
{
byte[] writeBuffer = new byte[1 + bytes.Length];
byte[] readBuffer = new byte[writeBuffer.Length];
Array.Copy(bytes, 0, writeBuffer, 1, bytes.Length);
writeBuffer[0] = address |= RegisterAddressWriteMask;
rfm9XLoraModem.TransferFullDuplex(writeBuffer, readBuffer);
}
public void RegisterDump()
{
Console.WriteLine("Register dump");
for (byte registerIndex = 0; registerIndex <= 0x42; registerIndex++)
{
byte registerValue = this.RegisterReadByte(registerIndex);
Console.WriteLine($"Register 0x{registerIndex:x2} - Value 0X{registerValue:x2}");
}
}
}
class Program
{
#if ST_STM32F429I_DISCOVERY
private const string SpiBusId = "SPI5";
#endif
#if ESP32_WROOM_32_LORA_1_CHANNEL
private const string SpiBusId = "SPI1";
#endif
static void Main()
{
int SendCount = 0;
#if ST_STM32F429I_DISCOVERY
int chipSelectPinNumber = PinNumber('C', 2);
int resetPinNumber = PinNumber('C', 3);
#endif
#if ESP32_WROOM_32_LORA_1_CHANNEL
int chipSelectPinNumber = Gpio.IO16;
#endif
try
{
#if ESP32_WROOM_32_LORA_1_CHANNEL
Configuration.SetPinFunction(Gpio.IO12, DeviceFunction.SPI1_MISO);
Configuration.SetPinFunction(Gpio.IO13, DeviceFunction.SPI1_MOSI);
Configuration.SetPinFunction(Gpio.IO14, DeviceFunction.SPI1_CLOCK);
Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SpiBusId, chipSelectPinNumber);
#endif
#if ST_STM32F429I_DISCOVERY
Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SpiBusId, chipSelectPinNumber, resetPinNumber);
#endif
Thread.Sleep(500);
// Put device into LoRa + Standby mode
rfm9XDevice.RegisterWriteByte(0x01, 0b10000001); // 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
rfm9XDevice.RegisterDump();
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
Console.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
Console.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
Console.WriteLine(".");
}
Console.WriteLine("");
rfm9XDevice.RegisterWriteByte(0x12, 0b00001000); // clear TxDone bit
Console.WriteLine("Send-Done");
Thread.Sleep(10000);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
#if ST_STM32F429I_DISCOVERY
static int PinNumber(char port, byte pin)
{
if (port < 'A' || port > 'J')
throw new ArgumentException();
return ((port - 'A') * 16) + pin;
}
#endif
}
}
When I initially ran the application in Visual Studio 2019 the text below was displayed in the output window.
Register dump
Register 0x00 - Value 0X00
Register 0x01 - Value 0X80
Register 0x02 - Value 0X1A
Register 0x03 - Value 0X0B
Register 0x04 - Value 0X00
…
Register 0x3E - Value 0X00
Register 0x3F - Value 0X00
Register 0x40 - Value 0X00
Register 0x41 - Value 0X00
Register 0x42 - Value 0X12
Sending 13 bytes message Hello LoRa 1!
Send-wait
.
.
.
.
.
Send-Done
Sending 13 bytes message Hello LoRa 2!
Send-wait
.
.
.
.
.
Send-Done
I could the see the messages arriving at the Armtronix device in the Arduino monitor.
The first message was getting corrupted (only when running in the debugger) which after some trial and error I think was most probably due to my RegOpMode register mode configuration.
SX127X RegOpMode details
// Put device into LoRa + Sleep mode
rfm9XDevice.RegisterWriteByte(0x01, 0b10000000);
// Put device into LoRa + Standby mode
rfm9XDevice.RegisterWriteByte(0x01, 0b10000001);
After a couple of years and half a dozen platform ports still finding bugs in my samples…