Windows 10, IoT Core–Getting that 2 Row LCD Panel Working!

I was right in my previous post where I catalogued some trials in trying to get an LCD 1602 display (HD44780 compatible) to work with Windows IoT Core and a Raspberry PI 2.

In the end, what it needed was solder.

I’d played around with code for hours while pretending that my dodgy connections to the LCD panel were ‘good enough’ but, in the end, my suspicions were right and making nice, shiny, soldered connections addressed the problem straight away. Stupid of me to waste time thinking that I could get away with it!

Here’s my handiwork – I was quite pleased given that I reckon it’s over 30 years since I soldered anything to anything;

WP_20150713_10_39_58_Pro

I wired up the LCD panel in exactly the same way as this article;

Basic LCD 16×2

But I didn’t use the code from that article. That wasn’t because it’s bad code or anything like that. It’s because I’d already started writing my own code prior to finding that article and also as part of trying to get things to work which (in the end) turned out to be down to me not soldering the connections.

There are 12 pins that you end up wiring on the LCD panel;

  • VSS – ground
  • VDD – 5V
  • V0 – voltage for the backlight. I wired this in to a 10K potentiometer so I could adjust the backlight
  • RS – shift register
  • RW – read/write which I wired to ground because I’m always writing
  • E – enable bit which you toggle to get the device to read instructions/data
  • Data registers D4,5,6,7
  • A – 5V
  • K – ground

Because I’ve only wired up 4 of the 8 data lines (D4…D7 missing out D0…3) I use the 4-bit addressing mode that’s well explained in the datasheet. Connecting 4 more wires felt like overkill to me.

In my setup, I’ve wired RS to GPIO pin 5, E to pin 6 and then Data pins D4,5,6,7 to GPIO pins 23,24,25,26 respectively as you’ll spot in the code listing in a moment.

Writing a Library to Control the Panel

I wrote a little library to control the panel and made sure that it referenced both the UWP and also the IoT Extensions SDK.

The first class I needed was something to represent how the RS/RW/E/Data4,5,6,7 pins have been wried up and so I wrote;

namespace LcdControl
{
  using System.Linq;

  public class LcdPinMapping
  {
    public int RSPin { get; set; }
    public int EnablePin { get; set; }
    public int Data4Pin { get; set; }
    public int Data5Pin { get; set; }
    public int Data6Pin { get; set; }
    public int Data7Pin { get; set; }
    public int[] AllPins
    {
      get
      {
        return (
          (new int[] { this.RSPin, this.EnablePin }).Concat(this.DataPinsHighToLow).ToArray());
      }
    }
    public int[] DataPinsHighToLow
    {
      get
      {
        return (new int[] { this.Data7Pin, this.Data6Pin, this.Data5Pin, this.Data4Pin });
      }
    }
  }
}

It’s pretty simple but it did the job. With that in place, I wrote a little class that would take one of these mappings and then use the extension SDK to open up the GpioPin objects for those pins and keep them around. I also made that class responsible for writing bytes/nibbles of data to the LCD which involves writing the high 4 bits first followed by the low 4 bits. It also involves signaling the E pin from High to Low to tell the panel there’s something waiting for it. That class looks like;

namespace LcdControl
{
  using System;
  using System.Collections.Generic;
  using System.Diagnostics;
  using System.Text;
  using System.Threading.Tasks;
  using Windows.Devices.Gpio;
  class LcdPinManager
  {
    public LcdPinManager(LcdPinMapping mapping)
    {
      this.mapping = mapping;
      this.pinMap = new Dictionary<int, GpioPin>();
    }
    public void OpenPins()
    {
      foreach (var pin in this.mapping.AllPins)
      {
        var gpioPin = GpioController.GetDefault().OpenPin(pin);
        this.pinMap[pin] = gpioPin;
        gpioPin.SetDriveMode(GpioPinDriveMode.Output);
      }
    }
    public async Task WriteByteAsync(byte bits, bool command)
    {
      Debug.WriteLine($"Writing byte {bits}");
      await this.WriteNibbleAsync((byte)(bits >> 4), command);
      await this.WriteNibbleAsync((byte)(bits & 0x0F), command);
    }
    public async Task WriteNibbleAsync(byte content, bool isCommand)
    {
#if DEBUG
      StringBuilder debugString = new StringBuilder();
      debugString.AppendFormat($"Writing Nibble {content}:");
#endif

      this.Enable(true);

      this.pinMap[this.mapping.RSPin].Write(
        isCommand ? GpioPinValue.Low : GpioPinValue.High);

      debugString.AppendFormat("RS({0}):", isCommand ? 0 : 1);

      for (int i = 0; i < this.mapping.DataPinsHighToLow.Length; i++)
      {
        bool high = ((content & (1 << (3 - i))) != 0);

        GpioPin gpioPin = this.pinMap[this.mapping.DataPinsHighToLow[i]];

        gpioPin.Write(high ? GpioPinValue.High : GpioPinValue.Low);

#if DEBUG
        debugString.AppendFormat("{0}", high ? 1 : 0);
#endif
      }
      Debug.WriteLine(debugString.ToString());

      this.Enable(false);

      await Task.Delay(enableDelay);
    }
    void Enable(bool high)
    {
      this.pinMap[this.mapping.EnablePin].Write(high ? GpioPinValue.High : GpioPinValue.Low);
    }
    static readonly TimeSpan enableDelay = TimeSpan.FromMilliseconds(20);
    LcdPinMapping mapping;
    Dictionary<int, GpioPin> pinMap;
  }
}

I should say that I think the 20ms delay here is overkill but it worked for me and I can spare 20ms here and there so I left it as is. If you read the datasheet then you’ll see that you can adjust these times down a lot for certain instructions.

On top of this class, I wrapped up a number of command bytes into enums as per;

namespace LcdControl
{
  enum Commands: byte
  {
    ClearDisplay = 0x1,
    CursorHome = 0x2,
    EntryModeSet = 0x4,
    DisplayControl = 0x8,
    CursorDisplayShift = 0x10,
    FunctionSet = 0x20,
    SetDDRAMAddress = 0x80
  }
  public enum RightLeftOptions : byte
  {
    Left = 0x0,
    Right = 0x4
  }
  public enum MoveDirections : byte
  {
    Decrement = 0x0,
    Increment = 0x2
  }
  // I haven't implemented this one because I think it means changing my DDRAM
  // addressing.
  enum EntryModeShiftOptions : byte
  {
    NoShift = 0x0,
    Shift = 0x1
  }
  public enum DisplayOptions : byte
  {
    Off = 0,
    On = 0x4
  }
  public enum CursorOptions : byte
  {
    Hidden = 0x0,
    Shown = 0x2
  }
  public enum BlinkOptions : byte
  {
    NoBlink = 0x0,
    Blink = 0x1
  }
  public enum DataLengthOptions : byte
  {
    FourBit = 0x0,
    EightBit = 0x10
  }
  public enum NumberLinesOptions : byte
  {
    OneLine = 0x0,
    TwoLines = 0x8
  }
  enum FontOptions : byte
  {
    FiveByEight = 0x0,
    FiveByTen = 0x4
  }
}



And then I wrote an LcdManager class to use these pieces to try and write data to the panel;

namespace LcdControl
{
  using System;
  using System.Text;
  using System.Threading.Tasks;

  /// <summary>
  /// Simple class to support a 16x02 LCD panel over GPIO. Note, this class gives
  /// the promise of supporting 4-bit and 8-bit comms but, in fact, I only wrote
  /// the 4-bit part because I couldn't be fussed connecting 8 wires from my
  /// PI to the device. Sorry!
  /// 
  /// </summary>
  public class LcdManager
  {
    LcdPinManager pinManager;
    public LcdManager(LcdPinMapping mapping, DataLengthOptions dataLength)
    {
      if (dataLength != DataLengthOptions.FourBit)
      {
        throw new ArgumentException("Sorry, I didn't write the 8 bit mode part of this");
      }
      this.pinManager = new LcdPinManager(mapping);
      this.pinManager.OpenPins();
    }
    /// <summary>
    /// Initialises the device. If you're wondering where these slightly
    /// wacky bit sequences come from, I took them from
    /// http://web.alfredstate.edu/weimandn/lcd/lcd_initialization/lcd_initialization_index.html
    /// </summary>
    /// <returns></returns>
    public async Task InitialiseAsync()
    {
      // Formal initialisation starts here.
      for (int i = 0; i < 3; i++)
      {
        // We write bit pattern 0011 three times to the device.
        // this is as per the referenced doc. The lower 4 bits
        // are irrelevant here.
        await this.pinManager.WriteNibbleAsync(0x3, true);
      }
      // We then send down 0010 which should switch the LCD
      // panel into 4-bit mode.
      await this.pinManager.WriteNibbleAsync(0x2, true);

      // Should be 0x28.
      await this.FunctionSetAsync(
        DataLengthOptions.FourBit,
        NumberLinesOptions.TwoLines,
        FontOptions.FiveByEight);

      // Should be 0x8. NB: this is part of initialisation, it's not
      // how I actually want the display (turned off). 
      await this.DisplayControlAsync(
        DisplayOptions.Off,
        CursorOptions.Hidden,
        BlinkOptions.NoBlink);

      // 0x1.
      await this.ClearDisplayAsync();

      // 0x6. Entry Mode Set.
      await this.EntryModeSetAsync(MoveDirections.Increment);

      // 0xF. Now, set the display on, the cursor visible and
      // the cursor blinking.
      await this.DisplayControlAsync(DisplayOptions.On,
        CursorOptions.Shown, BlinkOptions.Blink);
    }
    public Task ClearDisplayAsync()
    {
      return (this.pinManager.WriteByteAsync((byte)Commands.ClearDisplay, true));
    }
    public Task ReturnHomeAsync()
    {
      return (this.pinManager.WriteByteAsync((byte)Commands.CursorHome, true));
    }
    public Task EntryModeSetAsync(MoveDirections direction)
    {
      byte command = (byte)Commands.EntryModeSet;
      command |= (byte)direction;
      command |= (byte)EntryModeShiftOptions.NoShift;
      return (this.pinManager.WriteByteAsync(command, true));
    }
    public Task DisplayControlAsync(DisplayOptions onOff,
      CursorOptions cursorOnOff,
      BlinkOptions blinkOnOff)
    {
      byte command = (byte)Commands.DisplayControl;
      command |= (byte)onOff;
      command |= (byte)cursorOnOff;
      command |= (byte)blinkOnOff;
      return (this.pinManager.WriteByteAsync(command, true));
    }
    public async Task MoveToPosition(uint x, uint y)
    {
      if ((x >= SCREEN_WIDTH) || (y >= SCREEN_HEIGHT))
      {
        throw new ArgumentException("Invalid x,y position passed");
      }
      await this.SetDDRAMAddress(
        (byte)((y == 0 ? LINE_ONE : LINE_TWO) + (byte)x));
    }
    public async Task WriteCharAtCoordAsync(uint x, uint y, char output)
    {
      await this.MoveToPosition(x, y);
      await this.WriteCharAsync(output);
    }
    public async Task WriteCharAsync(char output)
    {
      var bytes = UTF8Encoding.UTF8.GetBytes(new char[] { output });

      if ((bytes.Length > 1) || (bytes[0] < ASCII_SPACE) || (bytes[0] > ASCII_TILDE))
      {
        throw new ArgumentException("Character doesn't look like an ASCII code");
      }
      await this.pinManager.WriteByteAsync(bytes[0], false);
    }
    public async Task WriteStringAsync(string output)
    {
      foreach (var character in output)
      {
        await this.WriteCharAsync(character);
      }
    }
    public async Task WriteStringLeftToRightAtCoordAsync(uint x, uint y, string output)
    {
      if ((x + output.Length) > SCREEN_WIDTH)
      {
        throw new ArgumentException("String is too long for the screen");
      }
      uint xCoord = x;

      foreach (var character in output)
      {
        await this.WriteCharAtCoordAsync(xCoord, y, character);
        xCoord++;
      }
    }
    /// <summary>
    /// Not public as I don't think you can control this once you've gone
    /// through the initialisation point.
    /// </summary>
    /// <param name="dataLength"></param>
    /// <param name="numberLines"></param>
    /// <param name="font"></param>
    /// <returns></returns>
    Task FunctionSetAsync(
      DataLengthOptions dataLength,
      NumberLinesOptions numberLines,
      FontOptions font)
    {
      byte command = (byte)Commands.FunctionSet;
      command |= (byte)dataLength;
      command |= (byte)numberLines;
      command |= (byte)font;
      return (this.pinManager.WriteByteAsync(command, true));
    }
    Task SetDDRAMAddress(byte address)
    {
      byte command = (byte)Commands.SetDDRAMAddress;
      command |= address;
      return (this.pinManager.WriteByteAsync(command, true));
    }
    static readonly int SCREEN_WIDTH = 16;
    static readonly int SCREEN_HEIGHT = 2;
    static readonly byte ASCII_SPACE = 0x20;
    static readonly byte ASCII_TILDE = 0x7E;
    static readonly byte LINE_ONE = 0x00;
    static readonly byte LINE_TWO = 0x40;
  }
}

With that in place, I was ready to try things out a little.

Building a ‘Test UI’

I wrote a little test UI to try out the main functions of that LcdManager and so I knocked up a little XAML;

<Page
    x:Class="LabProject.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:LabProject"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
  <Page.Resources>
    <Style TargetType="Button">
      <Setter Property="Margin" Value="5"/>
    </Style>
    <Style TargetType="TextBlock">
      <Setter Property="Margin" Value="5"/>
    </Style>
    <Style TargetType="StackPanel">
      <Setter Property="Margin" Value="5"/>
    </Style>
    <Style TargetType="TextBox">
      <Setter Property="Margin" Value="5"/>
    </Style>
    <Style TargetType="ToggleSwitch">
      <Setter Property="Margin" Value="5"/>
    </Style>
  </Page.Resources>

  <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
      <ToggleSwitch IsOn="{Binding DisplayOn,Mode=TwoWay}">Display On</ToggleSwitch>
      <ToggleSwitch IsOn="{Binding CursorOn,Mode=TwoWay}">Cursor On</ToggleSwitch>
      <ToggleSwitch IsOn="{Binding CursorBlink,Mode=TwoWay}">Cursor Blink</ToggleSwitch>
      <ToggleSwitch IsOn="{Binding MoveDirection,Mode=TwoWay}" OnContent="Increment" OffContent="Decrement"/>

      <Button Content="Clear" Click="OnClear"/>
      <Button Content="Return Home" Click="OnReturnHome"/>
      <StackPanel Orientation="Horizontal">
        <TextBlock Text="Move to (x,y)"/>
        <TextBox Text="{Binding XCoord,Mode=TwoWay}"/>
        <TextBox Text="{Binding YCoord,Mode=TwoWay}"/>
        <Button Click="MoveToPosition" Content="Move"/>
      </StackPanel>
      <StackPanel Orientation="Horizontal">
        <TextBlock Text="Write Character at Current"/>
        <TextBox MaxLength="1" Text="{Binding CharToWrite, Mode=TwoWay}"/>
        <Button Click="WriteCharAtCurrent" Content="Write"/>
      </StackPanel>
      <StackPanel Orientation="Horizontal">
        <TextBlock Text="Write String at Current"/>
        <TextBox MaxLength="16" Width="240" Text="{Binding StringToWrite, Mode=TwoWay}"/>
        <Button Click="WriteStringAtCurrent" Content="Write"/>
      </StackPanel>
      <StackPanel Orientation="Horizontal">
        <TextBlock Text="Write Character at (x,y)"/>
        <TextBox Text="{Binding XCoord,Mode=TwoWay}"/>
        <TextBox Text="{Binding YCoord,Mode=TwoWay}"/>
        <TextBox MaxLength="1" Text="{Binding CharToWrite, Mode=TwoWay}"/>
        <Button Click="WriteCharAtCoord" Content="Write"/>
      </StackPanel>
      <StackPanel Orientation="Horizontal">
        <TextBlock Text="Write String Left To Right at (x,y)"/>
        <TextBox Text="{Binding XStringCoord,Mode=TwoWay}"/>
        <TextBox Text="{Binding YStringCoord,Mode=TwoWay}"/>
        <TextBox MaxLength="16" Width="240" Text="{Binding StringToWrite, Mode=TwoWay}"/>
        <Button Click="WriteStringAtCoord" Content="Write"/>
      </StackPanel>
    </StackPanel>
  </Grid>
</Page>

With some code behind it;

namespace LabProject
{
  using LcdControl;
  using System.ComponentModel;
  using Windows.UI.Xaml;
  using Windows.UI.Xaml.Controls;

  public sealed partial class MainPage : Page, INotifyPropertyChanged
  {
    public event PropertyChangedEventHandler PropertyChanged;

    public MainPage()
    {
      this.InitializeComponent();
      this.Loaded += OnLoaded;
    }
    public bool MoveDirection
    {
      get
      {
        return (this.moveDirection == MoveDirections.Increment);
      }
      set
      {
        this.moveDirection = value ? MoveDirections.Increment : MoveDirections.Decrement;

        this.displayManager.EntryModeSetAsync(this.moveDirection);

        this.RaisePropertyChanged("MoveDirection");
      }
    }
    public uint XCoord
    {
      get
      {
        return (this.xCharCoord);
      }
      set
      {
        this.xCharCoord = value;
        this.RaisePropertyChanged("XCoord");
      }
    }
    public uint YCoord
    {
      get
      {
        return (this.yCharCoord);
      }
      set
      {
        this.yCharCoord = value;
        this.RaisePropertyChanged("YCoord");
      }
    }
    public string CharToWrite
    {
      get
      {
        return (this.charToWrite);
      }
      set
      {
        this.charToWrite = value;
        this.RaisePropertyChanged("CharToWrite");
      }
    }
    public uint XStringCoord
    {
      get
      {
        return (this.xStringCoord);
      }
      set
      {
        this.xStringCoord = value;
        this.RaisePropertyChanged("XStringCoord");
      }
    }
    public uint YStringCoord
    {
      get
      {
        return (this.yStringCoord);
      }
      set
      {
        this.yStringCoord = value;
        this.RaisePropertyChanged("YStringCoord");
      }
    }
    public string StringToWrite
    {
      get
      {
        return (this.stringToWrite);
      }
      set
      {
        this.stringToWrite = value;
        this.RaisePropertyChanged("StringToWrite");
      }
    }
    public bool CursorBlink
    {
      get
      {
        return (this.blinkOnOff == BlinkOptions.Blink);
      }
      set
      {
        this.blinkOnOff = value ? BlinkOptions.Blink : BlinkOptions.NoBlink;

        this.displayManager.DisplayControlAsync(this.displayOnOff, this.cursorOnOff,
          this.blinkOnOff);

        this.RaisePropertyChanged("CursorBlink");
      }
    }
    public bool DisplayOn
    {
      get
      {
        return (this.displayOnOff == DisplayOptions.On);
      }
      set
      {
        this.displayOnOff = value ? DisplayOptions.On : DisplayOptions.Off;

        this.displayManager.DisplayControlAsync(this.displayOnOff, this.cursorOnOff,
          this.blinkOnOff);

        this.RaisePropertyChanged("DisplayOn");
      }
    }
    public bool CursorOn
    {
      get
      {
        return (this.cursorOnOff == CursorOptions.Shown);
      }
      set
      {
        this.cursorOnOff = value ? CursorOptions.Shown : CursorOptions.Hidden;

        this.displayManager.DisplayControlAsync(this.displayOnOff, this.cursorOnOff,
          this.blinkOnOff);

        this.RaisePropertyChanged("CursorOn");
      }
    }
    async void OnLoaded(object sender, Windows.UI.Xaml.RoutedEventArgs e)
    {
      LcdPinMapping mapping = new LcdPinMapping()
      {
        RSPin = 5,
        EnablePin = 6,
        Data4Pin = 23,
        Data5Pin = 24,
        Data6Pin = 25,
        Data7Pin = 26
      };
      this.displayManager = new LcdManager(mapping, DataLengthOptions.FourBit);

      await this.displayManager.InitialiseAsync();

      this.blinkOnOff = BlinkOptions.Blink;
      this.displayOnOff = DisplayOptions.On;
      this.cursorOnOff = CursorOptions.Shown;
      this.moveDirection = MoveDirections.Increment;

      this.DataContext = this;
    }
    void RaisePropertyChanged(string propertyName)
    {
      var handlers = this.PropertyChanged;
      if (handlers != null)
      {
        handlers(this, new PropertyChangedEventArgs(propertyName));
      }
    }
    void OnClear(object sender, RoutedEventArgs e)
    {
      this.displayManager.ClearDisplayAsync();
    }
    void OnReturnHome(object sender, RoutedEventArgs e)
    {
      this.displayManager.ReturnHomeAsync();
    }
    void WriteCharAtCoord(object sender, RoutedEventArgs e)
    {
      this.displayManager.WriteCharAtCoordAsync(this.xCharCoord, this.yCharCoord, this.charToWrite[0]);
    }
    void WriteStringAtCoord(object sender, RoutedEventArgs e)
    {
      this.displayManager.WriteStringLeftToRightAtCoordAsync(this.xStringCoord, this.yStringCoord, this.stringToWrite);
    }
    void WriteStringAtCurrent(object sender, RoutedEventArgs e)
    {
      this.displayManager.WriteStringAsync(this.stringToWrite);
    }
    void WriteCharAtCurrent(object sender, RoutedEventArgs e)
    {
      this.displayManager.WriteCharAsync(this.charToWrite[0]);
    }
    void MoveToPosition(object sender, RoutedEventArgs e)
    {
      this.displayManager.MoveToPosition(this.xCharCoord, this.yCharCoord);
    }
    DisplayOptions displayOnOff;
    CursorOptions cursorOnOff;
    BlinkOptions blinkOnOff;
    MoveDirections moveDirection;
    LcdManager displayManager;
    uint xCharCoord;
    uint yCharCoord;
    string charToWrite;
    uint xStringCoord;
    uint yStringCoord;
    string stringToWrite;
  }
}

And that gives me one of the ugliest forms I’ve ever seen! But I could drive it on the PI via mouse and keyboard (I have a monitor plugged into my PI over HDMI);

image

But, sure enough, that let me test out this code as I’ll try and illustrate on the little (phone captured) video below where I wrapped a viewbox around the UI to make it a little bigger;

and that all seems to work reasonably well. The code for all of that is here;

Code Download

I then thought that it might be nice to wire this up such that a press of a switch did something on the display and so I wrote a little ‘SwitchManager’ class;

namespace LabProject
{
  using System;
  using Windows.Devices.Gpio;

  class SwitchManager
  {
    public event EventHandler SwitchPressed;

    public SwitchManager(uint gpioPin)
    {
      this.pin = GpioController.GetDefault().OpenPin((int)gpioPin);
      this.pin.ValueChanged += OnPinValueChanged;
    }
    void OnPinValueChanged(GpioPin sender, GpioPinValueChangedEventArgs args)
    {
      var handlers = this.SwitchPressed;

      var value = sender.Read();

      if ((value == GpioPinValue.High) &&
        (handlers != null))
      {
        handlers(this, EventArgs.Empty);
      }
    }  
    GpioPin pin;
  }
}

and connected a little switch on my breadboard to GPIO pin 12 and then changed my UI to be blank and wrote this little code behind;

namespace LabProject
{
  using LcdControl;
  using System.ComponentModel;
  using System.Threading.Tasks;
  using Windows.UI.Xaml.Controls;

  public sealed partial class MainPage : Page, INotifyPropertyChanged
  {
    public event PropertyChangedEventHandler PropertyChanged;

    public MainPage()
    {
      this.InitializeComponent();
      this.Loaded += OnLoaded;
    }
    async void OnLoaded(object sender, Windows.UI.Xaml.RoutedEventArgs e)
    {
      LcdPinMapping mapping = new LcdPinMapping()
      {
        RSPin = 5,
        EnablePin = 6,
        Data4Pin = 23,
        Data5Pin = 24,
        Data6Pin = 25,
        Data7Pin = 26
      };
      this.displayManager = new LcdManager(mapping, DataLengthOptions.FourBit);

      await this.displayManager.InitialiseAsync();

      this.switchManager = new SwitchManager(12);
      this.switchManager.SwitchPressed += OnSwitched;

      this.messageOne = true;
      await this.ChangeDisplayAsync();
    }
    async Task ChangeDisplayAsync()
    {
      await this.displayManager.ClearDisplayAsync();

      if (this.messageOne)
      {
        await this.displayManager.WriteStringLeftToRightAtCoordAsync(0, 0, "Press the switch");
        await this.displayManager.WriteStringLeftToRightAtCoordAsync(0, 1, "2 change display");
      }
      else
      {
        await this.displayManager.WriteStringLeftToRightAtCoordAsync(0, 0, "this is the");
        await this.displayManager.WriteStringLeftToRightAtCoordAsync(0, 1, "other display");
      }
      this.messageOne = !this.messageOne;
    }
    void OnSwitched(object sender, System.EventArgs e)
    {
      this.ChangeDisplayAsync();
    }
    bool messageOne;
    LcdManager displayManager;
    SwitchManager switchManager;
  }
}

And that gives me a little button that changes the display and could (e.g.) be the start of a simple menu/control system. The short video below shows that working;

Windows 10, UWP and DeviceFamily-Specific Resource Dictionaries

A small thing for a short post – as you no doubt know by now, with Windows 10 and the Universal Windows Platform you can perhaps;

  • write a specific app for a device family (e.g. mobile, desktop, team, xbox, etc).
  • write apps that span device families, maybe even all of them.

for the latter, there’s probably going to come a point where you need to adapt either;

  • the UX to cope with device families having different input/output mechanisms – e.g. keyboards, mice, pen, touch, game controllers, gaze, gesture, speech, etc. and then big/small/multiple screens (or no screens) and so on.
  • the functionality to cope with device families running different variants of Windows built on a common core which then expose different ‘contracts’ through which are exposed different sets of APIs.

for the former, there are new ways (in XAML) to adjust the UI such that it responds nicely to different sizes of screen and so on through techniques like;

  • the new RelativePanel
  • new options in Visual State Management like automatically responding to size changes (via the AdaptiveTrigger) and driving changes by changing settings (including relative positioning of elements in a RelativePanel)

and XAML’s use of effective pixels helps a lot with its ‘abstraction’ of pixel size based on some combination of the device’s [resolution, physical size, viewing distance].

It’s perhaps better to respond to ‘screen size’ than it is to tie behaviour directly to device family because it’s getting harder to know what sort of hardware a device in a particular device-family might have.

This seems reasonably obvious for a device family like ‘desktop’ – we’re very used to ‘desktops’ suddenly growing a 2nd or 3rd monitor of different resolutions and pixel density or magically acquiring a mouse or keyboard or similar. The hardware is far from fixed.

For devices in other families like ‘mobile’ it feels like it might be ok to assume [‘mobile’ == small screen, one screen, touch input] but then that set of assumptions isn’t so likely to hold true in a world of Windows 10 with its ‘Continuum for Phones’ feature where, suddenly, such a device can have a large monitor and mouse attached to it.

For those scenarios where you’ve decided it is ‘ok’ to tie a particular piece of UI to a device-family, the Windows ‘Modern Resource Technology’ comes into play in that you can put ‘assets’ into your project under particular naming schemes that effectively means they are only applicable to a particular device family and the packaging and deployment bits do smart things to make sure that (only) the right bits turn up on the right devices.

I’ve illustrated that idea before in quite a few places (recently at the end of the video in this post and also in the video for this post) but I made a simple demo today that combined this with resource files that I thought I’d share here.

Here’s my ‘demo app’ running on desktop and mobile;

Capture

this is from a blank project where I’ve just set up a datacontext in my code behind;

using System;
using System.Linq;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

namespace App4
{
  public sealed partial class MainPage : Page
  {
    public MainPage()
    {
      this.InitializeComponent();

      this.Loaded += OnLoaded;
    }

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
      Random r = new Random();

      this.DataContext =
        Enumerable.Range(1, 100).Select(
          i =&gt;
            new SolidColorBrush(
              Windows.UI.Color.FromArgb(
                0xFF,
                (byte)r.Next(0, 255),
                (byte)r.Next(0, 255),
                (byte)r.Next(0, 255))));
    }
  }
}

so, it’s just a list of random colours and then I have a XAML page that binds to that in a GridView;

<Page
    x:Class='App4.MainPage'
    xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
    xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
    xmlns:local='using:App4'
    xmlns:d='http://schemas.microsoft.com/expression/blend/2008'
    xmlns:mc='http://schemas.openxmlformats.org/markup-compatibility/2006'
    mc:Ignorable='d'>
    <Grid Background='{ThemeResource ApplicationPageBackgroundThemeBrush}'>
    <GridView ItemsSource='{Binding}' ItemTemplate='{StaticResource myTemplate}'>
    </GridView>
    </Grid>
</Page>

and then I have an App.xaml to bring in a resources dictionary;

<Application
    x:Class='App4.App'
    xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
    xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
    xmlns:local='using:App4'
    RequestedTheme='Light'>
  <Application.Resources>
    <ResourceDictionary>
      <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source='Resources.xaml'/>
      </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
  </Application.Resources>
</Application>

and, of course, the very small trick is to have 2 of these Resource files on a per-device-family basis;

Capture

with one containing a square based template and the other a circle based one;

<ResourceDictionary
    xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
    xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
    xmlns:local='using:App4.DeviceFamily_Desktop'>
  <DataTemplate x:Key='myTemplate'>
    <Rectangle Width='100' Height='100' Fill='{Binding}'/>
  </DataTemplate>
</ResourceDictionary>

and

<ResourceDictionary
    xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
    xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
    xmlns:local='using:App4.DeviceFamily_Mobile'>
  <DataTemplate x:Key='myTemplate'>
    <Ellipse Width='100' Height='100' Fill='{Binding}'/>
  </DataTemplate>
</ResourceDictionary>

and the job’s done.

I know, it’s only a small thing and it’s an ‘obvious’ thing but I thought it was a kind of ‘neat’ thing. Naturally, if I just wanted to just change something like the size per device-family (not sure I would) then I could just define some XAML resources for those sizes and pick them up as resource on a per device-family basis rather than change the whole template as I do here.

Update – thinking about this a little more, there’s another way in which I could do this which is via Visual States. That is, I could write just one XAML file;

<Page
    x:Class='App4.MainPage'
    xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
    xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
    xmlns:local='using:App4'
    xmlns:d='http://schemas.microsoft.com/expression/blend/2008'
    xmlns:mc='http://schemas.openxmlformats.org/markup-compatibility/2006'
    mc:Ignorable='d'>
  <Page.Resources>
    <DataTemplate x:Key='square'>
      <Ellipse Width='100' Height='100' Fill='{Binding}'/>
    </DataTemplate>
    <DataTemplate x:Key='round'>
      <Rectangle Width='100' Height='100' Fill='{Binding}'/>
    </DataTemplate>
  </Page.Resources>
  <Grid Background='{ThemeResource ApplicationPageBackgroundThemeBrush}'>
    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup x:Name='stateGroups'>
        <VisualState x:Name='Default'/>
        <VisualState x:Name='Mobile'>
          <VisualState.StateTriggers>
            <local:DeviceFamilyTrigger DeviceFamily='Windows.Team'/>
          </VisualState.StateTriggers>
          <VisualState.Setters>
            <Setter Target='gridview.ItemTemplate' Value='{StaticResource round}'/>
          </VisualState.Setters>
        </VisualState>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    <GridView x:Name='gridview' ItemsSource='{Binding}'
              ItemTemplate='{StaticResource square}'>
    </GridView>
  </Grid>
</Page>

and that contains both XAML DataTemplates for the mobile device family and any other families and relies on a custom trigger to switch based on device family (there are a few of these on the web already);

namespace App4
{
  using Windows.System.Profile;
  using Windows.UI.Xaml;

    public class DeviceFamilyTrigger : StateTriggerBase
    {
        private string _deviceFamily;

        public string DeviceFamily
        {
            get
            {
                return _deviceFamily;
            }
            set
            {
                _deviceFamily = value;
                CheckValue();
            }
        }
        private void CheckValue()

        {
            SetActive(AnalyticsInfo.VersionInfo.DeviceFamily == DeviceFamily);

        }

        public DeviceFamilyTrigger()

        {
            CheckValue();
        }
    }
}

and that’s another way of achieving the same thing without having to partition into multiple resource files.

Windows 10, UWP, Raspberry PI 2, AllJoyn and Lightbulb Demo

Just bringing together a few recent posts;

Windows 10–Simple Universal Windows Platform Demo App

Windows 10 IoT Core, UWP and Raspberry Pi 2–The Switched Blinking Light Demo

Windows 10 IoT Core–Network Enabling the Blinking LED Demo with AllJoyn

Windows 10 IoT Core–More Baby Steps with AllJoyn

I brought some of that into one little screencast video of ‘an app’ which does (in a very basic form);

  • Code which is both universal and (at times) device-family specific running on desktop, phone and IoT Core
  • UI which responds to device changes
  • A simple service which advertises itself on AllJoyn
  • A simple client which consumes that service

Here’s the video;