Using Optimus Mini Three with .net

Sign in to queue

Description

  Even though the device is connected to an usb port the communication is done through a (virtual) serial port. The protocol specification is available in the developer section on Art.Lebedev Studio's website. There's also some c sample code available which proved to be very useful to build this class. The document lists the following commands to send data to the device:
  • Switch the device on and off
  • Send image data for a specific key
  • Show sent image for a specific key
  • Change the brightness
There're two additional commands available, to read and write the internal id of the device. We're not going to use them for this article. They could be used if you've multiple devices connected to differentiate them.

All the commands we're sending have a length of 197 bytes, the last byte being the checksum. The response to a command consists of two bytes: a 0 to tell us this is a command confirmation and as second byte the checksum. Ideally that checksum matches the one of the sent command - if not the data was somehow corrupted and we've to send it again.

And the device is not just waiting until we send it something, it will also send something to us: the keys which are currently pressed - of course, wouldn't qualify as keyboard otherwise. These messages are also 2 bytes long, first byte being a 1 and the second byte is the 1-based index of the pressed key. We can get them anytime and very often - as long as one or more keys are pressed they're repeatedly send. When commands are send at the same time we get a mix of key messages and command confirmations.


Difficulty: Intermediate
Time Required: 1-3 hours
Cost: Free
Software: Optimus mini three software
Hardware: Optimus mini three keyboard
Download: Download

Getting started: the serial port

First we need to know the serial port to connect to. Usb devices are stored in the windows registry in HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\USB, and the device id we're looking for is Vid_067b&Pid_2303. Below that key can be multiple nodes, depending on how often and which usb ports you've plugged in the device.

Below each node is a sub node called Device Parameters and there we find what we're looking for: the key PortName with the serial port name as value. We're additionally verifying that the serial port actually exists. It can happen that the PortName key is set, but the port doesn't exist because it's an old/inactive registry entry from a previous connection.

C#

   1:  private static string GetPort()
   2:  {
   3:    string result = null;
   4:   
   5:    // Get all active ports
   6:    List<string> ports = new List<string>();
   7:    ports.AddRange(SerialPort.GetPortNames());
   8:    if (ports.Count == 0) { return null; }
   9:   
  10:    // Get root
  11:    string rootName = "SYSTEM\\CurrentControlSet\\Enum\\USB\\Vid_067b&Pid_2303";
  12:    RegistryKey root = Registry.LocalMachine.OpenSubKey(rootName);
  13:    if (root == null) { return null; }
  14:   
  15:    // Get all keys below root - there can be several of them if the
  16:    // device was connected to different usb ports
  17:    string[] devices = root.GetSubKeyNames();
  18:    if (devices == null) { return null; }
  19:   
  20:    // Loop through all devices and get first active
  21:    foreach (string deviceKey in devices)
  22:    {
  23:      RegistryKey device = root.OpenSubKey(deviceKey + "\\Device Parameters");
  24:      if (device == null) { continue; }
  25:   
  26:      object portValue = device.GetValue("PortName");
  27:      if (portValue == null) { continue; }
  28:   
  29:      // Check if that port is active
  30:      string port = (string)portValue;
  31:      if (ports.Contains(port))
  32:      {
  33:        result = port;
  34:        break;
  35:      }
  36:    }
  37:    // Result
  38:    return result;
  39:  }

VB

   1:  Private Shared Function GetPort() As String
   2:   
   3:    Dim result As String = Nothing
   4:   
   5:    'Get all active ports
   6:    Dim ports As List(Of String) = New List(Of String)
   7:    ports.AddRange(SerialPort.GetPortNames())
   8:    If (ports.Count = 0) Then
   9:      Return Nothing
  10:    End If
  11:   
  12:    'Get root
  13:    Dim rootName As String = "SYSTEM\\CurrentControlSet\\Enum\\USB\\Vid_067b&Pid_2303"
  14:    Dim root As RegistryKey = Registry.LocalMachine.OpenSubKey(rootName)
  15:    If (root Is Nothing) Then
  16:      Return Nothing
  17:    End If
  18:   
  19:    'Get all keys below root - there can be several of them if the
  20:    'device was connected to different usb ports
  21:    Dim devices() As String = root.GetSubKeyNames()
  22:    If (devices Is Nothing) Then
  23:      Return Nothing
  24:    End If
  25:   
  26:    'Loop through all devices and get first active
  27:    Dim deviceKey As String
  28:    For Each deviceKey In devices
  29:      Dim device As RegistryKey = root.OpenSubKey(deviceKey + "\\Device Parameters")
  30:      If (device Is Nothing) Then
  31:        Continue For
  32:      End If
  33:   
  34:      Dim portValue As Object = device.GetValue("PortName")
  35:      If (portValue Is Nothing) Then
  36:        Continue For
  37:      End If
  38:   
  39:      'Check if that port is active
  40:      Dim port As String = CType(portValue, String)
  41:      If (ports.Contains(port)) Then
  42:        result = port
  43:        Exit For
  44:      End If
  45:    Next
  46:   
  47:    'Result
  48:    Return result
  49:   
  50:  End Function

Connecting to the device

Now that we know the port name we can open it. After that we add an handler for the DataReceived event and tell the background thread to start working.

C#

   1:  private bool _Connected;
   2:  private SerialPort _Port;
   3:  private Thread _ProcessCommandsThread;
   4:   
   5:  public bool Init()
   6:  {
   7:    // If already connected exit
   8:    if (_Connected) { return true; }
   9:   
  10:    // Get port name where the device is connected
  11:    string port = GetPort();
  12:    if (string.IsNullOrEmpty(port)) { return false; }
  13:   
  14:    // Open port
  15:    _Port = new SerialPort(port);
  16:    _Port.BaudRate = 1000000;
  17:    _Port.DataBits = 8;
  18:    _Port.Open();
  19:    _Connected = true;
  20:   
  21:    // Add event handler for DataReceived
  22:    _Port.DataReceived += new SerialDataReceivedEventHandler(PortDataReceived);
  23:   
  24:    // Start command thread
  25:    _ProcessCommandsThread = new Thread(ProcessCommands);
  26:    _ProcessCommandsThread.Start();
  27:   
  28:    // Successfully connected
  29:    return true;
  30:  }
  31:   

VB

   1:  Private _Connected As Boolean
   2:  Private _Port As SerialPort
   3:  Private _ProcessCommandsThread As Thread
   4:   
   5:  Public Function Init() As Boolean
   6:    'If already connected exit
   7:    If (_Connected) Then Return True
   8:   
   9:    ' Get port name where the device is connected
  10:    Dim port As String = GetPort()
  11:    If (String.IsNullOrEmpty(port)) Then Return False
  12:   
  13:    'Open port
  14:    _Port = New SerialPort(port)
  15:    _Port.BaudRate = 1000000
  16:    _Port.DataBits = 8
  17:    _Port.Open()
  18:    _Connected = True
  19:   
  20:    'Add event handler for DataReceived
  21:    AddHandler _Port.DataReceived, AddressOf PortDataReceived
  22:   
  23:    'Start command thread
  24:    _ProcessCommandsThread = New Thread(AddressOf ProcessCommands)
  25:    _ProcessCommandsThread.Start()
  26:   
  27:    'Successfully connected
  28:    Return True
  29:  End Function

Handle incoming data

This handler added in Init will be called asynchronously as soon as there's data in the input buffer. We'll only get byte pairs from the device which makes parsing very easy - a pair is either a command confirmation (0 followed by checksum) or a key message (1 followed by the 1-based key index). A wait handle is used to signal our background thread that a command confirmation was received. If we received a key message we'll call our RaiseKeyDown method.

C#

   1:  private byte _LastCommandChecksum;
   2:  private EventWaitHandle _CommandWaitHandle;
   3:   
   4:  private void PortDataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
   5:  {
   6:    // If we've not at least 2 bytes no need to read
   7:    int length = _Port.BytesToRead;
   8:    if (length < 2) { return; }
   9:   
  10:    // We need byte pairs, single byte at the end is not read
  11:    int pairs = length / 2;
  12:    byte[] data = new byte[pairs * 2];
  13:    _Port.Read(data, 0, pairs * 2);
  14:   
  15:    // Parse data
  16:    bool commandReceived = false;
  17:    bool[] keyReceived = new bool[3];
  18:    for (int i = 0; i < pairs; i++)
  19:    {
  20:      byte dataType = data[i * 2];
  21:      byte dataValue = data[i * 2 + 1];
  22:   
  23:      if (dataType == 0)
  24:      {
  25:        // Command confirmation
  26:        _LastCommandChecksum = dataValue;
  27:        commandReceived = true;
  28:      }
  29:      else
  30:      {
  31:        // Key message
  32:        if (dataValue >= 1 && dataValue <= 3)
  33:        {
  34:          keyReceived[dataValue - 1] = true;
  35:        }
  36:      }
  37:    }
  38:   
  39:    // If a command confirmation was received notify wait handle
  40:    if (commandReceived && _CommandWaitHandle != null) { _CommandWaitHandle.Set(); }
  41:   
  42:    // If key messages were received raise event
  43:    for (byte i = 0; i <= 2; i++)
  44:    {
  45:      if (keyReceived[i]) { RaiseKeyDown(i); }
  46:    }
  47:  }

VB

   1:  Private _LastCommandChecksum As Byte
   2:  Private _CommandWaitHandle As EventWaitHandle
   3:   
   4:  Private Sub PortDataReceived(ByVal sender As Object, ByVal e As SerialDataReceivedEventArgs)
   5:    'If we've not at least 2 bytes no need to read
   6:    Dim length As Integer = _Port.BytesToRead
   7:    If (length < 2) Then Exit Sub
   8:   
   9:    'We need byte pairs, single byte at the end is not read
  10:    Dim pairs As Integer = length \ 2
  11:    Dim data() As Byte = New Byte(pairs * 2) {}
  12:    _Port.Read(data, 0, pairs * 2)
  13:   
  14:    'Parse data
  15:    Dim commandReceived As Boolean = False
  16:    Dim keyReceived() As Boolean = New Boolean(3) {}
  17:    Dim i As Integer
  18:    For i = 0 To pairs - 1 Step i + 1
  19:      Dim dataType As Byte = data(i * 2)
  20:      Dim dataValue As Byte = data(i * 2 + 1)
  21:   
  22:      If (dataType = 0) Then
  23:        'Command confirmation
  24:        _LastCommandChecksum = dataValue
  25:        commandReceived = True
  26:      Else
  27:        'Key message
  28:        If (dataValue >= 1 And dataValue <= 3) Then
  29:          keyReceived(dataValue - 1) = True
  30:        End If
  31:      End If
  32:    Next
  33:   
  34:    'If a command confirmation was received notify wait handle
  35:    If (commandReceived And Not _CommandWaitHandle Is Nothing) Then
  36:      _CommandWaitHandle.Set()
  37:    End If
  38:   
  39:    'If key messages were received raise event
  40:    Dim j As Byte
  41:    For j = 0 To 2
  42:      If (keyReceived(j)) Then
  43:        RaiseKeyDown(j)
  44:      End If
  45:    Next
  46:  End Sub

Key down event

This method is called from the DataReceived handler and raises OnKeyDown events when we received key messages. Now we know that we can get them at a very fast rate and we shouldn't raise an event for every single message. We'll restrict it to only raise the event once if the key was not pressed in the last 100 ms. I came up with the 100ms after some testing - sometimes there're just a few ms between the key messages, but from time to time the gap is up to 70.

C#

   1:  private int[] _LastKeyMessageOn = new int[3];
   2:  public delegate void KeyDownEventHandler(byte keyIndex);
   3:  public event KeyDownEventHandler OnKeyDown;
   4:   
   5:  private void RaiseKeyDown(byte keyIndex)
   6:  {
   7:    int current = Environment.TickCount;
   8:    if (current - _LastKeyMessageOn[keyIndex] >= 100)
   9:    {
  10:      // Time to raise event
  11:      if (OnKeyDown != null) { OnKeyDown(keyIndex); }
  12:    }
  13:    _LastKeyMessageOn[keyIndex] = current;
  14:  }

VB

   1:  Private _LastKeyMessageOn() As Integer = New Integer(3) {}
   2:  Public Event OnKeyDown(ByVal keyIndex As Byte)
   3:   
   4:  Private Sub RaiseKeyDown(ByVal keyIndex As Byte)
   5:    Dim current As Integer = Environment.TickCount
   6:    If (current - _LastKeyMessageOn(keyIndex) >= 100) Then
   7:      'Time to raise event
   8:      RaiseEvent OnKeyDown(keyIndex)
   9:    End If
  10:    _LastKeyMessageOn(keyIndex) = current
  11:  End Sub

Sending commands

This method takes a byte array as parameter and writes it to the output buffer of the serial port. Once sent it will wait up to a second at the handle we defined above for a command confirmation. If the confirmation arrives within time it'll compare the checksum. If they don't match or if there was a time out the command is send again, up to 3 times.

C#

   1:  private const int COMMAND_LENGTH = 197;
   2:  private const int COMMAND_LAST = 196;
   3:   
   4:  private bool SendCommand(byte[] command)
   5:  {
   6:    bool success = false;
   7:    int triesLeft = 3;
   8:   
   9:    while (!success && triesLeft > 0)
  10:    {
  11:      _Port.Write(command, 0, COMMAND_LENGTH);
  12:      _CommandWaitHandle = new System.Threading.AutoResetEvent(false);
  13:      if (_CommandWaitHandle.WaitOne(1000, false))
  14:      {
  15:        _CommandWaitHandle = null;
  16:        if (_LastCommandChecksum == command[COMMAND_LAST])
  17:        {
  18:          // Success
  19:          success = true;
  20:          break;
  21:        }
  22:        else
  23:        {
  24:          // Failed
  25:          triesLeft -= 1;
  26:        }
  27:      }
  28:      else
  29:      {
  30:        // Failed
  31:        triesLeft -= 1;
  32:      }
  33:    }
  34:   
  35:    return success;
  36:  }

VB

   1:  Private Const COMMAND_LENGTH As Integer = 197
   2:  Private Const COMMAND_LAST As Integer = 196
   3:   
   4:  Private Function SendCommand(ByVal command As Byte()) As Boolean
   5:    Dim success As Boolean = False
   6:    Dim triesLeft As Integer = 3
   7:   
   8:    While Not success And triesLeft > 0
   9:      _Port.Write(command, 0, COMMAND_LENGTH)
  10:      _CommandWaitHandle = New System.Threading.AutoResetEvent(False)
  11:      If (_CommandWaitHandle.WaitOne(1000, False)) Then
  12:        _CommandWaitHandle = Nothing
  13:        If (_LastCommandChecksum = command(COMMAND_LAST)) Then
  14:          'Success
  15:          success = True
  16:          Exit While
  17:        Else
  18:          'Failed
  19:          triesLeft -= 1
  20:        End If
  21:      Else
  22:        'Failed
  23:        triesLeft -= 1
  24:      End If
  25:    End While
  26:   
  27:    Return success
  28:  End Function

The commands

All commands we want to execute are added to a queue. That queue is a FIFO type - first in, first out. It's filled by the methods described below and emptied by the background thread. Because two different threads can modify the queue at the same time we need to synchronize it. The easiest way to do that is by using the lock statement to just let one thread in at a time.

Let's start with the simple commands. For better readability the possible brightness values of low, normal and high (20, 40 and 60) are added as enumeration. The keyIndex parameter is 0-based - 0 is the key on the side with the usb cable.

C#

   1:  private Queue<byte[]> _CommandQueue = new Queue<byte[]>();
   2:  public const int SCREEN_SIZE = 96;
   3:  public enum Brightness
   4:  {
   5:    Low = 20,
   6:    Normal = 40,
   7:    High = 60
   8:  }
   9:   
  10:  public void SwitchOn()
  11:  {
  12:    byte[] command = new byte[COMMAND_LENGTH];
  13:    command[0] = 2;
  14:    command[COMMAND_LAST] = 2;
  15:    lock (_CommandQueue)
  16:    {
  17:      _CommandQueue.Enqueue(command);
  18:    }
  19:  }
  20:  public void SwitchOff()
  21:  {
  22:    lock (_CommandQueue)
  23:    {
  24:      _CommandQueue.Enqueue(CreateSwitchOffCommand());
  25:    }
  26:  }
  27:  private byte[] CreateSwitchOffCommand()
  28:  {
  29:    byte[] command = new byte[COMMAND_LENGTH];
  30:    command[0] = 3;
  31:    command[COMMAND_LAST] = 3;
  32:    return command;
  33:  }
  34:  public void SetBrightness(Brightness brightness)
  35:  {
  36:    byte[] command = new byte[COMMAND_LENGTH];
  37:    command[0] = 9;
  38:    command[1] = (byte)brightness;
  39:    command[COMMAND_LAST] = (byte)(command[0] + command[1]);
  40:    lock (_CommandQueue)
  41:    {
  42:      _CommandQueue.Enqueue(command);
  43:    }
  44:  }
  45:  public void ShowImage(byte keyIndex)
  46:  {
  47:    lock (_CommandQueue)
  48:    {
  49:      _CommandQueue.Enqueue(CreateShowImageCommand(keyIndex));
  50:    }
  51:  }
  52:  private byte[] CreateShowImageCommand(byte keyIndex)
  53:  {
  54:    byte[] command = new byte[COMMAND_LENGTH];
  55:    command[0] = 4;
  56:    command[1] = (byte)(keyIndex + 1);
  57:    command[COMMAND_LAST] = (byte)(command[0] + command[1]);
  58:    return command;
  59:  }

VB

   1:  Private _CommandQueue As Queue(Of Byte()) = New Queue(Of Byte())
   2:   
   3:  Public Const SCREEN_SIZE As Integer = 96
   4:  Public Enum Brightness
   5:    Low = 20
   6:    Normal = 40
   7:    High = 60
   8:  End Enum
   9:   
  10:  Public Sub SwitchOn()
  11:    Dim command() As Byte = New Byte(COMMAND_LENGTH) {}
  12:    command(0) = 2
  13:    command(COMMAND_LAST) = 2
  14:    SyncLock _CommandQueue
  15:      _CommandQueue.Enqueue(command)
  16:    End SyncLock
  17:  End Sub
  18:  Public Sub SwitchOff()
  19:    SyncLock _CommandQueue
  20:      _CommandQueue.Enqueue(CreateSwitchOffCommand())
  21:    End SyncLock
  22:  End Sub
  23:  Private Function CreateSwitchOffCommand() As Byte()
  24:    Dim command() As Byte = New Byte(COMMAND_LENGTH) {}
  25:    command(0) = 3
  26:    command(COMMAND_LAST) = 3
  27:    Return command
  28:  End Function
  29:  Public Sub SetBrightness(ByVal brightness As Brightness)
  30:    Dim command() As Byte = New Byte(COMMAND_LENGTH) {}
  31:    command(0) = 9
  32:    command(1) = CType(brightness, Byte)
  33:    command(COMMAND_LAST) = CType((command(0) + command(1)), Byte)
  34:    SyncLock _CommandQueue
  35:      _CommandQueue.Enqueue(command)
  36:    End SyncLock
  37:  End Sub
  38:  Public Sub ShowImage(ByVal keyIndex As Byte)
  39:    SyncLock _CommandQueue
  40:      _CommandQueue.Enqueue(CreateShowImageCommand(keyIndex))
  41:    End SyncLock
  42:  End Sub
  43:  Private Function CreateShowImageCommand(ByVal keyIndex As Byte) As Byte()
  44:    Dim command() As Byte = New Byte(COMMAND_LENGTH) {}
  45:    command(0) = 4
  46:    command(1) = CType((keyIndex + 1), Byte)
  47:    command(COMMAND_LAST) = CType((command(0) + command(1)), Byte)
  48:    Return command
  49:  End Function

A more complex command is required to send the image data. Actually we need to send 96 such commands because the data is send line by line. We need to access the passed bitmap pixel by pixel to put together the commands, so we'll first copy the bitmap into a byte array for faster access.

C#

   1:  public void SetImage(byte keyIndex, System.Drawing.Bitmap image)
   2:  {
   3:    // Copy image into array for faster processing
   4:    BitmapData imageData = image.LockBits(
   5:      new Rectangle(0, 0, SCREEN_SIZE, SCREEN_SIZE),
   6:      ImageLockMode.ReadOnly,
   7:      PixelFormat.Format24bppRgb);
   8:    int imageRgbLength = SCREEN_SIZE * SCREEN_SIZE * 3;
   9:    byte[] imageRgb = new byte[imageRgbLength];
  10:    System.Runtime.InteropServices.Marshal.Copy(imageData.Scan0, imageRgb, 0, imageRgbLength);
  11:    image.UnlockBits(imageData);
  12:   
  13:    // Convert image to commands
  14:    byte colorR, colorG, colorB;
  15:    int imageRgbIndex;
  16:    byte[] command;
  17:   
  18:    for (int y = 0; y < SCREEN_SIZE; y += 1)
  19:    {
  20:      command = new byte[COMMAND_LENGTH];
  21:      command[0] = 1;
  22:      command[1] = (byte)(keyIndex + 1);
  23:      command[2] = (byte)((192 * y) >> 8);
  24:      command[3] = (byte)((192 * y) - (command[2] << 8));
  25:      command[COMMAND_LAST] += (byte)(command[0] + command[1] + command[2] + command[3]);
  26:   
  27:      for (int x = 0; x < SCREEN_SIZE; x += 1)
  28:      {
  29:        imageRgbIndex = y * SCREEN_SIZE * 3 + x * 3;
  30:   
  31:        colorR = imageRgb[imageRgbIndex + 2];
  32:        colorG = imageRgb[imageRgbIndex + 1];
  33:        colorB = imageRgb[imageRgbIndex];
  34:   
  35:        command[4 + x * 2] = (byte)((colorR & 0xF8) + (colorG >> 5));
  36:        command[5 + x * 2] = (byte)((colorB >> 3) + ((colorG & 0x1C) << 3));
  37:   
  38:        command[COMMAND_LAST] += command[4 + x * 2];
  39:        command[COMMAND_LAST] += command[5 + x * 2];
  40:      }
  41:   
  42:      lock (_CommandQueue)
  43:      {
  44:        _CommandQueue.Enqueue(command);
  45:      }
  46:    }
  47:  }

VB

   1:  Public Sub SetImage(ByVal keyIndex As Byte, ByVal image As Bitmap)
   2:    'Copy image into array for faster processing
   3:    Dim imageData As BitmapData = image.LockBits( _
   4:      New Rectangle(0, 0, SCREEN_SIZE, SCREEN_SIZE), _
   5:      ImageLockMode.ReadOnly, _
   6:      PixelFormat.Format24bppRgb)
   7:    Dim imageRgbLength As Integer = SCREEN_SIZE * SCREEN_SIZE * 3
   8:    Dim imageRgb() As Byte = New Byte(imageRgbLength) {}
   9:    System.Runtime.InteropServices.Marshal.Copy(imageData.Scan0, imageRgb, 0, imageRgbLength)
  10:    image.UnlockBits(imageData)
  11:   
  12:    'Convert image to commands
  13:    Dim colorR As Byte, colorG As Byte, colorB As Byte
  14:    Dim imageRgbIndex As Integer
  15:    Dim command() As Byte
  16:   
  17:    Dim y As Integer
  18:    For y = 0 To SCREEN_SIZE - 1
  19:      command = New Byte(COMMAND_LENGTH) {}
  20:      command(0) = 1
  21:      command(1) = CByte(keyIndex + 1)
  22:      command(2) = CByte((192 * y) >> 8)
  23:      command(3) = CByte((192 * y) And &HFF)
  24:   
  25:      command(COMMAND_LAST) = CByte((CShort(command(COMMAND_LAST)) + command(0)) And &HFF)
  26:      command(COMMAND_LAST) = CByte((CShort(command(COMMAND_LAST)) + command(1)) And &HFF)
  27:      command(COMMAND_LAST) = CByte((CShort(command(COMMAND_LAST)) + command(2)) And &HFF)
  28:      command(COMMAND_LAST) = CByte((CShort(command(COMMAND_LAST)) + command(3)) And &HFF)
  29:   
  30:      Dim x As Integer
  31:      For x = 0 To SCREEN_SIZE - 1
  32:        imageRgbIndex = y * SCREEN_SIZE * 3 + x * 3
  33:   
  34:        colorR = imageRgb(imageRgbIndex + 2)
  35:        colorG = imageRgb(imageRgbIndex + 1)
  36:        colorB = imageRgb(imageRgbIndex)
  37:   
  38:        command(4 + x * 2) = CByte(((colorR And &HF8) + (colorG >> 5)))
  39:        command(5 + x * 2) = CByte(((colorB >> 3) + ((colorG And &H1C) << 3)))
  40:   
  41:        command(COMMAND_LAST) = CByte((CShort(command(COMMAND_LAST)) + command(4 + x * 2)) And &HFF)
  42:        command(COMMAND_LAST) = CByte((CShort(command(COMMAND_LAST)) + command(5 + x * 2)) And &HFF)
  43:      Next
  44:   
  45:      SyncLock _CommandQueue
  46:        _CommandQueue.Enqueue(command)
  47:      End SyncLock
  48:    Next
  49:   
  50:  End Sub

There's some shifting taking place because the device requires 16 bit colors - 5 bits red, 6 bits green and another 5 bits for blue. The VB version has some additional code to be not dependent on the Remove integer overflow checks project setting.

The background thread

Everything is in place to fill up the queue with commands, time to add the background thread code to empty it. The task of the thread is quite simple: check if there're any queued commands and send them until we tell him to stop. Additionally it sends show commands every 5 seconds. That's done because the device would switch off automatically after about 10 seconds if there were no commands.

C#

   1:  private void ProcessCommands()
   2:  {
   3:    bool exit = false;
   4:    int lastShowOn = Environment.TickCount;
   5:   
   6:    while (_Connected && !exit)
   7:    {
   8:      // Time for regular refresh?
   9:      if (Environment.TickCount - lastShowOn > 5000)
  10:      {
  11:        for (byte i = 0; i <= 2; i++)
  12:        {
  13:          if (!SendCommand(CreateShowImageCommand(i))) { break; }
  14:        }
  15:        lastShowOn = Environment.TickCount;
  16:      }
  17:   
  18:      // If we've commands ...
  19:      if (_CommandQueue.Count > 0)
  20:      {
  21:        while (_CommandQueue.Count > 0)
  22:        {
  23:          // ... process them
  24:          byte[] command;
  25:          lock (_CommandQueue)
  26:          {
  27:            command = _CommandQueue.Dequeue();
  28:          }
  29:          if (!SendCommand(command))
  30:          {
  31:            exit = true;
  32:            break;
  33:          }
  34:        }
  35:      }
  36:      else
  37:      {
  38:        // No commands, time to relax
  39:       Thread.Sleep(10);
  40:      }
  41:    }
  42:  }

VB

   1:  Private Sub ProcessCommands()
   2:    Dim exitThread As Boolean = False
   3:    Dim lastShowOn As Integer = Environment.TickCount
   4:   
   5:    While _Connected And Not exitThread
   6:      'Time for regular refresh?
   7:      If (Environment.TickCount - lastShowOn > 5000) Then
   8:        Dim i As Byte
   9:        For i = 0 To 2
  10:          If (Not SendCommand(CreateShowImageCommand(i))) Then
  11:            Exit While
  12:          End If
  13:        Next
  14:        lastShowOn = Environment.TickCount
  15:      End If
  16:   
  17:      'If we've commands ...
  18:      If (_CommandQueue.Count > 0) Then
  19:        While _CommandQueue.Count > 0
  20:          '... process them
  21:          Dim command() As Byte
  22:          SyncLock _CommandQueue
  23:            command = _CommandQueue.Dequeue()
  24:          End SyncLock
  25:          If (Not SendCommand(command)) Then
  26:            exitThread = True
  27:            Exit While
  28:          End If
  29:        End While
  30:      Else
  31:        'No commands, time to relax
  32:        Thread.Sleep(10)
  33:      End If
  34:    End While
  35:   
  36:  End Sub

Turn off

The last method is used to turn the device off. We stop the background thread and before closing the port a switch off command is send.

C#

   1:  public void Terminate()
   2:  {
   3:    if (!_Connected) { return; }
   4:   
   5:    // Stop processing commands
   6:    if (_ProcessCommandsThread.IsAlive)
   7:    {
   8:      _ProcessCommandsThread.Abort();
   9:      _ProcessCommandsThread.Join(1000);
  10:    }
  11:   
  12:    // Switch off
  13:    SendCommand(CreateSwitchOffCommand());
  14:   
  15:    // Close port
  16:    _Port.Close();
  17:    _Connected = false;
  18:  }

VB

   1:  Public Sub Terminate()
   2:    If (Not _Connected) Then Exit Sub
   3:   
   4:    'Stop processing commands
   5:    If (_ProcessCommandsThread.IsAlive) Then
   6:      _ProcessCommandsThread.Abort()
   7:      _ProcessCommandsThread.Join(1000)
   8:    End If
   9:   
  10:    'Switch off
  11:    SendCommand(CreateSwitchOffCommand())
  12:   
  13:    'Close port
  14:    _Port.Close()
  15:    _Connected = False
  16:   
  17:  End Sub

Using this class

Now comes the most interesting part of this exercise: seeing the code in action. For this we'll add some code to the main method of the console application to display the RGB colors on the key and to print the key down events. Ok, not that cool yet, but should give you an idea how to use it.

C#

   1:  static void Main(string[] args)
   2:  {
   3:    OptimusMiniDevice device = new OptimusMiniDevice();
   4:   
   5:    device.OnKeyDown += new OptimusMiniDevice.KeyDownEventHandler(device_OnKeyDown);
   6:   
   7:    device.Init();
   8:    device.SwitchOn();
   9:    device.SetBrightness(OptimusMiniDevice.Brightness.Low);
  10:   
  11:    device.SetImage(0, GetColor(System.Drawing.Brushes.Red));
  12:    device.SetImage(1, GetColor(System.Drawing.Brushes.Green));
  13:    device.SetImage(2, GetColor(System.Drawing.Brushes.Blue));
  14:   
  15:    device.ShowImage(0);
  16:    device.ShowImage(1);
  17:    device.ShowImage(2);
  18:   
  19:    Console.ReadKey();
  20:   
  21:    device.SwitchOff();
  22:    device.Terminate();
  23:  }
  24:   
  25:  static System.Drawing.Bitmap GetColor(System.Drawing.Brush brush)
  26:  {
  27:    System.Drawing.Bitmap bitmap = new Bitmap(OptimusMiniDevice.SCREEN_SIZE, OptimusMiniDevice.SCREEN_SIZE);
  28:    System.Drawing.Graphics graphic = Graphics.FromImage(bitmap);
  29:    graphic.FillRectangle(brush, new Rectangle(0, 0, OptimusMiniDevice.SCREEN_SIZE, OptimusMiniDevice.SCREEN_SIZE));
  30:    graphic.Flush();
  31:    return bitmap;
  32:  }
  33:   
  34:  static void device_OnKeyDown(byte keyIndex)
  35:  {
  36:    Console.WriteLine(string.Format("key down {0}", keyIndex));
  37:  }

VB

   1:  Sub Main()
   2:   
   3:    Dim device As OptimusMiniDevice = New OptimusMiniDevice()
   4:   
   5:    AddHandler device.OnKeyDown, AddressOf device_OnKeyDown
   6:   
   7:    device.Init()
   8:    device.SwitchOn()
   9:    device.SetBrightness(OptimusMiniDevice.Brightness.Low)
  10:   
  11:    device.SetImage(0, GetColor(Brushes.Red))
  12:    device.SetImage(1, GetColor(Brushes.Green))
  13:    device.SetImage(2, GetColor(Brushes.Blue))
  14:   
  15:    device.ShowImage(0)
  16:    device.ShowImage(1)
  17:    device.ShowImage(2)
  18:   
  19:    Console.ReadKey()
  20:   
  21:    device.SwitchOff()
  22:    device.Terminate()
  23:   
  24:  End Sub
  25:   
  26:  Function GetColor(ByVal brush As Brush)
  27:    Dim bitmap As Bitmap = New Bitmap(OptimusMiniDevice.SCREEN_SIZE, OptimusMiniDevice.SCREEN_SIZE)
  28:    Dim graphic As Graphics = Graphics.FromImage(bitmap)
  29:    graphic.FillRectangle(brush, New Rectangle(0, 0, OptimusMiniDevice.SCREEN_SIZE, OptimusMiniDevice.SCREEN_SIZE))
  30:    graphic.Flush()
  31:    Return bitmap
  32:  End Function
  33:   
  34:  Sub device_OnKeyDown(ByVal keyIndex As Byte)
  35:    Console.WriteLine(String.Format("key down {0}", keyIndex))
  36:  End Sub

Conclusion

That's it, a pretty straightforward class and easy to use - and for sure extendable. This is the prototype i've built to play around with the device and i'm working on an improved version, but it's not quite ready for prime time yet (you can take a look on the source though, comments welcome). I hope you found the article interesting, my first one.

Bio

Harald has more than 5 years experience developing .net solutions and has been coding for fun for as long as he can remember. Works currently as an architect in the travel industry, building web based solutions. Outside the world of coding he is enjoying good books and lately he's working hard to improve his wii tennis skills. He can be reached through his website.

Tags:

Hardware, utility

The Discussion

Comments closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to send us feedback you can Contact Us.