Krolyxon
Commit 8ff50e2

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310mod.rs

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