.NET Core RAK3172 LoRaWAN library Part1

Basic connectivity

Over the weekend I have been working on a .NET Core C# library for the RAKwireless RAK3172 module using a RAK3172S breakout board, Seeedstudio Grove Base Hat for Raspberry PI and a Seeedstudio Grove-4 pin Female Jumper to Grove 4 pin Conversion Cable.

Raspberry Pi3 with Grove Base Hat and RAK3172 Breakout (using UART2)

The RaspberryPI OS is a bit more strict than the other devices I use about port access. To allow my .Net Core application to access a serial port I connected to the device with ExtraPutty, then ran the RaspberyPI configuration tool, from the command prompt with “sudo raspi-config”

RaspberyPI OS Software Configuration tool mains screen
RaspberryPI OS IO Serial Port configuration
Raspberry PI OS disabling remote serial login shell
RaspberryPI OS enabling serial port access

Once serial port access was enabled I could enumerate them with SerialPort.GetPortNames() which is in the System.IO.Ports NuGet package. My sample code has compile time options for synchronous and asynchronous operation.

//---------------------------------------------------------------------------------
// Copyright (c) September 2021, 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.NetCore.RAK3172.ShieldSerial
{
	using System;
	using System.Diagnostics;
	using System.IO.Ports;
	using System.Threading;

	public class Program
	{
		private const string SerialPortId = "/dev/ttyS0";

		public static void Main()
		{
			SerialPort serialPort;

			Debug.WriteLine("devMobile.IoT.NetCore.Rak3172.pHatSerial starting");

			Debug.WriteLine(String.Join(",", SerialPort.GetPortNames()));

			try
			{
				serialPort = new SerialPort(SerialPortId);

				// set parameters
				serialPort.BaudRate = 9600;
				serialPort.Parity = Parity.None;
				serialPort.DataBits = 8;
				serialPort.StopBits = StopBits.One;
				serialPort.Handshake = Handshake.None;

				serialPort.ReadTimeout = 1000;

				serialPort.NewLine = "\r\n";

				serialPort.Open();

#if SERIAL_ASYNC_READ
				serialPort.DataReceived += SerialDevice_DataReceived;
#endif

				while (true)
				{
					serialPort.WriteLine("AT+VER=?");

#if SERIAL_SYNC_READ
					// Read the response
					string response = serialPort.ReadLine();
					Debug.WriteLine($"RX:{response.Trim()} bytes:{response.Length}");

					// Read the blank line
					response = serialPort.ReadLine();
					Debug.WriteLine($"RX:{response.Trim()} bytes:{response.Length}");

					// Read the result
					response = serialPort.ReadLine();
					Debug.WriteLine($"RX:{response.Trim()} bytes:{response.Length}");
#endif

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

#if SERIAL_ASYNC_READ
		private static void SerialDevice_DataReceived(object sender, SerialDataReceivedEventArgs e)
		{
			SerialPort serialPort = (SerialPort)sender;

			switch (e.EventType)
			{
				case SerialData.Chars:
					string response = serialPort.ReadExisting();

					Debug.WriteLine($"RX:{response.Trim()} bytes:{response.Length}");
					break;

				case SerialData.Eof:
					Debug.WriteLine("RX :EoF");
					break;
				default:
					Debug.Assert(false, $"e.EventType {e.EventType} unknown");
					break;
			}
		}
#endif
	}
}

When I requested the RAK3172 version information with the AT+VER? command the response was three lines, consisting of the version information, a blank line, then the result of the command. If I sent an invalid command the response was two lines, a blank line then “AT_ERROR”

AT+VER? response synchronous

The asynchronous version of the application displays character(s) as they arrive so a response could be split across multiple SerialDataReceived events

AT+VER? response asynchronous

Unlike the RAK811 module the RAK3172 defaults 9600 baud which means there is no need to change the baudrate before using the device. I use the excellent RaspberryDebugger to download application and debug them on my Raspberry PI 3.

.NET Core Seeed LoRaE5 LoRaWAN library Part1

Basic connectivity

Over the weekend I started building a .Net Core C# library for a Seeedstudio LoRa-E5 Development Kit which was connected to a Raspberry PI 3 with a Grove Base Hat for Raspberry Pi

The RaspberryPI OS is a bit more strict than the other devices I use about port access. To allow my .Net Core application to access a serial port I connected to the device with ExtraPutty, then ran the RaspberyPI configuration tool, from the command prompt with “sudo raspi-config”

RaspberyPI OS Software Configuration tool mains screen
RaspberryPI OS IO Serial Port configuration
Raspberry PI OS disabling remote serial login shell
RaspberryPI OS enabling serial port access

Once serial port access was enabled I could enumerate them with SerialPort.GetPortNames() which is in the System.IO.Ports NuGet package. The code has compile time options for synchronous and asynchronous operation.

//---------------------------------------------------------------------------------
// Copyright (c) September 2021, 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.NetCore.SeeedLoRaE5.ShieldSerial
{
	using System;
	using System.Diagnostics;
	using System.IO.Ports;
	using System.Threading;

	public class Program
	{
		private const string SerialPortId = "/dev/ttyS0";

		public static void Main()
		{
			SerialPort serialPort;

			Debug.WriteLine("devMobile.IoT.NetCore.SeeedLoRaE5.ShieldSerial starting");

			Debug.WriteLine(String.Join(",", SerialPort.GetPortNames()));

			try
			{
				serialPort = new SerialPort(SerialPortId);

				// set parameters
				serialPort.BaudRate = 9600;
				serialPort.Parity = Parity.None;
				serialPort.DataBits = 8;
				serialPort.StopBits = StopBits.One;
				serialPort.Handshake = Handshake.None;

				serialPort.ReadTimeout = 1000;

				serialPort.NewLine = "\r\n";

				serialPort.Open();

#if SERIAL_ASYNC_READ
				serialPort.DataReceived += SerialDevice_DataReceived;
#endif

				while (true)
				{
					serialPort.WriteLine("AT+VER");

#if SERIAL_SYNC_READ
					string response = serialPort.ReadLine();

					Debug.WriteLine($"RX:{response.Trim()} bytes:{response.Length}");
#endif

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

#if SERIAL_ASYNC_READ
		private static void SerialDevice_DataReceived(object sender, SerialDataReceivedEventArgs e)
		{
			SerialPort serialPort = (SerialPort)sender;

			switch (e.EventType)
			{
				case SerialData.Chars:
					string response = serialPort.ReadExisting();

					Debug.WriteLine($"RX:{response.Trim()} bytes:{response.Length}");
					break;

				case SerialData.Eof:
					Debug.WriteLine("RX :EoF");
					break;
				default:
					Debug.Assert(false, $"e.EventType {e.EventType} unknown");
					break;
			}
		}
#endif
	}
}

The synchronous version of the test client requests the Seeeduino LoRa-E5 version information with the AT+VER command.

Synchronously reading characters from the Seeeduino LoRa-E5

The asynchronous version of the application displays character(s) as they arrive so a response can be split across multiple SerialDataReceived events.

Asynchronous versions displaying partial responses

I use the excellent RaspberryDebugger to download the application and debug it on my Raspberry PI 3.

.NET Core RAK811 LoRaWAN library Part1

Basic connectivity

In my spare time over the last couple of days I have been working on a .Net Core C# library for a RAKWireless RAK811 based PiSupply IoT LoRa Node pHat for Raspberry PI.

Raspberry Pi3 with PI Supply RAK811 based IoT node LoRaWAN pHat

The RaspberryPI OS is a bit more strict than the other devices I use about port access. To allow my .Net Core application to access a serial port I connected to the device with ExtraPutty, then ran the RaspberyPI configuration tool, from the command prompt with “sudo raspi-config”

RaspberyPI OS Software Configuration tool mains screen
RaspberryPI OS IO Serial Port configuration
Raspberry PI OS disabling remote serial login shell
RaspberryPI OS enabling serial port access

Once serial port access was enabled I could enumerate them with SerialPort.GetPortNames() which is in the System.IO.Ports NuGet package. The code has compile time options for synchronous and asynchronous operation.

//---------------------------------------------------------------------------------
// Copyright (c) September 2021, 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.NetCore.Rak811.pHatSerial
{
	using System;
	using System.Diagnostics;

	using System.IO.Ports;
	using System.Threading;

	public class Program
	{
		private const string SerialPortId = "/dev/ttyS0";

		public static void Main()
		{
			SerialPort serialPort;

			Debug.WriteLine("devMobile.IoT.NetCore.Rak811.pHatSerial starting");

			Debug.WriteLine(String.Join(",", SerialPort.GetPortNames()));

			try
			{
				serialPort = new SerialPort(SerialPortId);

				// set parameters
#if DEFAULT_BAUDRATE
				serialDevice.BaudRate = 115200;
#else
            serialPort.BaudRate = 9600;
#endif
				serialPort.Parity = Parity.None;
				serialPort.DataBits = 8;
				serialPort.StopBits = StopBits.One;
				serialPort.Handshake = Handshake.None;

				serialPort.ReadTimeout = 1000;

				serialPort.NewLine = "\r\n";

				serialPort.Open();

#if DEFAULT_BAUDRATE
				Debug.WriteLine("RAK811 baud rate set to 9600");
				serialDevice.Write("at+set_config=device:uart:1:9600");
#endif

#if SERIAL_ASYNC_READ
				serialPort.DataReceived += SerialDevice_DataReceived;
#endif

				while (true)
				{
					serialPort.WriteLine("at+version");

#if SERIAL_SYNC_READ
					string response = serialPort.ReadLine();

					Debug.WriteLine($"RX:{response.Trim()} bytes:{response.Length}");
#endif

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

#if SERIAL_ASYNC_READ
		private static void SerialDevice_DataReceived(object sender, SerialDataReceivedEventArgs e)
		{
			SerialPort serialPort = (SerialPort)sender;

			switch (e.EventType)
			{
				case SerialData.Chars:
					string response = serialPort.ReadExisting();

					Debug.WriteLine($"RX:{response.Trim()} bytes:{response.Length}");
					break;

				case SerialData.Eof:
					Debug.WriteLine("RX :EoF");
					break;
				default:
					Debug.Assert(false, $"e.EventType {e.EventType} unknown");
					break;
			}
		}
#endif
	}
}

The first step was to change the RAK811 serial port speed from 115200 to 9600 baud.

Changing RAK811 serial port from 115200 to 9600 baud

Then I requested the RAK811 version information with the at+version command.

Synchronously reading characters from the RAK811 partial response

I had to add a short delay between sending the command and reading the response.

Synchronously reading characters from the RAK811 complete command responses

The asynchronous version of the application displays character(s) as they arrive so a response could be split across multiple SerialDataReceived events

Asynchronous versions displaying partial responses

I use the excellent RaspberryDebugger to download the application and debug it on my Raspberry PI 3.

Azure Functions with VB.Net 4.X

As part of my “day job” I spend a lot of time working with C# and VB.Net 4.X “legacy” projects doing upgrades, bugs fixes and moving applications to Azure. For the last couple of months I have been working on a project replacing Microsoft message queue(MSMQ) queues with Azure Storage Queues so the solution is easier to deploy in Azure.

The next phase of the project is to replace a number of Windows Services with Azure Queue Trigger and Timer Trigger functions. The aim is a series of small steps which we can test before deployment rather than major changes, hence the use of V1 Azure functions for the first release.

Silver Fox systems sells a Visual Studio extension which generates an HTTP Trigger VB.Net project. I needed Timer and Queue Trigger functions so I created C# examples and then used them to figure out how to build VB.Net equivalents

Visual Studio Solution Explorer

After quite a few failed attempts I found this sequence worked for me

Add a new VB.Net class library
Provide a name for new class library
Select target framework

Even though the target platform is not .NET 5.0 ignore this and continue.

Microsoft.NET.Sdk.Functions

Added Microsoft.NET.Sdk.Functions (make sure version 1.0.38)

Visual Studio project with Azure Function Icon.

Then unload the project and open the file.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <RootNamespace>TimerClass</RootNamespace>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.38" />
  </ItemGroup>

</Project>

Add the TargetFramework and AzureFunctionsVersion lines

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <RootNamespace>TimerClass</RootNamespace>
    <TargetFramework>net48</TargetFramework>
    <AzureFunctionsVersion>v1</AzureFunctionsVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.38" />
  </ItemGroup>

</Project>

At this point the project should compile but won’t do much, so update the class to look like the code below.

Imports System.Threading

Imports Microsoft.Azure.WebJobs
Imports Microsoft.Extensions.Logging


Public Class TimerTrigger
   Shared executionCount As Int32

   <FunctionName("Timer")>
   Public Shared Sub Run(<TimerTrigger("0 */1 * * * *")> myTimer As TimerInfo, log As ILogger)
      Interlocked.Increment(executionCount)

      log.LogInformation("VB.Net TimerTrigger next trigger:{0} Execution count:{1}", myTimer.ScheduleStatus.Next, executionCount)

   End Sub
End Class

Then add an empty hosts.json file (make sure “copy if newer” is configured in properties) to the project directory, then depending on deployment model configure the AzureWebJobsStorage and AzureWebJobsDashboard connection strings via environment variables or a local.settings.json file.

Visual Studio Environment variables for AzureWebJobsStorage and AzureWebJobsDashboard connection strings

Blob Trigger Sample code

Imports System.IO
Imports System.Threading

Imports Microsoft.Azure.WebJobs
Imports Microsoft.Extensions.Logging


Public Class BlobTrigger
   Shared executionCount As Int32

   ' This function will get triggered/executed when a new message is written on an Azure Queue called events.
   <FunctionName("Notifications")>
   Public Shared Async Sub Run(<BlobTrigger("notifications/{name}", Connection:="BlobEndPoint")> payload As Stream, name As String, log As ILogger)
      Interlocked.Increment(executionCount)

      log.LogInformation("VB.Net BlobTrigger processed blob name:{0} Size:{1} bytes Execution count:{2}", name, payload.Length, executionCount)
   End Sub
End Class

HTTP Trigger Sample code

Imports System.Net
Imports System.Net.Http
Imports System.Threading

Imports Microsoft.Azure.WebJobs
Imports Microsoft.Azure.WebJobs.Extensions.Http
Imports Microsoft.Extensions.Logging


Public Class HttpTrigger
   Shared executionCount As Int32

   <FunctionName("Notifications")>
   Public Shared Async Function Run(<HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route:=Nothing)> req As HttpRequestMessage, log As ILogger) As Task(Of HttpResponseMessage)
      Interlocked.Increment(executionCount)

      log.LogInformation($"VB.Net HTTP trigger Execution count:{0} Method:{1}", executionCount, req.Method)

      Return New HttpResponseMessage(HttpStatusCode.OK)
   End Function
End Class

Queue Trigger Sample Code

Imports System.Threading

Imports Microsoft.Azure.WebJobs
Imports Microsoft.Extensions.Logging


Public Class QueueTrigger
   Shared ConcurrencyCount As Long
   Shared ExecutionCount As Long

   <FunctionName("Alerts")>
   Public Shared Sub ProcessQueueMessage(<QueueTrigger("notifications", Connection:="QueueEndpoint")> message As String, log As ILogger)
      Interlocked.Increment(ConcurrencyCount)
      Interlocked.Increment(ExecutionCount)

      log.LogInformation("VB.Net Concurrency:{0} Message:{1} Execution count:{2}", ConcurrencyCount, message, ExecutionCount)

      ' Wait for a bit to force some consurrency
      Thread.Sleep(5000)

      Interlocked.Decrement(ConcurrencyCount)
   End Sub
End Class

As well as counting the number of executions I also wanted to check that >1 instances were started to process messages when the queues had many messages. I added a “queues” section to the hosts.json file so I could tinker with the options.

{
  "queues": {
    "maxPollingInterval": 100,
    "visibilityTimeout": "00:00:05",
    "batchSize": 16,
    "maxDequeueCount": 5,
    "newBatchThreshold": 8
  }
}

The QueueMessageGenerator application inserts many messages into a queue for processing.

When I started the QueueTrigger function I could see the concurrency count was > 0

Timer Trigger Sample Code

Imports System.Threading

Imports Microsoft.Azure.WebJobs
Imports Microsoft.Extensions.Logging


Public Class TimerTrigger
   Shared executionCount As Int32

   <FunctionName("Timer")>
   Public Shared Sub Run(<TimerTrigger("0 */1 * * * *")> myTimer As TimerInfo, log As ILogger)
      Interlocked.Increment(executionCount)

      log.LogInformation("VB.Net TimerTrigger next trigger:{0} Execution count:{1}", myTimer.ScheduleStatus.Next, executionCount)

   End Sub
End Class

The source code for the C# and VB.Net functions is available on GitHub