Using Optimus Mini Three with .net

Published 13 July 07 12:00 PM | Coding4Fun 
  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.
    Filed under: ,

    Comment Notification

    If you would like to receive an email when updates are made to this post, please register here

    Subscribe to this post's comments using RSS

    Comments

    # om3 controller » Coding4Fun article said on July 14, 2007 11:29 AM:

    PingBack from http://optimus.toolz.at/2007/07/14/coding4funarticle/

    # Coding4Fun said on December 5, 2007 8:31 PM:

    If you're not sure what you need for this holiday season, look no further. After a year of coding and

    Leave a Comment

    (required) 
    (optional)
    (required) 

      
    Enter Code Here: Required

    Search

    This Blog

    Syndication

    Page view tracker