This project is a fun application that controls a SnowPI, the GPIO Snowman. The application works on both a Raspberry PI running Windows 10 IoT and the full Windows 10 desktop- the key difference being you can't plug the SnowPI into your desktop PC!
One of the aims of the project is to make it as easy as possible to control the SnowPI using C#- this has been achieved by providing a simple plugin mechanism that utilises a Fluent API. This making controlling the SnowPI's LEDs a breeze:
return snowman.Setup()
.TurnLedOn(Led.LeftEye)
.TurnLedOn(Led.RightEye)
.PauseForSeconds(3)
Introduction- Brief InstructionsWhen the application starts, the SnowPI is rendered onto the screen and instructions are presented to the user. Once the user taps on the hat (using either mouse or touchscreen), the app loads a different "light show" per tap:
You can easily add new "light shows" yourself, in just a few lines of code.
HardwareWhilst strictly speaking you don't even need the hardware to run the app, it really defeats the purpose of the application to not have a SnowPI!
Follow the soldering guide to assemble your SnowPI. If you can run Raspbian, I recommend you run the Python test program to check everything is assembled correctly.
Once this is done, we have some rewiring to perform as 3 of the GPIO pins used by the SnowPI cannot be accessed when using Windows 10 IoT- see the PinMappings page for further info on this limitation.
Using female to male jumper wires, create the following PIN remappings:
|Original Pin|New Pin|
| 7| 16|
| 8| 12|
| 9| 5|
The rest of the pins can be mapped directly:
GPIO pins- 17, 18, 22, 23, 24, 25 (GPIO numbering).
Ground pin- 14 (physical pin number).
After the GPIO pin remapping, your SnowPI should look like this:
Remember to recheck the wiring and don't power the PI on until you are confident it is done correctly.
If you can run Raspbian, you can modify the Python test script, to check that the modified pin mappings work- just change the following lines (first 3 lines are the original ones commented out):
#led7 = LED(7)
#led8 = LED(8)
#led9 = LED(9)
led7 = LED(16)
led8 = LED(12)
led9 = LED(5)
Mapping the PCB LED numbersThe final exercise is to map the LED numbers on the PCB (as used in the code), to the GPIO pins used. This mapping is as follows:
|PCB LED No|GPIO Pin|LED Enum Value|Friendly Enum Value|
| 1| 16 |One |LeftTop |
| 2| 12 |Two |LeftMiddle |
| 3| 5 |Three |LeftBottom |
| 4| 22 |Four |RightBottom |
| 5| 18 |Five |RightMiddle |
| 6| 17 |Six |RightTop |
| 7| 25 |Seven |Nose |
| 8| 23 |Eight |LeftEye |
| 9| 24 |Nine |RightEye |
Code
Assuming you have everything setup to work correctly with Windows 10 IoT, you can just grab the code from GitHub and deploy it to your Pi.
If you haven't previously written any Windows 10 IoT apps, it's worth following along with the 'Hello, World!' Sample first. This will ensure you have all of the prerequisites in order.
Once you have the code running, it is very easy to add new light shows. In the project is a subfolder named LightShows. This folder contains an interface ILightShow:
The interface contains a property called Name, which is simply the name of the lightshow (this gets rendered above the Snowman when the lightshow is being played). There is a method called RunLightDisplay, which needs to be implemented to control the LEDs.
Let's imagine we wanted to create a light display that just uses the 3 LEDs on the SnowPIs head. The steps are as follows:
- Add a new class that implements ILightShow
- Write the code to control the LEDs
- Run the app
It is worth noting that code written using the fluent API is read from left to right and top bottom.
The code translates into these instructions:
- Wait for 5 seconds (this is of course non-blocking on the UI Thread).
- Turn on the LED on the nose
- Wait for 3 more seconds
- Turn on the LED on the left eye
- Wait for 2 more seconds
- Turn on the LED on the right eye
Currently the fluent API provides just the following methods:
- Turn an LED on
- Turn an LED off
- Pause for a given number of seconds
- Pause for a given number of milliseconds (1000 of a second)
You might be wondering how the application automagically adds the new LightShow that you've written. The answer can be found looking at the C# code for the App.xaml
Container
.RegisterTypes(AllClasses.FromApplication()
.Where(type => typeof (ILightShow).IsAssignableFrom(type)),
WithMappings.FromAllInterfaces,
WithName.TypeName,
WithLifetime.Transient);
This simply registers every class that implements the ILightShow interface, using a framework called Unity. You can read how this works in great detail using the Unity Documentation, but the details aren't really that important. Its only purpose in the project is to minimise plumbing code.
More Details- RenderingIn order to render the SnowPI to the screen, first I needed to work out how to draw it. Looking at the photo of the SnowPi below, it is quite straightforward to see the shapes involved:
The hat is clearly made up of a trapezoid combined with a rectangle, whilst the body and the head are elliptical, and the GPIO Header is also rectangular. In order to get the proportions roughly correct I took some rough measurements and then proceeded to write the XAML.
Noteworthy points:
- A Viewbox is used to ensure the control scales to fill all the available space it is given.
- The controls are arranged horizontally using a stackpanel.
- The Trapezoid is drawn using the Polygon class, by specifying its Vertex points.
- The shapes overlap each other by having negative margin values (eg the ellipse used for the face overlaps the hat slightly)
In early iterations, I just ran the app locally (deploying each change to the PI proved to be too time consuming). After getting the basic shapes done, the XAML rendered as below:
Not being one to reinvent the wheel, I searched online for a XAML LED control and found an excellent one written by Michele Cattafesta for WPF. Unfortunately this doesn't work with 10 IoT as it uses a RadialGradientBrush, which is not supported.
In the end I decided to create my own simplified LED control:
This reusable control exposes two self explanatory useful dependency properties:
- BulbColour
- IsActive
The control itself detects if it is active and automatically adjusts the opacity to make it appear as an LED that is either on or off. This is all achieved in XAML using the Visual State Manager.
Using the control is trivial- the XAML for the nose LED is as follows:
<controls:LedControl IsActive="{Binding SnowPi.LedSevenSet}" BulbColour="Orange" />
More Details- Prism/UnityIn order to cut down on boiler plate code required I decided to utilise Microsoft's Prism framework. These involved adding the following NuGet packages:
The features utilised included:
- Unity for dependency injection (See the App.xaml C# code for more details).
- BindableBase class for implementing INPC.
- ViewModelLocator to autowire ViewModels (MainPageViewModel) to Views (MainPage.xaml).
If you're unfamiliar with using Prism, I recommend you have a look at the AdventureWorks.Shopper example over on GitHub.
More Details- Fluent APIIn order to make the code as accessible to as large an audience as possible and inspired by the simplicity of GPIOZero, I decided to create a fluent API to make controlling the LEDs as simple as possible.
The logic for fluent interface is self contained in the class SnowPiLightShowRecording. Internally it uses a consumer/producer queue (PcQueue) to ensure that the main UI thread is never blocked. This is particularly important for the method "PauseForSeconds". If this method were to actually run on the UI thread, the app would become unresponsive.
To prevent the UI thread blocking commands are inserted into a queue, and a single thread dequeues these tasks (this ensures they are always in order). If necessary any action that needs to performed on the UI thread is marshalled back to the UI thread.
This queue also allows us to easily cancel the current task show by using a CancellationToken. This is important to allow the user to switch lightshows before the current running one has ended.
The fluent API could be improved quite a bit- for example the following methods would be really useful:
- TurnHeadLedsOn()
- TurnHeadLedsOff()
- Repeat(Times.Once())
- RepeatForever()
Why not try adding them yourself?
Comments