Krolyxon
Commit 00f061b

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310codec.rs

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