.NET nanoFramework RAK11200 – Brownout Voltage Revisited

The voltage my test setup was calculating looked wrong, then I realised that the sample calculation in the RAK Wireless forums wasn’t applicable to my setup.

I reassembled my RAK11200 WisBlock WiFi Module, RAK19001 WisBlock Base Board, RAK1901 WisBlock Temperature and Humidity Sensor, 1200mAH Lithium Polymer (LiPo) battery, SKU920100 Solar Board test setup, put a new 9V battery (I had forgotten to turn it off last-time) in my multimeter then collected some data. A=ReadValue(), C= ReadRatio(), E= measured battery voltage.

Excel spreadsheet for calculating ratio

I updated the formula used to calculate the battery voltage and deployed the application

public static void Main()
{
    Debug.WriteLine($"{DateTime.UtcNow:HH:mm:ss} devMobile.IoT.RAK.Wisblock.AzureIoTHub.RAK11200.PowerSleep starting");

    Thread.Sleep(5000);

    try
    {
        double batteryVoltage;

        Configuration.SetPinFunction(Gpio.IO04, DeviceFunction.I2C1_DATA);
        Configuration.SetPinFunction(Gpio.IO05, DeviceFunction.I2C1_CLOCK);

        Debug.WriteLine($"{DateTime.UtcNow:HH:mm:ss} Battery voltage measurement");

        // Configure Analog input (AIN0) port then read the "battery charge"
        AdcController adcController = new AdcController();

        using (AdcChannel batteryVoltageAdcChannel = adcController.OpenChannel(AdcControllerChannel))
        {
            batteryVoltage = batteryVoltageAdcChannel.ReadValue() / 723.7685;

            Debug.WriteLine($" BatteryVoltage {batteryVoltage:F2}");

            if (batteryVoltage < Config.BatteryVoltageBrownOutThreshold)
            {
                Sleep.EnableWakeupByTimer(Config.FailureRetryInterval);
                Sleep.StartDeepSleep();
            }
        }
        catch (Exception ex)
        {
...    
}

To test the accuracy of the voltage calculation I am going to run my setup on the office windowsill for a week regularly measuring the voltage. Then, turn the solar panel over (so the battery is not getting charged) and monitor the battery discharging until the RAK11200 WisBlock WiFi Module won’t connect to the network.

.NET nanoFramework RAK11200 – Brownout Voltage

My test setup was a RAK11200 WisBlock WiFi Module, RAK19001 WisBlock Base Board, RAK1901 WisBlock Temperature and Humidity Sensor, 1200mAH Lithium Polymer (LiPo) battery and SKU920100 Solar Board. The test setup uploads temperature, humidity and battery voltage telemetry to an Azure IoT Hub every 5 minutes (short delay so battery life reduced).

The first step was to check that I could get a “battery voltage” value for the RAKWireless RAK11200 WisBlock WiFi Module on a RAK19001 WisBlock Base Board for managing “brownouts” and send to my Azure IoT Hub.

RAK19001 Power supply schematic

The RAK19001 WisBlock Base Board has a voltage divider (R4&R5 with output ADC_VBAT) which is connected to pin 21(AIN0) on the CPU slot connector.

RAK19001 connector schematic

The RAK19001 WisBlock Base Board has quite a low leakage current so the majority of the power consumption should be the RAK11200 WisBlock WiFi Module.

RAK19001 leakage current from specifications

I used AdcController + AdcChannel to read AIN0 and modified the code using the formula (for a RAK4631 module) in the RAK Wireless forums to calculate the battery voltage. (UPDATE This calculation is not applicable to my scenario)

RAK11200 Schematic with battery voltage analog input highlighted

When “slept” the RAK11200 WisBlock WiFi Module power consumption is very low

RAK11200 low power current from specifications
public static void Main()
{
    Debug.WriteLine($"{DateTime.UtcNow:HH:mm:ss} devMobile.IoT.RAK.Wisblock.AzureIoTHub.RAK11200.PowerSleep starting");

    Thread.Sleep(5000); // This do debugger can attach consider removing in realease version

    try
    {
        double batteryVoltage;

        Configuration.SetPinFunction(Gpio.IO04, DeviceFunction.I2C1_DATA);
        Configuration.SetPinFunction(Gpio.IO05, DeviceFunction.I2C1_CLOCK);

        Debug.WriteLine($"{DateTime.UtcNow:HH:mm:ss} Battery voltage measurement");

        // Configure Analog input (AIN0) port then read the "battery charge"
        AdcController adcController = new AdcController();

        using (AdcChannel batteryVoltageAdcChannel = adcController.OpenChannel(AdcControllerChannel))
        {

            // https://forum.rakwireless.com/t/custom-li-ion-battery-voltage-calculation-in-rak4630/4401/7
            // When I checked with multimeter I had to increase 1.72 to 1.9
            batteryVoltage = batteryVoltageAdcChannel.ReadValue() * (3.0 / 4096) * 1.9;

            Debug.WriteLine($" BatteryVoltage {batteryVoltage:F2}");

            if (batteryVoltage < Config.BatteryVoltageBrownOutThreshold)
            {
                Sleep.EnableWakeupByTimer(Config.FailureRetryInterval);
                Sleep.StartDeepSleep();
            }
        }

        Debug.WriteLine($"{DateTime.UtcNow:HH:mm:ss} Wifi connecting");

        if (!WifiNetworkHelper.ConnectDhcp(Config.Ssid, Config.Password, requiresDateTime: true))
        {
            if (NetworkHelper.HelperException != null)
            {
                Debug.WriteLine($"{DateTime.UtcNow:HH:mm:ss} WifiNetworkHelper.ConnectDhcp failed {NetworkHelper.HelperException}");
            }

            Sleep.EnableWakeupByTimer(Config.FailureRetryInterval);
            Sleep.StartDeepSleep();
        }
        Debug.WriteLine($"{DateTime.UtcNow:HH:mm:ss} Wifi connected");

        // Configure the SHTC3 
        I2cConnectionSettings settings = new(I2cDeviceBusID, Shtc3.DefaultI2cAddress);

        string payload ;

        using (I2cDevice device = I2cDevice.Create(settings))
        using (Shtc3 shtc3 = new(device))
        {
            if (shtc3.TryGetTemperatureAndHumidity(out var temperature, out var relativeHumidity))
            {
                Debug.WriteLine($" Temperature {temperature.DegreesCelsius:F1}°C Humidity {relativeHumidity.Value:F0}% BatteryVoltage {batteryVoltage:F2}");

                payload = $"{{\"RelativeHumidity\":{relativeHumidity.Value:F0},\"Temperature\":{temperature.DegreesCelsius:F1}, \"BatteryVoltage\":{batteryVoltage:F2}}}";
            }
            else
            {
                Debug.WriteLine($" BatteryVoltage {batteryVoltage:F2}");

                payload = $"{{\"BatteryVoltage\":{batteryVoltage:F2}}}";
            }

#if SLEEP_SHT3C
            shtc3.Sleep();
#endif
        }

        // Configure the HttpClient uri, certificate, and authorization
        string uri = $"{Config.AzureIoTHubHostName}.azure-devices.net/devices/{Config.DeviceID}";

        HttpClient httpClient = new HttpClient()
        {
            SslProtocols = System.Net.Security.SslProtocols.Tls12,
            HttpsAuthentCert = new X509Certificate(Config.DigiCertBaltimoreCyberTrustRoot),
            BaseAddress = new Uri($"https://{uri}/messages/events?api-version=2020-03-13"),
        };
        httpClient.DefaultRequestHeaders.Add("Authorization", SasTokenGenerate(uri, Config.Key, DateTime.UtcNow.Add(Config.SasTokenRenewFor)));

        Debug.WriteLine($"{DateTime.UtcNow:HH:mm:ss} Azure IoT Hub device {Config.DeviceID} telemetry update start");

        HttpResponseMessage response = httpClient.Post("", new StringContent(payload));

        Debug.WriteLine($"{DateTime.UtcNow:HH:mm:ss} Response code:{response.StatusCode}");

        response.EnsureSuccessStatusCode();
    }
    catch (Exception ex)
    {
        Debug.WriteLine($"{DateTime.UtcNow:HH:mm:ss} Azure IoT Hub telemetry update failed:{ex.Message} {ex?.InnerException?.Message}");

        Sleep.EnableWakeupByTimer(Config.FailureRetryInterval);
        Sleep.StartDeepSleep();
    }

    Sleep.EnableWakeupByTimer(Config.TelemetryUploadInterval);
#if SLEEP_LIGHT
    Sleep.StartLightSleep();
#endif
#if SLEEP_DEEP
    Sleep.StartDeepSleep();
#endif
}

The nanoFramework.Hardware.Esp32.Sleep functionality supports LightSleep and DeepSleep states. The ESP32 device can be “woken up” by GPIO pin(s), Touch pad activity or by a Timer.

RAK11200+RAK19007+RAK1901+ LiPo battery test rig

After some “tinkering” I found the voltage calculation was surprisingly accurate (usually within 0.01V) for my RAK19001 and RAK19007 base boards.

When the battery voltage was close to its minimum working voltage of the ESP32 device it would reboot when the WifiNetworkHelper.ConnectDhcp method was called. This would quickly drain the battery flat even when the solar panel was trying to charge the battery.

Now, before trying to connect to the wireless network the battery voltage is checked and if too low (more experimentation required) the device goes into a deep sleep for a configurable period (more experimentation required). This is so the solar panel can charge the battery to a level where wireless connectivity will work.

Maduino LoRa Air Temperature and Soil Moisture

This is a demo MakerFabs Maduino LoRa Radio 868MHz client (based on Maduino LoRa 868MHz example) that uploads telemetry data to my Windows 10 IoT Core on Raspberry PI AdaFruit.IO and Azure IoT Hub field gateways.

The code is available on github

Sample hardware
Azure IoT Central data visualisation

The Maduino device in the picture is a custom version with an onboard Microchip ATSHA204 crypto and authentication chip (currently only use for the unique 72 bit serial number) and a voltage divider connected to the analog pin A6 to monitor the battery voltage.

There are compile time options ATSHA204 & BATTERY_VOLTAGE_MONITOR which can be used to selectively enable this functionality.

I use the Arduino lowpower library to aggressively sleep the device between measurements

// Adjust the delay so period is close to desired sec as possible, first do 8sec chunks. 
  int delayCounter = SensorUploadDelay / 8 ;
  for( int i = 0 ; i < delayCounter ; i++ )
  {
     LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);  
  }
  
  // Then to 4 sec chunk
  delayCounter =  ( SensorUploadDelay % 8 ) / 4;
  for( int i = 0 ; i < delayCounter ; i++ )
  {
     LowPower.powerDown(SLEEP_4S, ADC_OFF, BOD_OFF);  
  }

  // Then to 2 sec chunk
  delayCounter =  ( SensorUploadDelay % 4 ) / 2 ;
  for( int i = 0 ; i < delayCounter ; i++ )
  {
     LowPower.powerDown(SLEEP_2S, ADC_OFF, BOD_OFF);  
  }

  // Then to 1 sec chunk
  delayCounter =  ( SensorUploadDelay % 2 ) ;
  for( int i = 0 ; i < delayCounter ; i++ )
  {
     LowPower.powerDown(SLEEP_1S, ADC_OFF, BOD_OFF);  
  }
}

I use a spare digital PIN for powering the soil moisture probe so it can be powered down when not in use. I have included a short delay after powering up the device to allow the reading to settle.

  // Turn on soil mosture sensor, take reading then turn off to save power
  digitalWrite(SoilMoistureSensorEnablePin, HIGH);
  delay(SoilMoistureSensorEnableDelay);
  int soilMoistureADCValue = analogRead(SoilMoistureSensorPin);
  digitalWrite(SoilMoistureSensorEnablePin, LOW);
  int soilMoisture = map(soilMoistureADCValue,SoilMoistureSensorMinimum,SoilMoistureSensorMaximum, SoilMoistureValueMinimum, SoilMoistureValueMaximum); 
  PayloadAdd( "s", soilMoisture, false);

Bill of materials (Prices Nov 2019)

  • Maduino LoRa Radion (868MHz) 18.90
  • SHT20 I2C Temperature & Humidity Sensor (Waterproof Probe) USD22.50
  • Pinotech SoilWatch 10 – Soil moisture sensor USD23
  • Elecrow 1 Watt solar panel with wires USD3.80
  • 500 mAh LI-Ion battery

The software could easily be modified to support additional sensors.

Maduino LoRa Radio 868MHz Payload Addressing client

This is a demo MakerFabs Maduino LoRa Radio 868MHz client (based on one of the examples from Arduino-LoRa) that uploads telemetry data to my Windows 10 IoT Core on Raspberry PI AdaFruit.IO and Azure IoT Hub field gateways.

The code is available on Github

MaduinoLoRa86820180914
/*
Adapted from LoRa Duplex communication with Sync Word Sends temperature & humidity data from Seeedstudio https://www.seeedstudio.com/Grove-Temperature-Humidity-Sensor-High-Accuracy-Min-p-1921.html To my Windows 10 IoT Core RFM 9X library https://blog.devmobile.co.nz/2018/09/03/rfm9x-iotcore-payload-addressing/*/#include // include libraries#include#includeconst int csPin = 10; // LoRa radio chip selectconst int resetPin = 9; // LoRa radio resetconst int irqPin = 2; // change for your board; must be a hardware interrupt pin// Field gateway configurationconst char FieldGatewayAddress[] = "LoRaIoT1";const float FieldGatewayFrequency = 915000000.0;//const float FieldGatewayFrequency = 433000000.0;const byte FieldGatewaySyncWord = 0x12 ;// Payload configurationconst int PayloadSizeMaximum = 64 ;byte payload[PayloadSizeMaximum] = "";const byte SensorReadingSeperator = ',' ;// Manual serial number configurationconst char DeviceId[] = {"Maduino1"};const int LoopSleepDelaySeconds = 10 ;void setup() { Serial.begin(9600); while (!Serial); Serial.println("LoRa Setup"); // override the default CS, reset, and IRQ pins (optional) LoRa.setPins(csPin, resetPin, irqPin);// set CS, reset, IRQ pin if (!LoRa.begin(FieldGatewayFrequency)) { Serial.println("LoRa init failed. Check your connections."); while (true); } // Need to do this so field gateways pays attention to messsages from this device LoRa.enableCrc(); LoRa.setSyncWord(FieldGatewaySyncWord); //LoRa.dumpRegisters(Serial); Serial.println("LoRa Setup done."); // Configure the Seeedstudio TH02 temperature &amp;amp;amp;amp;amp;amp;amp;amp;amp; humidity sensor Serial.println("TH02 setup"); TH02.begin(); delay(100); Serial.println("TH02 Setup done"); Serial.println("Setup done");}void loop(){ int payloadLength = 0 ; float temperature ; float humidity ; Serial.println("Loop called"); memset(payload, 0, sizeof(payload)); // prepare the payload header with "To" Address length (top nibble) and "From" address length (bottom nibble) &lt;&lt; 4) | strlen( DeviceId ) ; payloadLength += 1; // Copy the "To" address into payload memcpy(&amp;amp;amp;amp;amp;amp;amp;amp;amp;payload[payloadLength], FieldGatewayAddress, strlen(FieldGatewayAddress)); payloadLength += strlen(FieldGatewayAddress) ; // Copy the "From" into payload memcpy(&amp;amp;amp;amp;amp;amp;amp;amp;amp;payload[payloadLength], DeviceId, strlen(DeviceId)); payloadLength += strlen(DeviceId) ; // Read the temperature and humidity values then display nicely temperature = TH02.ReadTemperature(); humidity = TH02.ReadHumidity(); Serial.print("T:"); Serial.print( temperature, 1 ) ; Serial.print( "C" ) ; Serial.print(" H:"); Serial.print( humidity, 0 ) ; Serial.println( "%" ) ; // Copy the temperature into the payload payload[ payloadLength] = 't'; payloadLength += 1 ; payload[ payloadLength] = ' '; payloadLength += 1 ; payloadLength += strlen( dtostrf(temperature, -1, 1, (char*)&amp;payload[payloadLength])); payload[ payloadLength] = SensorReadingSeperator; payloadLength += sizeof(SensorReadingSeperator) ; // Copy the humidity into the payload payload[ payloadLength] = 'h'; payloadLength += 1 ; payload[ payloadLength] = ' '; payloadLength += 1 ; payloadLength += strlen( dtostrf(humidity, -1, 0, (char *)&amp;[payloadLength])); // display info about payload then send it (No ACK) with LoRa unlike nRF24L01 Serial.print( "RFM9X/SX127X Payload length:"); Serial.print( payloadLength ); Serial.println( " bytes" ); LoRa.beginPacket(); LoRa.write( payload, payloadLength ); LoRa.endPacket(); Serial.println("Loop done"); delay(LoopSleepDelaySeconds * 1000l);}

In the debugging output the data looked like this

13:40:28-RX From Maduino1 PacketSnr 9.8 Packet RSSI -65dBm RSSI -110dBm = 11 byte message "t 33.7,h 51"
 Sensor Maduino1t Value 33.7
 Sensor Maduino1h Value 51
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish
The thread 0x268 has exited with code 0 (0x0).
The thread 0xb28 has exited with code 0 (0x0).
13:40:38-RX From Maduino1 PacketSnr 9.5 Packet RSSI -66dBm RSSI -112dBm = 11 byte message "t 33.9,h 51"
 Sensor Maduino1t Value 33.9
 Sensor Maduino1h Value 51
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish
13:40:49-RX From Maduino1 PacketSnr 9.5 Packet RSSI -66dBm RSSI -110dBm = 11 byte message "t 34.0,h 51"
 Sensor Maduino1t Value 34.0
 Sensor Maduino1h Value 51
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish


Bill of materials (Prices Sep 2018)


  • Maduino LoRa Radion (868MHz) USD14.10
  • Seeedstudio Temperature&Humidity Sensor USD11.50
  • 4 pin Female Jumper to Grove 4 pin Conversion Cable USD2.90
  • 1 Watt solar panel with wires USD3.80
  • 3000 mAh LI-Ion battery

There is also a 433MHz version available at the same price


The code is pretty basic, it shows how to pack the payload and set the necessary RFM9X/SX127X LoRa module configuration, has no power conservation, advanced wireless configuration etc.


The onboard sockets for battery and charging make the device easier to package and power in the field.


The Grove 4 pin Female Jumper to Grove 4 pin Conversion Cable was a quick & convenient way to get the I2C Grove temperature and humidity sensor connected up.


Then in my Azure IoT Hub monitoring software


MaduinoLoRaAzureIoT20180914