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
public class Config
{
public const string DeviceID = "RAK11200-RAK19001";
public const string SasSignature = "..."; // sig
public const string SasExpiryTime = "..."; // se
public const string AzureIoTHubHostName = "..";
public const string Ssid = "...";
public const string Password = "..";
...
}
_httpClient = new HttpClient
{
SslProtocols = System.Net.Security.SslProtocols.Tls12,
HttpsAuthentCert = new X509Certificate(Config.DigiCertBaltimoreCyberTrustRoot),
BaseAddress = new Uri($"https://{Config.AzureIoTHubHostName}.azure-devices.net/devices/{Config.DeviceID}/messages/events?api-version=2020-03-13"),
};
string sasKey = $"SharedAccessSignature sr={Config.AzureIoTHubHostName}.azure-devices.net%2Fdevices%2F{Config.DeviceID}&sig={Config.SasSignature}&se={Config.SasExpiryTime}";
_httpClient.DefaultRequestHeaders.Add("Authorization", sasKey);
I then used Visual Studio 2022 Debugger to “single step” further into the BME680 code and the first thing that looked a bit odd was the TryReadTemperatureCore, TryReadPressureCore, TryReadHumidityCore and TryReadGasResistanceCore return values were ignored.
I then single stepped into the TryReadTemperatureCore which was returning a boolean indicating whether the read was success.
private bool TryReadTemperatureCore(out Temperature temperature)
{
if (TemperatureSampling == Sampling.Skipped)
{
temperature = default;
return false;
}
var temp = (int)Read24BitsFromRegister((byte)Bme680Register.TEMPDATA, Endianness.BigEndian);
temperature = CompensateTemperature(temp >> 4);
return true;
}
This library was based on the dotnet/iotBmxx80 code, it looked similar, but I missed an important detail lots more ?’s…
Console.WriteLine("Hello BME680!");
// The I2C bus ID on the Raspberry Pi 3.
const int busId = 1;
// set this to the current sea level pressure in the area for correct altitude readings
Pressure defaultSeaLevelPressure = WeatherHelper.MeanSeaLevel;
I2cConnectionSettings i2cSettings = new(busId, Bme680.DefaultI2cAddress);
I2cDevice i2cDevice = I2cDevice.Create(i2cSettings);
using Bme680 bme680 = new Bme680(i2cDevice, Temperature.FromDegreesCelsius(20.0));
while (true)
{
// reset will change settings back to default
bme680.Reset();
// 10 consecutive measurement with default settings
for (var i = 0; i < 10; i++)
{
// Perform a synchronous measurement
var readResult = bme680.Read();
// Print out the measured data
Console.WriteLine($"Gas resistance: {readResult.GasResistance?.Ohms:0.##}Ohm");
Console.WriteLine($"Temperature: {readResult.Temperature?.DegreesCelsius:0.#}\u00B0C");
Console.WriteLine($"Pressure: {readResult.Pressure?.Hectopascals:0.##}hPa");
Console.WriteLine($"Relative humidity: {readResult.Humidity?.Percent:0.#}%");
if (readResult.Temperature.HasValue && readResult.Pressure.HasValue)
{
var altValue = WeatherHelper.CalculateAltitude(readResult.Pressure.Value, defaultSeaLevelPressure, readResult.Temperature.Value);
Console.WriteLine($"Altitude: {altValue.Meters:0.##}m");
}
if (readResult.Temperature.HasValue && readResult.Humidity.HasValue)
{
// WeatherHelper supports more calculations, such as saturated vapor pressure, actual vapor pressure and absolute humidity.
Console.WriteLine($"Heat index: {WeatherHelper.CalculateHeatIndex(readResult.Temperature.Value, readResult.Humidity.Value).DegreesCelsius:0.#}\u00B0C");
Console.WriteLine($"Dew point: {WeatherHelper.CalculateDewPoint(readResult.Temperature.Value, readResult.Humidity.Value).DegreesCelsius:0.#}\u00B0C");
}
// when measuring the gas resistance on each cycle it is important to wait a certain interval
// because a heating plate is activated which will heat up the sensor without sleep, this can
// falsify all readings coming from the sensor
Thread.Sleep(1000);
}
...
}
The Bme680 Read() method checked the TryReadTemperatureCore, TryReadPressureCore, TryReadHumidityCore & TryReadGasResistanceCore return values.
/// <summary>
/// Performs a synchronous reading.
/// </summary>
/// <returns><see cref="Bme680ReadResult"/></returns>
public Bme680ReadResult Read()
{
SetPowerMode(Bme680PowerMode.Forced);
Thread.Sleep((int)GetMeasurementDuration(HeaterProfile).Milliseconds);
var tempSuccess = TryReadTemperatureCore(out var temperature);
var pressSuccess = TryReadPressureCore(out var pressure, skipTempFineRead: true);
var humiditySuccess = TryReadHumidityCore(out var humidity, skipTempFineRead: true);
var gasSuccess = TryReadGasResistanceCore(out var gasResistance);
return new Bme680ReadResult(tempSuccess ? temperature : null, pressSuccess ? pressure : null, humiditySuccess ? humidity : null, gasSuccess ? gasResistance : null);
}
Visual Studio 2022 Output Window Output window when application failed
When I connected to the device with Tera Term it confirmed that the device was in a “kernel panic” loop.
nanoFramework Kernel Panic loop captured with Tera Term
Before I could debug the BME680 sample I had to get the Bmxx80 & Bmxx80.sample projects to compile (update NuGet packages and remove NerdBank.GitVersioning references).
/// <summary>
/// Sets the power mode to the given mode
/// </summary>
/// <param name="powerMode">The <see cref="Bme680PowerMode"/> to set.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the power mode does not match a defined mode in <see cref="Bme680PowerMode"/>.</exception>
[Property("PowerMode")]
public void SetPowerMode(Bme680PowerMode powerMode)
{
//if (!powerMode.Equals(Bme680PowerMode.Forced) &&
// !powerMode.Equals(Bme680PowerMode.Sleep))
//{
// throw new ArgumentOutOfRangeException();
//}
var status = Read8BitsFromRegister((byte)Bme680Register.CTRL_MEAS);
status = (byte)((status & (byte)~Bme680Mask.PWR_MODE) | (byte)powerMode);
SpanByte command = new[]
{
(byte)Bme680Register.CTRL_MEAS, status
};
_i2cDevice.Write(command);
}
The first problem was the two powerMode.Equals statements used to validate the powerMode parameter around line 287 in Bme680.cs so I commented them out.
Exception when getting the “GasResistance” value
On start-up references to readResult.GasResistance.Ohms would regularly fail, so I commented out everywhere it was used.
Exception when getting the “Barometric Pressure” value
Then references to readResult.Pressure.Hectopascals would randomly fail, so I commented out everywhere it was used.
public static void RunSample()
{
Debug.WriteLine("Hello BME680!");
//////////////////////////////////////////////////////////////////////
Configuration.SetPinFunction(Gpio.IO04, DeviceFunction.I2C1_DATA);
Configuration.SetPinFunction(Gpio.IO05, DeviceFunction.I2C1_CLOCK);
// The I2C bus ID on the MCU.
const int busId = 1;
// set this to the current sea level pressure in the area for correct altitude readings
Pressure defaultSeaLevelPressure = WeatherHelper.MeanSeaLevel;
I2cConnectionSettings i2cSettings = new(busId, Bme680.DefaultI2cAddress);
I2cDevice i2cDevice = I2cDevice.Create(i2cSettings);
using Bme680 bme680 = new Bme680(i2cDevice, Temperature.FromDegreesCelsius(20.0));
while (true)
{
// reset will change settings back to default
bme680.Reset();
// 10 consecutive measurement with default settings
for (var i = 0; i < 10; i++)
{
// Perform a synchronous measurement
var readResult = bme680.Read();
// Print out the measured data
//Debug.WriteLine($"Gas resistance: {readResult.GasResistance.Ohms}Ohm");
Debug.WriteLine($"Temperature: {readResult.Temperature.DegreesCelsius}\u00B0C");
//Debug.WriteLine($"Pressure: {readResult.Pressure.Hectopascals}hPa");
Debug.WriteLine($"Relative humidity: {readResult.Humidity.Percent}%");
/*
if (!readResult.Temperature.Equals(null) && !readResult.Pressure.Equals(null))
{
var altValue = WeatherHelper.CalculateAltitude(readResult.Pressure, defaultSeaLevelPressure, readResult.Temperature);
Debug.WriteLine($"Altitude: {altValue.Meters}m");
}
if (!readResult.Temperature.Equals(null) && !readResult.Humidity.Equals(null))
{
// WeatherHelper supports more calculations, such as saturated vapor pressure, actual vapor pressure and absolute humidity.
Debug.WriteLine($"Heat index: {WeatherHelper.CalculateHeatIndex(readResult.Temperature, readResult. Humidity).DegreesCelsius}\u00B0C");
Debug.WriteLine($"Dew point: {WeatherHelper.CalculateDewPoint(readResult.Temperature, readResult.Humidity).DegreesCelsius}\u00B0C");
}
*/
// when measuring the gas resistance on each cycle it is important to wait a certain interval
// because a heating plate is activated which will heat up the sensor without sleep, this can
// falsify all readings coming from the sensor
Thread.Sleep(1000);
}
...
}
Visual Studio Debugger output displaying temperature and humidity values
The RAK2305 WisBlock Wifi Interface Module has the TX0 pin is connected to pin 11, RX0 is connected to pin 12, TX1 pin (Gpio.IO21) is connected to pin 34, and the RX1 pin (Gpio.IO21) is connected to pin 33 of the IO Slot connector (crossover TX & RX).
The RAK4630 Module is plugged into the CPU slot (BTB40_F) of the RAK5005 Base Board with TX pin 33 and RX pin 34. The RAK4630 Module UART2_TX pin is connected to 33, and the UART2_RX is connected to 34 on the CPU slot.
I then read the RAK4630 AT Command documentation to see if I could enable AT Commands on the second serial port
I had a look at the RAK4360 RAK Unified Interface (RUI) code to see if I could modify it so UART1 responded to AT Commands but I’m not certain this would work.
This is a longish post about failure, it took many hours to explore all the different approaches which was way longer than I should have spent. For why see “sunk cost fallacy”
I was using Azure IoT Explorer to monitor the telemetry and found that the initial versions of the application would fail after 6 or 7 hours. After reviewing the code I added a couple of “using” statements which appear to have fixed the problem as the soak test has been running for 12hrs, 24hrs, 36hrs, 48hrs, 96hrs…
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.