Krolyxon
Commit 0f2d3e5

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310mod.rs

1557 lines · 59.3 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::PairedDevice;
9use crate::bt::Uuid;
10use crate::devices::{Device, Session};
11use crate::error::{Error, Kind, Result};
12use crate::status::{
13 BassBand, DualDevice, EqBand, EqPreset, EqPresets, GestureChange, NoiseControl, NoiseMode, RawWrite, Status,
14 Switch, TouchChange, 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 new = codec::new_custom_eq(name, list.first())?;
402 let reply = self
403 .link
404 .request(codec::CUSTOM_EQ_WRITE, &codec::custom_eq_create_payload(&new)?)?;
405 let id = codec::decode_custom_eq_write_reply(&reply)?;
406 let created = self.read_back(
407 READ_BACK,
408 codec::CUSTOM_EQ_LIST,
409 &[],
410 |reply| {
411 let now = codec::decode_custom_eq_list(reply)?;
412 let created = codec::find_created(&now, &new.name, id).cloned();
413 Ok((created.is_some(), created))
414 },
415 |_| format!("the buds accepted the new preset {name}, but their list does not have it"),
416 )?;
417 created
418 .map(|p| p.preset())
419 .ok_or_else(|| Error::Protocol(format!("the new preset {name} vanished from the list")))
420 }
421
422 /// Spec: realme_t310.md#custom-eq-rename, #write-read-back
423 fn rename_custom_eq(&mut self, preset: &str, name: &str) -> Result<EqPreset> {
424 let Some(id) = codec::custom_eq_id(preset) else {
425 return Err(Error::Usage(format!(
426 "{preset} is not a custom preset; those are named custom-<id>, as `budctl eq` lists them"
427 )));
428 };
429 if !self.commands.allows(codec::CUSTOM_EQ_WRITE) || !self.commands.allows(codec::CUSTOM_EQ_LIST) {
430 return Err(Error::Unsupported(
431 "these buds do not offer custom EQ presets".to_owned(),
432 ));
433 }
434 // A save carries the whole preset, so it starts from the list read just now.
435 let list = codec::decode_custom_eq_list(&self.link.request(codec::CUSTOM_EQ_LIST, &[])?)?;
436 let Some(current) = list.iter().find(|p| p.id == id) else {
437 return Err(Error::Usage(format!("these buds have no {preset}")));
438 };
439 if list.iter().any(|p| p.id != id && p.name == name.as_bytes()) {
440 return Err(Error::Usage(format!("a custom preset is called {name} already")));
441 }
442 let wanted = codec::renamed(current, name)?;
443 let reply = self
444 .link
445 .request(codec::CUSTOM_EQ_WRITE, &codec::custom_eq_save_payload(&wanted)?)?;
446 codec::check_write_reply(&reply, "renaming the custom EQ")?;
447 let now = self.read_back(
448 READ_BACK,
449 codec::CUSTOM_EQ_LIST,
450 &[],
451 |reply| {
452 let now = codec::decode_custom_eq_list(reply)?.into_iter().find(|p| p.id == id);
453 let applied = now.as_ref().is_some_and(|p| p.name == wanted.name);
454 Ok((applied, now))
455 },
456 |now| match now {
457 None => format!("the buds accepted the new name, but their list no longer has {preset}"),
458 Some(_) => format!("the buds accepted the new name, but still list {preset} under the old one"),
459 },
460 )?;
461 now.map(|p| p.preset())
462 .ok_or_else(|| Error::Protocol(format!("{preset} vanished from the list")))
463 }
464
465 /// Spec: realme_t310.md#custom-eq-delete, #write-read-back
466 fn delete_custom_eq(&mut self, preset: &str) -> Result<Vec<EqPreset>> {
467 let Some(id) = codec::custom_eq_id(preset) else {
468 return Err(Error::Usage(format!(
469 "{preset} is not a custom preset; those are named custom-<id>, as `budctl eq` lists them"
470 )));
471 };
472 if !self.commands.allows(codec::CUSTOM_EQ_WRITE) || !self.commands.allows(codec::CUSTOM_EQ_LIST) {
473 return Err(Error::Unsupported(
474 "these buds do not offer custom EQ presets".to_owned(),
475 ));
476 }
477 // The delete carries the whole preset, as the list holds it just now.
478 let list = codec::decode_custom_eq_list(&self.link.request(codec::CUSTOM_EQ_LIST, &[])?)?;
479 let Some(gone) = list.iter().find(|p| p.id == id).cloned() else {
480 return Err(Error::Usage(format!("these buds have no {preset}")));
481 };
482 let reply = self
483 .link
484 .request(codec::CUSTOM_EQ_WRITE, &codec::custom_eq_delete_payload(&gone)?)?;
485 codec::check_write_reply(&reply, "deleting the custom EQ")?;
486 let left = self.read_back(
487 READ_BACK,
488 codec::CUSTOM_EQ_LIST,
489 &[],
490 |reply| {
491 let now = codec::decode_custom_eq_list(reply)?;
492 Ok((codec::deleted(&list, &gone, &now), now))
493 },
494 |_| format!("the buds accepted deleting {preset}, but still list it"),
495 )?;
496 Ok(left.iter().map(codec::CustomEq::preset).collect())
497 }
498
499 /// Spec: realme_t310.md#bass-write
500 fn bass_range(&self) -> Option<(i8, i8)> {
501 self.commands
502 .allows(codec::BASS_LEVEL_WRITE)
503 .then_some(codec::BASS_RANGE)
504 }
505
506 /// Ack-only: the T310 never answers the bass-level read. A level without its band is ignored,
507 /// so there is no band-less write. Spec: realme_t310.md#bass-bands
508 fn set_bass_band(&mut self, band: BassBand, level: i8) -> Result<()> {
509 let payload = codec::bass_band_payload(band, level)?;
510 if !self.commands.allows(codec::BASS_LEVEL_WRITE) {
511 return Err(Error::Unsupported("these buds do not offer a bass level".to_owned()));
512 }
513 let reply = self.link.request(codec::BASS_LEVEL_WRITE, &payload)?;
514 let what = match band {
515 BassBand::Low => "the low bass change",
516 BassBand::Medium => "the medium bass change",
517 BassBand::High => "the high bass change",
518 };
519 codec::check_write_reply(&reply, what)
520 }
521
522 /// Spec: realme_t310.md#find
523 fn find(&mut self, ring: bool) -> Result<()> {
524 if !self.commands.allows(codec::FIND) {
525 return Err(Error::Unsupported("these buds cannot ring through budctl".to_owned()));
526 }
527 let reply = self.link.request(codec::FIND, &codec::find_payload(ring))?;
528 codec::check_write_reply(&reply, if ring { "ringing" } else { "stopping the ringing" })
529 }
530
531 /// Spec: realme_t310.md#switch-write, #write-read-back
532 /// Read-only: the T310 has no device manager. Spec: realme_t310.md#dual-devices, #dual-manager
533 fn dual_devices(&mut self) -> Result<Vec<DualDevice>> {
534 if !self.commands.allows(codec::DUAL_DEVICES) {
535 return Err(Error::Unsupported(
536 "these buds do not list the devices they connect to".to_owned(),
537 ));
538 }
539 codec::decode_dual_devices(&self.link.request(codec::DUAL_DEVICES, &[])?)
540 }
541
542 fn set_switch(&mut self, key: &str, on: bool) -> Result<Switch> {
543 let Some(id) = codec::switch_id(key) else {
544 return Err(Error::Unsupported(format!("these buds have no switch called {key}")));
545 };
546 let Some(&(_, timing)) = WRITABLE_SWITCHES.iter().find(|(k, _)| *k == key) else {
547 return Err(Error::Unsupported(format!("changing {key} is not supported yet")));
548 };
549 if !self.commands.allows(codec::SWITCH_WRITE) || !self.commands.allows(codec::SWITCHES) {
550 return Err(Error::Unsupported("these buds do not offer switch settings".to_owned()));
551 }
552 let reply = self
553 .link
554 .request(codec::SWITCH_WRITE, &codec::switch_write_payload(id, on))?;
555 codec::check_write_reply(&reply, "the switch change")?;
556
557 self.read_back(
558 timing,
559 codec::SWITCHES,
560 &codec::SWITCH_QUERY,
561 |reply| {
562 let switch = codec::decode_switches(reply)?
563 .into_iter()
564 .find(|s| s.key == key)
565 .ok_or_else(|| Error::Protocol(format!("the switch reply has no {key}")))?;
566 Ok((switch.on == on, switch))
567 },
568 |now| {
569 format!(
570 "the buds accepted {} {}, but still report {}",
571 now.label,
572 on_off(on),
573 on_off(now.on)
574 )
575 },
576 )
577 }
578
579 fn touch_controls(&mut self) -> Result<TouchControls> {
580 if !self.commands.allows(codec::GESTURES) {
581 return Err(Error::Unsupported(
582 "these buds do not offer touch-control settings".to_owned(),
583 ));
584 }
585 let gestures = codec::decode_gestures(&self.link.request(codec::GESTURES, &[])?)?;
586 let mut reads = Reads::default();
587 let hold_cycle = self.read(
588 &mut reads,
589 "hold cycle",
590 codec::NOISE_CONTROL,
591 &codec::HOLD_CYCLE_QUERY,
592 codec::decode_hold_cycle,
593 )?;
594 // The choices only when the buds take writes: the GUI offers what it shows here.
595 let writable = |cmd| self.commands.allows(cmd);
596 let choices = if writable(codec::GESTURE_WRITE) {
597 codec::touch_choices()
598 } else {
599 Vec::new()
600 };
601 let cycle_modes = if hold_cycle.is_some() && writable(codec::NOISE_CONTROL_WRITE) {
602 codec::cycle_modes()
603 } else {
604 Vec::new()
605 };
606 Ok(TouchControls {
607 gestures,
608 hold_cycle,
609 choices,
610 cycle_modes,
611 warnings: reads.warnings,
612 })
613 }
614
615 /// Spec: realme_t310.md#hold-cycle-write, #write-read-back
616 fn set_hold_cycle(&mut self, modes: &[&str]) -> Result<Vec<String>> {
617 let mask = codec::hold_cycle_mask(modes)?;
618 if !self.commands.allows(codec::NOISE_CONTROL_WRITE) || !self.commands.allows(codec::NOISE_CONTROL) {
619 return Err(Error::Unsupported(
620 "these buds do not offer noise-control settings".to_owned(),
621 ));
622 }
623 let reply = self
624 .link
625 .request(codec::NOISE_CONTROL_WRITE, &codec::hold_cycle_write_payload(mask))?;
626 codec::check_write_reply(&reply, "the hold-cycle change")?;
627
628 self.read_back(
629 READ_BACK,
630 codec::NOISE_CONTROL,
631 &codec::HOLD_CYCLE_QUERY,
632 |reply| {
633 Ok((
634 codec::decode_hold_cycle_mask(reply)? == mask,
635 codec::decode_hold_cycle(reply)?,
636 ))
637 },
638 |now| {
639 format!(
640 "the buds accepted the hold cycle {}, but still report {}",
641 modes.join(" → "),
642 now.join(" → ")
643 )
644 },
645 )
646 }
647
648 /// Spec: realme_t310.md#gesture-write, #gesture-choices
649 fn set_touch(&mut self, side: &str, gesture: &str, action: &str, dry_run: bool) -> Result<TouchChange> {
650 let target = codec::touch_target(side, gesture, action)?;
651 if !self.commands.allows(codec::GESTURE_WRITE) || !self.commands.allows(codec::GESTURES) {
652 return Err(Error::Unsupported(
653 "these buds do not offer touch-control settings".to_owned(),
654 ));
655 }
656 // The write carries the whole table, so it starts from the table read just now.
657 let before = codec::decode_gesture_entries(&self.link.request(codec::GESTURES, &[])?)?;
658 let after = codec::retarget_gestures(&before, &target)?;
659 let changes: Vec<GestureChange> = before
660 .iter()
661 .zip(&after)
662 .filter(|(old, new)| old != new)
663 .map(|(old, new)| {
664 let (old, new) = (codec::name_gesture(old), codec::name_gesture(new));
665 GestureChange {
666 side: new.side,
667 gesture: new.gesture,
668 from: old.action,
669 to: new.action,
670 }
671 })
672 .collect();
673 let planned: Vec<_> = after.iter().map(codec::name_gesture).collect();
674 if changes.is_empty() {
675 return Ok(TouchChange {
676 changes,
677 write: None,
678 sent: false,
679 gestures: planned,
680 });
681 }
682 let write = RawWrite {
683 command: codec::GESTURE_WRITE,
684 payload: codec::gesture_write_payload(&after)?,
685 };
686 if dry_run {
687 return Ok(TouchChange {
688 changes,
689 write: Some(write),
690 sent: false,
691 gestures: planned,
692 });
693 }
694
695 let reply = self.link.request(write.command, &write.payload)?;
696 codec::check_write_reply(&reply, "the touch-control change")?;
697 let gestures = self.read_back(
698 TOUCH_READ_BACK,
699 codec::GESTURES,
700 &[],
701 |reply| {
702 let now = codec::decode_gesture_entries(reply)?;
703 // Applied when retargeting the table read back would change nothing.
704 let applied = codec::retarget_gestures(&now, &target).is_ok_and(|again| again == now);
705 Ok((applied, now))
706 },
707 |now| {
708 let still: Vec<String> = changes
709 .iter()
710 .filter_map(|change| {
711 now.iter()
712 .map(codec::name_gesture)
713 .find(|g| g.group.is_none() && g.side == change.side && g.gesture == change.gesture)
714 })
715 .filter(|g| g.action != action)
716 .map(|g| format!("{} {} {}", g.side, g.gesture, g.action))
717 .collect();
718 if still.is_empty() {
719 format!("the buds accepted {side} {gesture} {action}, but the table read back does not show it")
720 } else {
721 format!(
722 "the buds accepted {side} {gesture} {action}, but still report {}",
723 still.join(", ")
724 )
725 }
726 },
727 )?;
728 Ok(TouchChange {
729 changes,
730 write: Some(write),
731 sent: true,
732 gestures: gestures.iter().map(codec::name_gesture).collect(),
733 })
734 }
735}
736
737#[cfg(test)]
738mod tests {
739 use super::*;
740 use crate::status::{AncLevel, Charge};
741 use crate::transport::mock::{Script, Step, hex};
742
743 fn tx(cmd: u16, seq: u8, payload: &str) -> Step {
744 Step::Tx(frame::encode(cmd, seq, &hex(payload)).unwrap())
745 }
746
747 fn rx(cmd: u16, seq: u8, payload: &str) -> Step {
748 Step::Rx(frame::encode(cmd, seq, &hex(payload)).unwrap())
749 }
750
751 fn fixed_clock() -> u32 {
752 1_750_000_000
753 }
754
755 fn open(script: &Script) -> Result<T310Session> {
756 T310Session::open(Link::new(script.transport(), fixed_clock))
757 }
758
759 /// The handshake of the owner's T310 (HW1); seq 01-04 are used up.
760 fn handshake() -> Vec<Step> {
761 vec![
762 tx(0x0100, 0x01, ""),
763 rx(0x8100, 0x01, codec::T310_BITMAP_REPLY),
764 tx(0x0103, 0x02, ""),
765 rx(0x8103, 0x02, "00 12 3C 06"),
766 tx(0x0200, 0x03, ""),
767 rx(0x8200, 0x03, "00 09 01 02 03 04 05 06 0D F1 F4"),
768 tx(0x0205, 0x04, "04 01 02 03 04"),
769 rx(0x8205, 0x04, "00 04 01 00 02 00 03 00 04 00"),
770 ]
771 }
772
773 #[test]
774 fn serve_answers_the_buds_at_once() {
775 let mut steps = handshake();
776 // The time request, at Unix time 1750000000. Spec: realme_t310.md#buds-requests
777 steps.extend([rx(0x0500, 0x02, ""), tx(0x8500, 0x02, "00 80 E1 4E 68")]);
778 let script = Script::new(steps);
779 let mut session = open(&script).unwrap();
780 session.serve().unwrap();
781 script.assert_done();
782
783 let mut steps = handshake();
784 steps.push(Step::Close);
785 let script = Script::new(steps);
786 let mut session = open(&script).unwrap();
787 assert!(matches!(session.serve(), Err(Error::NotConnected(_))));
788 }
789
790 #[test]
791 fn full_status() {
792 let mut steps = handshake();
793 let mut firmware = String::from("00 03");
794 for byte in b"1,2,120,2,2,120,3,2,105" {
795 firmware.push_str(&format!(" {byte:02X}"));
796 }
797 steps.extend([
798 tx(0x010D, 0x05, "06 05 06 11 1A 1B 1D"),
799 rx(0x810D, 0x05, "00 06 05 00 06 00 11 01 1A 00 1B 01 1D 01"),
800 tx(0x010C, 0x06, "01 01"),
801 rx(0x810C, 0x06, "00 01 01 08"),
802 tx(0x0106, 0x07, ""),
803 rx(0x8106, 0x07, "00 03 01 50 02 E4 03 3C"),
804 tx(0x010F, 0x08, ""),
805 rx(0x810F, 0x08, "00 01"),
806 tx(0x0114, 0x09, ""),
807 rx(0x8114, 0x09, "00 02"),
808 tx(0x0105, 0x0A, ""),
809 rx(0x8105, 0x0A, &firmware),
810 ]);
811 let script = Script::new(steps);
812 let status = open(&script).unwrap().status().unwrap();
813 script.assert_done();
814
815 assert_eq!(
816 status.battery.left,
817 Some(Charge {
818 level: 80,
819 charging: false
820 })
821 );
822 assert_eq!(
823 status.battery.right,
824 Some(Charge {
825 level: 100,
826 charging: true
827 })
828 );
829 assert_eq!(
830 status.battery.case,
831 Some(Charge {
832 level: 60,
833 charging: false
834 })
835 );
836 let noise = status.noise_control.unwrap();
837 assert_eq!((noise.mode.as_str(), noise.level.as_deref()), ("anc", Some("deep")));
838 assert_eq!(status.eq.as_deref(), Some("deep-bass"));
839 assert_eq!(status.bass_level, None);
840 assert_eq!(status.codec.as_deref(), Some("AAC"));
841 assert_eq!(status.firmware.as_deref(), Some("120.120.105"));
842 let game = status.switches.iter().find(|s| s.key == "game").unwrap();
843 assert!(!game.on);
844 assert_eq!(status.switches.len(), 5);
845 assert!(status.warnings.is_empty());
846 }
847
848 #[test]
849 fn answers_the_time_request_during_the_settle() {
850 let mut steps = vec![
851 rx(0x0500, 0x01, ""),
852 Step::Tx(hex("AA 0C 00 00 00 85 01 05 00 00 80 E1 4E 68")),
853 ];
854 steps.extend(handshake());
855 // No 0x8205 ack this time: it may never arrive, and `open` does not wait for it.
856 // Spec: realme_t310.md#subscribe
857 steps.pop();
858 let script = Script::new(steps);
859 open(&script).unwrap();
860 script.assert_done();
861 }
862
863 #[test]
864 fn a_silent_handshake_reports_a_busy_channel() {
865 let script = Script::new(vec![tx(0x0100, 0x01, "")]);
866 assert!(matches!(open(&script), Err(Error::ChannelBusy(_))));
867 }
868
869 #[test]
870 fn other_models_are_refused() {
871 let script = Script::new(vec![
872 tx(0x0100, 0x01, ""),
873 rx(0x8100, 0x01, codec::T310_BITMAP_REPLY),
874 tx(0x0103, 0x02, ""),
875 rx(0x8103, 0x02, "00 14 54 06"),
876 ]);
877 match open(&script) {
878 Err(Error::WrongModel { found, .. }) => assert_eq!(found, "065414"),
879 other => panic!("expected WrongModel, got {:?}", other.err()),
880 }
881 }
882
883 #[test]
884 fn commands_missing_from_the_bitmap_are_never_sent() {
885 // A bitmap with no bits: only always-allowed commands go out, and no subscription.
886 let script = Script::new(vec![
887 tx(0x0100, 0x01, ""),
888 rx(0x8100, 0x01, "00"),
889 tx(0x0103, 0x02, ""),
890 rx(0x8103, 0x02, "00 12 3C 06"),
891 tx(0x0200, 0x03, ""),
892 rx(0x8200, 0x03, "00 02 01 02"),
893 tx(0x010D, 0x04, "06 05 06 11 1A 1B 1D"),
894 rx(0x810D, 0x04, "00 01 06 01"),
895 tx(0x0106, 0x05, ""),
896 // no battery reply: becomes a warning
897 ]);
898 let status = open(&script).unwrap().status().unwrap();
899 script.assert_done();
900 assert_eq!(status.switches.len(), 1);
901 assert!(status.noise_control.is_none() && status.firmware.is_none());
902 assert_eq!(status.warnings.len(), 1);
903 assert!(status.warnings.first().unwrap().starts_with("battery"));
904 }
905
906 #[test]
907 fn a_lost_link_stops_the_status() {
908 let mut steps = handshake();
909 steps.extend([tx(0x010D, 0x05, "06 05 06 11 1A 1B 1D"), Step::Close]);
910 let script = Script::new(steps);
911 assert!(matches!(open(&script).unwrap().status(), Err(Error::NotConnected(_))));
912 }
913
914 #[test]
915 fn touch_controls_and_hold_cycle() {
916 let mut steps = handshake();
917 steps.extend([
918 tx(0x0108, 0x05, ""),
919 rx(0x8108, 0x05, "00 02 01 01 02 01 02 01 04 08"),
920 tx(0x010C, 0x06, "02 01"),
921 rx(0x810C, 0x06, "00 02 01 0A"),
922 ]);
923 let script = Script::new(steps);
924 let touch = open(&script).unwrap().touch_controls().unwrap();
925 script.assert_done();
926 assert_eq!(touch.gestures.len(), 2);
927 assert_eq!(touch.hold_cycle.unwrap(), ["transparency", "anc"]);
928 let gestures: Vec<&str> = touch.choices.iter().map(|c| c.gesture.as_str()).collect();
929 assert_eq!(gestures, ["double-tap", "triple-tap", "hold"]);
930 let hold = touch.choices.iter().find(|c| c.gesture == "hold").unwrap();
931 assert_eq!(hold.actions, ["none", "noise-cycle", "voice-assistant", "game-mode"]);
932 assert_eq!(touch.cycle_modes, ["off", "transparency", "anc"]);
933 }
934
935 /// The owner's `budctl --debug touch` (HW1), byte for byte. The buds' own frames (slice
936 /// byte 08) arrive glued to replies and need no answer.
937 #[test]
938 fn replays_the_hw1_touch_session() {
939 let script = Script::new(vec![
940 Step::Tx(hex("AA 07 00 00 00 01 01 00 00")),
941 Step::Rx(hex(
942 "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",
943 )),
944 Step::Tx(hex("AA 07 00 00 03 01 02 00 00")),
945 Step::Rx(hex("AA 0B 00 00 03 81 02 04 00 00 12 3C 06")),
946 Step::Tx(hex("AA 07 00 00 00 02 03 00 00")),
947 Step::Rx(hex("AA 12 00 00 00 82 03 0B 00 00 09 01 02 03 04 05 06 0D F1 F4")),
948 Step::Tx(hex("AA 0C 00 00 05 02 04 05 00 04 01 02 03 04")),
949 Step::Rx(hex(
950 "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",
951 )),
952 Step::Tx(hex("AA 07 00 00 08 01 05 00 00")),
953 Step::Rx(hex(
954 "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",
955 )),
956 Step::Tx(hex("AA 09 00 00 0C 01 06 02 00 02 01")),
957 Step::Rx(hex("AA 0B 00 00 0C 81 06 04 00 00 02 01 0A")),
958 ]);
959 let touch = open(&script).unwrap().touch_controls().unwrap();
960 script.assert_done();
961 let table: Vec<(&str, &str, &str)> = touch
962 .gestures
963 .iter()
964 .map(|g| (g.side.as_str(), g.gesture.as_str(), g.action.as_str()))
965 .collect();
966 assert_eq!(
967 table,
968 [
969 ("left", "double-tap", "play-pause"),
970 ("left", "triple-tap", "next"),
971 ("left", "hold", "noise-cycle"),
972 ("right", "double-tap", "play-pause"),
973 ("right", "triple-tap", "next"),
974 ("right", "hold", "noise-cycle"),
975 ]
976 );
977 assert_eq!(touch.hold_cycle.unwrap(), ["transparency", "anc"]);
978 assert!(touch.warnings.is_empty());
979 }
980
981 #[test]
982 fn sets_noise_control_and_reads_it_back() {
983 let mut steps = handshake();
984 steps.extend([
985 tx(0x0404, 0x05, "01 01 02"),
986 rx(0x8404, 0x05, "00"),
987 // The buds announce the change; it needs no answer.
988 rx(0x0204, 0x10, "03 01 01 02"),
989 tx(0x010C, 0x06, "01 01"),
990 rx(0x810C, 0x06, "00 01 01 02"),
991 ]);
992 let script = Script::new(steps);
993 let now = open(&script)
994 .unwrap()
995 .set_noise_control(NoiseMode::Transparency)
996 .unwrap();
997 script.assert_done();
998 assert_eq!((now.mode.as_str(), now.level), ("transparency", None));
999 }
1000
1001 #[test]
1002 fn reads_back_again_until_the_change_shows() {
1003 let mut steps = handshake();
1004 steps.extend([
1005 tx(0x0404, 0x05, "01 01 01"),
1006 rx(0x8404, 0x05, "00"),
1007 tx(0x010C, 0x06, "01 01"),
1008 rx(0x810C, 0x06, "00 01 01 08"),
1009 tx(0x010C, 0x07, "01 01"),
1010 rx(0x810C, 0x07, "00 01 01 01"),
1011 ]);
1012 let script = Script::new(steps);
1013 let now = open(&script).unwrap().set_noise_control(NoiseMode::Off).unwrap();
1014 script.assert_done();
1015 assert_eq!(now.mode, "off");
1016 }
1017
1018 #[test]
1019 fn sets_a_noise_cancelling_level() {
1020 let mut steps = handshake();
1021 steps.extend([
1022 tx(0x0404, 0x05, "01 01 04"),
1023 rx(0x8404, 0x05, "00"),
1024 tx(0x010C, 0x06, "01 01"),
1025 rx(0x810C, 0x06, "00 01 01 04"),
1026 ]);
1027 let script = Script::new(steps);
1028 let now = open(&script)
1029 .unwrap()
1030 .set_noise_control(NoiseMode::Anc(AncLevel::Light))
1031 .unwrap();
1032 script.assert_done();
1033 assert_eq!((now.mode.as_str(), now.level.as_deref()), ("anc", Some("light")));
1034 }
1035
1036 #[test]
1037 fn a_rejected_noise_control_change_is_not_read_back() {
1038 let mut steps = handshake();
1039 steps.extend([tx(0x0404, 0x05, "01 01 08"), rx(0x8404, 0x05, "0E")]);
1040 let script = Script::new(steps);
1041 let result = open(&script).unwrap().set_noise_control(NoiseMode::Anc(AncLevel::Deep));
1042 script.assert_done();
1043 assert!(matches!(result, Err(Error::Rejected { status: 0x0E, .. })));
1044 }
1045
1046 #[test]
1047 fn noise_control_is_not_written_without_its_bitmap_bit() {
1048 let script = Script::new(vec![
1049 tx(0x0100, 0x01, ""),
1050 rx(0x8100, 0x01, "00"),
1051 tx(0x0103, 0x02, ""),
1052 rx(0x8103, 0x02, "00 12 3C 06"),
1053 tx(0x0200, 0x03, ""),
1054 rx(0x8200, 0x03, "00 00"),
1055 ]);
1056 let result = open(&script).unwrap().set_noise_control(NoiseMode::Off);
1057 script.assert_done();
1058 assert!(matches!(result, Err(Error::Unsupported(_))));
1059 }
1060
1061 #[test]
1062 fn sets_game_mode_and_reads_it_back() {
1063 let mut steps = handshake();
1064 steps.extend([
1065 tx(0x0403, 0x05, "06 01"),
1066 rx(0x8403, 0x05, "00"),
1067 // Still the old value: budctl reads again.
1068 tx(0x010D, 0x06, "06 05 06 11 1A 1B 1D"),
1069 rx(0x810D, 0x06, "00 06 05 00 06 00 11 00 1A 01 1B 00 1D 00"),
1070 tx(0x010D, 0x07, "06 05 06 11 1A 1B 1D"),
1071 rx(0x810D, 0x07, "00 06 05 00 06 01 11 00 1A 01 1B 00 1D 00"),
1072 ]);
1073 let script = Script::new(steps);
1074 let game = open(&script).unwrap().set_switch("game", true).unwrap();
1075 script.assert_done();
1076 assert_eq!((game.key, game.on), ("game", true));
1077 }
1078
1079 #[test]
1080 fn unknown_switches_are_never_written() {
1081 // Nothing is sent after the handshake, so the subscribe ack is left out.
1082 let mut steps = handshake();
1083 steps.pop();
1084 let script = Script::new(steps);
1085 let mut session = open(&script).unwrap();
1086 assert!(matches!(session.set_switch("turbo", true), Err(Error::Unsupported(_))));
1087 script.assert_done();
1088 }
1089
1090 #[test]
1091 fn sets_bass_boost_and_reads_it_back_at_once() {
1092 let mut steps = handshake();
1093 steps.extend([
1094 tx(0x0403, 0x05, "1D 01"),
1095 rx(0x8403, 0x05, "00"),
1096 tx(0x010D, 0x06, "06 05 06 11 1A 1B 1D"),
1097 rx(0x810D, 0x06, "00 06 05 00 06 00 11 00 1A 01 1B 00 1D 01"),
1098 ]);
1099 let script = Script::new(steps);
1100 let now = open(&script).unwrap().set_switch("bass-boost", true).unwrap();
1101 script.assert_done();
1102 assert_eq!((now.key, now.on), ("bass-boost", true));
1103 }
1104
1105 #[test]
1106 fn a_switch_the_buds_lack_is_rejected() {
1107 let mut steps = handshake();
1108 steps.extend([tx(0x0403, 0x05, "06 00"), rx(0x8403, 0x05, "01")]);
1109 let script = Script::new(steps);
1110 let result = open(&script).unwrap().set_switch("game", false);
1111 script.assert_done();
1112 assert!(matches!(result, Err(Error::Rejected { status: 0x01, .. })));
1113 }
1114
1115 #[test]
1116 fn selects_an_eq_preset_and_reads_it_back() {
1117 let mut steps = handshake();
1118 steps.extend([
1119 tx(0x0406, 0x05, "01"),
1120 rx(0x8406, 0x05, "00"),
1121 tx(0x010F, 0x06, ""),
1122 rx(0x810F, 0x06, "00 01"),
1123 ]);
1124 let script = Script::new(steps);
1125 let now = open(&script).unwrap().set_eq("deep-bass").unwrap();
1126 script.assert_done();
1127 assert_eq!(now, "deep-bass");
1128 }
1129
1130 #[test]
1131 fn an_unknown_eq_preset_is_a_usage_error() {
1132 let mut steps = handshake();
1133 steps.pop();
1134 let script = Script::new(steps);
1135 let result = open(&script).unwrap().set_eq("loudness");
1136 script.assert_done();
1137 match result {
1138 Err(Error::Usage(message)) => assert!(message.contains("pure-raw, deep-bass, pure-vocals, pure-bass")),
1139 other => panic!("expected a usage error, got {other:?}"),
1140 }
1141 }
1142
1143 #[test]
1144 fn rings_and_stops() {
1145 let mut steps = handshake();
1146 steps.extend([
1147 tx(0x0400, 0x05, "01"),
1148 rx(0x8400, 0x05, "00"),
1149 tx(0x0400, 0x06, "00"),
1150 rx(0x8400, 0x06, "00"),
1151 ]);
1152 let script = Script::new(steps);
1153 let mut session = open(&script).unwrap();
1154 session.find(true).unwrap();
1155 session.find(false).unwrap();
1156 script.assert_done();
1157 }
1158
1159 #[test]
1160 fn find_is_not_sent_without_its_bitmap_bit() {
1161 // Bits 0-4 only: no bit 5.
1162 let script = Script::new(vec![
1163 tx(0x0100, 0x01, ""),
1164 rx(0x8100, 0x01, "00 1F"),
1165 tx(0x0103, 0x02, ""),
1166 rx(0x8103, 0x02, "00 12 3C 06"),
1167 tx(0x0200, 0x03, ""),
1168 rx(0x8200, 0x03, "00 00"),
1169 ]);
1170 let result = open(&script).unwrap().find(true);
1171 script.assert_done();
1172 assert!(matches!(result, Err(Error::Unsupported(_))));
1173 }
1174
1175 #[test]
1176 fn lists_the_dual_connection_devices() {
1177 let mut steps = handshake();
1178 steps.extend([
1179 tx(0x0112, 0x05, ""),
1180 rx(0x8112, 0x05, "00 01 2B 1A 00 AA BB CC 07 02 01 04 61 72 63 68"),
1181 ]);
1182 let script = Script::new(steps);
1183 let devices = open(&script).unwrap().dual_devices().unwrap();
1184 script.assert_done();
1185 let [computer] = devices.as_slice() else {
1186 panic!("one device expected: {devices:?}");
1187 };
1188 assert_eq!(computer.address.to_string(), "2B:1A:00:AA:BB:CC");
1189 assert!(computer.connected && computer.this_computer);
1190 }
1191
1192 #[test]
1193 fn the_dual_connection_list_is_not_asked_for_without_its_bitmap_bit() {
1194 // Bytes 0-2 of the T310's bitmap and a clear byte 3: no bit 29.
1195 let script = Script::new(vec![
1196 tx(0x0100, 0x01, ""),
1197 rx(0x8100, 0x01, "00 BF 1F 5A 00"),
1198 tx(0x0103, 0x02, ""),
1199 rx(0x8103, 0x02, "00 12 3C 06"),
1200 tx(0x0200, 0x03, ""),
1201 rx(0x8200, 0x03, "00 00"),
1202 ]);
1203 let result = open(&script).unwrap().dual_devices();
1204 script.assert_done();
1205 assert!(matches!(result, Err(Error::Unsupported(_))));
1206 }
1207
1208 /// The owner's gesture table (HW1) and the same table with the left triple tap on volume up.
1209 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";
1210 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";
1211
1212 #[test]
1213 fn a_touch_dry_run_reads_the_table_and_sends_nothing() {
1214 let mut steps = handshake();
1215 steps.extend([tx(0x0108, 0x05, ""), rx(0x8108, 0x05, HW1_TABLE)]);
1216 let script = Script::new(steps);
1217 let change = open(&script)
1218 .unwrap()
1219 .set_touch("left", "triple-tap", "volume-up", true)
1220 .unwrap();
1221 script.assert_done();
1222 assert!(!change.sent);
1223 let write = change.write.unwrap();
1224 assert_eq!((write.command, write.payload), (0x0401, hex(LEFT_VOLUME_UP)));
1225 assert_eq!(
1226 change.changes,
1227 [GestureChange {
1228 side: "left".to_owned(),
1229 gesture: "triple-tap".to_owned(),
1230 from: "next".to_owned(),
1231 to: "volume-up".to_owned(),
1232 }]
1233 );
1234 let left_triple = change.gestures.get(1).unwrap();
1235 assert_eq!(left_triple.action, "volume-up");
1236 }
1237
1238 #[test]
1239 fn sets_a_gesture_and_reads_the_table_back() {
1240 let mut steps = handshake();
1241 steps.extend([
1242 tx(0x0108, 0x05, ""),
1243 rx(0x8108, 0x05, HW1_TABLE),
1244 tx(0x0401, 0x06, LEFT_VOLUME_UP),
1245 // The owner's ack (W8): 00, then the old table cut to the write's length.
1246 rx(
1247 0x8401,
1248 0x06,
1249 "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",
1250 ),
1251 tx(0x0108, 0x07, ""),
1252 rx(0x8108, 0x07, &format!("00 {LEFT_VOLUME_UP}")),
1253 ]);
1254 let script = Script::new(steps);
1255 let change = open(&script)
1256 .unwrap()
1257 .set_touch("left", "triple-tap", "volume-up", false)
1258 .unwrap();
1259 script.assert_done();
1260 assert!(change.sent);
1261 assert_eq!(change.changes.len(), 1);
1262 assert_eq!(change.gestures.get(1).unwrap().action, "volume-up");
1263 }
1264
1265 #[test]
1266 fn a_gesture_that_already_has_the_action_is_not_written() {
1267 let mut steps = handshake();
1268 steps.extend([tx(0x0108, 0x05, ""), rx(0x8108, 0x05, HW1_TABLE)]);
1269 let script = Script::new(steps);
1270 let change = open(&script)
1271 .unwrap()
1272 .set_touch("both", "hold", "noise-cycle", false)
1273 .unwrap();
1274 script.assert_done();
1275 assert!(change.changes.is_empty() && change.write.is_none() && !change.sent);
1276 }
1277
1278 #[test]
1279 fn a_bad_touch_action_is_refused_before_anything_is_sent() {
1280 let mut steps = handshake();
1281 steps.pop();
1282 let script = Script::new(steps);
1283 let result = open(&script)
1284 .unwrap()
1285 .set_touch("left", "double-tap", "volume-up", false);
1286 script.assert_done();
1287 assert!(matches!(result, Err(Error::Usage(_))));
1288 }
1289
1290 #[test]
1291 fn sets_the_hold_cycle_and_reads_it_back() {
1292 let mut steps = handshake();
1293 steps.extend([
1294 tx(0x0404, 0x05, "02 01 0B"),
1295 rx(0x8404, 0x05, "00"),
1296 tx(0x010C, 0x06, "02 01"),
1297 rx(0x810C, 0x06, "00 02 01 0B"),
1298 ]);
1299 let script = Script::new(steps);
1300 let now = open(&script)
1301 .unwrap()
1302 .set_hold_cycle(&["off", "transparency", "anc"])
1303 .unwrap();
1304 script.assert_done();
1305 assert_eq!(now, ["off", "transparency", "anc"]);
1306 }
1307
1308 #[test]
1309 fn a_one_mode_hold_cycle_is_refused_before_anything_is_sent() {
1310 let mut steps = handshake();
1311 steps.pop();
1312 let script = Script::new(steps);
1313 let result = open(&script).unwrap().set_hold_cycle(&["anc"]);
1314 script.assert_done();
1315 assert!(matches!(result, Err(Error::Usage(_))));
1316 }
1317
1318 /// The custom EQ list of the owner's T310 (W11a): one flat preset, "Custom1", id 04.
1319 const OWNER_CUSTOM_EQS: &str =
1320 "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";
1321
1322 #[test]
1323 fn lists_built_in_and_custom_eq_presets() {
1324 let mut steps = handshake();
1325 steps.extend([
1326 tx(0x010F, 0x05, ""),
1327 rx(0x810F, 0x05, "00 04"),
1328 tx(0x0122, 0x06, ""),
1329 // The owner's T310 (W11a).
1330 rx(0x8122, 0x06, OWNER_CUSTOM_EQS),
1331 ]);
1332 let script = Script::new(steps);
1333 let eq = open(&script).unwrap().eq_presets().unwrap();
1334 script.assert_done();
1335 assert_eq!(eq.current.as_deref(), Some("custom-4"));
1336 let names: Vec<(&str, bool)> = eq.presets.iter().map(|p| (p.name.as_str(), p.custom)).collect();
1337 assert_eq!(
1338 names,
1339 [
1340 ("pure-raw", false),
1341 ("deep-bass", false),
1342 ("pure-vocals", false),
1343 ("pure-bass", false),
1344 ("Custom1", true)
1345 ]
1346 );
1347 let bands: Vec<u16> = eq.presets.last().unwrap().bands.iter().map(|b| b.hz).collect();
1348 assert_eq!(bands, [50, 250, 1000, 4000, 8000, 16000]);
1349 assert!(eq.warnings.is_empty());
1350 }
1351
1352 #[test]
1353 fn sets_a_bass_band_with_its_band_byte() {
1354 let mut steps = handshake();
1355 steps.extend([
1356 tx(0x041B, 0x05, "FB 05 05 01"),
1357 rx(0x841B, 0x05, "00"),
1358 tx(0x041B, 0x06, "FB 05 FB 03"),
1359 rx(0x841B, 0x06, "00"),
1360 ]);
1361 let script = Script::new(steps);
1362 let mut session = open(&script).unwrap();
1363 assert_eq!(session.bass_range(), Some((-5, 5)));
1364 session.set_bass_band(BassBand::Low, 5).unwrap();
1365 session.set_bass_band(BassBand::High, -5).unwrap();
1366 script.assert_done();
1367 }
1368
1369 #[test]
1370 fn a_refused_bass_band_says_which() {
1371 let mut steps = handshake();
1372 steps.extend([tx(0x041B, 0x05, "FB 05 02 02"), rx(0x841B, 0x05, "01")]);
1373 let script = Script::new(steps);
1374 let result = open(&script).unwrap().set_bass_band(BassBand::Medium, 2);
1375 script.assert_done();
1376 assert!(matches!(result, Err(Error::Rejected { what, .. }) if what.contains("medium")));
1377 }
1378
1379 #[test]
1380 fn a_bass_level_out_of_range_is_refused_before_anything_is_sent() {
1381 let mut steps = handshake();
1382 steps.pop();
1383 let script = Script::new(steps);
1384 let result = open(&script).unwrap().set_bass_band(BassBand::Low, 6);
1385 script.assert_done();
1386 assert!(matches!(result, Err(Error::Usage(_))));
1387 }
1388
1389 #[test]
1390 fn creates_a_custom_preset_and_finds_it_in_the_list() {
1391 const LIST: &str =
1392 "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";
1393 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 \
1394 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";
1395 let mut steps = handshake();
1396 steps.extend([
1397 tx(0x0122, 0x05, ""),
1398 rx(0x8122, 0x05, LIST),
1399 tx(
1400 0x0418,
1401 0x06,
1402 "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",
1403 ),
1404 rx(0x8418, 0x06, "00 05"),
1405 tx(0x0122, 0x07, ""),
1406 rx(0x8122, 0x07, AFTER),
1407 ]);
1408 let script = Script::new(steps);
1409 let created = open(&script).unwrap().create_custom_eq("Test").unwrap();
1410 script.assert_done();
1411 assert_eq!((created.id, created.name.as_str()), (5, "Test"));
1412 assert_eq!(created.bands.len(), 6);
1413 }
1414
1415 #[test]
1416 fn renames_a_custom_preset_and_reads_it_back() {
1417 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 \
1418 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";
1419 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 \
1420 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";
1421 let mut steps = handshake();
1422 steps.extend([
1423 tx(0x0122, 0x05, ""),
1424 rx(0x8122, 0x05, LIST),
1425 tx(
1426 0x0418,
1427 0x06,
1428 "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",
1429 ),
1430 rx(0x8418, 0x06, "00 05"),
1431 tx(0x0122, 0x07, ""),
1432 rx(0x8122, 0x07, AFTER),
1433 ]);
1434 let script = Script::new(steps);
1435 let now = open(&script).unwrap().rename_custom_eq("custom-5", "Gym").unwrap();
1436 script.assert_done();
1437 assert_eq!((now.id, now.name.as_str()), (5, "Gym"));
1438
1439 // Another preset's name is refused before anything is written.
1440 let mut steps = handshake();
1441 steps.extend([tx(0x0122, 0x05, ""), rx(0x8122, 0x05, LIST)]);
1442 let script = Script::new(steps);
1443 let taken = open(&script).unwrap().rename_custom_eq("custom-5", "Custom1");
1444 script.assert_done();
1445 assert!(matches!(taken, Err(Error::Usage(_))));
1446 }
1447
1448 #[test]
1449 fn deletes_a_custom_preset_and_reads_the_list_back() {
1450 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 \
1451 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";
1452 const AFTER: &str =
1453 "00 01 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";
1454 let mut steps = handshake();
1455 steps.extend([
1456 tx(0x0122, 0x05, ""),
1457 rx(0x8122, 0x05, LIST),
1458 tx(
1459 0x0418,
1460 0x06,
1461 "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",
1462 ),
1463 rx(0x8418, 0x06, "00 05"),
1464 tx(0x0122, 0x07, ""),
1465 rx(0x8122, 0x07, AFTER),
1466 ]);
1467 let script = Script::new(steps);
1468 let left = open(&script).unwrap().delete_custom_eq("custom-5").unwrap();
1469 script.assert_done();
1470 let names: Vec<&str> = left.iter().map(|p| p.name.as_str()).collect();
1471 assert_eq!(names, ["Custom1"]);
1472
1473 // A preset the buds do not have is refused before anything is written.
1474 let mut steps = handshake();
1475 steps.extend([tx(0x0122, 0x05, ""), rx(0x8122, 0x05, LIST)]);
1476 let script = Script::new(steps);
1477 let missing = open(&script).unwrap().delete_custom_eq("custom-7");
1478 script.assert_done();
1479 assert!(matches!(missing, Err(Error::Usage(_))));
1480 }
1481
1482 #[test]
1483 fn refuses_a_fourth_preset_or_a_taken_name_before_writing() {
1484 let one = "00 FA 06 04 07 43 75 73 74 6F 6D 31 00";
1485 for (list, name) in [
1486 (format!("00 03 {one} {one} {one}"), "Test"),
1487 (format!("00 01 {one}"), "Custom1"),
1488 ] {
1489 let mut steps = handshake();
1490 steps.extend([tx(0x0122, 0x05, ""), rx(0x8122, 0x05, &list)]);
1491 let script = Script::new(steps);
1492 let result = open(&script).unwrap().create_custom_eq(name);
1493 script.assert_done();
1494 assert!(matches!(result, Err(Error::Usage(_))), "{list}");
1495 }
1496 }
1497
1498 #[test]
1499 fn retunes_a_custom_preset_and_reads_it_back() {
1500 let tuned =
1501 "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";
1502 let mut steps = handshake();
1503 steps.extend([
1504 tx(0x0122, 0x05, ""),
1505 rx(0x8122, 0x05, OWNER_CUSTOM_EQS),
1506 tx(
1507 0x0418,
1508 0x06,
1509 "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",
1510 ),
1511 rx(0x8418, 0x06, "00 04"),
1512 tx(0x0122, 0x07, ""),
1513 rx(0x8122, 0x07, tuned),
1514 ]);
1515 let script = Script::new(steps);
1516 let now = open(&script)
1517 .unwrap()
1518 .set_custom_eq("custom-4", &[EqBand { hz: 50, gain: 3 }])
1519 .unwrap();
1520 script.assert_done();
1521 assert_eq!((now.name.as_str(), now.id), ("Custom1", 4));
1522 assert_eq!(now.bands.first(), Some(&EqBand { hz: 50, gain: 3 }));
1523 }
1524
1525 #[test]
1526 fn eq_custom_selects_by_saving_the_preset_unchanged() {
1527 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";
1528 let mut steps = handshake();
1529 steps.extend([
1530 tx(0x0122, 0x05, ""),
1531 rx(0x8122, 0x05, OWNER_CUSTOM_EQS),
1532 tx(0x0418, 0x06, saved),
1533 rx(0x8418, 0x06, "00 04"),
1534 tx(0x0122, 0x07, ""),
1535 rx(0x8122, 0x07, OWNER_CUSTOM_EQS),
1536 ]);
1537 let script = Script::new(steps);
1538 let now = open(&script).unwrap().set_eq("custom-4").unwrap();
1539 script.assert_done();
1540 assert_eq!(now, "custom-4");
1541 }
1542
1543 #[test]
1544 fn a_custom_preset_the_buds_lack_is_never_written() {
1545 let mut steps = handshake();
1546 steps.extend([tx(0x0122, 0x05, ""), rx(0x8122, 0x05, OWNER_CUSTOM_EQS)]);
1547 let script = Script::new(steps);
1548 let result = open(&script)
1549 .unwrap()
1550 .set_custom_eq("custom-5", &[EqBand { hz: 50, gain: 3 }]);
1551 script.assert_done();
1552 match result {
1553 Err(Error::Usage(message)) => assert!(message.ends_with("custom-4")),
1554 other => panic!("expected a usage error, got {other:?}"),
1555 }
1556 }
1557}