Krolyxon
Commit 5e24422

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310codec.rs

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