budctlb37b005
budctl set <switch> on|off, starting with bass boost (W5a)
`set` is the generic switch command; `game on|off` stays as a shortcut. Only switches on the T310 allow-list can be written: game and now bass-boost (1D), which is read back at once. Any other switch is refused before anything is sent. Verified on the owner's T310 (W5a, 2026-10-27): on and off were acked, and the immediate read-back showed the change. With the EQ on pure-bass, the owner heard no clear difference. No new dependencies.
+32 −4
3 files changed
- M crates/budctl-cli/src/main.rs +11 −0
- M crates/budctl-core/src/devices/realme_t310/mod.rs +18 −3
- M docs/protocol/realme_t310.md +3 −1
| @@ -48,6 +48,13 @@ enum Command { | |||
| 48 | 48 | }, | |
| 49 | 49 | /// Select an EQ preset (pure-raw, deep-bass, pure-vocals, pure-bass), then read it back. | |
| 50 | 50 | Eq { preset: String }, | |
| 51 | + | /// Turn an on/off setting on or off, then read it back. Switches: game, bass-boost. | |
| 52 | + | Set { | |
| 53 | + | /// The switch's name, as `status --json` lists it. | |
| 54 | + | switch: String, | |
| 55 | + | #[arg(value_enum)] | |
| 56 | + | state: OnOff, | |
| 57 | + | }, | |
| 51 | 58 | /// Turn game mode (low latency) on or off, then read it back. | |
| 52 | 59 | Game { | |
| 53 | 60 | #[arg(value_enum)] | |
| @@ -142,6 +149,10 @@ fn run(cli: &Cli, out: &mut impl Write) -> Result<(), output::Failure> { | |||
| 142 | 149 | let now = session.set_eq(preset)?; | |
| 143 | 150 | output::eq(out, &device, model, &now, cli.json) | |
| 144 | 151 | } | |
| 152 | + | Command::Set { ref switch, state } => { | |
| 153 | + | let now = session.set_switch(switch, matches!(state, OnOff::On))?; | |
| 154 | + | output::switch(out, &device, model, &now, cli.json) | |
| 155 | + | } | |
| 145 | 156 | Command::Game { state } => { | |
| 146 | 157 | let now = session.set_switch("game", matches!(state, OnOff::On))?; | |
| 147 | 158 | output::switch(out, &device, model, &now, cli.json) | |
| @@ -43,7 +43,7 @@ const READ_BACK_PAUSE: Duration = Duration::from_millis(300); | |||
| 43 | 43 | ||
| 44 | 44 | /// The switches budctl may write, each added after the previous one passed its hardware | |
| 45 | 45 | /// checkpoint (CLAUDE.md rule 4). Spec: realme_t310.md#switch-write | |
| 46 | − | const WRITABLE_SWITCHES: [(&str, ReadBack); 1] = [("game", GAME_READ_BACK)]; | |
| 46 | + | const WRITABLE_SWITCHES: [(&str, ReadBack); 2] = [("game", GAME_READ_BACK), ("bass-boost", READ_BACK)]; | |
| 47 | 47 | ||
| 48 | 48 | fn on_off(on: bool) -> &'static str { | |
| 49 | 49 | if on { "on" } else { "off" } | |
| @@ -292,7 +292,7 @@ impl Session for T310Session { | |||
| 292 | 292 | return Err(Error::Unsupported(format!("these buds have no switch called {key}"))); | |
| 293 | 293 | }; | |
| 294 | 294 | let Some(&(_, timing)) = WRITABLE_SWITCHES.iter().find(|(k, _)| *k == key) else { | |
| 295 | − | return Err(Error::Unsupported(format!("budctl cannot change {key} yet"))); | |
| 295 | + | return Err(Error::Unsupported(format!("changing {key} is not supported yet"))); | |
| 296 | 296 | }; | |
| 297 | 297 | if !self.commands.allows(codec::SWITCH_WRITE) || !self.commands.allows(codec::SWITCHES) { | |
| 298 | 298 | return Err(Error::Unsupported("these buds do not offer switch settings".to_owned())); | |
| @@ -674,7 +674,7 @@ mod tests { | |||
| 674 | 674 | steps.pop(); | |
| 675 | 675 | let script = Script::new(steps); | |
| 676 | 676 | let mut session = open(&script).unwrap(); | |
| 677 | − | for key in ["dual", "wind", "spatial", "bass-boost"] { | |
| 677 | + | for key in ["dual", "wind", "spatial"] { | |
| 678 | 678 | assert!( | |
| 679 | 679 | matches!(session.set_switch(key, true), Err(Error::Unsupported(_))), | |
| 680 | 680 | "{key}" | |
| @@ -684,6 +684,21 @@ mod tests { | |||
| 684 | 684 | script.assert_done(); | |
| 685 | 685 | } | |
| 686 | 686 | ||
| 687 | + | #[test] | |
| 688 | + | fn sets_bass_boost_and_reads_it_back_at_once() { | |
| 689 | + | let mut steps = handshake(); | |
| 690 | + | steps.extend([ | |
| 691 | + | tx(0x0403, 0x05, "1D 01"), | |
| 692 | + | rx(0x8403, 0x05, "00"), | |
| 693 | + | tx(0x010D, 0x06, "06 05 06 11 1A 1B 1D"), | |
| 694 | + | rx(0x810D, 0x06, "00 06 05 00 06 00 11 00 1A 01 1B 00 1D 01"), | |
| 695 | + | ]); | |
| 696 | + | let script = Script::new(steps); | |
| 697 | + | let now = open(&script).unwrap().set_switch("bass-boost", true).unwrap(); | |
| 698 | + | script.assert_done(); | |
| 699 | + | assert_eq!((now.key, now.on), ("bass-boost", true)); | |
| 700 | + | } | |
| 701 | + | ||
| 687 | 702 | #[test] | |
| 688 | 703 | fn a_switch_the_buds_lack_is_rejected() { | |
| 689 | 704 | let mut steps = handshake(); | |
| @@ -361,7 +361,7 @@ Evidence: *vendor* | |||
| 361 | 361 | <a id="writes"></a> | |
| 362 | 362 | ## 10. Writes | |
| 363 | 363 | ||
| 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 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*), and the bass-boost switch `1D` (W5a, verified *ours-2026-10-27*). budctl reads it back at once, because the spec gives it no special timing. The others are not sent yet. | |
| 365 | 365 | ||
| 366 | 366 | **General rule**: | |
| 367 | 367 | - A write reply is `0x84xx <status …>`, and `00` means accepted. | |
| @@ -388,6 +388,7 @@ Evidence: *vendor* | |||
| 388 | 388 | - `02` or anything else: refused for now | |
| 389 | 389 | - **Check:** read `0x010D` again. | |
| 390 | 390 | - **Game mode on the T310.** The buds never push the new value (*t310*). On Buds 4 the change lands about 1 s after the ack, and a read sooner still returns the old value (*buds4*). So budctl reads back 1 s after the ack, then every 300 ms until 3 s after the ack. | |
| 391 | + | - **Bass boost on the owner's T310** (*ours-2026-10-27*, W5a): `0x0403 1D 01` and `1D 00` were acked with `00` within 30 ms. The immediate read-back already showed the new value, and no push came. With the EQ on pure-bass, the owner heard no clear difference. | |
| 391 | 392 | - **Game mode on the owner's T310** (*ours-2026-10-27*, W3): `0x0403 06 01` and `06 00` were acked with `00` within 13 ms. The read 1 s later already showed the new value. No push came, and the buds played no prompt. | |
| 392 | 393 | - **Dual connection.** Other clients also send `0x0413` afterwards. The T310's bitmap lacks it, so budctl sends only `0x0403 11`. Turning dual connection off may drop the link to one host, so it is tested last. | |
| 393 | 394 | ||
| @@ -512,6 +513,7 @@ Ask for an Android HCI snoop of realme Link before W11, and for any write that d | |||
| 512 | 513 | ||
| 513 | 514 | | Date | Checkpoint | Result | Notes | | |
| 514 | 515 | |---|---|---|---| | |
| 516 | + | | 2026-10-27 | W5a `set bass-boost on`, `off` | pass (protocol) | Acked, and the immediate read-back showed the change. The owner heard no clear difference with EQ pure-bass. See [§10.2](#switch-write) | | |
| 515 | 517 | | 2026-10-27 | W4 `eq pure-raw`, `deep-bass`, `pure-vocals`, `pure-bass` | pass | Acked and read back on the first try; the presets sound clearly different. See [§10.3](#eq-write) | | |
| 516 | 518 | | 2026-10-27 | W3 `game on`, `game off` | pass | Acked, and the 1 s read-back showed the change. No push or prompt. See [§10.2](#switch-write) | | |
| 517 | 519 | | 2026-10-27 | W2 `anc light`, `anc medium`, `anc deep`; level memory | pass | Each level acked and read back on the first try. The buds keep the level across a hold-gesture cycle. See [§10.1](#anc-write) | | |