Krolyxon
Commit 7a29fd2

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310mod.rs

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