Krolyxon
Commit 801bf1e

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310mod.rs

670 lines · 23.8 KB · Rust

1//! realme Buds T310. Spec: docs/protocol/realme_t310.md
2//!
3//! `frame.rs` and `link.rs` hold the protocol family's framing and request/reply logic;
4//! `codec.rs` holds everything specific to the T310.
5
6use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
7
8use crate::bluez::PairedDevice;
9use crate::bt::Uuid;
10use crate::devices::{Device, Session};
11use crate::error::{Error, Kind, Result};
12use crate::status::{NoiseControl, NoiseMode, Status, Switch, 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/// Spec: realme_t310.md#write-read-back
42const READ_BACK_PAUSE: Duration = Duration::from_millis(300);
43
44/// The switches budctl may write, each added after the previous one passed its hardware
45/// checkpoint (CLAUDE.md rule 4). Spec: realme_t310.md#switch-write
46const WRITABLE_SWITCHES: [(&str, ReadBack); 1] = [("game", GAME_READ_BACK)];
47
48fn on_off(on: bool) -> &'static str {
49 if on { "on" } else { "off" }
50}
51
52/// Spec: realme_t310.md#identity
53const BLUETOOTH_NAME: &str = "realme Buds T310";
54
55/// Control services, in the order to try. Spec: realme_t310.md#transport
56const SERVICES: [Uuid; 2] = [
57 Uuid::from_u128(0x0000_079A_D102_11E1_9B23_0002_5B00_A5A5),
58 Uuid::from_u128(0x0000_1107_D102_11E1_9B23_0002_5B00_A5A5),
59];
60
61/// Quiet time after connecting, before the first request. Spec: realme_t310.md#init
62const SETTLE: Duration = Duration::from_millis(300);
63
64pub struct RealmeT310;
65
66impl Device for RealmeT310 {
67 fn name(&self) -> &'static str {
68 BLUETOOTH_NAME
69 }
70
71 fn matches(&self, device: &PairedDevice) -> bool {
72 device.name == BLUETOOTH_NAME
73 }
74
75 fn rfcomm_uuids(&self) -> &'static [Uuid] {
76 &SERVICES
77 }
78
79 fn open(&self, transport: Box<dyn Transport>) -> Result<Box<dyn Session>> {
80 Ok(Box::new(T310Session::open(Link::new(transport, unix_time))?))
81 }
82}
83
84fn unix_time() -> u32 {
85 let seconds = SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.as_secs());
86 u32::try_from(seconds).unwrap_or(u32::MAX)
87}
88
89struct T310Session {
90 link: Link,
91 commands: Commands,
92}
93
94/// Collects what a series of best-effort reads produced.
95#[derive(Default)]
96struct Reads {
97 warnings: Vec<String>,
98 answered: usize,
99}
100
101impl T310Session {
102 /// The session start. Spec: realme_t310.md#init
103 fn open(mut link: Link) -> Result<Self> {
104 link.pump_until(Instant::now() + SETTLE)?;
105
106 let bitmap = link.request(codec::HANDSHAKE, &[]).map_err(|error| match error {
107 // Provisional until checkpoint HW1: a link that connects but stays silent is most
108 // likely held by another app. Spec: realme_t310.md#open
109 Error::Timeout(_) => Error::ChannelBusy("connected, but the buds did not answer the handshake".to_owned()),
110 other => other,
111 })?;
112 let commands = Commands::parse(&bitmap)?;
113
114 let product = codec::decode_product_id(&link.request(codec::PRODUCT_ID, &[])?)?;
115 if product != codec::T310_PRODUCT_ID {
116 return Err(Error::WrongModel {
117 expected: BLUETOOTH_NAME,
118 found: format!("{product:06X}"),
119 });
120 }
121
122 let offered = codec::decode_events_offered(&link.request(codec::EVENTS_OFFERED, &[])?)?;
123 let subscription = codec::subscribe_payload(&offered, &commands);
124 if commands.allows(codec::SUBSCRIBE) && subscription.len() > 1 {
125 // Some buds never acknowledge this, so budctl does not wait. Spec: realme_t310.md#subscribe
126 link.send(codec::SUBSCRIBE, &subscription)?;
127 }
128 Ok(Self { link, commands })
129 }
130
131 /// Read and decode one value, if the buds offer the command. A read the buds answer badly
132 /// (timeout, refusal, garbage) becomes a warning so the rest still shows; a lost link stops
133 /// everything.
134 fn read<T>(
135 &mut self,
136 reads: &mut Reads,
137 what: &str,
138 cmd: u16,
139 payload: &[u8],
140 decode: fn(&[u8]) -> Result<T>,
141 ) -> Result<Option<T>> {
142 if !self.commands.allows(cmd) {
143 return Ok(None);
144 }
145 match self.link.request(cmd, payload).and_then(|reply| decode(&reply)) {
146 Ok(value) => {
147 reads.answered += 1;
148 Ok(Some(value))
149 }
150 Err(error) if error.kind() == Kind::Failed => {
151 reads.warnings.push(format!("{what}: {error}"));
152 Ok(None)
153 }
154 Err(error) => Err(error),
155 }
156 }
157}
158
159impl T310Session {
160 /// After an acked write, read the setting back with `cmd query` until `check` reports the
161 /// new value, following `timing`. `check` returns whether the value is the one written,
162 /// and the value; `not_applied` describes a value that never changed.
163 /// Spec: realme_t310.md#write-read-back
164 fn read_back<T>(
165 &mut self,
166 timing: ReadBack,
167 cmd: u16,
168 query: &[u8],
169 check: impl Fn(&[u8]) -> Result<(bool, T)>,
170 not_applied: impl Fn(T) -> String,
171 ) -> Result<T> {
172 let acked = Instant::now();
173 self.link.pump_until(acked + timing.first)?;
174 loop {
175 let (applied, value) = check(&self.link.request(cmd, query)?)?;
176 if applied {
177 return Ok(value);
178 }
179 if Instant::now() >= acked + timing.window {
180 return Err(Error::NotApplied(not_applied(value)));
181 }
182 self.link.pump_until(Instant::now() + READ_BACK_PAUSE)?;
183 }
184 }
185}
186
187impl Session for T310Session {
188 fn status(&mut self) -> Result<Status> {
189 let mut reads = Reads::default();
190 let switches = self.read(
191 &mut reads,
192 "switches",
193 codec::SWITCHES,
194 &codec::SWITCH_QUERY,
195 codec::decode_switches,
196 )?;
197 let noise_control = self.read(
198 &mut reads,
199 "noise control",
200 codec::NOISE_CONTROL,
201 &codec::NOISE_MODE_QUERY,
202 codec::decode_noise_mode,
203 )?;
204 let battery = self.read(&mut reads, "battery", codec::BATTERY, &[], codec::decode_battery)?;
205 let eq = self.read(&mut reads, "EQ", codec::EQ_PRESET, &[], codec::decode_eq)?;
206 // No bass-level read: the T310 does not answer an empty 0x0124 (HW1). It returns with W6.
207 // Spec: realme_t310.md#bass-read
208 let codec = self.read(&mut reads, "codec", codec::CODEC, &[], codec::decode_codec)?;
209 let firmware = self.read(&mut reads, "firmware", codec::FIRMWARE, &[], codec::decode_firmware)?;
210
211 if reads.answered == 0 && !reads.warnings.is_empty() {
212 return Err(Error::Timeout(reads.warnings.join("; ")));
213 }
214 Ok(Status {
215 battery: battery.unwrap_or_default(),
216 noise_control,
217 eq,
218 bass_level: None,
219 switches: switches.unwrap_or_default(),
220 codec,
221 firmware,
222 warnings: reads.warnings,
223 })
224 }
225
226 /// Spec: realme_t310.md#anc-write, #write-read-back
227 fn set_noise_control(&mut self, mode: NoiseMode) -> Result<NoiseControl> {
228 if !self.commands.allows(codec::NOISE_CONTROL_WRITE) || !self.commands.allows(codec::NOISE_CONTROL) {
229 return Err(Error::Unsupported(
230 "these buds do not offer noise-control settings".to_owned(),
231 ));
232 }
233 let reply = self
234 .link
235 .request(codec::NOISE_CONTROL_WRITE, &codec::noise_write_payload(mode))?;
236 codec::check_write_reply(&reply, "the noise-control change")?;
237
238 let wanted = codec::noise_write_mask(mode);
239 self.read_back(
240 READ_BACK,
241 codec::NOISE_CONTROL,
242 &codec::NOISE_MODE_QUERY,
243 |reply| {
244 Ok((
245 codec::decode_noise_mask(reply)? == wanted,
246 codec::decode_noise_mode(reply)?,
247 ))
248 },
249 |now| {
250 let wanted = match mode {
251 NoiseMode::Anc(level) => format!("anc ({})", level.name()),
252 _ => mode.name().to_owned(),
253 };
254 let reported = match &now.level {
255 Some(level) => format!("{} ({level})", now.mode),
256 None => now.mode,
257 };
258 format!("the buds accepted noise control {wanted}, but still report {reported}")
259 },
260 )
261 }
262
263 /// Spec: realme_t310.md#switch-write, #write-read-back
264 fn set_switch(&mut self, key: &str, on: bool) -> Result<Switch> {
265 let Some(id) = codec::switch_id(key) else {
266 return Err(Error::Unsupported(format!("these buds have no switch called {key}")));
267 };
268 let Some(&(_, timing)) = WRITABLE_SWITCHES.iter().find(|(k, _)| *k == key) else {
269 return Err(Error::Unsupported(format!("budctl cannot change {key} yet")));
270 };
271 if !self.commands.allows(codec::SWITCH_WRITE) || !self.commands.allows(codec::SWITCHES) {
272 return Err(Error::Unsupported("these buds do not offer switch settings".to_owned()));
273 }
274 let reply = self
275 .link
276 .request(codec::SWITCH_WRITE, &codec::switch_write_payload(id, on))?;
277 codec::check_write_reply(&reply, "the switch change")?;
278
279 self.read_back(
280 timing,
281 codec::SWITCHES,
282 &codec::SWITCH_QUERY,
283 |reply| {
284 let switch = codec::decode_switches(reply)?
285 .into_iter()
286 .find(|s| s.key == key)
287 .ok_or_else(|| Error::Protocol(format!("the switch reply has no {key}")))?;
288 Ok((switch.on == on, switch))
289 },
290 |now| {
291 format!(
292 "the buds accepted {} {}, but still report {}",
293 now.label,
294 on_off(on),
295 on_off(now.on)
296 )
297 },
298 )
299 }
300
301 fn touch_controls(&mut self) -> Result<TouchControls> {
302 if !self.commands.allows(codec::GESTURES) {
303 return Err(Error::Unsupported(
304 "these buds do not offer touch-control settings".to_owned(),
305 ));
306 }
307 let gestures = codec::decode_gestures(&self.link.request(codec::GESTURES, &[])?)?;
308 let mut reads = Reads::default();
309 let hold_cycle = self.read(
310 &mut reads,
311 "hold cycle",
312 codec::NOISE_CONTROL,
313 &codec::HOLD_CYCLE_QUERY,
314 codec::decode_hold_cycle,
315 )?;
316 Ok(TouchControls {
317 gestures,
318 hold_cycle,
319 warnings: reads.warnings,
320 })
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use crate::status::{AncLevel, Charge};
328 use crate::transport::mock::{Script, Step, hex};
329
330 fn tx(cmd: u16, seq: u8, payload: &str) -> Step {
331 Step::Tx(frame::encode(cmd, seq, &hex(payload)).unwrap())
332 }
333
334 fn rx(cmd: u16, seq: u8, payload: &str) -> Step {
335 Step::Rx(frame::encode(cmd, seq, &hex(payload)).unwrap())
336 }
337
338 fn fixed_clock() -> u32 {
339 1_750_000_000
340 }
341
342 fn open(script: &Script) -> Result<T310Session> {
343 T310Session::open(Link::new(script.transport(), fixed_clock))
344 }
345
346 /// The handshake of the owner's T310 (HW1); seq 01-04 are used up.
347 fn handshake() -> Vec<Step> {
348 vec![
349 tx(0x0100, 0x01, ""),
350 rx(0x8100, 0x01, codec::T310_BITMAP_REPLY),
351 tx(0x0103, 0x02, ""),
352 rx(0x8103, 0x02, "00 12 3C 06"),
353 tx(0x0200, 0x03, ""),
354 rx(0x8200, 0x03, "00 09 01 02 03 04 05 06 0D F1 F4"),
355 tx(0x0205, 0x04, "04 01 02 03 04"),
356 rx(0x8205, 0x04, "00 04 01 00 02 00 03 00 04 00"),
357 ]
358 }
359
360 #[test]
361 fn full_status() {
362 let mut steps = handshake();
363 let mut firmware = String::from("00 03");
364 for byte in b"1,2,120,2,2,120,3,2,105" {
365 firmware.push_str(&format!(" {byte:02X}"));
366 }
367 steps.extend([
368 tx(0x010D, 0x05, "06 05 06 11 1A 1B 1D"),
369 rx(0x810D, 0x05, "00 06 05 00 06 00 11 01 1A 00 1B 01 1D 01"),
370 tx(0x010C, 0x06, "01 01"),
371 rx(0x810C, 0x06, "00 01 01 08"),
372 tx(0x0106, 0x07, ""),
373 rx(0x8106, 0x07, "00 03 01 50 02 E4 03 3C"),
374 tx(0x010F, 0x08, ""),
375 rx(0x810F, 0x08, "00 01"),
376 tx(0x0114, 0x09, ""),
377 rx(0x8114, 0x09, "00 02"),
378 tx(0x0105, 0x0A, ""),
379 rx(0x8105, 0x0A, &firmware),
380 ]);
381 let script = Script::new(steps);
382 let status = open(&script).unwrap().status().unwrap();
383 script.assert_done();
384
385 assert_eq!(
386 status.battery.left,
387 Some(Charge {
388 level: 80,
389 charging: false
390 })
391 );
392 assert_eq!(
393 status.battery.right,
394 Some(Charge {
395 level: 100,
396 charging: true
397 })
398 );
399 assert_eq!(
400 status.battery.case,
401 Some(Charge {
402 level: 60,
403 charging: false
404 })
405 );
406 let noise = status.noise_control.unwrap();
407 assert_eq!((noise.mode.as_str(), noise.level.as_deref()), ("anc", Some("deep")));
408 assert_eq!(status.eq.as_deref(), Some("deep-bass"));
409 assert_eq!(status.bass_level, None);
410 assert_eq!(status.codec.as_deref(), Some("AAC"));
411 assert_eq!(status.firmware.as_deref(), Some("120.120.105"));
412 let game = status.switches.iter().find(|s| s.key == "game").unwrap();
413 assert!(!game.on);
414 assert_eq!(status.switches.len(), 5);
415 assert!(status.warnings.is_empty());
416 }
417
418 #[test]
419 fn answers_the_time_request_during_the_settle() {
420 let mut steps = vec![
421 rx(0x0500, 0x01, ""),
422 Step::Tx(hex("AA 0C 00 00 00 85 01 05 00 00 80 E1 4E 68")),
423 ];
424 steps.extend(handshake());
425 // No 0x8205 ack this time: it may never arrive, and `open` does not wait for it.
426 // Spec: realme_t310.md#subscribe
427 steps.pop();
428 let script = Script::new(steps);
429 open(&script).unwrap();
430 script.assert_done();
431 }
432
433 #[test]
434 fn a_silent_handshake_reports_a_busy_channel() {
435 let script = Script::new(vec![tx(0x0100, 0x01, "")]);
436 assert!(matches!(open(&script), Err(Error::ChannelBusy(_))));
437 }
438
439 #[test]
440 fn other_models_are_refused() {
441 let script = Script::new(vec![
442 tx(0x0100, 0x01, ""),
443 rx(0x8100, 0x01, codec::T310_BITMAP_REPLY),
444 tx(0x0103, 0x02, ""),
445 rx(0x8103, 0x02, "00 14 54 06"),
446 ]);
447 match open(&script) {
448 Err(Error::WrongModel { found, .. }) => assert_eq!(found, "065414"),
449 other => panic!("expected WrongModel, got {:?}", other.err()),
450 }
451 }
452
453 #[test]
454 fn commands_missing_from_the_bitmap_are_never_sent() {
455 // A bitmap with no bits: only always-allowed commands go out, and no subscription.
456 let script = Script::new(vec![
457 tx(0x0100, 0x01, ""),
458 rx(0x8100, 0x01, "00"),
459 tx(0x0103, 0x02, ""),
460 rx(0x8103, 0x02, "00 12 3C 06"),
461 tx(0x0200, 0x03, ""),
462 rx(0x8200, 0x03, "00 02 01 02"),
463 tx(0x010D, 0x04, "06 05 06 11 1A 1B 1D"),
464 rx(0x810D, 0x04, "00 01 06 01"),
465 tx(0x0106, 0x05, ""),
466 // no battery reply: becomes a warning
467 ]);
468 let status = open(&script).unwrap().status().unwrap();
469 script.assert_done();
470 assert_eq!(status.switches.len(), 1);
471 assert!(status.noise_control.is_none() && status.firmware.is_none());
472 assert_eq!(status.warnings.len(), 1);
473 assert!(status.warnings.first().unwrap().starts_with("battery"));
474 }
475
476 #[test]
477 fn a_lost_link_stops_the_status() {
478 let mut steps = handshake();
479 steps.extend([tx(0x010D, 0x05, "06 05 06 11 1A 1B 1D"), Step::Close]);
480 let script = Script::new(steps);
481 assert!(matches!(open(&script).unwrap().status(), Err(Error::NotConnected(_))));
482 }
483
484 #[test]
485 fn touch_controls_and_hold_cycle() {
486 let mut steps = handshake();
487 steps.extend([
488 tx(0x0108, 0x05, ""),
489 rx(0x8108, 0x05, "00 02 01 01 02 01 02 01 04 08"),
490 tx(0x010C, 0x06, "02 01"),
491 rx(0x810C, 0x06, "00 02 01 0A"),
492 ]);
493 let script = Script::new(steps);
494 let touch = open(&script).unwrap().touch_controls().unwrap();
495 script.assert_done();
496 assert_eq!(touch.gestures.len(), 2);
497 assert_eq!(touch.hold_cycle.unwrap(), ["transparency", "anc"]);
498 }
499
500 /// The owner's `budctl --debug touch` (HW1), byte for byte. The buds' own frames (slice
501 /// byte 08) arrive glued to replies and need no answer.
502 #[test]
503 fn replays_the_hw1_touch_session() {
504 let script = Script::new(vec![
505 Step::Tx(hex("AA 07 00 00 00 01 01 00 00")),
506 Step::Rx(hex(
507 "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",
508 )),
509 Step::Tx(hex("AA 07 00 00 03 01 02 00 00")),
510 Step::Rx(hex("AA 0B 00 00 03 81 02 04 00 00 12 3C 06")),
511 Step::Tx(hex("AA 07 00 00 00 02 03 00 00")),
512 Step::Rx(hex("AA 12 00 00 00 82 03 0B 00 00 09 01 02 03 04 05 06 0D F1 F4")),
513 Step::Tx(hex("AA 0C 00 00 05 02 04 05 00 04 01 02 03 04")),
514 Step::Rx(hex(
515 "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",
516 )),
517 Step::Tx(hex("AA 07 00 00 08 01 05 00 00")),
518 Step::Rx(hex(
519 "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",
520 )),
521 Step::Tx(hex("AA 09 00 00 0C 01 06 02 00 02 01")),
522 Step::Rx(hex("AA 0B 00 00 0C 81 06 04 00 00 02 01 0A")),
523 ]);
524 let touch = open(&script).unwrap().touch_controls().unwrap();
525 script.assert_done();
526 let table: Vec<(&str, &str, &str)> = touch
527 .gestures
528 .iter()
529 .map(|g| (g.side.as_str(), g.gesture.as_str(), g.action.as_str()))
530 .collect();
531 assert_eq!(
532 table,
533 [
534 ("left", "double-tap", "play-pause"),
535 ("left", "triple-tap", "next"),
536 ("left", "hold", "noise-cycle"),
537 ("right", "double-tap", "play-pause"),
538 ("right", "triple-tap", "next"),
539 ("right", "hold", "noise-cycle"),
540 ]
541 );
542 assert_eq!(touch.hold_cycle.unwrap(), ["transparency", "anc"]);
543 assert!(touch.warnings.is_empty());
544 }
545
546 #[test]
547 fn sets_noise_control_and_reads_it_back() {
548 let mut steps = handshake();
549 steps.extend([
550 tx(0x0404, 0x05, "01 01 02"),
551 rx(0x8404, 0x05, "00"),
552 // The buds announce the change; it needs no answer.
553 rx(0x0204, 0x10, "03 01 01 02"),
554 tx(0x010C, 0x06, "01 01"),
555 rx(0x810C, 0x06, "00 01 01 02"),
556 ]);
557 let script = Script::new(steps);
558 let now = open(&script)
559 .unwrap()
560 .set_noise_control(NoiseMode::Transparency)
561 .unwrap();
562 script.assert_done();
563 assert_eq!((now.mode.as_str(), now.level), ("transparency", None));
564 }
565
566 #[test]
567 fn reads_back_again_until_the_change_shows() {
568 let mut steps = handshake();
569 steps.extend([
570 tx(0x0404, 0x05, "01 01 01"),
571 rx(0x8404, 0x05, "00"),
572 tx(0x010C, 0x06, "01 01"),
573 rx(0x810C, 0x06, "00 01 01 08"),
574 tx(0x010C, 0x07, "01 01"),
575 rx(0x810C, 0x07, "00 01 01 01"),
576 ]);
577 let script = Script::new(steps);
578 let now = open(&script).unwrap().set_noise_control(NoiseMode::Off).unwrap();
579 script.assert_done();
580 assert_eq!(now.mode, "off");
581 }
582
583 #[test]
584 fn sets_a_noise_cancelling_level() {
585 let mut steps = handshake();
586 steps.extend([
587 tx(0x0404, 0x05, "01 01 04"),
588 rx(0x8404, 0x05, "00"),
589 tx(0x010C, 0x06, "01 01"),
590 rx(0x810C, 0x06, "00 01 01 04"),
591 ]);
592 let script = Script::new(steps);
593 let now = open(&script)
594 .unwrap()
595 .set_noise_control(NoiseMode::Anc(AncLevel::Light))
596 .unwrap();
597 script.assert_done();
598 assert_eq!((now.mode.as_str(), now.level.as_deref()), ("anc", Some("light")));
599 }
600
601 #[test]
602 fn a_rejected_noise_control_change_is_not_read_back() {
603 let mut steps = handshake();
604 steps.extend([tx(0x0404, 0x05, "01 01 08"), rx(0x8404, 0x05, "0E")]);
605 let script = Script::new(steps);
606 let result = open(&script).unwrap().set_noise_control(NoiseMode::Anc(AncLevel::Deep));
607 script.assert_done();
608 assert!(matches!(result, Err(Error::Rejected { status: 0x0E, .. })));
609 }
610
611 #[test]
612 fn noise_control_is_not_written_without_its_bitmap_bit() {
613 let script = Script::new(vec![
614 tx(0x0100, 0x01, ""),
615 rx(0x8100, 0x01, "00"),
616 tx(0x0103, 0x02, ""),
617 rx(0x8103, 0x02, "00 12 3C 06"),
618 tx(0x0200, 0x03, ""),
619 rx(0x8200, 0x03, "00 00"),
620 ]);
621 let result = open(&script).unwrap().set_noise_control(NoiseMode::Off);
622 script.assert_done();
623 assert!(matches!(result, Err(Error::Unsupported(_))));
624 }
625
626 #[test]
627 fn sets_game_mode_and_reads_it_back() {
628 let mut steps = handshake();
629 steps.extend([
630 tx(0x0403, 0x05, "06 01"),
631 rx(0x8403, 0x05, "00"),
632 // Still the old value: budctl reads again.
633 tx(0x010D, 0x06, "06 05 06 11 1A 1B 1D"),
634 rx(0x810D, 0x06, "00 06 05 00 06 00 11 00 1A 01 1B 00 1D 00"),
635 tx(0x010D, 0x07, "06 05 06 11 1A 1B 1D"),
636 rx(0x810D, 0x07, "00 06 05 00 06 01 11 00 1A 01 1B 00 1D 00"),
637 ]);
638 let script = Script::new(steps);
639 let game = open(&script).unwrap().set_switch("game", true).unwrap();
640 script.assert_done();
641 assert_eq!((game.key, game.on), ("game", true));
642 }
643
644 #[test]
645 fn switches_not_yet_verified_are_never_written() {
646 // Nothing is sent after the handshake, so the subscribe ack is left out.
647 let mut steps = handshake();
648 steps.pop();
649 let script = Script::new(steps);
650 let mut session = open(&script).unwrap();
651 for key in ["dual", "wind", "spatial", "bass-boost"] {
652 assert!(
653 matches!(session.set_switch(key, true), Err(Error::Unsupported(_))),
654 "{key}"
655 );
656 }
657 assert!(matches!(session.set_switch("turbo", true), Err(Error::Unsupported(_))));
658 script.assert_done();
659 }
660
661 #[test]
662 fn a_switch_the_buds_lack_is_rejected() {
663 let mut steps = handshake();
664 steps.extend([tx(0x0403, 0x05, "06 00"), rx(0x8403, 0x05, "01")]);
665 let script = Script::new(steps);
666 let result = open(&script).unwrap().set_switch("game", false);
667 script.assert_done();
668 assert!(matches!(result, Err(Error::Rejected { status: 0x01, .. })));
669 }
670}