Building Blazor ''Hello, Blinky'' IoT Application
Build your Blazor IoT app in the blink of an eye!
Join the DZone community and get the full member experience.
Join For FreeI thought my first ASP.NET Core edition of Hello, Blinky would be my last, at least for a long time. But then something reminded me of Blazor, and I thought why not build a Blazor edition of Hello, Blinky for Windows IoT Core and Raspberry Pi? After some hacking, I made it work. Here's my Hello, Blinky for Blazor.
Who the Heck Is Hello, Blinky?
Hello, Blinky is sort of like the "Hello, World" from Raspberry Pi and other microboards. While it's possible to do a "Hello, World" with these boards, why not do something more interesting with connected electronics? Isn't that what those boards were made for anyway?
Hello, Blinky is a blinking LED lamp. It's one of the simplest things you can do on a Raspberry Pi. There are many hands-on examples of Hello, Blinky on the web using different languages, tools, and utilities. This one here is for Blazor.
Wiring
To use an LED lamp with your Raspberry Pi, we need to buy one, of course. We also need a breadboard, wires, and a resistor. Just visit your local electronics shop and ask them for these components. They know what to sell you.
Wiring is actually easy. Just connect the LED, resistor, and wires as shown in the following image.
NB! I'm using Raspberry Pi with Windows 10 IoT Core (there's a free version available). This code should actually work also on Linux but I have not tried it out, and I have no idea if publishing works the same way for Blazor applications we want to host on Linux.
With the wiring done, it's time to start coding.
Blazor Solution
I decided to go with the client-side Blazor application that is supported by the server-side web application.
Server-side Blazor handles events in the server. So all UI events are handled in server. In this case, the UI and server communicate using web sockets and SignalR.
This is something I want to avoid. The Raspberry Pi is a small board with few resources — at least the one I have is not very powerful. I don't want to put the UI workload there, as the browser has way more resources to use.
With this in mind, I created a Visual Studio solution, which is shown in the image on right. BlazorHelloBlinky.Client is a client-side Blazor application, and BlazorHelloBlinky.Server is a web application that runs on Raspberry Pi.
Blinking LED From ASP.NET Core Controller
Before anything else, we need a class to blink the LED lamp. There's no programming concept for blinking. No command like make-led-lamp-blink()
. So we will have to write it ourselves.
Here is what the blinking cycle means for our computer:
- Sends a signal to GPIO pin
- Wait for one second
- Cut signal off
- Wait one second
- Proceed to step 1
The problem is we cannot blink LED with just one command. We need something that is going through this cycle until it is stopped. For this, I wrote the LedBlinkClient
class. It hosts tasks with an endless loop, but inside the loop, it checks if it's time to stop.
Here is the LedBlinkClient
class for a server-side web application (it needs System.Device.Gpio Nuget package to work).
public class LedBlinkClient : IDisposable
{
private const int LedPin = 17;
private const int LightTimeInMilliseconds = 1000;
private const int DimTimeInMilliseconds = 200;
private bool disposedValue = false;
private object _locker = new object();
private bool _isBlinking = false;
private Task _blinkTask;
private CancellationTokenSource _tokenSource;
private CancellationToken _token;
public void StartBlinking()
{
if (_blinkTask != null)
{
return;
}
lock (_locker)
{
if (_blinkTask != null)
{
return;
}
_tokenSource = new CancellationTokenSource();
_token = _tokenSource.Token;
_blinkTask = new Task(() =>
{
using (var controller = new GpioController())
{
controller.OpenPin(LedPin, PinMode.Output);
_isBlinking = true;
while (true)
{
if (_token.IsCancellationRequested)
{
break;
}
controller.Write(LedPin, PinValue.High);
Thread.Sleep(LightTimeInMilliseconds);
controller.Write(LedPin, PinValue.Low);
Thread.Sleep(DimTimeInMilliseconds);
}
_isBlinking = false;
}
});
_blinkTask.Start();
}
}
public void StopBlinking()
{
if (_blinkTask == null)
{
return;
}
lock (_locker)
{
if (_blinkTask == null)
{
return;
}
_tokenSource.Cancel();
_blinkTask.Wait();
_isBlinking = false;
_tokenSource.Dispose();
_blinkTask.Dispose();
_tokenSource = null;
_blinkTask = null;
}
}
public bool IsBlinking
{
get { return _isBlinking; }
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
StopBlinking();
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
}
}
The server-side application also needs a controller so that Blazor can control blinking. Here is the simple controller I created.
[Route("api/[controller]")]
public class BlinkyController
{
private readonly LedBlinkClient _blinkClient;
public BlinkyController(LedBlinkClient blinkClient)
{
_blinkClient = blinkClient;
}
[HttpGet("[action]")]
public bool IsBlinking()
{
return _blinkClient.IsBlinking;
}
[HttpGet("[action]")]
public void StartBlinking()
{
_blinkClient.StartBlinking();
}
[HttpGet("[action]")]
public void StopBlinking()
{
_blinkClient.StopBlinking();
}
}
Controller actions
Controller actions are just public HTTP-based end-points for the LED client class.
Of course, LedBlinkClient
must be registered in the Startup
class as we want to get it through the constructor of the controller and dependency injection.
services.AddSingleton<LedBlinkClient>();
I registered it as a singleton as I don't want multiple instances of LED client to be created.
Client-Side Blazor Application
The client-side application, in my case, contains only the Index page and its code-behind class (Blazor supports code-behind files, name it as PageName.razor.cs). Here is the mark-up for Index page.
@page "/"
@inherits IndexPage
<h1>Hello, blinky!</h1>
<p>Led is @BlinkyStatus</p>
<div>
<button class="btn btn-success" @onclick="@StartBlinking">Start blinking</button> |
<button class="btn btn-danger" @onclick="@StopBlinking">Stop blinking</button>
</div>
Here is the class for Index page. I keep it as a code-behind file so my Blazor page doesn't contain any logic.
public class IndexPage : ComponentBase
{
[Inject]
public HttpClient HttpClient { get; set; }
[Inject]
public IJSRuntime JsRuntime { get; set; }
public string BlinkyStatus;
protected override async Task OnInitializedAsync()
{
var thisRef = DotNetObjectReference.Create(this);
await JsRuntime.InvokeVoidAsync("blinkyFunctions.startBlinky", thisRef);
}
protected async Task StartBlinking()
{
await HttpClient.GetStringAsync("/api/Blinky/StartBlinking");
}
protected async Task StopBlinking()
{
await HttpClient.GetStringAsync("/api/Blinky/StopBlinking");
}
[JSInvokable]
public async Task UpdateStatus()
{
var isBlinkingValue = await HttpClient.GetStringAsync("/api/Blinky/IsBlinking");
if (string.IsNullOrEmpty(isBlinkingValue))
{
BlinkyStatus = "in unknown status";
}
else
{
bool.TryParse(isBlinkingValue, out var isBlinking);
BlinkyStatus = isBlinking ? "blinking" : "not blinking";
}
StateHasChanged();
}
}
One important thing to notice is the OnInitializedAsync()
method. This method is called when the page is opened. It creates an Index page reference for JavaScript and starts a JavaScript timer to update the blinky status periodically.
Updating the Blinking Status Automatically
I wasn't able to get any C# timer to work in my browser so I went with JavaScript interop. The good old setInterval()
with some Blazor tricks made things work. The trick I did is illustrated in the following image.
When the page is loaded, I use JavaScript interop to send the page reference to JavaScript. The reference is saved after the timer is created using the setInterval()
method. After every five seconds, the timer callback is fired and the method read LED state is called. Yes, this method is defined in Blazor form and is called from JavaScript with no hacks.
Add a JavaScript file to the wwwroot folder of the client-side Blazor application and include it in the index.html file in the same folder. Here's the content of the JavaScript file:
window.blinkyFunctions = {
blazorForm: null,
startBlinky: function (formInstance) {
window.blinkyFunctions.blazorForm = formInstance;
setInterval(window.blinkyFunctions.updateStatus, 5000);
},
updateStatus: function () {
if (window.blinkyFunctions.blazorForm == null) {
return;
}
window.blinkyFunctions.blazorForm.invokeMethodAsync('UpdateStatus');
}
}
startBlinky()
is the method called from the Blazor page. The updateSatus()
method is called by the timer after every five seconds. In the timer callback method, there's a invokeMethodAsync()
call. This is how we can invoke methods of Blazor objects in JavaScript.
Why not updating status in JavaScript? Well, because this post is about Blazor, and as much as I want to build using Blazor, this is also a good temporary solution for Balzor applications that needs a timer.
Publishing Blazor Application to Raspberry Pi
Publishing is currently challenging as client-side Blazor is still under construction and all supportive tooling is not ready yet. There are a few tricks I had to figure out the hard way, and to save your time, I will describe here what I did.
The first thing is about assemblies. If there is version downgrade, then it's handled as an error. For some people, it worked when they added Nuget packages with downgraded versions to their solution, but it didn't work out for me. We can ignore warning NU1605 on the project level by modifying the project file for the web application.
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<LangVersion>7.3</LangVersion>
<OutputType>Exe</OutputType>
<NoWarn>$(NoWarn);NU1605</NoWarn>
</PropertyGroup>
The NoWarn
tag tells your tools that this warning must not be handled as an error.
On my Raspberry Pi, I want to use port 5000 for my blinky application. To make it happen with minimal effort, I added port binding to the Program.cs file of the web application.
Now it's time to build the whole solution and then focus on real publishing.
I know, I know, the last and most annoying steps to do before LED starts blinking... It's also called fun of playing with non-stable technology. So, here's the dirtiest part of the publishing process:
- Publish client-side Blazor application using Visual Studio. Leave all settings like they are as everything should work out well with defaults
- Publish server-side Blazor application on command-line using the following command: dotnet publish -c Release -r win10-arm /p:PublishSingleFile=true
- Open C:\ drive of your Raspberry and create folder Blinky
- Copy files from BlazorHelloBlinky.Server\bin\Release\netcoreapp3.0\win10-arm\ to Blinky folder on Raspberry Pi.
- Open file BlazorHelloBlinky.Client.blazor.config and make it look like here: c:\Blinky\BlazorHelloBlinky.Client.csproj
BlazorHelloBlinky.Client.dll - Copy wwwroot folder of client-side Blazor app to Blinky folder on Raspberry Pi
- Copy dist folder from folder where client-size Blazor application was published to Blinky folder on Raspberry Pi
- Connect to your Raspberry Pi using PowerShell
- Move to Blinky folder and run BlazorHelloBlinky.Server.exe
- Open the browser on your coding box and navigate to http://minwinpc:5000/ (use the name or IP of your Raspberry Pi)
If there were no problems and all required files got to the Raspberry Pi, then Hello, Blinky should be ready for action!
Further Reading
Published at DZone with permission of Gunnar Peipman, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments