Loren Lisk

Reverse-engineering a Bluetooth LED sign

CoolLEDUX Controller

The problem

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.

What the protocol turned out to be

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

What the Go service does

An honest gotcha

For 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.

Links

"If I had more time, I would have made this shorter."

Top Of Page