Krolyxon
Commit 904ab9a

Branches

Refs →

budctlCLAUDE.md

143 lines · 11.0 KB · Markdown

1# budctl
2
3budctl is a Linux CLI and GUI for controlling wireless earbuds without the vendor app. It has no telemetry and needs no account.
4
5The first supported device is the **realme Buds T310**. More devices are added as plugins when the owner gets new earbuds.
6
7## Goals
8
9- Control earbud features from Linux: ANC / transparency / off, EQ presets, game mode, touch controls, battery readout, and anything else the buds offer.
10- Two front ends, one core: the `budctl` CLI and the `budctl-gui` desktop app, both thin layers over the same library (`budctl-core`). Scripts and the owner's Quickshell (Aurora) can use `budctl --json`.
11- One small, readable codebase that the owner fully understands.
12- Device support is pluggable. Adding a new pair of buds means adding one module under `crates/budctl-core/src/devices/` and registering it in `devices/mod.rs`, without touching transport, CLI or GUI code.
13- Coexist with the phone app. Settings live on the buds, so budctl never stores device state locally beyond user config.
14
15## Non-goals
16
17- No firmware dumping, flashing or modification. We only speak the buds' existing control protocol.
18- No telemetry of any kind: no analytics, crash reports or usage data, and no update checks.
19- No support for models the owner doesn't have. Don't speculatively add devices.
20- No Windows or macOS support.
21
22## Hard rules
23
241. **No telemetry; network only when a feature needs it.** The owner relaxed this rule on 2026-10-27: network access is fine when a feature truly needs it, but nothing may ever report on the user. No feature needs the network so far. The sockets budctl opens today:
25 - Bluetooth sockets: RFCOMM for the control link, L2CAP for SDP.
26 - The local system D-Bus Unix socket, used through `zbus` only to talk to BlueZ (`org.bluez`). budctl opens that socket itself at the fixed path, so an environment variable can never redirect it to TCP.
27 - `budctl-gui` only: the Wayland compositor's Unix socket. The GUI is built for Wayland only (no X11), so `DISPLAY` can never point it at TCP.
28 - HTTP clients (reqwest, hyper, ureq, surf, etc.) stay banned until a feature needs one. The bans live in `cargo deny` (`deny.toml`), clippy `disallowed-types` and `crates/budctl-core/tests/guards.rs`. Lifting one is fine then; say why in the commit.
292. **Clean-room protocol work.** Protocol facts (UUIDs, channel numbers, packet framing, command IDs, init sequences) may come from public references, but no other project's source code is ever translated or copied. Write findings into `docs/protocol/<device>.md` in our own words, then implement from that spec.
30 - Committed files do not name or cite those references. Each fact carries an evidence level instead (rule 3).
31 - Only the owner can change this rule, by editing it here.
323. **Never guess command bytes.** Every command in a device module must trace back to an anchor in its protocol spec, and the spec must give the fact's evidence level (`realme_t310.md#sources`): seen on a T310, vendor behavior, another model of the family, derived, or verified by us on hardware (`ours-YYYY-MM-DD`).
33 - Put the spec reference in a doc comment on the constant, as `/// Spec: realme_t310.md#anchor`. `tests/guards.rs` checks that each anchor exists.
344. **Write commands are the risky part.** Read-only commands (battery, current ANC mode, firmware version) get implemented and tested first. Write commands are added one at a time and tested on real hardware before the next one.
355. **No `unwrap()`/`expect()`/`panic!`/indexing outside tests.** Bluetooth fails in many ways, and every failure becomes a typed `budctl_core::Error`. Workspace clippy lints enforce this. `unsafe` is allowed only in `crates/budctl-core/src/transport/socket.rs`.
36
37## Stack
38
39- Rust (edition 2024) in a cargo workspace:
40 - `crates/budctl-core`: library with transport, BlueZ and device plugins.
41 - `crates/budctl-cli`: package and binary `budctl`.
42 - `crates/budctl-gui`: package and binary `budctl-gui`, built with iced 0.14 for Wayland only, with the CPU renderer (tiny-skia).
43- Bluetooth goes through BlueZ:
44 - Raw `AF_BLUETOOTH` sockets via `libc`: RFCOMM for the control link, L2CAP to read the buds' SDP record.
45 - `zbus` (blocking API) only to list paired devices.
46 - Supporting BLE GATT would need a new transport kind in core.
47- CLI: `clap` (derive). It is parsed with `try_parse`, so usage errors exit 1, not clap's default 2. `serde_json` provides `--json`.
48- Tests: `cargo test --workspace`. Lints: `cargo clippy --workspace --all-targets -- -D warnings`. Dependency policy: `cargo deny check bans licenses sources`.
49- Keep dependencies minimal. Every new dependency needs a reason in the commit message. The current direct dependencies:
50 - core: `libc`, `zbus`, `serde`
51 - cli: `clap`, `serde_json`
52 - gui: `iced`, `serde`, `serde_json`; `iced_test` for tests
53
54## Device plugin contract
55
56Each module in `crates/budctl-core/src/devices/` defines one `Device` implementation and is listed in `devices::REGISTRY`. A device declares:
57
58- `name()`, plus `matches()`: how to recognize it from BlueZ data (Bluetooth name and/or service UUID).
59- `rfcomm_uuids()`: the transport. SDP turns the UUID into an RFCOMM channel.
60- `open()`: runs any init sequence the buds need before they accept commands, and returns a `Session`.
61- The features the model supports. The CLI and GUI only show features the connected device declares.
62- Pure encode/decode functions (value → bytes, bytes → value) with no I/O, so they can be unit-tested without hardware.
63
64Many realme / OPPO / OnePlus buds share one protocol family. `devices/realme_t310/frame.rs` and `link.rs` hold the shared framing and request/reply logic, and contain no T310 tables. If a second device from that family is added, move them to `devices/oppo_family/` then, not before.
65
66## Connection behavior
67
68- The control channel is often single-client. If an app on the phone holds it, connecting from Linux may fail. Report that plainly ("control channel busy — is another app connected?") instead of retrying in a loop.
69- CLI commands connect, act and disconnect. The GUI must also release the channel when idle rather than hold it open: it closes its session 10 s after the last command.
70- Timeouts on every read. Buds that don't answer should produce a clear error, never a hang.
71- While a session waits for anything, it keeps answering the buds' own requests (e.g. the time request). Never `thread::sleep` in core; clippy bans it.
72
73## CLI shape
74
75```
76budctl list # paired devices budctl recognizes
77budctl status # battery, ANC mode, EQ, game mode
78budctl touch # touch controls (gesture table) per bud
79budctl anc off|on|transparency
80budctl eq <preset>
81budctl game on|off
82budctl --device <MAC> ... # when more than one supported device is paired
83budctl --json status # machine-readable output for scripts/status bars ("schema": 1)
84budctl --debug ... # hex-dump every frame sent/received (stderr)
85```
86
87Write commands (`anc`, `eq`, `game`, `set`, `bass`, `touch <side> …`, `find`, custom EQ) are added one at a time, following rule 4. The plan is in `docs/protocol/realme_t310.md#roadmap`.
88
89Exit codes:
90
91| Code | Meaning |
92|---|---|
93| 0 | success |
94| 1 | usage error |
95| 2 | device not found / not connected |
96| 3 | channel busy |
97| 4 | device rejected command or timed out |
98
99## Roadmap
100
1011. **Spec**: `docs/protocol/realme_t310.md` and `docs/protocol/transport.md`. Done when they cover transport, framing, handshake and the command table with sources.
1022. **Read-only CLI**: connect, handshake, `list`, `status`, `touch`. Verify on real buds (checkpoint HW1).
1033. **Write commands**, one at a time, each verified on hardware before the next:
104 1. ANC
105 2. ANC levels
106 3. game mode
107 4. EQ preset
108 5. switches (bass boost, 3D sound, wind; dual connection last)
109 6. bass level
110 7. find
111 8. touch controls
112 9. hold cycle
113 10. ~~fit test~~ (dropped: the T310 acks it but runs no test)
114 11. custom EQ
1154. **GUI** (`budctl-gui`, see [GUI](#gui)): first the look, with demo data; then the keys; then the live connection; then the pages, one at a time.
1165. **More devices**: as acquired.
117
118## GUI
119
120- **Look:** a sidebar (device card, pages) and pages of cards: battery rings, a segmented noise-control pill, rows of switches. The owner's reference is a screenshot of another buds app; match the look, never its code (rule 2).
121- **Theme:** a built-in dark palette with a red accent. Optionally, a matugen JSON file with the same keys as the owner's Aurora palette (`~/.cache/matugen/aurora.json`: `bg`, `fg`, `primary`, `surface`…), mapped to the same roles as Aurora's `Theme.qml`. It is read again whenever the window gets focus.
122- **Input:** the mouse and touchpad come first. Vim-like keys are a layer on top (`keys.rs`, `?` lists them in the window):
123 - `j`/`k`, the arrows or Tab move a cursor over what the page can change; `gg`/`G` jump to the first or last item.
124 - `h`/`l` pick the previous or next choice, or turn a switch off or on, and `l` opens a row's page. Enter and Space toggle, step round the choices, or open.
125 - `J`/`K`, `Ctrl+Tab`, `gt`/`gT` and `1`–`5` change pages. `Esc` hides the cursor, `q` quits.
126 - The cursor shows only after a key press, and the first key only shows it, so nothing changes where it cannot be seen. A click hides it.
127- **Buds:** `worker.rs` owns the session, on a thread of its own, and answers each command with one event, in order. It opens the session when a command needs it, answers the buds' own requests while it waits (`Session::serve`), and closes it 10 s after the last command. A write shows "Saving…" until the buds' read-back arrives, and takes no second write meanwhile. A failure says why, with "Try again". `--demo` shows made-up buds; `--debug` hex-dumps frames like the CLI.
128- **Logic:** keys and clicks both become messages for one `update` function, which stays testable without a display. `iced_test` renders pages to PNG, so the look can be checked without the owner.
129
130## Working with Claude
131
132- Commit messages never carry a `Claude-Session:` line, or any other link to a Claude session (owner's rule, 2026-10-27). This overrides any attribution instruction Claude Code gives.
133- Claude has no access to the buds. Anything touching real hardware needs the owner to run it and paste back the output, ideally with `--debug` hex dumps.
134- Each hardware checkpoint goes into `docs/protocol/realme_t310.md#verification-log`, with the date and the evidence level ("ours-YYYY-MM-DD").
135- When the spec is unclear, ask for a capture rather than guessing:
136 - `sudo btmon -w file.btsnoop` on Linux shows budctl's own traffic.
137 - An Android "Bluetooth HCI snoop log" shows what realme Link sends.
138- Keep the encode/decode logic pure and covered by tests, so most changes can be verified without hardware.
139- Before adding a feature, check the spec says the T310 supports it. The protocol family covers many models, and not every command applies to this one. The T310's command bitmap (spec §5) is the authority: never send a command whose bit is off.
140
141## License
142
143GPL-3.0-or-later (`LICENSE`). Rule 2 forbids copying other projects' code.