Krolyxon
Commit b37b005

Branches

Refs →

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.
Author
krolyxon
Date
2026-10-27 17:26 +0530
Parent
39f4a39
Commit
b37b00514517ccd30616b58dddd55de92786c8e0

+32 −4

3 files changed

  1. M crates/budctl-cli/src/main.rs +11 −0
  2. M crates/budctl-core/src/devices/realme_t310/mod.rs +18 −3
  3. M docs/protocol/realme_t310.md +3 −1
M crates/budctl-cli/src/main.rs View
@@ -48,6 +48,13 @@ enum Command {
4848 },
4949 /// Select an EQ preset (pure-raw, deep-bass, pure-vocals, pure-bass), then read it back.
5050 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+ },
5158 /// Turn game mode (low latency) on or off, then read it back.
5259 Game {
5360 #[arg(value_enum)]
@@ -142,6 +149,10 @@ fn run(cli: &Cli, out: &mut impl Write) -> Result<(), output::Failure> {
142149 let now = session.set_eq(preset)?;
143150 output::eq(out, &device, model, &now, cli.json)
144151 }
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+ }
145156 Command::Game { state } => {
146157 let now = session.set_switch("game", matches!(state, OnOff::On))?;
147158 output::switch(out, &device, model, &now, cli.json)
M crates/budctl-core/src/devices/realme_t310/mod.rs View
@@ -43,7 +43,7 @@ const READ_BACK_PAUSE: Duration = Duration::from_millis(300);
4343
4444 /// The switches budctl may write, each added after the previous one passed its hardware
4545 /// 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)];
4747
4848 fn on_off(on: bool) -> &'static str {
4949 if on { "on" } else { "off" }
@@ -292,7 +292,7 @@ impl Session for T310Session {
292292 return Err(Error::Unsupported(format!("these buds have no switch called {key}")));
293293 };
294294 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")));
296296 };
297297 if !self.commands.allows(codec::SWITCH_WRITE) || !self.commands.allows(codec::SWITCHES) {
298298 return Err(Error::Unsupported("these buds do not offer switch settings".to_owned()));
@@ -674,7 +674,7 @@ mod tests {
674674 steps.pop();
675675 let script = Script::new(steps);
676676 let mut session = open(&script).unwrap();
677− for key in ["dual", "wind", "spatial", "bass-boost"] {
677+ for key in ["dual", "wind", "spatial"] {
678678 assert!(
679679 matches!(session.set_switch(key, true), Err(Error::Unsupported(_))),
680680 "{key}"
@@ -684,6 +684,21 @@ mod tests {
684684 script.assert_done();
685685 }
686686
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+
687702 #[test]
688703 fn a_switch_the_buds_lack_is_rejected() {
689704 let mut steps = handshake();
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 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.
365365
366366 **General rule**:
367367 - A write reply is `0x84xx <status …>`, and `00` means accepted.
@@ -388,6 +388,7 @@ Evidence: *vendor*
388388 - `02` or anything else: refused for now
389389 - **Check:** read `0x010D` again.
390390 - **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.
391392 - **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.
392393 - **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.
393394
@@ -512,6 +513,7 @@ Ask for an Android HCI snoop of realme Link before W11, and for any write that d
512513
513514 | Date | Checkpoint | Result | Notes |
514515 |---|---|---|---|
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) |
515517 | 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) |
516518 | 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) |
517519 | 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) |