Smart Shades with Windows IoT 1

I wanted to be able to have my office shades go up or down automatically, depending on the amount of light outside. Then I thought, why not take it to the next level with Windows IoT?  I could design a UI with the help of the recently open-sourced Telerik UI for UWP and also leverage a FEZ HAT’s & Raspberry Pi 3 to drive the motors, detect light levels and temperature.

Why would I want to also measure temperature? Because if it were too hot inside, I wouldn’t want to open the shades even if it were light outside.

I’ll go into detail about the software and hardware, but before I do: here’s the GitHub repo with the source code and lets watch a short video of the UI animation and motor in action.

About the Hardware

The hardware you see is a Raspberry Pi 3 (can also be a Pi 2), a FEZ HAT, and a 90 degree motor (like this one). That motor is connected to the FEZ HAT’s Motor A connection, however I also have a second smaller 1:150 motor connected to Motor B (I am still testing out torque of different motors to see which is small and powerful enough to raise a shade).

The FEZ HAT has a great built-in motor driver so that you only need to connect a more powerful external power source to drive the motors. This is the two black wires you see connected to the FEZ HAT on the left.

2017-02-14_1600.png

About the Software

I built the app to be scalable, so there’s a ShadeItemViewModel that contains all the logic to control each shade independently of each other, but still use the same FEZ HAT.

The UI you see is DashboardPage.xaml, it has a GridView at the top with an item for each ShadeItemViewModel. Under the GridView is the light and temperature measurements displayed in a Telerik RadGauge control with LinearGaugeIndicators.

Each item in the GridView has command buttons to open and close the shade, but also a custom control I made that indicates the current position of the shade.

2017-02-14_16-12-43.jpg

 

In the ShadeItemViewModel there are several properties to help tweak the motor speed and for how long to run the motor. I considered using a servo for a while, but I’d need to build a very high ration gear set to turn the degrees of the servo rotations of the shade. It’s easier and more scalable to use time.

This way anyone can change the time and speed of the motor from the settings button to fit their shade’s length. Also, the way the “% complete” algorithm works, it will adapt to the current settings and give proper percentages and it opens or closes.

I’m still experimenting with the motors. I’ll be 3D printing up some housings, and likely gears, to connect the motor to the shade. Once that’s complete, I’ll be writing part 2 of this series and give you all an update with any additional STL files so you can print up the parts for the motors.

Until then, enjoy running/exploring the code and trying it out for yourself!

 

Manipulation is easier than you think

You know that bottom drawer on the Windows 10 navigation app where the upcoming turns are in a list? You know how you can drag it up or down to show more or less of the content? Want to learn how to create it? You’re in luck because that’s what today’s topic is!

Here’s the result of what you’ll be able to do:
navdrawer

 

The Approach

Let’s get started. Since there are no built-in controls that do this, we’ll create a simple layout (links to source code at the end of the article).

There are the three major sections to the layout:

  1. A root container for the main content – This can just be a Grid that fills the page, nothing special here. In my demo I use an Image control with a picture of a map to keep the concept simple.
  2. A handle that the user will drag up and down  – We’ll do that with a thin Grid that contains something so the user knows it can be manipulated (I used a horizontal ellipsis). The responsibility of this Grid is that we need to hook into the ManipulationStarted, ManiuplationDelta and ManipulationEnded events.
  3. A drawer container for the content that will be moved –  This is also just another Grid, but it will be on top of the main content Grid. This Grid should have two Rows, one for the “handle” and one for the ListView that holds the example navigation route steps.

There are two main ways to approach moving the drawer container:

  1. We can translate (move) the entire drawer from off-screen to on-screen
  2. We can increase the height of the drawer container

Since we want the area inside the drawer to become larger or smaller, we need to use option #2. Changing the height comes with a cost, the content inside will be forced to do layout passes when the height is changed.

However, this is in fact what we want because if we did a translate, then a portion of the container would be off the screen and the content can’t be reached (e.g. the ListView wouldn’t be able to show the last item because it will be offscreen).

If you don’t need to cause layout changes and only want to move it off-screen, take a look at the tutorial here where it shows you how to use a TranslateTransform.

The XAML

Okay, lets get started with the XAML. Here’s the page layout. You’ll see that the “HandleGrid” has manipulation events defined


<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid x:Name="MainContentGrid">
            <Image Source="Assets/FakeMap.png" VerticalAlignment="Top" />
        </Grid>

        <Grid x:Name="DrawerContentGrid" VerticalAlignment="Bottom" Background="{ThemeResource AppBarBackgroundThemeBrush}" RenderTransformOrigin="0.5,0.5">

            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition />
            </Grid.RowDefinitions>

            <Grid x:Name="HandleGrid" ManipulationStarted="HandleGrid_OnManipulationStarted" ManipulationDelta="HandleGrid_OnManipulationDelta" ManipulationCompleted="HandleGrid_OnManipulationCompleted" ManipulationMode="TranslateY" Height="15" Background="{ThemeResource AppBarBorderThemeBrush}" BorderThickness="0,1,0,1" BorderBrush="{ThemeResource AppBarToggleButtonCheckedDisabledBackgroundThemeBrush}">
                <SymbolIcon Symbol="More" />
            </Grid>

            <Grid x:Name="DrawerContent" Grid.Row="1">
                <ListView x:Name="RouteSteps" ItemsSource="{Binding RouteSteps}">
                    <ListView.ItemTemplate>
                        <DataTemplate>
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="Auto" />
                                    <ColumnDefinition />
                                </Grid.ColumnDefinitions>

                                <Viewbox Width="48" Height="48">
                                    <Canvas Width="24" Height="24">
                                        <Path Data="{Binding Icon}" Fill="Black" />
                                    </Canvas>
                                </Viewbox>

                                <TextBlock Text="{Binding Summary}" TextWrapping="Wrap" Margin="10,0,0,0" Grid.Column="1" VerticalAlignment="Center" />
                            </Grid>
                        </DataTemplate>
                    </ListView.ItemTemplate>
                </ListView>
            </Grid>
        </Grid>
    </Grid>

 

The C#

Now, let’s take a look at the event handlers for the events. In the ManipulationStarted and ManipulationEnded event handlers I’m only changing the background brush to the accent color (and back). This lets the user know they’re in contact with the handle and that it can be moved.


private void HandleGrid_OnManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e)
{
    var themeBrush = Application.Current.Resources["AppBarToggleButtonBackgroundCheckedPointerOver"] as SolidColorBrush;

    if (themeBrush != null) HandleGrid.Background = themeBrush;
}

private void HandleGrid_OnManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
{
    var themeBrush = Application.Current.Resources["AppBarBorderThemeBrush"] as SolidColorBrush;

    if (themeBrush != null) HandleGrid.Background = themeBrush;
}

 

The actual manipulation of the drawer’s height happens in the ManipulationDelta handler, we take the current height and add it to the Y delta (the distance the user moved in the Y direction), then set the Grid’s height with that sum.


private void HandleGrid_OnManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{
    DrawerContentGrid.Height = DrawerContentGrid.ActualHeight + -e.Delta.Translation.Y;
}

 

As I mentioned earlier, changing the height of the container means that the bottom edge will always be visible, thus allowing the user to scroll to the last item in the ListView. Just keep in mind that layout passes can be expensive depending on how much content you have in there.

That’s all there is to it, now go add some gestures to your app (and don’t forget to make them discoverable)!

 

Source Code

Here are the three relevant files to the demo:

Surface Dial and Real-Time Video Effects

I was given a Surface Dial the other day and I thought “what can I do with this to create a better experience for the user”. One thing came right to mind, applying real-time video effects.

I have a UWP app in the Windows Store, Video Diary, where you can apply real-time video effects while recording a video. One of the features of these effects is to increase or decrease the video effect’s properties. For example, the intensity of a Vignette effect, here’s what I want:

2016-11-10_19-39-54

So I whipped out the RadialController API documentation and dug in. It turns out to be extremely simple, here is the result:

 

Let’s take a look at the code.

Note: Going into the specifics of applying real time video effects is out of scope for this article. You can see the source code to this demo app here  to see how it’s done, or  you can see my DynamicBlur Video Effect contribution to the official Win2D Demo app.

Since I didn’t want to go too crazy with the Surface Dial for my first demo, I thought about how the controller can be interacted with; turning the dial and clicking down on the dial. So I thought, why not use the menu to select a video effect and the rotation to change the effect’s intensity. Let’s get started.

First, when the page loads, I need to get a handle to the RadialController:

 

dialController = RadialController.CreateForCurrentView();

 

Next, I want to hook into the event that fires when the dial is turned and set the rotation resolution:

dialController.RotationResolutionInDegrees = 1;
dialController.RotationChanged += DialControllerRotationChanged;

 

Now, I want to make some room before adding my custom menu items, so I grab a handle to the RadialControllerConfiguration and assign it just one default menu item:

var config = RadialControllerConfiguration.GetForCurrentView();
config.SetDefaultMenuItems(new[] { RadialControllerSystemMenuItemKind.Scroll });

 

I need to add some menu items to the circular menu that appears when the dial is clicked. For this I just iterated over the list of effects I added and create a RadialControllerMenuItem for each one and hook into it’s Invoked event:

foreach (var effect in PageViewModel.VideoEffects)
{
    // Create a menu item, using the effect's name and thumbnail
    var menuItem = RadialControllerMenuItem.CreateFromIcon(effect.DisplayName,
 RandomAccessStreamReference.CreateFromUri(new Uri(effect.IconImagePath)));

    // Hook up it's invoked event handler
    menuItem.Invoked += MenuItem_Invoked;

    // Add it to the RadialDial
    dialController.Menu.Items.Add(menuItem);
 }

 

The menu item’s Invoked event handler is fired when an effect is chosen by the user, I get the selected effect by checking what the DisplayName of the menu item was using the RadialControllerMenuItem sender

private async void MenuItem_Invoked(RadialControllerMenuItem sender, object args)
{
    var selectedEffect = PageViewModel.VideoEffects.FirstOrDefault(
        e => e.DisplayName == sender?.DisplayText);

    // apply effect
 }

 

At this point, the effect is applied to the video stream. So we need to switch our focus to the RadialControler’s RotationChanged event handler. This is where I can get the rotation delta (which direction was it turned and by how much) from the RotationDeltaInDegrees property of the RadialControllerRotationChangedEventArgs. 

Since I also have a slider in the UI for the user to change the value (because not every user is going to have a Surface Dial!), I update the slider’s value directly:

 

private void DialControllerRotationChanged(RadialController sender, RadialControllerRotationChangedEventArgs args)
{
    SelectedEffectSlider.Value += args.RotationDeltaInDegrees / 100;
    UpdateEffect();
}

 

Now in the UpdateEffect method, I can use the slider’s new value to apply the effect change:

 

private void UpdateEffect()
{
    // Update effect's values
    PageViewModel.SelectedEffect.PropertyValue = (float) SelectedEffectSlider.Value;
    effectPropertySet[PageViewModel.SelectedEffect.PropertyName] = (float) PageViewModel.SelectedEffect.PropertyValue;
}

 

That’s it! Check out the video above to see the app in action and see the full source code here on GitHub.

 

 

 

 

Build A Custom Win2D RadImageEditor Tool

Telerik has recently ported the RadImageEditor to Windows Universal (8.1 Universal right now, UWP is coming very soon). It is powerful for something that only needs a few lines of code to use the 20 predefined tools.

But what if you wanted something not in those 20? Or what if you didn’t want to have a dependency on the Lumia Imaging SDK (needed for the built-in tools)?

One great features of RadImageEditor is the ability to make a custom tool, tool group or layer. Today I’ll show you how to create a Win2D tool group and add a custom GaussianBlurTool.

Here’s the result:

 

Let’s start with the tool class.

RadImageEditor provides four classes that you can inherit from to make your tool:

  • ImageEditorTool: The most basic tool type.
  • RangeTool: The effect of these tools can vary in the predefined range of values. You get a Slider for user input
  • ImageEditorTransformTool: Allows the user to physically transform the image with gestures.
  • ImageEditorEffectTool: These tools do not support any configuration, they directly apply an effect by selecting the tool.

 

Since Win2D’s GaussianBlur only needs a float value to apply a blur, RangeTool is the best fit. You will need to override a few things:

  • string Name (name of the effect to be shown in the tool group)
  • string Icon (the string path to the icon image file)
  • async Task<WriteableBitmap> ApplyCore (this Task is where you apply your effect)
  • double Min (this is for the Slider shown to the user)
  • double Max (this is for the Slider shown to the user)

ApplyCore is the one that needs a little further explanation.It gets passed two objects:

  • IRandomAccessStream stream (This is the unmodified image from the StorageFile)
  • WriteableBitmap targetBitmap (after applying an effect, copy the pixels into this and return it)

Now that we’re armed with that information, we can get to work on the Win2D blur. Explaining how Win2D works is out-of-scope for this article, but what you need to know is that we can create a CanvasBitmap from the stream, apply an effect to it and copy those pixels and return it.

Here’s the code for the tool (Github gist here)


public class GaussianBlurTool : RangeTool
    {
        public override string Name => "Gaussian Blur";

        public override string Icon => "ms-appx:///CustomTools/ToolIcons/blur.png";

        public override double Min => 0; //Maximum value for the slider

        public override double Max => 20; //Minimum value for the slider

        protected override async Task<WriteableBitmap> ApplyCore(IRandomAccessStream stream, WriteableBitmap targetBitmap)
        {
            try
            {
                stream.Seek(0);

                using (var device = CanvasDevice.GetSharedDevice())
                using (CanvasBitmap cbm = await CanvasBitmap.LoadAsync(device, stream))
                using (CanvasRenderTarget renderer = new CanvasRenderTarget(device, cbm.SizeInPixels.Width, cbm.SizeInPixels.Height, cbm.Dpi))
                using (CanvasDrawingSession ds = renderer.CreateDrawingSession())
                {
                    var blur = new GaussianBlurEffect
                    {
                        BlurAmount = (float) this.Value,
                        Source = cbm
                    };

                    ds.DrawImage(blur);
                    ds.Flush(); //important, this forces the drawing operation to complete

                    await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.High, () =>
                    {
                        //IMPORTANT NOTE:
                        //You need to add using System.Runtime.InteropServices.WindowsRuntime in order to use CopyTo(IBuffer)
                        renderer.GetPixelBytes().CopyTo(targetBitmap.PixelBuffer);
                    });
                }

                return targetBitmap;
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"ApplyCore in GaussianBlurTool Exception: {ex}");
                throw;
            }
        }
    }

The XAML

Now how do we use this? You define the tool group in the RadImageEditor within a custom ToolGroup. I named my tool group “Win2D Effects” and inside that placed the GaussianBlurTool. Note that you can place more than one tool in a tool group, I plan on adding more Win2D tools in there.


<input:RadImageEditor x:Name="MyImageEditor">
    <imageEditor:ImageEditorToolGroup Name="Win2D Effects" Icon="ms-appx:///CustomTools/ToolIcons/Win2DToolGroupIcon.png">
        <customTools:GaussianBlurTool />
    </imageEditor:ImageEditorToolGroup>
</input:RadImageEditor>

 

That’s it! Now go forth and extend the RadImageEditor with some great Win2D goodness and let me know how it goes.

Sending Image Data with parameter using HttpClient Post

I recently needed to send image data to a server for processing and thought I’d share how to do that using System.Net.Http.HttpClient in a UWP (Universal Windows) app. First, let’s start with the API’s requirements, it states:

Parameter: image  The image parameter should be the binary file data for the image you would like analyzed (PNG, GIF, JPG only). Files cannot be larger than 500k.

So that means I have to send image data as binary data with the parameter image. I can use System.Net.Http.ByteArrayContent for the image data. To get the image data from the file as a byte[] the approach I use is the following (there are other ways to do this):


var myImageFile = await Windows.Storage.ApplicationData.Current.LocalFolder.GetFileAsync(fileName);

byte[] fileBytes;
using (var fileStream = await myImageFile.OpenStreamForReadAsync())
{
var binaryReader = new BinaryReader(fileStream);
fileBytes = binaryReader.ReadBytes((int)fileStream.Length);
}

 

Now that I have a byte[], I can create an instance of System.Net.Http.ByteArrayContent to hold the image’s binary data:


var imageBinaryContent= new ByteArrayContent(fileBytes);

 

Normally when sending content, you’d just pass the content as itself to the PostAsync() method directly. However, because I need to send the content with the parameter name image, I’ll need to use System.Net.Http.MultiPartFormDataContent. It allows you to set content with a parameter name. Here’s how I did it:


var multipartContent = new MultipartFormDataContent();
multipartContent.Add(imageBinaryContent, &quot;image&quot;);

 

Now that we have the content ready to go, all that’s left to do is to pass it to PostAsync() when the call is made. Here’s the entire snippet:


//get image file
var myImageFile = await Windows.Storage.ApplicationData.Current.LocalFolder.GetFileAsync(fileName);

//convert filestream to byte array
byte[] fileBytes;
using (var fileStream = await myImageFile.OpenStreamForReadAsync())
{
var binaryReader = new BinaryReader(fileStream);
fileBytes = binaryReader.ReadBytes((int)fileStream.Length);
}

//instantiate the client
using(var client = new HttpClient())
{

//api endpoint
var apiUri = new Uri(&quot;http://someawesomeapi.com/api/1.0/&quot;);

//load the image byte[] into a System.Net.Http.ByteArrayContent
var imageBinaryContent = new ByteArrayContent(fileBytes);

//create a System.Net.Http.MultiPartFormDataContent
var multipartContent = new MultipartFormDataContent();
multipartContent.Add(imageBinaryContent, &quot;image&quot;);

//make the POST request using the URI enpoint and the MultiPartFormDataContent
var result = await client.PostAsync(apiUri, multipartContent);
}

 

I hope this makes things easier for you,

Happy coding!

 

As suggested by my buddy Scott Lovegrove, you could also move this into an HttpClient Extension Method. How much functionality you put in it is up to you, but I went with most of it. To use it, simply, pass the StorageFile, API url and the parameter name:


var result = await client.PostImageDataAsync(myImageFile, &quot;http://myapi.com/&quot;, &quot;image&quot;);

 

HttpClient Extension Method:

 

Reward your users for feedback and bug reports

One of the new features of DevCenter is the ability to generate Promotional Codes for your app. With this new ability, I’ve started rewarding my users for taking the time to make the app better.

Let’s be specific so that you know exactly what ‘m talking about. I have a relatively popular app in the store and get all sorts of feedback, crazy nonsense and helpful alike. For those users whom send in a helpful bug report or would get the following email from me:

Hi [Name],

Thank you very much for taking the time to send in this [error report/feedback]. I care deeply about your experience with [app name] and work hard to ensure there are no [bugs/missing features]. I will [fix this bug\add feature] and include it in the next app update.

To show my appreciation for you sending this to me, please accept my gift of “Ultimate App Unlock” code below. It will unlock all the features in the app after you redeem it (instructions included).

If you have any future suggestions, feature requests or bug reports, please let me know.

Thank you for your support,

Lance

Developer, [app name]

Here’s how to get your Promotional Codes from DevCenter:

  1. Go to your app’s App Overview page from the DevCenter Dashboard
  2. Expand the Monetization node (on the left)
  3. Select Promotional Codes
  4. Click Order Codes

You’ll be presented with the following screen:

2015-10-14_1258

Fill out the form by choosing an IAP, choose the number of codes you want made (up to 250) and click the Order Codes button.

DevCenter will generate the codes and you’ll see a new item on the Promotional Codes page with a “Download” link (you get a .tsv file that can be opened in Excel).

Hopefully, this user experience tip will help you as much as it has helped me,

Lance

Telerik Universal on Raspberry Pi2

Most people think of IoT (Internet of Things) as a collection of tiny brains, without user interfaces, collecting data. Some think it’s a remote controlled animal shelter cat toy or a Grant Imahara style robot to do battle. The truth is that IoT is a very broad term, it covers many types of devices, including remote cat toys!

Here’s a Vine of what Windows IoT means to me… Telerik UI on IoT! This post is a overview tutorial, find the full source code on GitHub. (#bananaForScale)

Windows 10 on IoT

The introduction of Windows 10 IoT brings IoT to a whole new level. You get access to your familiar hardware benefits (GPIOI²C, etc.), but you can now do it with XAML and C#! You can run Windows IoT headless or with a UI (ultimately, this depends on the device you chose to install it on).

I choose the Raspberry PI2 because it has an HDMI video out for display. In my photo and Vine, you’ll see a strange 7″ display, it’s just an HDMI monitor, hardware hacker-style.

Telerik Super Polish

Now that I have access to a UI, one of the very first things I did was attempt to run Telerik UI for Windows Universal on it 🙂 To my delight, it just works. I can actually write an app once and it just works on PC, Tablet, Xbox One, Hololens and IoT. The promise Microsoft gave us WPDEVs two years ago is finally taking shape.

Enough talk, let’s get coding

I’ve posted my source for this project and it’s prerequisites on Github. Please see the README.md so you know what you’ll need to get started.

Step One – File > New

Open Visual Studio 2015 and go to File > New > Project. Drill down to Universal, select Blank App, give it a name and click OK.

1

Step Two – Add Project References 

Normally you may be used to adding actually DLLs to a project. This approach is slightly different. we’re using Extension SDKs. Right click on your project’s references, select Add Reference, drill down to Extensions under Universal Windows (#1) and check off the two items you see in #2.

2

[UPDATE – Telerik for UWP is available as of Q1 2016, get it here. and reference Telerik UI for Universal Windows Platform in the image above]

The first reference, Telerik UI for Windows 8.1, is the Extension SDK name for the Telerik UI for Windows Universal controls. Don’t worry about the asterisk (*), it just means that it wasn’t compiled specifically for UWP yet, but it still works as expected.

The second item, Windows IoT Extensions for the UWP, is what allows your app to deploy to Windows IoT and provides the base classes for things like GPIO.

Your references should now look like this:

3

Step Three – Dev Time

First, let’s switch your designer to show a 10″ IoT device. Its usually a 5″ mobile phone at startup because that’s the first item in the list. Note: Take a step back for a second and see all those device types, your Telerik app will run on all of them!

4

Now let’s add a Telerik Chart to the UI. In my demo app on GitHub, I have some sample Car data that has the following properties: Make, Model Year and Price (model code is here). That sample data is loaded into a collection in the MainViewModel.

Now that I have a collection of cars, I want to show a chart. Let’s use a BarChart that groups by Make and shows the Price (find the UI code here).

  • Begin by adding this to the top of the XAML page: xmlns:chart=”using:Telerik.UI.Xaml.Controls.Chart”, this is so you can instantiate any control in the Chart namespace.
  • Next, declare RadCartesianChart and set the axes accordingly, CategoryAxis for the horizontal and LinearAxis for the vertical (#1).
  • Now, we add a BarSeries and set the ItemsSource via databinding to the Cars collection (#2).
  • Lastly, set the property name Make for the CategoryBinding and Price for the ValueBinding (#3).

6

If you do a Build at this point, and you’ve added sample data to the view model like I did, you’ll see data in the UI designer (very cool that methods written to work at run-time, work at design-time). Like this:

5

Step Four – Deploy to Device

I added a couple more chart examples and a Slider control to change the Price values dynamically, but you don’t need to take it that far to see your progress. Just deploy onto your Windows IoT device at this point to see that it works!

Some tips on Windows 10 IoT

I highly recommend that you put aside a about two hours to prepare your board for the first time. You will use a tool that comes with the Windows 10 IoT build for your device to deploy, it creates the partition and loads the ffu. Its pretty straightforward and much easier than it was before RTM.

Deploying and debugging is pretty easy, the IoT device is treated as any other remote machine. Steps to take are:

  1. Right click on the project and select Properties
  2. In the left column, select Debug
  3. In the Target DropDownList, select Remote Machine
  4. Then click the “Find” button and enter the IP address of the Raspberry Pi 2 (you’ll find this displayed on the Pi’s home screen)
  5. Set the Authentication Mode to None
  6. Save and close the Properties page.

Now when you start debugging, it will look for your Pi 2 on the network, deploy and run the app package. For more Remote Debugging details see this documentation (note that the Remote Debugger tools are already built into Windows 10 Core IoT).

Well, that’s it for now. I will be doing another IoT and Telerik post soon. I’ll do another post on how to use GPIO ports, reading sensor data and showing it in a gorgeous UI

Happy coding,

Lance

Windows 10 IoT