TL;DR
A few days ago I showed how the recent v3 release of the Uno Platform allowed you to run UWP apps on Linux. This was fantastic but really only half the story I wanted to tell. What I really wanted to do was see if I could get an app written in my favourite UI framework running on my favourite SBC; to wit, UWP on the the Raspberry Pi. In this post I show how, yet again, the Uno team have made this not only possible but startlingly easy and shockingly powerful.
Intro
In my last post "Running UWP on Linux With Uno", I used the Uno Platform to write a UWP app which could run on Linux under WSL2. This was a great proof-of-concept and showed that, despite only in preview, Uno's support for UWP under Linux was more than skin deep. However, running UWP in Linux on the desktop, while cool, wasn't my primary motivator here. No, what I really wanted to do was run a UWP app on a Raspberry Pi.
Now, those who have worked in the UWP space for a while probably know that you've been able to run UWP on a Pi for some time via Windows 10 IoT Core. Unfortunately Windows 10 IoT Core seems destined for the same fate as many cool Microsoft technologies, namely silicon heaven. The last release of Windows 10 IoT Core was back in 2018 and, despite a new and incredibly powerful Raspberry Pi coming to market, there are no signs of a compatible Windows 10 IoT Core release coming any time soon.
Furthermore, the choice to use Windows 10 IoT Core on a Raspberry Pi was a costly one. While you got to run a UWP app, you did so at the expense of huge swathes of other software, open-source libraries, educational material and community support which are available for the Raspberry Pi when running a Linux variant. Indeed, while .NET now enjoys excellent support for interfacing with electronic devices via the "dotnet/iot" library, when Windows 10 IoT Core was first released, just toggling a GPIO pin was a somewhat tricky proposition.
As such I run Raspberry Pi OS (formerly Raspbian) on almost all of the (embarrassingly large number of) Pi's I own. This has lead to my development on the Pi being mainly being targeted at console apps via .NET Core's support for Linux.
But no more...
UWP on Raspberry Pi OS
The Uno team have made compiling a UWP app for the Raspberry Pi almost embarrassingly easy. Assuming you have a Windows PC with .NET Core SDK v3.1 and the pre-release Uno Project Templates installed, and assuming you have a Raspberry Pi running 32-bit Raspberry Pi OS (and which has SSH & GTK correctly configured), then Uno's basic "Hello world" app can be run on the Pi by simply doing the following (note the change in prompt towards the bottom as we shift from executing commands on Windows to executing them remotely on the Pi):
PS> mkdir UnoHelloWorld
PS> cd UnoHelloWorld
PS> dotnet new unoapp
PS> cd UnoHelloWorld.Skia.Gtk
PS> dotnet build
PS> dotnet publish --runtime linux-arm -c Release --self-contained
PS> scp -rp bin\Release\netcoreapp3.1\linux-arm\publish pi@[RPI IP ADDRESS]:~/UnoHelloWorld
PS> ssh pi@[RPI IP ADDRESS]
pi@raspberrypi:~ $ cd UnoHelloWorld
pi@raspberrypi:~/UnoHelloWorld $ chmod +x UnoHelloWorld.Skia.Gtk
pi@raspberrypi:~/UnoHelloWorld $ export DISPLAY=:0
pi@raspberrypi:~/UnoHelloWorld $ ./UnoHelloWorld.Skia.Gtk
If everything was setup correctly, you should see something like this on the Raspberry Pi screen:
A UWP app, running under Raspberry Pi OS on a Raspberry Pi 3B+. As I said, almost embarrassingly easy!
Performance
So, after showing that we could run a UWP app on the Pi, I was interested to compare the performance of an app running on the Pi with one running on my PC. I then remembered that during UnoConf there had been a discussion of Dopes Bench. Unfortunately the code in this repo didn't (at the time of writing) contain Skia backend projects so, following the process above, I quickly knocked up a new Uno project and simply copied the "MainPage.*" and "Random2.cs" files from the Dopes Bench project into it (cue amazement that exactly the same code runs on Windows, Mac, Android, iOS, Web and, now, Linux).
I then compiled and ran the test on my PC (DopeTestUno.UWP / release build) and the Raspberry Pi 3B+ (DopeTestUno.Skia.GTK / release build). Here are the results:
9217.42 Dopes/s | 401.05 Dopes/s |
Dell Precision T7910 6 Core (12 Thread) Xeon E5-2620v3 @ 2.4GHz 32 Gb RAM NVidia GeForce GTX 980 |
Raspberry Pi 3B+ 4 Core BCM2837B0 A53 (ARMv8) 64-bit @ 1.4GHz 1Gb RAM Broadcom Videocore-IV |
Well, given the difference in spec between the PC and the Pi, it's not surprising that there's a large difference in "Dopes" but is 401.05 dopes good or bad? Furthermore what does this mean for real world performance of an app?
No idea, guess we're going to have to build a "real world" app...
Unopify ("Uno-Pi-fy"):
Unopify is a UWP Spotify client written using the fantastic SpotifyApi.NetCore library along with the usual compliment of supporting libraries including Microsoft.Extensions.DependencyInjection, System.Net.Http, System.Reactive and, of course, my faithful MVx.Observable.
While only a proof-of-concept which took just a few hours to write, it already demonstrates a significant amount of functionality such as:
- Frictionless support for .NET Standard 2.0 libraries
- Functional, Reactive, MVVM
- Navigation (the app moves from an "Authenticating" view to a "Home" view)
- Visual States & Visual State Triggers
- Full layout capabilities (uses auto, proportional and explicit sizing of elements)
- Image fetching, display and scaling (the image URI's retrieved from Spotify web calls are directly bound to each Image's
Source
property) - Opacity (a semi-transparent white rectangle is laid over the background image)
- Lookless controls (via the previous, play, next buttons)
- Command binding and dispatch (via the previous, play, next buttons)
- XAML drawing primitives (via
Ellipse
andPath
elements in the previous, play and next buttons) - ... and loads more
In fact, about the only thing I wasn't able to get working was the web-based OAuth2 authentication flow. This wasn't particularly surprising given that this flow needs to invoke and interact with a system browser so I simply worked around this (temporary) limitation by using another UWP app to do the authentication and shared the access token with Unopify via SignalR (there were probably better ways to do this but I had the SignalR code to hand).
There were a couple of minor issues - UIElement.Opacity
doesn't seem to work and an inline control template for the button seemed to cause the button to disappear - but nothing that couldn't be easily worked around. In short, writing a UWP app that worked on the Raspberry Pi under Linux was no more difficult than writing a UWP app that runs on a phone under Android or iOS (which itself is a minor miracle!).
Below you can see a video of Unopify running on a Raspberry Pi 3B+. In it I'm using Spotify Web Player on the PC to control a Spotify Connect amp and running Unopify on the Pi you can just see below the TV. At start-up, Unopify requests an authentication token from SignalR then starts polling the Spotify Web API for player state and using the responses to update the UI. Finally, the previous/next/play/pause buttons within Unopify directly call the Spotify Web API which causes the amp to play, pause or change track accordingly.
(Apologies for the poor quality but a direct screen capture wasn't an option as I wanted to include audio from the amp.)
As you can see, while start-up was a little slow (exacerbated by there currently being no splash-screen) the running app is completely usable. Furthermore, while the app is very raw (as I said, it only took a few hours) many of the rough edges (i.e. the delay between showing track name and album image and the play/pause button glitch caused by the 1 second polling interval) could easily be smoothed with a few simple changes.
To put this in perspective, this is a preview build of a UWP app running on a two year old Raspberry Pi which has 1Gb of RAM, an 80Mb/s capable SD card for a hard drive and costs just £35!
I have a(nother!) 4Gb Raspberry Pi 4B+ on order and will update this post once with performance metrics and "real world" experience once it arrives.
Finally, the code for Unopify can be found on Github. Should you wish to run it, you will need to deploy the Unopify.AuthRelay
service (for which a free-tier AppService on Azure works well) and implement partial methods on the "Secrets.cs" files in a couple of projects (appropriate exceptions will be thrown if you fail to do this).
Conclusion
Uno Platform have once again significantly expanded the vista for UWP (definitely no pun intended) and left me almost dizzy with new possibilities. By supporting Linux on low-power devices, the Uno team has propelled UWP beyond desktop, mobile and web applications into the realm of appliances. Want UWP on your fridge? Sure! Watch? No problem. A graphical, touch-driven interface for your thermostat? You got it.
UWP is now a truly Universal Platform and your "write-once" code really can "run anywhere".
From a commercial perspective, the recent deluge of single-board computers and their rapidly advancing capabilities provides this technology with immense value. Leveraging the power of UWP and the .NET ecosystem on everything from embedded devices to mobile phones allows businesses to benefit from the incredible cost savings and RoI value proposition of a "one stack" approach. With little to no training, your .NET developers are now able to deliver on the promise of ambient computing, efficiently supporting every use-case on every device "from edge to cloud".
Wow.
Finally
If you're interested in using the Uno Platform to deliver cross-platform apps or have an upcoming project for which you'd like evaluate Uno Platform's fit, then please feel free to drop me a line using any of the links below or from my about page. As a freelance software developer and remote contractor I'm always interested in hearing from potential new clients or ideas for new collaborations.