Krolyxon
Commit 9c570ea

Branches

Refs →

budctl9c570ea

Drop the fit test (W10)

The owner dropped the fit test: the T310 acks 0x0405 but runs no test.
budctl has no `fit-test` command any more, and the code is back to its
W9 state. The spec keeps the W10 result, and the implementation is in
8fdeefb.

No new dependencies.
Author
krolyxon
Date
2026-10-27 20:35 +0530
Parent
95ea4d7
Commit
9c570ead435fdf3132faa3d839436bbefca13c72

+13 −113

7 files changed

  1. M CLAUDE.md +2 −2
  2. M crates/budctl-cli/src/main.rs +4 −25
  3. M crates/budctl-cli/src/output.rs +1 −34
  4. M crates/budctl-core/src/devices/mod.rs +1 −15
  5. M crates/budctl-core/src/devices/realme_t310/mod.rs +1 −23
  6. M crates/budctl-core/src/status.rs +0 −9
  7. M docs/protocol/realme_t310.md +4 −5
M CLAUDE.md View
@@ -81,7 +81,7 @@ budctl --json status # machine-readable output for scripts/status ba
8181 budctl --debug ... # hex-dump every frame sent/received (stderr)
8282 ```
8383
84−Write commands (`anc`, `eq`, `game`, `set`, `bass`, `touch <side> …`, `find`, `fit-test`, custom EQ) are added one at a time, following rule 4. The plan is in `docs/protocol/realme_t310.md#roadmap`.
84+Write 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`.
8585
8686 Exit codes:
8787
@@ -107,7 +107,7 @@ Exit codes:
107107 7. find
108108 8. touch controls
109109 9. hold cycle
110− 10. fit test
110+ 10. ~~fit test~~ (dropped: the T310 acks it but runs no test)
111111 11. custom EQ
112112 4. **GUI**: Quickshell/Aurora via `--json`, or a Rust GUI on top of the same core.
113113 5. **More devices**: as acquired.
M crates/budctl-cli/src/main.rs View
@@ -77,17 +77,11 @@ enum Command {
7777 /// Ring the buds so you can find them (loud: take them out of your ears first).
7878 Find {
7979 #[arg(value_enum)]
80− action: StartStop,
80+ action: FindAction,
8181 /// Confirm that the buds are out of your ears. Needed to start ringing.
8282 #[arg(long)]
8383 yes: bool,
8484 },
85− /// Check how well each bud seals in your ear. Keep both buds in your ears; it takes about 10 s.
86− FitTest {
87− /// `stop` ends a test that is still running.
88− #[arg(value_enum, default_value = "start")]
89− action: StartStop,
90− },
9185 /// Turn game mode (low latency) on or off, then read it back.
9286 Game {
9387 #[arg(value_enum)]
@@ -96,7 +90,7 @@ enum Command {
9690 }
9791
9892 #[derive(Clone, Copy, ValueEnum)]
99−enum StartStop {
93+enum FindAction {
10094 Start,
10195 Stop,
10296 }
@@ -212,7 +206,7 @@ fn main() -> ExitCode {
212206 fn run(cli: &Cli, out: &mut impl Write) -> Result<(), output::Failure> {
213207 // Checked before connecting: ringing is loud. Spec: realme_t310.md#find
214208 if let Command::Find {
215− action: StartStop::Start,
209+ action: FindAction::Start,
216210 yes: false,
217211 } = cli.command
218212 {
@@ -269,25 +263,10 @@ fn run(cli: &Cli, out: &mut impl Write) -> Result<(), output::Failure> {
269263 output::switch(out, &device, model, &now, cli.json)
270264 }
271265 Command::Find { action, .. } => {
272− let ring = matches!(action, StartStop::Start);
266+ let ring = matches!(action, FindAction::Start);
273267 session.find(ring)?;
274268 output::find(out, &device, model, ring, cli.json)
275269 }
276− Command::FitTest {
277− action: StartStop::Start,
278− } => {
279− if !cli.json {
280− eprintln!("Running the fit test: keep both buds in your ears…");
281− }
282− let buds = session.fit_test()?;
283− output::fit_test(out, &device, model, &buds, cli.json)
284− }
285− Command::FitTest {
286− action: StartStop::Stop,
287− } => {
288− session.stop_fit_test()?;
289− output::fit_test_stopped(out, &device, model, cli.json)
290− }
291270 Command::Game { state } => {
292271 let now = session.set_switch("game", matches!(state, OnOff::On))?;
293272 output::switch(out, &device, model, &now, cli.json)
M crates/budctl-cli/src/output.rs View
@@ -4,7 +4,7 @@ use std::io::{self, Write};
44
55 use budctl_core::bluez::PairedDevice;
66 use budctl_core::devices::Device;
7−use budctl_core::status::{BudFit, Charge, JSON_SCHEMA, NoiseControl, Status, Switch, TouchChange, TouchControls};
7+use budctl_core::status::{Charge, JSON_SCHEMA, NoiseControl, Status, Switch, TouchChange, TouchControls};
88 use budctl_core::{Error, Found};
99 use serde_json::Value;
1010
@@ -294,39 +294,6 @@ pub fn eq(
294294 Ok(())
295295 }
296296
297−/// The fit-test result of each bud.
298−pub fn fit_test(
299− out: &mut impl Write,
300− device: &PairedDevice,
301− model: &dyn Device,
302− buds: &[BudFit],
303− as_json: bool,
304−) -> Result<(), Failure> {
305− if as_json {
306− return json(out, Some((device, model)), serde_json::json!({ "fit": buds }));
307− }
308− header(out, device, model)?;
309− for bud in buds {
310− writeln!(out, " {:<6} {}", bud.side, bud.fit)?;
311− }
312− Ok(())
313−}
314−
315−/// After `fit-test stop`: the buds acked.
316−pub fn fit_test_stopped(
317− out: &mut impl Write,
318− device: &PairedDevice,
319− model: &dyn Device,
320− as_json: bool,
321−) -> Result<(), Failure> {
322− if as_json {
323− return json(out, Some((device, model)), serde_json::json!({ "fit_test": "stopped" }));
324− }
325− header(out, device, model)?;
326− writeln!(out, " Fit test stopped.")?;
327− Ok(())
328−}
329−
330297 /// After `find start` or `find stop`: the buds acked; there is nothing to read back.
331298 pub fn find(
332299 out: &mut impl Write,
M crates/budctl-core/src/devices/mod.rs View
@@ -6,7 +6,7 @@
66 use crate::bluez::PairedDevice;
77 use crate::bt::Uuid;
88 use crate::error::{Error, Result};
9−use crate::status::{BudFit, NoiseControl, NoiseMode, Status, Switch, TouchChange, TouchControls};
9+use crate::status::{NoiseControl, NoiseMode, Status, Switch, TouchChange, TouchControls};
1010 use crate::transport::Transport;
1111
1212 pub mod realme_t310;
@@ -66,20 +66,6 @@ pub trait Session: Send {
6666 Err(Error::Unsupported(format!("budctl cannot change {key} on these buds")))
6767 }
6868
69− /// Run a fit test and wait for its result. Both buds must be in the ears.
70− fn fit_test(&mut self) -> Result<Vec<BudFit>> {
71− Err(Error::Unsupported(
72− "these buds have no fit test budctl can run".to_owned(),
73− ))
74− }
75−
76− /// Stop a fit test that is still running.
77− fn stop_fit_test(&mut self) -> Result<()> {
78− Err(Error::Unsupported(
79− "these buds have no fit test budctl can run".to_owned(),
80− ))
81− }
82−
8369 /// Set the noise-control modes a hold steps through (`off`, `transparency`, `anc`), then
8470 /// read it back. Returns the modes the buds now report.
8571 fn set_hold_cycle(&mut self, _modes: &[&str]) -> Result<Vec<String>> {
M crates/budctl-core/src/devices/realme_t310/mod.rs View
@@ -9,9 +9,7 @@ use crate::bluez::PairedDevice;
99 use crate::bt::Uuid;
1010 use crate::devices::{Device, Session};
1111 use crate::error::{Error, Kind, Result};
12−use crate::status::{
13− BudFit, GestureChange, NoiseControl, NoiseMode, RawWrite, Status, Switch, TouchChange, TouchControls,
14−};
12+use crate::status::{GestureChange, NoiseControl, NoiseMode, RawWrite, Status, Switch, TouchChange, TouchControls};
1513 use crate::transport::Transport;
1614
1715 mod codec;
@@ -368,15 +366,6 @@ impl Session for T310Session {
368366 })
369367 }
370368
371− /// The T310 acks `0x0405` but runs no test, so budctl does not send it.
372− /// Spec: realme_t310.md#fit-test
373− fn fit_test(&mut self) -> Result<Vec<BudFit>> {
374− Err(Error::Unsupported(
375− "the T310 accepts the fit-test command but runs no test (no sound, no result), so budctl does not offer it"
376− .to_owned(),
377− ))
378− }
379−
380369 /// Spec: realme_t310.md#hold-cycle-write, #write-read-back
381370 fn set_hold_cycle(&mut self, modes: &[&str]) -> Result<Vec<String>> {
382371 let mask = codec::hold_cycle_mask(modes)?;
@@ -1024,15 +1013,4 @@ mod tests {
10241013 script.assert_done();
10251014 assert!(matches!(result, Err(Error::Usage(_))));
10261015 }
1027−
1028− #[test]
1029− fn the_fit_test_is_refused_and_nothing_is_sent() {
1030− let mut steps = handshake();
1031− steps.pop();
1032− let script = Script::new(steps);
1033− let mut session = open(&script).unwrap();
1034− assert!(matches!(session.fit_test(), Err(Error::Unsupported(_))));
1035− assert!(matches!(session.stop_fit_test(), Err(Error::Unsupported(_))));
1036− script.assert_done();
1037− }
10381016 }
M crates/budctl-core/src/status.rs View
@@ -113,15 +113,6 @@ pub struct Gesture {
113113 pub group: Option<u8>,
114114 }
115115
116−/// One bud's result in a fit test.
117−#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
118−pub struct BudFit {
119− /// `left` or `right`.
120− pub side: String,
121− /// `good`, `average` or `poor`, or `unknown (…)` for a value the device module cannot name.
122− pub fit: String,
123−}
124−
125116 /// A touch-control change: what changes, the write that does it and, once sent, the table the
126117 /// buds report afterwards.
127118 #[derive(Debug, Serialize)]
M docs/protocol/realme_t310.md View
@@ -361,7 +361,7 @@ Evidence: *vendor*
361361 <a id="writes"></a>
362362 ## 10. Writes
363363
364−**One at a time.** budctl adds a write only after the previous one passes its hardware checkpoint (see the [roadmap](#roadmap)). Sent so far: [§10.1](#anc-write) off/transparency/on (W1, verified *ours-2026-10-27*), the light/medium/deep levels (W2, verified *ours-2026-10-27*), and [§10.2](#switch-write) game mode (W3, verified *ours-2026-10-27*), and [§10.3](#eq-write) the built-in EQ presets (W4, verified *ours-2026-10-27*), the bass-boost switch `1D` (W5a, verified *ours-2026-10-27*), 3D sound `1B` (W5b, verified *ours-2026-10-27*), wind-noise reduction `1A` (W5c, verified *ours-2026-10-27*), and dual connection `11` (W5d, verified *ours-2026-10-27*); [§10.5](#find) find (W7, verified *ours-2026-10-27*); [§10.6](#gesture-write) touch controls (W8, verified *ours-2026-10-27*); [§10.7](#hold-cycle-write) the hold cycle (W9, verified *ours-2026-10-27*). The fit test ([§10.8](#fit-test), W10) is acked but does nothing on the T310, so budctl does not send it. Bass level (W6) waits for a realme Link capture. budctl reads it back at once, because the spec gives it no special timing. The others are not sent yet.
364+**One at a time.** budctl adds a write only after the previous one passes its hardware checkpoint (see the [roadmap](#roadmap)). Sent so far: [§10.1](#anc-write) off/transparency/on (W1, verified *ours-2026-10-27*), the light/medium/deep levels (W2, verified *ours-2026-10-27*), and [§10.2](#switch-write) game mode (W3, verified *ours-2026-10-27*), and [§10.3](#eq-write) the built-in EQ presets (W4, verified *ours-2026-10-27*), the bass-boost switch `1D` (W5a, verified *ours-2026-10-27*), 3D sound `1B` (W5b, verified *ours-2026-10-27*), wind-noise reduction `1A` (W5c, verified *ours-2026-10-27*), and dual connection `11` (W5d, verified *ours-2026-10-27*); [§10.5](#find) find (W7, verified *ours-2026-10-27*); [§10.6](#gesture-write) touch controls (W8, verified *ours-2026-10-27*); [§10.7](#hold-cycle-write) the hold cycle (W9, verified *ours-2026-10-27*). The fit test ([§10.8](#fit-test), W10) is acked but does nothing on the T310; it is dropped. Bass level (W6) waits for a realme Link capture. budctl reads it back at once, because the spec gives it no special timing. The others are not sent yet.
365365
366366 **General rule**:
367367 - A write reply is `0x84xx <status …>`, and `00` means accepted.
@@ -464,7 +464,7 @@ Evidence: *buds4*
464464 - **Result:** arrives as push `04 (<dev> <result>)…`, where `1` = good, `0` = average and `6` = poor. The push needs event `04` subscribed.
465465 - **Timing:** about 8.6 s on Buds 4. Both buds must be in the ears.
466466 - **The T310 runs no test** (*ours-2026-10-27*, W10). With both buds in the owner's ears, `AA 08 00 00 05 04 05 01 00 01` (start) got `0x8405 00` within 19 ms. Then nothing came for 15 s: no push of any kind, and no sound. `0x0405 00` (stop) got `0x8405 00`, but after 417 ms, where other writes take about 20 ms. Event `04` was subscribed and acked. Bit 9 of the bitmap is set, so the bitmap overstates what the T310 does here.
467−- **budctl does not send it.** `budctl fit-test` on the T310 is refused before anything is sent (exit 1). It could return if a realme Link capture shows a fit test on the T310 ([open questions](#open)). The first W10 implementation is in git history (commit `8fdeefb`): it waited 15 s for the event-`04` push and sent stop if none came.
467+- **budctl does not send it.** The owner dropped the fit test (2026-10-27), and budctl has no `fit-test` command. The W10 implementation is in git history (commit `8fdeefb`): it waited 15 s for the event-`04` push and sent stop if none came.
468468
469469 <a id="custom-eq"></a>
470470 ### 10.9 Custom EQ: list `0x0122`, write `0x0418`
@@ -500,7 +500,6 @@ Evidence: *vendor*
500500 9. Whether the minimum 200 ms spacing between sends is needed.
501501 10. Why the T310 ignores `0x0124` although bit 38 is set ([§9.7](#bass-read)). Needs a realme Link capture before W6.
502502 11. How to turn noise cancelling on at the level the buds saved, without forcing deep ([§10.1](#anc-write)). Needs a realme Link snoop.
503−12. Whether realme Link offers a fit test for the T310 at all. The T310 acks `0x0405 01` but runs no test ([§10.8](#fit-test)). If the app has one, a snoop of it would show what the app sends.
504503
505504 <a id="roadmap"></a>
506505 ## 13. Roadmap and hardware checkpoints
@@ -524,7 +523,7 @@ The owner runs each checkpoint with `--debug` and pastes the output. Results go
524523 | W7 | `find` |
525524 | W8 | `touch <side> <gesture> <action>` (`--dry-run` first) |
526525 | W9 | `touch cycle` |
527−| W10 | `fit-test`: acked but does nothing on the T310; parked ([§10.8](#fit-test)) |
526+| W10 | ~~`fit-test`~~: dropped, the T310 runs no test ([§10.8](#fit-test)) |
528527 | W11 | custom EQ (create, then save, then delete) |
529528
530529 Ask for an Android HCI snoop of realme Link before W11, and for any write that does nothing.
@@ -534,7 +533,7 @@ Ask for an Android HCI snoop of realme Link before W11, and for any write that d
534533
535534 | Date | Checkpoint | Result | Notes |
536535 |---|---|---|---|
537−| 2026-10-27 | W10 `fit-test` | fail: no test | Start and stop were acked, but no result push and no sound came in 15 s. budctl now refuses the fit test on the T310. See [§10.8](#fit-test) |
536+| 2026-10-27 | W10 `fit-test` | fail: no test | Start and stop were acked, but no result push and no sound came in 15 s. The owner dropped the fit test. See [§10.8](#fit-test) |
538537 | 2026-10-27 | W9 `touch cycle off transparency anc`, then `off anc` | pass | Masks `0B` and `09` were acked and read back on the first try. The owner confirmed the new cycle with the hold gesture. The cycle is now off → anc. See [§10.7](#hold-cycle-write) |
539538 | 2026-10-27 | W8 `touch left triple-tap volume-up` (`--dry-run`, then sent), then `previous` | pass | The dry run's payload matched the derived frame. Both writes were acked and read back on the first try, 600 ms after the ack. The ack echoes the old table, cut short. The owner confirmed that the new gesture works. The left triple tap is now `previous`. See [§10.6](#gesture-write) |
540539 | 2026-10-27 | W7 `find start --yes`, `find stop` | pass | Both acked. Both buds rang, kept ringing after budctl disconnected, and stopped on `find stop`. See [§10.5](#find) |