Random wanderings through Microsoft Azure esp. PaaS plumbing, the IoT bits, AI on Micro controllers, AI on Edge Devices, .NET nanoFramework, .NET Core on *nix and ML.NET+ONNX
Uputronics Raspberry PIZero LoRa Expansion board on a Raspberry PI 3 device
M2M Single channel shield on Raspberry Pi 3 Device
The spiDevice.ReadByte() and spiDevice.WriteBye() version worked with a custom chip select pin(25) and CS0 or CS1 selected in the SpiConnectionSettings (but this CS line was “unusable” by other applications). This approach also worked with standard select line (CS01 or CS1) if the SpiConnectionSettings was configured to use the “other” CS line and the selected CS pin managed by the application.
namespace devMobile.IoT.SX127x.ShieldSPIWriteRead
{
class Program
{
private const int SpiBusId = 0;
private const int ChipSelectLine = 1; // 0 or 1 for Uputronics depends on the switch, for the others choose CS pin not already in use
#if ChipSelectNonStandard
private const int ChipSelectPinNumber = 25; // 25 for M2M, Dragino etc.
#endif
private const byte RegisterAddress = 0x6; // RegFrfMsb 0x6c
//private const byte RegisterAddress = 0x7; // RegFrfMid 0x80
//private const byte RegisterAddress = 0x8; // RegFrfLsb 0x00
//private const byte RegisterAddress = 0x42; // RegVersion 0x12
static void Main(string[] args)
{
#if ChipSelectNonStandard
GpioController controller = null;
controller = new GpioController(PinNumberingScheme.Logical);
controller.OpenPin(ChipSelectPinNumber, PinMode.Output);
controller.Write(ChipSelectPinNumber, PinValue.High);
#endif
var settings = new SpiConnectionSettings(SpiBusId, ChipSelectLine)
{
ClockFrequency = 5000000,
Mode = SpiMode.Mode0, // From SemTech docs pg 80 CPOL=0, CPHA=0
};
SpiDevice spiDevice = SpiDevice.Create(settings);
Thread.Sleep(500);
while (true)
{
#if ChipSelectNonStandard
controller.Write(ChipSelectPinNumber, PinValue.Low);
#endif
spiDevice.WriteByte(RegisterAddress);
byte registerValue = spiDevice.ReadByte();
#if ChipSelectNonStandard
controller.Write(ChipSelectPinNumber, PinValue.High);
#endif
byte registerValue = readBuffer[writeBuffer.Length - 1];
Console.WriteLine($"Register 0x{RegisterAddress:x2} - Value 0X{registerValue:x2} - Bits {Convert.ToString(registerValue, 2).PadLeft(8, '0')}");
Thread.Sleep(5000);
}
}
}
}
The spiDevice.TransferFullDuplex worked for a standard CS line (CS0 or CS1), and for a non-standard CS line, though the CS line configured in SpiConnectionSettings was “unusable” by other applications “.
namespace devMobile.IoT.SX127x.ShieldSPITransferFullDuplex
{
class Program
{
private const int SpiBusId = 0;
private const int ChipSelectLine = 0; // 0 or 1 for Uputronics depends on the switch, for the others choose CS pin not already in use
#if ChipSelectNonStandard
private const int ChipSelectPinNumber = 25; // 25 for M2M, Dragino etc.
#endif
private const byte RegisterAddress = 0x6; // RegFrfMsb 0x6c
//private const byte RegisterAddress = 0x7; // RegFrfMid 0x80
//private const byte RegisterAddress = 0x8; // RegFrfLsb 0x00
//private const byte RegisterAddress = 0x42; // RegVersion 0x12
static void Main(string[] args)
{
#if ChipSelectNonStandard
GpioController controller = null;
controller = new GpioController(PinNumberingScheme.Logical);
controller.OpenPin(ChipSelectPinNumber, PinMode.Output);
controller.Write(ChipSelectPinNumber, PinValue.High);
#endif
var settings = new SpiConnectionSettings(SpiBusId, ChipSelectLine)
{
ClockFrequency = 5000000,
Mode = SpiMode.Mode0, // From SemTech docs pg 80 CPOL=0, CPHA=0
};
SpiDevice spiDevice = SpiDevice.Create(settings);
Thread.Sleep(500);
while (true)
{
byte[] writeBuffer = new byte[] { RegisterAddress, 0 };
byte[] readBuffer = new byte[writeBuffer.Length];
#if ChipSelectNonStandard
controller.Write(ChipSelectPinNumber, PinValue.Low);
#endif
spiDevice.TransferFullDuplex(writeBuffer, readBuffer);
#if ChipSelectNonStandard
controller.Write(ChipSelectPinNumber, PinValue.High);
#endif
byte registerValue = readBuffer[writeBuffer.Length - 1];
Console.WriteLine($"Register 0x{RegisterAddress:x2} - Value 0X{registerValue:x2} - Bits {Convert.ToString(registerValue, 2).PadLeft(8, '0')}");
Thread.Sleep(5000);
}
}
}
}
The output when the application was working as expected
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/Microsoft.Win32.Primitives.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Register 0x06 - Value 0X6c - Bits 01101100
Register 0x06 - Value 0X6c - Bits 01101100
Register 0x06 - Value 0X6c - Bits 01101100
Register 0x06 - Value 0X6c - Bits 01101100
Register 0x06 - Value 0X6c - Bits 01101100
Register 0x06 - Value 0X6c - Bits 01101100
The program 'dotnet' has exited with code 0 (0x0).
Summary
Though the spiDevice.TransferFullDuplex code was slightly more complex it worked with both standard and non-standard CS pins.
Uputronics Raspberry PIZero LoRa Expansion board on a Raspberry 3 device
The Uputronics pHat has a pair of Light Emitting Diodes(LEDs) so I adapted some code from a previous post to flash these to confirm the card was working.
static void UputronicsLeds()
{
const int RedLedPinNumber = 6;
const int GreenLedPinNumber = 13;
GpioController controller = new GpioController(PinNumberingScheme.Logical);
controller.OpenPin(RedLedPinNumber, PinMode.Output);
controller.OpenPin(GreenLedPinNumber, PinMode.Output);
while (true)
{
if (controller.Read(RedLedPinNumber) == PinValue.Low)
{
controller.Write(RedLedPinNumber, PinValue.High);
controller.Write(GreenLedPinNumber, PinValue.Low);
}
else
{
controller.Write(RedLedPinNumber, PinValue.Low);
controller.Write(GreenLedPinNumber, PinValue.High);
}
Thread.Sleep(1000);
}
}
The first Uputronics pHat version using spiDevice.TransferFullDuplex didn’t work. I tried allocating memory for the buffers with new and stackalloc which didn’t seem to make any difference in my trivial example. I tried different Chip Select(CS) pin options, frequencies and modes (the mode used is based on the timings specified in the SX127X datasheet).
static void TransferFullDuplex()
{
//byte[] writeBuffer = new byte[1]; // Memory allocation didn't seem to make any difference
//byte[] readBuffer = new byte[1];
Span<byte> writeBuffer = stackalloc byte[1];
Span<byte> readBuffer = stackalloc byte[1];
//var settings = new SpiConnectionSettings(0)
var settings = new SpiConnectionSettings(0, 0)
//var settings = new SpiConnectionSettings(0, 1)
{
ClockFrequency = 5000000,
//ClockFrequency = 500000, // Frequency didn't seem to make any difference
Mode = SpiMode.Mode0, // From SemTech docs pg 80 CPOL=0, CPHA=0
};
SpiDevice spiDevice = SpiDevice.Create(settings);
Thread.Sleep(500);
while (true)
{
try
{
for (byte registerIndex = 0; registerIndex <= 0x42; registerIndex++)
{
writeBuffer[0] = registerIndex;
spiDevice.TransferFullDuplex(writeBuffer, readBuffer);
//Debug.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", writeBuffer[0], readBuffer[0], Convert.ToString(readBuffer[0], 2).PadLeft(8, '0')); // Debug output stopped after roughly 3 times round for loop often debugger would barf as well
Console.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", writeBuffer[0], readBuffer[0], Convert.ToString(readBuffer[0], 2).PadLeft(8, '0'));
// Would be nice if SpiDevice has a TransferSequential
/*
writeBuffer[0] = registerIndex;
spiDevice.TransferSequential(writeBuffer, readBuffer);
Console.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", writeBuffer[0], readBuffer[0], Convert.ToString(readBuffer[0], 2).PadLeft(8, '0'));
*/
}
Console.WriteLine("");
Thread.Sleep(5000);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
static void ReadWriteChipSelectStandard()
{
var settings = new SpiConnectionSettings(0) // Doesn't work
// var settings = new SpiConnectionSettings(0, 0) // Doesn't work
//var settings = new SpiConnectionSettings(0, 1) // Doesn't Work
{
ClockFrequency = 5000000,
ChipSelectLineActiveState = PinValue.Low,
Mode = SpiMode.Mode0, // From SemTech docs pg 80 CPOL=0, CPHA=0
};
SpiDevice spiDevice = SpiDevice.Create(settings);
Thread.Sleep(500);
while (true)
{
try
{
for (byte registerIndex = 0; registerIndex <= 0x42; registerIndex++)
{
spiDevice.WriteByte(registerIndex);
//Thread.Sleep(5); These made no difference
//Thread.Sleep(10);
//Thread.Sleep(20);
//Thread.Sleep(40);
byte registerValue = spiDevice.ReadByte();
Console.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", registerIndex, registerValue, Convert.ToString(registerValue, 2).PadLeft(8, '0'));
}
Console.WriteLine("");
Thread.Sleep(5000);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
The third Uputronics pHat version using spiDevice.ReadByte() and spiDevice.WriteByte() with DIY Chip Select(CS) worked. In previous SPI device libraries I have found that “managing” the CS line in code can be easier to get working The MicroFramework also has more connectionSettings options for better control of CS line timings which reduces the need for DIY.
static void ReadWriteChipSelectDiy()
{
const int CSPinNumber = 8; // CS0
//const int CSPinNumber = 7; // CS1
// DIY CS0 implented with GPIO pin application controls
GpioController controller = new GpioController(PinNumberingScheme.Logical);
controller.OpenPin(CSPinNumber, PinMode.Output);
//controller.Write(CSPinNumber, PinValue.High);
//var settings = new SpiConnectionSettings(0) // Doesn't work
var settings = new SpiConnectionSettings(0, 1) // Works, have to point at unused CS1, this could be a problem is other device on CS1
//var settings = new SpiConnectionSettings(0, 0) // Works, have to point at unused CS0, this could be a problem is other device on CS0
{
ClockFrequency = 5000000,
Mode = SpiMode.Mode0, // From SemTech docs pg 80 CPOL=0, CPHA=0
};
SpiDevice spiDevice = SpiDevice.Create(settings);
Thread.Sleep(500);
while (true)
{
try
{
for (byte registerIndex = 0; registerIndex <= 0x42; registerIndex++)
{
controller.Write(CSPinNumber, PinValue.Low);
spiDevice.WriteByte(registerIndex);
//Thread.Sleep(2); // This maybe necessary
byte registerValue = spiDevice.ReadByte();
controller.Write(CSPinNumber, PinValue.High);
Console.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", registerIndex, registerValue, Convert.ToString(registerValue, 2).PadLeft(8, '0'));
}
Console.WriteLine("");
Thread.Sleep(5000);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
The dotNet/IoT doesn’t support (July2021) the option to “exclusively” open a port so there could be issues with other applications assuming they control CS0/CS1.
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/Microsoft.Win32.Primitives.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Register 0x00 - Value 0X00 - Bits 00000000
Register 0x01 - Value 0X09 - Bits 00001001
Register 0x02 - Value 0X1a - Bits 00011010
Register 0x03 - Value 0X0b - Bits 00001011
Register 0x04 - Value 0X00 - Bits 00000000
Register 0x05 - Value 0X52 - Bits 01010010
Register 0x06 - Value 0X6c - Bits 01101100
Register 0x07 - Value 0X80 - Bits 10000000
Register 0x08 - Value 0X00 - Bits 00000000
Register 0x09 - Value 0X4f - Bits 01001111
Register 0x0a - Value 0X09 - Bits 00001001
Register 0x0b - Value 0X2b - Bits 00101011
Register 0x0c - Value 0X20 - Bits 00100000
Register 0x0d - Value 0X08 - Bits 00001000
Register 0x0e - Value 0X02 - Bits 00000010
Register 0x0f - Value 0X0a - Bits 00001010
Register 0x10 - Value 0Xff - Bits 11111111
Register 0x11 - Value 0X70 - Bits 01110000
Register 0x12 - Value 0X15 - Bits 00010101
Register 0x13 - Value 0X0b - Bits 00001011
Register 0x14 - Value 0X28 - Bits 00101000
Register 0x15 - Value 0X0c - Bits 00001100
Register 0x16 - Value 0X12 - Bits 00010010
Register 0x17 - Value 0X47 - Bits 01000111
Register 0x18 - Value 0X32 - Bits 00110010
Register 0x19 - Value 0X3e - Bits 00111110
Register 0x1a - Value 0X00 - Bits 00000000
Register 0x1b - Value 0X00 - Bits 00000000
Register 0x1c - Value 0X00 - Bits 00000000
Register 0x1d - Value 0X00 - Bits 00000000
Register 0x1e - Value 0X00 - Bits 00000000
Register 0x1f - Value 0X40 - Bits 01000000
Register 0x20 - Value 0X00 - Bits 00000000
Register 0x21 - Value 0X00 - Bits 00000000
Register 0x22 - Value 0X00 - Bits 00000000
Register 0x23 - Value 0X00 - Bits 00000000
Register 0x24 - Value 0X05 - Bits 00000101
Register 0x25 - Value 0X00 - Bits 00000000
Register 0x26 - Value 0X03 - Bits 00000011
Register 0x27 - Value 0X93 - Bits 10010011
Register 0x28 - Value 0X55 - Bits 01010101
Register 0x29 - Value 0X55 - Bits 01010101
Register 0x2a - Value 0X55 - Bits 01010101
Register 0x2b - Value 0X55 - Bits 01010101
Register 0x2c - Value 0X55 - Bits 01010101
Register 0x2d - Value 0X55 - Bits 01010101
Register 0x2e - Value 0X55 - Bits 01010101
Register 0x2f - Value 0X55 - Bits 01010101
Register 0x30 - Value 0X90 - Bits 10010000
Register 0x31 - Value 0X40 - Bits 01000000
Register 0x32 - Value 0X40 - Bits 01000000
Register 0x33 - Value 0X00 - Bits 00000000
Register 0x34 - Value 0X00 - Bits 00000000
Register 0x35 - Value 0X0f - Bits 00001111
Register 0x36 - Value 0X00 - Bits 00000000
Register 0x37 - Value 0X00 - Bits 00000000
Register 0x38 - Value 0X00 - Bits 00000000
Register 0x39 - Value 0Xf5 - Bits 11110101
Register 0x3a - Value 0X20 - Bits 00100000
Register 0x3b - Value 0X82 - Bits 10000010
Register 0x3c - Value 0Xf6 - Bits 11110110
Register 0x3d - Value 0X02 - Bits 00000010
Register 0x3e - Value 0X80 - Bits 10000000
Register 0x3f - Value 0X40 - Bits 01000000
Register 0x40 - Value 0X00 - Bits 00000000
Register 0x41 - Value 0X00 - Bits 00000000
Register 0x42 - Value 0X12 - Bits 00010010
The fourth Uputronics pHat version using spiDevice.TransferFullDuplex with read and write buffers two bytes long and the leading bye of the response ignored worked.
...
while (true)
{
try
{
for (byte registerIndex = 0; registerIndex <= 0x42; registerIndex++)
{
// Doesn't work
writeBuffer[0] = registerIndex;
spiDevice.TransferFullDuplex(writeBuffer, readBuffer);
Console.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", registerIndex, readBuffer[0], Convert.ToString(readBuffer[0], 2).PadLeft(8, '0'));
// Does work
writeBuffer[0] = registerIndex;
spiDevice.TransferFullDuplex(writeBuffer, readBuffer);
Console.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", registerIndex, readBuffer[1], Convert.ToString(readBuffer[1], 2).PadLeft(8, '0'));
// Does work
writeBuffer[1] = registerIndex;
spiDevice.TransferFullDuplex(writeBuffer, readBuffer);
Console.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", registerIndex, readBuffer[1], Convert.ToString(readBuffer[1], 2).PadLeft(8, '0'));
Console.WriteLine("");
}
Console.WriteLine("");
Thread.Sleep(5000);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Register 0x00 - Value 0X00 - Bits 00000000
Register 0x00 - Value 0X00 - Bits 00000000
Register 0x00 - Value 0X00 - Bits 00000000
...
Register 0x42 - Value 0X00 - Bits 00000000
Register 0x42 - Value 0X12 - Bits 00010010
Register 0x42 - Value 0X12 - Bits 00010010
M2M Single channel shield on Raspberry Pi 3 Device
The first M2M pHat version using SpiDevice.Read and SpiDevice.Write with a “custom” CS pin worked.
...
// Chip select with pin which isn't CS0 or CS1 needs M2M shield
static void ReadWriteDiyChipSelectNonStandard()
{
const int CSPinNumber = 25;
// DIY CS0 implented with GPIO pin application controls
GpioController controller = new GpioController(PinNumberingScheme.Logical);
controller.OpenPin(CSPinNumber, PinMode.Output);
//controller.Write(CSPinNumber, PinValue.High);
// Work, this could be a problem is other device on CS0/CS1
var settings = new SpiConnectionSettings(0)
//var settings = new SpiConnectionSettings(0, 0)
//var settings = new SpiConnectionSettings(0, 1)
{
ClockFrequency = 5000000,
Mode = SpiMode.Mode0, // From SemTech docs pg 80 CPOL=0, CPHA=0
};
SpiDevice spiDevice = SpiDevice.Create(settings);
Thread.Sleep(500);
while (true)
{
try
{
for (byte registerIndex = 0; registerIndex <= 0x42; registerIndex++)
{
controller.Write(CSPinNumber, PinValue.Low);
spiDevice.WriteByte(registerIndex);
//Thread.Sleep(2); // This maybe necessary
byte registerValue = spiDevice.ReadByte();
controller.Write(CSPinNumber, PinValue.High);
Console.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", registerIndex, registerValue, Convert.ToString(registerValue, 2).PadLeft(8, '0'));
}
Console.WriteLine("");
Thread.Sleep(5000);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
The next step was to read an array of bytes, using spiDevice.TransferFullDuplex. The SX127X transmit/receive frequency is specified in registers 0x06 RegFrMSB, 0x07 RegFrMid, and 0x08 RegFrLsb. The default frequency is 868MHz which is 0xE4, 0xC0, 0x00
static void TransferFullDuplexBufferBytesRead()
{
const byte length = 3;
byte[] writeBuffer = new byte[length + 1];
byte[] readBuffer = new byte[length + 1];
// Read the frequency which is 3 bytes RegFrMsb 0x6c, RegFrMid 0x80, RegFrLsb 0x00
writeBuffer[0] = 0x06; //
// Works, have to point at unused CS0/CS1, others could be a problem is another another SPI device is on on CS0/CS1
//var settings = new SpiConnectionSettings(0)
var settings = new SpiConnectionSettings(0, 0)
//var settings = new SpiConnectionSettings(0, 1)
{
ClockFrequency = 5000000,
Mode = SpiMode.Mode0, // From SemTech docs pg 80 CPOL=0, CPHA=0
};
SpiDevice spiDevice = SpiDevice.Create(settings);
spiDevice.TransferFullDuplex(writeBuffer, readBuffer);
Console.WriteLine($"Register 0x06-0x{readBuffer[1]:x2} 0x07-0x{readBuffer[2]:x2} 0x08-0x{readBuffer[3]:x2}");
}
-------------------------------------------------------------------
You may only use the Microsoft .NET Core Debugger (vsdbg) with
Visual Studio Code, Visual Studio or Visual Studio for Mac software
to help you develop and test your applications.
-------------------------------------------------------------------
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/System.Private.CoreLib.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
...
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/Microsoft.Win32.Primitives.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Register 0x06-0xe4 0x07-0xc0 0x08-0x00
The final step was write an array of bytes, using spiDevice.TransferFullDuplex to change the transmit/receive frequency to 915MHz. To write a value the first bit of the address byte must be set to 1 hence the 0x86 RegFrMsb address.
static void TransferFullDuplexBufferBytesWrite()
{
const byte length = 3;
byte[] writeBuffer = new byte[length + 1];
byte[] readBuffer = new byte[length + 1];
// Write the frequency which is 3 bytes RegFrMsb 0x6c, RegFrMid 0x80, RegFrLsb or with 0x00 the write mask
writeBuffer[0] = 0x86 ;
// Works, have to point at unused CS0/CS1, others could be a problem is another another SPI device is on on CS0/CS1
//var settings = new SpiConnectionSettings(0)
var settings = new SpiConnectionSettings(0, 0)
//var settings = new SpiConnectionSettings(0, 1)
{
ClockFrequency = 5000000,
Mode = SpiMode.Mode0, // From SemTech docs pg 80 CPOL=0, CPHA=0
};
SpiDevice spiDevice = SpiDevice.Create(settings);
// Set the frequency to 915MHz
writeBuffer[1] = 0xE4;
writeBuffer[2] = 0xC0;
writeBuffer[3] = 0x00;
spiDevice.TransferFullDuplex(writeBuffer, readBuffer);
}
-------------------------------------------------------------------
You may only use the Microsoft .NET Core Debugger (vsdbg) with
Visual Studio Code, Visual Studio or Visual Studio for Mac software
to help you develop and test your applications.
-------------------------------------------------------------------
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/System.Private.CoreLib.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
...
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/Microsoft.Win32.Primitives.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Register 0x06-0x6c 0x07-0x80 0x08-0x00
Register 0x06-0xe4 0x07-0xc0 0x08-0x00
The program 'dotnet' has exited with code 0 (0x0).
Summary
This exceptionally long post was to highlight that with SPI it’s all about timing, first read the datasheet, then build code to validate your understanding.
SX127X SPI interface timing diagram
Some platforms have native TransferSequential implementations but the dotNet/IoT library only has TransferFullDuplex. SPI hardware is always full duplex, if “sequential” is available the implementation will write the provided bytes and then follow them with zeros to read the requested bytes.
-------------------------------------------------------------------
You may only use the Microsoft .NET Core Debugger (vsdbg) with
Visual Studio Code, Visual Studio or Visual Studio for Mac software
to help you develop and test your applications.
-------------------------------------------------------------------
...
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/Microsoft.Win32.Primitives.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Main thread:1
Doing stuff
Doing stuff
Doing stuff
Doing stuff
Doing stuff
Interrupt Thread:6Doing stuff
Doing stuff
Doing stuff
Interrupt Thread:6Doing stuff
Doing stuff
Interrupt Thread:6Doing stuff
Doing stuff
Doing stuff
Doing stuff
Doing stuff
Doing stuff
The program 'dotnet' has exited with code 0 (0x0).
The ManagedThreadId for the main loop(1) was different to the callback(6) which needs some further investigation.
using System;
using System.Diagnostics;
using System.Threading;
namespace devMobile.NetCore.ConsoleApp
{
class Program
{
static void Main(string[] args)
{
while (true)
{
Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss} Hello World!");
Thread.Sleep(1000);
}
}
}
}
The RaspberryDebugger is really simple to install, and “frictionless” to use. The developers have put a lot of effort into making it easy to deploy and debug a .Net Core application running on a Raspberry PI with Visual Studio. All I had to do was search for, then download and install their Visual Studio Extension(VSIX).
Visual Studio Manage Extensions search
Then configure the connection information for the devices I wanted to use.
Visual Studio Options menu for RaspberryDebugger
On my main development system I was using multiple Raspberry PI devices so it was great to be able to pre-configure several devices.
RaspberryDebugger device(s) configuration)
I had connected to each device with PuTTY to check that connectivity was sorted.
After typing in my “Hello world” application I had to select the device I wanted to use
Project menu RaspberryDebugger option
RaspberryDebugger device selection
Then I pressed F5 and it worked! It’s very unusual for things to work first time so I was stunned. The application was “automagically” downloaded and run in the debugger on the device.
-------------------------------------------------------------------
You may only use the Microsoft .NET Core Debugger (vsdbg) with
Visual Studio Code, Visual Studio or Visual Studio for Mac software
to help you develop and test your applications.
-------------------------------------------------------------------
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/System.Private.CoreLib.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Loaded '/home/pi/vsdbg/ConsoleApp/ConsoleApp.dll'. Symbols loaded.
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/System.Runtime.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/System.Console.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/System.Threading.Thread.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/System.Threading.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/System.Text.Encoding.Extensions.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/Microsoft.Win32.Primitives.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
05:50:37 Hello World!
05:50:39 Hello World!
05:50:40 Hello World!
05:50:41 Hello World!
05:50:42 Hello World!
05:50:43 Hello World!
...
I have been soak testing Seeed LoRa-E5 equipped TinyCLR and netNF devices for the last couple of weeks and after approximately two days they would stop sending data. The code is still running but The Things Industries device live data tab is empty.
I have reviewed the code line by line and it looks okay. When I run the application on the device with the debugger attached the device does not stop transmitting (a heisenbug) which is a problem.
First step was to disable the sleep/wakeup power conservation calls and see what happens
#if PAYLOAD_COUNTER
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Send Timeout:{SendTimeout.TotalSeconds} Seconds payload Counter:{PayloadCounter}");
#if CONFIRMED
result = device.Send(BitConverter.GetBytes(PayloadCounter), true, SendTimeout);
#else
result = device.Send(BitConverter.GetBytes(PayloadCounter), false, SendTimeout);
#endif
PayloadCounter += 1;
#endif
if (result != Result.Success)
{
Debug.WriteLine($"Send failed {result}");
}
/*
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Sleep");
result = device.Sleep();
if (result != Result.Success)
{
Debug.WriteLine($"Sleep failed {result}");
return;
}
*/
Thread.Sleep(60000);
/*
Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Wakeup");
result = device.Wakeup();
if (result != Result.Success)
{
Debug.WriteLine($"Wakeup failed {result}");
return;
}
*/
The devices have now been running fine for 4.5 days so it looks like it might be something todo with entering and/or exiting low power mode.
Fiddler Composer with the image field name and upload file button highlighted
The currentimplementation only supports the uploading of one image at a time in a field called “image”.
Fiddler console after succesfull upload
This implementation supports a “Content-Type” of “application/octet-stream” or “image/jpeg”.
[HttpPost("{id}/image")]
public async Task<ActionResult> Upload([FromRoute(Name = "id")][Range(1, int.MaxValue, ErrorMessage = "StockItem id must greater than 0")] int id, [FromForm] IFormFile image)
{
if (image == null)
{
return this.BadRequest("Image image file missing");
}
if (image.Length == 0)
{
return this.BadRequest("Image image file is empty");
}
if ((string.Compare(image.ContentType, "application/octet-stream",true) != 0) && (string.Compare(image.ContentType, "image/jpeg", true) != 0))
{
return this.BadRequest("Image image file content-type is not application/octet-stream or image/jpeg");
}
try
{
using (MemoryStream ms = new MemoryStream())
{
await image.CopyToAsync(ms);
ms.Seek(0, SeekOrigin.Begin);
using (SqlConnection db = new SqlConnection(this.connectionString))
{
DynamicParameters parameters = new DynamicParameters();
parameters.Add("StockItemId", id);
parameters.Add("photo", ms, DbType.Binary, ParameterDirection.Input);
await db.ExecuteAsync(sql: @"UPDATE [WareHouse].[StockItems] SET [Photo]=@Photo WHERE StockItemID=@StockItemId", param: parameters, commandType: CommandType.Text);
}
}
}
catch (SqlException ex)
{
logger.LogError(ex, "Updating photo of StockItem with ID:{0}", id);
return this.StatusCode(StatusCodes.Status500InternalServerError);
}
return this.Ok();
}
After uploading the image I could download it as either a stream of bytes(displayed in Fiddler) or Base64 encoded (this had to be converted to an image)
Fiddler displaying downloaded jpeg image
This implementation doesn’t support the uploading of multiple images or the streaming of larger images but would be sufficient for uploading thumbnails etc.
I needed to add some code using Dapper to retrieve images stored in a database for a webby client. The stockItems table has a column for a photo but they were all null…
CREATE TABLE [Warehouse].[StockItems](
[StockItemID] [int] NOT NULL,
[StockItemName] [nvarchar](100) NOT NULL,
[SupplierID] [int] NOT NULL,
[ColorID] [int] NULL,
[UnitPackageID] [int] NOT NULL,
[OuterPackageID] [int] NOT NULL,
[Brand] [nvarchar](50) NULL,
[Size] [nvarchar](20) NULL,
[LeadTimeDays] [int] NOT NULL,
[QuantityPerOuter] [int] NOT NULL,
[IsChillerStock] [bit] NOT NULL,
[Barcode] [nvarchar](50) NULL,
[TaxRate] [decimal](18, 3) NOT NULL,
[UnitPrice] [decimal](18, 2) NOT NULL,
[RecommendedRetailPrice] [decimal](18, 2) NULL,
[TypicalWeightPerUnit] [decimal](18, 3) NOT NULL,
[MarketingComments] [nvarchar](max) NULL,
[InternalComments] [nvarchar](max) NULL,
[Photo] [varbinary](max) NULL,
[CustomFields] [nvarchar](max) NULL,
[Tags] AS (json_query([CustomFields],N'$.Tags')),
[SearchDetails] AS (concat([StockItemName],N' ',[MarketingComments])),
[LastEditedBy] [int] NOT NULL,
[ValidFrom] [datetime2](7) GENERATED ALWAYS AS ROW START NOT NULL,
[ValidTo] [datetime2](7) GENERATED ALWAYS AS ROW END NOT NULL,
CONSTRAINT [PK_Warehouse_StockItems] PRIMARY KEY CLUSTERED
(
[StockItemID] ASC
)
I uploaded images of three different colours of sellotape dispensers with the following SQL
UPDATE Warehouse.StockItems
SET [Photo] =(SELECT * FROM Openrowset( Bulk 'C:\Users\BrynLewis\Pictures\TapeDispenserBlue.jpg', Single_Blob) as MyImage) where StockItemID =
-- 203 Tape dispenser (Black)
-- 204 Tape dispenser (Red)
-- 205 Tape dispenser (Blue)
There are two options for downloading the image. The first is as a stream of bytes
[HttpGet("{id}/image")]
public async Task<ActionResult> GetImage([Range(1, int.MaxValue, ErrorMessage = "StockItem id must greater than 0")] int id)
{
Byte[] response;
try
{
using (SqlConnection db = new SqlConnection(this.connectionString))
{
response = await db.ExecuteScalarAsync<byte[]>(sql: @"SELECT [Photo] as ""photo"" FROM [Warehouse].[StockItems] WHERE StockItemID=@StockItemId", param: new { StockItemId = id }, commandType: CommandType.Text);
}
if (response == default)
{
logger.LogInformation("StockItem:{0} image not found", id);
return this.NotFound($"StockItem:{id} image not found");
}
}
catch (SqlException ex)
{
logger.LogError(ex, "Looking up a StockItem:{0} image", id);
return this.StatusCode(StatusCodes.Status500InternalServerError);
}
return File(response, "image/jpeg");
}
[HttpGet("{id}/base64")]
public async Task<ActionResult> GetBase64([Range(1, int.MaxValue, ErrorMessage = "Stock item id must greater than 0")] int id)
{
Byte[] response;
try
{
using (SqlConnection db = new SqlConnection(this.connectionString))
{
response = await db.ExecuteScalarAsync<byte[]>(sql: @"SELECT [Photo] as ""photo"" FROM [Warehouse].[StockItems] WHERE StockItemID=@StockItemId", param: new { StockItemId = id }, commandType: CommandType.Text);
}
if (response == default)
{
logger.LogInformation("StockItem:{0} Base64 not found", id);
return this.NotFound($"StockItem:{id} image not found");
}
}
catch (SqlException ex)
{
logger.LogError(ex, "Looking up a StockItem withID:{0} base64", id);
return this.StatusCode(StatusCodes.Status500InternalServerError);
}
return Ok("data:image/jpeg;base64," + Convert.ToBase64String(response));
}
I lost an hour from my life I will never get back figuring out that a correctly formatted/spelt content types “image/jpeg” and “data:image/jpeg;base64” were key to getting the webby client to render image.
It’s not uncommon for SQL Azure servers and databases to suffer from “transient failures”. In application logs I have seen these occur during scale up/down events, periods where my application’s performance has been temporarily impacted (but its throughput has not changed), which I assume has been some load balancing going on in the background and when network connectivity has been a bit flakey.
Now I’m using The Polly Project which builds on the concepts of TOPAZ but has been thoroughly re-engineered with lots of extensibility, an active community and modern codebase. Inspired by Ben Hyrman and several other developers I have built a minimalist wrapper for the Dapper Async methods which detects transient errors using the same approach as the Entity Framework Core library.
I did think about retry functionality for async methods which returned object/dynamic but have only implemented strongly typed ones for the initial version.
[HttpGet]
public async Task<ActionResult<IEnumerable<Model.StockItemListDtoV1>>> Get()
{
IEnumerable<Model.StockItemListDtoV1> response = null;
try
{
using (SqlConnection db = new SqlConnection(this.connectionString))
{
response = await db.QueryWithRetryAsync<Model.StockItemListDtoV1>(sql: @"SELECT [StockItemID] as ""ID"", [StockItemName] as ""Name"", [RecommendedRetailPrice], [TaxRate] FROM [Warehouse].[StockItems]", commandType: CommandType.Text);
}
}
catch (SqlException ex)
{
logger.LogError(ex, "Retrieving list of StockItems");
return this.StatusCode(StatusCodes.Status500InternalServerError);
}
return this.Ok(response);
}
I have struggled to get reproduceable transient failures without pausing execution in the Visual Studio debugger and tinkering with variables or scaling up/down my databases (limit to how often this can be done) or unplugging the network cable at the wrong time.
On a couple of the systems I work on there are a number of queries (often complex spatial searches) which are very resource intensive but are quite readily cached. In these systems we have used HTTP GET and HEAD Request methods together so that the client only re-GETs the query results after a HEAD method indicates there have been updates.
I have been trying to keep the number of changes to my Microsoft SQL Azure World Wide Importers database to a minimum but for this post I have added a rowversion column to the StockGroups table. The rowversion data type is an automatically generated, unique 8 byte binary(12 bytes Base64 encoded) number within a database.
StockGroups table with Version column
Adding a rowversion table to an existing System Versioned table in the SQL Server Management Studio Designer is painful so I used…
ALTER TABLE [Warehouse].[StockGroups] ADD [Version] [timestamp] NULL
To reduce complexity the embedded SQL is contains two commands (normally I wouldn’t do this) one for retrieving the list StockGroups the other for retrieving the maximum StockGroup rowversion. If a StockGroup is changed the rowversion will be “automagically” updated and the maximum value will change.
[HttpGet]
public async Task<ActionResult<IEnumerable<Model.StockGroupListDtoV1>>> Get()
{
IEnumerable<Model.StockGroupListDtoV1> response = null;
try
{
using (SqlConnection db = new SqlConnection(this.connectionString))
{
var parameters = new DynamicParameters();
parameters.Add("@RowVersion", dbType: DbType.Binary, direction: ParameterDirection.Output, size: ETagBytesLength);
response = await db.QueryAsync<Model.StockGroupListDtoV1>(sql: @"SELECT [StockGroupID] as ""ID"", [StockGroupName] as ""Name""FROM [Warehouse].[StockGroups] ORDER BY Name; SELECT @RowVersion=MAX(Version) FROM [Warehouse].[StockGroups]", param: parameters, commandType: CommandType.Text);
if (response.Any())
{
byte[] rowVersion = parameters.Get<byte[]>("RowVersion");
this.HttpContext.Response.Headers.Add("ETag", Convert.ToBase64String(rowVersion));
}
}
}
catch (SqlException ex)
{
logger.LogError(ex, "Retrieving list of StockGroups");
return this.StatusCode(StatusCodes.Status500InternalServerError);
}
return this.Ok(response);
}
I used Telerik Fiddler to to capture the GET response payload.
The HEAD method requests the maximum rwoversion value from the StockGroups table and compares it to the eTag. In a more complex scenario this could be a call to a local cache to see if a query result has bee refreshed.
[HttpHead]
public async Task<ActionResult> Head([Required][FromHeader(Name = "ETag")][MinLength(ETagBase64Length, ErrorMessage = "eTag length invalid too short")][MaxLength(ETagBase64Length, ErrorMessage = "eTag length {0} invalid too long")] string eTag)
{
byte[] headerVersion = new byte[ETagBytesLength];
if (!Convert.TryFromBase64String(eTag, headerVersion, out _))
{
logger.LogInformation("eTag invalid format");
return this.BadRequest("eTag invalid format");
}
try
{
using (SqlConnection db = new SqlConnection(this.connectionString))
{
byte[] databaseVersion = await db.ExecuteScalarAsync<byte[]>(sql: "SELECT MAX(Version) FROM [Warehouse].[StockGroups]", commandType: CommandType.Text);
if (headerVersion.SequenceEqual(databaseVersion))
{
return this.StatusCode(StatusCodes.Status304NotModified);
}
}
}
catch (SqlException ex)
{
logger.LogError(ex, "Retrieving StockItem list");
return this.StatusCode(StatusCodes.Status500InternalServerError);
}
return this.Ok();
}
I used Fiddler to to capture a HEAD response payload a 304 Not modified.
HTTP/1.1 304 Not Modified
Server: Microsoft-IIS/10.0
X-Powered-By: ASP.NET
Date: Sat, 26 Jun 2021 22:09:02 GMT
I then modified the database and the response changed to 200 OK indicating the local cache should be updated with a GET.
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Server: Microsoft-IIS/10.0
X-Powered-By: ASP.NET
Date: Sat, 26 Jun 2021 22:09:59 GMT
This approach combined with the use of the If-Match, If-Modified-Since, If-None-Match and If-Unmodified-since allows web and client side caches to use previously requested results when there have been no changes. This can significantly reduce the amount of network traffic and server requests.
HTTP/1.1 400 Bad Request
Content-Length: 240
Content-Type: application/problem+json; charset=utf-8
Server: Microsoft-IIS/10.0
X-Powered-By: ASP.NET
Date: Sat, 26 Jun 2021 06:28:11 GMT
This was unlike the helpful validation messages returned by the GET method of the StockItems pagination example code
{
"type":"https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title":"One or more validation errors occurred.",
"status":400,
"traceId":"00-bd68c94bf05f5c4ca8752011d6a60533-48e966211dec4847-00",
"errors":
{
"PageSize":["PageSize must be present and greater than 0"],
"PageNumber":["PageNumber must be present and greater than 0"]
}
}
The lack of diagnostic information was not helpful and I’ll explore this further in a future post. I often work on Fintech applications which are “insert only”, or nothing is deleted just marked as inactive/readonly so this approach is viable.