Using Optimus Mini Three with .net

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.

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.

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.

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: privateint[] _LastKeyMessageOn = newint[3];

2: publicdelegatevoid KeyDownEventHandler(byte keyIndex);

3: publicevent KeyDownEventHandler OnKeyDown;

4:

5: privatevoid 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() AsInteger = NewInteger(3) {}

2: PublicEvent OnKeyDown(ByVal keyIndex AsByte)

3:

4: PrivateSub RaiseKeyDown(ByVal keyIndex AsByte)

5: Dim current AsInteger = Environment.TickCount

6: If (current - _LastKeyMessageOn(keyIndex) >= 100) Then

7: 'Time to raise event

8: RaiseEvent OnKeyDown(keyIndex)

9: EndIf

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: privateconstint COMMAND_LENGTH = 197;

2: privateconstint COMMAND_LAST = 196;

3:

4: privatebool 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: PrivateConst COMMAND_LENGTH AsInteger = 197

2: PrivateConst COMMAND_LAST AsInteger = 196

3:

4: PrivateFunction SendCommand(ByVal command AsByte()) AsBoolean

5: Dim success AsBoolean = False

6: Dim triesLeft AsInteger = 3

7:

8: WhileNot 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: ExitWhile

17: Else

18: 'Failed

19: triesLeft -= 1

20: EndIf

21: Else

22: 'Failed

23: triesLeft -= 1

24: EndIf

25: EndWhile

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.

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.

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: privatevoid 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: PrivateSub ProcessCommands()

2: Dim exitThread AsBoolean = False

3: Dim lastShowOn AsInteger = Environment.TickCount

4:

5: While _Connected AndNot exitThread

6: 'Time for regular refresh?

7: If (Environment.TickCount - lastShowOn > 5000) Then

8: Dim i AsByte

9: For i = 0 To 2

10: If (Not SendCommand(CreateShowImageCommand(i))) Then

11: ExitWhile

12: EndIf

13: Next

14: lastShowOn = Environment.TickCount

15: EndIf

16:

17: 'If we've commands ...

18: If (_CommandQueue.Count > 0) Then

19: While _CommandQueue.Count > 0

20: '... process them

21: Dim command() AsByte

22: SyncLock _CommandQueue

23: command = _CommandQueue.Dequeue()

24: EndSyncLock

25: If (Not SendCommand(command)) Then

26: exitThread = True

27: ExitWhile

28: EndIf

29: EndWhile

30: Else

31: 'No commands, time to relax

32: Thread.Sleep(10)

33: EndIf

34: EndWhile

35:

36: EndSub

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: publicvoid 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: PublicSub Terminate()

2: If (Not _Connected) ThenExitSub

3:

4: 'Stop processing commands

5: If (_ProcessCommandsThread.IsAlive) Then

6: _ProcessCommandsThread.Abort()

7: _ProcessCommandsThread.Join(1000)

8: EndIf

9:

10: 'Switch off

11: SendCommand(CreateSwitchOffCommand())

12:

13: 'Close port

14: _Port.Close()

15: _Connected = False

16:

17: EndSub

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.

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.