Skip to main content

ditoo-claude-meter

Pushes Claude Code's 5h/7d usage quota to a Divoom Ditoo Mic's 16x16 Bluetooth display, as a ring gauge around a small creature that reflects whatever Claude Code is currently doing (working / waiting / idle / asleep).

uv tool install ditoo-claude-meter
ditoo-meter list-devices   # find your paired device
ditoo-meter setup          # wire up statusLine + hooks + the background daemon

Full wire-protocol detail (frame format, commands, real-hardware findings) lives in docs/PROTOCOL.md and is not repeated here. This README exists to explain the handful of design decisions that aren't obvious from reading the code, so that extending this project doesn't mean re-deriving them from scratch.

1. Two names, one device -- the first mistake everyone makes

The device answers to two different Bluetooth names for the same physical unit and address: the Classic radio calls itself <name>-Audio, the BLE radio advertises as <name>-Light. The display protocol lives on the Classic (-Audio) side, despite what the name suggests -- BLE only exposes a transparent-UART service that nothing in this project (or any known open-source Divoom project) has gotten to accept image data. If you're poking around with a Bluetooth scanner and see -Light, that's the wrong radio for this project's purposes; look for pairing/SDP activity on the -Audio name instead. See PROTOCOL.md's "Transport" and "Why not BLE" sections for the full story, including what was tried against BLE and why it didn't pan out.

2. Why the Bluetooth helper is a separate subprocess

link_helper.py runs as its own subprocess (python -m ditoo_meter.link_helper <MAC>), spoken to over stdin/stdout by link.py, rather than being called directly from the daemon. This isn't incidental complexity: pyobjc-framework- IOBluetooth's async APIs only deliver their callbacks (RFCOMM channel open, incoming data, channel closed) by pumping an NSRunLoop. The daemon's own loop is a plain synchronous while with a time.sleep(0.5) -- there's no run loop for IOBluetooth's callbacks to arrive on. Reconciling the two in one process means either running IOBluetooth on a dedicated thread with its own run loop and shuttling data across a queue, or making the daemon's main loop itself run loop-driven and fighting IOBluetooth for control of it. A separate process sidesteps both: the helper owns an event loop it can pump freely (link_helper.py's _pump()), and the daemon side (link.py) stays fully synchronous -- ensure_connected(), send_burst(), and friends are plain function calls with no async machinery of their own. The cost is a pipe protocol and a subprocess lifecycle to manage (see link.py's lock-guarded state machine), which is a smaller problem than making two different concurrency models share one process.

3. The statusLine tap passthrough contract

ditoo-meter setup takes over ~/.claude/settings.json's statusLine slot, pointing it at ditoo-meter tap. If you already had a statusLine command configured, whatever it printed must keep printing -- this tool occupies the slot, it doesn't get to break your existing statusline.

  • setup snapshots your prior statusLine.command (if any) into ~/.config/ditoo-claude-meter/config.json as passthrough, then installs its own command in its place. It has to happen at install time, once -- by the time tap is running, statusLine already points at us, so the original command has nowhere else to be read from later.
  • tap (called by Claude Code on every statusline tick) always reads the usage payload off stdin first and records it, then -- if a passthrough command was saved -- re-invokes that command with the same stdin and relays its stdout/stderr/exit code byte-for-byte, unmodified. Whatever your original statusline displayed (formatting, colors, other integrations) is not ours to reinterpret. If there's no saved passthrough, or the passthrough command times out or errors, tap falls back to printing its own compact usage line -- a stuck passthrough must not be able to take Claude Code's statusline down with it.
  • undo restores the saved passthrough command back into statusLine and clears the saved copy -- but only when statusLine still actually points at us at the time undo runs. If you've hand-edited statusLine since setup (pointing it at something else), undo leaves it alone and does not discard the saved passthrough value, since that might be the only copy of your original command; it prints a note telling you where to find it in config.json instead.

4. Why animations make the daemon simpler

Real hardware finding (see PROTOCOL.md's "Multi-frame animation"): the device loops a pushed animation locally and indefinitely. Push once, it keeps playing until replaced -- there's no need to re-send frames on a timer. Every type in this codebase reflects that: render() always returns an Animation (a tuple of (frame, duration_ms) pairs), even a static scene is just a one-frame animation, and protocol.commands_for() decides whether that goes out as a single 0x44 image or a chunked 0x49 animation. The consequence that matters most for the daemon: alert blinking (usage over 100%) is a real 2-frame animation, not a 500ms re-render loop. The daemon hashes the animation it would push and only talks to Bluetooth when that hash changes from what's already on the device -- in steady state, with nothing to say, it sends nothing at all.

5. Silent failure -- why pacing is insurance, not decoration

0x44 and 0x49 (the image/animation push commands) never get an ACK from the device. A dropped write and a successful one look identical at the RFCOMM layer -- silence either way. Two consequences follow directly from this, both load-bearing:

  • link.py waits CHANNEL_SETTLE_S (~1.5s, real-hardware-verified) after sending the channel-switch command before it will attempt to push an image. Skipping this produces a write that "succeeds" and a screen that never updates.
  • Link.send_burst() puts a small gap between chunks of a multi-part push instead of firing them back-to-back. It's the only line of defense against a burst the device can't keep up with, given that there's no way to ask it afterward whether the push landed.

If you need positive confirmation the link is alive, 0x46 (get view) is the one command in this protocol that does reply -- see PROTOCOL.md.

A note on naming

The repo/package is ditoo-claude-meter, the importable module is ditoo_meter, and the installed command is ditoo-meter. That's three different spellings for one project. It's intentional (PyPI package names can't contain underscores the way Python module names require), not an inconsistency to "fix" -- if you see ditoo_meter in an import and ditoo-claude-meter in a pip install, that's expected.

Linux

Not implemented. link.py's Link._popen() has an explicit branch on sys.platform where a Linux transport would plug in -- Classic SPP over socket.AF_BLUETOOTH is roughly a dozen lines (connect, then plain socket.send/recv instead of the IOBluetooth async dance macOS needs). No IOBluetooth-style run-loop problem exists on Linux, so a Linux helper likely wouldn't even need the separate-subprocess split described above. Contributions welcome.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ditoo_claude_meter-0.1.0.tar.gz (57.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ditoo_claude_meter-0.1.0-py3-none-any.whl (34.2 kB view details)

Uploaded Python 3

File details

Details for the file ditoo_claude_meter-0.1.0.tar.gz.

File metadata

  • Download URL: ditoo_claude_meter-0.1.0.tar.gz
  • Upload date:
  • Size: 57.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for ditoo_claude_meter-0.1.0.tar.gz
Algorithm Hash digest
SHA256 c17b8034886bb20ee3bd96da21212296d9aeaa9e31e2c743a11ac403660cd3d6
MD5 0e18d70db1eba2ac121b34fcb3e76d55
BLAKE2b-256 8ed6ee0033493ce55392a477ebefacf79c37a0f223ed3d247976a7f4c4dd5a5f

See more details on using hashes here.

File details

Details for the file ditoo_claude_meter-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: ditoo_claude_meter-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 34.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for ditoo_claude_meter-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3e0bc6439f450c65ba9fe3621370995e1b2ce45e357eef36c924410c223d40ef
MD5 97bc09bed8d3fe7b8fc7b767520132de
BLAKE2b-256 58c640b88e5bee06bc52dbc21068c75b22891b8ada399e959b5f95bd5c19abb4

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page