Krolyxon
Commit 7a29fd2

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310codec.rs

666 lines · 24.2 KB · Rust

1//! realme Buds T310 commands, tables and pure decoders. No I/O here, so every function can be
2//! tested with bytes from the spec. Each constant cites docs/protocol/realme_t310.md.
3
4use crate::error::{Error, Result};
5use crate::reader::Reader;
6use crate::status::{AncLevel, Battery, Charge, Gesture, NoiseControl, NoiseMode, Switch};
7
8/// Spec: realme_t310.md#bitmap
9pub(super) const HANDSHAKE: u16 = 0x0100;
10/// Spec: realme_t310.md#identity
11pub(super) const PRODUCT_ID: u16 = 0x0103;
12/// Spec: realme_t310.md#firmware
13pub(super) const FIRMWARE: u16 = 0x0105;
14/// Spec: realme_t310.md#battery
15pub(super) const BATTERY: u16 = 0x0106;
16/// Spec: realme_t310.md#gestures
17pub(super) const GESTURES: u16 = 0x0108;
18/// Spec: realme_t310.md#anc-read
19pub(super) const NOISE_CONTROL: u16 = 0x010C;
20/// Spec: realme_t310.md#switches
21pub(super) const SWITCHES: u16 = 0x010D;
22/// Spec: realme_t310.md#anc-write
23pub(super) const NOISE_CONTROL_WRITE: u16 = 0x0404;
24/// Spec: realme_t310.md#switch-write
25pub(super) const SWITCH_WRITE: u16 = 0x0403;
26/// Spec: realme_t310.md#eq-write
27pub(super) const EQ_SELECT: u16 = 0x0406;
28/// Spec: realme_t310.md#find
29pub(super) const FIND: u16 = 0x0400;
30
31/// `01` starts ringing, `00` stops it. Spec: realme_t310.md#find
32pub(super) fn find_payload(ring: bool) -> [u8; 1] {
33 [u8::from(ring)]
34}
35/// Spec: realme_t310.md#eq-read
36pub(super) const EQ_PRESET: u16 = 0x010F;
37/// Spec: realme_t310.md#codec
38pub(super) const CODEC: u16 = 0x0114;
39/// Not sent: the T310 does not answer it (HW1). Kept for W6. Spec: realme_t310.md#bass-read
40#[allow(dead_code)]
41pub(super) const BASS_LEVEL: u16 = 0x0124;
42/// Spec: realme_t310.md#events-offered
43pub(super) const EVENTS_OFFERED: u16 = 0x0200;
44/// Spec: realme_t310.md#subscribe
45pub(super) const SUBSCRIBE: u16 = 0x0205;
46/// Spec: realme_t310.md#fit-test
47pub(super) const FIT_TEST: u16 = 0x0405;
48
49/// Spec: realme_t310.md#identity
50pub(super) const T310_PRODUCT_ID: u32 = 0x06_3C_12;
51
52/// Payload asking for the current noise-control mode. Spec: realme_t310.md#anc-read
53pub(super) const NOISE_MODE_QUERY: [u8; 2] = [0x01, 0x01];
54/// Payload asking for the hold's noise-control cycle (one setting for both buds).
55/// Spec: realme_t310.md#hold-cycle
56pub(super) const HOLD_CYCLE_QUERY: [u8; 2] = [0x02, 0x01];
57/// Count, then the T310's switch ids: `05 06 11 1A 1B 1D`. Spec: realme_t310.md#switches
58pub(super) const SWITCH_QUERY: [u8; 7] = [6, 0x05, 0x06, 0x11, 0x1A, 0x1B, 0x1D];
59
60/// Event codes to subscribe to; the optional ones only when their command is allowed.
61/// Spec: realme_t310.md#subscribe
62const EVENTS: [(u8, Option<u16>); 4] = [(0x01, None), (0x02, None), (0x03, None), (0x04, Some(FIT_TEST))];
63
64/// Spec: realme_t310.md#switches
65const SWITCH_NAMES: [(u8, &str, &str); 5] = [
66 (0x06, "game", "Game mode"),
67 (0x11, "dual", "Dual connection"),
68 (0x1A, "wind", "Wind-noise reduction"),
69 (0x1B, "spatial", "3D sound"),
70 (0x1D, "bass-boost", "Bass boost"),
71];
72
73/// Noise-control masks: mode and level. Spec: realme_t310.md#anc-read
74const NOISE_MODES: [(u32, &str, Option<&str>); 5] = [
75 (0x01, "off", None),
76 (0x02, "transparency", None),
77 (0x04, "anc", Some("light")),
78 (0x08, "anc", Some("deep")),
79 (0x10, "anc", Some("medium")),
80];
81
82/// Bits of the hold cycle. Spec: realme_t310.md#hold-cycle
83const CYCLE_MODES: [(u32, &str); 3] = [(0x01, "off"), (0x02, "transparency"), (0x08, "anc")];
84
85/// Built-in EQ presets by id. Spec: realme_t310.md#eq-read
86const EQ_PRESETS: [(u8, &str); 4] = [
87 (0x00, "pure-raw"),
88 (0x01, "deep-bass"),
89 (0x02, "pure-vocals"),
90 (0x03, "pure-bass"),
91];
92
93/// Spec: realme_t310.md#codec
94const CODECS: [(u8, &str); 8] = [
95 (0x01, "SBC"),
96 (0x02, "AAC"),
97 (0x03, "LDAC"),
98 (0x04, "aptX"),
99 (0x05, "aptX HD"),
100 (0x06, "aptX Adaptive"),
101 (0x07, "LHDC"),
102 (0x08, "LHDC V5"),
103];
104
105/// Gesture-table names. Spec: realme_t310.md#gestures
106const SIDES: [(u8, &str); 3] = [(0x01, "left"), (0x02, "right"), (0x04, "both")];
107/// Spec: realme_t310.md#gestures
108const MAIN_GROUP: u8 = 0x01;
109/// Spec: realme_t310.md#gestures
110const ACTS: [(u8, &str); 6] = [
111 (0x01, "single-tap"),
112 (0x02, "double-tap"),
113 (0x03, "triple-tap"),
114 (0x04, "hold"),
115 (0x05, "slide"),
116 (0x06, "long-hold"),
117];
118/// Spec: realme_t310.md#gestures
119const FUNCTIONS: [(u8, &str); 15] = [
120 (0x00, "none"),
121 (0x01, "play-pause"),
122 (0x03, "voice-assistant"),
123 (0x05, "previous"),
124 (0x06, "next"),
125 (0x07, "volume"),
126 (0x08, "noise-cycle"),
127 (0x09, "favourite-music"),
128 (0x0A, "switch-track"),
129 (0x0B, "volume-up"),
130 (0x0C, "volume-down"),
131 (0x0D, "switch-devices"),
132 (0x11, "game-mode"),
133 (0x1C, "decline-call"),
134 (0x1D, "answer-call"),
135];
136
137fn name_of<K: PartialEq + Copy>(table: &[(K, &'static str)], key: K) -> Option<&'static str> {
138 table.iter().find(|(k, _)| *k == key).map(|(_, name)| *name)
139}
140
141/// When a command may be sent. Spec: realme_t310.md#bitmap
142enum Gate {
143 Always,
144 Bit(u8),
145}
146
147/// The gate of every command budctl uses. An unlisted command is never sent.
148/// Spec: realme_t310.md#bitmap
149fn gate(cmd: u16) -> Option<Gate> {
150 Some(match cmd {
151 0x0100..=0x0104 | 0x0106 | 0x010B | 0x010D | 0x0200 => Gate::Always,
152 0x0105 => Gate::Bit(0),
153 0x0108 | 0x0401 => Gate::Bit(3),
154 0x0109 => Gate::Bit(4),
155 0x0400 => Gate::Bit(5),
156 0x0403 => Gate::Bit(7),
157 0x010C | 0x0404 => Gate::Bit(8),
158 0x0405 => Gate::Bit(9),
159 0x010F | 0x0406 => Gate::Bit(10),
160 0x0114 => Gate::Bit(17),
161 0x0205 => Gate::Bit(22),
162 0x0122 | 0x0418 => Gate::Bit(34),
163 0x0124 | 0x041B => Gate::Bit(38),
164 _ => return None,
165 })
166}
167
168/// The commands these buds accept, from the handshake reply. Spec: realme_t310.md#bitmap
169#[derive(Debug)]
170pub(super) struct Commands {
171 bitmap: Vec<u8>,
172}
173
174impl Commands {
175 pub(super) fn parse(reply: &[u8]) -> Result<Self> {
176 let mut r = ok_reply(reply, "the handshake")?;
177 Ok(Self {
178 bitmap: r.rest().to_vec(),
179 })
180 }
181
182 pub(super) fn allows(&self, cmd: u16) -> bool {
183 match gate(cmd) {
184 Some(Gate::Always) => true,
185 Some(Gate::Bit(n)) => self
186 .bitmap
187 .get(usize::from(n / 8))
188 .is_some_and(|byte| byte & (1 << (n % 8)) != 0),
189 None => false,
190 }
191 }
192}
193
194/// A reply whose status byte is `00`, positioned after it. Spec: realme_t310.md#seq
195fn ok_reply<'a>(reply: &'a [u8], what: &'static str) -> Result<Reader<'a>> {
196 let mut r = Reader::new(reply, what);
197 match r.u8()? {
198 0x00 => Ok(r),
199 status => Err(Error::Rejected {
200 what: what.to_owned(),
201 status,
202 }),
203 }
204}
205
206/// Spec: realme_t310.md#identity
207pub(super) fn decode_product_id(reply: &[u8]) -> Result<u32> {
208 let mut r = ok_reply(reply, "the product id query")?;
209 let [low, middle, high] = [r.u8()?, r.u8()?, r.u8()?];
210 Ok(u32::from_le_bytes([low, middle, high, 0]))
211}
212
213/// Spec: realme_t310.md#events-offered
214pub(super) fn decode_events_offered(reply: &[u8]) -> Result<Vec<u8>> {
215 let mut r = ok_reply(reply, "the event list")?;
216 let count = r.u8()?;
217 Ok(r.take(usize::from(count))?.to_vec())
218}
219
220/// The subscribe payload: count, then the wanted codes the buds offer.
221/// Spec: realme_t310.md#subscribe
222pub(super) fn subscribe_payload(offered: &[u8], commands: &Commands) -> Vec<u8> {
223 let codes: Vec<u8> = EVENTS
224 .iter()
225 .filter(|(code, needs)| offered.contains(code) && needs.is_none_or(|cmd| commands.allows(cmd)))
226 .map(|(code, _)| *code)
227 .collect();
228 let mut payload = vec![u8::try_from(codes.len()).unwrap_or_default()];
229 payload.extend(codes);
230 payload
231}
232
233/// Spec: realme_t310.md#battery
234pub(super) fn decode_battery(reply: &[u8]) -> Result<Battery> {
235 let mut r = ok_reply(reply, "the battery reply")?;
236 let mut battery = Battery::default();
237 for _ in 0..r.u8()? {
238 let part = r.u8()?;
239 let raw = r.u8()?;
240 let level = raw & 0x7F;
241 if level == 0 {
242 continue; // not known
243 }
244 let charge = Some(Charge {
245 level,
246 charging: raw & 0x80 != 0,
247 });
248 match part {
249 0x01 => battery.left = charge,
250 0x02 => battery.right = charge,
251 0x03 => battery.case = charge,
252 _ => {}
253 }
254 }
255 Ok(battery)
256}
257
258/// The versions of kind 2: plain numbers are joined with dots; full dotted versions (the T310's)
259/// are shown once each, joined with ` / `. The raw text when there are none.
260/// Spec: realme_t310.md#firmware
261pub(super) fn decode_firmware(reply: &[u8]) -> Result<String> {
262 let mut r = ok_reply(reply, "the firmware reply")?;
263 let _count = r.u8()?;
264 let text = String::from_utf8_lossy(r.rest()).into_owned();
265 let fields: Vec<&str> = text.split(',').map(str::trim).collect();
266 let mut versions: Vec<&str> = fields
267 .chunks(3)
268 .filter_map(|triple| match triple {
269 [_device, "2", version] => Some(*version),
270 _ => None,
271 })
272 .collect();
273 if versions.is_empty() {
274 return Ok(text);
275 }
276 if !versions.iter().any(|v| v.contains('.')) {
277 return Ok(versions.join("."));
278 }
279 let mut seen = Vec::new();
280 versions.retain(|v| {
281 let new = !seen.contains(v);
282 seen.push(*v);
283 new
284 });
285 Ok(versions.join(" / "))
286}
287
288/// Spec: realme_t310.md#codec
289pub(super) fn decode_codec(reply: &[u8]) -> Result<String> {
290 let codec = ok_reply(reply, "the codec reply")?.u8()?;
291 Ok(name_of(&CODECS, codec).map_or_else(|| format!("unknown (0x{codec:02X})"), str::to_owned))
292}
293
294/// Spec: realme_t310.md#anc-read
295pub(super) fn decode_noise_mode(reply: &[u8]) -> Result<NoiseControl> {
296 let mask = noise_mask(reply, NOISE_MODE_QUERY, "the noise-control reply")?;
297 Ok(match NOISE_MODES.iter().find(|(m, _, _)| *m == mask) {
298 Some((_, mode, level)) => NoiseControl {
299 mode: (*mode).to_owned(),
300 level: level.map(str::to_owned),
301 },
302 None => NoiseControl {
303 mode: format!("unknown (0x{mask:02X})"),
304 level: None,
305 },
306 })
307}
308
309/// The mask that selects `mode`, as written and as read back. Spec: realme_t310.md#anc-write
310pub(super) fn noise_write_mask(mode: NoiseMode) -> u32 {
311 match mode {
312 NoiseMode::Off => 0x01,
313 NoiseMode::Transparency => 0x02,
314 NoiseMode::Anc(AncLevel::Light) => 0x04,
315 NoiseMode::Anc(AncLevel::Medium) => 0x10,
316 // Also plain "noise cancelling on".
317 NoiseMode::Anc(AncLevel::Deep) => 0x08,
318 }
319}
320
321/// `01 01 <mask>`: the noise-mode kind, then the mask in the fewest bytes (all fit in one).
322/// Spec: realme_t310.md#anc-write
323pub(super) fn noise_write_payload(mode: NoiseMode) -> Vec<u8> {
324 let mut payload = NOISE_MODE_QUERY.to_vec();
325 let mask = noise_write_mask(mode).to_le_bytes();
326 let used = mask.iter().rposition(|&b| b != 0).map_or(1, |last| last + 1);
327 payload.extend(mask.iter().take(used));
328 payload
329}
330
331/// The mask in a noise-control read reply. Spec: realme_t310.md#anc-read
332pub(super) fn decode_noise_mask(reply: &[u8]) -> Result<u32> {
333 noise_mask(reply, NOISE_MODE_QUERY, "the noise-control reply")
334}
335
336/// A write reply: `00` means accepted. Spec: realme_t310.md#writes
337pub(super) fn check_write_reply(reply: &[u8], what: &'static str) -> Result<()> {
338 ok_reply(reply, what).map(|_| ())
339}
340
341/// Spec: realme_t310.md#hold-cycle
342pub(super) fn decode_hold_cycle(reply: &[u8]) -> Result<Vec<String>> {
343 let mask = noise_mask(reply, HOLD_CYCLE_QUERY, "the hold-cycle reply")?;
344 let mut modes: Vec<String> = CYCLE_MODES
345 .iter()
346 .filter(|(bit, _)| mask & bit != 0)
347 .map(|(_, name)| (*name).to_owned())
348 .collect();
349 let known = CYCLE_MODES.iter().fold(0, |all, (bit, _)| all | bit);
350 if mask & !known != 0 {
351 modes.push(format!("unknown (0x{:02X})", mask & !known));
352 }
353 Ok(modes)
354}
355
356/// `00 <echo of the query> <mask, 1-4 bytes LE>`. Spec: realme_t310.md#anc-read
357fn noise_mask(reply: &[u8], query: [u8; 2], what: &'static str) -> Result<u32> {
358 let mut r = ok_reply(reply, what)?;
359 if [r.u8()?, r.u8()?] != query {
360 return Err(Error::Protocol(format!("{what} answers a different question")));
361 }
362 let bytes = r.rest();
363 if bytes.is_empty() || bytes.len() > 4 {
364 return Err(Error::Protocol(format!("{what} has a {} byte value", bytes.len())));
365 }
366 Ok(bytes.iter().rev().fold(0, |mask, &b| (mask << 8) | u32::from(b)))
367}
368
369/// Spec: realme_t310.md#switches
370pub(super) fn decode_switches(reply: &[u8]) -> Result<Vec<Switch>> {
371 let mut r = ok_reply(reply, "the switch reply")?;
372 let mut switches = Vec::new();
373 for _ in 0..r.u8()? {
374 let id = r.u8()?;
375 let value = r.u8()?;
376 if let Some((_, key, label)) = SWITCH_NAMES.iter().find(|(known, _, _)| *known == id) {
377 switches.push(Switch {
378 key,
379 label,
380 on: value == 0x01,
381 });
382 }
383 }
384 Ok(switches)
385}
386
387/// The id of a switch, by its key (`game`…). Spec: realme_t310.md#switches
388pub(super) fn switch_id(key: &str) -> Option<u8> {
389 SWITCH_NAMES.iter().find(|(_, k, _)| *k == key).map(|(id, _, _)| *id)
390}
391
392/// `<id> <01|00>`. Spec: realme_t310.md#switch-write
393pub(super) fn switch_write_payload(id: u8, on: bool) -> [u8; 2] {
394 [id, u8::from(on)]
395}
396
397/// The id of a built-in EQ preset, by name. Spec: realme_t310.md#eq-read
398pub(super) fn eq_id(name: &str) -> Option<u8> {
399 EQ_PRESETS.iter().find(|(_, n)| *n == name).map(|(id, _)| *id)
400}
401
402/// The built-in EQ presets' names, in id order. Spec: realme_t310.md#eq-read
403pub(super) fn eq_names() -> Vec<&'static str> {
404 EQ_PRESETS.iter().map(|(_, name)| *name).collect()
405}
406
407/// Spec: realme_t310.md#eq-read
408pub(super) fn decode_eq(reply: &[u8]) -> Result<String> {
409 let id = ok_reply(reply, "the EQ reply")?.u8()?;
410 Ok(name_of(&EQ_PRESETS, id).map_or_else(|| format!("custom-{id}"), str::to_owned))
411}
412
413/// `00 <min> <max> <level>`, all signed. Not used until W6. Spec: realme_t310.md#bass-read
414#[allow(dead_code)]
415pub(super) fn decode_bass_level(reply: &[u8]) -> Result<i8> {
416 let mut r = ok_reply(reply, "the bass-level reply")?;
417 let (min, max, level) = (r.i8()?, r.i8()?, r.i8()?);
418 if !(min..=max).contains(&level) {
419 return Err(Error::Protocol(format!("bass level {level} outside {min}..={max}")));
420 }
421 Ok(level)
422}
423
424/// Spec: realme_t310.md#gestures
425pub(super) fn decode_gestures(reply: &[u8]) -> Result<Vec<Gesture>> {
426 let mut r = ok_reply(reply, "the gesture table")?;
427 let mut gestures = Vec::new();
428 for _ in 0..r.u8()? {
429 let [side, group, act, function] = [r.u8()?, r.u8()?, r.u8()?, r.u8()?];
430 gestures.push(Gesture {
431 side: name_of(&SIDES, side).map_or_else(|| format!("device-0x{side:02X}"), str::to_owned),
432 gesture: name_of(&ACTS, act).map_or_else(|| format!("act-0x{act:02X}"), str::to_owned),
433 action: name_of(&FUNCTIONS, function).map_or_else(|| format!("fn-0x{function:02X}"), str::to_owned),
434 group: (group != MAIN_GROUP).then_some(group),
435 });
436 }
437 Ok(gestures)
438}
439
440/// The handshake reply of the owner's T310 (HW1). Spec: realme_t310.md#bitmap-t310
441#[cfg(test)]
442pub(super) const T310_BITMAP_REPLY: &str = "00 BF 1F 5A 20 45 04 40 00";
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447 use crate::transport::mock::hex;
448
449 #[test]
450 fn t310_bitmap_allows_its_commands_and_nothing_else() {
451 let commands = Commands::parse(&hex(T310_BITMAP_REPLY)).unwrap();
452 // Every gated command in the T310's logged command set (realme_t310.md#bitmap-t310).
453 for cmd in [
454 0x0100, 0x0103, 0x0105, 0x0106, 0x0108, 0x0109, 0x010C, 0x010D, 0x010F, 0x0114, 0x0122, 0x0124, 0x0205,
455 0x0400, 0x0401, 0x0403, 0x0404, 0x0405, 0x0406, 0x0418, 0x041B, 0x0200,
456 ] {
457 assert!(commands.allows(cmd), "0x{cmd:04X} should be allowed");
458 }
459 // Commands the T310 lacks, and commands budctl has no gate for.
460 for cmd in [0x012F, 0x041E, 0x0413, 0x0130, 0x0427, 0x0423, 0x012A, 0x0422, 0x9999] {
461 assert!(!commands.allows(cmd), "0x{cmd:04X} should be refused");
462 }
463 }
464
465 #[test]
466 fn a_short_bitmap_allows_only_the_always_commands() {
467 let commands = Commands::parse(&hex("00")).unwrap();
468 assert!(commands.allows(BATTERY) && commands.allows(SWITCHES) && commands.allows(HANDSHAKE));
469 assert!(!commands.allows(FIRMWARE) && !commands.allows(NOISE_CONTROL));
470 assert!(matches!(
471 Commands::parse(&hex("01 FF")),
472 Err(Error::Rejected { status: 1, .. })
473 ));
474 }
475
476 #[test]
477 fn product_id_is_three_bytes_little_endian() {
478 assert_eq!(decode_product_id(&hex("00 12 3C 06")).unwrap(), T310_PRODUCT_ID);
479 assert_eq!(decode_product_id(&hex("00 14 54 06")).unwrap(), 0x06_54_14);
480 assert!(decode_product_id(&hex("00 12 3C")).is_err());
481 }
482
483 #[test]
484 fn subscribes_to_offered_events_only() {
485 let commands = Commands::parse(&hex(T310_BITMAP_REPLY)).unwrap();
486 let buds4_offer = hex("00 09 01 02 03 04 08 0B F1 F2 F3");
487 let offered = decode_events_offered(&buds4_offer).unwrap();
488 assert_eq!(subscribe_payload(&offered, &commands), hex("04 01 02 03 04"));
489 assert_eq!(subscribe_payload(&[0x02, 0x01], &commands), hex("02 01 02"));
490 let no_fit_test = Commands::parse(&hex("00 BD 0D")).unwrap();
491 assert_eq!(subscribe_payload(&offered, &no_fit_test), hex("03 01 02 03"));
492 }
493
494 #[test]
495 fn battery_levels_charging_and_unknown_parts() {
496 let battery = decode_battery(&hex("00 03 01 64 02 E4 03 00")).unwrap();
497 assert_eq!(
498 battery.left,
499 Some(Charge {
500 level: 100,
501 charging: false
502 })
503 );
504 assert_eq!(
505 battery.right,
506 Some(Charge {
507 level: 100,
508 charging: true
509 })
510 );
511 assert_eq!(battery.case, None);
512 assert_eq!(decode_battery(&hex("00 00")).unwrap(), Battery::default());
513 assert!(decode_battery(&hex("00 03 01 64")).is_err());
514 }
515
516 #[test]
517 fn firmware_shows_kind_two_versions() {
518 let mut reply = hex("00 04");
519 reply.extend(b"1,2,138,2,2,138,3,1,01,3,2,105");
520 assert_eq!(decode_firmware(&reply).unwrap(), "138.138.105");
521 // The owner's T310 (HW1): both buds report the same full version.
522 let mut t310 = hex("00 02");
523 t310.extend(b"1,2,1.1.0.43,2,2,1.1.0.43");
524 assert_eq!(decode_firmware(&t310).unwrap(), "1.1.0.43");
525 let mut differ = hex("00 02");
526 differ.extend(b"1,2,1.1.0.43,2,2,1.1.0.44");
527 assert_eq!(decode_firmware(&differ).unwrap(), "1.1.0.43 / 1.1.0.44");
528 let mut odd = hex("00 01");
529 odd.extend(b"V1.0.2");
530 assert_eq!(decode_firmware(&odd).unwrap(), "V1.0.2");
531 }
532
533 #[test]
534 fn noise_control_modes() {
535 let mode = |reply: &str| decode_noise_mode(&hex(reply)).unwrap();
536 assert_eq!(mode("00 01 01 01").mode, "off");
537 assert_eq!(mode("00 01 01 02 00").mode, "transparency");
538 let deep = mode("00 01 01 08");
539 assert_eq!((deep.mode.as_str(), deep.level.as_deref()), ("anc", Some("deep")));
540 assert_eq!(mode("00 01 01 10").level.as_deref(), Some("medium"));
541 assert_eq!(mode("00 01 01 00 08").mode, "unknown (0x800)");
542 assert!(decode_noise_mode(&hex("00 02 01 08")).is_err());
543 assert!(decode_noise_mode(&hex("00 01 01")).is_err());
544 assert!(matches!(
545 decode_noise_mode(&hex("0E 01 01 08")),
546 Err(Error::Rejected { status: 0x0E, .. })
547 ));
548 }
549
550 #[test]
551 fn noise_control_write_payloads() {
552 assert_eq!(noise_write_payload(NoiseMode::Off), hex("01 01 01"));
553 assert_eq!(noise_write_payload(NoiseMode::Transparency), hex("01 01 02"));
554 assert_eq!(noise_write_payload(NoiseMode::Anc(AncLevel::Light)), hex("01 01 04"));
555 assert_eq!(noise_write_payload(NoiseMode::Anc(AncLevel::Medium)), hex("01 01 10"));
556 assert_eq!(noise_write_payload(NoiseMode::Anc(AncLevel::Deep)), hex("01 01 08"));
557 // What is written is what a read reports: every write mask decodes to its own mode.
558 for level in [AncLevel::Light, AncLevel::Medium, AncLevel::Deep] {
559 let mut reply = hex("00 01 01");
560 reply.extend(noise_write_payload(NoiseMode::Anc(level)).iter().skip(2));
561 let read = decode_noise_mode(&reply).unwrap();
562 assert_eq!((read.mode.as_str(), read.level.as_deref()), ("anc", Some(level.name())));
563 }
564 // The family's documented frame for "off" (realme_t310.md#frame-whole).
565 let frame =
566 crate::devices::realme_t310::frame::encode(NOISE_CONTROL_WRITE, 0x06, &noise_write_payload(NoiseMode::Off))
567 .unwrap();
568 assert_eq!(frame, hex("AA 0A 00 00 04 04 06 03 00 01 01 01"));
569 }
570
571 #[test]
572 fn switch_writes() {
573 assert_eq!(switch_id("game"), Some(0x06));
574 assert_eq!(switch_id("dual"), Some(0x11));
575 assert_eq!(switch_id("turbo"), None);
576 assert_eq!(switch_write_payload(0x06, true), [0x06, 0x01]);
577 assert_eq!(switch_write_payload(0x06, false), [0x06, 0x00]);
578 }
579
580 #[test]
581 fn eq_presets_by_name() {
582 assert_eq!(eq_id("pure-raw"), Some(0x00));
583 assert_eq!(eq_id("pure-bass"), Some(0x03));
584 assert_eq!(eq_id("custom-7"), None);
585 assert_eq!(eq_names(), ["pure-raw", "deep-bass", "pure-vocals", "pure-bass"]);
586 // Every preset reads back under the name it was selected by.
587 for name in eq_names() {
588 let reply = [0x00, eq_id(name).unwrap()];
589 assert_eq!(decode_eq(&reply).unwrap(), name);
590 }
591 }
592
593 #[test]
594 fn find_payloads() {
595 assert_eq!(find_payload(true), [0x01]);
596 assert_eq!(find_payload(false), [0x00]);
597 }
598
599 #[test]
600 fn write_replies() {
601 assert!(check_write_reply(&hex("00"), "the write").is_ok());
602 assert!(matches!(
603 check_write_reply(&hex("0E"), "the write"),
604 Err(Error::Rejected { status: 0x0E, .. })
605 ));
606 assert!(check_write_reply(&[], "the write").is_err());
607 }
608
609 #[test]
610 fn hold_cycle_lists_modes() {
611 assert_eq!(decode_hold_cycle(&hex("00 02 01 0A")).unwrap(), ["transparency", "anc"]);
612 assert_eq!(
613 decode_hold_cycle(&hex("00 02 01 0B 00")).unwrap(),
614 ["off", "transparency", "anc"]
615 );
616 assert_eq!(decode_hold_cycle(&hex("00 02 01 14")).unwrap(), ["unknown (0x14)"]);
617 }
618
619 #[test]
620 fn switches_from_report_22_values() {
621 let switches = decode_switches(&hex("00 06 05 00 06 00 11 01 1A 00 1B 01 1D 01")).unwrap();
622 let on: Vec<(&str, bool)> = switches.iter().map(|s| (s.key, s.on)).collect();
623 assert_eq!(
624 on,
625 [
626 ("game", false),
627 ("dual", true),
628 ("wind", false),
629 ("spatial", true),
630 ("bass-boost", true)
631 ]
632 );
633 assert!(decode_switches(&hex("00 02 06 01")).is_err());
634 }
635
636 #[test]
637 fn eq_codec_and_bass_level() {
638 assert_eq!(decode_eq(&hex("00 00")).unwrap(), "pure-raw");
639 assert_eq!(decode_eq(&hex("00 03")).unwrap(), "pure-bass");
640 assert_eq!(decode_eq(&hex("00 05")).unwrap(), "custom-5");
641 assert_eq!(decode_codec(&hex("00 02")).unwrap(), "AAC");
642 assert_eq!(decode_codec(&hex("00 09")).unwrap(), "unknown (0x09)");
643 assert_eq!(decode_bass_level(&hex("00 FB 05 FE")).unwrap(), -2);
644 assert!(decode_bass_level(&hex("00 FB 05 07")).is_err());
645 }
646
647 #[test]
648 fn gesture_table_names_known_values() {
649 let gestures = decode_gestures(&hex("00 03 01 01 02 01 02 01 04 08 04 06 02 1D")).unwrap();
650 let rows: Vec<(&str, &str, &str, Option<u8>)> = gestures
651 .iter()
652 .map(|g| (g.side.as_str(), g.gesture.as_str(), g.action.as_str(), g.group))
653 .collect();
654 assert_eq!(
655 rows,
656 [
657 ("left", "double-tap", "play-pause", None),
658 ("right", "hold", "noise-cycle", None),
659 ("both", "double-tap", "answer-call", Some(6)),
660 ]
661 );
662 let odd = decode_gestures(&hex("00 01 09 01 07 42")).unwrap();
663 assert_eq!(odd.first().map(|g| g.action.as_str()), Some("fn-0x42"));
664 assert!(decode_gestures(&hex("00 02 01 01 02 01")).is_err());
665 }
666}