budctl047794f
Spec: deleting a custom EQ preset works on the owner's T310 (W11e)
eq delete custom-5 went out byte for byte and was acked with 00 05; the list read back is empty. The EQ in use (deep-bass, not the deleted preset) did not change; deleting the preset in use is still untested. The owner had deleted Custom1 from budctl-gui before. budctl-gui: with no custom preset left, a new one in --demo gets the T310's layout as the buds' create does, and the "use the pencil" hint shows only when there is a custom preset to open.
+37 −18
3 files changed
- M crates/budctl-gui/src/equalizer.rs +10 −7
- M crates/budctl-gui/src/main.rs +25 −11
- M docs/protocol/realme_t310.md +2 −0
| @@ -116,12 +116,15 @@ pub fn view<'a>( | |||
| 116 | 116 | ) | |
| 117 | 117 | }) | |
| 118 | 118 | .collect(); | |
| 119 | − | let editor: Element<'a, Message> = match edited(buds, editing) { | |
| 120 | − | Some(preset) => bands(look, preset, draft, marks), | |
| 121 | − | None => text("Use the pencil on one of your presets to change its bands.") | |
| 122 | − | .size(13) | |
| 123 | − | .color(p.dim) | |
| 124 | − | .into(), | |
| 119 | + | let editor: Option<Element<'a, Message>> = match edited(buds, editing) { | |
| 120 | + | Some(preset) => Some(bands(look, preset, draft, marks)), | |
| 121 | + | None if eq.presets.iter().any(|p| p.custom) => Some( | |
| 122 | + | text("Use the pencil on one of your presets to change its bands.") | |
| 123 | + | .size(13) | |
| 124 | + | .color(p.dim) | |
| 125 | + | .into(), | |
| 126 | + | ), | |
| 127 | + | None => None, | |
| 125 | 128 | }; | |
| 126 | 129 | column![title, ui::card(look, rows), new_preset(look, marks), editor] | |
| 127 | 130 | .spacing(18) | |
| @@ -134,7 +137,7 @@ fn new_preset<'a>(look: Look, marks: Marks) -> Element<'a, Message> { | |||
| 134 | 137 | .pending | |
| 135 | 138 | .contains(&Item::NewPreset) | |
| 136 | 139 | .then(|| text("Creating…").size(13).color(look.p.icon)); | |
| 137 | − | let about = text("A new preset starts flat, with the bands of your other presets, and is put to use.") | |
| 140 | + | let about = text("A new preset starts flat and is put to use.") | |
| 138 | 141 | .size(13) | |
| 139 | 142 | .color(look.p.dim) | |
| 140 | 143 | .width(Length::Fill); | |
| @@ -545,18 +545,20 @@ impl App { | |||
| 545 | 545 | .flat_map(|eq| eq.presets.iter()) | |
| 546 | 546 | .filter(|p| p.custom) | |
| 547 | 547 | }; | |
| 548 | − | let model = customs().next().cloned(); | |
| 548 | + | // As the buds do: the bands of another preset, else the T310's own, all flat. | |
| 549 | + | let (bands, range) = customs().next().map_or_else( | |
| 550 | + | || (vec![50, 250, 1000, 4000, 8000, 16000], Some((-6, 6))), | |
| 551 | + | |model| (model.bands.iter().map(|b| b.hz).collect(), model.range), | |
| 552 | + | ); | |
| 549 | 553 | let id = customs().map(|p| p.id).max().map_or(4, |id| id.saturating_add(1)); | |
| 550 | − | if let Some(model) = model { | |
| 551 | − | self.editing = Some(id); | |
| 552 | − | self.buds.set_custom(EqPreset { | |
| 553 | − | name, | |
| 554 | − | id, | |
| 555 | − | custom: true, | |
| 556 | − | bands: model.bands.iter().map(|b| EqBand { hz: b.hz, gain: 0 }).collect(), | |
| 557 | − | range: model.range, | |
| 558 | − | }); | |
| 559 | − | } | |
| 554 | + | self.editing = Some(id); | |
| 555 | + | self.buds.set_custom(EqPreset { | |
| 556 | + | name, | |
| 557 | + | id, | |
| 558 | + | custom: true, | |
| 559 | + | bands: bands.into_iter().map(|hz| EqBand { hz, gain: 0 }).collect(), | |
| 560 | + | range, | |
| 561 | + | }); | |
| 560 | 562 | } | |
| 561 | 563 | Command::DeleteEq(_) => { | |
| 562 | 564 | let id = item_preset(item); | |
| @@ -2094,6 +2096,18 @@ mod tests { | |||
| 2094 | 2096 | let _ = app.update(Message::RenamePreset(gym)); | |
| 2095 | 2097 | press(&mut app, "Esc"); | |
| 2096 | 2098 | assert_eq!(app.prompt, None); | |
| 2099 | + | ||
| 2100 | + | // With every custom preset gone, a new one still gets bands. | |
| 2101 | + | for id in [4, gym] { | |
| 2102 | + | let _ = app.update(Message::DeletePreset(id)); | |
| 2103 | + | let _ = app.update(Message::ConfirmDelete(true)); | |
| 2104 | + | } | |
| 2105 | + | assert_eq!(app.buds.custom_names().count(), 0); | |
| 2106 | + | let _ = app.update(Message::NewPreset); | |
| 2107 | + | let _ = app.update(Message::NameInput("Fresh".to_owned())); | |
| 2108 | + | let _ = app.update(Message::SubmitName); | |
| 2109 | + | let fresh = equalizer::in_use(&app.buds).unwrap(); | |
| 2110 | + | assert_eq!((fresh.name.as_str(), fresh.bands.len()), ("Fresh", 6)); | |
| 2097 | 2111 | } | |
| 2098 | 2112 | ||
| 2099 | 2113 | #[test] | |
| @@ -518,6 +518,7 @@ Evidence: *vendor* | |||
| 518 | 518 | - <a id="custom-eq-delete"></a>**W11e, delete** (`03`), wanted by the owner (2026-10-28): `budctl eq delete custom-<id>` reads the list and sends the preset as listed, with action `03`: the write has the same layout for every action. A preset the buds do not have is refused before anything is sent. | |
| 519 | 519 | - **Check:** read the list again, as for [read-back](#write-read-back). It must have one preset fewer, and none left with the deleted one's name and bands. Ids can be renumbered after a delete, so they prove nothing. | |
| 520 | 520 | - **Unknown:** which EQ the buds use after the preset in use is deleted. budctl reads the EQ again after a delete. | |
| 521 | + | - **On the owner's T310** (*ours-2026-10-28*, W11e): the delete of Gym (id `05`, not in use) went out byte for byte as the expected frame below, and the buds answered `0x8418 00 05` within 50 ms. The list read 150 ms after the ack was `00 00`: no custom preset left. The EQ in use (deep-bass) did not change. The owner had deleted Custom1 (id `04`) before, from budctl-gui. | |
| 521 | 522 | - **Expected frame** (*derived*), for `eq delete custom-5` on the owner's list after W11d (Gym, flat), with seq `06`: `AA 22 00 00 18 04 06 1B 00 03 FA 06 05 03 47 79 6D 06 32 00 00 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E 00`. | |
| 522 | 523 | - **With no custom preset left**, a create has nothing to copy. It then uses the layout of the owner's T310 (*ours-2026-10-27*): range `FA 06` and bands at 50, 250, 1000, 4000, 8000 and 16000 Hz. | |
| 523 | 524 | ||
| @@ -575,6 +576,7 @@ The owner will not install realme Link (2026-10-27), so there is no app capture. | |||
| 575 | 576 | ||
| 576 | 577 | | Date | Checkpoint | Result | Notes | | |
| 577 | 578 | |---|---|---|---| | |
| 579 | + | | 2026-10-28 | W11e `eq delete custom-5` | pass | The delete matched the derived frame byte for byte, and was acked with `00 05`. The list read back at once is empty. Deleting the preset in use is still untested. See [§10.9](#custom-eq-delete) | | |
| 578 | 580 | | 2026-10-28 | W11d `eq rename custom-5 Gym` | pass | The save with the new name matched the derived frame byte for byte, and was acked with `00 05`. The list read back at once has id `05` named Gym, bands unchanged, in use. See [§10.9](#custom-eq-rename) | | |
| 579 | 581 | | 2026-10-28 | W11c `eq new Test` | pass | The create frame matched the derived one byte for byte, and was acked with `00 05`. The list read back at once has Test as id `05`, flat, and in use. See [§10.9](#custom-eq-create) | | |
| 580 | 582 | | 2026-10-27 | G5 `budctl-gui --debug`, Gestures page: left triple tap by dropdown and by `h`/`l`, transparency into the hold cycle and out | pass (protocol) | Every `0x0401` write (`06`, `03`, `0B`, `03`, `06`, then `05` to restore it) and both `0x0404 02 01` writes (`0B`, `09`) were acked and read back on the first try. Keys pressed during a write went out one by one as each write finished, and the last one was read back. A session closed after 10 s idle; the next pick opened a new one and wrote at once | | |