TinyCLR OS V2 nRF24L01 library Part2

After sorting out Serial Peripheral Interface(SPI) connectivity the next step porting the techfooninja nRF24L01P library to GHI Electronics TinyCLR was rewriting the initialisation code. Overall changes were minimal as the TinyCLR V2 SPI library has similar methods to the Windows 10 IoT Core ones.

SC20100 and MikroE nRF24 C Click

I need to refactor the initialise method so that failure exceptions are not caught and add the interrupt trigger edge so I can remove test from the handler.

      public void Initialize(string spiPortName, byte chipEnablePin, byte chipSelectPin, byte interruptPin, int clockFrequency = 2000000)
      {
         var gpio = GpioController.GetDefault();

         if (gpio == null)
         {
            Debug.WriteLine("GPIO Initialization failed.");
         }
         else
         {
            _cePin = gpio.OpenPin(chipEnablePin);
            _cePin.SetDriveMode(GpioPinDriveMode.Output);
            _cePin.Write(GpioPinValue.Low);

            _irqPin = gpio.OpenPin((byte)interruptPin);
            _irqPin.SetDriveMode(GpioPinDriveMode.InputPullUp);
            _irqPin.Write(GpioPinValue.High);
            _irqPin.ValueChanged += _irqPin_ValueChanged;
         }

         try
         {
            var settings = new SpiConnectionSettings()
            {
               ChipSelectType = SpiChipSelectType.Gpio,
               ChipSelectLine = gpio.OpenPin(chipSelectPin),
               Mode = SpiMode.Mode0,
               ClockFrequency = clockFrequency,
               ChipSelectActiveState = false,
            };

            SpiController controller = SpiController.FromName(spiPortName);
            _spiPort = controller.GetDevice(settings);
         }
         catch (Exception ex)
         {
            Debug.WriteLine("SPI Initialization failed. Exception: " + ex.Message);
            return;
         }

         // Module reset time
         Thread.Sleep(100);

         IsInitialized = true;

         // Set reasonable default values
         Address = Encoding.UTF8.GetBytes("NRF1");
         DataRate = DataRate.DR2Mbps;
         IsDynamicPayload = true;
         IsAutoAcknowledge = true;

         FlushReceiveBuffer();
         FlushTransferBuffer();
         ClearIrqMasks();
         SetRetries(5, 60);

         // Setup, CRC enabled, Power Up, PRX
         SetReceiveMode();
      }

The Initialise method gained parameters for the SPI port name and SPI clock frequency.

      static void Main()
      {
         RF24 Radio = new RF24();

         try
         {
            Radio.OnDataReceived += Radio_OnDataReceived;
            Radio.OnTransmitFailed += Radio_OnTransmitFailed;
            Radio.OnTransmitSuccess += Radio_OnTransmitSuccess;

            // SC20100.GpioPin.PD3
            Radio.Initialize(SC20100.SpiBus.Spi3, SC20100.GpioPin.PD4, SC20100.GpioPin.PD3, SC20100.GpioPin.PC5);
            Radio.Address = Encoding.UTF8.GetBytes(DeviceAddress);

            Radio.Channel = 15;
            //Radio.PowerLevel = PowerLevel.Max;
            //Radio.PowerLevel = PowerLevel.High;
            //Radio.PowerLevel = PowerLevel.Low;
            //Radio.PowerLevel = PowerLevel.Minimum
            Radio.DataRate = DataRate.DR250Kbps;
            //Radio.DataRate = DataRate.DR1Mbps;
            Radio.IsEnabled = true;

            Radio.IsAutoAcknowledge = true;
            Radio.IsDyanmicAcknowledge = false;
            Radio.IsDynamicPayload = true;

            Debug.WriteLine($"Address: {Encoding.UTF8.GetString(Radio.Address)}");
            Debug.WriteLine($"PowerLevel: {Radio.PowerLevel}");
            Debug.WriteLine($"IsAutoAcknowledge: {Radio.IsAutoAcknowledge}");
            Debug.WriteLine($"Channel: {Radio.Channel}");
            Debug.WriteLine($"DataRate: {Radio.DataRate}");
            Debug.WriteLine($"IsDynamicAcknowledge: {Radio.IsDyanmicAcknowledge}");
            Debug.WriteLine($"IsDynamicPayload: {Radio.IsDynamicPayload}");
            Debug.WriteLine($"IsEnabled: {Radio.IsEnabled}");
            Debug.WriteLine($"Frequency: {Radio.Frequency}");
            Debug.WriteLine($"IsInitialized: {Radio.IsInitialized}");
            Debug.WriteLine($"IsPowered: {Radio.IsPowered}");

            while (true)
            {
               string payload = "hello " + DateTime.Now.Second;
               Debug.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-TX {payload.Length} byte message {payload}");
               Radio.SendTo(Encoding.UTF8.GetBytes(BaseStationAddress), Encoding.UTF8.GetBytes(payload));

               Thread.Sleep(30000);
            }
         }
         catch (Exception ex)
         {
            Debug.WriteLine(ex.Message);

            return;
         }
      }

I can send and receive messages but the PowerLevel doesn’t look right so I need to apply fix from the Meadow version.

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
Address: Dev01
PowerLevel: 15
IsAutoAcknowledge: True
Channel: 15
DataRate: 2
IsDynamicAcknowledge: False
IsDynamicPayload: True
IsEnabled: False
Frequency: 2415
IsInitialized: True
IsPowered: True
00:00:01-TX 7 byte message hello 1
Data Sent!
00:00:01-TX Succeeded!
00:00:31-TX 8 byte message hello 31
Data Sent!
00:00:31-TX Succeeded!

TinyCLR OS V2 nRF24L01 library Part1

After debugging Windows 10 IoT Core, .NetMF and Wilderness Labs Meadow nRF24L01P libraries I figured yet another port, this time to a GHI Electronics Tiny CLR V2 powered device shouldn’t be “rocket science”.

This test rig uses SC20100S Dev and MikroE nRF C Click boards.

//---------------------------------------------------------------------------------
// Copyright (c) May 2020, 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.
//
//---------------------------------------------------------------------------------
namespace devMobile.IoT.nRf24L01.ModuleSPI
{
   using System;
   using System.Diagnostics;
   using System.Reflection;
   using System.Text;
   using System.Threading;

   using GHIElectronics.TinyCLR.Devices.Gpio;
   using GHIElectronics.TinyCLR.Devices.Spi;
   using GHIElectronics.TinyCLR.Pins;

   class Program
   {
      const byte SETUP_AW = 0x03;
      const byte RF_CH = 0x05;
      const byte RX_ADDR_P0 = 0x0A;
      const byte R_REGISTER = 0b00000000;
      const byte W_REGISTER = 0b00100000;
      const string P0_Address = "ZYXWV";
      static SpiDevice nrf24L01Device;

      static void Main()
      {
         try
         {
            GpioController gpioController = GpioController.GetDefault();

            var settings = new SpiConnectionSettings()
            {
               ChipSelectType = SpiChipSelectType.Gpio,
               //ChipSelectLine = FEZ.GpioPin.D10,
               ChipSelectLine = gpioController.OpenPin(SC20100.GpioPin.PD3),
               Mode = SpiMode.Mode0,
               //Mode = SpiMode.Mode1,
               //Mode = SpiMode.Mode2,
               //Mode = SpiMode.Mode3,
               ClockFrequency = 500000,
               //ChipSelectActiveState = true
               ChipSelectActiveState = false,
               //ChipSelectHoldTime = new TimeSpan(0, 0, 0, 0, 500),
               //ChipSelectSetupTime = new TimeSpan(0, 0, 0, 0, 500),
            };

            var spiController = SpiController.FromName(SC20100.SpiBus.Spi3);

            Debug.WriteLine("nrf24L01Device Device...");
            nrf24L01Device = spiController.GetDevice(settings);
            if (nrf24L01Device == null)
            {
               Debug.WriteLine("nrf24L01Device == null");
            }

            Thread.Sleep(100);

            Debug.WriteLine("ConfigureSpiPort Done...");
            Debug.WriteLine("");

            Thread.Sleep(500);
         }
         catch (Exception ex)
         {
            Debug.WriteLine("Configure SpiPort " + ex.Message);
         }

         try
         {
            // Read the Address width
            Debug.WriteLine("Read address width");
            byte[] txBuffer1 = new byte[] { SETUP_AW | R_REGISTER, 0x0 };
            byte[] rxBuffer1 = new byte[txBuffer1.Length];

            Debug.WriteLine(" nrf24L01Device.TransferFullDuplex...SETUP_AW");
            Debug.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer1));
            nrf24L01Device.TransferFullDuplex(txBuffer1, rxBuffer1);
            Debug.WriteLine(" rxBuffer:" + BitConverter.ToString(rxBuffer1));

            // Extract then adjust the address width
            byte addressWidthValue = rxBuffer1[1];
            addressWidthValue &= 0b00000011;
            addressWidthValue += 2;
            Debug.WriteLine($"Address width 0x{SETUP_AW:x2} - Value 0X{rxBuffer1[1]:x2} Value adjusted {addressWidthValue}");
            Debug.WriteLine("");

            // Write Pipe0 Receive address
            Debug.WriteLine($"Write Pipe0 Receive Address {P0_Address}");
            byte[] txBuffer2 = new byte[addressWidthValue + 1];
            txBuffer2[0] = RX_ADDR_P0 | W_REGISTER;
            Array.Copy(Encoding.UTF8.GetBytes(P0_Address), 0, txBuffer2, 1, addressWidthValue);

            Debug.WriteLine(" nrf24L01Device.Write...RX_ADDR_P0");
            Debug.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer2));
            nrf24L01Device.Write(txBuffer2);
            Debug.WriteLine("");

            // Read Pipe0 Receive address
            Debug.WriteLine("Read Pipe0 Receive address");
            byte[] txBuffer3 = new byte[addressWidthValue + 1];
            txBuffer3[0] = RX_ADDR_P0 | R_REGISTER;
            byte[] rxBuffer3 = new byte[txBuffer3.Length];

            Debug.WriteLine(" nrf24L01Device.TransferFullDuplex...RX_ADDR_P0");
            Debug.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer3));
            nrf24L01Device.TransferFullDuplex(txBuffer3, rxBuffer3);
            Debug.WriteLine(" rxBuffer:" + BitConverter.ToString(rxBuffer3));
            Debug.WriteLine($"Address 0x{RX_ADDR_P0:x2} Address {UTF8Encoding.UTF8.GetString(rxBuffer3, 1, addressWidthValue)}");
            Debug.WriteLine("");

            // Read the RF Channel
            Debug.WriteLine("RF Channel read 1");
            byte[] txBuffer4 = new byte[] { RF_CH | R_REGISTER, 0x0 };
            byte[] rxBuffer4 = new byte[txBuffer4.Length];

            Debug.WriteLine(" nrf24L01Device.TransferFullDuplex...RF_CH");
            Debug.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer4));
            nrf24L01Device.TransferFullDuplex(txBuffer4, rxBuffer4);
            Debug.WriteLine(" rxBuffer:" + BitConverter.ToString(rxBuffer4));

            ushort rfChannel1 = rxBuffer4[1];
            rfChannel1 += 2400;
            Debug.WriteLine($"RF Channel 1 0x{RF_CH:x2} - Value 0X{rxBuffer4[1]:x2} - Value adjusted {rfChannel1}");
            Debug.WriteLine("");

            // Write the RF Channel
            Debug.WriteLine("RF Channel write");
            byte[] txBuffer5 = new byte[] { RF_CH | W_REGISTER, rxBuffer4[1]+=1};

            Debug.WriteLine(" nrf24L01Device.Write...RF_CH");
            Debug.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer5));
            nrf24L01Device.Write(txBuffer5);
            Debug.WriteLine("");

            // Read the RF Channel
            Debug.WriteLine("RF Channel read 2");
            byte[] txBuffer6 = new byte[] { RF_CH | R_REGISTER, 0x0 };
            byte[] rxBuffer6 = new byte[txBuffer6.Length];

            Debug.WriteLine(" nrf24L01Device.TransferFullDuplex...RF_CH");
            Debug.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer6));
            nrf24L01Device.TransferFullDuplex(txBuffer6, rxBuffer6);
            Debug.WriteLine(" rxBuffer:" + BitConverter.ToString(rxBuffer6));

            ushort rfChannel2 = rxBuffer6[1];
            rfChannel2 += 2400;
            Debug.WriteLine($"RF Channel 2 0x{RF_CH:x2} - Value 0X{rxBuffer6[1]:x2} - Value adjusted {rfChannel2}");
            Debug.WriteLine("");
         }
         catch (Exception ex)
         {
            Debug.WriteLine("Configure Port0 " + ex.Message);
         }
      }
   }
}

After lots of tinkering with SPI configuration options I can read and write my nRF24L01 device receive port address

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
nrf24L01Device Device...
ConfigureSpiPort Done...

Read address width
 nrf24L01Device.TransferFullDuplex...SETUP_AW
 txBuffer:03-00
 rxBuffer:0E-03
Address width 0x03 - Value 0X03 Value adjusted 5

Write Pipe0 Receive Address ZYXWV
 nrf24L01Device.Write...RX_ADDR_P0
 txBuffer:2A-5A-59-58-57-56

Read Pipe0 Receive address
 nrf24L01Device.TransferFullDuplex...RX_ADDR_P0
 txBuffer:0A-00-00-00-00-00
 rxBuffer:0E-5A-59-58-57-56
Address 0x0a Address ZYXWV

RF Channel read 1
 nrf24L01Device.TransferFullDuplex...RF_CH
 txBuffer:05-00
 rxBuffer:0E-15
RF Channel 1 0x05 - Value 0X15 - Value adjusted 2421

RF Channel write
 nrf24L01Device.Write...RF_CH
 txBuffer:25-16

RF Channel read 2
 nrf24L01Device.TransferFullDuplex...RF_CH
 txBuffer:05-00
 rxBuffer:0E-16
RF Channel 2 0x05 - Value 0X16 - Value adjusted 2422

.Net Meadow nRF24L01 library Part3

While testing my initial port of the the techfooninja nRF24L01P library to a Wilderness Labs Meadow I noticed that the power level value was a bit odd.

nRF24L01P Test Harness
The program '[16720] App.exe' has exited with code 0 (0x0).
 IsPowered: True
 Address: Dev01
 PA: 15
 IsAutoAcknowledge: True
 Channel: 15
 DataRate: DR250Kbps
 Power: 15
 IsDynamicAcknowledge: False
 IsDynamicPayload: True
 IsEnabled: False
 Frequency: 2415
 IsInitialized: True
 IsPowered: True
 00:00:18-TX 8 byte message hello 17
 Data Sent!
00:00:18-TX Succeeded!
 00:00:48-TX 8 byte message hello 48
 Data Sent!

Looking at nRF24L01P datasheet and how this has been translated into code

/// <summary>
///   The power level for the radio.
/// </summary>
public PowerLevel PowerLevel
{
  get
   {
      var regValue = Execute(Commands.R_REGISTER, Registers.RF_SETUP, new byte[1])[1] & 0xF8;
      var newValue = (regValue - 1) >> 1;
      return (PowerLevel)newValue;
   }
  set
   {
      var regValue = Execute(Commands.R_REGISTER, Registers.RF_SETUP, new byte[1])[1] & 0xF8;

      byte newValue = (byte)((byte)value << 1 + 1);

      Execute(Commands.W_REGISTER, Registers.RF_SETUP,
              new[]
                  {
                     (byte) (newValue | regValue)
                  });
   }
}

The power level enumeration is declared in PowerLevel.cs

namespace Radios.RF24
{
    /// <summary>
    ///   Power levels the radio can operate with
    /// </summary>
    public enum PowerLevel : byte
    {
        /// <summary>
        ///   Minimum power setting for the radio
        /// </summary>
        Minimum = 0,

        /// <summary>
        ///   Low power setting for the radio
        /// </summary>
        Low,

        /// <summary>
        ///   High power setting for the radio
        /// </summary>
        High,

        /// <summary>
        ///   Max power setting for the radio
        /// </summary>
        Max,

        /// <summary>
        ///   Error with the power setting
        /// </summary>
        Error
    }
}

No debugging support or Debug.WriteLine in beta 3.7 (March 2020) so first step was to insert a Console.Writeline so I could see what the RF_SETUP register value was.

The program '[11212] App.exe' has exited with code 0 (0x0).
 Address: Dev01
 PowerLevel regValue 00100101
 PowerLevel: 15
 IsAutoAcknowledge: True
 Channel: 15
 DataRate: DR250Kbps
 IsDynamicAcknowledge: False
 IsDynamicPayload: True
 IsEnabled: False
 Frequency: 2415
 IsInitialized: True
 IsPowered: True
 00:00:18-TX 8 byte message hello 17
 Data Sent!
00:00:18-TX Succeeded!

The PowerLevel setting appeared to make no difference and the bits 5, 2 & 0 were set which meant 250Kbps & high power which I was expecting.

The RF_SETUP register in the datasheet, contains the following settings (WARNING – some nRF24L01 registers differ from nRF24L01P)

After looking at the code my initial “quick n dirty” fix was to mask out the existing power level bits and then mask in the new setting.

public PowerLevel PowerLevel
      {
         get
         {
            byte regValue = Execute(Commands.R_REGISTER, Registers.RF_SETUP, new byte[1])[1];;
            Console.WriteLine($"PowerLevel regValue {Convert.ToString(regValue, 2).PadLeft(8, '0')}");
            var newValue = (regValue & 0x06) >> 1;
            
            return (PowerLevel)newValue;
         }
         set
         {
            byte regValue = Execute(Commands.R_REGISTER, Registers.RF_SETUP, new byte[1])[1];
            regValue &= 0b11111000;
            regValue |= (byte)((byte)value << 1);

            Execute(Commands.W_REGISTER, Registers.RF_SETUP,
                    new[]
                        {
                            (byte)regValue
                        });
         }
      }

I wonder if the code mighty be simpler if I used a similar approach to my Windows 10 IoT RFM9X LoRa library

// RegModemConfig1
public enum RegModemConfigBandwidth : byte
{
	_7_8KHz = 0b00000000,
	_10_4KHz = 0b00010000,
	_15_6KHz = 0b00100000,
	_20_8KHz = 0b00110000,
	_31_25KHz = 0b01000000,
	_41_7KHz = 0b01010000,
	_62_5KHz = 0b01100000,
	_125KHz = 0b01110000,
	_250KHz = 0b10000000,
	_500KHz = 0b10010000
}
public const RegModemConfigBandwidth RegModemConfigBandwidthDefault = RegModemConfigBandwidth._125KHz;

...

[Flags]
enum RegIrqFlagsMask : byte
{
	RxTimeoutMask = 0b10000000,
	RxDoneMask = 0b01000000,
	PayLoadCrcErrorMask = 0b00100000,
	ValidHeadrerMask = 0b00010000,
	TxDoneMask = 0b00001000,
	CadDoneMask = 0b00000100,
	FhssChangeChannelMask = 0b00000010,
	CadDetectedMask = 0b00000001,
}

[Flags]
enum RegIrqFlags : byte
{
	RxTimeout = 0b10000000,
	RxDone = 0b01000000,
	PayLoadCrcError = 0b00100000,
	ValidHeadrer = 0b00010000,
	TxDone = 0b00001000,
	CadDone = 0b00000100,
	FhssChangeChannel = 0b00000010,
	CadDetected = 0b00000001,
	ClearAll = 0b11111111,
}

This would require some significant modifications to the Techfooninja library. e.g. the PowerLevel enumeration

namespace Radios.RF24
{
    /// <summary>
    ///   Power levels the radio can operate with
    /// </summary>
    public enum PowerLevel : byte
    {
        /// <summary>
        ///   Minimum power setting for the radio
        /// </summary>
        Minimum = 0b00000000,

        /// <summary>
        ///   Low power setting for the radio
        /// </summary>
        Low = 0b00000010,

        /// <summary>
        ///   High power setting for the radio
        /// </summary>
        High = 0b00000100,

        /// <summary>
        ///   Max power setting for the radio
        /// </summary>
        Max = 0b00000110,
    }
}

I need to do some more testing of the of library to see if the pattern is repeated.

Wilderness Labs nRF24L01 Wireless field gateway Meadow client

After a longish pause in development work on my nrf24L01 AdaFruit.IO and Azure IOT Hub field gateways I figured a client based on my port of the techfooninja nRF24 library to Wilderness Labs Meadow would be a good test.

This sample client is an Wilderness Labs Meadow with a Sensiron SHT31 Temperature & humidity sensor (supported by meadow foundation), and a generic nRF24L01 device connected with jumper cables.

Bill of materials (prices as at March 2020)

  • Wilderness Labs Meadow 7F Micro device USD50
  • Seeedstudio Temperature and Humidity Sensor(SHT31) USD11.90
  • Seeedstudio 4 pin Male Jumper to Grove 4 pin Conversion Cable USD2.90
  • 2.4G Wireless Module nRF24L01+PA USD9.90

The initial version of the code was pretty basic with limited error handling and no power conservation support.

namespace devMobile.IoT.FieldGateway.Client
{
   using System;
   using System.Text;
   using System.Threading;

   using Radios.RF24;

   using Meadow;
   using Meadow.Devices;
   using Meadow.Foundation.Leds;
   using Meadow.Foundation.Sensors.Atmospheric;
   using Meadow.Hardware;
   using Meadow.Peripherals.Leds;

   public class MeadowClient : App<F7Micro, MeadowClient>
   {
      private const string BaseStationAddress = "Base1";
      private const string DeviceAddress = "WLAB1";
      private const byte nRF24Channel = 15;
      private RF24 Radio = new RF24();
      private readonly TimeSpan periodTime = new TimeSpan(0, 0, 60);
      private readonly Sht31D sensor;
      private readonly ILed Led;

      public MeadowClient()
      {
         Led = new Led(Device, Device.Pins.OnboardLedGreen);

         try
         {
            sensor = new Sht31D(Device.CreateI2cBus());

            var config = new Meadow.Hardware.SpiClockConfiguration(
                           2000,
                           SpiClockConfiguration.Mode.Mode0);

            ISpiBus spiBus = Device.CreateSpiBus(
               Device.Pins.SCK,
               Device.Pins.MOSI,
               Device.Pins.MISO, config);

            Radio.OnDataReceived += Radio_OnDataReceived;
            Radio.OnTransmitFailed += Radio_OnTransmitFailed;
            Radio.OnTransmitSuccess += Radio_OnTransmitSuccess;

            Radio.Initialize(Device, spiBus, Device.Pins.D09, Device.Pins.D10, Device.Pins.D11);
            //Radio.Address = Encoding.UTF8.GetBytes(Environment.MachineName);
            Radio.Address = Encoding.UTF8.GetBytes(DeviceAddress);

            Radio.Channel = nRF24Channel;
            Radio.PowerLevel = PowerLevel.Low;
            Radio.DataRate = DataRate.DR250Kbps;
            Radio.IsEnabled = true;

            Radio.IsAutoAcknowledge = true;
            Radio.IsDyanmicAcknowledge = false;
            Radio.IsDynamicPayload = true;

            Console.WriteLine($"Address: {Encoding.UTF8.GetString(Radio.Address)}");
            Console.WriteLine($"PowerLevel: {Radio.PowerLevel}");
            Console.WriteLine($"IsAutoAcknowledge: {Radio.IsAutoAcknowledge}");
            Console.WriteLine($"Channel: {Radio.Channel}");
            Console.WriteLine($"DataRate: {Radio.DataRate}");
            Console.WriteLine($"IsDynamicAcknowledge: {Radio.IsDyanmicAcknowledge}");
            Console.WriteLine($"IsDynamicPayload: {Radio.IsDynamicPayload}");
            Console.WriteLine($"IsEnabled: {Radio.IsEnabled}");
            Console.WriteLine($"Frequency: {Radio.Frequency}");
            Console.WriteLine($"IsInitialized: {Radio.IsInitialized}");
            Console.WriteLine($"IsPowered: {Radio.IsPowered}");
         }
         catch (Exception ex)
         {
            Console.WriteLine(ex.Message);
         }

         while (true)
         {
            sensor.Update();

            Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-TX T:{sensor.Temperature:0.0}C H:{sensor.Humidity:0}%");

            Led.IsOn = true;

            string values = "T " + sensor.Temperature.ToString("F1") + ",H " + sensor.Humidity.ToString("F0");

            // Stuff the 2 byte header ( payload type & deviceIdentifierLength ) + deviceIdentifier into payload
            byte[] payload = new byte[1 + Radio.Address.Length + values.Length];
            payload[0] = (byte)((1 << 4) | Radio.Address.Length);
            Array.Copy(Radio.Address, 0, payload, 1, Radio.Address.Length);
            Encoding.UTF8.GetBytes(values, 0, values.Length, payload, Radio.Address.Length + 1);

            Radio.SendTo(Encoding.UTF8.GetBytes(BaseStationAddress), payload);

            Thread.Sleep(periodTime);
         }
      }

      private void Radio_OnDataReceived(byte[] data)
      {
         // Display as Unicode
         string unicodeText = Encoding.UTF8.GetString(data);
         Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-RX Unicode Length {0} Unicode Length {1} Unicode text {2}", data.Length, unicodeText.Length, unicodeText);

         // display as hex
         Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-RX Hex Length {data.Length} Payload {BitConverter.ToString(data)}");
      }

      private void Radio_OnTransmitSuccess()
      {
         Led.IsOn = false;

         Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-TX Succeeded!");
      }

      private void Radio_OnTransmitFailed()
      {
         Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-TX failed!");
      }
   }
}

After sorting out power to the SHT31 (I had to push the jumper cable further into the back of the jumper cable plug). I could see temperature and humidity values getting uploaded to Adafruit.IO.

Visual Studio 2019 debug output

Adafruit.IO “automagically” provisions new feeds which is helpful when building a proof of concept (PoC)

Adafruit.IO feed with default feed IDs

I then modified the feed configuration to give it a user friendly name.

Feed Configuration

All up configuration took about 10 minutes.

Meadow device temperature and humidity

.Net Meadow nRF24L01 library Part2

After getting SPI connectivity going my next step porting the techfooninja nRF24L01P library to a Wilderness Labs Meadow was rewriting the SPI port initialisation, plus GetStatus and Execute methods.

nRF24L01P Test Harness

I added a digital output port for the Chip Select and because I can specify the interrupt trigger edge I removed the test from the interrupt handler.

 public void Initialize(IIODevice device, ISpiBus spiBus, IPin chipEnablePin, IPin chipSelectLine, IPin interruptPin)
{
   _SpiBus = spiBus;

   _cePin = device.CreateDigitalOutputPort(chipEnablePin, false);

   _csPin = device.CreateDigitalOutputPort(chipSelectLine, false);

   _irqPin = device.CreateDigitalInputPort(interruptPin, InterruptMode.EdgeFalling, resistorMode: ResistorMode.PullUp);
   _irqPin.Changed += InterruptGpioPin_ValueChanged;

   // Module reset time
   Task.Delay(100).GetAwaiter().GetResult();

   IsInitialized = true;

   // Set reasonable default values
   Address = Encoding.UTF8.GetBytes("NRF1");
   DataRate = DataRate.DR2Mbps;
   IsDynamicPayload = true;
   IsAutoAcknowledge = true;

   FlushReceiveBuffer();
   FlushTransferBuffer();
   ClearIrqMasks();
   SetRetries(5, 60);

   // Setup, CRC enabled, Power Up, PRX
   SetReceiveMode();
}

The core of the Initialise method was moved to the Meadow application startup.

public MeadowApp()
{
   try
   {
		var config = new Meadow.Hardware.SpiClockConfiguration(
			2000,
			SpiClockConfiguration.Mode.Mode0);

		ISpiBus spiBus = Device.CreateSpiBus(
			Device.Pins.SCK,
			Device.Pins.MOSI,
			Device.Pins.MISO,config);

		Radio.OnDataReceived += Radio_OnDataReceived;
		Radio.OnTransmitFailed += Radio_OnTransmitFailed;
		Radio.OnTransmitSuccess += Radio_OnTransmitSuccess;

		Radio.Initialize(Device, spiBus, Device.Pins.D09, Device.Pins.D10, Device.Pins.D11);
		Radio.Address = Encoding.UTF8.GetBytes(DeviceAddress);

		Radio.Channel = nRF24Channel;
		Radio.PowerLevel = PowerLevel.High;
		Radio.DataRate = DataRate.DR250Kbps;
		Radio.IsEnabled = true;

		Radio.IsAutoAcknowledge = true;
		Radio.IsDyanmicAcknowledge = false;
		Radio.IsDynamicPayload = true;

		Console.WriteLine($"Address: {Encoding.UTF8.GetString(Radio.Address)}");
		Console.WriteLine($"PA: {Radio.PowerLevel}");
		Console.WriteLine($"IsAutoAcknowledge: {Radio.IsAutoAcknowledge}");
		Console.WriteLine($"Channel: {Radio.Channel}");
		Console.WriteLine($"DataRate: {Radio.DataRate}");
		Console.WriteLine($"Power: {Radio.PowerLevel}");
		Console.WriteLine($"IsDynamicAcknowledge: {Radio.IsDyanmicAcknowledge}");
		Console.WriteLine($"IsDynamicPayload: {Radio.IsDynamicPayload}");
		Console.WriteLine($"IsEnabled: {Radio.IsEnabled}");
		Console.WriteLine($"Frequency: {Radio.Frequency}");
		Console.WriteLine($"IsInitialized: {Radio.IsInitialized}");
		Console.WriteLine($"IsPowered: {Radio.IsPowered}");
	}
	catch (Exception ex)
	{
		Console.WriteLine(ex.Message);

		return;
	}

I modified the GetStatus and ExecuteMethods to use the ExchangeData method

   /// <summary>
      ///   Executes a command in NRF24L01+ (for details see module datasheet)
      /// </summary>
      /// <param name = "command">Command</param>
      /// <param name = "addres">Register to write to or read from</param>
      /// <param name = "data">Data to write or buffer to read to</param>
      /// <returns>Response byte array. First byte is the status register</returns>
      public byte[] Execute(byte command, byte addres, byte[] data)
      {
         CheckIsInitialized();

         // This command requires module to be in power down or standby mode
         if (command == Commands.W_REGISTER)
            IsEnabled = false;

         // Create SPI Buffers with Size of Data + 1 (For Command)
         var writeBuffer = new byte[data.Length + 1];
         var readBuffer = new byte[data.Length + 1];

         // Add command and address to SPI buffer
         writeBuffer[0] = (byte)(command | addres);

         // Add data to SPI buffer
         Array.Copy(data, 0, writeBuffer, 1, data.Length);

         // Do SPI Read/Write
         _SpiBus.ExchangeData(_csPin, ChipSelectMode.ActiveLow, writeBuffer, readBuffer);

         // Enable module back if it was disabled
         if (command == Commands.W_REGISTER && _enabled)
            IsEnabled = true;

         // Return ReadBuffer
         return readBuffer;
      }

      /// <summary>
      ///   Gets module basic status information
      /// </summary>
      /// <returns>Status object representing the current status of the radio</returns>
      public Status GetStatus()
      {
         CheckIsInitialized();

         var readBuffer = new byte[1];
         _SpiBus.ExchangeData(_csPin, ChipSelectMode.ActiveLow, new[] { Commands.NOP }, readBuffer);

         return new Status(readBuffer[0]);
      }

After these modifications I can send and receive messages but the PowerLevel doesn’t look right.

The program '[16720] App.exe' has exited with code 0 (0x0).
 IsPowered: True
 Address: Dev01
 PA: 15
 IsAutoAcknowledge: True
 Channel: 15
 DataRate: DR250Kbps
 Power: 15
 IsDynamicAcknowledge: False
 IsDynamicPayload: True
 IsEnabled: False
 Frequency: 2415
 IsInitialized: True
 IsPowered: True
 00:00:18-TX 8 byte message hello 17
 Data Sent!
00:00:18-TX Succeeded!
 00:00:48-TX 8 byte message hello 48
 Data Sent!

Time to dig into the nRF24L01P datasheet.

.Net Meadow nRF24L01 library Part1

After debugging Windows 10 IoT Core & .NetMF nRF24L01P libraries I figured a port to a Wilderness Labs Meadow device shouldn’t be “rocket science”.

I couldn’t source an nRF24L01 feather wing so built a test rig with jumpers

nRF24L01P Test Harness
//---------------------------------------------------------------------------------
// Copyright (c) Feb 2020, 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.
//
//---------------------------------------------------------------------------------
namespace devMobile.IoT.nRf24L01
{
   using System;
   using System.Text;
   using System.Threading;
   using Meadow;
   using Meadow.Devices;
   using Meadow.Hardware;

   public class MeadowApp : App<F7Micro, MeadowApp>
   {
      const byte SETUP_AW = 0x03;
      const byte RX_ADDR_P0 = 0x0A;
      const byte R_REGISTER = 0b00000000;
      const byte W_REGISTER = 0b00100000;
      ISpiBus spiBus;
      SpiPeripheral nrf24L01Device;
      IDigitalOutputPort spiPeriphChipSelect;
      IDigitalOutputPort ChipEnable;


      public MeadowApp()
      {
         ConfigureSpiPort();
         SetPipe0RxAddress("ZYXWV");
      }

      public void ConfigureSpiPort()
      {
         try
         {
            ChipEnable = Device.CreateDigitalOutputPort(Device.Pins.D09, initialState: false);
            if (ChipEnable == null)
            {
               Console.WriteLine("chipEnable == null");
            }

            var spiClockConfiguration = new SpiClockConfiguration(2000, SpiClockConfiguration.Mode.Mode0);
            spiBus = Device.CreateSpiBus(Device.Pins.SCK,
                                         Device.Pins.MOSI,
                                         Device.Pins.MISO,
                                         spiClockConfiguration);
            if (spiBus == null)
            {
               Console.WriteLine("spiBus == null");
            }

            Console.WriteLine("Creating SPI NSS Port...");
            spiPeriphChipSelect = Device.CreateDigitalOutputPort(Device.Pins.D10, initialState: true);
            if (spiPeriphChipSelect == null)
            {
               Console.WriteLine("spiPeriphChipSelect == null");
            }

            Console.WriteLine("nrf24L01Device Device...");
            nrf24L01Device = new SpiPeripheral(spiBus, spiPeriphChipSelect);
            if (nrf24L01Device == null)
            {
               Console.WriteLine("nrf24L01Device == null");
            }

            Thread.Sleep(100);

            Console.WriteLine("ConfigureSpiPort Done...");
         }
         catch (Exception ex)
         {
            Console.WriteLine("ConfigureSpiPort " + ex.Message);
         }
      }

      public void SetPipe0RxAddress(string address)
      {
         try
         {
            // Read the Address width
            byte[] txBuffer1 = new byte[] { SETUP_AW | R_REGISTER, 0x0 };
            Console.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer1));

            /*
            // Appears to work but not certain it does
            Console.WriteLine(" nrf24L01Device.WriteRead...SETUP_AW");
            byte[] rxBuffer1 = nrf24L01Device.WriteRead(txBuffer1, (ushort)txBuffer1.Length);
            Console.WriteLine(" nrf24L01Device.WriteRead...SETUP_AW");
            */

            byte[] rxBuffer1 = new byte[txBuffer1.Length];
            Console.WriteLine(" spiBus.ExchangeData...RX_ADDR_P0");
            spiBus.ExchangeData(spiPeriphChipSelect, ChipSelectMode.ActiveLow, txBuffer1, rxBuffer1);

            Console.WriteLine(" rxBuffer:" + BitConverter.ToString(rxBuffer1));

            // Extract then adjust the address width
            byte addressWidthValue = rxBuffer1[1];
            addressWidthValue &= 0b00000011;
            addressWidthValue += 2;
            Console.WriteLine("Address width 0x{0:x2} - Value 0X{1:x2} - Bits {2} Value adjusted {3}", SETUP_AW, rxBuffer1[1], Convert.ToString(rxBuffer1[1], 2).PadLeft(8, '0'), addressWidthValue);
            Console.WriteLine();

            // Write Pipe0 Receive address
            Console.WriteLine("Address write 1");
            byte[] txBuffer2 = new byte[addressWidthValue + 1];
            txBuffer2[0] = RX_ADDR_P0 | W_REGISTER;
            Array.Copy(Encoding.UTF8.GetBytes(address), 0, txBuffer2, 1, addressWidthValue);
            Console.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer2));

            Console.WriteLine(" nrf24L01Device.Write...RX_ADDR_P0");
            nrf24L01Device.WriteBytes(txBuffer2);
            Console.WriteLine();

            // Read Pipe0 Receive address
            Console.WriteLine("Address read 1");
            byte[] txBuffer3 = new byte[addressWidthValue + 1];
            txBuffer3[0] = RX_ADDR_P0 | R_REGISTER;
            Console.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer3));

            /*
            // Broken returns  Address 0x0a - RX Buffer 5A-5A-5A-5A-59-58 RX Address 5A-5A-5A-59-58 Address ZZZYX
            Console.WriteLine(" nrf24L01Device.WriteRead...RX_ADDR_P0");
            byte[] rxBuffer3 = nrf24L01Device.WriteRead(txBuffer3, (ushort)txBuffer3.Length);
            */

            byte[] rxBuffer3 = new byte[addressWidthValue + 1];
            Console.WriteLine(" spiBus.ExchangeData...RX_ADDR_P0");
            spiBus.ExchangeData(spiPeriphChipSelect, ChipSelectMode.ActiveLow, txBuffer3, rxBuffer3);

            Console.WriteLine("Address 0x{0:x2} - RX Buffer {1} RX Address {2} Address {3}", RX_ADDR_P0, BitConverter.ToString(rxBuffer3, 0), BitConverter.ToString(rxBuffer3, 1), UTF8Encoding.UTF8.GetString(rxBuffer3, 1, addressWidthValue));
         }
         catch (Exception ex)
         {
            Console.WriteLine("ReadDeviceIDDiy " + ex.Message);
         }
      }
   }
}

After lots of tinkering with SPI configuration options and trialing different methods (spiBus vs.SpiPeripheral) I can read and write my nRF24L01 device receive port address

 Creating SPI NSS Port...
 nrf24L01Device Device...
 ConfigureSpiPort Done...
  txBuffer:03-00
  spiBus.ExchangeData...RX_ADDR_P0
  rxBuffer:0E-03
 Address width 0x03 - Value 0X03 - Bits 00000011 Value adjusted 5
 
 Address write 1
  txBuffer:2A-5A-59-58-57-56
  nrf24L01Device.Write...RX_ADDR_P0
 
 Address read 1
  txBuffer:0A-00-00-00-00-00
  spiBus.ExchangeData...RX_ADDR_P0
 Address 0x0a - RX Buffer 0E-5A-59-58-57-56 RX Address 5A-59-58-57-56 Address ZYXWV

I need to investigate why the first byte of the buffer returned by nrf24L01Device.ReadBytes and nrf24L01Device.WriteRead is wrong.

Azure IOT Hub nRF24L01 Windows 10 IoT Core Field Gateway with BorosRF2

A couple of BorosRF2 Dual nRF24L01 Hats arrived earlier in the week. After some testing with my nRF24L01 Test application I have added compile-time configuration options for the two nRF24L01 sockets to my Azure IoT Hub nRF24L01 Field Gateway.

Boros RF2 with Dual nRF24L01 devices
public sealed class StartupTask : IBackgroundTask
{
   private const string ConfigurationFilename = "config.json";

   private const byte MessageHeaderPosition = 0;
   private const byte MessageHeaderLength = 1;

   // nRF24 Hardware interface configuration
#if CEECH_NRF24L01P_SHIELD
   private const byte RF24ModuleChipEnablePin = 25;
   private const byte RF24ModuleChipSelectPin = 0;
   private const byte RF24ModuleInterruptPin = 17;
#endif

#if BOROS_RF2_SHIELD_RADIO_0
   private const byte RF24ModuleChipEnablePin = 24;
   private const byte RF24ModuleChipSelectPin = 0;
   private const byte RF24ModuleInterruptPin = 27;
#endif

#if BOROS_RF2_SHIELD_RADIO_1
   private const byte RF24ModuleChipEnablePin = 25;
   private const byte RF24ModuleChipSelectPin = 1;
   private const byte RF24ModuleInterruptPin = 22;
#endif

private readonly LoggingChannel logging = new LoggingChannel("devMobile Azure IotHub nRF24L01 Field Gateway", null, new Guid("4bd2826e-54a1-4ba9-bf63-92b73ea1ac4a"));
private readonly RF24 rf24 = new RF24();

This version supports one nRF24L01 device socket active at a time.

Enabling both nRF24L01 device sockets broke outbound message routing in a prototype branch with cloud to device(C2D) messaging support. This functionality is part of an Over The Air(OTA) device provisioning implementation I’m working o.

Adafruit.IO nRF24L01 Windows 10 IoT Core Field Gateway with BorosRF2

A couple of BorosRF2 Dual nRF24L01 Hats arrived earlier in the week. After some testing with my nRF24L01 Test application I have added compile-time configuration options for the two nRF24L01 sockets to my Adafruit.IO nRF24L01 Field Gateway.

Boros RF2 with Dual nRF24L01 devices
public sealed class StartupTask : IBackgroundTask
{
   private const string ConfigurationFilename = "config.json";

   private const byte MessageHeaderPosition = 0;
   private const byte MessageHeaderLength = 1;

   // nRF24 Hardware interface configuration
#if CEECH_NRF24L01P_SHIELD
   private const byte RF24ModuleChipEnablePin = 25;
   private const byte RF24ModuleChipSelectPin = 0;
   private const byte RF24ModuleInterruptPin = 17;
#endif

#if BOROS_RF2_SHIELD_RADIO_0
   private const byte RF24ModuleChipEnablePin = 24;
   private const byte RF24ModuleChipSelectPin = 0;
   private const byte RF24ModuleInterruptPin = 27;
#endif

#if BOROS_RF2_SHIELD_RADIO_1
   private const byte RF24ModuleChipEnablePin = 25;
   private const byte RF24ModuleChipSelectPin = 1;
   private const byte RF24ModuleInterruptPin = 22;
#endif

private readonly LoggingChannel loggingChannel = new LoggingChannel("devMobile AdaFruit.IO nRF24L01 Field Gateway", null, new Guid("4bd2826e-54a1-4ba9-bf63-92b73ea1ac4a"));
private readonly RF24 rf24 = new RF24();

For this initial version only one nRF24L01 device socket active at a time is supported.

Windows 10 IoT Core BorosRf2 – Dual nRF24L01 pHat/Hat

I have a couple of nRF24L01P Raspberry PI projects (primarily my Adafruit.IO and Azure IoT Hubs/Central Windows 10 IoT Core telemetry field gateways) and recently Boros Lab a vendor of suitable Raspberry PI Hats opened a store on Tindie.com.

I ordered a couple of BorosRf2 – Dual nRF24L01 pHat/Hat + RTC for Pis (mine were without the Real-time clock(RTC)) for testing. The vendor’s github repository had details of the GPIO pins used so it was relatively quick and easy to modify my Windows 10 IoT nRF24L01 test harness to work with a single port on the hat.

Single port configuration

By setting a conditional compile option (CEECH_NRF24L01P_SHIELD, BOROS_RF2_SHIELD_RADIO_0 or BOROS_RF2_SHIELD_RADIO_1) my test application could be configured to support the Boros or Ceech (with a modification detailed here) shields.

namespace devmobile.IoTCore.nRF24L01BackGroundTask
{
	public sealed class StartupTask : IBackgroundTask
	{
		// nRF24 Hardware interface configuration
#if CEECH_NRF24L01P_SHIELD
      private const byte ChipEnablePin = 25;
      private const byte ChipSelectPin = 0;
      private const byte InterruptPin = 17;
#endif
#if BOROS_RF2_SHIELD_RADIO_0
      private const byte ChipEnablePin = 24;
      private const byte ChipSelectPin = 0;
      private const byte InterruptPin = 27;
#endif
#if BOROS_RF2_SHIELD_RADIO_1
      private const byte ChipEnablePin = 25;
      private const byte ChipSelectPin = 1;
      private const byte InterruptPin = 22;
#endif
      private const string BaseStationAddress = "Node1";
      private const byte nRF24Channel = 20;
      private RF24 Radio = new RF24();
      private BackgroundTaskDeferral deferral;
      private ThreadPoolTimer timer;


Both vendors’ shields worked well with my test application, the ceech shield (USD9.90 April 2019) is a little bit cheaper, but the Boros shield (USD15.90 April 2019 ) doesn’t require any modification and has a socket for a second nRF24 device.

Easy Sensors Wireless field gateway Arduino Nano client

After not much development on my nrf24L01 AdaFruit.IO and Azure IOT Hub field gateways for a while some new nRF24L01 devices arrived in the post last week.

This sample client is an Arduino Nano clone with an Arduino Nano radio shield for NRF24L01+.

I use the shield’s onboard SHA204A crypto and authentication chip, and a Seeedstudio Temperature & Humidity sensor with the data uploaded to adafruit.io.

/*
  Copyright ® 2018 September devMobile Software, All Rights Reserved

  THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
  KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
  IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
  PURPOSE.

  You can do what you want with this code, acknowledgment would be nice.

  http://www.devmobile.co.nz

*/
#include &lt;RF24.h&gt;
#include &lt;sha204_library.h&gt;
#include &lt;TH02_dev.h&gt;

// RF24 radio( ChipeEnable , ChipSelect )
RF24 radio(9, 10);
const byte FieldGatewayChannel = 15 ;
const byte FieldGatewayAddress[] = {"Base1"};
const rf24_datarate_e RadioDataRate = RF24_250KBPS;
const rf24_pa_dbm_e RadioPALevel = RF24_PA_HIGH;

// Payload configuration
const int PayloadSizeMaximum = 32 ;
char payload[PayloadSizeMaximum] = "";
const byte DeviceIdPlusCsvSensorReadings = 1 ;
const byte SensorReadingSeperator = ',' ;

// ATSHA204 secure authentication, validation with crypto and hashing (only using for unique serial number)
atsha204Class sha204(A3);
const int DeviceSerialNumberLength = 9 ;
uint8_t deviceSerialNumber[DeviceSerialNumberLength] = {""};
const int LoopSleepDelaySeconds = 10 ;

void setup()
{
  Serial.begin(9600);
  Serial.println("Setup called");

  // Retrieve the serial number then display it nicely
  sha204.getSerialNumber(deviceSerialNumber);

  Serial.print("SNo:");
  for (int i = 0; i &lt; sizeof( deviceSerialNumber) ; i++)
  {
    // Add a leading zero
    if ( deviceSerialNumber[i] &lt; 16)
    {
      Serial.print("0");
    }
    Serial.print(deviceSerialNumber[i], HEX);
    Serial.print(" ");
  }

  Serial.println();

  // Configure the Seeedstudio TH02 temperature &amp; humidity sensor
  Serial.println("TH02 setup");
  TH02.begin();
  delay(100);

  // Configure the nRF24 module
  Serial.println("nRF24 setup");
  radio.begin();
  radio.setChannel(FieldGatewayChannel);
  radio.openWritingPipe(FieldGatewayAddress);
  radio.setDataRate(RadioDataRate) ;
  radio.setPALevel(RadioPALevel);
  radio.enableDynamicPayloads();

  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 PayloadMessageType (top nibble) and DeviceID length (bottom nibble)
  payload[0] = (DeviceIdPlusCsvSensorReadings &lt;&lt; 4) | sizeof(deviceSerialNumber) ;
  payloadLength += 1;

  // Copy the device serial number into the payload
  memcpy( &amp;payload[payloadLength], deviceSerialNumber, sizeof( deviceSerialNumber));
  payloadLength += sizeof( deviceSerialNumber) ;

  // Read the temperature, humidity &amp; battery voltage values then display nicely
  temperature = TH02.ReadTemperature();
  Serial.print("T:");
  Serial.print( temperature, 1 ) ;
  Serial.print( "C" ) ;

  humidity = TH02.ReadHumidity();
  Serial.print(" H:");
  Serial.print( humidity, 0 ) ;
  Serial.println( "%" ) ;

  // Copy the temperature into the payload
  payload[ payloadLength] = 't';
  payloadLength += 1 ;
  dtostrf(temperature, 6, 1, &amp;payload[payloadLength]);
  payloadLength += 6;

  payload[ payloadLength] = ',';
  payloadLength += 1 ;

  // Copy the humidity into the payload
  payload[ payloadLength] = 'h';
  payloadLength += 1 ;
  dtostrf(humidity, 4, 0, &amp;payload[payloadLength]);
  payloadLength += 4;

  // Powerup the nRF24 chipset then send the payload to base station
  Serial.print( "Payload length:");
  Serial.println( payloadLength );

  Serial.println( "nRF24 write" ) ;
  boolean result = radio.write(payload, payloadLength);
  if (result)
    Serial.println("Write Ok...");
  else
    Serial.println("Write failed.");

  Serial.println("Loop done");
  delay(LoopSleepDelaySeconds * 1000l);
}

Arduino monitor output

NanoArduinoNrf24

Prototype hardware

ArduinoNanonRF24

Bill of materials (prices as at Sep 2018)

  • Arduino Nano clone USD4.70
  • Easy Sensors Arduino Nano Radio Shield for nRF24L01 USD13
  • Seeedstudio Temperature and Humidity Sensor Pro USD11.50
  • Seeedstudio 4 pin Male Jumper to Grove 4 pin Conversion Cable USD2.90

Adafruit.IO temperature display when I moved the sensor outside.

NanoNrf24