


Reverse-engineering a Bluetooth LED sign
The sign is a CoolLEDUX 16x96 full-color RGB LED matrix that talks Bluetooth Low Energy and normally runs from its stock Android app. I wanted it on my own terms: scriptable, reachable over HTTP, and wired into Home Assistant.
An existing community Python SDK was the obvious starting point, but its command codes were wrong for this firmware. So the decompiled stock app became the source of truth, and every command, checksum and frame format was checked against it (and against the real sign wherever possible) before it went into the Go code.
0x01, 0x02 or 0x03 inside the length or payload is escaped as 0x02 followed by the byte XOR 0x04.0x4C11DB7, initial value 0xFFFFFFFF, no final XOR), computed bitwise the way the stock app does it.The frame, from the public BLE protocol spec:
[0x01] [length:2 BE] [escaped(payload)] [0x03] | | | | | | | +-- end byte (never escaped) | +---------------+----------------- escaped together: | 0x01/0x02/0x03 -> [0x02, B ^ 0x04] +-------------------------------------------- start byte (never escaped) payload = [command:1] [args...] e.g. 0x04 brightness, 0x05 power
letterbox, stretch and cover fit modesFor a while the scoreboard looked broken: the sign ACKed every scoreboard command and showed nothing. The sign is very polite about ignoring you. The control commands only do something once the scoreboard's composite program (content type 0x0B) has been uploaded, so the fix was building that program the way the stock app does, after which it renders on the 16x96 panel.
Even then the clock is picky. A new time only latches while the clock is stopped, so changing it means stop, set, start. Scores update immediately either way.