Krolyxon
Commit dd7c4d8

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310mod.rs

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