netNF Electric Longboard Part 4

The Rideable Prototype

After some experimentation I gave up on the radio control(RC) servo library for controlling my Longboard’s Electronic Speed Control(ESC).

My new longboard controller uses the following parts

  • Netduino 3 Wifi
  • Generic wireless wii nuchuk
  • generic Arduino protoshield

I reused the initial protoshield and only had to shift the PWM output to the ESC from pin 8 to pin 7.

FEZ Panda III Protoshield for longboard with RC Servo for testing
Protoshield for longboard RC Servo test
public class Program
{
   private const double PulseFrequency = 50.0;
   private const double PulseDurationMinimum = 0.05; // 1000uSec
   private const double PulseDurationMaximum = 0.1; // 2000uSec
   private const double WiiNunchukYMinimum = 0.0;
   private const double WiiNunchukYMaximum = 255.0;
   private const int ThrottleUpdatePeriod = 100;

   public static void Main()
   {
      Debug.WriteLine("devMobile.Longboard starting");
      Debug.WriteLine($"I2C:{I2cDevice.GetDeviceSelector()}");
      Debug.WriteLine($"PWM:{PwmController.GetDeviceSelector()}");

      try
      {
         Debug.WriteLine("LED Starting");
         GpioPin led = GpioController.GetDefault().OpenPin(PinNumber('A', 10));
         led.SetDriveMode(GpioPinDriveMode.Output);
         led.Write(GpioPinValue.Low);

         Debug.WriteLine("LED Starting");
         WiiNunchuk nunchuk = new WiiNunchuk("I2C1");

         Debug.WriteLine("ESC Starting");
         PwmController pwm = PwmController.FromId("TIM5");
         PwmPin pwmPin = pwm.OpenPin(PinNumber('A', 1));
         pwmPin.Controller.SetDesiredFrequency(PulseFrequency);
         pwmPin.Start();

         Debug.WriteLine("Thread.Sleep Starting");
         Thread.Sleep(2000);

         Debug.WriteLine("Mainloop Starting");
         while (true)
         {
            nunchuk.Read();

            double duration = Map(nunchuk.AnalogStickY, WiiNunchukYMinimum, WiiNunchukYMaximum, PulseDurationMinimum, PulseDurationMaximum);
            Debug.WriteLine($"Value:{nunchuk.AnalogStickY} Duration:{duration:F3}");

            pwmPin.SetActiveDutyCyclePercentage(duration);
            led.Toggle();
            Thread.Sleep(ThrottleUpdatePeriod);
         }
      }
      catch (Exception ex)
      {
         Debug.WriteLine(ex.Message);
      }
   }

   private static int PinNumber(char port, byte pin)
   {
      if (port < 'A' || port > 'J')
         throw new ArgumentException();

      return ((port - 'A') * 16) + pin;
   }

   private static double Map(double x, double inputMinimum, double inputMaximum, double outputMinimum, double outputMaximum)
   {
      return (x - inputMinimum) * (outputMaximum - outputMinimum) / (inputMaximum - inputMinimum) + outputMinimum;
   }
}

The nanoFramework code polls the wii nunchuk for the joystick position every 100mSec and then updates the PWM duty cycle.

By convention the ESSC PWM frequency is 50Hz (a pulse ever 20mSec) and the duration of the pulse is 1000uSec(minimum throttle) to 2000uSec(maximum throttle), note the change of units.

After converting to the same units there is a pulse every 20mSec and its duration is 1mSec too 2mSec. Then converting the durations to the active duty cycle percentage (for the PWM SetActiveDutyCyclePercentage) the duration of the pulse is 5% to 10%.

I need to re-calibrate the ESC for these durations and ensure that reverse is disabled. Then tinker with the brake (braking percent & percent drag brake) and acceleration(initial acceleration low, medium, high, very high) configurations of my ESC to make the longboard easier to ride.

Next I will look at configurable throttle maps (to make it easier for new and different weight users), then using one of the wii-nunchuk buttons for cruise control (keeping the throttle steady when riding is difficult) and how the software reacts when the connection with nunchuk fails

netNF Electric Longboard Part 3

Servo Control

The next step was to figure out how to operate a radio control(RC) servo as a proxy for an Electronic Speed Control(ESC).

My test rig uses (prices as at Aug 2020) the following parts

  • Netduino 3 Wifi
  • Grove-Base Shield V2.0 for Arduino USD4.45
  • Grove-Universal 4 Pin Bucked 20cm cable(5 PCs Pack) USD2.90
  • Grove-Servo USD5.90
  • Grove-Rotary Angle Sensor USD2.90

My servo test harness

public class Program
{
   public static void Main()
   {
      Debug.WriteLine("devMobile.Longboard.ServoTest starting");

      try
      {
         AdcController adc = AdcController.GetDefault();
         AdcChannel adcChannel = adc.OpenChannel(0);

         ServoMotor servo = new ServoMotor("TIM5", ServoMotor.ServoType.Positional, PinNumber('A', 0));
         servo.ConfigurePulseParameters(0.6, 2.3);

         while (true)
         {
            double value = adcChannel.ReadRatio();
            double position = Map(value, 0.0, 1.0, 0.0, 180);

            Debug.WriteLine($"Value: {value:F2} Position: {position:F1}");

            servo.Set(position);

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

   private static int PinNumber(char port, byte pin)
   {
      if (port < 'A' || port > 'J')
         throw new ArgumentException();

      return ((port - 'A') * 16) + pin;
   }

   private static double Map(double x, double inputMinimum, double inputMaximum, double outputMinimum, double outputMaximum)
   {
      return (x - inputMinimum) * (outputMaximum - outputMinimum) / (inputMaximum - inputMinimum) + outputMinimum;
   }
}

The nanoFramework code polls for the rotary angle sensor for its position every 100mSec and then updates the servo.

The servo code was based on sample code provided by GHI Electronics for their TinyCLR which I had to adapt to work with the nanoFramework.

The next test rig will be getting the Netduino 3 software working my Longboard ESC and Lithium Polymer(LiPo) batteries.

netNF Electric Longboard Part 2

Analog Inputs & Pulse Width Modulation

The next step was to figure out how to configure a Pulse Width Modulation (PWM) output and an Analog Input so I could adjust the duty cycle and control the brightness of a Light Emitting Diode(LED).

Netduino 3 ADC & PWN test rig

My test rig uses (prices as at Aug 2020) the following parts

  • Netduino 3 Wifi
  • Grove-Base Shield V2.0 for Arduino USD4.45
  • Grove-Universal 4 Pin Bucked 5cm cable(5 PCs Pack) USD1.90
  • Grove-Universal 4 Pin Bucked 20cm cable(5 PCs Pack) USD2.90
  • Grove-LED Pack USD2.90
  • Grove-Rotary Angle Sensor USD2.90

My analog input test harness

 public class Program
   {
      public static void Main()
      {
         Debug.WriteLine("devMobile.Longboard.AdcTest starting");
         Debug.WriteLine(AdcController.GetDeviceSelector());

         try
         {
            AdcController adc = AdcController.GetDefault();
            AdcChannel adcChannel = adc.OpenChannel(0);

            while (true)
            {
               double value = adcChannel.ReadRatio();

               Debug.WriteLine($"Value: {value:F2}");

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

The nanoFramework code polls for the rotary angle sensor for its position value every 100mSec.

The setup to use for the Analog to Digital Convertor(ADC) port was determined by looking at the board.h and target_windows_devices_adc_config.cpp file.

//
// Copyright (c) 2018 The nanoFramework project contributors
// See LICENSE file in the project root for full license information.
//

#include <win_dev_adc_native_target.h>

const NF_PAL_ADC_PORT_PIN_CHANNEL AdcPortPinConfig[] = {
    
    // ADC1
    {1, GPIOC, 0, ADC_CHANNEL_IN10},
    {1, GPIOC, 1, ADC_CHANNEL_IN11},

    // ADC2
    {2, GPIOC, 2, ADC_CHANNEL_IN14},
    {2, GPIOC, 3, ADC_CHANNEL_IN15},

    // ADC3
    {3, GPIOC, 4, ADC_CHANNEL_IN12},
    {3, GPIOC, 5, ADC_CHANNEL_IN13},

    // these are the internal sources, available only at ADC1
    {1, NULL, 0, ADC_CHANNEL_SENSOR},
    {1, NULL, 0, ADC_CHANNEL_VREFINT},
    {1, NULL, 0, ADC_CHANNEL_VBAT},
};

const int AdcChannelCount = ARRAYSIZE(AdcPortPinConfig);

The call to AdcController.GetDeviceSelector() only returned one controller

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.Longboard.AdcTest starting
ADC1

After some experimentation it appears that only A0 & A1 work on a Netduino. (Aug 2020).

My PWM test harness

public class Program
{
   public static void Main()
   {
      Debug.WriteLine("devMobile.Longboard.PwmTest starting");
      Debug.WriteLine(PwmController.GetDeviceSelector());

      try
      {
         PwmController pwm = PwmController.FromId("TIM5");
         AdcController adc = AdcController.GetDefault();
         AdcChannel adcChannel = adc.OpenChannel(0);

         PwmPin pwmPin = pwm.OpenPin(PinNumber('A', 0));
         pwmPin.Controller.SetDesiredFrequency(1000);
         pwmPin.Start();

         while (true)
         {
            double value = adcChannel.ReadRatio();

            Debug.WriteLine(value.ToString("F2"));

            pwmPin.SetActiveDutyCyclePercentage(value);

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

   private static int PinNumber(char port, byte pin)
   {
      if (port < 'A' || port > 'J')
         throw new ArgumentException();
      return ((port - 'A') * 16) + pin;
   }
}

I had to refer to the Netduino schematic to figure out pin mapping

With my test rig (with easy access to D0 thru D8) I found that only D2,D3,D7 and D8 work as PWM outputs.

The next test rig will be getting Servo working.

nanoFramework nRF24L01 library Part2

After sorting out Serial Peripheral Interface(SPI) connectivity the next step porting my GHI Electronics TinyCLR V2 library to the nanoFramework was rewriting the initialisation code. Overall changes were minimal as the nanoFramework similar methods to the TinyCLR V2 ones.

The Tiny CLR SPI and interrupt port configuration (note the slightly different interrupt port configuration)

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;
}

The nanoFramework SPI and interrupt port configuration (note the slightly different SPI port configuration)

public void Initialize(string spiPortName, int chipEnablePin, int chipSelectPin, int 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.ValueChanged += irqPin_ValueChanged;
   }

   try
   {
      var settings = new SpiConnectionSettings(chipSelectPin)
      {
         ClockFrequency = clockFrequency,
         Mode = SpiMode.Mode0,
         SharingMode = SpiSharingMode.Shared,
      };

      _spiPort = SpiDevice.FromId(spiPortName, settings);
   }
   catch (Exception ex)
   {
      Debug.WriteLine("SPI Initialization failed. Exception: " + ex.Message);
   return;
   }

The error handling of the initialise method is broken. If the some of the GPIO or SPI port configuration fails a message is displayed in the Debug output but the caller is not notified.

I’m using a Netduino 3 Wifi as the SPI port configuration means I can use a standard Arduino shield to connect up the NRF24L01 wireless module without any jumpers

Netduino 3 Wifi and embedded coolness shield

I have applied the PowerLevel fix from the TinyCLR and Meadow libraries but worry that there maybe other issues.

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

Based on my experiences porting the library to three similar platforms and debugging it on two others I’m considering writing my own compile-time platform portable library.

nanoFramework nRF24L01 library Part1

After porting then debugging Windows 10 IoT Core, .NetMF, Wilderness Labs Meadow and GHI Electronics TinyCLR nRF24L01P libraries I figured yet another port, this time to a nanoFramework powered devices should be low risk.

My initial test rig uses a Netduino 3 Wifi and an Embedded Coolness nRF24 shield as I didn’t need to use jumper wires.

//---------------------------------------------------------------------------------
// Copyright (c) July 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.
//
//---------------------------------------------------------------------------------
#define NETDUINO3_WIFI   // nanoff --target NETDUINO3_WIFI --update

namespace devMobile.IoT.nRf24L01.ModuleSPI
{
   using System;
   using System.Threading;
   using System.Diagnostics;
   using System.Text;
   using Windows.Devices.Gpio;
   using Windows.Devices.Spi;

   public 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";

#if NETDUINO3_WIFI
      private const string SpiBusId = "SPI2";
#endif

      public static void Main()
      {
#if NETDUINO3_WIFI
         // Arduino D7->PD7
         int chipSelectPinNumber = PinNumber('A', 1);
#endif
         Debug.WriteLine("devMobile.IoT.nRf24L01.ModuleSPI starting");

         Debug.WriteLine(Windows.Devices.Spi.SpiDevice.GetDeviceSelector());

         try
         {
            GpioController gpioController = GpioController.GetDefault();

            var settings = new SpiConnectionSettings(chipSelectPinNumber)
            {
               ClockFrequency = 2000000,
               Mode = SpiMode.Mode0,
               SharingMode = SpiSharingMode.Shared,
            };

            using (SpiDevice device = SpiDevice.FromId(SpiBusId, settings))
            {
               Debug.WriteLine("nrf24L01Device Device...");
               if (device == null)
               {
                  Debug.WriteLine("nrf24L01Device == null");
               }

               Thread.Sleep(100);

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

               Thread.Sleep(500);
               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));
                  device.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];
                  byte[] rxBuffer2 = new byte[txBuffer2.Length];
                  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));
                  device.TransferFullDuplex(txBuffer2, rxBuffer2);
                  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));
                  device.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));
                  device.TransferFullDuplex(txBuffer4, rxBuffer4);
                  Debug.WriteLine(" rxBuffer:" + BitConverter.ToString(rxBuffer4));

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

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

                  Debug.WriteLine(" nrf24L01Device.Write...RF_CH");
                  Debug.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer5));
                  //device.Write(txBuffer5);
                  device.TransferFullDuplex(txBuffer5, rxBuffer5);
                  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));
                  device.TransferFullDuplex(txBuffer6, rxBuffer6);
                  Debug.WriteLine(" rxBuffer:" + BitConverter.ToString(rxBuffer6));

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

#if NETDUINO3_WIFI
      static int PinNumber(char port, byte pin)
      {
         if (port < 'A' || port > 'J')
            throw new ArgumentException();

         return ((port - 'A') * 16) + pin;
      }
#endif
   }
}

After bit of tinkering with SPI configuration options and checking device.Write vs. device.TransferFullDuplex usage. I can reliably read and write my nRF24L01 device’s receive port address and channel configuration.

devMobile.IoT.nRf24L01.ModuleSPI starting
SPI1,SPI2,SPI3,SPI4
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-02
RF Channel 1 0x05 - Value 0X02 - Value adjusted 2402

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

RF Channel read 2
 nrf24L01Device.TransferFullDuplex...RF_CH
 txBuffer:05-00
 rxBuffer:0E-03
RF Channel 2 0x05 - Value 0X03 - Value adjusted 2403

The thread '<No Name>' (0x1) has exited with code 0 (0x0).
Done.

Next step is to port my TinyCLR nRF24L01 library which is based on the Techfoonina Windows 10 IoT Core port which is based on .NetMF library by Gralin.

RFM9X.NetNF on Netduino

After publishing my RFM9X.NetMF library working I noticed a Netduino 3 Wifi sitting at the back of my desk. I have got a few Netduinos and NanoFramework support (as a reference platform) for the Netduino 3 Wifi had caught my attention.

Netduino 3 Wifi LoRa test rig

The first step was to get the chip select, reset and Serial Peripheral interface(SPI) configurations sorted. I’m using a Dragino LoRa shield for Arduino and a Netduino 3 Wifi.

Dragino LoRa Shield Schematic

The first step was to figure out the configuration using the 00.Shield project. After some experimentation I figured out the SPI port connected to D10-D13 was SPI2 (SPI1 is connected to the MicroSD port)

//---------------------------------------------------------------------------------
// Copyright (c) April 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.
//
//---------------------------------------------------------------------------------
//#define ESP32_WROOM_32_LORA_1_CHANNEL   //nanoff --target ESP32_WROOM_32 --serialport COM4 --update
#define NETDUINO3_WIFI   // nanoff --target NETDUINO3_WIFI --update
//NOTE May 2020 ST_NUCLEO64_F091RC device doesn't work something broken in SPI configuration
//#define ST_NUCLEO64_F091RC // nanoff --target ST_NUCLEO64_F091RC --update
//#define ST_STM32F429I_DISCOVERY       //nanoff --target ST_STM32F429I_DISCOVERY --update
//NOTE May 2020 ST_STM32F769I_DISCOVERY device doesn't work SPI2 mappings broken 
//#define ST_STM32F769I_DISCOVERY      // nanoff --target ST_STM32F769I_DISCOVERY --update 
namespace devMobile.IoT.Rfm9x.ShieldSPI
{
   using System;
   using System.Diagnostics;
   using System.Threading;

   using Windows.Devices.Gpio;
   using Windows.Devices.Spi;

#if ESP32_WROOM_32_LORA_1_CHANNEL
   using nanoFramework.Hardware.Esp32;
#endif

   public class Program
   {
      private const byte RegVersion = 0x42;
#if ESP32_WROOM_32_LORA_1_CHANNEL
      private const string SpiBusId = "SPI1";
#endif
#if NETDUINO3_WIFI
      private const string SpiBusId = "SPI2";
#endif
#if ST_NUCLEO64_F091RC
      private const string SpiBusId = "SPI1";
#endif
#if ST_STM32F429I_DISCOVERY
      private const string SpiBusId = "SPI5";
#endif
#if ST_STM32F769I_DISCOVERY
      private const string SpiBusId = "SPI5";
#endif

      public static void Main()
      {
#if ESP32_WROOM_32_LORA_1_CHANNEL // No reset line for this device as it isn't connected on SX127X
         int ledPinNumber = Gpio.IO17;
         int chipSelectPinNumber = Gpio.IO16;
#endif
#if NETDUINO3_WIFI
         int ledPinNumber  = PinNumber('A', 10);
         // Arduino D10->PB10
         int chipSelectPinNumber = PinNumber('B', 10);
         // Arduino D9->PE5
         int resetPinNumber = PinNumber('E', 5);
#endif
#if ST_NUCLEO64_F091RC // No LED for this device as driven by D13 the SPI CLK line
         // Arduino D10->PB6
         int chipSelectPinNumber = PinNumber('B', 6);
         // Arduino D9->PC7
         int resetPinNumber = PinNumber('C', 7);
#endif
#if ST_STM32F429I_DISCOVERY // No reset line for this device as I didn't bother with jumper to SX127X pin
         int ledPinNumber  = PinNumber('G', 14);
         int chipSelectPinNumber = PinNumber('C', 2);
#endif
#if ST_STM32F769I_DISCOVERY
         int ledPinNumber  = PinNumber('J', 5);
         // Arduino D10->PA11
         int chipSelectPinNumber = PinNumber('A', 11);
         // Arduino D9->PH6
         int resetPinNumber = PinNumber('H', 6);
#endif
         Debug.WriteLine("devMobile.IoT.Rfm9x.ShieldSPI starting");

         try
         {
            GpioController gpioController = GpioController.GetDefault();

#if NETDUINO3_WIFI|| ST_NUCLEO64_F091RC || ST_STM32F769I_DISCOVERY
            // Setup the reset pin
            GpioPin resetGpioPin = gpioController.OpenPin(resetPinNumber);
            resetGpioPin.SetDriveMode(GpioPinDriveMode.Output);
            resetGpioPin.Write(GpioPinValue.High);
#endif

#if ESP32_WROOM_32_LORA_1_CHANNEL || NETDUINO3_WIFI|| ST_STM32F429I_DISCOVERY || ST_STM32F769I_DISCOVERY
            // Setup the onboard LED
            GpioPin led = gpioController.OpenPin(ledPinNumber);
            led.SetDriveMode(GpioPinDriveMode.Output);
#endif

#if ESP32_WROOM_32_LORA_1_CHANNEL
            Configuration.SetPinFunction(nanoFramework.Hardware.Esp32.Gpio.IO12, DeviceFunction.SPI1_MISO);
            Configuration.SetPinFunction(nanoFramework.Hardware.Esp32.Gpio.IO13, DeviceFunction.SPI1_MOSI);
            Configuration.SetPinFunction(nanoFramework.Hardware.Esp32.Gpio.IO14, DeviceFunction.SPI1_CLOCK);
#endif

            var settings = new SpiConnectionSettings(chipSelectPinNumber)
            {
               ClockFrequency = 500000,
               Mode = SpiMode.Mode0,// From SemTech docs pg 80 CPOL=0, CPHA=0
               SharingMode = SpiSharingMode.Shared,
            };

            using (SpiDevice device = SpiDevice.FromId(SpiBusId, settings))
            {
               Thread.Sleep(500);
            
               while (true)
               {
                  byte[] writeBuffer = new byte[] { RegVersion, 0x0 };
                  byte[] readBuffer = new byte[writeBuffer.Length];

                  device.TransferFullDuplex(writeBuffer, readBuffer);

                  Debug.WriteLine(String.Format("Register 0x{0:x2} - Value 0X{1:x2}", RegVersion, readBuffer[1]));

                  #if ESP32_WROOM_32_LORA_1_CHANNEL|| NETDUINO3_WIFI || ST_STM32F429I_DISCOVERY || ST_STM32F769I_DISCOVERY
                     led.Toggle();
                  #endif
                  Thread.Sleep(10000);
               }
            }
         }
         catch (Exception ex)
         {
            Debug.WriteLine(ex.Message);
         }
      }

#if  NETDUINO3_WIFI || ST_NUCLEO64_F091RC || ST_STM32F429I_DISCOVERY || ST_STM32F769I_DISCOVERY
      static int PinNumber(char port, byte pin)
      {
         if (port < 'A' || port > 'J')
            throw new ArgumentException();

         return ((port - 'A') * 16) + pin;
      }
#endif
   }
}

In the Visual Studio output windows I could see the correct version register value

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.IoT.Rfm9x.ShieldSPI starting
Register 0x42 - Value 0X12
Register 0x42 - Value 0X12
...

After checking the configuration of the reset (D9) and interrupt (D2) pins in other test harness programs my final configuration for Rfm9xLoRaDevice client was

//---------------------------------------------------------------------------------
// Copyright (c) April/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.
//
//---------------------------------------------------------------------------------
//#define ADDRESSED_MESSAGES_PAYLOAD
//#define ESP32_WROOM_32_LORA_1_CHANNEL   //nanoff --target ESP32_WROOM_32 --serialport COM4 --update
#define NETDUINO3_WIFI   // nanoff --target NETDUINO3_WIFI --update
//#define ST_STM32F429I_DISCOVERY       //nanoff --target ST_STM32F429I_DISCOVERY --update
namespace devMobile.IoT.Rfm9x.LoRaDeviceClient
{
	using System;
	using System.Diagnostics;
	using System.Text;
	using System.Threading;

#if ESP32_WROOM_32_LORA_1_CHANNEL
	using nanoFramework.Hardware.Esp32;
#endif

	using devMobile.IoT.Rfm9x;

	class Program
	{
		private const double Frequency = 915000000.0;
#if ST_STM32F429I_DISCOVERY
		private const string DeviceName = "Disco429";
		private const string SpiBusId = "SPI5";
#endif
#if ESP32_WROOM_32_LORA_1_CHANNEL
		private const string DeviceName = "ESP32";
		private const string SpiBusId = "SPI1";
#endif
#if NETDUINO3_WIFI
		private const string DeviceName = "N3W";
		private const string SpiBusId = "SPI2";
#endif
#if ADDRESSED_MESSAGES_PAYLOAD
		private const string DeviceName = "LoRaIoT1";
#endif

		static void Main()
		{
			byte MessageCount = System.Byte.MaxValue;
#if ST_STM32F429I_DISCOVERY
			int chipSelectPinNumber = PinNumber('C', 2);
			int resetPinNumber = PinNumber('C', 3);
			int interruptPinNumber = PinNumber('A', 4);
#endif
#if ESP32_WROOM_32_LORA_1_CHANNEL
         int chipSelectPinNumber = Gpio.IO16;
         int interruptPinNumber = Gpio.IO26;

			Configuration.SetPinFunction(Gpio.IO12, DeviceFunction.SPI1_MISO);
			Configuration.SetPinFunction(Gpio.IO13, DeviceFunction.SPI1_MOSI);
			Configuration.SetPinFunction(Gpio.IO14, DeviceFunction.SPI1_CLOCK);

			Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SpiBusId, chipSelectPinNumber, interruptPinNumber);
#endif
#if NETDUINO3_WIFI
			// Arduino D10->PB10
			int chipSelectPinNumber = PinNumber('B', 10);
			// Arduino D9->PE5
			int resetPinNumber = PinNumber('E', 5);
			// Arduino D2->PA3
			int interruptPinNumber = PinNumber('A', 3);
#endif

#if ST_STM32F429I_DISCOVERY || NETDUINO3_WIFI
			Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SpiBusId, chipSelectPinNumber, resetPinNumber, interruptPinNumber);
#endif
			rfm9XDevice.Initialise(Frequency, paBoost: true);

#if DEBUG
			rfm9XDevice.RegisterDump();
#endif

			rfm9XDevice.OnReceive += Rfm9XDevice_OnReceive;
#if ADDRESSED_MESSAGES_PAYLOAD
			rfm9XDevice.Receive(UTF8Encoding.UTF8.GetBytes(DeviceName));
#else
			rfm9XDevice.Receive();
#endif
			rfm9XDevice.OnTransmit += Rfm9XDevice_OnTransmit;

			Thread.Sleep(10000);

			while (true)
			{
				string messageText = string.Format("Hello from {0} ! {1}", DeviceName, MessageCount);
				MessageCount -= 1;

				byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
				Debug.WriteLine(string.Format("{0}-TX {1} byte message {2}", DateTime.UtcNow.ToString("HH:mm:ss"), messageBytes.Length, messageText));
#if ADDRESSED_MESSAGES_PAYLOAD
				rfm9XDevice.Send(UTF8Encoding.UTF8.GetBytes(HostName), messageBytes);
#else
				rfm9XDevice.Send(messageBytes);
#endif
				Thread.Sleep(10000);
			}
		}

		private static void Rfm9XDevice_OnReceive(object sender, Rfm9XDevice.OnDataReceivedEventArgs e)
		{
			try
			{
				// Remove unprintable characters from messages
				for (int index = 0; index < e.Data.Length; index++)
				{
					if ((e.Data[index] < 0x20) || (e.Data[index] > 0x7E))
					{
						e.Data[index] = 0x20;
					}
				}

				string messageText = UTF8Encoding.UTF8.GetString(e.Data, 0, e.Data.Length);

#if ADDRESSED_MESSAGES_PAYLOAD
				string addressText = UTF8Encoding.UTF8.GetString(e.Address, 0, e.Address.Length);

				Debug.WriteLine(string.Format(@"{0}-RX From {1} PacketSnr {2} Packet RSSI {3}dBm RSSI {4}dBm ={5} ""{6}""", DateTime.UtcNow.ToString("HH:mm:ss"), addressText, e.PacketSnr, e.PacketRssi, e.Rssi, e.Data.Length, messageText));
#else
				Debug.WriteLine(string.Format(@"{0}-RX PacketSnr {1} Packet RSSI {2}dBm RSSI {3}dBm ={4} ""{5}""", DateTime.UtcNow.ToString("HH:mm:ss"), e.PacketSnr, e.PacketRssi, e.Rssi, e.Data.Length, messageText));
#endif
			}
			catch (Exception ex)
			{
				Debug.WriteLine(ex.Message);
			}
		}

		private static void Rfm9XDevice_OnTransmit(object sender, Rfm9XDevice.OnDataTransmitedEventArgs e)
		{
			Debug.WriteLine(string.Format("{0}-TX Done", DateTime.UtcNow.ToString("HH:mm:ss")));
		}

#if ST_STM32F429I_DISCOVERY || NETDUINO3_WIFI
		static int PinNumber(char port, byte pin)
      {
         if (port < 'A' || port > 'J')
            throw new ArgumentException();

         return ((port - 'A') * 16) + pin;
      }
#endif
	}
}


The sample client could reliable send and receive messages.

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
Register dump
Register 0x00 - Value 0X7A
Register 0x01 - Value 0X80
Register 0x02 - Value 0X1A
Register 0x03 - Value 0X0B
Register 0x04 - Value 0X00
Register 0x05 - Value 0X52
Register 0x06 - Value 0XE4
Register 0x07 - Value 0XC0
Register 0x08 - Value 0X00
Register 0x09 - Value 0XCF
Register 0x0A - Value 0X09
Register 0x0B - Value 0X2B
Register 0x0C - Value 0X20
Register 0x0D - Value 0X01
Register 0x0E - Value 0X80
Register 0x0F - Value 0X00
Register 0x10 - Value 0X00
Register 0x11 - Value 0X00
Register 0x12 - Value 0X00
Register 0x13 - Value 0X00
Register 0x14 - Value 0X00
Register 0x15 - Value 0X00
Register 0x16 - Value 0X00
Register 0x17 - Value 0X00
Register 0x18 - Value 0X10
Register 0x19 - Value 0X00
Register 0x1A - Value 0X00
Register 0x1B - Value 0X00
Register 0x1C - Value 0X00
Register 0x1D - Value 0X72
Register 0x1E - Value 0X70
Register 0x1F - Value 0X64
Register 0x20 - Value 0X00
Register 0x21 - Value 0X08
Register 0x22 - Value 0X01
Register 0x23 - Value 0XFF
Register 0x24 - Value 0X00
Register 0x25 - Value 0X00
Register 0x26 - Value 0X04
Register 0x27 - Value 0X00
Register 0x28 - Value 0X00
Register 0x29 - Value 0X00
Register 0x2A - Value 0X00
Register 0x2B - Value 0X00
Register 0x2C - Value 0X00
Register 0x2D - Value 0X50
Register 0x2E - Value 0X14
Register 0x2F - Value 0X45
Register 0x30 - Value 0X55
Register 0x31 - Value 0XC3
Register 0x32 - Value 0X05
Register 0x33 - Value 0X27
Register 0x34 - Value 0X1C
Register 0x35 - Value 0X0A
Register 0x36 - Value 0X03
Register 0x37 - Value 0X0A
Register 0x38 - Value 0X42
Register 0x39 - Value 0X12
Register 0x3A - Value 0X49
Register 0x3B - Value 0X1D
Register 0x3C - Value 0X00
Register 0x3D - Value 0XAF
Register 0x3E - Value 0X00
Register 0x3F - Value 0X00
Register 0x40 - Value 0X00
Register 0x41 - Value 0X00
Register 0x42 - Value 0X12
00:00:25-TX 20 byte message Hello from N3W ! 255
00:00:25-TX Done
00:00:35-TX 20 byte message Hello from N3W ! 254
00:00:35-TX Done
00:00:45-TX 20 byte message Hello from N3W ! 253
00:00:45-TX Done
00:00:46-RX PacketSnr 9.50 Packet RSSI -70dBm RSSI -110dBm =59 " LoRaIoT1Maduino2at 43.9,ah 75,wsa 1,wsg 2,wd 36.00,r 0.00,"
00:00:55-TX 20 byte message Hello from N3W ! 252
00:00:55-TX Done
00:01:05-TX 20 byte message Hello from N3W ! 251
00:01:05-TX Done

Overall the process was fairly painless and helped identify a bug in the configuration of the Mode register in one of the test harness applications.

Azure IoT Hub SAS Tokens revisited

A long time ago I wrote a post about uploading telemetry data to an Azure Event Hub from a Netduino 3 Wifi using HTTPS. To send messages to the EventHub I had to create a valid SAS Token which took a surprising amount of effort because of the reduced text encoding/decoding and cryptographic functionality available in .NET Micro Framework v4.3 (NetMF)

// Create a SAS token for a specified scope. SAS tokens are described in http://msdn.microsoft.com/en-us/library/windowsazure/dn170477.aspx.
private static string CreateSasToken(string uri, string keyName, string key)
{
   // Set token lifetime to 20 minutes. When supplying a device with a token, you might want to use a longer expiration time.
   uint tokenExpirationTime = GetExpiry(20 * 60);
 
   string stringToSign = HttpUtility.UrlEncode(uri) + "\n" + tokenExpirationTime;
 
   var hmac = SHA.computeHMAC_SHA256(Encoding.UTF8.GetBytes(key), Encoding.UTF8.GetBytes(stringToSign));
   string signature = Convert.ToBase64String(hmac);
 
   signature = Base64NetMf42ToRfc4648(signature);
 
   string token = "SharedAccessSignature sr=" + HttpUtility.UrlEncode(uri) + "&sig=" + HttpUtility.UrlEncode(signature) + "&se=" + tokenExpirationTime.ToString() + "&skn=" + keyName;
 
   return token;
}
 
private static string Base64NetMf42ToRfc4648(string base64netMf)
{
   var base64Rfc = string.Empty;
 
   for (var i = 0; i < base64netMf.Length; i++)
   {
      if (base64netMf[i] == '!')
      {
         base64Rfc += '+';
      }
      else if (base64netMf[i] == '*')
      {
         base64Rfc += '/';
      }
      else
      {
         base64Rfc += base64netMf[i];
      }
   }
   return base64Rfc;
}
 
static uint GetExpiry(uint tokenLifetimeInSeconds)
{
   const long ticksPerSecond = 1000000000 / 100; // 1 tick = 100 nano seconds
 
   DateTime origin = new DateTime(1970, 1, 1, 0, 0, 0, 0);
   TimeSpan diff = DateTime.Now.ToUniversalTime() - origin;
 
   return ((uint)(diff.Ticks / ticksPerSecond)) + tokenLifetimeInSeconds;
}

Initially for testing my Azure MQTT Test Client I manually generated the SAS tokens using Azure Device Explorer but figured it would be better if the application generated them.

An initial search lead to this article about how to generate a SAS token for an Azure Event Hub in multiple languages. For my first attempt I “copied and paste” the code sample for C# (I also wasn’t certain what to put in the KeyName parameter) and it didn’t work.

private static string createToken(string resourceUri, string keyName, string key)
{
    TimeSpan sinceEpoch = DateTime.UtcNow - new DateTime(1970, 1, 1);
    var week = 60 * 60 * 24 * 7;
    var expiry = Convert.ToString((int)sinceEpoch.TotalSeconds + week);
    string stringToSign = HttpUtility.UrlEncode(resourceUri) + "\n" + expiry;
    HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key));
    var signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));
    var sasToken = String.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}", HttpUtility.UrlEncode(resourceUri), HttpUtility.UrlEncode(signature), expiry, keyName);
    return sasToken;
}

By comparing the Device Explorer and C# generated SAS keys I worked out the keyName parameter was unnecessary so I removed.

private static string createToken(string resourceUri, string key)
{
    TimeSpan sinceEpoch = DateTime.UtcNow - new DateTime(1970, 1, 1);
    var week = 60 * 60 * 24 * 7;
    var expiry = Convert.ToString((int)sinceEpoch.TotalSeconds + week);
    string stringToSign = HttpUtility.UrlEncode(resourceUri) + "\n" + expiry;
    HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key));
    var signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));
    var sasToken = String.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}", HttpUtility.UrlEncode(resourceUri), HttpUtility.UrlEncode(signature), expiry);
    return sasToken;
}

The shared SAS token now looked closer to what I was expecting but the MQTTNet ConnectAsync was failing with an authentication exception. After looking at the Device Explorer SAS Key code, my .NetMF implementation and the code for the IoT Hub SDK I noticed the encoding for the HMAC Key was different. Encoding.UTF8.GetBytes vs. Convert.FromBase64String.

 private static string createToken(string resourceUri,string key, TimeSpan ttl)
      {
         TimeSpan afterEpoch = DateTime.UtcNow.Add( ttl ) - new DateTime(1970, 1, 1);

         string expiry = afterEpoch.TotalSeconds.ToString("F0");
         string stringToSign = HttpUtility.UrlEncode(resourceUri) + "\n" + expiry;
         HMACSHA256 hmac = new HMACSHA256(Convert.FromBase64String(key));
         string signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));
         return  String.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}", HttpUtility.UrlEncode(resourceUri), HttpUtility.UrlEncode(signature), expiry);
      }

This approach appears to work reliably in my test harness.

MQTTnet client with new SAS Key Generator

User beware DIY Crypto often ends badly

Netduino LoRa Radio 433/868/915 MHz Payload Addressing client

This is a demo Netduino client (based on one of the examples in my RFM9XLoRaNetMF library) that uploads telemetry data to my Windows 10 IoT Core on Raspberry PI field gateway proof of concept(PoC).

Bill of materials (Prices Sep 2018)

//---------------------------------------------------------------------------------
// Copyright (c) 2017, 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.Netduino.FieldGateway
{
   using System;
   using System.Text;
   using System.Threading;
   using Microsoft.SPOT;
   using Microsoft.SPOT.Hardware;
   using SecretLabs.NETMF.Hardware.Netduino;
   using devMobile.IoT.NetMF.ISM;
   using devMobile.NetMF.Sensor;

   class NetduinoClient
   {
      Rfm9XDevice rfm9XDevice;
      private readonly TimeSpan dueTime = new TimeSpan(0, 0, 15);
      private readonly TimeSpan periodTime = new TimeSpan(0, 0, 300);
      private readonly SiliconLabsSI7005 sensor = new SiliconLabsSI7005();
      private readonly OutputPort _led = new OutputPort(Pins.ONBOARD_LED, false);
      private readonly byte[] fieldGatewayAddress = Encoding.UTF8.GetBytes("LoRaIoT1");
      private readonly byte[] deviceAddress = Encoding.UTF8.GetBytes("Netduino1");

      public NetduinoClient()
      {
         rfm9XDevice = new Rfm9XDevice(Pins.GPIO_PIN_D10, Pins.GPIO_PIN_D9, Pins.GPIO_PIN_D2);
      }

      public void Run()
      {
         //rfm9XDevice.Initialise(frequency: 915000000, paBoost: true, rxPayloadCrcOn: true);
         rfm9XDevice.Initialise(frequency: 433000000, paBoost: true, rxPayloadCrcOn: true);
         rfm9XDevice.Receive(deviceAddress);

         rfm9XDevice.OnDataReceived += rfm9XDevice_OnDataReceived;
         rfm9XDevice.OnTransmit += rfm9XDevice_OnTransmit;

         Timer humidityAndtemperatureUpdates = new Timer(HumidityAndTemperatureTimerProc, null, dueTime, periodTime);

         Thread.Sleep(Timeout.Infinite);
      }

      private void HumidityAndTemperatureTimerProc(object state)
      {
         _led.Write(true);

         double humidity = sensor.Humidity();
         double temperature = sensor.Temperature();

         Debug.Print(DateTime.UtcNow.ToString("hh:mm:ss") + " H:" + humidity.ToString("F1") + " T:" + temperature.ToString("F1"));

         rfm9XDevice.Send(fieldGatewayAddress, Encoding.UTF8.GetBytes( "t " + temperature.ToString("F1") + ",H " + humidity.ToString("F0")));

         _led.Write(true);
      }

      void rfm9XDevice_OnTransmit()
      {
         Debug.Print("Transmit-Done");
         _led.Write(false);
      }

      void rfm9XDevice_OnDataReceived(byte[] address, float packetSnr, int packetRssi, int rssi, byte[] data)
      {
         try
         {
            string messageText = new string(UTF8Encoding.UTF8.GetChars(data));
            string addressText = new string(UTF8Encoding.UTF8.GetChars(address));

            Debug.Print(DateTime.UtcNow.ToString("HH:MM:ss") + "-Rfm9X PacketSnr " + packetSnr.ToString("F1") + " Packet RSSI " + packetRssi + "dBm RSSI " + rssi + "dBm = " + data.Length + " byte message " + @"""" + messageText + @"""");
         }
         catch (Exception ex)
         {
            Debug.Print(ex.Message);
         }
      }
   }
}

The code is available on GitHub
FieldGatewayNetduinoLoRaElecrow915
Elecrow shield
FieldGatewayNetduinoLoRaDragino915
Dragino shield
FieldGatewayNetduinLoRaMakerFabs433
MakerFabs shield
Net Micro Framework debug output from device

The thread '' (0x2) has exited with code 0 (0x0).
12:00:18 H:96.9 T:19.6
Transmit-Done
12:05:17 H:95.1 T:20.1
Transmit-Done

.Net Framework debug output Field Gateway

The thread 0x1550 has exited with code 0 (0x0).
21:21:49-RX From Netduino1 PacketSnr 9.5 Packet RSSI -40dBm RSSI -107dBm = 11 byte message "t 19.6,H 97"
 Sensor Netduino1t Value 19.6
 Sensor Netduino1H Value 97
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish
...
21:26:49-RX From Netduino1 PacketSnr 9.5 Packet RSSI -33dBm RSSI -103dBm = 11 byte message "t 20.1,H 95"
 Sensor Netduino1t Value 20.1
 Sensor Netduino1H Value 95
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish
The thread 0xfbc has exited with code 0 (0x0).

Then in my Azure IoT Hub

AzureIOTHubExplorerScreenGrab20180917

Azure IoT Hubs LoRa Windows 10 IoT Core Field Gateway

This project is now live on github.com, sample Arduino with Dragino LoRa Shield for Arduino, MakerFabs Maduino, Dragino LoRa Mini Dev, M2M Low power Node and Netduino with Elecrow LoRa RFM95 Shield clients uploaded in the next couple of days.

AzureIOTHubExplorerScreenGrab20180912

The bare minimum configuration is

{
  "AzureIoTHubDeviceConnectionString": "HostName=qwertyuiop.azure-devices.net;DeviceId=LoRaGateway;SharedAccessKey=1234567890qwertyuiop987654321qwertyuiop1234g=",
  "AzureIoTHubTransportType": "Amqp",
  "SensorIDIsDeviceIDSensorID": true,
  "Address": "LoRaIoT1",
  "Frequency": 915000000.0
}

So far battery life and wireless communications range for the Arduino clients is looking pretty good. CRC presence checking and validation is turned so have a look at one of the sample clients.

ArduinoUnoR3DraginoLoRa
It took a bit longer than expected as upgrading to the latest version (v1.18.0 as at 12 Sep 2018) of Microsoft.Azure.Devices.Client (from 1.6.3) broke my field gateway with timeouts and exceptions.

I’ll be doing some more testing over the next couple of weeks so it is a work in progress.

AdaFruit.IO LoRa Windows 10 IoT Core Field Gateway

This project is now live on github.com, sample Arduino with Dragino LoRa Shield for Arduino, MakerFabs Maduino, Dragino LoRa Mini Dev, M2M Low power Node and Netduino with Elecrow LoRa RFM95 Shield clients uploaded in the next couple of days.

AdaFruit.IO.LoRaScreenShot
While building this AdaFruit.IO LoRa field gateway, and sample clients I revisited my RFM9XLoRa-Net library a couple of times adding functionality and renaming constants to make it more consistent. I made many of the default values public so they could be used in the field gateway config file.
The bare minimum configuration is

{
“AdaFruitIOUserName”: “——“,
“AdaFruitIOApiKey”: “——“,
“AdaFruitIOGroupName”: “——”
“Address”: “——“,
“Frequency”: 915000000.0
}

So far battery life and wireless communications range for the Arduino clients is looking pretty good.

ArduinoUnoR3DraginoLoRa