Summary
EXPECTED_MESSAGE_LENGTH_MAP declares a fixed payload length of 64 for MP3_FILE_NAME_RECEIVE_COMMAND (0x11) and MP3_ARTIST_NAME_RECEIVE_COMMAND (0x12), but the controller sends these as variable-length, null-terminated strings. Observed frames are 7 and 16 bytes total, not 69.
This desyncs the frame parser. It is not latent — _parse_frame consults the length map to determine framing before dispatching to a handler, so the bug is active today even though the 0x11/0x12 handlers are commented out at base_client.py:367-371.
Evidence
Captured from a Lync 12 (firmware responds to model probe as Lync) with a USB drive attached, via a passive TCP sniffer on port 10006.
02 00 00 11 31 00 44 cmd=0x11, payload "1\0" (7 bytes)
02 00 00 12 41 57 4f 4c 4e 41 54 49 4f 4e 00 10 cmd=0x12, payload "AWOLNATION\0" (16 bytes)
02 00 00 12 41 64 65 6c 65 00 ef cmd=0x12, payload "Adele\0" (11 bytes)
02 00 00 12 fe ff 00 11 cmd=0x12, payload BOM only (8 bytes)
The lengths are confirmed by the transmitted checksums, not merely by a parser's interpretation:
0x02 + 0x00 + 0x00 + 0x11 + 0x31 = 0x44 — matches the checksum on the 7-byte frame.
0x02 + 0x00 + 0x00 + 0x12 + "Adele" + 0x00 = 0xEF — matches the checksum on the 11-byte frame.
A 64-byte payload would produce entirely different checksums. The frames really are this short, and they vary in length with the string.
Note the third case: for some tags the controller emits a bare UTF-16 BOM (fe ff) and no text at all — a 1-byte payload plus terminator. Any fix needs to handle that without raising.
Failure mode
At base_client.py:266:
if len(data) <= data_idx + expected_length:
return None, 0 # "wait for more data"
With expected_length = 64 against a 7-byte frame:
- The parser stalls at the head of the buffer waiting for 65 bytes of payload that will never belong to this frame.
- Because parsing proceeds from
start_message_index, this head-of-line blocks every frame queued behind it — including zone-status updates — until enough unrelated traffic accumulates.
- Once 65 bytes do accumulate, the parser reads a 64-byte window spanning several real frames, the checksum fails, and it logs
Bad checksum and resyncs by MESSAGE_HEADER_LENGTH.
- The swallowed frames are then re-scanned from the resync point, so state does eventually recover — but with delay, log noise, and dropped updates in between.
On a Lync with a USB drive this triggers on every track change, since 0x11 and 0x12 are emitted as a pair each time the track advances (whether by soft key or naturally).
Suggested fix
0x11 and 0x12 need length-by-terminator rather than a fixed length: scan forward from data_idx for the 0x00 terminator and derive the payload length from it, with a sanity bound so a corrupt buffer cannot scan unboundedly.
The same approach was applied to the diagnostic sniffer used to gather these captures, and it resolved the misparses cleanly across all four capture sessions.
Two adjacent entries carry # should be 11 comments:
ZONE_NAME_RECEIVE_COMMAND: 13, # should be 11
SOURCE_NAME_RECEIVE_COMMAND: 13, # should be 11
These may be a related symptom of the same fixed-length assumption applied to string-valued commands. I have not captured either of them, so I am flagging the resemblance rather than asserting they are the same bug.
For contrast, the two MP3 commands whose payloads genuinely are fixed-length are correct as declared, and both were confirmed on the wire in the same session:
MP3_ON_RECEIVE_COMMAND (0x13) = 1 — observed 02 00 00 13 00 15
MP3_OFF_RECEIVE_COMMAND (0x14) = 17 — observed with payload "Device Not Found\0", exactly 17 bytes
So the map is not wrong in general; the defect is specific to the variable-length string commands.
Context
Found while reverse-engineering the MP3 soft keys for the Home Assistant integration. Full protocol analysis and the capture logs are in theharshl/htd-home-assistant#29.
Summary
EXPECTED_MESSAGE_LENGTH_MAPdeclares a fixed payload length of 64 forMP3_FILE_NAME_RECEIVE_COMMAND(0x11) andMP3_ARTIST_NAME_RECEIVE_COMMAND(0x12), but the controller sends these as variable-length, null-terminated strings. Observed frames are 7 and 16 bytes total, not 69.This desyncs the frame parser. It is not latent —
_parse_frameconsults the length map to determine framing before dispatching to a handler, so the bug is active today even though the0x11/0x12handlers are commented out atbase_client.py:367-371.Evidence
Captured from a Lync 12 (firmware responds to model probe as Lync) with a USB drive attached, via a passive TCP sniffer on port 10006.
The lengths are confirmed by the transmitted checksums, not merely by a parser's interpretation:
0x02 + 0x00 + 0x00 + 0x11 + 0x31 = 0x44— matches the checksum on the 7-byte frame.0x02 + 0x00 + 0x00 + 0x12 + "Adele" + 0x00 = 0xEF— matches the checksum on the 11-byte frame.A 64-byte payload would produce entirely different checksums. The frames really are this short, and they vary in length with the string.
Note the third case: for some tags the controller emits a bare UTF-16 BOM (
fe ff) and no text at all — a 1-byte payload plus terminator. Any fix needs to handle that without raising.Failure mode
At
base_client.py:266:With
expected_length = 64against a 7-byte frame:start_message_index, this head-of-line blocks every frame queued behind it — including zone-status updates — until enough unrelated traffic accumulates.Bad checksumand resyncs byMESSAGE_HEADER_LENGTH.On a Lync with a USB drive this triggers on every track change, since
0x11and0x12are emitted as a pair each time the track advances (whether by soft key or naturally).Suggested fix
0x11and0x12need length-by-terminator rather than a fixed length: scan forward fromdata_idxfor the0x00terminator and derive the payload length from it, with a sanity bound so a corrupt buffer cannot scan unboundedly.The same approach was applied to the diagnostic sniffer used to gather these captures, and it resolved the misparses cleanly across all four capture sessions.
Two adjacent entries carry
# should be 11comments:These may be a related symptom of the same fixed-length assumption applied to string-valued commands. I have not captured either of them, so I am flagging the resemblance rather than asserting they are the same bug.
For contrast, the two MP3 commands whose payloads genuinely are fixed-length are correct as declared, and both were confirmed on the wire in the same session:
MP3_ON_RECEIVE_COMMAND(0x13) = 1 — observed02 00 00 13 00 15MP3_OFF_RECEIVE_COMMAND(0x14) = 17 — observed with payload"Device Not Found\0", exactly 17 bytesSo the map is not wrong in general; the defect is specific to the variable-length string commands.
Context
Found while reverse-engineering the MP3 soft keys for the Home Assistant integration. Full protocol analysis and the capture logs are in theharshl/htd-home-assistant#29.