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
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…
//---------------------------------------------------------------------------------
// Copyright (c) August 2022, 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.
//
// RAK Core WisBlock
// https://docs.rakwireless.com/Product-Categories/WisBlock/RAK11200
//
// RAK WisBlock Wireless
// https://docs.rakwireless.com/Product-Categories/WisBlock/RAK2305/Overview/
//
// RAK WisBlock Bases
// https://docs.rakwireless.com/Product-Categories/WisBlock/RAK5005-O
// https://docs.rakwireless.com/Product-Categories/WisBlock/RAK19001
//
// RAK WisBlock Sensor
// https://docs.rakwireless.com/Product-Categories/WisBlock/RAK1910
//
// Uses the library
// https://github.com/mboud/TinyGPSPlusNF
//
// Inspired by
// https://github.com/RAKWireless/WisBlock/tree/master/examples/common/sensors/RAK1910_GPS_UBLOX7
//
// Pins mapped with
// https://docs.rakwireless.com/Knowledge-Hub/Pin-Mapper/
//
// Flash device with
// nanoff --target ESP32_REV0 --serialport COM16 --update
//
//---------------------------------------------------------------------------------
namespace devMobile.IoT.RAK.Wisblock.RAK1910
{
using System;
using System.Device.Gpio;
using System.Diagnostics;
using System.IO.Ports;
using System.Threading;
using nanoFramework.Hardware.Esp32;
using TinyGPSPlusNF;
public class Program
{
private static TinyGPSPlus _gps;
public static void Main()
{
Debug.WriteLine($"devMobile.IoT.RAK.Wisblock.RAK1910 starting TinyGPS {TinyGPSPlus.LibraryVersion}");
try
{
#if RAK11200
Configuration.SetPinFunction(Gpio.IO21, DeviceFunction.COM2_TX);
Configuration.SetPinFunction(Gpio.IO19, DeviceFunction.COM2_RX);
#endif
#if RAK2350
Configuration.SetPinFunction(Gpio.IO21, DeviceFunction.COM2_RX);
Configuration.SetPinFunction(Gpio.IO19, DeviceFunction.COM2_TX);
#endif
_gps = new TinyGPSPlus();
// UART1 with default Max7Q baudrate
SerialPort serialPort = new SerialPort("COM2", 9600);
serialPort.DataReceived += SerialDevice_DataReceived;
serialPort.Open();
serialPort.WatchChar = '\n';
// Enable the GPS module GPS 3V3_S/RESET_GPS - IO2 - GPIO27
GpioController gpioController = new GpioController();
GpioPin Gps3V3 = gpioController.OpenPin(Gpio.IO27, PinMode.Output);
Gps3V3.Write(PinValue.High);
Debug.WriteLine("Waiting...");
Thread.Sleep(Timeout.Infinite);
}
catch (Exception ex)
{
Debug.WriteLine($"UBlox MAX7Q initialisation failed {ex.Message}");
Thread.Sleep(Timeout.Infinite);
}
}
private static void SerialDevice_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
// we only care if got EoL character
if (e.EventType != SerialData.WatchChar)
{
return;
}
SerialPort serialDevice = (SerialPort)sender;
string sentence = serialDevice.ReadExisting();
if (_gps.Encode(sentence))
{
if (_gps.Date.IsValid)
{
Debug.Write($"{_gps.Date.Year}-{_gps.Date.Month:D2}-{_gps.Date.Day:D2} ");
}
if (_gps.Time.IsValid)
{
Debug.Write($"{_gps.Time.Hour:D2}:{_gps.Time.Minute:D2}:{_gps.Time.Second:D2}.{_gps.Time.Centisecond:D2} ");
}
if (_gps.Location.IsValid)
{
Debug.Write($"Lat:{_gps.Location.Latitude.Degrees:F5}° Lon:{_gps.Location.Longitude.Degrees:F5}° ");
}
if (_gps.Altitude.IsValid)
{
Debug.Write($"Alt:{_gps.Altitude.Meters:F1}M ");
}
if (_gps.Location.IsValid)
{
Debug.Write($"Hdop:{_gps.Hdop.Value:F2}");
}
if (_gps.Date.IsValid || _gps.Time.IsValid || _gps.Location.IsValid || _gps.Altitude.IsValid)
{
Debug.WriteLine("");
}
}
}
}
}
After some experimentation I found that serial port TX/RX lines had to be reversed because both devices would normally be connected to a WisBlock core module.
//---------------------------------------------------------------------------------
// Copyright (c) September 2022, 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.
//
// https://docs.rakwireless.com/Product-Categories/WisBlock/RAK2305
//
// https://docs.rakwireless.com/Product-Categories/WisBlock/RAK11200
//
// https://store.rakwireless.com/products/rak1901-shtc3-temperature-humidity-sensor
//
// https://github.com/nanoframework/nanoFramework.IoT.Device/tree/develop/devices/Shtc3
//
//---------------------------------------------------------------------------------
namespace devMobile.IoT.RAK.Wisblock.RAK1901
{
using System;
using System.Diagnostics;
using System.Device.I2c;
using System.Threading;
using nanoFramework.Hardware.Esp32;
using Iot.Device.Shtc3;
public class Program
{
public static void Main()
{
Debug.WriteLine("devMobile.IoT.RAK.Wisblock.RAK11200RAK1901 starting");
try
{
// RAK11200 & RAK2305
Configuration.SetPinFunction(Gpio.IO04, DeviceFunction.I2C1_DATA);
Configuration.SetPinFunction(Gpio.IO05, DeviceFunction.I2C1_CLOCK);
I2cConnectionSettings settings = new(1, Shtc3.DefaultI2cAddress);
using (I2cDevice device = I2cDevice.Create(settings))
using (Shtc3 shtc3 = new(device))
{
while (true)
{
if (shtc3.TryGetTemperatureAndHumidity(out var temperature, out var relativeHumidity))
{
Debug.WriteLine($"Temperature {temperature.DegreesCelsius:F1}°C Humidity {relativeHumidity.Value:F0}%");
}
Thread.Sleep(10000);
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"SHTC3 initialisation or read failed {ex.Message}");
Thread.Sleep(Timeout.Infinite);
}
}
}
}
Visual Studio Output window displaying SHT31 temperature & humidity values
The RAK2305 Low Level Developer documentation described how to upload software developed with the Arduino tools by putting the ESP32 into “bootloader mode”. This is done by connecting (with the white jumper) the GPIO0 and GND pins on J14, and pressing the reset button.
The RAK2305 has has one onboard LED(TEST_LED) attached to IO18 which I added to the .NET nanoFrameworkBlinky sample.
//
// Copyright (c) .NET Foundation and Contributors
// See LICENSE file in the project root for full license information.
//
//
using System;
using System.Device.Gpio;
using System.Threading;
using nanoFramework.Hardware.Esp32;
namespace Blinky
{
public class Program
{
private static GpioController s_GpioController;
public static void Main()
{
s_GpioController = new GpioController();
// pick a board, uncomment one line for GpioPin; default is STM32F769I_DISCO
// DISCOVERY4: PD15 is LED6
//GpioPin led = s_GpioController.OpenPin(PinNumber('D', 15), PinMode.Output);
// ESP32 DevKit: 4 is a valid GPIO pin in, some boards like Xiuxin ESP32 may require GPIO Pin 2 instead.
//GpioPin led = s_GpioController.OpenPin(4, PinMode.Output);
// FEATHER S2:
//GpioPin led = s_GpioController.OpenPin(13, PinMode.Output);
// F429I_DISCO: PG14 is LEDLD4
//GpioPin led = s_GpioController.OpenPin(PinNumber('G', 14), PinMode.Output);
// NETDUINO 3 Wifi: A10 is LED onboard blue
//GpioPin led = s_GpioController.OpenPin(PinNumber('A', 10), PinMode.Output);
// QUAIL: PE15 is LED1
//GpioPin led = s_GpioController.OpenPin(PinNumber('E', 15), PinMode.Output);
// STM32F091RC: PA5 is LED_GREEN
//GpioPin led = s_GpioController.OpenPin(PinNumber('A', 5), PinMode.Output);
// STM32F746_NUCLEO: PB75 is LED2
//GpioPin led = s_GpioController.OpenPin(PinNumber('B', 7), PinMode.Output);
//STM32F769I_DISCO: PJ5 is LD2
//GpioPin led = s_GpioController.OpenPin(PinNumber('J', 5), PinMode.Output);
// ST_B_L475E_IOT01A: PB14 is LD2
//GpioPin led = s_GpioController.OpenPin(PinNumber('B', 14), PinMode.Output);
// STM32L072Z_LRWAN1: PA5 is LD2
//GpioPin led = s_GpioController.OpenPin(PinNumber('A', 5), PinMode.Output);
// TI CC13x2 Launchpad: DIO_07 it's the green LED
//GpioPin led = s_GpioController.OpenPin(7, PinMode.Output);
// TI CC13x2 Launchpad: DIO_06 it's the red LED
//GpioPin led = s_GpioController.OpenPin(6, PinMode.Output);
// ULX3S FPGA board: for the red D22 LED from the ESP32-WROOM32, GPIO5
//GpioPin led = s_GpioController.OpenPin(5, PinMode.Output);
// Silabs SLSTK3701A: LED1 PH14 is LLED1
//GpioPin led = s_GpioController.OpenPin(PinNumber('H', 14), PinMode.Output);
// RAK11200 on RAK5005
//GpioPin led = s_GpioController.OpenPin(Gpio.IO12, PinMode.Output); // LED1 Green
//GpioPin led = s_GpioController.OpenPin(Gpio.IO02, PinMode.Output); // LED2 Blue
// RAK11200 on RAK19001 needs battery connected or power switch in rechargeable position.
//GpioPin led = s_GpioController.OpenPin(Gpio.IO12, PinMode.Output); // LED1 Green
//GpioPin led = s_GpioController.OpenPin(Gpio.IO02, PinMode.Output); // LED2 Blue
// RAK2305
//GpioPin led = s_GpioController.OpenPin(Gpio.IO18, PinMode.Output); // LED Green (Test LED) on device
// RAK2305 On 5005 throws exceptions
//GpioPin led = s_GpioController.OpenPin(Gpio.IO34, PinMode.Output); // LED1 Green
//GpioPin led = s_GpioController.OpenPin(Gpio.IO35, PinMode.Output); // LED2 Blue
// RAK2305 On 17001 throws exceptions
//GpioPin led = s_GpioController.OpenPin(Gpio.IO34, PinMode.Output); // LED1 Green
//GpioPin led = s_GpioController.OpenPin(Gpio.IO35, PinMode.Output); // LED2 Blue
led.Write(PinValue.Low);
while (true)
{
led.Toggle();
Thread.Sleep(125);
led.Toggle();
Thread.Sleep(125);
led.Toggle();
Thread.Sleep(125);
led.Toggle();
Thread.Sleep(525);
}
}
static int PinNumber(char port, byte pin)
{
if (port < 'A' || port > 'J')
throw new ArgumentException();
return ((port - 'A') * 16) + pin;
}
}
}
I added the RAK2305 configuration to my version of the nanoFramework Blinky sample and could reliably flash the onboard LED.