Witmotion Servo Control Reverse Engineering
Using the Witmotion 16-channel Servo board’s native USB port the way it should be used
I wanted to control some servos from a computer. Often this from a microcontroller, but since my application is going to be entirely computer-controlled, I prefer to use a board built specifically for the purpose, rather than having to do any custom wiring. There are several such boards that have a microcontroller, servo pins, and power distribution, intended for beign controlled directly from a computer, such as the Pololu Micro Maestro 6-channel servo board which I previously used.
The Witmotion 16-channel servo board
This time, I went with a different board, a Witmotion 16-channel servo board. A board that initially attracted me because it used an STM32 with both USB, and UART headers.
However, to my dismay, unlike the Micro Maestro, the Witmotion board didn’t enumerate as a serial port, and didn’t come with any SDKs either. Wit’s website provides some sample code for other microcontrollers to talk to this board over UART, but no code for running on a computer.
I dislike VCOM in general — it’s always a challenge when you have multiple devices connected to the computer and have to work out which COM port is which. But not providing an SDK is also bad. Why provide a USB port but not the means to use it with anything other than a small utility for manually moving servos with sliders?
The manual even goes as far as to suggests that you should plug in a TTL serial module into the UART socket of the board rather than controlling it from the built-in USB port if you want to control it from a computer.
However, it is clear that the device does actually support USB control over its native USB port. The small Windows utility provided by the manufacturer does just that; and it can be seen in Device Manager that the board presents itself as a USB HID device.
USB Descriptors
So I did what any reasonable person would do, and reverse-engineer the USB protocol so that I could write my own drivers for it. I don’t do this lightly, but I was basing this on prior experience with Cortex-M3 microcontrollers with built-in USB that enumerate as HID — their protocols tend to be straightforward.
To do begin, and to confirm my theories, I needed to take a look at the USB descriptors, which describe what kind of data is expected to be sent and received.
This screen tells me there very important pieces of information:
- the Wit-motion controller has VID 0x1920 and PID 0x0100, I will need this info when writing the library
- The Wit-motion controller uses a 64 byte IN interrupt transfer at endpoint 2
- The Wit-motion controller uses a 64-byte OUT interrupt transfer at endpoint 1
This is good news. With only two interrupt endpoints (one for IN, and one for OUT) this should be relatively simple. However they are both 64 bytes long. Since the device has a UART port and a protocol to use on there, my initial guess is that they simply use these 64 bytes to encapsulate this other protocol, rather than doing it in proper USB style.
Having one protocol encapsulate another, and not using any advanced features of the first protocol is relatively common. For example uBlox GPS modules do a similar thing for their I2C bus — instead of using proper I2C addresses and data, it simply puts the same UART serial stream onto I2C.
USB Protocol analysis
So we need to snoop further to confirm the protocol. To do that, I use a USB analyzer to look at the actual raw data going through these endpoints, while I fire up the windows utility provided by Wit.
After repeating this experiment a few times, I’ve broken down the exchanges into a few different situations:
Starting up:
When the Windows utility starts, it connects to the USB and sends three messages:
- OUT: 03 02 02 03
- OUT: 03 02 03 03
- OUT: 03 02 04 03
It’s unclear what or why it does this, but it reliably does this every startup. There’s no data returned from the device, so in the worst case I can have my driver blindly output these as well if they are needed.
Idle/Heartbeat:
The software performs what appears to be either a heartbeat or a status request every second:
- OUT: 05 03 ff 00 12
- IN: 05 01 ff f0 12 <followed by some more data>
Every outgoing heartbeat or status request is accompanied with a reply that contains the same 5 bytes plus some more complex data. The incoming data is probably some status data from the device itself. Fortunately, as it’s likely purely informational, and since servos are open-loop control anyway, we shouldn’t need to decode any of this (it would be much more challenging if we had to)
Moving a slider:
When moving a slider around, it can be seen that the app sends a single outgoing message:
- OUT: 05 03 ff 02 01 f1 06
From the third byte onwards, this matches with the message displayed in the bottom left corner of the Windows app, as well as the UART data format provided in the documentation
So this reveals the way the protocol works is simply a 0x05 0x03 header indicating that what follows is a UART protocol, so I assume the microcontroller simply decodes the remainder of the message as it would do for data coming in by UART. If this is true then we can easily guess how the other commands listed in the manual should look.
Trying it out
Since I’m going to be using Python, and since this device uses HID API, it is easier to build on top of an HID API library, such as cython-hidapi.
A quick test in the command line shows a successful connection, and poking the heartbeat/status message causes the blue LED to flash on the device as they do when the Windows client is connected, so it would seem the startup commands were not necessarily needed.
It would seem that the device doesn’t even need the heartbeat either, and is quite happy just being sent position control commands straight from power-up. This means we don’t even have to send a heartbeat (though doing so provides us with the flashing blue LED, useful for feedback that the host is connected).
Writing the driver
Writing the driver is now a simple case of sticking these in a class, with some input validation, and connect/disconnects.
The code is available at https://github.com/meseta/py-witmotion-servo