Krolyxon
Commit 95ea4d7

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310mod.rs

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