Krolyxon
Commit 2476af5

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310mod.rs

1429 lines · 53.8 KB · Rust

1//! realme Buds T310. Spec: docs/protocol/realme_t310.md
2//!
3//! `frame.rs` and `link.rs` hold the protocol family's framing and request/reply logic;
4//! `codec.rs` holds everything specific to the T310.
5
6use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
7
8use crate::bluez::PairedDevice;
9use crate::bt::Uuid;
10use crate::devices::{Device, Session};
11use crate::error::{Error, Kind, Result};
12use crate::status::{
13 EqBand, EqPreset, EqPresets, GestureChange, NoiseControl, NoiseMode, RawWrite, Status, Switch, TouchChange,
14 TouchControls,
15};
16use crate::transport::Transport;
17
18mod codec;
19mod frame;
20mod link;
21
22use codec::Commands;
23use link::Link;
24
25/// When to read a setting back after the buds ack a write. Spec: realme_t310.md#write-read-back
26#[derive(Clone, Copy)]
27struct ReadBack {
28 /// How long after the ack the first read goes out.
29 first: Duration,
30 /// How long after the ack budctl gives up.
31 window: Duration,
32}
33
34/// Spec: realme_t310.md#write-read-back
35const READ_BACK: ReadBack = ReadBack {
36 first: Duration::ZERO,
37 window: Duration::from_secs(2),
38};
39/// Game mode lands about 1 s after the ack. Spec: realme_t310.md#switch-write
40const GAME_READ_BACK: ReadBack = ReadBack {
41 first: Duration::from_secs(1),
42 window: Duration::from_secs(3),
43};
44/// The gesture table is read back about 600 ms after the ack. Spec: realme_t310.md#gesture-write
45const TOUCH_READ_BACK: ReadBack = ReadBack {
46 first: Duration::from_millis(600),
47 window: Duration::from_secs(2),
48};
49/// Spec: realme_t310.md#write-read-back
50const READ_BACK_PAUSE: Duration = Duration::from_millis(300);
51
52/// The switches budctl may write, each added after the previous one passed its hardware
53/// checkpoint (CLAUDE.md rule 4). Spec: realme_t310.md#switch-write
54const WRITABLE_SWITCHES: [(&str, ReadBack); 5] = [
55 ("game", GAME_READ_BACK),
56 ("bass-boost", READ_BACK),
57 ("spatial", READ_BACK),
58 ("wind", READ_BACK),
59 ("dual", READ_BACK),
60];
61
62fn on_off(on: bool) -> &'static str {
63 if on { "on" } else { "off" }
64}
65
66/// Spec: realme_t310.md#identity
67const BLUETOOTH_NAME: &str = "realme Buds T310";
68
69/// Control services, in the order to try. Spec: realme_t310.md#transport
70const SERVICES: [Uuid; 2] = [
71 Uuid::from_u128(0x0000_079A_D102_11E1_9B23_0002_5B00_A5A5),
72 Uuid::from_u128(0x0000_1107_D102_11E1_9B23_0002_5B00_A5A5),
73];
74
75/// Quiet time after connecting, before the first request. Spec: realme_t310.md#init
76const SETTLE: Duration = Duration::from_millis(300);
77
78pub struct RealmeT310;
79
80impl Device for RealmeT310 {
81 fn name(&self) -> &'static str {
82 BLUETOOTH_NAME
83 }
84
85 fn matches(&self, device: &PairedDevice) -> bool {
86 device.name == BLUETOOTH_NAME
87 }
88
89 fn rfcomm_uuids(&self) -> &'static [Uuid] {
90 &SERVICES
91 }
92
93 fn open(&self, transport: Box<dyn Transport>) -> Result<Box<dyn Session>> {
94 Ok(Box::new(T310Session::open(Link::new(transport, unix_time))?))
95 }
96}
97
98fn unix_time() -> u32 {
99 let seconds = SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.as_secs());
100 u32::try_from(seconds).unwrap_or(u32::MAX)
101}
102
103struct T310Session {
104 link: Link,
105 commands: Commands,
106}
107
108/// Collects what a series of best-effort reads produced.
109#[derive(Default)]
110struct Reads {
111 warnings: Vec<String>,
112 answered: usize,
113}
114
115impl T310Session {
116 /// The session start. Spec: realme_t310.md#init
117 fn open(mut link: Link) -> Result<Self> {
118 link.pump_until(Instant::now() + SETTLE)?;
119
120 let bitmap = link.request(codec::HANDSHAKE, &[]).map_err(|error| match error {
121 // Provisional until checkpoint HW1: a link that connects but stays silent is most
122 // likely held by another app. Spec: realme_t310.md#open
123 Error::Timeout(_) => Error::ChannelBusy("connected, but the buds did not answer the handshake".to_owned()),
124 other => other,
125 })?;
126 let commands = Commands::parse(&bitmap)?;
127
128 let product = codec::decode_product_id(&link.request(codec::PRODUCT_ID, &[])?)?;
129 if product != codec::T310_PRODUCT_ID {
130 return Err(Error::WrongModel {
131 expected: BLUETOOTH_NAME,
132 found: format!("{product:06X}"),
133 });
134 }
135
136 let offered = codec::decode_events_offered(&link.request(codec::EVENTS_OFFERED, &[])?)?;
137 let subscription = codec::subscribe_payload(&offered, &commands);
138 if commands.allows(codec::SUBSCRIBE) && subscription.len() > 1 {
139 // Some buds never acknowledge this, so budctl does not wait. Spec: realme_t310.md#subscribe
140 link.send(codec::SUBSCRIBE, &subscription)?;
141 }
142 Ok(Self { link, commands })
143 }
144
145 /// Read and decode one value, if the buds offer the command. A read the buds answer badly
146 /// (timeout, refusal, garbage) becomes a warning so the rest still shows; a lost link stops
147 /// everything.
148 fn read<T>(
149 &mut self,
150 reads: &mut Reads,
151 what: &str,
152 cmd: u16,
153 payload: &[u8],
154 decode: fn(&[u8]) -> Result<T>,
155 ) -> Result<Option<T>> {
156 if !self.commands.allows(cmd) {
157 return Ok(None);
158 }
159 match self.link.request(cmd, payload).and_then(|reply| decode(&reply)) {
160 Ok(value) => {
161 reads.answered += 1;
162 Ok(Some(value))
163 }
164 Err(error) if error.kind() == Kind::Failed => {
165 reads.warnings.push(format!("{what}: {error}"));
166 Ok(None)
167 }
168 Err(error) => Err(error),
169 }
170 }
171}
172
173impl T310Session {
174 /// After an acked write, read the setting back with `cmd query` until `check` reports the
175 /// new value, following `timing`. `check` returns whether the value is the one written,
176 /// and the value; `not_applied` describes a value that never changed.
177 /// Spec: realme_t310.md#write-read-back
178 fn read_back<T>(
179 &mut self,
180 timing: ReadBack,
181 cmd: u16,
182 query: &[u8],
183 check: impl Fn(&[u8]) -> Result<(bool, T)>,
184 not_applied: impl Fn(T) -> String,
185 ) -> Result<T> {
186 let acked = Instant::now();
187 self.link.pump_until(acked + timing.first)?;
188 loop {
189 let (applied, value) = check(&self.link.request(cmd, query)?)?;
190 if applied {
191 return Ok(value);
192 }
193 if Instant::now() >= acked + timing.window {
194 return Err(Error::NotApplied(not_applied(value)));
195 }
196 self.link.pump_until(Instant::now() + READ_BACK_PAUSE)?;
197 }
198 }
199}
200
201impl Session for T310Session {
202 /// Spec: realme_t310.md#buds-requests
203 fn serve(&mut self) -> Result<()> {
204 self.link.pump_until(Instant::now())
205 }
206
207 fn status(&mut self) -> Result<Status> {
208 let mut reads = Reads::default();
209 let switches = self.read(
210 &mut reads,
211 "switches",
212 codec::SWITCHES,
213 &codec::SWITCH_QUERY,
214 codec::decode_switches,
215 )?;
216 let noise_control = self.read(
217 &mut reads,
218 "noise control",
219 codec::NOISE_CONTROL,
220 &codec::NOISE_MODE_QUERY,
221 codec::decode_noise_mode,
222 )?;
223 let battery = self.read(&mut reads, "battery", codec::BATTERY, &[], codec::decode_battery)?;
224 let eq = self.read(&mut reads, "EQ", codec::EQ_PRESET, &[], codec::decode_eq)?;
225 // No bass-level read: the T310 answers 0x0124 neither with bass boost off (HW1) nor on
226 // (W6a). Spec: realme_t310.md#bass-read
227 let codec = self.read(&mut reads, "codec", codec::CODEC, &[], codec::decode_codec)?;
228 let firmware = self.read(&mut reads, "firmware", codec::FIRMWARE, &[], codec::decode_firmware)?;
229
230 if reads.answered == 0 && !reads.warnings.is_empty() {
231 return Err(Error::Timeout(reads.warnings.join("; ")));
232 }
233 Ok(Status {
234 battery: battery.unwrap_or_default(),
235 noise_control,
236 eq,
237 bass_level: None,
238 switches: switches.unwrap_or_default(),
239 codec,
240 firmware,
241 warnings: reads.warnings,
242 })
243 }
244
245 /// Spec: realme_t310.md#anc-write, #write-read-back
246 fn set_noise_control(&mut self, mode: NoiseMode) -> Result<NoiseControl> {
247 if !self.commands.allows(codec::NOISE_CONTROL_WRITE) || !self.commands.allows(codec::NOISE_CONTROL) {
248 return Err(Error::Unsupported(
249 "these buds do not offer noise-control settings".to_owned(),
250 ));
251 }
252 let reply = self
253 .link
254 .request(codec::NOISE_CONTROL_WRITE, &codec::noise_write_payload(mode))?;
255 codec::check_write_reply(&reply, "the noise-control change")?;
256
257 let wanted = codec::noise_write_mask(mode);
258 self.read_back(
259 READ_BACK,
260 codec::NOISE_CONTROL,
261 &codec::NOISE_MODE_QUERY,
262 |reply| {
263 Ok((
264 codec::decode_noise_mask(reply)? == wanted,
265 codec::decode_noise_mode(reply)?,
266 ))
267 },
268 |now| {
269 let wanted = match mode {
270 NoiseMode::Anc(level) => format!("anc ({})", level.name()),
271 _ => mode.name().to_owned(),
272 };
273 let reported = match &now.level {
274 Some(level) => format!("{} ({level})", now.mode),
275 None => now.mode,
276 };
277 format!("the buds accepted noise control {wanted}, but still report {reported}")
278 },
279 )
280 }
281
282 /// Spec: realme_t310.md#eq-read, #custom-eq
283 fn eq_presets(&mut self) -> Result<EqPresets> {
284 let mut reads = Reads::default();
285 let current = self.read(&mut reads, "EQ", codec::EQ_PRESET, &[], codec::decode_eq)?;
286 let custom = self.read(
287 &mut reads,
288 "custom EQ list",
289 codec::CUSTOM_EQ_LIST,
290 &[],
291 codec::decode_custom_eqs,
292 )?;
293 let mut presets = codec::builtin_eqs();
294 presets.extend(custom.into_iter().flatten());
295 Ok(EqPresets {
296 current,
297 presets,
298 warnings: reads.warnings,
299 })
300 }
301
302 /// Built-in presets only; custom EQs come with W11. Spec: realme_t310.md#eq-write
303 fn set_eq(&mut self, preset: &str) -> Result<String> {
304 if codec::custom_eq_id(preset).is_some() {
305 return self.set_custom_eq(preset, &[]).map(|now| format!("custom-{}", now.id));
306 }
307 let Some(id) = codec::eq_id(preset) else {
308 return Err(Error::Usage(format!(
309 "no EQ preset called {preset}; these buds have: {}, and the custom-<id> ones `budctl eq` lists",
310 codec::eq_names().join(", ")
311 )));
312 };
313 if !self.commands.allows(codec::EQ_SELECT) || !self.commands.allows(codec::EQ_PRESET) {
314 return Err(Error::Unsupported("these buds do not offer EQ presets".to_owned()));
315 }
316 let reply = self.link.request(codec::EQ_SELECT, &[id])?;
317 codec::check_write_reply(&reply, "the EQ change")?;
318
319 self.read_back(
320 READ_BACK,
321 codec::EQ_PRESET,
322 &[],
323 |reply| {
324 let now = codec::decode_eq(reply)?;
325 Ok((now == preset, now))
326 },
327 |now| format!("the buds accepted EQ {preset}, but still report {now}"),
328 )
329 }
330
331 /// Spec: realme_t310.md#custom-eq, #write-read-back
332 fn set_custom_eq(&mut self, preset: &str, bands: &[EqBand]) -> Result<EqPreset> {
333 let Some(id) = codec::custom_eq_id(preset) else {
334 return Err(Error::Usage(format!(
335 "{preset} is not a custom preset; those are named custom-<id>, as `budctl eq` lists them"
336 )));
337 };
338 if !self.commands.allows(codec::CUSTOM_EQ_WRITE) || !self.commands.allows(codec::CUSTOM_EQ_LIST) {
339 return Err(Error::Unsupported(
340 "these buds do not offer custom EQ presets".to_owned(),
341 ));
342 }
343 // A save carries the whole preset, so it starts from the list read just now.
344 let list = codec::decode_custom_eq_list(&self.link.request(codec::CUSTOM_EQ_LIST, &[])?)?;
345 let Some(current) = list.iter().find(|p| p.id == id) else {
346 let have: Vec<String> = list.iter().map(|p| format!("custom-{}", p.id)).collect();
347 return Err(Error::Usage(format!(
348 "these buds have no {preset}; their custom presets: {}",
349 if have.is_empty() {
350 "none".to_owned()
351 } else {
352 have.join(", ")
353 }
354 )));
355 };
356 let wanted = codec::retune(current, bands)?;
357 let reply = self
358 .link
359 .request(codec::CUSTOM_EQ_WRITE, &codec::custom_eq_save_payload(&wanted)?)?;
360 codec::check_write_reply(&reply, "saving the custom EQ")?;
361
362 let now = self.read_back(
363 READ_BACK,
364 codec::CUSTOM_EQ_LIST,
365 &[],
366 |reply| {
367 let now = codec::decode_custom_eq_list(reply)?.into_iter().find(|p| p.id == id);
368 let applied = now.as_ref().is_some_and(|p| p.bands == wanted.bands && p.selected);
369 Ok((applied, now))
370 },
371 |now| match now {
372 None => format!("the buds accepted {preset}, but their list no longer has it"),
373 Some(p) if p.bands == wanted.bands => format!("the buds saved {preset}, but did not select it"),
374 Some(_) => format!("the buds accepted {preset}, but still report the old bands"),
375 },
376 )?;
377 now.map(|p| p.preset())
378 .ok_or_else(|| Error::Protocol(format!("{preset} vanished from the list")))
379 }
380
381 /// Ack-only: the T310 never answers the bass-level read. Spec: realme_t310.md#bass-write
382 /// Spec: realme_t310.md#custom-eq-create, #write-read-back
383 fn create_custom_eq(&mut self, name: &str) -> Result<EqPreset> {
384 if !self.commands.allows(codec::CUSTOM_EQ_WRITE) || !self.commands.allows(codec::CUSTOM_EQ_LIST) {
385 return Err(Error::Unsupported(
386 "these buds do not offer custom EQ presets".to_owned(),
387 ));
388 }
389 let list = codec::decode_custom_eq_list(&self.link.request(codec::CUSTOM_EQ_LIST, &[])?)?;
390 if list.len() >= codec::CUSTOM_EQ_MAX {
391 return Err(Error::Usage(format!(
392 "these buds keep at most {} custom presets, and have {}; change one of them instead",
393 codec::CUSTOM_EQ_MAX,
394 list.len()
395 )));
396 }
397 if list.iter().any(|p| p.name == name.as_bytes()) {
398 return Err(Error::Usage(format!("a custom preset is called {name} already")));
399 }
400 // The new preset copies the bands the buds use: the vendor's defaults do not hold here.
401 let Some(model) = list.first() else {
402 return Err(Error::Usage(
403 "budctl copies a new preset's bands from one the buds have, and these buds have none".to_owned(),
404 ));
405 };
406 let new = codec::new_custom_eq(name, model)?;
407 let reply = self
408 .link
409 .request(codec::CUSTOM_EQ_WRITE, &codec::custom_eq_create_payload(&new)?)?;
410 let id = codec::decode_custom_eq_write_reply(&reply)?;
411 let created = self.read_back(
412 READ_BACK,
413 codec::CUSTOM_EQ_LIST,
414 &[],
415 |reply| {
416 let now = codec::decode_custom_eq_list(reply)?;
417 let created = codec::find_created(&now, &new.name, id).cloned();
418 Ok((created.is_some(), created))
419 },
420 |_| format!("the buds accepted the new preset {name}, but their list does not have it"),
421 )?;
422 created
423 .map(|p| p.preset())
424 .ok_or_else(|| Error::Protocol(format!("the new preset {name} vanished from the list")))
425 }
426
427 /// Spec: realme_t310.md#custom-eq-rename, #write-read-back
428 fn rename_custom_eq(&mut self, preset: &str, name: &str) -> Result<EqPreset> {
429 let Some(id) = codec::custom_eq_id(preset) else {
430 return Err(Error::Usage(format!(
431 "{preset} is not a custom preset; those are named custom-<id>, as `budctl eq` lists them"
432 )));
433 };
434 if !self.commands.allows(codec::CUSTOM_EQ_WRITE) || !self.commands.allows(codec::CUSTOM_EQ_LIST) {
435 return Err(Error::Unsupported(
436 "these buds do not offer custom EQ presets".to_owned(),
437 ));
438 }
439 // A save carries the whole preset, so it starts from the list read just now.
440 let list = codec::decode_custom_eq_list(&self.link.request(codec::CUSTOM_EQ_LIST, &[])?)?;
441 let Some(current) = list.iter().find(|p| p.id == id) else {
442 return Err(Error::Usage(format!("these buds have no {preset}")));
443 };
444 if list.iter().any(|p| p.id != id && p.name == name.as_bytes()) {
445 return Err(Error::Usage(format!("a custom preset is called {name} already")));
446 }
447 let wanted = codec::renamed(current, name)?;
448 let reply = self
449 .link
450 .request(codec::CUSTOM_EQ_WRITE, &codec::custom_eq_save_payload(&wanted)?)?;
451 codec::check_write_reply(&reply, "renaming the custom EQ")?;
452 let now = self.read_back(
453 READ_BACK,
454 codec::CUSTOM_EQ_LIST,
455 &[],
456 |reply| {
457 let now = codec::decode_custom_eq_list(reply)?.into_iter().find(|p| p.id == id);
458 let applied = now.as_ref().is_some_and(|p| p.name == wanted.name);
459 Ok((applied, now))
460 },
461 |now| match now {
462 None => format!("the buds accepted the new name, but their list no longer has {preset}"),
463 Some(_) => format!("the buds accepted the new name, but still list {preset} under the old one"),
464 },
465 )?;
466 now.map(|p| p.preset())
467 .ok_or_else(|| Error::Protocol(format!("{preset} vanished from the list")))
468 }
469
470 /// Spec: realme_t310.md#bass-write
471 fn bass_range(&self) -> Option<(i8, i8)> {
472 self.commands
473 .allows(codec::BASS_LEVEL_WRITE)
474 .then_some(codec::BASS_RANGE)
475 }
476
477 fn set_bass_level(&mut self, level: i8) -> Result<()> {
478 let payload = codec::bass_write_payload(level)?;
479 if !self.commands.allows(codec::BASS_LEVEL_WRITE) {
480 return Err(Error::Unsupported("these buds do not offer a bass level".to_owned()));
481 }
482 let reply = self.link.request(codec::BASS_LEVEL_WRITE, &payload)?;
483 codec::check_write_reply(&reply, "the bass-level change")
484 }
485
486 /// Spec: realme_t310.md#find
487 fn find(&mut self, ring: bool) -> Result<()> {
488 if !self.commands.allows(codec::FIND) {
489 return Err(Error::Unsupported("these buds cannot ring through budctl".to_owned()));
490 }
491 let reply = self.link.request(codec::FIND, &codec::find_payload(ring))?;
492 codec::check_write_reply(&reply, if ring { "ringing" } else { "stopping the ringing" })
493 }
494
495 /// Spec: realme_t310.md#switch-write, #write-read-back
496 fn set_switch(&mut self, key: &str, on: bool) -> Result<Switch> {
497 let Some(id) = codec::switch_id(key) else {
498 return Err(Error::Unsupported(format!("these buds have no switch called {key}")));
499 };
500 let Some(&(_, timing)) = WRITABLE_SWITCHES.iter().find(|(k, _)| *k == key) else {
501 return Err(Error::Unsupported(format!("changing {key} is not supported yet")));
502 };
503 if !self.commands.allows(codec::SWITCH_WRITE) || !self.commands.allows(codec::SWITCHES) {
504 return Err(Error::Unsupported("these buds do not offer switch settings".to_owned()));
505 }
506 let reply = self
507 .link
508 .request(codec::SWITCH_WRITE, &codec::switch_write_payload(id, on))?;
509 codec::check_write_reply(&reply, "the switch change")?;
510
511 self.read_back(
512 timing,
513 codec::SWITCHES,
514 &codec::SWITCH_QUERY,
515 |reply| {
516 let switch = codec::decode_switches(reply)?
517 .into_iter()
518 .find(|s| s.key == key)
519 .ok_or_else(|| Error::Protocol(format!("the switch reply has no {key}")))?;
520 Ok((switch.on == on, switch))
521 },
522 |now| {
523 format!(
524 "the buds accepted {} {}, but still report {}",
525 now.label,
526 on_off(on),
527 on_off(now.on)
528 )
529 },
530 )
531 }
532
533 fn touch_controls(&mut self) -> Result<TouchControls> {
534 if !self.commands.allows(codec::GESTURES) {
535 return Err(Error::Unsupported(
536 "these buds do not offer touch-control settings".to_owned(),
537 ));
538 }
539 let gestures = codec::decode_gestures(&self.link.request(codec::GESTURES, &[])?)?;
540 let mut reads = Reads::default();
541 let hold_cycle = self.read(
542 &mut reads,
543 "hold cycle",
544 codec::NOISE_CONTROL,
545 &codec::HOLD_CYCLE_QUERY,
546 codec::decode_hold_cycle,
547 )?;
548 // The choices only when the buds take writes: the GUI offers what it shows here.
549 let writable = |cmd| self.commands.allows(cmd);
550 let choices = if writable(codec::GESTURE_WRITE) {
551 codec::touch_choices()
552 } else {
553 Vec::new()
554 };
555 let cycle_modes = if hold_cycle.is_some() && writable(codec::NOISE_CONTROL_WRITE) {
556 codec::cycle_modes()
557 } else {
558 Vec::new()
559 };
560 Ok(TouchControls {
561 gestures,
562 hold_cycle,
563 choices,
564 cycle_modes,
565 warnings: reads.warnings,
566 })
567 }
568
569 /// Spec: realme_t310.md#hold-cycle-write, #write-read-back
570 fn set_hold_cycle(&mut self, modes: &[&str]) -> Result<Vec<String>> {
571 let mask = codec::hold_cycle_mask(modes)?;
572 if !self.commands.allows(codec::NOISE_CONTROL_WRITE) || !self.commands.allows(codec::NOISE_CONTROL) {
573 return Err(Error::Unsupported(
574 "these buds do not offer noise-control settings".to_owned(),
575 ));
576 }
577 let reply = self
578 .link
579 .request(codec::NOISE_CONTROL_WRITE, &codec::hold_cycle_write_payload(mask))?;
580 codec::check_write_reply(&reply, "the hold-cycle change")?;
581
582 self.read_back(
583 READ_BACK,
584 codec::NOISE_CONTROL,
585 &codec::HOLD_CYCLE_QUERY,
586 |reply| {
587 Ok((
588 codec::decode_hold_cycle_mask(reply)? == mask,
589 codec::decode_hold_cycle(reply)?,
590 ))
591 },
592 |now| {
593 format!(
594 "the buds accepted the hold cycle {}, but still report {}",
595 modes.join(" → "),
596 now.join(" → ")
597 )
598 },
599 )
600 }
601
602 /// Spec: realme_t310.md#gesture-write, #gesture-choices
603 fn set_touch(&mut self, side: &str, gesture: &str, action: &str, dry_run: bool) -> Result<TouchChange> {
604 let target = codec::touch_target(side, gesture, action)?;
605 if !self.commands.allows(codec::GESTURE_WRITE) || !self.commands.allows(codec::GESTURES) {
606 return Err(Error::Unsupported(
607 "these buds do not offer touch-control settings".to_owned(),
608 ));
609 }
610 // The write carries the whole table, so it starts from the table read just now.
611 let before = codec::decode_gesture_entries(&self.link.request(codec::GESTURES, &[])?)?;
612 let after = codec::retarget_gestures(&before, &target)?;
613 let changes: Vec<GestureChange> = before
614 .iter()
615 .zip(&after)
616 .filter(|(old, new)| old != new)
617 .map(|(old, new)| {
618 let (old, new) = (codec::name_gesture(old), codec::name_gesture(new));
619 GestureChange {
620 side: new.side,
621 gesture: new.gesture,
622 from: old.action,
623 to: new.action,
624 }
625 })
626 .collect();
627 let planned: Vec<_> = after.iter().map(codec::name_gesture).collect();
628 if changes.is_empty() {
629 return Ok(TouchChange {
630 changes,
631 write: None,
632 sent: false,
633 gestures: planned,
634 });
635 }
636 let write = RawWrite {
637 command: codec::GESTURE_WRITE,
638 payload: codec::gesture_write_payload(&after)?,
639 };
640 if dry_run {
641 return Ok(TouchChange {
642 changes,
643 write: Some(write),
644 sent: false,
645 gestures: planned,
646 });
647 }
648
649 let reply = self.link.request(write.command, &write.payload)?;
650 codec::check_write_reply(&reply, "the touch-control change")?;
651 let gestures = self.read_back(
652 TOUCH_READ_BACK,
653 codec::GESTURES,
654 &[],
655 |reply| {
656 let now = codec::decode_gesture_entries(reply)?;
657 // Applied when retargeting the table read back would change nothing.
658 let applied = codec::retarget_gestures(&now, &target).is_ok_and(|again| again == now);
659 Ok((applied, now))
660 },
661 |now| {
662 let still: Vec<String> = changes
663 .iter()
664 .filter_map(|change| {
665 now.iter()
666 .map(codec::name_gesture)
667 .find(|g| g.group.is_none() && g.side == change.side && g.gesture == change.gesture)
668 })
669 .filter(|g| g.action != action)
670 .map(|g| format!("{} {} {}", g.side, g.gesture, g.action))
671 .collect();
672 if still.is_empty() {
673 format!("the buds accepted {side} {gesture} {action}, but the table read back does not show it")
674 } else {
675 format!(
676 "the buds accepted {side} {gesture} {action}, but still report {}",
677 still.join(", ")
678 )
679 }
680 },
681 )?;
682 Ok(TouchChange {
683 changes,
684 write: Some(write),
685 sent: true,
686 gestures: gestures.iter().map(codec::name_gesture).collect(),
687 })
688 }
689}
690
691#[cfg(test)]
692mod tests {
693 use super::*;
694 use crate::status::{AncLevel, Charge};
695 use crate::transport::mock::{Script, Step, hex};
696
697 fn tx(cmd: u16, seq: u8, payload: &str) -> Step {
698 Step::Tx(frame::encode(cmd, seq, &hex(payload)).unwrap())
699 }
700
701 fn rx(cmd: u16, seq: u8, payload: &str) -> Step {
702 Step::Rx(frame::encode(cmd, seq, &hex(payload)).unwrap())
703 }
704
705 fn fixed_clock() -> u32 {
706 1_750_000_000
707 }
708
709 fn open(script: &Script) -> Result<T310Session> {
710 T310Session::open(Link::new(script.transport(), fixed_clock))
711 }
712
713 /// The handshake of the owner's T310 (HW1); seq 01-04 are used up.
714 fn handshake() -> Vec<Step> {
715 vec![
716 tx(0x0100, 0x01, ""),
717 rx(0x8100, 0x01, codec::T310_BITMAP_REPLY),
718 tx(0x0103, 0x02, ""),
719 rx(0x8103, 0x02, "00 12 3C 06"),
720 tx(0x0200, 0x03, ""),
721 rx(0x8200, 0x03, "00 09 01 02 03 04 05 06 0D F1 F4"),
722 tx(0x0205, 0x04, "04 01 02 03 04"),
723 rx(0x8205, 0x04, "00 04 01 00 02 00 03 00 04 00"),
724 ]
725 }
726
727 #[test]
728 fn serve_answers_the_buds_at_once() {
729 let mut steps = handshake();
730 // The time request, at Unix time 1750000000. Spec: realme_t310.md#buds-requests
731 steps.extend([rx(0x0500, 0x02, ""), tx(0x8500, 0x02, "00 80 E1 4E 68")]);
732 let script = Script::new(steps);
733 let mut session = open(&script).unwrap();
734 session.serve().unwrap();
735 script.assert_done();
736
737 let mut steps = handshake();
738 steps.push(Step::Close);
739 let script = Script::new(steps);
740 let mut session = open(&script).unwrap();
741 assert!(matches!(session.serve(), Err(Error::NotConnected(_))));
742 }
743
744 #[test]
745 fn full_status() {
746 let mut steps = handshake();
747 let mut firmware = String::from("00 03");
748 for byte in b"1,2,120,2,2,120,3,2,105" {
749 firmware.push_str(&format!(" {byte:02X}"));
750 }
751 steps.extend([
752 tx(0x010D, 0x05, "06 05 06 11 1A 1B 1D"),
753 rx(0x810D, 0x05, "00 06 05 00 06 00 11 01 1A 00 1B 01 1D 01"),
754 tx(0x010C, 0x06, "01 01"),
755 rx(0x810C, 0x06, "00 01 01 08"),
756 tx(0x0106, 0x07, ""),
757 rx(0x8106, 0x07, "00 03 01 50 02 E4 03 3C"),
758 tx(0x010F, 0x08, ""),
759 rx(0x810F, 0x08, "00 01"),
760 tx(0x0114, 0x09, ""),
761 rx(0x8114, 0x09, "00 02"),
762 tx(0x0105, 0x0A, ""),
763 rx(0x8105, 0x0A, &firmware),
764 ]);
765 let script = Script::new(steps);
766 let status = open(&script).unwrap().status().unwrap();
767 script.assert_done();
768
769 assert_eq!(
770 status.battery.left,
771 Some(Charge {
772 level: 80,
773 charging: false
774 })
775 );
776 assert_eq!(
777 status.battery.right,
778 Some(Charge {
779 level: 100,
780 charging: true
781 })
782 );
783 assert_eq!(
784 status.battery.case,
785 Some(Charge {
786 level: 60,
787 charging: false
788 })
789 );
790 let noise = status.noise_control.unwrap();
791 assert_eq!((noise.mode.as_str(), noise.level.as_deref()), ("anc", Some("deep")));
792 assert_eq!(status.eq.as_deref(), Some("deep-bass"));
793 assert_eq!(status.bass_level, None);
794 assert_eq!(status.codec.as_deref(), Some("AAC"));
795 assert_eq!(status.firmware.as_deref(), Some("120.120.105"));
796 let game = status.switches.iter().find(|s| s.key == "game").unwrap();
797 assert!(!game.on);
798 assert_eq!(status.switches.len(), 5);
799 assert!(status.warnings.is_empty());
800 }
801
802 #[test]
803 fn answers_the_time_request_during_the_settle() {
804 let mut steps = vec![
805 rx(0x0500, 0x01, ""),
806 Step::Tx(hex("AA 0C 00 00 00 85 01 05 00 00 80 E1 4E 68")),
807 ];
808 steps.extend(handshake());
809 // No 0x8205 ack this time: it may never arrive, and `open` does not wait for it.
810 // Spec: realme_t310.md#subscribe
811 steps.pop();
812 let script = Script::new(steps);
813 open(&script).unwrap();
814 script.assert_done();
815 }
816
817 #[test]
818 fn a_silent_handshake_reports_a_busy_channel() {
819 let script = Script::new(vec![tx(0x0100, 0x01, "")]);
820 assert!(matches!(open(&script), Err(Error::ChannelBusy(_))));
821 }
822
823 #[test]
824 fn other_models_are_refused() {
825 let script = Script::new(vec![
826 tx(0x0100, 0x01, ""),
827 rx(0x8100, 0x01, codec::T310_BITMAP_REPLY),
828 tx(0x0103, 0x02, ""),
829 rx(0x8103, 0x02, "00 14 54 06"),
830 ]);
831 match open(&script) {
832 Err(Error::WrongModel { found, .. }) => assert_eq!(found, "065414"),
833 other => panic!("expected WrongModel, got {:?}", other.err()),
834 }
835 }
836
837 #[test]
838 fn commands_missing_from_the_bitmap_are_never_sent() {
839 // A bitmap with no bits: only always-allowed commands go out, and no subscription.
840 let script = Script::new(vec![
841 tx(0x0100, 0x01, ""),
842 rx(0x8100, 0x01, "00"),
843 tx(0x0103, 0x02, ""),
844 rx(0x8103, 0x02, "00 12 3C 06"),
845 tx(0x0200, 0x03, ""),
846 rx(0x8200, 0x03, "00 02 01 02"),
847 tx(0x010D, 0x04, "06 05 06 11 1A 1B 1D"),
848 rx(0x810D, 0x04, "00 01 06 01"),
849 tx(0x0106, 0x05, ""),
850 // no battery reply: becomes a warning
851 ]);
852 let status = open(&script).unwrap().status().unwrap();
853 script.assert_done();
854 assert_eq!(status.switches.len(), 1);
855 assert!(status.noise_control.is_none() && status.firmware.is_none());
856 assert_eq!(status.warnings.len(), 1);
857 assert!(status.warnings.first().unwrap().starts_with("battery"));
858 }
859
860 #[test]
861 fn a_lost_link_stops_the_status() {
862 let mut steps = handshake();
863 steps.extend([tx(0x010D, 0x05, "06 05 06 11 1A 1B 1D"), Step::Close]);
864 let script = Script::new(steps);
865 assert!(matches!(open(&script).unwrap().status(), Err(Error::NotConnected(_))));
866 }
867
868 #[test]
869 fn touch_controls_and_hold_cycle() {
870 let mut steps = handshake();
871 steps.extend([
872 tx(0x0108, 0x05, ""),
873 rx(0x8108, 0x05, "00 02 01 01 02 01 02 01 04 08"),
874 tx(0x010C, 0x06, "02 01"),
875 rx(0x810C, 0x06, "00 02 01 0A"),
876 ]);
877 let script = Script::new(steps);
878 let touch = open(&script).unwrap().touch_controls().unwrap();
879 script.assert_done();
880 assert_eq!(touch.gestures.len(), 2);
881 assert_eq!(touch.hold_cycle.unwrap(), ["transparency", "anc"]);
882 let gestures: Vec<&str> = touch.choices.iter().map(|c| c.gesture.as_str()).collect();
883 assert_eq!(gestures, ["double-tap", "triple-tap", "hold"]);
884 let hold = touch.choices.iter().find(|c| c.gesture == "hold").unwrap();
885 assert_eq!(hold.actions, ["none", "noise-cycle", "voice-assistant", "game-mode"]);
886 assert_eq!(touch.cycle_modes, ["off", "transparency", "anc"]);
887 }
888
889 /// The owner's `budctl --debug touch` (HW1), byte for byte. The buds' own frames (slice
890 /// byte 08) arrive glued to replies and need no answer.
891 #[test]
892 fn replays_the_hw1_touch_session() {
893 let script = Script::new(vec![
894 Step::Tx(hex("AA 07 00 00 00 01 01 00 00")),
895 Step::Rx(hex(
896 "AA 10 00 00 00 81 01 09 00 00 BF 1F 5A 20 45 04 40 00 AA 07 08 00 01 05 0F 00 00",
897 )),
898 Step::Tx(hex("AA 07 00 00 03 01 02 00 00")),
899 Step::Rx(hex("AA 0B 00 00 03 81 02 04 00 00 12 3C 06")),
900 Step::Tx(hex("AA 07 00 00 00 02 03 00 00")),
901 Step::Rx(hex("AA 12 00 00 00 82 03 0B 00 00 09 01 02 03 04 05 06 0D F1 F4")),
902 Step::Tx(hex("AA 0C 00 00 05 02 04 05 00 04 01 02 03 04")),
903 Step::Rx(hex(
904 "AA 11 00 00 05 82 04 0A 00 00 04 01 00 02 00 03 00 04 00 AA 0F 08 00 04 02 10 08 00 01 03 02 64 01 64 03 00",
905 )),
906 Step::Tx(hex("AA 07 00 00 08 01 05 00 00")),
907 Step::Rx(hex(
908 "AA 21 00 00 08 81 05 1A 00 00 06 01 01 02 01 01 01 03 06 01 01 04 08 02 01 02 01 02 01 03 06 02 01 04 08",
909 )),
910 Step::Tx(hex("AA 09 00 00 0C 01 06 02 00 02 01")),
911 Step::Rx(hex("AA 0B 00 00 0C 81 06 04 00 00 02 01 0A")),
912 ]);
913 let touch = open(&script).unwrap().touch_controls().unwrap();
914 script.assert_done();
915 let table: Vec<(&str, &str, &str)> = touch
916 .gestures
917 .iter()
918 .map(|g| (g.side.as_str(), g.gesture.as_str(), g.action.as_str()))
919 .collect();
920 assert_eq!(
921 table,
922 [
923 ("left", "double-tap", "play-pause"),
924 ("left", "triple-tap", "next"),
925 ("left", "hold", "noise-cycle"),
926 ("right", "double-tap", "play-pause"),
927 ("right", "triple-tap", "next"),
928 ("right", "hold", "noise-cycle"),
929 ]
930 );
931 assert_eq!(touch.hold_cycle.unwrap(), ["transparency", "anc"]);
932 assert!(touch.warnings.is_empty());
933 }
934
935 #[test]
936 fn sets_noise_control_and_reads_it_back() {
937 let mut steps = handshake();
938 steps.extend([
939 tx(0x0404, 0x05, "01 01 02"),
940 rx(0x8404, 0x05, "00"),
941 // The buds announce the change; it needs no answer.
942 rx(0x0204, 0x10, "03 01 01 02"),
943 tx(0x010C, 0x06, "01 01"),
944 rx(0x810C, 0x06, "00 01 01 02"),
945 ]);
946 let script = Script::new(steps);
947 let now = open(&script)
948 .unwrap()
949 .set_noise_control(NoiseMode::Transparency)
950 .unwrap();
951 script.assert_done();
952 assert_eq!((now.mode.as_str(), now.level), ("transparency", None));
953 }
954
955 #[test]
956 fn reads_back_again_until_the_change_shows() {
957 let mut steps = handshake();
958 steps.extend([
959 tx(0x0404, 0x05, "01 01 01"),
960 rx(0x8404, 0x05, "00"),
961 tx(0x010C, 0x06, "01 01"),
962 rx(0x810C, 0x06, "00 01 01 08"),
963 tx(0x010C, 0x07, "01 01"),
964 rx(0x810C, 0x07, "00 01 01 01"),
965 ]);
966 let script = Script::new(steps);
967 let now = open(&script).unwrap().set_noise_control(NoiseMode::Off).unwrap();
968 script.assert_done();
969 assert_eq!(now.mode, "off");
970 }
971
972 #[test]
973 fn sets_a_noise_cancelling_level() {
974 let mut steps = handshake();
975 steps.extend([
976 tx(0x0404, 0x05, "01 01 04"),
977 rx(0x8404, 0x05, "00"),
978 tx(0x010C, 0x06, "01 01"),
979 rx(0x810C, 0x06, "00 01 01 04"),
980 ]);
981 let script = Script::new(steps);
982 let now = open(&script)
983 .unwrap()
984 .set_noise_control(NoiseMode::Anc(AncLevel::Light))
985 .unwrap();
986 script.assert_done();
987 assert_eq!((now.mode.as_str(), now.level.as_deref()), ("anc", Some("light")));
988 }
989
990 #[test]
991 fn a_rejected_noise_control_change_is_not_read_back() {
992 let mut steps = handshake();
993 steps.extend([tx(0x0404, 0x05, "01 01 08"), rx(0x8404, 0x05, "0E")]);
994 let script = Script::new(steps);
995 let result = open(&script).unwrap().set_noise_control(NoiseMode::Anc(AncLevel::Deep));
996 script.assert_done();
997 assert!(matches!(result, Err(Error::Rejected { status: 0x0E, .. })));
998 }
999
1000 #[test]
1001 fn noise_control_is_not_written_without_its_bitmap_bit() {
1002 let script = Script::new(vec![
1003 tx(0x0100, 0x01, ""),
1004 rx(0x8100, 0x01, "00"),
1005 tx(0x0103, 0x02, ""),
1006 rx(0x8103, 0x02, "00 12 3C 06"),
1007 tx(0x0200, 0x03, ""),
1008 rx(0x8200, 0x03, "00 00"),
1009 ]);
1010 let result = open(&script).unwrap().set_noise_control(NoiseMode::Off);
1011 script.assert_done();
1012 assert!(matches!(result, Err(Error::Unsupported(_))));
1013 }
1014
1015 #[test]
1016 fn sets_game_mode_and_reads_it_back() {
1017 let mut steps = handshake();
1018 steps.extend([
1019 tx(0x0403, 0x05, "06 01"),
1020 rx(0x8403, 0x05, "00"),
1021 // Still the old value: budctl reads again.
1022 tx(0x010D, 0x06, "06 05 06 11 1A 1B 1D"),
1023 rx(0x810D, 0x06, "00 06 05 00 06 00 11 00 1A 01 1B 00 1D 00"),
1024 tx(0x010D, 0x07, "06 05 06 11 1A 1B 1D"),
1025 rx(0x810D, 0x07, "00 06 05 00 06 01 11 00 1A 01 1B 00 1D 00"),
1026 ]);
1027 let script = Script::new(steps);
1028 let game = open(&script).unwrap().set_switch("game", true).unwrap();
1029 script.assert_done();
1030 assert_eq!((game.key, game.on), ("game", true));
1031 }
1032
1033 #[test]
1034 fn unknown_switches_are_never_written() {
1035 // Nothing is sent after the handshake, so the subscribe ack is left out.
1036 let mut steps = handshake();
1037 steps.pop();
1038 let script = Script::new(steps);
1039 let mut session = open(&script).unwrap();
1040 assert!(matches!(session.set_switch("turbo", true), Err(Error::Unsupported(_))));
1041 script.assert_done();
1042 }
1043
1044 #[test]
1045 fn sets_bass_boost_and_reads_it_back_at_once() {
1046 let mut steps = handshake();
1047 steps.extend([
1048 tx(0x0403, 0x05, "1D 01"),
1049 rx(0x8403, 0x05, "00"),
1050 tx(0x010D, 0x06, "06 05 06 11 1A 1B 1D"),
1051 rx(0x810D, 0x06, "00 06 05 00 06 00 11 00 1A 01 1B 00 1D 01"),
1052 ]);
1053 let script = Script::new(steps);
1054 let now = open(&script).unwrap().set_switch("bass-boost", true).unwrap();
1055 script.assert_done();
1056 assert_eq!((now.key, now.on), ("bass-boost", true));
1057 }
1058
1059 #[test]
1060 fn a_switch_the_buds_lack_is_rejected() {
1061 let mut steps = handshake();
1062 steps.extend([tx(0x0403, 0x05, "06 00"), rx(0x8403, 0x05, "01")]);
1063 let script = Script::new(steps);
1064 let result = open(&script).unwrap().set_switch("game", false);
1065 script.assert_done();
1066 assert!(matches!(result, Err(Error::Rejected { status: 0x01, .. })));
1067 }
1068
1069 #[test]
1070 fn selects_an_eq_preset_and_reads_it_back() {
1071 let mut steps = handshake();
1072 steps.extend([
1073 tx(0x0406, 0x05, "01"),
1074 rx(0x8406, 0x05, "00"),
1075 tx(0x010F, 0x06, ""),
1076 rx(0x810F, 0x06, "00 01"),
1077 ]);
1078 let script = Script::new(steps);
1079 let now = open(&script).unwrap().set_eq("deep-bass").unwrap();
1080 script.assert_done();
1081 assert_eq!(now, "deep-bass");
1082 }
1083
1084 #[test]
1085 fn an_unknown_eq_preset_is_a_usage_error() {
1086 let mut steps = handshake();
1087 steps.pop();
1088 let script = Script::new(steps);
1089 let result = open(&script).unwrap().set_eq("loudness");
1090 script.assert_done();
1091 match result {
1092 Err(Error::Usage(message)) => assert!(message.contains("pure-raw, deep-bass, pure-vocals, pure-bass")),
1093 other => panic!("expected a usage error, got {other:?}"),
1094 }
1095 }
1096
1097 #[test]
1098 fn rings_and_stops() {
1099 let mut steps = handshake();
1100 steps.extend([
1101 tx(0x0400, 0x05, "01"),
1102 rx(0x8400, 0x05, "00"),
1103 tx(0x0400, 0x06, "00"),
1104 rx(0x8400, 0x06, "00"),
1105 ]);
1106 let script = Script::new(steps);
1107 let mut session = open(&script).unwrap();
1108 session.find(true).unwrap();
1109 session.find(false).unwrap();
1110 script.assert_done();
1111 }
1112
1113 #[test]
1114 fn find_is_not_sent_without_its_bitmap_bit() {
1115 // Bits 0-4 only: no bit 5.
1116 let script = Script::new(vec![
1117 tx(0x0100, 0x01, ""),
1118 rx(0x8100, 0x01, "00 1F"),
1119 tx(0x0103, 0x02, ""),
1120 rx(0x8103, 0x02, "00 12 3C 06"),
1121 tx(0x0200, 0x03, ""),
1122 rx(0x8200, 0x03, "00 00"),
1123 ]);
1124 let result = open(&script).unwrap().find(true);
1125 script.assert_done();
1126 assert!(matches!(result, Err(Error::Unsupported(_))));
1127 }
1128
1129 /// The owner's gesture table (HW1) and the same table with the left triple tap on volume up.
1130 const HW1_TABLE: &str = "00 06 01 01 02 01 01 01 03 06 01 01 04 08 02 01 02 01 02 01 03 06 02 01 04 08";
1131 const LEFT_VOLUME_UP: &str = "06 01 01 02 01 01 01 03 0B 01 01 04 08 02 01 02 01 02 01 03 06 02 01 04 08";
1132
1133 #[test]
1134 fn a_touch_dry_run_reads_the_table_and_sends_nothing() {
1135 let mut steps = handshake();
1136 steps.extend([tx(0x0108, 0x05, ""), rx(0x8108, 0x05, HW1_TABLE)]);
1137 let script = Script::new(steps);
1138 let change = open(&script)
1139 .unwrap()
1140 .set_touch("left", "triple-tap", "volume-up", true)
1141 .unwrap();
1142 script.assert_done();
1143 assert!(!change.sent);
1144 let write = change.write.unwrap();
1145 assert_eq!((write.command, write.payload), (0x0401, hex(LEFT_VOLUME_UP)));
1146 assert_eq!(
1147 change.changes,
1148 [GestureChange {
1149 side: "left".to_owned(),
1150 gesture: "triple-tap".to_owned(),
1151 from: "next".to_owned(),
1152 to: "volume-up".to_owned(),
1153 }]
1154 );
1155 let left_triple = change.gestures.get(1).unwrap();
1156 assert_eq!(left_triple.action, "volume-up");
1157 }
1158
1159 #[test]
1160 fn sets_a_gesture_and_reads_the_table_back() {
1161 let mut steps = handshake();
1162 steps.extend([
1163 tx(0x0108, 0x05, ""),
1164 rx(0x8108, 0x05, HW1_TABLE),
1165 tx(0x0401, 0x06, LEFT_VOLUME_UP),
1166 // The owner's ack (W8): 00, then the old table cut to the write's length.
1167 rx(
1168 0x8401,
1169 0x06,
1170 "00 06 01 01 02 01 01 01 03 06 01 01 04 08 02 01 02 01 02 01 03 06 02 01 04",
1171 ),
1172 tx(0x0108, 0x07, ""),
1173 rx(0x8108, 0x07, &format!("00 {LEFT_VOLUME_UP}")),
1174 ]);
1175 let script = Script::new(steps);
1176 let change = open(&script)
1177 .unwrap()
1178 .set_touch("left", "triple-tap", "volume-up", false)
1179 .unwrap();
1180 script.assert_done();
1181 assert!(change.sent);
1182 assert_eq!(change.changes.len(), 1);
1183 assert_eq!(change.gestures.get(1).unwrap().action, "volume-up");
1184 }
1185
1186 #[test]
1187 fn a_gesture_that_already_has_the_action_is_not_written() {
1188 let mut steps = handshake();
1189 steps.extend([tx(0x0108, 0x05, ""), rx(0x8108, 0x05, HW1_TABLE)]);
1190 let script = Script::new(steps);
1191 let change = open(&script)
1192 .unwrap()
1193 .set_touch("both", "hold", "noise-cycle", false)
1194 .unwrap();
1195 script.assert_done();
1196 assert!(change.changes.is_empty() && change.write.is_none() && !change.sent);
1197 }
1198
1199 #[test]
1200 fn a_bad_touch_action_is_refused_before_anything_is_sent() {
1201 let mut steps = handshake();
1202 steps.pop();
1203 let script = Script::new(steps);
1204 let result = open(&script)
1205 .unwrap()
1206 .set_touch("left", "double-tap", "volume-up", false);
1207 script.assert_done();
1208 assert!(matches!(result, Err(Error::Usage(_))));
1209 }
1210
1211 #[test]
1212 fn sets_the_hold_cycle_and_reads_it_back() {
1213 let mut steps = handshake();
1214 steps.extend([
1215 tx(0x0404, 0x05, "02 01 0B"),
1216 rx(0x8404, 0x05, "00"),
1217 tx(0x010C, 0x06, "02 01"),
1218 rx(0x810C, 0x06, "00 02 01 0B"),
1219 ]);
1220 let script = Script::new(steps);
1221 let now = open(&script)
1222 .unwrap()
1223 .set_hold_cycle(&["off", "transparency", "anc"])
1224 .unwrap();
1225 script.assert_done();
1226 assert_eq!(now, ["off", "transparency", "anc"]);
1227 }
1228
1229 #[test]
1230 fn a_one_mode_hold_cycle_is_refused_before_anything_is_sent() {
1231 let mut steps = handshake();
1232 steps.pop();
1233 let script = Script::new(steps);
1234 let result = open(&script).unwrap().set_hold_cycle(&["anc"]);
1235 script.assert_done();
1236 assert!(matches!(result, Err(Error::Usage(_))));
1237 }
1238
1239 /// The custom EQ list of the owner's T310 (W11a): one flat preset, "Custom1", id 04.
1240 const OWNER_CUSTOM_EQS: &str =
1241 "00 01 01 FA 06 04 07 43 75 73 74 6F 6D 31 06 32 00 00 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E 00";
1242
1243 #[test]
1244 fn lists_built_in_and_custom_eq_presets() {
1245 let mut steps = handshake();
1246 steps.extend([
1247 tx(0x010F, 0x05, ""),
1248 rx(0x810F, 0x05, "00 04"),
1249 tx(0x0122, 0x06, ""),
1250 // The owner's T310 (W11a).
1251 rx(0x8122, 0x06, OWNER_CUSTOM_EQS),
1252 ]);
1253 let script = Script::new(steps);
1254 let eq = open(&script).unwrap().eq_presets().unwrap();
1255 script.assert_done();
1256 assert_eq!(eq.current.as_deref(), Some("custom-4"));
1257 let names: Vec<(&str, bool)> = eq.presets.iter().map(|p| (p.name.as_str(), p.custom)).collect();
1258 assert_eq!(
1259 names,
1260 [
1261 ("pure-raw", false),
1262 ("deep-bass", false),
1263 ("pure-vocals", false),
1264 ("pure-bass", false),
1265 ("Custom1", true)
1266 ]
1267 );
1268 let bands: Vec<u16> = eq.presets.last().unwrap().bands.iter().map(|b| b.hz).collect();
1269 assert_eq!(bands, [50, 250, 1000, 4000, 8000, 16000]);
1270 assert!(eq.warnings.is_empty());
1271 }
1272
1273 #[test]
1274 fn sets_the_bass_level() {
1275 let mut steps = handshake();
1276 steps.extend([tx(0x041B, 0x05, "FB 05 FE"), rx(0x841B, 0x05, "00")]);
1277 let script = Script::new(steps);
1278 let mut session = open(&script).unwrap();
1279 assert_eq!(session.bass_range(), Some((-5, 5)));
1280 session.set_bass_level(-2).unwrap();
1281 script.assert_done();
1282 }
1283
1284 #[test]
1285 fn a_bass_level_out_of_range_is_refused_before_anything_is_sent() {
1286 let mut steps = handshake();
1287 steps.pop();
1288 let script = Script::new(steps);
1289 let result = open(&script).unwrap().set_bass_level(6);
1290 script.assert_done();
1291 assert!(matches!(result, Err(Error::Usage(_))));
1292 }
1293
1294 #[test]
1295 fn creates_a_custom_preset_and_finds_it_in_the_list() {
1296 const LIST: &str =
1297 "00 01 01 FA 06 04 07 43 75 73 74 6F 6D 31 06 32 00 00 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E 00";
1298 const AFTER: &str = "00 02 01 FA 06 04 07 43 75 73 74 6F 6D 31 06 32 00 00 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E 00 \
1299 00 FA 06 05 04 54 65 73 74 06 32 00 00 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E 00";
1300 let mut steps = handshake();
1301 steps.extend([
1302 tx(0x0122, 0x05, ""),
1303 rx(0x8122, 0x05, LIST),
1304 tx(
1305 0x0418,
1306 0x06,
1307 "01 FA 06 00 04 54 65 73 74 06 32 00 00 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E 00",
1308 ),
1309 rx(0x8418, 0x06, "00 05"),
1310 tx(0x0122, 0x07, ""),
1311 rx(0x8122, 0x07, AFTER),
1312 ]);
1313 let script = Script::new(steps);
1314 let created = open(&script).unwrap().create_custom_eq("Test").unwrap();
1315 script.assert_done();
1316 assert_eq!((created.id, created.name.as_str()), (5, "Test"));
1317 assert_eq!(created.bands.len(), 6);
1318 }
1319
1320 #[test]
1321 fn renames_a_custom_preset_and_reads_it_back() {
1322 const LIST: &str = "00 02 00 FA 06 04 07 43 75 73 74 6F 6D 31 06 32 00 06 FA 00 03 E8 03 03 A0 0F 04 40 1F 06 80 3E 06 \
1323 01 FA 06 05 04 54 65 73 74 06 32 00 00 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E 00";
1324 const AFTER: &str = "00 02 00 FA 06 04 07 43 75 73 74 6F 6D 31 06 32 00 06 FA 00 03 E8 03 03 A0 0F 04 40 1F 06 80 3E 06 \
1325 01 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";
1326 let mut steps = handshake();
1327 steps.extend([
1328 tx(0x0122, 0x05, ""),
1329 rx(0x8122, 0x05, LIST),
1330 tx(
1331 0x0418,
1332 0x06,
1333 "02 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",
1334 ),
1335 rx(0x8418, 0x06, "00 05"),
1336 tx(0x0122, 0x07, ""),
1337 rx(0x8122, 0x07, AFTER),
1338 ]);
1339 let script = Script::new(steps);
1340 let now = open(&script).unwrap().rename_custom_eq("custom-5", "Gym").unwrap();
1341 script.assert_done();
1342 assert_eq!((now.id, now.name.as_str()), (5, "Gym"));
1343
1344 // Another preset's name is refused before anything is written.
1345 let mut steps = handshake();
1346 steps.extend([tx(0x0122, 0x05, ""), rx(0x8122, 0x05, LIST)]);
1347 let script = Script::new(steps);
1348 let taken = open(&script).unwrap().rename_custom_eq("custom-5", "Custom1");
1349 script.assert_done();
1350 assert!(matches!(taken, Err(Error::Usage(_))));
1351 }
1352
1353 #[test]
1354 fn refuses_a_fourth_preset_or_a_taken_name_before_writing() {
1355 let one = "00 FA 06 04 07 43 75 73 74 6F 6D 31 00";
1356 for (list, name) in [
1357 (format!("00 03 {one} {one} {one}"), "Test"),
1358 (format!("00 01 {one}"), "Custom1"),
1359 ("00 00".to_owned(), "Test"),
1360 ] {
1361 let mut steps = handshake();
1362 steps.extend([tx(0x0122, 0x05, ""), rx(0x8122, 0x05, &list)]);
1363 let script = Script::new(steps);
1364 let result = open(&script).unwrap().create_custom_eq(name);
1365 script.assert_done();
1366 assert!(matches!(result, Err(Error::Usage(_))), "{list}");
1367 }
1368 }
1369
1370 #[test]
1371 fn retunes_a_custom_preset_and_reads_it_back() {
1372 let tuned =
1373 "00 01 01 FA 06 04 07 43 75 73 74 6F 6D 31 06 32 00 03 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E 00";
1374 let mut steps = handshake();
1375 steps.extend([
1376 tx(0x0122, 0x05, ""),
1377 rx(0x8122, 0x05, OWNER_CUSTOM_EQS),
1378 tx(
1379 0x0418,
1380 0x06,
1381 "02 FA 06 04 07 43 75 73 74 6F 6D 31 06 32 00 03 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E 00",
1382 ),
1383 rx(0x8418, 0x06, "00 04"),
1384 tx(0x0122, 0x07, ""),
1385 rx(0x8122, 0x07, tuned),
1386 ]);
1387 let script = Script::new(steps);
1388 let now = open(&script)
1389 .unwrap()
1390 .set_custom_eq("custom-4", &[EqBand { hz: 50, gain: 3 }])
1391 .unwrap();
1392 script.assert_done();
1393 assert_eq!((now.name.as_str(), now.id), ("Custom1", 4));
1394 assert_eq!(now.bands.first(), Some(&EqBand { hz: 50, gain: 3 }));
1395 }
1396
1397 #[test]
1398 fn eq_custom_selects_by_saving_the_preset_unchanged() {
1399 let saved = "02 FA 06 04 07 43 75 73 74 6F 6D 31 06 32 00 00 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E 00";
1400 let mut steps = handshake();
1401 steps.extend([
1402 tx(0x0122, 0x05, ""),
1403 rx(0x8122, 0x05, OWNER_CUSTOM_EQS),
1404 tx(0x0418, 0x06, saved),
1405 rx(0x8418, 0x06, "00 04"),
1406 tx(0x0122, 0x07, ""),
1407 rx(0x8122, 0x07, OWNER_CUSTOM_EQS),
1408 ]);
1409 let script = Script::new(steps);
1410 let now = open(&script).unwrap().set_eq("custom-4").unwrap();
1411 script.assert_done();
1412 assert_eq!(now, "custom-4");
1413 }
1414
1415 #[test]
1416 fn a_custom_preset_the_buds_lack_is_never_written() {
1417 let mut steps = handshake();
1418 steps.extend([tx(0x0122, 0x05, ""), rx(0x8122, 0x05, OWNER_CUSTOM_EQS)]);
1419 let script = Script::new(steps);
1420 let result = open(&script)
1421 .unwrap()
1422 .set_custom_eq("custom-5", &[EqBand { hz: 50, gain: 3 }]);
1423 script.assert_done();
1424 match result {
1425 Err(Error::Usage(message)) => assert!(message.ends_with("custom-4")),
1426 other => panic!("expected a usage error, got {other:?}"),
1427 }
1428 }
1429}