Krolyxon
Commit 39f4a39

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310mod.rs

724 lines · 25.7 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 /// Built-in presets only; custom EQs come with W11. Spec: realme_t310.md#eq-write
264 fn set_eq(&mut self, preset: &str) -> Result<String> {
265 let Some(id) = codec::eq_id(preset) else {
266 return Err(Error::Usage(format!(
267 "no EQ preset called {preset}; these buds have: {}",
268 codec::eq_names().join(", ")
269 )));
270 };
271 if !self.commands.allows(codec::EQ_SELECT) || !self.commands.allows(codec::EQ_PRESET) {
272 return Err(Error::Unsupported("these buds do not offer EQ presets".to_owned()));
273 }
274 let reply = self.link.request(codec::EQ_SELECT, &[id])?;
275 codec::check_write_reply(&reply, "the EQ change")?;
276
277 self.read_back(
278 READ_BACK,
279 codec::EQ_PRESET,
280 &[],
281 |reply| {
282 let now = codec::decode_eq(reply)?;
283 Ok((now == preset, now))
284 },
285 |now| format!("the buds accepted EQ {preset}, but still report {now}"),
286 )
287 }
288
289 /// Spec: realme_t310.md#switch-write, #write-read-back
290 fn set_switch(&mut self, key: &str, on: bool) -> Result<Switch> {
291 let Some(id) = codec::switch_id(key) else {
292 return Err(Error::Unsupported(format!("these buds have no switch called {key}")));
293 };
294 let Some(&(_, timing)) = WRITABLE_SWITCHES.iter().find(|(k, _)| *k == key) else {
295 return Err(Error::Unsupported(format!("budctl cannot change {key} yet")));
296 };
297 if !self.commands.allows(codec::SWITCH_WRITE) || !self.commands.allows(codec::SWITCHES) {
298 return Err(Error::Unsupported("these buds do not offer switch settings".to_owned()));
299 }
300 let reply = self
301 .link
302 .request(codec::SWITCH_WRITE, &codec::switch_write_payload(id, on))?;
303 codec::check_write_reply(&reply, "the switch change")?;
304
305 self.read_back(
306 timing,
307 codec::SWITCHES,
308 &codec::SWITCH_QUERY,
309 |reply| {
310 let switch = codec::decode_switches(reply)?
311 .into_iter()
312 .find(|s| s.key == key)
313 .ok_or_else(|| Error::Protocol(format!("the switch reply has no {key}")))?;
314 Ok((switch.on == on, switch))
315 },
316 |now| {
317 format!(
318 "the buds accepted {} {}, but still report {}",
319 now.label,
320 on_off(on),
321 on_off(now.on)
322 )
323 },
324 )
325 }
326
327 fn touch_controls(&mut self) -> Result<TouchControls> {
328 if !self.commands.allows(codec::GESTURES) {
329 return Err(Error::Unsupported(
330 "these buds do not offer touch-control settings".to_owned(),
331 ));
332 }
333 let gestures = codec::decode_gestures(&self.link.request(codec::GESTURES, &[])?)?;
334 let mut reads = Reads::default();
335 let hold_cycle = self.read(
336 &mut reads,
337 "hold cycle",
338 codec::NOISE_CONTROL,
339 &codec::HOLD_CYCLE_QUERY,
340 codec::decode_hold_cycle,
341 )?;
342 Ok(TouchControls {
343 gestures,
344 hold_cycle,
345 warnings: reads.warnings,
346 })
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353 use crate::status::{AncLevel, Charge};
354 use crate::transport::mock::{Script, Step, hex};
355
356 fn tx(cmd: u16, seq: u8, payload: &str) -> Step {
357 Step::Tx(frame::encode(cmd, seq, &hex(payload)).unwrap())
358 }
359
360 fn rx(cmd: u16, seq: u8, payload: &str) -> Step {
361 Step::Rx(frame::encode(cmd, seq, &hex(payload)).unwrap())
362 }
363
364 fn fixed_clock() -> u32 {
365 1_750_000_000
366 }
367
368 fn open(script: &Script) -> Result<T310Session> {
369 T310Session::open(Link::new(script.transport(), fixed_clock))
370 }
371
372 /// The handshake of the owner's T310 (HW1); seq 01-04 are used up.
373 fn handshake() -> Vec<Step> {
374 vec![
375 tx(0x0100, 0x01, ""),
376 rx(0x8100, 0x01, codec::T310_BITMAP_REPLY),
377 tx(0x0103, 0x02, ""),
378 rx(0x8103, 0x02, "00 12 3C 06"),
379 tx(0x0200, 0x03, ""),
380 rx(0x8200, 0x03, "00 09 01 02 03 04 05 06 0D F1 F4"),
381 tx(0x0205, 0x04, "04 01 02 03 04"),
382 rx(0x8205, 0x04, "00 04 01 00 02 00 03 00 04 00"),
383 ]
384 }
385
386 #[test]
387 fn full_status() {
388 let mut steps = handshake();
389 let mut firmware = String::from("00 03");
390 for byte in b"1,2,120,2,2,120,3,2,105" {
391 firmware.push_str(&format!(" {byte:02X}"));
392 }
393 steps.extend([
394 tx(0x010D, 0x05, "06 05 06 11 1A 1B 1D"),
395 rx(0x810D, 0x05, "00 06 05 00 06 00 11 01 1A 00 1B 01 1D 01"),
396 tx(0x010C, 0x06, "01 01"),
397 rx(0x810C, 0x06, "00 01 01 08"),
398 tx(0x0106, 0x07, ""),
399 rx(0x8106, 0x07, "00 03 01 50 02 E4 03 3C"),
400 tx(0x010F, 0x08, ""),
401 rx(0x810F, 0x08, "00 01"),
402 tx(0x0114, 0x09, ""),
403 rx(0x8114, 0x09, "00 02"),
404 tx(0x0105, 0x0A, ""),
405 rx(0x8105, 0x0A, &firmware),
406 ]);
407 let script = Script::new(steps);
408 let status = open(&script).unwrap().status().unwrap();
409 script.assert_done();
410
411 assert_eq!(
412 status.battery.left,
413 Some(Charge {
414 level: 80,
415 charging: false
416 })
417 );
418 assert_eq!(
419 status.battery.right,
420 Some(Charge {
421 level: 100,
422 charging: true
423 })
424 );
425 assert_eq!(
426 status.battery.case,
427 Some(Charge {
428 level: 60,
429 charging: false
430 })
431 );
432 let noise = status.noise_control.unwrap();
433 assert_eq!((noise.mode.as_str(), noise.level.as_deref()), ("anc", Some("deep")));
434 assert_eq!(status.eq.as_deref(), Some("deep-bass"));
435 assert_eq!(status.bass_level, None);
436 assert_eq!(status.codec.as_deref(), Some("AAC"));
437 assert_eq!(status.firmware.as_deref(), Some("120.120.105"));
438 let game = status.switches.iter().find(|s| s.key == "game").unwrap();
439 assert!(!game.on);
440 assert_eq!(status.switches.len(), 5);
441 assert!(status.warnings.is_empty());
442 }
443
444 #[test]
445 fn answers_the_time_request_during_the_settle() {
446 let mut steps = vec![
447 rx(0x0500, 0x01, ""),
448 Step::Tx(hex("AA 0C 00 00 00 85 01 05 00 00 80 E1 4E 68")),
449 ];
450 steps.extend(handshake());
451 // No 0x8205 ack this time: it may never arrive, and `open` does not wait for it.
452 // Spec: realme_t310.md#subscribe
453 steps.pop();
454 let script = Script::new(steps);
455 open(&script).unwrap();
456 script.assert_done();
457 }
458
459 #[test]
460 fn a_silent_handshake_reports_a_busy_channel() {
461 let script = Script::new(vec![tx(0x0100, 0x01, "")]);
462 assert!(matches!(open(&script), Err(Error::ChannelBusy(_))));
463 }
464
465 #[test]
466 fn other_models_are_refused() {
467 let script = Script::new(vec![
468 tx(0x0100, 0x01, ""),
469 rx(0x8100, 0x01, codec::T310_BITMAP_REPLY),
470 tx(0x0103, 0x02, ""),
471 rx(0x8103, 0x02, "00 14 54 06"),
472 ]);
473 match open(&script) {
474 Err(Error::WrongModel { found, .. }) => assert_eq!(found, "065414"),
475 other => panic!("expected WrongModel, got {:?}", other.err()),
476 }
477 }
478
479 #[test]
480 fn commands_missing_from_the_bitmap_are_never_sent() {
481 // A bitmap with no bits: only always-allowed commands go out, and no subscription.
482 let script = Script::new(vec![
483 tx(0x0100, 0x01, ""),
484 rx(0x8100, 0x01, "00"),
485 tx(0x0103, 0x02, ""),
486 rx(0x8103, 0x02, "00 12 3C 06"),
487 tx(0x0200, 0x03, ""),
488 rx(0x8200, 0x03, "00 02 01 02"),
489 tx(0x010D, 0x04, "06 05 06 11 1A 1B 1D"),
490 rx(0x810D, 0x04, "00 01 06 01"),
491 tx(0x0106, 0x05, ""),
492 // no battery reply: becomes a warning
493 ]);
494 let status = open(&script).unwrap().status().unwrap();
495 script.assert_done();
496 assert_eq!(status.switches.len(), 1);
497 assert!(status.noise_control.is_none() && status.firmware.is_none());
498 assert_eq!(status.warnings.len(), 1);
499 assert!(status.warnings.first().unwrap().starts_with("battery"));
500 }
501
502 #[test]
503 fn a_lost_link_stops_the_status() {
504 let mut steps = handshake();
505 steps.extend([tx(0x010D, 0x05, "06 05 06 11 1A 1B 1D"), Step::Close]);
506 let script = Script::new(steps);
507 assert!(matches!(open(&script).unwrap().status(), Err(Error::NotConnected(_))));
508 }
509
510 #[test]
511 fn touch_controls_and_hold_cycle() {
512 let mut steps = handshake();
513 steps.extend([
514 tx(0x0108, 0x05, ""),
515 rx(0x8108, 0x05, "00 02 01 01 02 01 02 01 04 08"),
516 tx(0x010C, 0x06, "02 01"),
517 rx(0x810C, 0x06, "00 02 01 0A"),
518 ]);
519 let script = Script::new(steps);
520 let touch = open(&script).unwrap().touch_controls().unwrap();
521 script.assert_done();
522 assert_eq!(touch.gestures.len(), 2);
523 assert_eq!(touch.hold_cycle.unwrap(), ["transparency", "anc"]);
524 }
525
526 /// The owner's `budctl --debug touch` (HW1), byte for byte. The buds' own frames (slice
527 /// byte 08) arrive glued to replies and need no answer.
528 #[test]
529 fn replays_the_hw1_touch_session() {
530 let script = Script::new(vec![
531 Step::Tx(hex("AA 07 00 00 00 01 01 00 00")),
532 Step::Rx(hex(
533 "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",
534 )),
535 Step::Tx(hex("AA 07 00 00 03 01 02 00 00")),
536 Step::Rx(hex("AA 0B 00 00 03 81 02 04 00 00 12 3C 06")),
537 Step::Tx(hex("AA 07 00 00 00 02 03 00 00")),
538 Step::Rx(hex("AA 12 00 00 00 82 03 0B 00 00 09 01 02 03 04 05 06 0D F1 F4")),
539 Step::Tx(hex("AA 0C 00 00 05 02 04 05 00 04 01 02 03 04")),
540 Step::Rx(hex(
541 "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",
542 )),
543 Step::Tx(hex("AA 07 00 00 08 01 05 00 00")),
544 Step::Rx(hex(
545 "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",
546 )),
547 Step::Tx(hex("AA 09 00 00 0C 01 06 02 00 02 01")),
548 Step::Rx(hex("AA 0B 00 00 0C 81 06 04 00 00 02 01 0A")),
549 ]);
550 let touch = open(&script).unwrap().touch_controls().unwrap();
551 script.assert_done();
552 let table: Vec<(&str, &str, &str)> = touch
553 .gestures
554 .iter()
555 .map(|g| (g.side.as_str(), g.gesture.as_str(), g.action.as_str()))
556 .collect();
557 assert_eq!(
558 table,
559 [
560 ("left", "double-tap", "play-pause"),
561 ("left", "triple-tap", "next"),
562 ("left", "hold", "noise-cycle"),
563 ("right", "double-tap", "play-pause"),
564 ("right", "triple-tap", "next"),
565 ("right", "hold", "noise-cycle"),
566 ]
567 );
568 assert_eq!(touch.hold_cycle.unwrap(), ["transparency", "anc"]);
569 assert!(touch.warnings.is_empty());
570 }
571
572 #[test]
573 fn sets_noise_control_and_reads_it_back() {
574 let mut steps = handshake();
575 steps.extend([
576 tx(0x0404, 0x05, "01 01 02"),
577 rx(0x8404, 0x05, "00"),
578 // The buds announce the change; it needs no answer.
579 rx(0x0204, 0x10, "03 01 01 02"),
580 tx(0x010C, 0x06, "01 01"),
581 rx(0x810C, 0x06, "00 01 01 02"),
582 ]);
583 let script = Script::new(steps);
584 let now = open(&script)
585 .unwrap()
586 .set_noise_control(NoiseMode::Transparency)
587 .unwrap();
588 script.assert_done();
589 assert_eq!((now.mode.as_str(), now.level), ("transparency", None));
590 }
591
592 #[test]
593 fn reads_back_again_until_the_change_shows() {
594 let mut steps = handshake();
595 steps.extend([
596 tx(0x0404, 0x05, "01 01 01"),
597 rx(0x8404, 0x05, "00"),
598 tx(0x010C, 0x06, "01 01"),
599 rx(0x810C, 0x06, "00 01 01 08"),
600 tx(0x010C, 0x07, "01 01"),
601 rx(0x810C, 0x07, "00 01 01 01"),
602 ]);
603 let script = Script::new(steps);
604 let now = open(&script).unwrap().set_noise_control(NoiseMode::Off).unwrap();
605 script.assert_done();
606 assert_eq!(now.mode, "off");
607 }
608
609 #[test]
610 fn sets_a_noise_cancelling_level() {
611 let mut steps = handshake();
612 steps.extend([
613 tx(0x0404, 0x05, "01 01 04"),
614 rx(0x8404, 0x05, "00"),
615 tx(0x010C, 0x06, "01 01"),
616 rx(0x810C, 0x06, "00 01 01 04"),
617 ]);
618 let script = Script::new(steps);
619 let now = open(&script)
620 .unwrap()
621 .set_noise_control(NoiseMode::Anc(AncLevel::Light))
622 .unwrap();
623 script.assert_done();
624 assert_eq!((now.mode.as_str(), now.level.as_deref()), ("anc", Some("light")));
625 }
626
627 #[test]
628 fn a_rejected_noise_control_change_is_not_read_back() {
629 let mut steps = handshake();
630 steps.extend([tx(0x0404, 0x05, "01 01 08"), rx(0x8404, 0x05, "0E")]);
631 let script = Script::new(steps);
632 let result = open(&script).unwrap().set_noise_control(NoiseMode::Anc(AncLevel::Deep));
633 script.assert_done();
634 assert!(matches!(result, Err(Error::Rejected { status: 0x0E, .. })));
635 }
636
637 #[test]
638 fn noise_control_is_not_written_without_its_bitmap_bit() {
639 let script = Script::new(vec![
640 tx(0x0100, 0x01, ""),
641 rx(0x8100, 0x01, "00"),
642 tx(0x0103, 0x02, ""),
643 rx(0x8103, 0x02, "00 12 3C 06"),
644 tx(0x0200, 0x03, ""),
645 rx(0x8200, 0x03, "00 00"),
646 ]);
647 let result = open(&script).unwrap().set_noise_control(NoiseMode::Off);
648 script.assert_done();
649 assert!(matches!(result, Err(Error::Unsupported(_))));
650 }
651
652 #[test]
653 fn sets_game_mode_and_reads_it_back() {
654 let mut steps = handshake();
655 steps.extend([
656 tx(0x0403, 0x05, "06 01"),
657 rx(0x8403, 0x05, "00"),
658 // Still the old value: budctl reads again.
659 tx(0x010D, 0x06, "06 05 06 11 1A 1B 1D"),
660 rx(0x810D, 0x06, "00 06 05 00 06 00 11 00 1A 01 1B 00 1D 00"),
661 tx(0x010D, 0x07, "06 05 06 11 1A 1B 1D"),
662 rx(0x810D, 0x07, "00 06 05 00 06 01 11 00 1A 01 1B 00 1D 00"),
663 ]);
664 let script = Script::new(steps);
665 let game = open(&script).unwrap().set_switch("game", true).unwrap();
666 script.assert_done();
667 assert_eq!((game.key, game.on), ("game", true));
668 }
669
670 #[test]
671 fn switches_not_yet_verified_are_never_written() {
672 // Nothing is sent after the handshake, so the subscribe ack is left out.
673 let mut steps = handshake();
674 steps.pop();
675 let script = Script::new(steps);
676 let mut session = open(&script).unwrap();
677 for key in ["dual", "wind", "spatial", "bass-boost"] {
678 assert!(
679 matches!(session.set_switch(key, true), Err(Error::Unsupported(_))),
680 "{key}"
681 );
682 }
683 assert!(matches!(session.set_switch("turbo", true), Err(Error::Unsupported(_))));
684 script.assert_done();
685 }
686
687 #[test]
688 fn a_switch_the_buds_lack_is_rejected() {
689 let mut steps = handshake();
690 steps.extend([tx(0x0403, 0x05, "06 00"), rx(0x8403, 0x05, "01")]);
691 let script = Script::new(steps);
692 let result = open(&script).unwrap().set_switch("game", false);
693 script.assert_done();
694 assert!(matches!(result, Err(Error::Rejected { status: 0x01, .. })));
695 }
696
697 #[test]
698 fn selects_an_eq_preset_and_reads_it_back() {
699 let mut steps = handshake();
700 steps.extend([
701 tx(0x0406, 0x05, "01"),
702 rx(0x8406, 0x05, "00"),
703 tx(0x010F, 0x06, ""),
704 rx(0x810F, 0x06, "00 01"),
705 ]);
706 let script = Script::new(steps);
707 let now = open(&script).unwrap().set_eq("deep-bass").unwrap();
708 script.assert_done();
709 assert_eq!(now, "deep-bass");
710 }
711
712 #[test]
713 fn an_unknown_eq_preset_is_a_usage_error() {
714 let mut steps = handshake();
715 steps.pop();
716 let script = Script::new(steps);
717 let result = open(&script).unwrap().set_eq("loudness");
718 script.assert_done();
719 match result {
720 Err(Error::Usage(message)) => assert!(message.contains("pure-raw, deep-bass, pure-vocals, pure-bass")),
721 other => panic!("expected a usage error, got {other:?}"),
722 }
723 }
724}