Krolyxon
Commit 4c49f6c

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310codec.rs

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