Protocol Packets

The Ambit protocol messages need to be transferred between application and watch. The transfer method of choice is that of USB-HID interrupt transfers. These interrupt transfers have one important requirement that needs to be satisfied: every single transfer must send a fixed amount of bytes. The value for that amount is available for each USB interrupt endpoint in its \(wMaxPacketSize\) field.

This means that messages can to be too short or too long for a single USB interrupt transfer. To address that, the Ambit protocol messages are “stuffed” into Ambit protocol packets. Each such protocol packet is exactly \(wMaxPacketSize\) bytes long. Protocol messages that are too short are padded, those that are too long are split up over multiple packets. Hence, messages always correspond to one or more packets. Conversely, each packet always contains (part of) a message.

Structure

For administrative purposes, each packet starts with a small header of its own followed by a cyclic redundancy check (CRC). Next comes a packet payload followed by a second CRC. The CRC values are included so that header and payload can be checked for errors. Left over space is padding and should be ignored. Each packet but the last holds as much data as will fit. Only the last packet for a message will have any padding.

Writing the packet structure down in Augmented Backus–Naur Form (ABNF) gives

packet         ::=  packet-header CRC packet-payload CRC padding
packet-header  ::=  marker offset type size index
packet-payload ::=  *OCTET
padding        ::=  *OCTET

Taking into account the sizes of the various components, the maximum payload size for a single packet works out to \(wMaxPacketSize - 10\) bytes.

Illustrating the layout for a 64 byte packet containing 42 bytes of a message payload gives the picture below. As far as the packets are concerned, only the red outlined parts are of interest.

_images/packet.svg

Marker

marker ::=  %x3f

The first byte of every Ambit protocol packet is always 0x3f. In ASCII that translates to a question mark. Any payload from a USB-HID transfer that starts with something else is most probably not an Ambit protocol packet.

Offset

offset ::=  %d8-%d254

The single byte integer in the second byte of the packet tells where one can find the second of the cyclic redundancy checks (CRC-2). As payload sizes vary, the check value is not located at a fixed position. This byte makes it easy to find. The offset is always equal to the sum of header and payload size, per packet layout.

From the packet layout it also follows that its value is always between \(8\), the size of a packet header and the first CRC, and \(wMaxPacketSize - 2\), both inclusive. Combined with the one byte nature for this offset, this means that \(256\) is the largest \(wMaxPacketSize\) value that can be accommodated.

Note

The \(wMaxPacketSize\) value has to be a power of 2 per USB specification.

Type

type    ::=  starter | trailer
starter ::=  %x5d
trailer ::=  %x5e

Packets come in two different types: starter and trailer packets. A starter packet has a type byte equal to 0x5d, a trailer packet uses 0x5e. The type byte is at an offset of 2.

When Ambit protocol messages are “stuffed” into packets, only the first part of the message travels in a starter packet. Any other packets that may be necessary to send the whole message travel in trailer packets.

Size

size ::=  %d0-%d246

Although very easily computed from the check value offset, the payload size is also stored at offset 3 in the packet. It gives the number of bytes of the payload that is sandwiched between the two check values (CRC-1 and CRC-2).

Notwithstanding the ABNF, its value is always between \(0\) and \(wMaxPacketSize - 10\), both values inclusive. So for a watch with a \(wMaxPacketSize\) of \(64\), the ABNF would become

size ::=  %d0-%d54

Index

index ::=  %d1-%d65535

All packets include a little-endian, two byte integer index at offsets 4 and 5. The index of the starter packet counts the number of packets that make up a single message. Indices for the message’s remaining trailer packets increase from one to one less than the index value in the starter packet. So for an index value of \(n\) in the starter packet, the indices of trailer packets will run from \(1\) through \(n - 1\).

Combined with the payload size constraints, this means that the Ambit protocol message size has an upper bound of \(65535 (wMaxPacketSize - 10)\), where \(wMaxPacketSize\) is \(256\) at most, per offset constraints.

CRC

CRC ::=  2OCTET

They have been mentioned a few times above already, but Ambit protocol packets use a cyclic redundancy check (CRC) pair to guard against packet corruption going unnoticed. Many CRC algorithms exist but the algorithm used by the Ambits goes by the name of CRC-16/CCITT FALSE (or something similar).

CRC-1

The first CRC, CRC-1, covers the type, size and index, that is, the four bytes starting at offset 2.

CRC-2

The second CRC, CRC-2, covers the payload as well as everything covered by CRC-1. In practice, this means that one would normally compute CRC-1 first and use that as the seed when continuing the computation over the payload bytes to get CRC-2.

Note

Code for this widely used algorithm should be easy to find. The Python crcmod module supports it as one of its predefined CRC algorithms and can generate code for C/C++. The Wireshark code includes a crc16_x25_ccitt or crc16_x25_ccitt_seed function (depending on the version) that implements it as well.