August 2019

Volume 34 Number 8

[.NET Core]

Cross-Platform IoT Programming with .NET Core 3.0

By Dawid Borycki

Microsoft Build 2019 brought exciting news for .NET developers: .NET Core 3.0 now supports C# 8.0, Windows Desktop and IoT, so you can use your existing .NET skills to develop cross-platform apps for smart devices. In this article I’ll show you how to create a .NET Core app for Raspberry Pi 2/3 with the Sense HAT add-on board. The app will obtain various sensor readings, and the most recent readings will be accessible through the ASP.NET Core Web API service. I’ll create the simple UI for this service with Swagger (Figure 1), which will let you easily interact with the IoT device. Besides getting data from the device, you’ll also be able to remotely change the color of the Sense HAT’s LED array (Figure 2). The companion code is available through my GitHub page: bit.ly/2WCj0G2.

Obtaining Sensor Readings from the IoT Device Running .NET Core 3.0 App Through the Web API
Figure 1 Obtaining Sensor Readings from the IoT Device Running .NET Core 3.0 App Through the Web API

Remote Control of the IoT Device (Raspberry Pi 2 with the Sense HAT Add-On Board)
Figure 2 Remote Control of the IoT Device (Raspberry Pi 2 with the Sense HAT Add-On Board)

My Device

I started by setting up my IoT device, comprising Raspberry Pi 2 (or RPi2 for short) with the Sense HAT add-on board (see the right side of Figure 2). RPi2 is a popular single-board computer that can run the Linux or Windows 10 IoT Core operating systems. You can get this device from, for example, adafruit.com. The Sense HAT add-on board is equipped with several sensors, including thermometer, barometer, magnetometer, gyroscope, and accelerometer. Additionally, Sense HAT has 64 RGB LEDs you can use as an indicator or a low-resolution screen. Sense HAT is easily attachable to the RPi2, so you can quickly access sensor readings without any soldering. To power up the RPi2 I used a USB cable, then connected to the local Wi-Fi network using a USB dongle (because RPi2, unlike RPi3, doesn’t have the onboard Wi-Fi module).

After setting up the hardware, I installed the Windows 10 IoT Core. The full instructions are available at bit.ly/2Ic1Ew1. Briefly, you flash the OS onto the microSD card. This can be done easily with Windows 10 Core Dashboard (bit.ly/2VvXm76). Once the dashboard is installed, go to the Set up a new device tab and choose the device type, OS Build, Device name, Administrator password and Wi-Fi connection available to your PC. Accept the software license terms and click Download and install. When this process is finished, insert the microSD card into the IoT device and power it up. You’ll soon see your device as an entry under the dashboard’s My devices tab.

Common Library

Before starting the actual implementation, I installed .NET Core 3.0 Preview 5. I then opened Visual Studio 2019 and created the new project using the Class Library (.NET Core) template. I set the project and solution names to SenseHat.DotNetCore.Common and SenseHat.DotNetCore, respectively. Then I installed the Iot.Device.Bindings NuGet package (github.com/dotnet/iot) by invoking the following command in the Package Manager Console (bit.ly/2KRvCHj):

Install-Package IoT.Device.Bindings -PreRelease
  -Source https://dotnetfeed.blob.core.windows.net/dotnet-iot/index.json

IoT.Device.Bindings is an open source .NET Core implementation for popular IoT hardware components that lets you quickly implement .NET Core apps for IoT devices. The Sense HAT binding, which is most relevant here, is available under the src/devices/SenseHat subfolder. A quick glance at this code shows that IoT.Device.Bindings uses the I2C bus to access Sense HAT components. Before showing you how to use IoT.Device.Bindings for the Sense HAT, I first implemented the simple POCO class, SensorReadings (SenseHat.DotNetCore.Common/Sensors/SensorReadings.cs):

public class SensorReadings
{
  public Temperature Temperature { get; set; }
  public Temperature Temperature2 { get; set; }
  public float Humidity { get; set; }
  public float Pressure { get; set; }
  public DateTime TimeStamp { get; } = DateTime.UtcNow;
}

This class has five public members. Four members store the actual sensor readings, two temperature readings, plus humidity and pressure. There are two temperatures because Sense HAT has two temperature sensors, one embedded into the LPS25H pressure sensor (bit.ly/2MiYZEI) and the other in the HTS221 humidity sensor (bit.ly/2HMP9GO). Temperatures are represented by the Iot.Units.Temperature struct from Iot.Device.Bindings. This struct doesn’t have any public constructors but can be instantiated using one of the static methods: FromCelsius, FromFahrenheit or FromKelvin. The struct, given the temperature in one of these scales, will convert a value to other units. You can then obtain the temperature in selected units by reading the appropriate properties: Celsius, Fahrenheit or Kelvin.

The fifth member of the SensorReadings class, TimeStamp, contains the time point when the sensor readings were recorded. This can be useful when you stream data to the cloud and then perform a time series analysis with dedicated services like Azure Stream Analytics or Time Series Insights.

In addition, the SensorReadings class overrides the ToString method to properly display sensor values in the console:

public override string ToString()
{
  return $"Temperature: {Temperature.Celsius,5:F2} °C"
    + $" Temperature2: {Temperature2.Celsius,5:F2} °C"
    + $" Humidity: {Humidity,4:F1} %"
    + $" Pressure: {Pressure,6:F1} hPa";
}

The next step is to implement the class SenseHatService to access selected Sense HAT components. For testing purposes, I also decided to implement another class, SenseHatEmulationService, which serves as the emulator. I wanted to quickly test the Web API and other elements of the code without the need to connect the hardware. To easily switch between two concrete implementations, I used the dependency injection software design pattern. To this end, I first declared an interface:

public interface ISenseHatService
{
  public SensorReadings SensorReadings { get; }
  public void Fill(Color color);
  public bool EmulationMode { get; }
}

This interface defines the common members for the two concrete implementations:

  • SensorReadings: This property enables the caller to get values obtained from the sensors—real values in the case of SenseHatService—and randomly generated values with SenseHatEmulationService.
  • Fill: This method will be used to uniformly set the color of all LEDs.
  • EmulationMode: This property indicates whether the Sense HAT is emulated (true) or not (false).

Next, I implemented the SenseHatService class. Let’s look at the class constructor first, as shown in Figure 3. This constructor instantiates three private read-only fields: ledMatrix, pressure­AndTemperatureSensor and temperatureAndHumiditySensor. To do this, I used appropriate classes from the IoT.Device.Bindings package: SenseHatLedMatrixI2c, SenseHatPressureAndTemperature and SenseHatTemperatureAndHumidity, respectively. Each class has a public constructor, which accepts one parameter, i2cDevice of an abstract type System.Device.I2c.I2cDevice. The default value of this parameter is null. In such a case, IoT.Device.Bindings will internally use an instance of the System.Device.I2c.Drivers.UnixI2cDevice or System.Device.I2c.Drivers.Windows10I2cDevice, depending on the OS.

Figure 3 Selected Members of the SenseHatService Class

public class SenseHatService : ISenseHatService
{
  // Other members of the SenseHatService (refer to the companion code)
  private readonly SenseHatLedMatrix ledMatrix;
  private readonly SenseHatPressureAndTemperature pressureAndTemperatureSensor;
  private readonly SenseHatTemperatureAndHumidity temperatureAndHumiditySensor;
  public SenseHatService()
  {
    ledMatrix = new SenseHatLedMatrixI2c();   
    pressureAndTemperatureSensor = new SenseHatPressureAndTemperature();
    temperatureAndHumiditySensor = new SenseHatTemperatureAndHumidity(); 
  }
}

Afterward, you can obtain sensor values (in the companion code, refer to SenseHat.DotNetCore.Common/Services/SenseHatService.cs):

public SensorReadings SensorReadings => GetReadings();
private SensorReadings GetReadings()
{
  return new SensorReadings
  {
    Temperature = temperatureAndHumiditySensor.Temperature,
    Humidity = temperatureAndHumiditySensor.Humidity,
    Temperature2 = pressureAndTemperatureSensor.Temperature,
    Pressure = pressureAndTemperatureSensor.Pressure
  };
}

and set the color of LED array:

public void Fill(Color color) => ledMatrix.Fill(color);

Before trying that out, though, let’s first implement the emulator, SenseHatEmulationService. The full code for this class is available in the companion code (SenseHat.DotNetCore.Common/Services/SenseHatEmulationService.cs). The class derives from the ISenseHatService interface, so it has to implement the three public members described earlier: SensorReadings, Fill and EmulationMode.

I started by synthesizing sensor readings. For this purpose, I implemented the following helper method:

private double GetRandomValue(SensorReadingRange sensorReadingRange)
{
  var randomValueRescaled = randomNumberGenerator.NextDouble()
    * sensorReadingRange.ValueRange();
  return sensorReadingRange.Min + randomValueRescaled;
}
private readonly Random randomNumberGenerator = new Random();

GetRandomValue uses the System.Random class to generate the double, whose value falls in the specified range. This range is represented by an instance of the SensorReadingRange (SenseHat.DotNetCore.Common/Sensors/SensorReadingRange.cs), which has two public properties, Min and Max, that specify the minimum and maximum values for the emulated sensor reading. Additionally, the SensorReadingRange class implements one instance method, ValueRange, that returns the difference between Max and Min.

GetRandomValue is employed in the GetSensorReadings private method to synthesize sensor readings as shown in Figure 4.

Figure 4 SenseHatEmulationService

private SensorReadings GetSensorReadings()
{
  return new SensorReadings
  {
    Temperature =
      Temperature.FromCelsius(GetRandomValue(temperatureRange)),
    Humidity = (float)GetRandomValue(humidityRange),
    Temperature2 =
      Temperature.FromCelsius(GetRandomValue(temperatureRange)),
    Pressure = (float)GetRandomValue(pressureRange)
  };
}
private readonly SensorReadingRange temperatureRange =   new SensorReadingRange { Min = 20, Max = 40 };
private readonly SensorReadingRange humidityRange =   new SensorReadingRange { Min = 0, Max = 100 };
private readonly SensorReadingRange pressureRange =   new SensorReadingRange { Min = 1000, Max = 1050 };

Finally, I implemented the public members:

public SensorReadings SensorReadings => GetSensorReadings();
public void Fill(Color color) {/* Intentionally do nothing*/}
public bool EmulationMode => true;

I also created the SenseHatServiceHelper class, which I’ll use later (SenseHat.DotNetCore.Common/Helpers/SenseHatServiceHelper.cs). SenseHatServiceHelper class has one public method, which returns an instance of either the SenseHatService or SenseHatEmulationService, depending on the Boolean parameter, emulationMode:

public static ISenseHatService GetService(bool emulationMode = false)
{
  if (emulationMode)
  {
    return new SenseHatEmulationService();
  }
  else
  {
    return new SenseHatService();
  }
}

The Console App

I can now test the code using my console app. This app can be exe­cuted on your development PC or the IoT device. When running on the PC, the app can use the emulator. To switch between emulation and non-emulation modes, I’ll use a command-line argument, which will be a string with either a Y or N letter. The console app will parse this argument and then use either SenseHatEmulationService (Y) or SenseHatService (N). In emulation mode, the app will only display synthesized sensor readings. In non-emulation mode, the app will display values obtained from real sensors, and will also sequentially change the LED array color.

To create the console app, I supplemented the SenseHat.DotNetCore solution with a new project, SenseHat.DotNetCore.ConsoleApp, which I created using the Console App (.NET Core) project template. Then I referenced SenseHat.DotNetCore.Common and started implementing the Program class. I defined three private members:

private static readonly Color[] ledColors =
  { Color.Red, Color.Blue, Color.Green };
private static readonly int msDelayTime = 1000;
private static int ledColorIndex = 0;

The first member, ledColors, is a collection of colors that will be used sequentially to uniformly change the LED array. The second member, msDelayTime, specifies a time duration between accessing consecutive sensor readings and changing the LED array. The last member, ledColorIndex, stores the value of the currently displayed color from ledColors collection.

Next, I wrote two helper methods:

private static bool ParseInputArgumentsToSetEmulationMode(string[] args)
{
    return args.Length == 1 && string.Equals(
      args[0], "Y", StringComparison.OrdinalIgnoreCase);
}

and:

private static void ChangeFillColor(ISenseHatService senseHatService)
{
  if (!senseHatService.EmulationMode)
  {
    senseHatService.Fill(ledColors[ledColorIndex]);
    ledColorIndex = ++ledColorIndex % ledColors.Length;
  }
}

The first method, ParseInputArgumentsToSetEmulationMode, analyzes the collection of input arguments (passed to the Main method) to determine whether emulation mode should be used. The method will return true only if the collection has one element equal to y or Y.

The second method, ChangeFillColor, is effective only when emulation mode is off. If it is, the method will take a color from the ledColors collection and pass it to the Fill method of the concrete implementation of the SenseHatService; ledColorIndex is then incremented. In this specific implementation, the if statement in ChangeFillColor can be omitted because the Fill method of the SenseHatEmulationService does nothing. However, in general, the if statement should be included.

Finally, I implemented the Main method, shown in Figure 5, which wires everything together. First, the input arguments are parsed and, based on the result, I invoke the GetService static method of the SenseHatServiceHelper. Second, I display the string to inform the user whether or not the app works in emulation mode. Third, I start the infinite loop, in which sensor readings are obtained and, eventually, the LED array color is changed. The loop uses the msDelayTime to pause the app execution.

Figure 5 Entry Point of the Console App

static void Main(string[] args)
{
  // Parse input arguments, and set emulation mode accordingly
  var emulationMode = ParseInputArgumentsToSetEmulationMode(args);
  // Instantiate service
  var senseHatService = SenseHatServiceHelper.GetService(emulationMode);
  // Display the mode
  Console.WriteLine($"Emulation mode: {senseHatService.EmulationMode}");
  // Infinite loop
  while (true)
  {
    // Display sensor readings
    Console.WriteLine(senseHatService.SensorReadings);
    // Change the LED array color
    ChangeFillColor(senseHatService);
    // Delay
    Task.Delay(msDelayTime).Wait();
  }
}

Deploying an App to a Device

To run the app in the RPi, you can download the .NET Core 3.0 SDK to your device, copy your code there, build the app, and finally execute it using the dotnet run .NET Core CLI command. Alternatively, you can publish the app using your development PC and then copy the binaries to the device. Here, I’ll go with the second option.

You first build the SenseHat.DotNetCore solution and then, in the solution folder, invoke the following command:

dotnet publish -r win-arm

The parameter -r win-arm can be omitted if project file contains the following property: <RuntimeIdentifier>win-arm</Runtime­Identifier>. You can use the command-line interface of your choice or the Package Manager Console from Visual Studio. If you’re using Visual Studio 2019, you can also publish the app using the UI tools. To do so, go to the Solution Explorer, right-click the ConsoleApp project, and choose Publish from the context menu. Visual Studio will display a dialog in which you choose Folder as a publish target. Then, under the publish profile settings, set Deployment Mode to Self-contained and Target Runtime to win-arm.

No matter which method you choose, the .NET Core SDK will prepare the binaries for deployment. By default, you’ll find them in the bin\$(Configuration)\netcoreapp3.0\win-arm\publish output folder. Copy this folder to your device. The most straightforward way to copy these files is to use Windows File Explorer (bit.ly/2WYtnrT). Open File Explorer and type into the address bar the IP address of your device preceded by a double backslash and followed by c$. My RPi has an IP of 192.168.0.109, so I typed \\192.168.0.109\c$. After a while, File Explorer will display a prompt, asking you for the Administrator’s password. Be patient, this can take several seconds. Finally, copy the binaries to your device.

To actually run the app, you can use PowerShell. The easiest way is to use the IoT Dashboard, shown in Figure 6. Simply right-click your device under the My devices tab and then choose PowerShell. You’ll need to type the Administrator’s password again when prompted. Then go to the folder with your binaries and execute the app by invoking the following:

 

.\SenseHat.DotNetCore.ConsoleApp N

Launching PowerShell Using the Windows 10 IoT Dashboard
Figure 6 Launching PowerShell Using the Windows 10 IoT Dashboard

You’ll see the actual sensor readings as shown in Figure 7, and the SenseHat LED array will change color. You can see that the temperature reported by the two sensors is almost the same. The humidity and pressure are also within the expected range. To further confirm that everything works correctly, let’s induce some changes. To do so, cover the device with your hand. As you’ll see, the humidity goes up. In my case, the humidity increased from about 37% to 51%.

Obtaining Sensor Readings Using the Console App Executed on the Raspberry Pi 2
Figure 7 Obtaining Sensor Readings Using the Console App Executed on the Raspberry Pi 2

Web API

With .NET Core I can go even further and expose the sensor readings through the Web API service. I’ll create a simple UI using Swagger UI (bit.ly/2IEnXXV). This UI will let the end user send HTTP requests to the IoT device as he would send them to a regular Web app! Let’s see how this works.

I started by extending the SenseHat.DotNetCore solution with another ASP.NET Core Web Application project, SenseHat.Dot­NetCore.WebApp, creating the project with the API template. Then I referenced the SenseHat.DotNetCore.Common project and installed the Swashbuckle NuGet package (Install-Package Swashbuckle.AspNetCore). Detailed instructions for setting up Swagger in ASP.NET Core Web Application are given at bit.ly/2BpFzWC, so I’ll omit all the details and only show the instructions necessary to set up the Swagger UI in my app. All these changes will be made in the Startup.cs file of the SenseHat.DotNetCore.WebApp project.

First, I implemented the read-only field openApiInfo:

private readonly OpenApiInfo openApiInfo = new OpenApiInfo
{
  Title = "Sense HAT API",
  Version = "v1"
};

Then I modified the ConfigureServices method by adding the following statements:

services.AddSwaggerGen(o =>
{
  o.SwaggerDoc(openApiInfo.Version, openApiInfo);
});

These statements are responsible for setting up the Swagger generator. Next, I registered the concrete implementation of the ISenseHatService interface for dependency injection:

var emulationMode = string.Equals(Configuration["Emulation"], "Y",
  StringComparison.OrdinalIgnoreCase);
var senseHatService = SenseHatServiceHelper.GetService(emulationMode);
services.AddSingleton(senseHatService);

Here, the concrete implementation of the SenseHat service is set depending on the Emulation setting from the configuration file. The Emulation key is set to N in appsettings.Development.json, and to Y in appsettings.json. Consequently, the Web app will use the emulator in the development environment and the true Sense HAT hardware in the production environment. As in any other ASP.NET Core Web app, the production environment is enabled by default for the Release build configuration.

The last modification is to extend the Configure method with the instructions for setting up the Swagger endpoint:

app.UseSwagger();
  app.UseSwaggerUI(o =>
  {
    o.SwaggerEndpoint($"/swagger/
      {openApiInfo.Version}/swagger.
      json",
        openApiInfo.Title);
  });

Then, I implemented the actual class for the Web API controller. In the Controllers folder I created the new file, SenseHat­Controller.cs, which I modified as shown in Figure 8. SenseHat­Controller has a public constructor, which is used for dependency injection to obtain an instance of the ISenseHatService. A reference to this instance is stored within the senseHatService field.

Figure 8 A Full Definition of the SenseHatController Web API Service

[Route("api/[controller]")]
[ApiController]
public class SenseHatController : ControllerBase
{
  private readonly ISenseHatService senseHatService;
  public SenseHatController(ISenseHatService senseHatService)
  {
    this.senseHatService = senseHatService;
  }
  [HttpGet]
  [ProducesResponseType(typeof(SensorReadings), (int)HttpStatusCode.OK)]
  public ActionResult<SensorReadings> Get()
  {
    return senseHatService.SensorReadings;
  }
  [HttpPost]
  [ProducesResponseType((int)HttpStatusCode.Accepted)]
  [ProducesResponseType((int)HttpStatusCode.BadRequest)]
  [ProducesResponseType((int)HttpStatusCode.InternalServerError)]
  public ActionResult SetColor(string colorName)
  {
    var color = Color.FromName(colorName);
    if (color.IsKnownColor)
    {
      senseHatService.Fill(color);
      return Accepted();
    }
    else
    {
      return BadRequest();
    }
  }
}

SenseHatController has two public methods, Get and SetColor. The first handles HTTP GET requests, and returns sensor readings from the Sense HAT add-on board. The second method, SetColor, handles HTTP POST requests. SetColor has one string argument, colorName. The client apps use this argument to choose the color, which is then utilized to uniformly change the LED array color.

I can now test the final version of the app. Again, I can do it using the emulator or true hardware. Let’s start with the development PC and run the app with the Debug build configuration. After executing the app, type localhost:5000/swagger in the browser address bar. Note that if you use the companion code and execute the app using the Kestrel Web server, the browser will open automatically, and you’ll be redirected to the swagger endpoint. I configured this using launchUrl of the launchSettings.json.

In the Swagger UI, you’ll see a page with the Sense HAT API header. Right below this header are two rows with GET and POST labels. If you click one of them, a more detailed view appears. This view allows you to send requests to the SenseHatController, see responses and investigate the API documentation (shown previously in the left side of Figure 1). To show the sensor readings, expand the GET row and then click the Try It Out and Execute buttons. The request is sent to the Web API controller and handled, and sensor readings will appear under the Response body (shown previously in the right side of Figure 1). You send POST requests in a similar way—you just need to set the colorName parameter before clicking the Execute button.

To test the app on my device, I published the app using the Release configuration and then deployed the resulting binaries to Raspberry Pi (as with the Console App). Then I had to open port 5000, which I did by invoking the following command from PowerShell on the RPi2:

netsh advfirewall firewall add rule name="ASP.NET Core Web Server port"
  dir=in action=allow protocol=TCP localport=5000

Finally, I executed the app from PowerShell using the command:

.\SenseHat.DotNetCore.WebApp.exe --urls https://*:5000

The additional command-line argument (urls) is used to change the default Web server endpoint from localhost to a local IP address (bit.ly/2VEUIji) so the Web server can be accessed from other devices within the network. Once this was done, I opened the browser on my development PC, typed 192.168.0.109:5000/swagger, and the Swagger UI appeared (of course, you’ll need to use the IP of your device). I then started sending HTTP requests to get sensor readings and change the LED array color, as shown previously in Figure 1 and Figure 2.

Wrapping Up

In this article I showed how to implement a cross-platform IoT app with .NET Core 3.0. The app runs on the Raspberry Pi 2/3 and interacts with components of the Sense HAT add-on board. Using the app, I obtained various sensor readings (temperature, humid­ity and atmospheric pressure) through the Iot.Device.Bindings library. Then I implemented the ASP.NET Core Web API service and created a simple UI with Swagger. Now anyone can access those sensor readings and control the device remotely with just a few mouse clicks. The code can run, without any changes on the other system, including Raspbian. This example shows how you—a .NET developer—can utilize your existing skills and codebase to program various IoT devices.


Dawid Borycki is a software engineer and biomedical researcher with extensive experience in Microsoft technologies. He has completed a broad range of challenging projects involving the development of software for device prototypes (mostly medical equipment), embedded device interfacing, and desktop and mobile programming. Borycki is an author of two Microsoft Press books: “Programming for Mixed Reality (2018)” and “Programming for the Internet of Things (2017).”

Thanks to the following Microsoft technical expert for reviewing this article: Krzysztof Wicher
Krzysztof Wicher is the author of Sense HAT device binding in the IoT repository and software engineer on .NET Core team (which includes .NET IoT team).


Discuss this article in the MSDN Magazine forum