A simple but realistic Raspberry Pi .NET solution that demonstrates project layout, unit testing and best practices. Development is on a machine capable of running Visual Studio Code, which is connected remotely via SSH to a Raspberry Pi.
Introduction
Applications can be written in .NET for the Raspberry Pi just as they can for any other environment. The advantages of well written .NET (and C#) code is that much more scalable and maintainable solutions are possible. In this article, I will describe a project structure that can be used to create such solutions. This will include unit testing, mocking and dependency injection. The application will be a simple one, but the solution structure will enable two key aims of any large development project:
- The code should attempt to achieve separation of concerns.
- The code should lend itself to refactoring.
Both of these principles will be demonstrated later in the article. I will be using the .NET Core IoT Libraries which is Microsoft's official solution to interfacing with low level devices on platforms such as the Raspberry Pi.
Background
Part 1: Easy Set Up of .NET Core on Raspberry Pi and Remote Debugging with VS Code
In a previous article, I described how to setup a development environment that would make it easy to develop for a remote Raspberry Pi using automated deployment and debugging. That article included a couple of simple programs. This article will build on that article and show how a more realistic, larger solution could be created.
Problem Description
I have chosen a simple problem for this example solution. The requirements of this are very made up, but this is intentional. The solution structure is the important part and by having a simple problem, this hopefully will enable the focus to be on the solution.
So the problem: There will two push button switches and two LEDs. We will be creating a console application that will enable each LED to be turned on or off. The buttons will recognise short of long presses. We will also have two test modes - one to blink the LEDs continually until button 2 is long pressed, and another one to display messages saying which button was pressed, which will also be terminated by long pressing button 2.
I have connected the switches and LEDs to the following GPIO pins:
Obviously, this is a contrived example. A more realistic solution would do something with the inputs and outputs and would likely include more sophisticated devices. At the time of writing, the IOT project included over 70 interfaces to devices such as temperature, accelerometer, and light sensors as well as lots of wired and wireless devices.
Solution Structure
The top level structure looks like this:
> .vscode
> Doc
> RpiBlinkButtonApp
> RpiBlinkButtonLib
> RpiBlinkButtonLibTests
> Scripts
.gitignore
There are three code folders, RpiBlinkButtonApp, RpiBlinkButtonLib and RpiBlinkButtonLibTests. In a larger solution, there are likely to be more library projects, and each library should have its own Tests
project. There may be more Application
projects. Also, note that I have not included unit tests for the App
project, but for a real solution, there should be unit tests for this project as well.
The .vscode and Scripts folders are based on the previous article, and include scripts and configuration to enable remote deployment and debugging.
Separation of Concerns
One of the key objectives of the approach in this article is separation of concerns. This is important in a larger solutions where it is often not possible or desirable to have to comprehend the entire solution in order to work on it. To achieve this, there needs to be clear abstraction layers. An obvious first abstraction layer is the actual iot library itself. It is worthwhile looking at the code in that repository, but it shouldn't be essential to understand all of it in order to use it.
Our first abstraction is to have a library project. Within that project, I have chosen to create two controller classes. The LED controller class encapsulates both LEDs - I have chosen to have a red and green LED. There are two methods to turn either the red or Green LED on or off. The button controller has two buttons, and as these are slightly more complicated (requiring debouncing of the switches for example), the controller uses another class, GpioButton
. Using the button controller should be easy however, and I have decided to provide the event ButtonPressed
which has an EventArgs
argument that describes what sort of event happened (button 1 or 2 short or long pressed).
Refactoring
The code should lend itself to refactoring. Unlike in the old days when a design was set in stone at the beginning of the project, modern code should be continuously refactored as the project progresses. IDEs including VS Code provide tools to make this easier, but we also need to make sure that the complexity of the code doesn't get out of hand, because once this has happened, it gets difficult to refactor, and in some case becomes impossible.
Another key part of refactoring is being confident that the refactoring has not broken anything. This is one of the key reasons for having good unit tests.
Unit Testing, Mocking and Dependency Injection
Each class in the library has its own test class. Ideally, we only want to test the class and stub out calls to other classes. The best way to do this is by mocking the called classes. I have used Moq for this, which can be easily installed via NuGet. A key part of mocking is using dependency injection and inversion of control. They key idea here is that we pass instantiated objects to our classes rather than the class creating the objects itself. By doing this, we are able to pass mocked objects rather than real objects. An example of this is CreateMockedObjects()
in the ButtonControllerTests
:
private MockButtonCollection CreateMockObjects()
{
var mockObjects = new MockButtonCollection();
mockObjects.Button1 = new Mock<GpioButton>(null);
mockObjects.Button2 = new Mock<GpioButton>(null);
mockObjects.GpioDriver = new Mock<GpioDriver>();
mockObjects.GpioDriver.Protected().Setup<bool>("IsPinModeSupported", ItExpr.IsAny<int>(),
ItExpr.IsAny<PinMode>()).Returns(true);
mockObjects.GpioController = new GpioController(PinNumberingScheme.Logical,
mockObjects.GpioDriver.Object);
mockObjects.ButtonController = new ButtonController(mockObjects.GpioController,
mockObjects.Button1.Object, mockObjects.Button2.Object);
mockObjects.ButtonController.Initialize();
return mockObjects;
}
Another example is the DateTimeProvider
class which we pass to the GpioButton
class. This can be mocked as follows:
var mockDateTime = new Mock<IDateTimeProvider>();
var dateNow = DateTime.UtcNow;
mockDateTime.SetupSequence(m => m.UtcNow)
.Returns(dateNow)
.Returns(dateNow.AddMilliseconds(20))
.Returns(dateNow.AddMilliseconds(30));
This allows us to return specific values, in this case, the first call to UtcNow()
gets the current time, subsequent calls get the time +20ms and then +30ms. We then have control of code like this in the GpioButton
:
else if ((_dateTime.UtcNow - PressedStart).TotalMilliseconds >= LONG_PRESS_DURATION)
{
longReleaseAction();
}
One of the problems with dependency injection is using third party classes that aren't mockable. A good example of this is the GpioController
. In order for a class to be mockable, it needs to either be derived from an interface (for example IList
) or it needs to have methods that are virtual. The GpioController
doesn't have either of these, so we can't create a mock GpioController
. Fortunately, the GpioController
constructor does take a GpioDriver
as a parameter. This allows us to create a mock GpioDriver
. We can then make calls to the GpioController
that will call our mock GpioDriver
. A simple example of this is the LedControllerTests
where we can check the SetLed()
method actually writes to the correct gpio pin
:
public void SetLed_ShouldCall_DriverWriteMethod(string method, int pin, bool on)
{
var mockDriver = new Mock<GpioDriver>();
mockDriver.Protected().Setup<bool>("IsPinModeSupported", ItExpr.IsAny<int>(),
ItExpr.IsAny<PinMode>()).Returns(true);
mockDriver.Protected().Setup<PinMode>("GetPinMode", ItExpr.IsAny<int>())
.Returns(PinMode.Output);
var gpioController = new GpioController(PinNumberingScheme.Logical, mockDriver.Object);
var ledController = new LedController(gpioController);
ledController.Initialize();
typeof(LedController).GetMethod(method).Invoke(ledController, new object[] { on });
mockDriver.Protected().Verify("Write", Times.Once(), ItExpr.Is<int>(p => p == pin),
ItExpr.Is<PinValue>(m => m == (on ? PinValue.High : PinValue.Low)));
}
Best practice is to have Setup()
methods that accept any input (using IsAny
) and Verify()
methods that check for specific parameters (using Is
).
Running the Code
There are two steps to running or debugging the program. Assuming that the Raspberry Pi is setup as per Part 1 of these articles, there are two steps required. These steps are:
- Set Raspberry Pi name (via Ctrl Shift P)
- Run, either from the menu or the activity bar which is usually on the left hand side
Points of Interest
Working on the Raspberry Pi C# code is just as easy as working on any other C# code. We have all the tools available to us like refactoring, intellisense, etc. that make writing Raspberry Pi applications much easier than if we tried to do it on the Raspberry Pi itself.
I haven't covered it here, but obviously, we can include features that we would include in any other .NET application, such as entity framework, web apps with swagger support, etc.
Developing in .NET for the Raspberry Pi is different from developing in Python, JavaScript and C++ (which should all be possible in VS Code with the Raspberry Pi setup described here) in that with .NET, the C# code is on the development machine, whereas for the other languages, the source is (usually) on the target machine. I guess it doesn't matter either way (although you may have your own preference), but understanding this makes it clearer what is going on.
Theoretically, we can run the code locally on our development machine (whether that is Linux, MacOS or Windows). However, the gpio calls won't work (if you try it, you'll find they throw a "not supported" exception). But it should be possible to create a simulator that a GpioDriver
communicates with. That is beyond the scope of this article though!
History
- 25th May, 2020: Initial release
Jon is a Software engineer with over 30 years of experience, the last 18 of which have been using C# and ASP.NET. Previously he has used C++ and MFC. He has a degree in Electronic Systems Engineering and is also a fully licensed radio amateur (M0TWM).