Opinion-free, async-first Python SDK for iDotMatrix BLE pixel displays. Licensed GPL-3.0-or-later — see LICENSE and NOTICE.
This project has two equally important goals:
- The definitive Python SDK for iDotMatrix displays — controlling a panel should feel like controlling a device, not constructing packets.
- The reference implementation and documentation of the device protocol — every verified reverse-engineering discovery lands here; every unverified one is clearly marked experimental. Protocol research is a first-class contribution (a good hardware probe log is worth as much as a feature).
See docs/ROADMAP.md for the full architecture review, capability inventory with evidence, and the path to 1.0.
| Getting Started | Install → discover → connect → clock/text/gif/full frame, ten lines. |
| Feature Guide | Every namespace, with usage examples and hardware-verification status. |
| Hardware Compatibility | The full capability table, and how to extend it with your own panel. |
| Protocol Notes | Acks vs. effect, chunking, persistence, endianness, streaming/performance — the SDK's moat. |
| Architecture | The layer diagram and why the driver is opinion-free. |
| API Reference | Exact public signatures. |
| Firmware Notes | What's known to vary across panel sizes/firmware. |
| Reverse-engineering notes | APK decompile analysis behind the protocol findings above. |
Full doc index: docs/README.md.
Runnable examples: examples/ — discovery, images, animation, graffiti, GIFs, native widgets, the simulator, and the capability table, as standalone scripts.
Why
pyidotmatrixand notidotmatrix? Theidotmatrixname on PyPI (and its import namespace) belongs to the incumbent library by derkalle4 — installing both would collide in site-packages. One name everywhere, zero collision (ROADMAP §14, decided 2026-07-20).
Not yet on PyPI. From a checkout:
pip install -e . # library
pip install -e .[test] # + test tooling
Requires Python 3.12–3.14. BLE via bleak (Windows/Linux/macOS).
| Subsystem | Status |
|---|---|
| BLE transport (reconnect, acks, notifications) | ✅ hardware-proven |
| Framebuffer (DIY full frames + entry/quit modes) | ✅ (device renders full frames at a hard ~1.75 fps; unacked/without-response writes honored) |
| Graffiti (partial pixel updates + h/v mirror) | ✅ header fully mapped 2026-07-21 |
| Images / GIF (adapt + native playback) | ✅ |
| Native clock · countdown · stopwatch · scoreboard | ✅ |
| Text (device-rendered, per-panel-size builders) | ✅ verified on 32×32 |
| Alarms (Timer slots: GIF + PNG content, buzzer, week-day mask) | ✅ incl. week-bit mapping |
| Eco (scheduled dim) · screen flip | ✅ |
| Effects / color | ✅ incl. animation speed (effect byte 5, verified 2026-07-25) |
| Weekly schedule | ⚠ partially verified |
| Music sync | ✅ host-streamed rhythm levels render; ✖ the device-side send_image_rhythm path |
| freeze / set_speed / time indicator | ✖ acked, proven inert on the reference panel |
✅ hardware-verified · ⚠ experimental / partially mapped · ✖ known-broken on the reference 32×32 (an ack confirms receipt, not effect). Every entry carries machine-readable evidence:
from pyidotmatrix import capability
capability("text.show").status # CapabilityStatus.VERIFIED
capability("device.set_speed").evidence # cites the probe and dateFull table with evidence: pyidotmatrix/capabilities.py and ROADMAP §3.
protocol/ pure byte builders (no I/O), one per device feature
transport/ BLE connection lifecycle, chunked writes, reconnect supervision
display/ DisplayBackend interface + BleDisplay (hardware) and SimulatorDisplay
client.py IDotMatrixClient — full-feature facade over one connection
imaging.py canvas-fitting helpers (image adaptation)
The driver moves bytes to the device. It does not schedule, render app frames, diff frames, or decide between full frames and pixel updates — those are caller concerns.
DisplayBackend — the minimal frame pipeline seam (show_frame, set_pixels,
brightness, power). BleDisplay and SimulatorDisplay both satisfy it, so
callers are backend-agnostic.
from pyidotmatrix import BleDisplay, BleTransport, ScreenSize, SimulatorDisplay
display = BleDisplay(ScreenSize.SIZE_32x32, BleTransport(mac_address=None))
await display.connect()
await display.show_frame(rgb_bytes) # full frame (32*32*3 bytes)
await display.set_pixels((0, 255, 0), [(1, 1)]) # partial update
sim = SimulatorDisplay(ScreenSize.SIZE_32x32, on_frame=lambda buf: ...) # no hardwareIDotMatrixClient — the full native-feature facade; every device capability,
sharing one connection with .display.
from pyidotmatrix import IDotMatrixClient, ScreenSize, discover
devices = await discover() # [DeviceInfo(name='IDM-...', address=..., rssi=...)]
async with IDotMatrixClient.connect_to(devices[0], ScreenSize.SIZE_32x32) as client:
await client.countdown.start(25, 0) # e.g. a Pomodoro (device runs it natively)
await client.clock.show()
await client.text.show("HELLO", font_path=...)
await client.gif.upload_file("anim.gif")
await client.display.show_frame(rgb_bytes) # rendered frames, same connectionCommands are verified by default: a device rejection raises
CommandRejectedError (opt out with verify_commands=False).
Feature namespaces: chronograph (alias stopwatch), countdown, clock, scoreboard, eco,
color, graffiti, effect, music_sync (alias music), text, gif, device, plus display.
Alarms and weekly schedules live under experimental (bytes confirmed,
hardware-verified for the core paths, but not yet promoted out of that
namespace) — see the Feature Guide.
The device pushes a status ack for every recognized command (accepted / rejected). Observe them passively, or await one for a specific command:
# passive: fires for every command's ack
unsubscribe = client.add_response_listener(lambda ack: print(ack.command_type, ack.accepted))
# active (opt-in): send a command and wait for its ack, or None on timeout
from pyidotmatrix.protocol import common
ack = await client.await_device_ack(common.build_set_brightness(60))Protocol truth worth knowing: an ack confirms receipt, not effect —
the device can accept a command and not act on it. The inverse happens too:
device.set_time works and never acks at all, so this client sends it
fire-and-forget. The SDK documents these cases rather than hiding them (see
Protocol Notes and ROADMAP §4).
The other one worth knowing: the panel commits its displayed mode to
flash lazily. Write content and disconnect a few seconds later and it reverts
— acked, successful, gone. Dwell, or reconnect once before the real write; and
recover a lost GIF with gif.activate_stored() rather than re-uploading. Full
rules: Protocol Notes § Persistence.
client.set_auto_reconnect(True) # arm/disarm reconnect at runtime
unsub = client.add_event_listener(print) # write failures, reconnects
snap = client.snapshot() # address, connected, write_size, reconnect_count, last_failure
await client.show_image("photo.png") # adapt to the screen and displayListener registrations return an unsubscribe callable. A listener that raises is isolated — it cannot break connection handling.
Low-MTU panels: the transport trusts the characteristic's reported write size.
Some iDotMatrix panels on BlueZ under-report it (~20 bytes); pass
BleTransport(..., write_size_override=514) for full-speed frames on those.
Streaming/animation performance (the ~1.75 fps DIY-frame render cap, why
deltas beat full frames for sustained animation): see
Protocol Notes § Streaming & performance.
pip install -e .[test]
pytest
Protocol builders are covered by byte-exact golden tests. Hardware probes
live in probes/ — human-run against a real panel, never in CI.
Reverse engineering is a first-class contribution: hardware probe results,
BLE packet captures, firmware/model comparisons, and protocol documentation
are as valuable as code. Captures decode with the bundled analysis CLI —
pyidotmatrix-btsnoop capture.log turns an Android btsnoop HCI log into
annotated iDotMatrix traffic, matched against this SDK's own builders (add
--stats, --only writes, --grep, or --json). See
CONTRIBUTING.md for how to
run the test suite, how to run hardware probes safely, and the ⚠→✅
graduation process for experimental features.
This SDK builds on the reverse-engineering lineage of 8none1, derkalle4 (GPLv3), and markusressel — see NOTICE.