Krolyxon
Commit 83c5894

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310mod.rs

1016 lines · 36.4 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::{GestureChange, NoiseControl, NoiseMode, RawWrite, Status, Switch, TouchChange, TouchControls};
13use crate::transport::Transport;
14
15mod codec;
16mod frame;
17mod link;
18
19use codec::Commands;
20use link::Link;
21
22/// When to read a setting back after the buds ack a write. Spec: realme_t310.md#write-read-back
23#[derive(Clone, Copy)]
24struct ReadBack {
25 /// How long after the ack the first read goes out.
26 first: Duration,
27 /// How long after the ack budctl gives up.
28 window: Duration,
29}
30
31/// Spec: realme_t310.md#write-read-back
32const READ_BACK: ReadBack = ReadBack {
33 first: Duration::ZERO,
34 window: Duration::from_secs(2),
35};
36/// Game mode lands about 1 s after the ack. Spec: realme_t310.md#switch-write
37const GAME_READ_BACK: ReadBack = ReadBack {
38 first: Duration::from_secs(1),
39 window: Duration::from_secs(3),
40};
41/// The gesture table is read back about 600 ms after the ack. Spec: realme_t310.md#gesture-write
42const TOUCH_READ_BACK: ReadBack = ReadBack {
43 first: Duration::from_millis(600),
44 window: Duration::from_secs(2),
45};
46/// Spec: realme_t310.md#write-read-back
47const READ_BACK_PAUSE: Duration = Duration::from_millis(300);
48
49/// The switches budctl may write, each added after the previous one passed its hardware
50/// checkpoint (CLAUDE.md rule 4). Spec: realme_t310.md#switch-write
51const WRITABLE_SWITCHES: [(&str, ReadBack); 5] = [
52 ("game", GAME_READ_BACK),
53 ("bass-boost", READ_BACK),
54 ("spatial", READ_BACK),
55 ("wind", READ_BACK),
56 ("dual", READ_BACK),
57];
58
59fn on_off(on: bool) -> &'static str {
60 if on { "on" } else { "off" }
61}
62
63/// Spec: realme_t310.md#identity
64const BLUETOOTH_NAME: &str = "realme Buds T310";
65
66/// Control services, in the order to try. Spec: realme_t310.md#transport
67const SERVICES: [Uuid; 2] = [
68 Uuid::from_u128(0x0000_079A_D102_11E1_9B23_0002_5B00_A5A5),
69 Uuid::from_u128(0x0000_1107_D102_11E1_9B23_0002_5B00_A5A5),
70];
71
72/// Quiet time after connecting, before the first request. Spec: realme_t310.md#init
73const SETTLE: Duration = Duration::from_millis(300);
74
75pub struct RealmeT310;
76
77impl Device for RealmeT310 {
78 fn name(&self) -> &'static str {
79 BLUETOOTH_NAME
80 }
81
82 fn matches(&self, device: &PairedDevice) -> bool {
83 device.name == BLUETOOTH_NAME
84 }
85
86 fn rfcomm_uuids(&self) -> &'static [Uuid] {
87 &SERVICES
88 }
89
90 fn open(&self, transport: Box<dyn Transport>) -> Result<Box<dyn Session>> {
91 Ok(Box::new(T310Session::open(Link::new(transport, unix_time))?))
92 }
93}
94
95fn unix_time() -> u32 {
96 let seconds = SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.as_secs());
97 u32::try_from(seconds).unwrap_or(u32::MAX)
98}
99
100struct T310Session {
101 link: Link,
102 commands: Commands,
103}
104
105/// Collects what a series of best-effort reads produced.
106#[derive(Default)]
107struct Reads {
108 warnings: Vec<String>,
109 answered: usize,
110}
111
112impl T310Session {
113 /// The session start. Spec: realme_t310.md#init
114 fn open(mut link: Link) -> Result<Self> {
115 link.pump_until(Instant::now() + SETTLE)?;
116
117 let bitmap = link.request(codec::HANDSHAKE, &[]).map_err(|error| match error {
118 // Provisional until checkpoint HW1: a link that connects but stays silent is most
119 // likely held by another app. Spec: realme_t310.md#open
120 Error::Timeout(_) => Error::ChannelBusy("connected, but the buds did not answer the handshake".to_owned()),
121 other => other,
122 })?;
123 let commands = Commands::parse(&bitmap)?;
124
125 let product = codec::decode_product_id(&link.request(codec::PRODUCT_ID, &[])?)?;
126 if product != codec::T310_PRODUCT_ID {
127 return Err(Error::WrongModel {
128 expected: BLUETOOTH_NAME,
129 found: format!("{product:06X}"),
130 });
131 }
132
133 let offered = codec::decode_events_offered(&link.request(codec::EVENTS_OFFERED, &[])?)?;
134 let subscription = codec::subscribe_payload(&offered, &commands);
135 if commands.allows(codec::SUBSCRIBE) && subscription.len() > 1 {
136 // Some buds never acknowledge this, so budctl does not wait. Spec: realme_t310.md#subscribe
137 link.send(codec::SUBSCRIBE, &subscription)?;
138 }
139 Ok(Self { link, commands })
140 }
141
142 /// Read and decode one value, if the buds offer the command. A read the buds answer badly
143 /// (timeout, refusal, garbage) becomes a warning so the rest still shows; a lost link stops
144 /// everything.
145 fn read<T>(
146 &mut self,
147 reads: &mut Reads,
148 what: &str,
149 cmd: u16,
150 payload: &[u8],
151 decode: fn(&[u8]) -> Result<T>,
152 ) -> Result<Option<T>> {
153 if !self.commands.allows(cmd) {
154 return Ok(None);
155 }
156 match self.link.request(cmd, payload).and_then(|reply| decode(&reply)) {
157 Ok(value) => {
158 reads.answered += 1;
159 Ok(Some(value))
160 }
161 Err(error) if error.kind() == Kind::Failed => {
162 reads.warnings.push(format!("{what}: {error}"));
163 Ok(None)
164 }
165 Err(error) => Err(error),
166 }
167 }
168}
169
170impl T310Session {
171 /// After an acked write, read the setting back with `cmd query` until `check` reports the
172 /// new value, following `timing`. `check` returns whether the value is the one written,
173 /// and the value; `not_applied` describes a value that never changed.
174 /// Spec: realme_t310.md#write-read-back
175 fn read_back<T>(
176 &mut self,
177 timing: ReadBack,
178 cmd: u16,
179 query: &[u8],
180 check: impl Fn(&[u8]) -> Result<(bool, T)>,
181 not_applied: impl Fn(T) -> String,
182 ) -> Result<T> {
183 let acked = Instant::now();
184 self.link.pump_until(acked + timing.first)?;
185 loop {
186 let (applied, value) = check(&self.link.request(cmd, query)?)?;
187 if applied {
188 return Ok(value);
189 }
190 if Instant::now() >= acked + timing.window {
191 return Err(Error::NotApplied(not_applied(value)));
192 }
193 self.link.pump_until(Instant::now() + READ_BACK_PAUSE)?;
194 }
195 }
196}
197
198impl Session for T310Session {
199 fn status(&mut self) -> Result<Status> {
200 let mut reads = Reads::default();
201 let switches = self.read(
202 &mut reads,
203 "switches",
204 codec::SWITCHES,
205 &codec::SWITCH_QUERY,
206 codec::decode_switches,
207 )?;
208 let noise_control = self.read(
209 &mut reads,
210 "noise control",
211 codec::NOISE_CONTROL,
212 &codec::NOISE_MODE_QUERY,
213 codec::decode_noise_mode,
214 )?;
215 let battery = self.read(&mut reads, "battery", codec::BATTERY, &[], codec::decode_battery)?;
216 let eq = self.read(&mut reads, "EQ", codec::EQ_PRESET, &[], codec::decode_eq)?;
217 // No bass-level read: the T310 does not answer an empty 0x0124 (HW1). It returns with W6.
218 // Spec: realme_t310.md#bass-read
219 let codec = self.read(&mut reads, "codec", codec::CODEC, &[], codec::decode_codec)?;
220 let firmware = self.read(&mut reads, "firmware", codec::FIRMWARE, &[], codec::decode_firmware)?;
221
222 if reads.answered == 0 && !reads.warnings.is_empty() {
223 return Err(Error::Timeout(reads.warnings.join("; ")));
224 }
225 Ok(Status {
226 battery: battery.unwrap_or_default(),
227 noise_control,
228 eq,
229 bass_level: None,
230 switches: switches.unwrap_or_default(),
231 codec,
232 firmware,
233 warnings: reads.warnings,
234 })
235 }
236
237 /// Spec: realme_t310.md#anc-write, #write-read-back
238 fn set_noise_control(&mut self, mode: NoiseMode) -> Result<NoiseControl> {
239 if !self.commands.allows(codec::NOISE_CONTROL_WRITE) || !self.commands.allows(codec::NOISE_CONTROL) {
240 return Err(Error::Unsupported(
241 "these buds do not offer noise-control settings".to_owned(),
242 ));
243 }
244 let reply = self
245 .link
246 .request(codec::NOISE_CONTROL_WRITE, &codec::noise_write_payload(mode))?;
247 codec::check_write_reply(&reply, "the noise-control change")?;
248
249 let wanted = codec::noise_write_mask(mode);
250 self.read_back(
251 READ_BACK,
252 codec::NOISE_CONTROL,
253 &codec::NOISE_MODE_QUERY,
254 |reply| {
255 Ok((
256 codec::decode_noise_mask(reply)? == wanted,
257 codec::decode_noise_mode(reply)?,
258 ))
259 },
260 |now| {
261 let wanted = match mode {
262 NoiseMode::Anc(level) => format!("anc ({})", level.name()),
263 _ => mode.name().to_owned(),
264 };
265 let reported = match &now.level {
266 Some(level) => format!("{} ({level})", now.mode),
267 None => now.mode,
268 };
269 format!("the buds accepted noise control {wanted}, but still report {reported}")
270 },
271 )
272 }
273
274 /// Built-in presets only; custom EQs come with W11. Spec: realme_t310.md#eq-write
275 fn set_eq(&mut self, preset: &str) -> Result<String> {
276 let Some(id) = codec::eq_id(preset) else {
277 return Err(Error::Usage(format!(
278 "no EQ preset called {preset}; these buds have: {}",
279 codec::eq_names().join(", ")
280 )));
281 };
282 if !self.commands.allows(codec::EQ_SELECT) || !self.commands.allows(codec::EQ_PRESET) {
283 return Err(Error::Unsupported("these buds do not offer EQ presets".to_owned()));
284 }
285 let reply = self.link.request(codec::EQ_SELECT, &[id])?;
286 codec::check_write_reply(&reply, "the EQ change")?;
287
288 self.read_back(
289 READ_BACK,
290 codec::EQ_PRESET,
291 &[],
292 |reply| {
293 let now = codec::decode_eq(reply)?;
294 Ok((now == preset, now))
295 },
296 |now| format!("the buds accepted EQ {preset}, but still report {now}"),
297 )
298 }
299
300 /// Spec: realme_t310.md#find
301 fn find(&mut self, ring: bool) -> Result<()> {
302 if !self.commands.allows(codec::FIND) {
303 return Err(Error::Unsupported("these buds cannot ring through budctl".to_owned()));
304 }
305 let reply = self.link.request(codec::FIND, &codec::find_payload(ring))?;
306 codec::check_write_reply(&reply, if ring { "ringing" } else { "stopping the ringing" })
307 }
308
309 /// Spec: realme_t310.md#switch-write, #write-read-back
310 fn set_switch(&mut self, key: &str, on: bool) -> Result<Switch> {
311 let Some(id) = codec::switch_id(key) else {
312 return Err(Error::Unsupported(format!("these buds have no switch called {key}")));
313 };
314 let Some(&(_, timing)) = WRITABLE_SWITCHES.iter().find(|(k, _)| *k == key) else {
315 return Err(Error::Unsupported(format!("changing {key} is not supported yet")));
316 };
317 if !self.commands.allows(codec::SWITCH_WRITE) || !self.commands.allows(codec::SWITCHES) {
318 return Err(Error::Unsupported("these buds do not offer switch settings".to_owned()));
319 }
320 let reply = self
321 .link
322 .request(codec::SWITCH_WRITE, &codec::switch_write_payload(id, on))?;
323 codec::check_write_reply(&reply, "the switch change")?;
324
325 self.read_back(
326 timing,
327 codec::SWITCHES,
328 &codec::SWITCH_QUERY,
329 |reply| {
330 let switch = codec::decode_switches(reply)?
331 .into_iter()
332 .find(|s| s.key == key)
333 .ok_or_else(|| Error::Protocol(format!("the switch reply has no {key}")))?;
334 Ok((switch.on == on, switch))
335 },
336 |now| {
337 format!(
338 "the buds accepted {} {}, but still report {}",
339 now.label,
340 on_off(on),
341 on_off(now.on)
342 )
343 },
344 )
345 }
346
347 fn touch_controls(&mut self) -> Result<TouchControls> {
348 if !self.commands.allows(codec::GESTURES) {
349 return Err(Error::Unsupported(
350 "these buds do not offer touch-control settings".to_owned(),
351 ));
352 }
353 let gestures = codec::decode_gestures(&self.link.request(codec::GESTURES, &[])?)?;
354 let mut reads = Reads::default();
355 let hold_cycle = self.read(
356 &mut reads,
357 "hold cycle",
358 codec::NOISE_CONTROL,
359 &codec::HOLD_CYCLE_QUERY,
360 codec::decode_hold_cycle,
361 )?;
362 Ok(TouchControls {
363 gestures,
364 hold_cycle,
365 warnings: reads.warnings,
366 })
367 }
368
369 /// Spec: realme_t310.md#hold-cycle-write, #write-read-back
370 fn set_hold_cycle(&mut self, modes: &[&str]) -> Result<Vec<String>> {
371 let mask = codec::hold_cycle_mask(modes)?;
372 if !self.commands.allows(codec::NOISE_CONTROL_WRITE) || !self.commands.allows(codec::NOISE_CONTROL) {
373 return Err(Error::Unsupported(
374 "these buds do not offer noise-control settings".to_owned(),
375 ));
376 }
377 let reply = self
378 .link
379 .request(codec::NOISE_CONTROL_WRITE, &codec::hold_cycle_write_payload(mask))?;
380 codec::check_write_reply(&reply, "the hold-cycle change")?;
381
382 self.read_back(
383 READ_BACK,
384 codec::NOISE_CONTROL,
385 &codec::HOLD_CYCLE_QUERY,
386 |reply| {
387 Ok((
388 codec::decode_hold_cycle_mask(reply)? == mask,
389 codec::decode_hold_cycle(reply)?,
390 ))
391 },
392 |now| {
393 format!(
394 "the buds accepted the hold cycle {}, but still report {}",
395 modes.join(" → "),
396 now.join(" → ")
397 )
398 },
399 )
400 }
401
402 /// Spec: realme_t310.md#gesture-write, #gesture-choices
403 fn set_touch(&mut self, side: &str, gesture: &str, action: &str, dry_run: bool) -> Result<TouchChange> {
404 let target = codec::touch_target(side, gesture, action)?;
405 if !self.commands.allows(codec::GESTURE_WRITE) || !self.commands.allows(codec::GESTURES) {
406 return Err(Error::Unsupported(
407 "these buds do not offer touch-control settings".to_owned(),
408 ));
409 }
410 // The write carries the whole table, so it starts from the table read just now.
411 let before = codec::decode_gesture_entries(&self.link.request(codec::GESTURES, &[])?)?;
412 let after = codec::retarget_gestures(&before, &target)?;
413 let changes: Vec<GestureChange> = before
414 .iter()
415 .zip(&after)
416 .filter(|(old, new)| old != new)
417 .map(|(old, new)| {
418 let (old, new) = (codec::name_gesture(old), codec::name_gesture(new));
419 GestureChange {
420 side: new.side,
421 gesture: new.gesture,
422 from: old.action,
423 to: new.action,
424 }
425 })
426 .collect();
427 let planned: Vec<_> = after.iter().map(codec::name_gesture).collect();
428 if changes.is_empty() {
429 return Ok(TouchChange {
430 changes,
431 write: None,
432 sent: false,
433 gestures: planned,
434 });
435 }
436 let write = RawWrite {
437 command: codec::GESTURE_WRITE,
438 payload: codec::gesture_write_payload(&after)?,
439 };
440 if dry_run {
441 return Ok(TouchChange {
442 changes,
443 write: Some(write),
444 sent: false,
445 gestures: planned,
446 });
447 }
448
449 let reply = self.link.request(write.command, &write.payload)?;
450 codec::check_write_reply(&reply, "the touch-control change")?;
451 let gestures = self.read_back(
452 TOUCH_READ_BACK,
453 codec::GESTURES,
454 &[],
455 |reply| {
456 let now = codec::decode_gesture_entries(reply)?;
457 // Applied when retargeting the table read back would change nothing.
458 let applied = codec::retarget_gestures(&now, &target).is_ok_and(|again| again == now);
459 Ok((applied, now))
460 },
461 |now| {
462 let still: Vec<String> = changes
463 .iter()
464 .filter_map(|change| {
465 now.iter()
466 .map(codec::name_gesture)
467 .find(|g| g.group.is_none() && g.side == change.side && g.gesture == change.gesture)
468 })
469 .filter(|g| g.action != action)
470 .map(|g| format!("{} {} {}", g.side, g.gesture, g.action))
471 .collect();
472 if still.is_empty() {
473 format!("the buds accepted {side} {gesture} {action}, but the table read back does not show it")
474 } else {
475 format!(
476 "the buds accepted {side} {gesture} {action}, but still report {}",
477 still.join(", ")
478 )
479 }
480 },
481 )?;
482 Ok(TouchChange {
483 changes,
484 write: Some(write),
485 sent: true,
486 gestures: gestures.iter().map(codec::name_gesture).collect(),
487 })
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494 use crate::status::{AncLevel, Charge};
495 use crate::transport::mock::{Script, Step, hex};
496
497 fn tx(cmd: u16, seq: u8, payload: &str) -> Step {
498 Step::Tx(frame::encode(cmd, seq, &hex(payload)).unwrap())
499 }
500
501 fn rx(cmd: u16, seq: u8, payload: &str) -> Step {
502 Step::Rx(frame::encode(cmd, seq, &hex(payload)).unwrap())
503 }
504
505 fn fixed_clock() -> u32 {
506 1_750_000_000
507 }
508
509 fn open(script: &Script) -> Result<T310Session> {
510 T310Session::open(Link::new(script.transport(), fixed_clock))
511 }
512
513 /// The handshake of the owner's T310 (HW1); seq 01-04 are used up.
514 fn handshake() -> Vec<Step> {
515 vec![
516 tx(0x0100, 0x01, ""),
517 rx(0x8100, 0x01, codec::T310_BITMAP_REPLY),
518 tx(0x0103, 0x02, ""),
519 rx(0x8103, 0x02, "00 12 3C 06"),
520 tx(0x0200, 0x03, ""),
521 rx(0x8200, 0x03, "00 09 01 02 03 04 05 06 0D F1 F4"),
522 tx(0x0205, 0x04, "04 01 02 03 04"),
523 rx(0x8205, 0x04, "00 04 01 00 02 00 03 00 04 00"),
524 ]
525 }
526
527 #[test]
528 fn full_status() {
529 let mut steps = handshake();
530 let mut firmware = String::from("00 03");
531 for byte in b"1,2,120,2,2,120,3,2,105" {
532 firmware.push_str(&format!(" {byte:02X}"));
533 }
534 steps.extend([
535 tx(0x010D, 0x05, "06 05 06 11 1A 1B 1D"),
536 rx(0x810D, 0x05, "00 06 05 00 06 00 11 01 1A 00 1B 01 1D 01"),
537 tx(0x010C, 0x06, "01 01"),
538 rx(0x810C, 0x06, "00 01 01 08"),
539 tx(0x0106, 0x07, ""),
540 rx(0x8106, 0x07, "00 03 01 50 02 E4 03 3C"),
541 tx(0x010F, 0x08, ""),
542 rx(0x810F, 0x08, "00 01"),
543 tx(0x0114, 0x09, ""),
544 rx(0x8114, 0x09, "00 02"),
545 tx(0x0105, 0x0A, ""),
546 rx(0x8105, 0x0A, &firmware),
547 ]);
548 let script = Script::new(steps);
549 let status = open(&script).unwrap().status().unwrap();
550 script.assert_done();
551
552 assert_eq!(
553 status.battery.left,
554 Some(Charge {
555 level: 80,
556 charging: false
557 })
558 );
559 assert_eq!(
560 status.battery.right,
561 Some(Charge {
562 level: 100,
563 charging: true
564 })
565 );
566 assert_eq!(
567 status.battery.case,
568 Some(Charge {
569 level: 60,
570 charging: false
571 })
572 );
573 let noise = status.noise_control.unwrap();
574 assert_eq!((noise.mode.as_str(), noise.level.as_deref()), ("anc", Some("deep")));
575 assert_eq!(status.eq.as_deref(), Some("deep-bass"));
576 assert_eq!(status.bass_level, None);
577 assert_eq!(status.codec.as_deref(), Some("AAC"));
578 assert_eq!(status.firmware.as_deref(), Some("120.120.105"));
579 let game = status.switches.iter().find(|s| s.key == "game").unwrap();
580 assert!(!game.on);
581 assert_eq!(status.switches.len(), 5);
582 assert!(status.warnings.is_empty());
583 }
584
585 #[test]
586 fn answers_the_time_request_during_the_settle() {
587 let mut steps = vec![
588 rx(0x0500, 0x01, ""),
589 Step::Tx(hex("AA 0C 00 00 00 85 01 05 00 00 80 E1 4E 68")),
590 ];
591 steps.extend(handshake());
592 // No 0x8205 ack this time: it may never arrive, and `open` does not wait for it.
593 // Spec: realme_t310.md#subscribe
594 steps.pop();
595 let script = Script::new(steps);
596 open(&script).unwrap();
597 script.assert_done();
598 }
599
600 #[test]
601 fn a_silent_handshake_reports_a_busy_channel() {
602 let script = Script::new(vec![tx(0x0100, 0x01, "")]);
603 assert!(matches!(open(&script), Err(Error::ChannelBusy(_))));
604 }
605
606 #[test]
607 fn other_models_are_refused() {
608 let script = Script::new(vec![
609 tx(0x0100, 0x01, ""),
610 rx(0x8100, 0x01, codec::T310_BITMAP_REPLY),
611 tx(0x0103, 0x02, ""),
612 rx(0x8103, 0x02, "00 14 54 06"),
613 ]);
614 match open(&script) {
615 Err(Error::WrongModel { found, .. }) => assert_eq!(found, "065414"),
616 other => panic!("expected WrongModel, got {:?}", other.err()),
617 }
618 }
619
620 #[test]
621 fn commands_missing_from_the_bitmap_are_never_sent() {
622 // A bitmap with no bits: only always-allowed commands go out, and no subscription.
623 let script = Script::new(vec![
624 tx(0x0100, 0x01, ""),
625 rx(0x8100, 0x01, "00"),
626 tx(0x0103, 0x02, ""),
627 rx(0x8103, 0x02, "00 12 3C 06"),
628 tx(0x0200, 0x03, ""),
629 rx(0x8200, 0x03, "00 02 01 02"),
630 tx(0x010D, 0x04, "06 05 06 11 1A 1B 1D"),
631 rx(0x810D, 0x04, "00 01 06 01"),
632 tx(0x0106, 0x05, ""),
633 // no battery reply: becomes a warning
634 ]);
635 let status = open(&script).unwrap().status().unwrap();
636 script.assert_done();
637 assert_eq!(status.switches.len(), 1);
638 assert!(status.noise_control.is_none() && status.firmware.is_none());
639 assert_eq!(status.warnings.len(), 1);
640 assert!(status.warnings.first().unwrap().starts_with("battery"));
641 }
642
643 #[test]
644 fn a_lost_link_stops_the_status() {
645 let mut steps = handshake();
646 steps.extend([tx(0x010D, 0x05, "06 05 06 11 1A 1B 1D"), Step::Close]);
647 let script = Script::new(steps);
648 assert!(matches!(open(&script).unwrap().status(), Err(Error::NotConnected(_))));
649 }
650
651 #[test]
652 fn touch_controls_and_hold_cycle() {
653 let mut steps = handshake();
654 steps.extend([
655 tx(0x0108, 0x05, ""),
656 rx(0x8108, 0x05, "00 02 01 01 02 01 02 01 04 08"),
657 tx(0x010C, 0x06, "02 01"),
658 rx(0x810C, 0x06, "00 02 01 0A"),
659 ]);
660 let script = Script::new(steps);
661 let touch = open(&script).unwrap().touch_controls().unwrap();
662 script.assert_done();
663 assert_eq!(touch.gestures.len(), 2);
664 assert_eq!(touch.hold_cycle.unwrap(), ["transparency", "anc"]);
665 }
666
667 /// The owner's `budctl --debug touch` (HW1), byte for byte. The buds' own frames (slice
668 /// byte 08) arrive glued to replies and need no answer.
669 #[test]
670 fn replays_the_hw1_touch_session() {
671 let script = Script::new(vec![
672 Step::Tx(hex("AA 07 00 00 00 01 01 00 00")),
673 Step::Rx(hex(
674 "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",
675 )),
676 Step::Tx(hex("AA 07 00 00 03 01 02 00 00")),
677 Step::Rx(hex("AA 0B 00 00 03 81 02 04 00 00 12 3C 06")),
678 Step::Tx(hex("AA 07 00 00 00 02 03 00 00")),
679 Step::Rx(hex("AA 12 00 00 00 82 03 0B 00 00 09 01 02 03 04 05 06 0D F1 F4")),
680 Step::Tx(hex("AA 0C 00 00 05 02 04 05 00 04 01 02 03 04")),
681 Step::Rx(hex(
682 "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",
683 )),
684 Step::Tx(hex("AA 07 00 00 08 01 05 00 00")),
685 Step::Rx(hex(
686 "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",
687 )),
688 Step::Tx(hex("AA 09 00 00 0C 01 06 02 00 02 01")),
689 Step::Rx(hex("AA 0B 00 00 0C 81 06 04 00 00 02 01 0A")),
690 ]);
691 let touch = open(&script).unwrap().touch_controls().unwrap();
692 script.assert_done();
693 let table: Vec<(&str, &str, &str)> = touch
694 .gestures
695 .iter()
696 .map(|g| (g.side.as_str(), g.gesture.as_str(), g.action.as_str()))
697 .collect();
698 assert_eq!(
699 table,
700 [
701 ("left", "double-tap", "play-pause"),
702 ("left", "triple-tap", "next"),
703 ("left", "hold", "noise-cycle"),
704 ("right", "double-tap", "play-pause"),
705 ("right", "triple-tap", "next"),
706 ("right", "hold", "noise-cycle"),
707 ]
708 );
709 assert_eq!(touch.hold_cycle.unwrap(), ["transparency", "anc"]);
710 assert!(touch.warnings.is_empty());
711 }
712
713 #[test]
714 fn sets_noise_control_and_reads_it_back() {
715 let mut steps = handshake();
716 steps.extend([
717 tx(0x0404, 0x05, "01 01 02"),
718 rx(0x8404, 0x05, "00"),
719 // The buds announce the change; it needs no answer.
720 rx(0x0204, 0x10, "03 01 01 02"),
721 tx(0x010C, 0x06, "01 01"),
722 rx(0x810C, 0x06, "00 01 01 02"),
723 ]);
724 let script = Script::new(steps);
725 let now = open(&script)
726 .unwrap()
727 .set_noise_control(NoiseMode::Transparency)
728 .unwrap();
729 script.assert_done();
730 assert_eq!((now.mode.as_str(), now.level), ("transparency", None));
731 }
732
733 #[test]
734 fn reads_back_again_until_the_change_shows() {
735 let mut steps = handshake();
736 steps.extend([
737 tx(0x0404, 0x05, "01 01 01"),
738 rx(0x8404, 0x05, "00"),
739 tx(0x010C, 0x06, "01 01"),
740 rx(0x810C, 0x06, "00 01 01 08"),
741 tx(0x010C, 0x07, "01 01"),
742 rx(0x810C, 0x07, "00 01 01 01"),
743 ]);
744 let script = Script::new(steps);
745 let now = open(&script).unwrap().set_noise_control(NoiseMode::Off).unwrap();
746 script.assert_done();
747 assert_eq!(now.mode, "off");
748 }
749
750 #[test]
751 fn sets_a_noise_cancelling_level() {
752 let mut steps = handshake();
753 steps.extend([
754 tx(0x0404, 0x05, "01 01 04"),
755 rx(0x8404, 0x05, "00"),
756 tx(0x010C, 0x06, "01 01"),
757 rx(0x810C, 0x06, "00 01 01 04"),
758 ]);
759 let script = Script::new(steps);
760 let now = open(&script)
761 .unwrap()
762 .set_noise_control(NoiseMode::Anc(AncLevel::Light))
763 .unwrap();
764 script.assert_done();
765 assert_eq!((now.mode.as_str(), now.level.as_deref()), ("anc", Some("light")));
766 }
767
768 #[test]
769 fn a_rejected_noise_control_change_is_not_read_back() {
770 let mut steps = handshake();
771 steps.extend([tx(0x0404, 0x05, "01 01 08"), rx(0x8404, 0x05, "0E")]);
772 let script = Script::new(steps);
773 let result = open(&script).unwrap().set_noise_control(NoiseMode::Anc(AncLevel::Deep));
774 script.assert_done();
775 assert!(matches!(result, Err(Error::Rejected { status: 0x0E, .. })));
776 }
777
778 #[test]
779 fn noise_control_is_not_written_without_its_bitmap_bit() {
780 let script = Script::new(vec![
781 tx(0x0100, 0x01, ""),
782 rx(0x8100, 0x01, "00"),
783 tx(0x0103, 0x02, ""),
784 rx(0x8103, 0x02, "00 12 3C 06"),
785 tx(0x0200, 0x03, ""),
786 rx(0x8200, 0x03, "00 00"),
787 ]);
788 let result = open(&script).unwrap().set_noise_control(NoiseMode::Off);
789 script.assert_done();
790 assert!(matches!(result, Err(Error::Unsupported(_))));
791 }
792
793 #[test]
794 fn sets_game_mode_and_reads_it_back() {
795 let mut steps = handshake();
796 steps.extend([
797 tx(0x0403, 0x05, "06 01"),
798 rx(0x8403, 0x05, "00"),
799 // Still the old value: budctl reads again.
800 tx(0x010D, 0x06, "06 05 06 11 1A 1B 1D"),
801 rx(0x810D, 0x06, "00 06 05 00 06 00 11 00 1A 01 1B 00 1D 00"),
802 tx(0x010D, 0x07, "06 05 06 11 1A 1B 1D"),
803 rx(0x810D, 0x07, "00 06 05 00 06 01 11 00 1A 01 1B 00 1D 00"),
804 ]);
805 let script = Script::new(steps);
806 let game = open(&script).unwrap().set_switch("game", true).unwrap();
807 script.assert_done();
808 assert_eq!((game.key, game.on), ("game", true));
809 }
810
811 #[test]
812 fn unknown_switches_are_never_written() {
813 // Nothing is sent after the handshake, so the subscribe ack is left out.
814 let mut steps = handshake();
815 steps.pop();
816 let script = Script::new(steps);
817 let mut session = open(&script).unwrap();
818 assert!(matches!(session.set_switch("turbo", true), Err(Error::Unsupported(_))));
819 script.assert_done();
820 }
821
822 #[test]
823 fn sets_bass_boost_and_reads_it_back_at_once() {
824 let mut steps = handshake();
825 steps.extend([
826 tx(0x0403, 0x05, "1D 01"),
827 rx(0x8403, 0x05, "00"),
828 tx(0x010D, 0x06, "06 05 06 11 1A 1B 1D"),
829 rx(0x810D, 0x06, "00 06 05 00 06 00 11 00 1A 01 1B 00 1D 01"),
830 ]);
831 let script = Script::new(steps);
832 let now = open(&script).unwrap().set_switch("bass-boost", true).unwrap();
833 script.assert_done();
834 assert_eq!((now.key, now.on), ("bass-boost", true));
835 }
836
837 #[test]
838 fn a_switch_the_buds_lack_is_rejected() {
839 let mut steps = handshake();
840 steps.extend([tx(0x0403, 0x05, "06 00"), rx(0x8403, 0x05, "01")]);
841 let script = Script::new(steps);
842 let result = open(&script).unwrap().set_switch("game", false);
843 script.assert_done();
844 assert!(matches!(result, Err(Error::Rejected { status: 0x01, .. })));
845 }
846
847 #[test]
848 fn selects_an_eq_preset_and_reads_it_back() {
849 let mut steps = handshake();
850 steps.extend([
851 tx(0x0406, 0x05, "01"),
852 rx(0x8406, 0x05, "00"),
853 tx(0x010F, 0x06, ""),
854 rx(0x810F, 0x06, "00 01"),
855 ]);
856 let script = Script::new(steps);
857 let now = open(&script).unwrap().set_eq("deep-bass").unwrap();
858 script.assert_done();
859 assert_eq!(now, "deep-bass");
860 }
861
862 #[test]
863 fn an_unknown_eq_preset_is_a_usage_error() {
864 let mut steps = handshake();
865 steps.pop();
866 let script = Script::new(steps);
867 let result = open(&script).unwrap().set_eq("loudness");
868 script.assert_done();
869 match result {
870 Err(Error::Usage(message)) => assert!(message.contains("pure-raw, deep-bass, pure-vocals, pure-bass")),
871 other => panic!("expected a usage error, got {other:?}"),
872 }
873 }
874
875 #[test]
876 fn rings_and_stops() {
877 let mut steps = handshake();
878 steps.extend([
879 tx(0x0400, 0x05, "01"),
880 rx(0x8400, 0x05, "00"),
881 tx(0x0400, 0x06, "00"),
882 rx(0x8400, 0x06, "00"),
883 ]);
884 let script = Script::new(steps);
885 let mut session = open(&script).unwrap();
886 session.find(true).unwrap();
887 session.find(false).unwrap();
888 script.assert_done();
889 }
890
891 #[test]
892 fn find_is_not_sent_without_its_bitmap_bit() {
893 // Bits 0-4 only: no bit 5.
894 let script = Script::new(vec![
895 tx(0x0100, 0x01, ""),
896 rx(0x8100, 0x01, "00 1F"),
897 tx(0x0103, 0x02, ""),
898 rx(0x8103, 0x02, "00 12 3C 06"),
899 tx(0x0200, 0x03, ""),
900 rx(0x8200, 0x03, "00 00"),
901 ]);
902 let result = open(&script).unwrap().find(true);
903 script.assert_done();
904 assert!(matches!(result, Err(Error::Unsupported(_))));
905 }
906
907 /// The owner's gesture table (HW1) and the same table with the left triple tap on volume up.
908 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";
909 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";
910
911 #[test]
912 fn a_touch_dry_run_reads_the_table_and_sends_nothing() {
913 let mut steps = handshake();
914 steps.extend([tx(0x0108, 0x05, ""), rx(0x8108, 0x05, HW1_TABLE)]);
915 let script = Script::new(steps);
916 let change = open(&script)
917 .unwrap()
918 .set_touch("left", "triple-tap", "volume-up", true)
919 .unwrap();
920 script.assert_done();
921 assert!(!change.sent);
922 let write = change.write.unwrap();
923 assert_eq!((write.command, write.payload), (0x0401, hex(LEFT_VOLUME_UP)));
924 assert_eq!(
925 change.changes,
926 [GestureChange {
927 side: "left".to_owned(),
928 gesture: "triple-tap".to_owned(),
929 from: "next".to_owned(),
930 to: "volume-up".to_owned(),
931 }]
932 );
933 let left_triple = change.gestures.get(1).unwrap();
934 assert_eq!(left_triple.action, "volume-up");
935 }
936
937 #[test]
938 fn sets_a_gesture_and_reads_the_table_back() {
939 let mut steps = handshake();
940 steps.extend([
941 tx(0x0108, 0x05, ""),
942 rx(0x8108, 0x05, HW1_TABLE),
943 tx(0x0401, 0x06, LEFT_VOLUME_UP),
944 // The owner's ack (W8): 00, then the old table cut to the write's length.
945 rx(
946 0x8401,
947 0x06,
948 "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",
949 ),
950 tx(0x0108, 0x07, ""),
951 rx(0x8108, 0x07, &format!("00 {LEFT_VOLUME_UP}")),
952 ]);
953 let script = Script::new(steps);
954 let change = open(&script)
955 .unwrap()
956 .set_touch("left", "triple-tap", "volume-up", false)
957 .unwrap();
958 script.assert_done();
959 assert!(change.sent);
960 assert_eq!(change.changes.len(), 1);
961 assert_eq!(change.gestures.get(1).unwrap().action, "volume-up");
962 }
963
964 #[test]
965 fn a_gesture_that_already_has_the_action_is_not_written() {
966 let mut steps = handshake();
967 steps.extend([tx(0x0108, 0x05, ""), rx(0x8108, 0x05, HW1_TABLE)]);
968 let script = Script::new(steps);
969 let change = open(&script)
970 .unwrap()
971 .set_touch("both", "hold", "noise-cycle", false)
972 .unwrap();
973 script.assert_done();
974 assert!(change.changes.is_empty() && change.write.is_none() && !change.sent);
975 }
976
977 #[test]
978 fn a_bad_touch_action_is_refused_before_anything_is_sent() {
979 let mut steps = handshake();
980 steps.pop();
981 let script = Script::new(steps);
982 let result = open(&script)
983 .unwrap()
984 .set_touch("left", "double-tap", "volume-up", false);
985 script.assert_done();
986 assert!(matches!(result, Err(Error::Usage(_))));
987 }
988
989 #[test]
990 fn sets_the_hold_cycle_and_reads_it_back() {
991 let mut steps = handshake();
992 steps.extend([
993 tx(0x0404, 0x05, "02 01 0B"),
994 rx(0x8404, 0x05, "00"),
995 tx(0x010C, 0x06, "02 01"),
996 rx(0x810C, 0x06, "00 02 01 0B"),
997 ]);
998 let script = Script::new(steps);
999 let now = open(&script)
1000 .unwrap()
1001 .set_hold_cycle(&["off", "transparency", "anc"])
1002 .unwrap();
1003 script.assert_done();
1004 assert_eq!(now, ["off", "transparency", "anc"]);
1005 }
1006
1007 #[test]
1008 fn a_one_mode_hold_cycle_is_refused_before_anything_is_sent() {
1009 let mut steps = handshake();
1010 steps.pop();
1011 let script = Script::new(steps);
1012 let result = open(&script).unwrap().set_hold_cycle(&["anc"]);
1013 script.assert_done();
1014 assert!(matches!(result, Err(Error::Usage(_))));
1015 }
1016}