Krolyxon
Commit 54dede1

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310codec.rs

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