Krolyxon
Commit 83c5894

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310codec.rs

905 lines · 34.8 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 payload.extend(fewest_bytes(noise_write_mask(mode)));
328 payload
329}
330
331/// A mask in the fewest little-endian bytes that hold it, at least one.
332/// Spec: realme_t310.md#anc-write, #hold-cycle-write
333fn fewest_bytes(mask: u32) -> Vec<u8> {
334 let bytes = mask.to_le_bytes();
335 let used = bytes.iter().rposition(|&b| b != 0).map_or(1, |last| last + 1);
336 bytes.iter().take(used).copied().collect()
337}
338
339/// The mask in a noise-control read reply. Spec: realme_t310.md#anc-read
340pub(super) fn decode_noise_mask(reply: &[u8]) -> Result<u32> {
341 noise_mask(reply, NOISE_MODE_QUERY, "the noise-control reply")
342}
343
344/// A write reply: `00` means accepted. Spec: realme_t310.md#writes
345pub(super) fn check_write_reply(reply: &[u8], what: &'static str) -> Result<()> {
346 ok_reply(reply, what).map(|_| ())
347}
348
349/// Spec: realme_t310.md#hold-cycle
350pub(super) fn decode_hold_cycle(reply: &[u8]) -> Result<Vec<String>> {
351 let mask = decode_hold_cycle_mask(reply)?;
352 let mut modes: Vec<String> = CYCLE_MODES
353 .iter()
354 .filter(|(bit, _)| mask & bit != 0)
355 .map(|(_, name)| (*name).to_owned())
356 .collect();
357 let known = CYCLE_MODES.iter().fold(0, |all, (bit, _)| all | bit);
358 if mask & !known != 0 {
359 modes.push(format!("unknown (0x{:02X})", mask & !known));
360 }
361 Ok(modes)
362}
363
364/// The mask for a hold cycle through `modes` (`off`, `transparency`, `anc`): at least two,
365/// none twice. The buds keep their own order, so the order given does not matter.
366/// Spec: realme_t310.md#hold-cycle-write
367pub(super) fn hold_cycle_mask(modes: &[&str]) -> Result<u32> {
368 let mut mask = 0;
369 for &mode in modes {
370 let Some(&(bit, _)) = CYCLE_MODES.iter().find(|(_, name)| *name == mode) else {
371 let names: Vec<&str> = CYCLE_MODES.iter().map(|(_, name)| *name).collect();
372 return Err(Error::Usage(format!(
373 "no mode called {mode} in the hold cycle; use: {}",
374 names.join(", ")
375 )));
376 };
377 if mask & bit != 0 {
378 return Err(Error::Usage(format!("{mode} is in the hold cycle twice")));
379 }
380 mask |= bit;
381 }
382 if mask.count_ones() < 2 {
383 return Err(Error::Usage("the hold cycle needs at least two modes".to_owned()));
384 }
385 Ok(mask)
386}
387
388/// `02 01 <mask>`, the mask in the fewest bytes. Spec: realme_t310.md#hold-cycle-write
389pub(super) fn hold_cycle_write_payload(mask: u32) -> Vec<u8> {
390 let mut payload = HOLD_CYCLE_QUERY.to_vec();
391 payload.extend(fewest_bytes(mask));
392 payload
393}
394
395/// The mask in a hold-cycle read reply. Spec: realme_t310.md#hold-cycle
396pub(super) fn decode_hold_cycle_mask(reply: &[u8]) -> Result<u32> {
397 noise_mask(reply, HOLD_CYCLE_QUERY, "the hold-cycle reply")
398}
399
400/// `00 <echo of the query> <mask, 1-4 bytes LE>`. Spec: realme_t310.md#anc-read
401fn noise_mask(reply: &[u8], query: [u8; 2], what: &'static str) -> Result<u32> {
402 let mut r = ok_reply(reply, what)?;
403 if [r.u8()?, r.u8()?] != query {
404 return Err(Error::Protocol(format!("{what} answers a different question")));
405 }
406 let bytes = r.rest();
407 if bytes.is_empty() || bytes.len() > 4 {
408 return Err(Error::Protocol(format!("{what} has a {} byte value", bytes.len())));
409 }
410 Ok(bytes.iter().rev().fold(0, |mask, &b| (mask << 8) | u32::from(b)))
411}
412
413/// Spec: realme_t310.md#switches
414pub(super) fn decode_switches(reply: &[u8]) -> Result<Vec<Switch>> {
415 let mut r = ok_reply(reply, "the switch reply")?;
416 let mut switches = Vec::new();
417 for _ in 0..r.u8()? {
418 let id = r.u8()?;
419 let value = r.u8()?;
420 if let Some((_, key, label)) = SWITCH_NAMES.iter().find(|(known, _, _)| *known == id) {
421 switches.push(Switch {
422 key,
423 label,
424 on: value == 0x01,
425 });
426 }
427 }
428 Ok(switches)
429}
430
431/// The id of a switch, by its key (`game`…). Spec: realme_t310.md#switches
432pub(super) fn switch_id(key: &str) -> Option<u8> {
433 SWITCH_NAMES.iter().find(|(_, k, _)| *k == key).map(|(id, _, _)| *id)
434}
435
436/// `<id> <01|00>`. Spec: realme_t310.md#switch-write
437pub(super) fn switch_write_payload(id: u8, on: bool) -> [u8; 2] {
438 [id, u8::from(on)]
439}
440
441/// The id of a built-in EQ preset, by name. Spec: realme_t310.md#eq-read
442pub(super) fn eq_id(name: &str) -> Option<u8> {
443 EQ_PRESETS.iter().find(|(_, n)| *n == name).map(|(id, _)| *id)
444}
445
446/// The built-in EQ presets' names, in id order. Spec: realme_t310.md#eq-read
447pub(super) fn eq_names() -> Vec<&'static str> {
448 EQ_PRESETS.iter().map(|(_, name)| *name).collect()
449}
450
451/// Spec: realme_t310.md#eq-read
452pub(super) fn decode_eq(reply: &[u8]) -> Result<String> {
453 let id = ok_reply(reply, "the EQ reply")?.u8()?;
454 Ok(name_of(&EQ_PRESETS, id).map_or_else(|| format!("custom-{id}"), str::to_owned))
455}
456
457/// `00 <min> <max> <level>`, all signed. Not used until W6. Spec: realme_t310.md#bass-read
458#[allow(dead_code)]
459pub(super) fn decode_bass_level(reply: &[u8]) -> Result<i8> {
460 let mut r = ok_reply(reply, "the bass-level reply")?;
461 let (min, max, level) = (r.i8()?, r.i8()?, r.i8()?);
462 if !(min..=max).contains(&level) {
463 return Err(Error::Protocol(format!("bass level {level} outside {min}..={max}")));
464 }
465 Ok(level)
466}
467
468/// One gesture-table entry: `<dev> <btn> <act> <fn>`. Spec: realme_t310.md#gestures
469pub(super) type GestureEntry = [u8; 4];
470
471/// The entries of a gesture-table reply, in the order the buds sent them.
472/// Spec: realme_t310.md#gestures
473pub(super) fn decode_gesture_entries(reply: &[u8]) -> Result<Vec<GestureEntry>> {
474 let mut r = ok_reply(reply, "the gesture table")?;
475 let mut entries = Vec::new();
476 for _ in 0..r.u8()? {
477 entries.push([r.u8()?, r.u8()?, r.u8()?, r.u8()?]);
478 }
479 Ok(entries)
480}
481
482/// Spec: realme_t310.md#gestures
483pub(super) fn name_gesture(&[side, group, act, function]: &GestureEntry) -> Gesture {
484 Gesture {
485 side: name_of(&SIDES, side).map_or_else(|| format!("device-0x{side:02X}"), str::to_owned),
486 gesture: name_of(&ACTS, act).map_or_else(|| format!("act-0x{act:02X}"), str::to_owned),
487 action: name_of(&FUNCTIONS, function).map_or_else(|| format!("fn-0x{function:02X}"), str::to_owned),
488 group: (group != MAIN_GROUP).then_some(group),
489 }
490}
491
492/// Spec: realme_t310.md#gestures
493pub(super) fn decode_gestures(reply: &[u8]) -> Result<Vec<Gesture>> {
494 Ok(decode_gesture_entries(reply)?.iter().map(name_gesture).collect())
495}
496
497/// What `touch <side>` changes: `both` is the left and the right bud's entry.
498/// Spec: realme_t310.md#gesture-choices
499const TOUCH_SIDES: [(&str, &[u8]); 3] = [("left", &[0x01]), ("right", &[0x02]), ("both", &[0x01, 0x02])];
500
501/// The actions realme Link offers per gesture on the T310, in its order. budctl offers no
502/// others. Spec: realme_t310.md#gesture-choices
503const TOUCH_CHOICES: [(u8, &[u8]); 3] = [
504 // double tap: none, play/pause, next, voice assistant
505 (0x02, &[0x00, 0x01, 0x06, 0x03]),
506 // triple tap: none, previous, next, voice assistant, volume up, volume down, game mode
507 (0x03, &[0x00, 0x05, 0x06, 0x03, 0x0B, 0x0C, 0x11]),
508 // hold: none, noise-control cycle, voice assistant, game mode
509 (0x04, &[0x00, 0x08, 0x03, 0x11]),
510];
511
512/// A checked `touch <side> <gesture> <action>`: the bud ids, the act and the fn to write.
513#[derive(Debug, PartialEq, Eq)]
514pub(super) struct TouchTarget {
515 pub(super) devices: &'static [u8],
516 pub(super) act: u8,
517 pub(super) function: u8,
518}
519
520/// Check the names against what the T310 offers. Every failure is a usage error that lists
521/// the valid names. Spec: realme_t310.md#gesture-choices
522pub(super) fn touch_target(side: &str, gesture: &str, action: &str) -> Result<TouchTarget> {
523 let list = |names: Vec<&str>| names.join(", ");
524 let Some(&(_, devices)) = TOUCH_SIDES.iter().find(|(name, _)| *name == side) else {
525 return Err(Error::Usage(format!(
526 "no side called {side}; use one of: {}",
527 list(TOUCH_SIDES.iter().map(|(name, _)| *name).collect())
528 )));
529 };
530 let gesture_names = || {
531 list(
532 TOUCH_CHOICES
533 .iter()
534 .filter_map(|(act, _)| name_of(&ACTS, *act))
535 .collect(),
536 )
537 };
538 let Some(&(act, choices)) = TOUCH_CHOICES
539 .iter()
540 .find(|(act, _)| name_of(&ACTS, *act) == Some(gesture))
541 else {
542 return Err(Error::Usage(format!(
543 "these buds have no {gesture} gesture to set; use one of: {}",
544 gesture_names()
545 )));
546 };
547 let Some(&function) = choices.iter().find(|f| name_of(&FUNCTIONS, **f) == Some(action)) else {
548 return Err(Error::Usage(format!(
549 "{gesture} cannot be set to {action} on these buds; use one of: {}",
550 list(choices.iter().filter_map(|f| name_of(&FUNCTIONS, *f)).collect())
551 )));
552 };
553 Ok(TouchTarget { devices, act, function })
554}
555
556/// The table with the target's entries changed, and nothing else. Every targeted entry must
557/// already be in the table: budctl never adds entries. Spec: realme_t310.md#gesture-write
558pub(super) fn retarget_gestures(table: &[GestureEntry], target: &TouchTarget) -> Result<Vec<GestureEntry>> {
559 for &device in target.devices {
560 if !table
561 .iter()
562 .any(|&[d, group, act, _]| d == device && group == MAIN_GROUP && act == target.act)
563 {
564 let entry = name_gesture(&[device, MAIN_GROUP, target.act, 0]);
565 return Err(Error::Unsupported(format!(
566 "the buds report no {} {} entry, so budctl will not write one",
567 entry.side, entry.gesture
568 )));
569 }
570 }
571 Ok(table
572 .iter()
573 .map(|&[device, group, act, function]| {
574 if target.devices.contains(&device) && group == MAIN_GROUP && act == target.act {
575 [device, group, act, target.function]
576 } else {
577 [device, group, act, function]
578 }
579 })
580 .collect())
581}
582
583/// `<count> (<dev> <btn> <act> <fn>)…`: the whole table. Spec: realme_t310.md#gesture-write
584pub(super) fn gesture_write_payload(table: &[GestureEntry]) -> Result<Vec<u8>> {
585 let count = u8::try_from(table.len())
586 .map_err(|_| Error::Protocol(format!("a gesture table of {} entries does not fit", table.len())))?;
587 let mut payload = vec![count];
588 payload.extend(table.iter().flatten());
589 Ok(payload)
590}
591
592/// The handshake reply of the owner's T310 (HW1). Spec: realme_t310.md#bitmap-t310
593#[cfg(test)]
594pub(super) const T310_BITMAP_REPLY: &str = "00 BF 1F 5A 20 45 04 40 00";
595
596#[cfg(test)]
597mod tests {
598 use super::*;
599 use crate::transport::mock::hex;
600
601 #[test]
602 fn t310_bitmap_allows_its_commands_and_nothing_else() {
603 let commands = Commands::parse(&hex(T310_BITMAP_REPLY)).unwrap();
604 // Every gated command in the T310's logged command set (realme_t310.md#bitmap-t310).
605 for cmd in [
606 0x0100, 0x0103, 0x0105, 0x0106, 0x0108, 0x0109, 0x010C, 0x010D, 0x010F, 0x0114, 0x0122, 0x0124, 0x0205,
607 0x0400, 0x0401, 0x0403, 0x0404, 0x0405, 0x0406, 0x0418, 0x041B, 0x0200,
608 ] {
609 assert!(commands.allows(cmd), "0x{cmd:04X} should be allowed");
610 }
611 // Commands the T310 lacks, and commands budctl has no gate for.
612 for cmd in [0x012F, 0x041E, 0x0413, 0x0130, 0x0427, 0x0423, 0x012A, 0x0422, 0x9999] {
613 assert!(!commands.allows(cmd), "0x{cmd:04X} should be refused");
614 }
615 }
616
617 #[test]
618 fn a_short_bitmap_allows_only_the_always_commands() {
619 let commands = Commands::parse(&hex("00")).unwrap();
620 assert!(commands.allows(BATTERY) && commands.allows(SWITCHES) && commands.allows(HANDSHAKE));
621 assert!(!commands.allows(FIRMWARE) && !commands.allows(NOISE_CONTROL));
622 assert!(matches!(
623 Commands::parse(&hex("01 FF")),
624 Err(Error::Rejected { status: 1, .. })
625 ));
626 }
627
628 #[test]
629 fn product_id_is_three_bytes_little_endian() {
630 assert_eq!(decode_product_id(&hex("00 12 3C 06")).unwrap(), T310_PRODUCT_ID);
631 assert_eq!(decode_product_id(&hex("00 14 54 06")).unwrap(), 0x06_54_14);
632 assert!(decode_product_id(&hex("00 12 3C")).is_err());
633 }
634
635 #[test]
636 fn subscribes_to_offered_events_only() {
637 let commands = Commands::parse(&hex(T310_BITMAP_REPLY)).unwrap();
638 let buds4_offer = hex("00 09 01 02 03 04 08 0B F1 F2 F3");
639 let offered = decode_events_offered(&buds4_offer).unwrap();
640 assert_eq!(subscribe_payload(&offered, &commands), hex("04 01 02 03 04"));
641 assert_eq!(subscribe_payload(&[0x02, 0x01], &commands), hex("02 01 02"));
642 let no_fit_test = Commands::parse(&hex("00 BD 0D")).unwrap();
643 assert_eq!(subscribe_payload(&offered, &no_fit_test), hex("03 01 02 03"));
644 }
645
646 #[test]
647 fn battery_levels_charging_and_unknown_parts() {
648 let battery = decode_battery(&hex("00 03 01 64 02 E4 03 00")).unwrap();
649 assert_eq!(
650 battery.left,
651 Some(Charge {
652 level: 100,
653 charging: false
654 })
655 );
656 assert_eq!(
657 battery.right,
658 Some(Charge {
659 level: 100,
660 charging: true
661 })
662 );
663 assert_eq!(battery.case, None);
664 assert_eq!(decode_battery(&hex("00 00")).unwrap(), Battery::default());
665 assert!(decode_battery(&hex("00 03 01 64")).is_err());
666 }
667
668 #[test]
669 fn firmware_shows_kind_two_versions() {
670 let mut reply = hex("00 04");
671 reply.extend(b"1,2,138,2,2,138,3,1,01,3,2,105");
672 assert_eq!(decode_firmware(&reply).unwrap(), "138.138.105");
673 // The owner's T310 (HW1): both buds report the same full version.
674 let mut t310 = hex("00 02");
675 t310.extend(b"1,2,1.1.0.43,2,2,1.1.0.43");
676 assert_eq!(decode_firmware(&t310).unwrap(), "1.1.0.43");
677 let mut differ = hex("00 02");
678 differ.extend(b"1,2,1.1.0.43,2,2,1.1.0.44");
679 assert_eq!(decode_firmware(&differ).unwrap(), "1.1.0.43 / 1.1.0.44");
680 let mut odd = hex("00 01");
681 odd.extend(b"V1.0.2");
682 assert_eq!(decode_firmware(&odd).unwrap(), "V1.0.2");
683 }
684
685 #[test]
686 fn noise_control_modes() {
687 let mode = |reply: &str| decode_noise_mode(&hex(reply)).unwrap();
688 assert_eq!(mode("00 01 01 01").mode, "off");
689 assert_eq!(mode("00 01 01 02 00").mode, "transparency");
690 let deep = mode("00 01 01 08");
691 assert_eq!((deep.mode.as_str(), deep.level.as_deref()), ("anc", Some("deep")));
692 assert_eq!(mode("00 01 01 10").level.as_deref(), Some("medium"));
693 assert_eq!(mode("00 01 01 00 08").mode, "unknown (0x800)");
694 assert!(decode_noise_mode(&hex("00 02 01 08")).is_err());
695 assert!(decode_noise_mode(&hex("00 01 01")).is_err());
696 assert!(matches!(
697 decode_noise_mode(&hex("0E 01 01 08")),
698 Err(Error::Rejected { status: 0x0E, .. })
699 ));
700 }
701
702 #[test]
703 fn noise_control_write_payloads() {
704 assert_eq!(noise_write_payload(NoiseMode::Off), hex("01 01 01"));
705 assert_eq!(noise_write_payload(NoiseMode::Transparency), hex("01 01 02"));
706 assert_eq!(noise_write_payload(NoiseMode::Anc(AncLevel::Light)), hex("01 01 04"));
707 assert_eq!(noise_write_payload(NoiseMode::Anc(AncLevel::Medium)), hex("01 01 10"));
708 assert_eq!(noise_write_payload(NoiseMode::Anc(AncLevel::Deep)), hex("01 01 08"));
709 // What is written is what a read reports: every write mask decodes to its own mode.
710 for level in [AncLevel::Light, AncLevel::Medium, AncLevel::Deep] {
711 let mut reply = hex("00 01 01");
712 reply.extend(noise_write_payload(NoiseMode::Anc(level)).iter().skip(2));
713 let read = decode_noise_mode(&reply).unwrap();
714 assert_eq!((read.mode.as_str(), read.level.as_deref()), ("anc", Some(level.name())));
715 }
716 // The family's documented frame for "off" (realme_t310.md#frame-whole).
717 let frame =
718 crate::devices::realme_t310::frame::encode(NOISE_CONTROL_WRITE, 0x06, &noise_write_payload(NoiseMode::Off))
719 .unwrap();
720 assert_eq!(frame, hex("AA 0A 00 00 04 04 06 03 00 01 01 01"));
721 }
722
723 #[test]
724 fn switch_writes() {
725 assert_eq!(switch_id("game"), Some(0x06));
726 assert_eq!(switch_id("dual"), Some(0x11));
727 assert_eq!(switch_id("turbo"), None);
728 assert_eq!(switch_write_payload(0x06, true), [0x06, 0x01]);
729 assert_eq!(switch_write_payload(0x06, false), [0x06, 0x00]);
730 }
731
732 #[test]
733 fn eq_presets_by_name() {
734 assert_eq!(eq_id("pure-raw"), Some(0x00));
735 assert_eq!(eq_id("pure-bass"), Some(0x03));
736 assert_eq!(eq_id("custom-7"), None);
737 assert_eq!(eq_names(), ["pure-raw", "deep-bass", "pure-vocals", "pure-bass"]);
738 // Every preset reads back under the name it was selected by.
739 for name in eq_names() {
740 let reply = [0x00, eq_id(name).unwrap()];
741 assert_eq!(decode_eq(&reply).unwrap(), name);
742 }
743 }
744
745 #[test]
746 fn find_payloads() {
747 assert_eq!(find_payload(true), [0x01]);
748 assert_eq!(find_payload(false), [0x00]);
749 }
750
751 #[test]
752 fn write_replies() {
753 assert!(check_write_reply(&hex("00"), "the write").is_ok());
754 assert!(matches!(
755 check_write_reply(&hex("0E"), "the write"),
756 Err(Error::Rejected { status: 0x0E, .. })
757 ));
758 assert!(check_write_reply(&[], "the write").is_err());
759 }
760
761 #[test]
762 fn hold_cycle_lists_modes() {
763 assert_eq!(decode_hold_cycle(&hex("00 02 01 0A")).unwrap(), ["transparency", "anc"]);
764 assert_eq!(
765 decode_hold_cycle(&hex("00 02 01 0B 00")).unwrap(),
766 ["off", "transparency", "anc"]
767 );
768 assert_eq!(decode_hold_cycle(&hex("00 02 01 14")).unwrap(), ["unknown (0x14)"]);
769 }
770
771 #[test]
772 fn switches_from_report_22_values() {
773 let switches = decode_switches(&hex("00 06 05 00 06 00 11 01 1A 00 1B 01 1D 01")).unwrap();
774 let on: Vec<(&str, bool)> = switches.iter().map(|s| (s.key, s.on)).collect();
775 assert_eq!(
776 on,
777 [
778 ("game", false),
779 ("dual", true),
780 ("wind", false),
781 ("spatial", true),
782 ("bass-boost", true)
783 ]
784 );
785 assert!(decode_switches(&hex("00 02 06 01")).is_err());
786 }
787
788 #[test]
789 fn eq_codec_and_bass_level() {
790 assert_eq!(decode_eq(&hex("00 00")).unwrap(), "pure-raw");
791 assert_eq!(decode_eq(&hex("00 03")).unwrap(), "pure-bass");
792 assert_eq!(decode_eq(&hex("00 05")).unwrap(), "custom-5");
793 assert_eq!(decode_codec(&hex("00 02")).unwrap(), "AAC");
794 assert_eq!(decode_codec(&hex("00 09")).unwrap(), "unknown (0x09)");
795 assert_eq!(decode_bass_level(&hex("00 FB 05 FE")).unwrap(), -2);
796 assert!(decode_bass_level(&hex("00 FB 05 07")).is_err());
797 }
798
799 #[test]
800 fn gesture_table_names_known_values() {
801 let gestures = decode_gestures(&hex("00 03 01 01 02 01 02 01 04 08 04 06 02 1D")).unwrap();
802 let rows: Vec<(&str, &str, &str, Option<u8>)> = gestures
803 .iter()
804 .map(|g| (g.side.as_str(), g.gesture.as_str(), g.action.as_str(), g.group))
805 .collect();
806 assert_eq!(
807 rows,
808 [
809 ("left", "double-tap", "play-pause", None),
810 ("right", "hold", "noise-cycle", None),
811 ("both", "double-tap", "answer-call", Some(6)),
812 ]
813 );
814 let odd = decode_gestures(&hex("00 01 09 01 07 42")).unwrap();
815 assert_eq!(odd.first().map(|g| g.action.as_str()), Some("fn-0x42"));
816 assert!(decode_gestures(&hex("00 02 01 01 02 01")).is_err());
817 }
818
819 /// The owner's table (HW1): per bud, double tap play/pause, triple tap next, hold cycle.
820 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";
821
822 #[test]
823 fn touch_targets_follow_the_t310_choices() {
824 assert_eq!(
825 touch_target("left", "triple-tap", "volume-up").unwrap(),
826 TouchTarget {
827 devices: &[0x01],
828 act: 0x03,
829 function: 0x0B
830 }
831 );
832 assert_eq!(touch_target("both", "hold", "none").unwrap().devices, [0x01, 0x02]);
833 let usage = |side, gesture, action| match touch_target(side, gesture, action) {
834 Err(Error::Usage(message)) => message,
835 other => panic!("expected a usage error, got {other:?}"),
836 };
837 assert!(usage("middle", "hold", "none").contains("left, right, both"));
838 assert!(usage("left", "single-tap", "none").contains("double-tap, triple-tap, hold"));
839 // Offered by the family, but not by realme Link for this gesture.
840 assert!(usage("left", "double-tap", "volume-up").contains("none, play-pause, next, voice-assistant"));
841 assert!(usage("left", "hold", "previous").contains("none, noise-cycle, voice-assistant, game-mode"));
842 }
843
844 #[test]
845 fn retargeting_changes_only_the_targeted_entries() {
846 let table = decode_gesture_entries(&hex(HW1_GESTURES)).unwrap();
847 let left = touch_target("left", "triple-tap", "volume-up").unwrap();
848 let payload = gesture_write_payload(&retarget_gestures(&table, &left).unwrap()).unwrap();
849 assert_eq!(
850 payload,
851 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")
852 );
853 // The frame the spec gives for this change (realme_t310.md#gesture-write).
854 let frame = crate::devices::realme_t310::frame::encode(GESTURE_WRITE, 0x06, &payload).unwrap();
855 assert_eq!(
856 crate::debug::hex(&frame),
857 "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"
858 );
859 let both = touch_target("both", "hold", "game-mode").unwrap();
860 let payload = gesture_write_payload(&retarget_gestures(&table, &both).unwrap()).unwrap();
861 assert_eq!(
862 payload,
863 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")
864 );
865 // Unchanged, the payload is the reply without its status byte.
866 let same = touch_target("right", "double-tap", "play-pause").unwrap();
867 let payload = gesture_write_payload(&retarget_gestures(&table, &same).unwrap()).unwrap();
868 assert_eq!(payload, hex(HW1_GESTURES).get(1..).unwrap());
869 }
870
871 #[test]
872 fn entries_the_buds_did_not_report_are_never_written() {
873 // Only the left bud's entries.
874 let table = decode_gesture_entries(&hex("00 03 01 01 02 01 01 01 03 06 01 01 04 08")).unwrap();
875 let both = touch_target("both", "hold", "none").unwrap();
876 assert!(matches!(retarget_gestures(&table, &both), Err(Error::Unsupported(_))));
877 // An on-call row (group 06) is not a main-group entry.
878 let table = decode_gesture_entries(&hex("00 01 01 06 02 1D")).unwrap();
879 let left = touch_target("left", "double-tap", "none").unwrap();
880 assert!(matches!(retarget_gestures(&table, &left), Err(Error::Unsupported(_))));
881 }
882
883 #[test]
884 fn hold_cycle_writes() {
885 assert_eq!(hold_cycle_mask(&["transparency", "anc"]).unwrap(), 0x0A);
886 assert_eq!(hold_cycle_mask(&["anc", "off", "transparency"]).unwrap(), 0x0B);
887 assert_eq!(hold_cycle_write_payload(0x0B), hex("02 01 0B"));
888 assert_eq!(hold_cycle_write_payload(0x09), hex("02 01 09"));
889 let usage = |modes: &[&str]| matches!(hold_cycle_mask(modes), Err(Error::Usage(_)));
890 assert!(usage(&["anc"]));
891 assert!(usage(&[]));
892 assert!(usage(&["anc", "anc"]));
893 assert!(usage(&["anc", "deep"]));
894 // What is written reads back as the same modes.
895 let mut reply = hex("00");
896 reply.extend(hold_cycle_write_payload(0x0B));
897 assert_eq!(decode_hold_cycle_mask(&reply).unwrap(), 0x0B);
898 assert_eq!(decode_hold_cycle(&reply).unwrap(), ["off", "transparency", "anc"]);
899 // The frame the spec gives (realme_t310.md#hold-cycle-write).
900 let frame =
901 crate::devices::realme_t310::frame::encode(NOISE_CONTROL_WRITE, 0x06, &hold_cycle_write_payload(0x0B))
902 .unwrap();
903 assert_eq!(crate::debug::hex(&frame), "AA 0A 00 00 04 04 06 03 00 02 01 0B");
904 }
905}