Krolyxon
Commit 449c3e4

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310codec.rs

1334 lines · 52.7 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::{
7 AncLevel, Battery, Charge, EqBand, EqPreset, Gesture, GestureChoice, NoiseControl, NoiseMode, Switch,
8};
9
10/// Spec: realme_t310.md#bitmap
11pub(super) const HANDSHAKE: u16 = 0x0100;
12/// Spec: realme_t310.md#identity
13pub(super) const PRODUCT_ID: u16 = 0x0103;
14/// Spec: realme_t310.md#firmware
15pub(super) const FIRMWARE: u16 = 0x0105;
16/// Spec: realme_t310.md#battery
17pub(super) const BATTERY: u16 = 0x0106;
18/// Spec: realme_t310.md#gestures
19pub(super) const GESTURES: u16 = 0x0108;
20/// Spec: realme_t310.md#anc-read
21pub(super) const NOISE_CONTROL: u16 = 0x010C;
22/// Spec: realme_t310.md#switches
23pub(super) const SWITCHES: u16 = 0x010D;
24/// Spec: realme_t310.md#anc-write
25pub(super) const NOISE_CONTROL_WRITE: u16 = 0x0404;
26/// Spec: realme_t310.md#switch-write
27pub(super) const SWITCH_WRITE: u16 = 0x0403;
28/// Spec: realme_t310.md#eq-write
29pub(super) const EQ_SELECT: u16 = 0x0406;
30/// Spec: realme_t310.md#find
31pub(super) const FIND: u16 = 0x0400;
32/// Spec: realme_t310.md#bass-write
33pub(super) const BASS_LEVEL_WRITE: u16 = 0x041B;
34/// The bass-level range. Spec: realme_t310.md#bass-write
35pub(super) const BASS_RANGE: (i8, i8) = (-5, 5);
36
37/// `FB 05 <level s8>`: the range bytes, then the level. Spec: realme_t310.md#bass-write
38pub(super) fn bass_write_payload(level: i8) -> Result<[u8; 3]> {
39 let (min, max) = BASS_RANGE;
40 if !(min..=max).contains(&level) {
41 return Err(Error::Usage(format!(
42 "the bass level goes from {min} to +{max}; {level} is outside"
43 )));
44 }
45 let [min, max, level] = [min, max, level].map(|v| v.to_le_bytes()[0]);
46 Ok([min, max, level])
47}
48/// Spec: realme_t310.md#gesture-write
49pub(super) const GESTURE_WRITE: u16 = 0x0401;
50
51/// `01` starts ringing, `00` stops it. Spec: realme_t310.md#find
52pub(super) fn find_payload(ring: bool) -> [u8; 1] {
53 [u8::from(ring)]
54}
55/// Spec: realme_t310.md#eq-read
56pub(super) const EQ_PRESET: u16 = 0x010F;
57/// Spec: realme_t310.md#codec
58pub(super) const CODEC: u16 = 0x0114;
59/// Spec: realme_t310.md#custom-eq
60pub(super) const CUSTOM_EQ_LIST: u16 = 0x0122;
61/// Spec: realme_t310.md#custom-eq
62pub(super) const CUSTOM_EQ_WRITE: u16 = 0x0418;
63/// The `0x0418` action that saves a whole preset and selects it. Spec: realme_t310.md#custom-eq
64const CUSTOM_EQ_SAVE: u8 = 0x02;
65/// The `0x0418` action that creates a preset; the buds give it an id. Spec: realme_t310.md#custom-eq
66const CUSTOM_EQ_CREATE: u8 = 0x01;
67/// The `0x0418` action that deletes a preset. Spec: realme_t310.md#custom-eq
68const CUSTOM_EQ_DELETE: u8 = 0x03;
69/// The range and bands of the owner's T310's custom presets: what a new preset gets when there is
70/// none to copy. Spec: realme_t310.md#custom-eq-create
71const T310_CUSTOM_RANGE: (i8, i8) = (-6, 6);
72/// Spec: realme_t310.md#custom-eq-create
73const T310_CUSTOM_BANDS: [u16; 6] = [50, 250, 1000, 4000, 8000, 16000];
74/// The most custom presets the buds keep. Spec: realme_t310.md#custom-eq
75pub(super) const CUSTOM_EQ_MAX: usize = 3;
76/// The longest preset name budctl writes, in bytes. budctl's own limit: the name's length is one
77/// byte, and the vendor's limit is unknown. Spec: realme_t310.md#custom-eq-create
78const CUSTOM_EQ_NAME_MAX: usize = 16;
79/// Spec: realme_t310.md#events-offered
80pub(super) const EVENTS_OFFERED: u16 = 0x0200;
81/// Spec: realme_t310.md#subscribe
82pub(super) const SUBSCRIBE: u16 = 0x0205;
83/// Spec: realme_t310.md#fit-test
84pub(super) const FIT_TEST: u16 = 0x0405;
85
86/// Spec: realme_t310.md#identity
87pub(super) const T310_PRODUCT_ID: u32 = 0x06_3C_12;
88
89/// Payload asking for the current noise-control mode. Spec: realme_t310.md#anc-read
90pub(super) const NOISE_MODE_QUERY: [u8; 2] = [0x01, 0x01];
91/// Payload asking for the hold's noise-control cycle (one setting for both buds).
92/// Spec: realme_t310.md#hold-cycle
93pub(super) const HOLD_CYCLE_QUERY: [u8; 2] = [0x02, 0x01];
94/// Count, then the T310's switch ids: `05 06 11 1A 1B 1D`. Spec: realme_t310.md#switches
95pub(super) const SWITCH_QUERY: [u8; 7] = [6, 0x05, 0x06, 0x11, 0x1A, 0x1B, 0x1D];
96
97/// Event codes to subscribe to; the optional ones only when their command is allowed.
98/// Spec: realme_t310.md#subscribe
99const EVENTS: [(u8, Option<u16>); 4] = [(0x01, None), (0x02, None), (0x03, None), (0x04, Some(FIT_TEST))];
100
101/// Spec: realme_t310.md#switches
102const SWITCH_NAMES: [(u8, &str, &str); 5] = [
103 (0x06, "game", "Game mode"),
104 (0x11, "dual", "Dual connection"),
105 (0x1A, "wind", "Wind-noise reduction"),
106 (0x1B, "spatial", "3D sound"),
107 (0x1D, "bass-boost", "Bass boost"),
108];
109
110/// Noise-control masks: mode and level. Spec: realme_t310.md#anc-read
111const NOISE_MODES: [(u32, &str, Option<&str>); 5] = [
112 (0x01, "off", None),
113 (0x02, "transparency", None),
114 (0x04, "anc", Some("light")),
115 (0x08, "anc", Some("deep")),
116 (0x10, "anc", Some("medium")),
117];
118
119/// Bits of the hold cycle. Spec: realme_t310.md#hold-cycle
120const CYCLE_MODES: [(u32, &str); 3] = [(0x01, "off"), (0x02, "transparency"), (0x08, "anc")];
121
122/// Built-in EQ presets by id. Spec: realme_t310.md#eq-read
123const EQ_PRESETS: [(u8, &str); 4] = [
124 (0x00, "pure-raw"),
125 (0x01, "deep-bass"),
126 (0x02, "pure-vocals"),
127 (0x03, "pure-bass"),
128];
129
130/// Spec: realme_t310.md#codec
131const CODECS: [(u8, &str); 8] = [
132 (0x01, "SBC"),
133 (0x02, "AAC"),
134 (0x03, "LDAC"),
135 (0x04, "aptX"),
136 (0x05, "aptX HD"),
137 (0x06, "aptX Adaptive"),
138 (0x07, "LHDC"),
139 (0x08, "LHDC V5"),
140];
141
142/// Gesture-table names. Spec: realme_t310.md#gestures
143const SIDES: [(u8, &str); 3] = [(0x01, "left"), (0x02, "right"), (0x04, "both")];
144/// Spec: realme_t310.md#gestures
145const MAIN_GROUP: u8 = 0x01;
146/// Spec: realme_t310.md#gestures
147const ACTS: [(u8, &str); 6] = [
148 (0x01, "single-tap"),
149 (0x02, "double-tap"),
150 (0x03, "triple-tap"),
151 (0x04, "hold"),
152 (0x05, "slide"),
153 (0x06, "long-hold"),
154];
155/// Spec: realme_t310.md#gestures
156const FUNCTIONS: [(u8, &str); 15] = [
157 (0x00, "none"),
158 (0x01, "play-pause"),
159 (0x03, "voice-assistant"),
160 (0x05, "previous"),
161 (0x06, "next"),
162 (0x07, "volume"),
163 (0x08, "noise-cycle"),
164 (0x09, "favourite-music"),
165 (0x0A, "switch-track"),
166 (0x0B, "volume-up"),
167 (0x0C, "volume-down"),
168 (0x0D, "switch-devices"),
169 (0x11, "game-mode"),
170 (0x1C, "decline-call"),
171 (0x1D, "answer-call"),
172];
173
174fn name_of<K: PartialEq + Copy>(table: &[(K, &'static str)], key: K) -> Option<&'static str> {
175 table.iter().find(|(k, _)| *k == key).map(|(_, name)| *name)
176}
177
178/// When a command may be sent. Spec: realme_t310.md#bitmap
179enum Gate {
180 Always,
181 Bit(u8),
182}
183
184/// The gate of every command budctl uses. An unlisted command is never sent.
185/// Spec: realme_t310.md#bitmap
186fn gate(cmd: u16) -> Option<Gate> {
187 Some(match cmd {
188 0x0100..=0x0104 | 0x0106 | 0x010B | 0x010D | 0x0200 => Gate::Always,
189 0x0105 => Gate::Bit(0),
190 0x0108 | 0x0401 => Gate::Bit(3),
191 0x0109 => Gate::Bit(4),
192 0x0400 => Gate::Bit(5),
193 0x0403 => Gate::Bit(7),
194 0x010C | 0x0404 => Gate::Bit(8),
195 0x0405 => Gate::Bit(9),
196 0x010F | 0x0406 => Gate::Bit(10),
197 0x0114 => Gate::Bit(17),
198 0x0205 => Gate::Bit(22),
199 0x0122 | 0x0418 => Gate::Bit(34),
200 0x0124 | 0x041B => Gate::Bit(38),
201 _ => return None,
202 })
203}
204
205/// The commands these buds accept, from the handshake reply. Spec: realme_t310.md#bitmap
206#[derive(Debug)]
207pub(super) struct Commands {
208 bitmap: Vec<u8>,
209}
210
211impl Commands {
212 pub(super) fn parse(reply: &[u8]) -> Result<Self> {
213 let mut r = ok_reply(reply, "the handshake")?;
214 Ok(Self {
215 bitmap: r.rest().to_vec(),
216 })
217 }
218
219 pub(super) fn allows(&self, cmd: u16) -> bool {
220 match gate(cmd) {
221 Some(Gate::Always) => true,
222 Some(Gate::Bit(n)) => self
223 .bitmap
224 .get(usize::from(n / 8))
225 .is_some_and(|byte| byte & (1 << (n % 8)) != 0),
226 None => false,
227 }
228 }
229}
230
231/// A reply whose status byte is `00`, positioned after it. Spec: realme_t310.md#seq
232fn ok_reply<'a>(reply: &'a [u8], what: &'static str) -> Result<Reader<'a>> {
233 let mut r = Reader::new(reply, what);
234 match r.u8()? {
235 0x00 => Ok(r),
236 status => Err(Error::Rejected {
237 what: what.to_owned(),
238 status,
239 }),
240 }
241}
242
243/// Spec: realme_t310.md#identity
244pub(super) fn decode_product_id(reply: &[u8]) -> Result<u32> {
245 let mut r = ok_reply(reply, "the product id query")?;
246 let [low, middle, high] = [r.u8()?, r.u8()?, r.u8()?];
247 Ok(u32::from_le_bytes([low, middle, high, 0]))
248}
249
250/// Spec: realme_t310.md#events-offered
251pub(super) fn decode_events_offered(reply: &[u8]) -> Result<Vec<u8>> {
252 let mut r = ok_reply(reply, "the event list")?;
253 let count = r.u8()?;
254 Ok(r.take(usize::from(count))?.to_vec())
255}
256
257/// The subscribe payload: count, then the wanted codes the buds offer.
258/// Spec: realme_t310.md#subscribe
259pub(super) fn subscribe_payload(offered: &[u8], commands: &Commands) -> Vec<u8> {
260 let codes: Vec<u8> = EVENTS
261 .iter()
262 .filter(|(code, needs)| offered.contains(code) && needs.is_none_or(|cmd| commands.allows(cmd)))
263 .map(|(code, _)| *code)
264 .collect();
265 let mut payload = vec![u8::try_from(codes.len()).unwrap_or_default()];
266 payload.extend(codes);
267 payload
268}
269
270/// Spec: realme_t310.md#battery
271pub(super) fn decode_battery(reply: &[u8]) -> Result<Battery> {
272 let mut r = ok_reply(reply, "the battery reply")?;
273 let mut battery = Battery::default();
274 for _ in 0..r.u8()? {
275 let part = r.u8()?;
276 let raw = r.u8()?;
277 let level = raw & 0x7F;
278 if level == 0 {
279 continue; // not known
280 }
281 let charge = Some(Charge {
282 level,
283 charging: raw & 0x80 != 0,
284 });
285 match part {
286 0x01 => battery.left = charge,
287 0x02 => battery.right = charge,
288 0x03 => battery.case = charge,
289 _ => {}
290 }
291 }
292 Ok(battery)
293}
294
295/// The versions of kind 2: plain numbers are joined with dots; full dotted versions (the T310's)
296/// are shown once each, joined with ` / `. The raw text when there are none.
297/// Spec: realme_t310.md#firmware
298pub(super) fn decode_firmware(reply: &[u8]) -> Result<String> {
299 let mut r = ok_reply(reply, "the firmware reply")?;
300 let _count = r.u8()?;
301 let text = String::from_utf8_lossy(r.rest()).into_owned();
302 let fields: Vec<&str> = text.split(',').map(str::trim).collect();
303 let mut versions: Vec<&str> = fields
304 .chunks(3)
305 .filter_map(|triple| match triple {
306 [_device, "2", version] => Some(*version),
307 _ => None,
308 })
309 .collect();
310 if versions.is_empty() {
311 return Ok(text);
312 }
313 if !versions.iter().any(|v| v.contains('.')) {
314 return Ok(versions.join("."));
315 }
316 let mut seen = Vec::new();
317 versions.retain(|v| {
318 let new = !seen.contains(v);
319 seen.push(*v);
320 new
321 });
322 Ok(versions.join(" / "))
323}
324
325/// Spec: realme_t310.md#codec
326pub(super) fn decode_codec(reply: &[u8]) -> Result<String> {
327 let codec = ok_reply(reply, "the codec reply")?.u8()?;
328 Ok(name_of(&CODECS, codec).map_or_else(|| format!("unknown (0x{codec:02X})"), str::to_owned))
329}
330
331/// Spec: realme_t310.md#anc-read
332pub(super) fn decode_noise_mode(reply: &[u8]) -> Result<NoiseControl> {
333 let mask = noise_mask(reply, NOISE_MODE_QUERY, "the noise-control reply")?;
334 Ok(match NOISE_MODES.iter().find(|(m, _, _)| *m == mask) {
335 Some((_, mode, level)) => NoiseControl {
336 mode: (*mode).to_owned(),
337 level: level.map(str::to_owned),
338 },
339 None => NoiseControl {
340 mode: format!("unknown (0x{mask:02X})"),
341 level: None,
342 },
343 })
344}
345
346/// The mask that selects `mode`, as written and as read back. Spec: realme_t310.md#anc-write
347pub(super) fn noise_write_mask(mode: NoiseMode) -> u32 {
348 match mode {
349 NoiseMode::Off => 0x01,
350 NoiseMode::Transparency => 0x02,
351 NoiseMode::Anc(AncLevel::Light) => 0x04,
352 NoiseMode::Anc(AncLevel::Medium) => 0x10,
353 // Also plain "noise cancelling on".
354 NoiseMode::Anc(AncLevel::Deep) => 0x08,
355 }
356}
357
358/// `01 01 <mask>`: the noise-mode kind, then the mask in the fewest bytes (all fit in one).
359/// Spec: realme_t310.md#anc-write
360pub(super) fn noise_write_payload(mode: NoiseMode) -> Vec<u8> {
361 let mut payload = NOISE_MODE_QUERY.to_vec();
362 payload.extend(fewest_bytes(noise_write_mask(mode)));
363 payload
364}
365
366/// A mask in the fewest little-endian bytes that hold it, at least one.
367/// Spec: realme_t310.md#anc-write, #hold-cycle-write
368fn fewest_bytes(mask: u32) -> Vec<u8> {
369 let bytes = mask.to_le_bytes();
370 let used = bytes.iter().rposition(|&b| b != 0).map_or(1, |last| last + 1);
371 bytes.iter().take(used).copied().collect()
372}
373
374/// The mask in a noise-control read reply. Spec: realme_t310.md#anc-read
375pub(super) fn decode_noise_mask(reply: &[u8]) -> Result<u32> {
376 noise_mask(reply, NOISE_MODE_QUERY, "the noise-control reply")
377}
378
379/// A write reply: `00` means accepted. Spec: realme_t310.md#writes
380pub(super) fn check_write_reply(reply: &[u8], what: &'static str) -> Result<()> {
381 ok_reply(reply, what).map(|_| ())
382}
383
384/// Spec: realme_t310.md#hold-cycle
385pub(super) fn decode_hold_cycle(reply: &[u8]) -> Result<Vec<String>> {
386 let mask = decode_hold_cycle_mask(reply)?;
387 let mut modes: Vec<String> = CYCLE_MODES
388 .iter()
389 .filter(|(bit, _)| mask & bit != 0)
390 .map(|(_, name)| (*name).to_owned())
391 .collect();
392 let known = CYCLE_MODES.iter().fold(0, |all, (bit, _)| all | bit);
393 if mask & !known != 0 {
394 modes.push(format!("unknown (0x{:02X})", mask & !known));
395 }
396 Ok(modes)
397}
398
399/// The mask for a hold cycle through `modes` (`off`, `transparency`, `anc`): at least two,
400/// none twice. The buds keep their own order, so the order given does not matter.
401/// Spec: realme_t310.md#hold-cycle-write
402pub(super) fn hold_cycle_mask(modes: &[&str]) -> Result<u32> {
403 let mut mask = 0;
404 for &mode in modes {
405 let Some(&(bit, _)) = CYCLE_MODES.iter().find(|(_, name)| *name == mode) else {
406 let names: Vec<&str> = CYCLE_MODES.iter().map(|(_, name)| *name).collect();
407 return Err(Error::Usage(format!(
408 "no mode called {mode} in the hold cycle; use: {}",
409 names.join(", ")
410 )));
411 };
412 if mask & bit != 0 {
413 return Err(Error::Usage(format!("{mode} is in the hold cycle twice")));
414 }
415 mask |= bit;
416 }
417 if mask.count_ones() < 2 {
418 return Err(Error::Usage("the hold cycle needs at least two modes".to_owned()));
419 }
420 Ok(mask)
421}
422
423/// `02 01 <mask>`, the mask in the fewest bytes. Spec: realme_t310.md#hold-cycle-write
424pub(super) fn hold_cycle_write_payload(mask: u32) -> Vec<u8> {
425 let mut payload = HOLD_CYCLE_QUERY.to_vec();
426 payload.extend(fewest_bytes(mask));
427 payload
428}
429
430/// The mask in a hold-cycle read reply. Spec: realme_t310.md#hold-cycle
431pub(super) fn decode_hold_cycle_mask(reply: &[u8]) -> Result<u32> {
432 noise_mask(reply, HOLD_CYCLE_QUERY, "the hold-cycle reply")
433}
434
435/// `00 <echo of the query> <mask, 1-4 bytes LE>`. Spec: realme_t310.md#anc-read
436fn noise_mask(reply: &[u8], query: [u8; 2], what: &'static str) -> Result<u32> {
437 let mut r = ok_reply(reply, what)?;
438 if [r.u8()?, r.u8()?] != query {
439 return Err(Error::Protocol(format!("{what} answers a different question")));
440 }
441 let bytes = r.rest();
442 if bytes.is_empty() || bytes.len() > 4 {
443 return Err(Error::Protocol(format!("{what} has a {} byte value", bytes.len())));
444 }
445 Ok(bytes.iter().rev().fold(0, |mask, &b| (mask << 8) | u32::from(b)))
446}
447
448/// Spec: realme_t310.md#switches
449pub(super) fn decode_switches(reply: &[u8]) -> Result<Vec<Switch>> {
450 let mut r = ok_reply(reply, "the switch reply")?;
451 let mut switches = Vec::new();
452 for _ in 0..r.u8()? {
453 let id = r.u8()?;
454 let value = r.u8()?;
455 if let Some((_, key, label)) = SWITCH_NAMES.iter().find(|(known, _, _)| *known == id) {
456 switches.push(Switch {
457 key,
458 label,
459 on: value == 0x01,
460 });
461 }
462 }
463 Ok(switches)
464}
465
466/// The id of a switch, by its key (`game`…). Spec: realme_t310.md#switches
467pub(super) fn switch_id(key: &str) -> Option<u8> {
468 SWITCH_NAMES.iter().find(|(_, k, _)| *k == key).map(|(id, _, _)| *id)
469}
470
471/// `<id> <01|00>`. Spec: realme_t310.md#switch-write
472pub(super) fn switch_write_payload(id: u8, on: bool) -> [u8; 2] {
473 [id, u8::from(on)]
474}
475
476/// The id of a built-in EQ preset, by name. Spec: realme_t310.md#eq-read
477pub(super) fn eq_id(name: &str) -> Option<u8> {
478 EQ_PRESETS.iter().find(|(_, n)| *n == name).map(|(id, _)| *id)
479}
480
481/// The built-in EQ presets' names, in id order. Spec: realme_t310.md#eq-read
482pub(super) fn eq_names() -> Vec<&'static str> {
483 EQ_PRESETS.iter().map(|(_, name)| *name).collect()
484}
485
486/// Spec: realme_t310.md#eq-read
487pub(super) fn decode_eq(reply: &[u8]) -> Result<String> {
488 let id = ok_reply(reply, "the EQ reply")?.u8()?;
489 Ok(name_of(&EQ_PRESETS, id).map_or_else(|| format!("custom-{id}"), str::to_owned))
490}
491
492/// The built-in EQ presets, in id order. Spec: realme_t310.md#eq-read
493pub(super) fn builtin_eqs() -> Vec<EqPreset> {
494 EQ_PRESETS
495 .iter()
496 .map(|&(id, name)| EqPreset {
497 name: name.to_owned(),
498 id,
499 custom: false,
500 bands: Vec::new(),
501 range: None,
502 })
503 .collect()
504}
505
506/// One custom preset as the list holds it. Spec: realme_t310.md#custom-eq
507#[derive(Debug, Clone, PartialEq, Eq)]
508pub(super) struct CustomEq {
509 pub(super) selected: bool,
510 /// Gain range, dB.
511 pub(super) min: i8,
512 pub(super) max: i8,
513 pub(super) id: u8,
514 /// The stored name, byte for byte, so a save writes it back unchanged.
515 pub(super) name: Vec<u8>,
516 pub(super) bands: Vec<EqBand>,
517}
518
519impl CustomEq {
520 pub(super) fn preset(&self) -> EqPreset {
521 EqPreset {
522 name: String::from_utf8_lossy(&self.name).into_owned(),
523 id: self.id,
524 custom: true,
525 bands: self.bands.clone(),
526 range: Some((self.min, self.max)),
527 }
528 }
529}
530
531/// `00 <count>`, then per preset `<selected> <min> <max> <id> <nameLen> <name> <bands>
532/// (<freq u16 LE> <gain s8>)…`. Spec: realme_t310.md#custom-eq
533pub(super) fn decode_custom_eq_list(reply: &[u8]) -> Result<Vec<CustomEq>> {
534 let mut r = ok_reply(reply, "the custom EQ list")?;
535 let mut presets = Vec::new();
536 for _ in 0..r.u8()? {
537 let selected = r.u8()? == 0x01;
538 let (min, max, id) = (r.i8()?, r.i8()?, r.u8()?);
539 let name_len = r.u8()?;
540 let name = r.take(usize::from(name_len))?.to_vec();
541 let mut bands = Vec::new();
542 for _ in 0..r.u8()? {
543 bands.push(EqBand {
544 hz: r.u16_le()?,
545 gain: r.i8()?,
546 });
547 }
548 presets.push(CustomEq {
549 selected,
550 min,
551 max,
552 id,
553 name,
554 bands,
555 });
556 }
557 Ok(presets)
558}
559
560/// Spec: realme_t310.md#custom-eq
561pub(super) fn decode_custom_eqs(reply: &[u8]) -> Result<Vec<EqPreset>> {
562 Ok(decode_custom_eq_list(reply)?.iter().map(CustomEq::preset).collect())
563}
564
565/// The id in `custom-<id>`, the name [`decode_eq`] gives a custom preset.
566/// Spec: realme_t310.md#eq-read
567pub(super) fn custom_eq_id(name: &str) -> Option<u8> {
568 name.strip_prefix("custom-")?.parse().ok()
569}
570
571/// The preset with the given bands' gains changed, and nothing else. Every band must be one the
572/// preset has, at most once, with a gain inside the preset's range.
573/// Spec: realme_t310.md#custom-eq
574pub(super) fn retune(preset: &CustomEq, changes: &[EqBand]) -> Result<CustomEq> {
575 let mut tuned = preset.clone();
576 for (i, change) in changes.iter().enumerate() {
577 if changes.iter().take(i).any(|earlier| earlier.hz == change.hz) {
578 return Err(Error::Usage(format!("the {} Hz band is given twice", change.hz)));
579 }
580 let Some(band) = tuned.bands.iter_mut().find(|b| b.hz == change.hz) else {
581 let have: Vec<String> = preset.bands.iter().map(|b| b.hz.to_string()).collect();
582 return Err(Error::Usage(format!(
583 "custom-{} has no {} Hz band; its bands are {} Hz",
584 preset.id,
585 change.hz,
586 have.join(", ")
587 )));
588 };
589 if !(preset.min..=preset.max).contains(&change.gain) {
590 return Err(Error::Usage(format!(
591 "a gain of {:+} dB is outside {:+}..{:+} dB",
592 change.gain, preset.min, preset.max
593 )));
594 }
595 band.gain = change.gain;
596 }
597 Ok(tuned)
598}
599
600/// `02 <min> <max> <id> <nameLen> <name> <bands> (<freq u16 LE> <gain s8>)…`: save the whole
601/// preset and select it. Spec: realme_t310.md#custom-eq
602pub(super) fn custom_eq_save_payload(preset: &CustomEq) -> Result<Vec<u8>> {
603 custom_eq_payload(CUSTOM_EQ_SAVE, preset)
604}
605
606/// `01 <min> <max> 00 <nameLen> <name> <bands> …`: create a preset, as [`new_custom_eq`] makes
607/// it. Spec: realme_t310.md#custom-eq-create
608pub(super) fn custom_eq_create_payload(preset: &CustomEq) -> Result<Vec<u8>> {
609 custom_eq_payload(CUSTOM_EQ_CREATE, preset)
610}
611
612/// A preset name as budctl writes it: 1 to [`CUSTOM_EQ_NAME_MAX`] bytes of UTF-8, no control
613/// characters, no spaces around it. Spec: realme_t310.md#custom-eq-create
614pub(super) fn custom_eq_name(name: &str) -> Result<Vec<u8>> {
615 if name.is_empty() || name.trim() != name {
616 return Err(Error::Usage(
617 "a preset name cannot be empty, or start or end with a space".to_owned(),
618 ));
619 }
620 if name.chars().any(char::is_control) {
621 return Err(Error::Usage("a preset name cannot hold control characters".to_owned()));
622 }
623 if name.len() > CUSTOM_EQ_NAME_MAX {
624 return Err(Error::Usage(format!(
625 "a preset name can be at most {CUSTOM_EQ_NAME_MAX} bytes; \"{name}\" has {}",
626 name.len()
627 )));
628 }
629 Ok(name.as_bytes().to_vec())
630}
631
632/// A new preset called `name`: the range and the bands of `model`, a preset the buds have, else
633/// the T310's own, with every gain at 0, and id `00` for the buds to replace.
634/// Spec: realme_t310.md#custom-eq-create
635pub(super) fn new_custom_eq(name: &str, model: Option<&CustomEq>) -> Result<CustomEq> {
636 let ((min, max), bands): ((i8, i8), Vec<u16>) = match model {
637 Some(model) => ((model.min, model.max), model.bands.iter().map(|b| b.hz).collect()),
638 None => (T310_CUSTOM_RANGE, T310_CUSTOM_BANDS.to_vec()),
639 };
640 Ok(CustomEq {
641 selected: false,
642 min,
643 max,
644 id: 0x00,
645 name: custom_eq_name(name)?,
646 bands: bands.into_iter().map(|hz| EqBand { hz, gain: 0 }).collect(),
647 })
648}
649
650/// `03 <min> <max> <id> <nameLen> <name> <bands> …`: delete the preset, sent as the list holds it.
651/// Spec: realme_t310.md#custom-eq-delete
652pub(super) fn custom_eq_delete_payload(preset: &CustomEq) -> Result<Vec<u8>> {
653 custom_eq_payload(CUSTOM_EQ_DELETE, preset)
654}
655
656/// Whether a list read after deleting `gone` from `before` shows it gone: one preset fewer, and
657/// none left with its name and bands. Ids can change, so they prove nothing.
658/// Spec: realme_t310.md#custom-eq-delete
659pub(super) fn deleted(before: &[CustomEq], gone: &CustomEq, now: &[CustomEq]) -> bool {
660 let same = |p: &CustomEq| p.name == gone.name && p.bands == gone.bands;
661 let left_before = before.iter().filter(|p| same(p)).count().saturating_sub(1);
662 now.len() + 1 == before.len() && now.iter().filter(|p| same(p)).count() == left_before
663}
664
665/// The preset under a new name, and nothing else changed: saved with [`custom_eq_save_payload`],
666/// it is renamed. Spec: realme_t310.md#custom-eq-rename
667pub(super) fn renamed(preset: &CustomEq, name: &str) -> Result<CustomEq> {
668 Ok(CustomEq {
669 name: custom_eq_name(name)?,
670 ..preset.clone()
671 })
672}
673
674/// The id in a `0x0418` reply: `00 <id>`. Spec: realme_t310.md#custom-eq
675pub(super) fn decode_custom_eq_write_reply(reply: &[u8]) -> Result<u8> {
676 let mut r = ok_reply(reply, "the custom EQ write")?;
677 r.u8()
678}
679
680/// The preset a create made, in the list read after it: the one with that name and the id the
681/// buds answered, else the only one with that name. Spec: realme_t310.md#custom-eq-create
682pub(super) fn find_created<'a>(list: &'a [CustomEq], name: &[u8], id: u8) -> Option<&'a CustomEq> {
683 let mut named = list.iter().filter(|p| p.name == name);
684 list.iter()
685 .find(|p| p.name == name && p.id == id)
686 .or_else(|| match (named.next(), named.next()) {
687 (Some(only), None) => Some(only),
688 _ => None,
689 })
690}
691
692fn custom_eq_payload(action: u8, preset: &CustomEq) -> Result<Vec<u8>> {
693 let too_long = |what: &str| Error::Protocol(format!("the preset's {what} does not fit in a byte"));
694 let mut payload = vec![action];
695 payload.extend([preset.min.to_le_bytes()[0], preset.max.to_le_bytes()[0], preset.id]);
696 payload.push(u8::try_from(preset.name.len()).map_err(|_| too_long("name"))?);
697 payload.extend(&preset.name);
698 payload.push(u8::try_from(preset.bands.len()).map_err(|_| too_long("band count"))?);
699 for band in &preset.bands {
700 payload.extend(band.hz.to_le_bytes());
701 payload.push(band.gain.to_le_bytes()[0]);
702 }
703 Ok(payload)
704}
705
706/// One gesture-table entry: `<dev> <btn> <act> <fn>`. Spec: realme_t310.md#gestures
707pub(super) type GestureEntry = [u8; 4];
708
709/// The entries of a gesture-table reply, in the order the buds sent them.
710/// Spec: realme_t310.md#gestures
711pub(super) fn decode_gesture_entries(reply: &[u8]) -> Result<Vec<GestureEntry>> {
712 let mut r = ok_reply(reply, "the gesture table")?;
713 let mut entries = Vec::new();
714 for _ in 0..r.u8()? {
715 entries.push([r.u8()?, r.u8()?, r.u8()?, r.u8()?]);
716 }
717 Ok(entries)
718}
719
720/// Spec: realme_t310.md#gestures
721pub(super) fn name_gesture(&[side, group, act, function]: &GestureEntry) -> Gesture {
722 Gesture {
723 side: name_of(&SIDES, side).map_or_else(|| format!("device-0x{side:02X}"), str::to_owned),
724 gesture: name_of(&ACTS, act).map_or_else(|| format!("act-0x{act:02X}"), str::to_owned),
725 action: name_of(&FUNCTIONS, function).map_or_else(|| format!("fn-0x{function:02X}"), str::to_owned),
726 group: (group != MAIN_GROUP).then_some(group),
727 }
728}
729
730/// Spec: realme_t310.md#gestures
731pub(super) fn decode_gestures(reply: &[u8]) -> Result<Vec<Gesture>> {
732 Ok(decode_gesture_entries(reply)?.iter().map(name_gesture).collect())
733}
734
735/// What `touch <side>` changes: `both` is the left and the right bud's entry.
736/// Spec: realme_t310.md#gesture-choices
737const TOUCH_SIDES: [(&str, &[u8]); 3] = [("left", &[0x01]), ("right", &[0x02]), ("both", &[0x01, 0x02])];
738
739/// The actions realme Link offers per gesture on the T310, in its order. budctl offers no
740/// others. Spec: realme_t310.md#gesture-choices
741const TOUCH_CHOICES: [(u8, &[u8]); 3] = [
742 // double tap: none, play/pause, next, voice assistant
743 (0x02, &[0x00, 0x01, 0x06, 0x03]),
744 // triple tap: none, previous, next, voice assistant, volume up, volume down, game mode
745 (0x03, &[0x00, 0x05, 0x06, 0x03, 0x0B, 0x0C, 0x11]),
746 // hold: none, noise-control cycle, voice assistant, game mode
747 (0x04, &[0x00, 0x08, 0x03, 0x11]),
748];
749
750/// The actions each gesture can be given, by name, in realme Link's order.
751/// Spec: realme_t310.md#gesture-choices
752pub(super) fn touch_choices() -> Vec<GestureChoice> {
753 TOUCH_CHOICES
754 .iter()
755 .filter_map(|&(act, functions)| {
756 Some(GestureChoice {
757 gesture: name_of(&ACTS, act)?.to_owned(),
758 actions: functions
759 .iter()
760 .filter_map(|&f| name_of(&FUNCTIONS, f).map(str::to_owned))
761 .collect(),
762 })
763 })
764 .collect()
765}
766
767/// The modes a hold cycle can include, by name. Spec: realme_t310.md#hold-cycle-write
768pub(super) fn cycle_modes() -> Vec<String> {
769 CYCLE_MODES.iter().map(|&(_, name)| name.to_owned()).collect()
770}
771
772/// A checked `touch <side> <gesture> <action>`: the bud ids, the act and the fn to write.
773#[derive(Debug, PartialEq, Eq)]
774pub(super) struct TouchTarget {
775 pub(super) devices: &'static [u8],
776 pub(super) act: u8,
777 pub(super) function: u8,
778}
779
780/// Check the names against what the T310 offers. Every failure is a usage error that lists
781/// the valid names. Spec: realme_t310.md#gesture-choices
782pub(super) fn touch_target(side: &str, gesture: &str, action: &str) -> Result<TouchTarget> {
783 let list = |names: Vec<&str>| names.join(", ");
784 let Some(&(_, devices)) = TOUCH_SIDES.iter().find(|(name, _)| *name == side) else {
785 return Err(Error::Usage(format!(
786 "no side called {side}; use one of: {}",
787 list(TOUCH_SIDES.iter().map(|(name, _)| *name).collect())
788 )));
789 };
790 let gesture_names = || {
791 list(
792 TOUCH_CHOICES
793 .iter()
794 .filter_map(|(act, _)| name_of(&ACTS, *act))
795 .collect(),
796 )
797 };
798 let Some(&(act, choices)) = TOUCH_CHOICES
799 .iter()
800 .find(|(act, _)| name_of(&ACTS, *act) == Some(gesture))
801 else {
802 return Err(Error::Usage(format!(
803 "these buds have no {gesture} gesture to set; use one of: {}",
804 gesture_names()
805 )));
806 };
807 let Some(&function) = choices.iter().find(|f| name_of(&FUNCTIONS, **f) == Some(action)) else {
808 return Err(Error::Usage(format!(
809 "{gesture} cannot be set to {action} on these buds; use one of: {}",
810 list(choices.iter().filter_map(|f| name_of(&FUNCTIONS, *f)).collect())
811 )));
812 };
813 Ok(TouchTarget { devices, act, function })
814}
815
816/// The table with the target's entries changed, and nothing else. Every targeted entry must
817/// already be in the table: budctl never adds entries. Spec: realme_t310.md#gesture-write
818pub(super) fn retarget_gestures(table: &[GestureEntry], target: &TouchTarget) -> Result<Vec<GestureEntry>> {
819 for &device in target.devices {
820 if !table
821 .iter()
822 .any(|&[d, group, act, _]| d == device && group == MAIN_GROUP && act == target.act)
823 {
824 let entry = name_gesture(&[device, MAIN_GROUP, target.act, 0]);
825 return Err(Error::Unsupported(format!(
826 "the buds report no {} {} entry, so budctl will not write one",
827 entry.side, entry.gesture
828 )));
829 }
830 }
831 Ok(table
832 .iter()
833 .map(|&[device, group, act, function]| {
834 if target.devices.contains(&device) && group == MAIN_GROUP && act == target.act {
835 [device, group, act, target.function]
836 } else {
837 [device, group, act, function]
838 }
839 })
840 .collect())
841}
842
843/// `<count> (<dev> <btn> <act> <fn>)…`: the whole table. Spec: realme_t310.md#gesture-write
844pub(super) fn gesture_write_payload(table: &[GestureEntry]) -> Result<Vec<u8>> {
845 let count = u8::try_from(table.len())
846 .map_err(|_| Error::Protocol(format!("a gesture table of {} entries does not fit", table.len())))?;
847 let mut payload = vec![count];
848 payload.extend(table.iter().flatten());
849 Ok(payload)
850}
851
852/// The handshake reply of the owner's T310 (HW1). Spec: realme_t310.md#bitmap-t310
853#[cfg(test)]
854pub(super) const T310_BITMAP_REPLY: &str = "00 BF 1F 5A 20 45 04 40 00";
855
856#[cfg(test)]
857mod tests {
858 use super::*;
859 use crate::transport::mock::hex;
860
861 #[test]
862 fn t310_bitmap_allows_its_commands_and_nothing_else() {
863 let commands = Commands::parse(&hex(T310_BITMAP_REPLY)).unwrap();
864 // Every gated command in the T310's logged command set (realme_t310.md#bitmap-t310).
865 for cmd in [
866 0x0100, 0x0103, 0x0105, 0x0106, 0x0108, 0x0109, 0x010C, 0x010D, 0x010F, 0x0114, 0x0122, 0x0124, 0x0205,
867 0x0400, 0x0401, 0x0403, 0x0404, 0x0405, 0x0406, 0x0418, 0x041B, 0x0200,
868 ] {
869 assert!(commands.allows(cmd), "0x{cmd:04X} should be allowed");
870 }
871 // Commands the T310 lacks, and commands budctl has no gate for.
872 for cmd in [0x012F, 0x041E, 0x0413, 0x0130, 0x0427, 0x0423, 0x012A, 0x0422, 0x9999] {
873 assert!(!commands.allows(cmd), "0x{cmd:04X} should be refused");
874 }
875 }
876
877 #[test]
878 fn a_short_bitmap_allows_only_the_always_commands() {
879 let commands = Commands::parse(&hex("00")).unwrap();
880 assert!(commands.allows(BATTERY) && commands.allows(SWITCHES) && commands.allows(HANDSHAKE));
881 assert!(!commands.allows(FIRMWARE) && !commands.allows(NOISE_CONTROL));
882 assert!(matches!(
883 Commands::parse(&hex("01 FF")),
884 Err(Error::Rejected { status: 1, .. })
885 ));
886 }
887
888 #[test]
889 fn product_id_is_three_bytes_little_endian() {
890 assert_eq!(decode_product_id(&hex("00 12 3C 06")).unwrap(), T310_PRODUCT_ID);
891 assert_eq!(decode_product_id(&hex("00 14 54 06")).unwrap(), 0x06_54_14);
892 assert!(decode_product_id(&hex("00 12 3C")).is_err());
893 }
894
895 #[test]
896 fn subscribes_to_offered_events_only() {
897 let commands = Commands::parse(&hex(T310_BITMAP_REPLY)).unwrap();
898 let buds4_offer = hex("00 09 01 02 03 04 08 0B F1 F2 F3");
899 let offered = decode_events_offered(&buds4_offer).unwrap();
900 assert_eq!(subscribe_payload(&offered, &commands), hex("04 01 02 03 04"));
901 assert_eq!(subscribe_payload(&[0x02, 0x01], &commands), hex("02 01 02"));
902 let no_fit_test = Commands::parse(&hex("00 BD 0D")).unwrap();
903 assert_eq!(subscribe_payload(&offered, &no_fit_test), hex("03 01 02 03"));
904 }
905
906 #[test]
907 fn battery_levels_charging_and_unknown_parts() {
908 let battery = decode_battery(&hex("00 03 01 64 02 E4 03 00")).unwrap();
909 assert_eq!(
910 battery.left,
911 Some(Charge {
912 level: 100,
913 charging: false
914 })
915 );
916 assert_eq!(
917 battery.right,
918 Some(Charge {
919 level: 100,
920 charging: true
921 })
922 );
923 assert_eq!(battery.case, None);
924 assert_eq!(decode_battery(&hex("00 00")).unwrap(), Battery::default());
925 assert!(decode_battery(&hex("00 03 01 64")).is_err());
926 }
927
928 #[test]
929 fn firmware_shows_kind_two_versions() {
930 let mut reply = hex("00 04");
931 reply.extend(b"1,2,138,2,2,138,3,1,01,3,2,105");
932 assert_eq!(decode_firmware(&reply).unwrap(), "138.138.105");
933 // The owner's T310 (HW1): both buds report the same full version.
934 let mut t310 = hex("00 02");
935 t310.extend(b"1,2,1.1.0.43,2,2,1.1.0.43");
936 assert_eq!(decode_firmware(&t310).unwrap(), "1.1.0.43");
937 let mut differ = hex("00 02");
938 differ.extend(b"1,2,1.1.0.43,2,2,1.1.0.44");
939 assert_eq!(decode_firmware(&differ).unwrap(), "1.1.0.43 / 1.1.0.44");
940 let mut odd = hex("00 01");
941 odd.extend(b"V1.0.2");
942 assert_eq!(decode_firmware(&odd).unwrap(), "V1.0.2");
943 }
944
945 #[test]
946 fn noise_control_modes() {
947 let mode = |reply: &str| decode_noise_mode(&hex(reply)).unwrap();
948 assert_eq!(mode("00 01 01 01").mode, "off");
949 assert_eq!(mode("00 01 01 02 00").mode, "transparency");
950 let deep = mode("00 01 01 08");
951 assert_eq!((deep.mode.as_str(), deep.level.as_deref()), ("anc", Some("deep")));
952 assert_eq!(mode("00 01 01 10").level.as_deref(), Some("medium"));
953 assert_eq!(mode("00 01 01 00 08").mode, "unknown (0x800)");
954 assert!(decode_noise_mode(&hex("00 02 01 08")).is_err());
955 assert!(decode_noise_mode(&hex("00 01 01")).is_err());
956 assert!(matches!(
957 decode_noise_mode(&hex("0E 01 01 08")),
958 Err(Error::Rejected { status: 0x0E, .. })
959 ));
960 }
961
962 #[test]
963 fn noise_control_write_payloads() {
964 assert_eq!(noise_write_payload(NoiseMode::Off), hex("01 01 01"));
965 assert_eq!(noise_write_payload(NoiseMode::Transparency), hex("01 01 02"));
966 assert_eq!(noise_write_payload(NoiseMode::Anc(AncLevel::Light)), hex("01 01 04"));
967 assert_eq!(noise_write_payload(NoiseMode::Anc(AncLevel::Medium)), hex("01 01 10"));
968 assert_eq!(noise_write_payload(NoiseMode::Anc(AncLevel::Deep)), hex("01 01 08"));
969 // What is written is what a read reports: every write mask decodes to its own mode.
970 for level in [AncLevel::Light, AncLevel::Medium, AncLevel::Deep] {
971 let mut reply = hex("00 01 01");
972 reply.extend(noise_write_payload(NoiseMode::Anc(level)).iter().skip(2));
973 let read = decode_noise_mode(&reply).unwrap();
974 assert_eq!((read.mode.as_str(), read.level.as_deref()), ("anc", Some(level.name())));
975 }
976 // The family's documented frame for "off" (realme_t310.md#frame-whole).
977 let frame =
978 crate::devices::realme_t310::frame::encode(NOISE_CONTROL_WRITE, 0x06, &noise_write_payload(NoiseMode::Off))
979 .unwrap();
980 assert_eq!(frame, hex("AA 0A 00 00 04 04 06 03 00 01 01 01"));
981 }
982
983 #[test]
984 fn switch_writes() {
985 assert_eq!(switch_id("game"), Some(0x06));
986 assert_eq!(switch_id("dual"), Some(0x11));
987 assert_eq!(switch_id("turbo"), None);
988 assert_eq!(switch_write_payload(0x06, true), [0x06, 0x01]);
989 assert_eq!(switch_write_payload(0x06, false), [0x06, 0x00]);
990 }
991
992 #[test]
993 fn eq_presets_by_name() {
994 assert_eq!(eq_id("pure-raw"), Some(0x00));
995 assert_eq!(eq_id("pure-bass"), Some(0x03));
996 assert_eq!(eq_id("custom-7"), None);
997 assert_eq!(eq_names(), ["pure-raw", "deep-bass", "pure-vocals", "pure-bass"]);
998 // Every preset reads back under the name it was selected by.
999 for name in eq_names() {
1000 let reply = [0x00, eq_id(name).unwrap()];
1001 assert_eq!(decode_eq(&reply).unwrap(), name);
1002 }
1003 }
1004
1005 #[test]
1006 fn find_payloads() {
1007 assert_eq!(find_payload(true), [0x01]);
1008 assert_eq!(find_payload(false), [0x00]);
1009 }
1010
1011 #[test]
1012 fn write_replies() {
1013 assert!(check_write_reply(&hex("00"), "the write").is_ok());
1014 assert!(matches!(
1015 check_write_reply(&hex("0E"), "the write"),
1016 Err(Error::Rejected { status: 0x0E, .. })
1017 ));
1018 assert!(check_write_reply(&[], "the write").is_err());
1019 }
1020
1021 #[test]
1022 fn hold_cycle_lists_modes() {
1023 assert_eq!(decode_hold_cycle(&hex("00 02 01 0A")).unwrap(), ["transparency", "anc"]);
1024 assert_eq!(
1025 decode_hold_cycle(&hex("00 02 01 0B 00")).unwrap(),
1026 ["off", "transparency", "anc"]
1027 );
1028 assert_eq!(decode_hold_cycle(&hex("00 02 01 14")).unwrap(), ["unknown (0x14)"]);
1029 }
1030
1031 #[test]
1032 fn switches_from_report_22_values() {
1033 let switches = decode_switches(&hex("00 06 05 00 06 00 11 01 1A 00 1B 01 1D 01")).unwrap();
1034 let on: Vec<(&str, bool)> = switches.iter().map(|s| (s.key, s.on)).collect();
1035 assert_eq!(
1036 on,
1037 [
1038 ("game", false),
1039 ("dual", true),
1040 ("wind", false),
1041 ("spatial", true),
1042 ("bass-boost", true)
1043 ]
1044 );
1045 assert!(decode_switches(&hex("00 02 06 01")).is_err());
1046 }
1047
1048 #[test]
1049 fn eq_and_codec() {
1050 assert_eq!(decode_eq(&hex("00 00")).unwrap(), "pure-raw");
1051 assert_eq!(decode_eq(&hex("00 03")).unwrap(), "pure-bass");
1052 assert_eq!(decode_eq(&hex("00 05")).unwrap(), "custom-5");
1053 assert_eq!(decode_codec(&hex("00 02")).unwrap(), "AAC");
1054 assert_eq!(decode_codec(&hex("00 09")).unwrap(), "unknown (0x09)");
1055 }
1056
1057 #[test]
1058 fn gesture_table_names_known_values() {
1059 let gestures = decode_gestures(&hex("00 03 01 01 02 01 02 01 04 08 04 06 02 1D")).unwrap();
1060 let rows: Vec<(&str, &str, &str, Option<u8>)> = gestures
1061 .iter()
1062 .map(|g| (g.side.as_str(), g.gesture.as_str(), g.action.as_str(), g.group))
1063 .collect();
1064 assert_eq!(
1065 rows,
1066 [
1067 ("left", "double-tap", "play-pause", None),
1068 ("right", "hold", "noise-cycle", None),
1069 ("both", "double-tap", "answer-call", Some(6)),
1070 ]
1071 );
1072 let odd = decode_gestures(&hex("00 01 09 01 07 42")).unwrap();
1073 assert_eq!(odd.first().map(|g| g.action.as_str()), Some("fn-0x42"));
1074 assert!(decode_gestures(&hex("00 02 01 01 02 01")).is_err());
1075 }
1076
1077 /// The owner's table (HW1): per bud, double tap play/pause, triple tap next, hold cycle.
1078 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";
1079
1080 #[test]
1081 fn touch_targets_follow_the_t310_choices() {
1082 assert_eq!(
1083 touch_target("left", "triple-tap", "volume-up").unwrap(),
1084 TouchTarget {
1085 devices: &[0x01],
1086 act: 0x03,
1087 function: 0x0B
1088 }
1089 );
1090 assert_eq!(touch_target("both", "hold", "none").unwrap().devices, [0x01, 0x02]);
1091 let usage = |side, gesture, action| match touch_target(side, gesture, action) {
1092 Err(Error::Usage(message)) => message,
1093 other => panic!("expected a usage error, got {other:?}"),
1094 };
1095 assert!(usage("middle", "hold", "none").contains("left, right, both"));
1096 assert!(usage("left", "single-tap", "none").contains("double-tap, triple-tap, hold"));
1097 // Offered by the family, but not by realme Link for this gesture.
1098 assert!(usage("left", "double-tap", "volume-up").contains("none, play-pause, next, voice-assistant"));
1099 assert!(usage("left", "hold", "previous").contains("none, noise-cycle, voice-assistant, game-mode"));
1100 }
1101
1102 #[test]
1103 fn retargeting_changes_only_the_targeted_entries() {
1104 let table = decode_gesture_entries(&hex(HW1_GESTURES)).unwrap();
1105 let left = touch_target("left", "triple-tap", "volume-up").unwrap();
1106 let payload = gesture_write_payload(&retarget_gestures(&table, &left).unwrap()).unwrap();
1107 assert_eq!(
1108 payload,
1109 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")
1110 );
1111 // The frame the spec gives for this change (realme_t310.md#gesture-write).
1112 let frame = crate::devices::realme_t310::frame::encode(GESTURE_WRITE, 0x06, &payload).unwrap();
1113 assert_eq!(
1114 crate::debug::hex(&frame),
1115 "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"
1116 );
1117 let both = touch_target("both", "hold", "game-mode").unwrap();
1118 let payload = gesture_write_payload(&retarget_gestures(&table, &both).unwrap()).unwrap();
1119 assert_eq!(
1120 payload,
1121 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")
1122 );
1123 // Unchanged, the payload is the reply without its status byte.
1124 let same = touch_target("right", "double-tap", "play-pause").unwrap();
1125 let payload = gesture_write_payload(&retarget_gestures(&table, &same).unwrap()).unwrap();
1126 assert_eq!(payload, hex(HW1_GESTURES).get(1..).unwrap());
1127 }
1128
1129 #[test]
1130 fn entries_the_buds_did_not_report_are_never_written() {
1131 // Only the left bud's entries.
1132 let table = decode_gesture_entries(&hex("00 03 01 01 02 01 01 01 03 06 01 01 04 08")).unwrap();
1133 let both = touch_target("both", "hold", "none").unwrap();
1134 assert!(matches!(retarget_gestures(&table, &both), Err(Error::Unsupported(_))));
1135 // An on-call row (group 06) is not a main-group entry.
1136 let table = decode_gesture_entries(&hex("00 01 01 06 02 1D")).unwrap();
1137 let left = touch_target("left", "double-tap", "none").unwrap();
1138 assert!(matches!(retarget_gestures(&table, &left), Err(Error::Unsupported(_))));
1139 }
1140
1141 #[test]
1142 fn hold_cycle_writes() {
1143 assert_eq!(hold_cycle_mask(&["transparency", "anc"]).unwrap(), 0x0A);
1144 assert_eq!(hold_cycle_mask(&["anc", "off", "transparency"]).unwrap(), 0x0B);
1145 assert_eq!(hold_cycle_write_payload(0x0B), hex("02 01 0B"));
1146 assert_eq!(hold_cycle_write_payload(0x09), hex("02 01 09"));
1147 let usage = |modes: &[&str]| matches!(hold_cycle_mask(modes), Err(Error::Usage(_)));
1148 assert!(usage(&["anc"]));
1149 assert!(usage(&[]));
1150 assert!(usage(&["anc", "anc"]));
1151 assert!(usage(&["anc", "deep"]));
1152 // What is written reads back as the same modes.
1153 let mut reply = hex("00");
1154 reply.extend(hold_cycle_write_payload(0x0B));
1155 assert_eq!(decode_hold_cycle_mask(&reply).unwrap(), 0x0B);
1156 assert_eq!(decode_hold_cycle(&reply).unwrap(), ["off", "transparency", "anc"]);
1157 // The frame the spec gives (realme_t310.md#hold-cycle-write).
1158 let frame =
1159 crate::devices::realme_t310::frame::encode(NOISE_CONTROL_WRITE, 0x06, &hold_cycle_write_payload(0x0B))
1160 .unwrap();
1161 assert_eq!(crate::debug::hex(&frame), "AA 0A 00 00 04 04 06 03 00 02 01 0B");
1162 }
1163
1164 #[test]
1165 fn custom_eq_list() {
1166 assert!(decode_custom_eqs(&hex("00 00")).unwrap().is_empty());
1167 // One preset "Me", selected, range FA 06, two bands: 62 Hz +3, 16 kHz -2.
1168 let reply = hex("00 01 01 FA 06 04 02 4D 65 02 3E 00 03 80 3E FE");
1169 let presets = decode_custom_eqs(&reply).unwrap();
1170 assert_eq!(
1171 presets,
1172 [EqPreset {
1173 name: "Me".to_owned(),
1174 id: 0x04,
1175 custom: true,
1176 bands: vec![EqBand { hz: 62, gain: 3 }, EqBand { hz: 16000, gain: -2 }],
1177 range: Some((-6, 6)),
1178 }]
1179 );
1180 // Cut short inside the bands.
1181 assert!(decode_custom_eqs(&hex("00 01 01 FA 06 04 02 4D 65 02 3E 00")).is_err());
1182 assert_eq!(builtin_eqs().len(), 4);
1183 }
1184
1185 #[test]
1186 fn bass_level_payloads() {
1187 assert_eq!(bass_write_payload(0).unwrap(), [0xFB, 0x05, 0x00]);
1188 assert_eq!(bass_write_payload(5).unwrap(), [0xFB, 0x05, 0x05]);
1189 assert_eq!(bass_write_payload(-2).unwrap(), [0xFB, 0x05, 0xFE]);
1190 assert_eq!(bass_write_payload(-5).unwrap(), [0xFB, 0x05, 0xFB]);
1191 assert!(matches!(bass_write_payload(6), Err(Error::Usage(_))));
1192 assert!(matches!(bass_write_payload(-6), Err(Error::Usage(_))));
1193 // The frame the spec gives (realme_t310.md#bass-write).
1194 let frame = crate::devices::realme_t310::frame::encode(BASS_LEVEL_WRITE, 0x05, &bass_write_payload(3).unwrap())
1195 .unwrap();
1196 assert_eq!(crate::debug::hex(&frame), "AA 0A 00 00 1B 04 05 03 00 FB 05 03");
1197 }
1198
1199 /// The owner's custom EQ list (W11a): "Custom1", id 04, 6 flat bands from 50 Hz.
1200 const OWNER_CUSTOM_EQS: &str =
1201 "00 01 01 FA 06 04 07 43 75 73 74 6F 6D 31 06 32 00 00 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E 00";
1202
1203 #[test]
1204 fn retunes_the_owners_custom_preset() {
1205 let list = decode_custom_eq_list(&hex(OWNER_CUSTOM_EQS)).unwrap();
1206 let custom1 = list.first().unwrap();
1207 assert!(custom1.selected);
1208 assert_eq!((custom1.min, custom1.max, custom1.id), (-6, 6, 4));
1209 assert_eq!(custom_eq_id("custom-4"), Some(4));
1210 assert_eq!(custom_eq_id("pure-bass"), None);
1211 assert_eq!(custom_eq_id("custom-x"), None);
1212
1213 // Unchanged, the save carries the preset as listed.
1214 assert_eq!(
1215 custom_eq_save_payload(&retune(custom1, &[]).unwrap()).unwrap(),
1216 hex("02 FA 06 04 07 43 75 73 74 6F 6D 31 06 32 00 00 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E 00")
1217 );
1218 let tuned = retune(custom1, &[EqBand { hz: 50, gain: 3 }, EqBand { hz: 16000, gain: -2 }]).unwrap();
1219 let payload = custom_eq_save_payload(&tuned).unwrap();
1220 assert_eq!(
1221 payload,
1222 hex("02 FA 06 04 07 43 75 73 74 6F 6D 31 06 32 00 03 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E FE")
1223 );
1224 // The frame the spec gives (realme_t310.md#custom-eq).
1225 let frame = crate::devices::realme_t310::frame::encode(CUSTOM_EQ_WRITE, 0x06, &payload).unwrap();
1226 assert_eq!(
1227 crate::debug::hex(&frame),
1228 "AA 26 00 00 18 04 06 1F 00 02 FA 06 04 07 43 75 73 74 6F 6D 31 06 32 00 03 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E FE"
1229 );
1230
1231 let usage = |changes: &[EqBand]| matches!(retune(custom1, changes), Err(Error::Usage(_)));
1232 assert!(usage(&[EqBand { hz: 62, gain: 1 }]));
1233 assert!(usage(&[EqBand { hz: 50, gain: 7 }]));
1234 assert!(usage(&[EqBand { hz: 50, gain: -7 }]));
1235 assert!(usage(&[EqBand { hz: 50, gain: 1 }, EqBand { hz: 50, gain: 2 }]));
1236 }
1237
1238 #[test]
1239 fn creates_a_flat_copy_of_the_owners_preset() {
1240 let list = decode_custom_eq_list(&hex(OWNER_CUSTOM_EQS)).unwrap();
1241 let new = new_custom_eq("Test", list.first()).unwrap();
1242 // With no preset to copy, the T310's own layout: the same, here.
1243 assert_eq!(new_custom_eq("Test", None).unwrap(), new);
1244 assert_eq!((new.id, new.min, new.max), (0x00, -6, 6));
1245 assert!(new.bands.iter().all(|b| b.gain == 0));
1246 let hz: Vec<u16> = new.bands.iter().map(|b| b.hz).collect();
1247 assert_eq!(hz, [50, 250, 1000, 4000, 8000, 16000]);
1248 // Spec: realme_t310.md#custom-eq-create (the expected frame, with seq 06)
1249 let frame =
1250 crate::devices::realme_t310::frame::encode(CUSTOM_EQ_WRITE, 0x06, &custom_eq_create_payload(&new).unwrap())
1251 .unwrap();
1252 assert_eq!(
1253 frame,
1254 hex("AA 23 00 00 18 04 06 1C 00 01 FA 06 00 04 54 65 73 74 06 \
1255 32 00 00 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E 00")
1256 );
1257 }
1258
1259 /// The owner's list after W11c: Custom1 (id 04) and Test (id 05, flat, selected).
1260 const OWNER_AFTER_W11C: &str = "00 02 00 FA 06 04 07 43 75 73 74 6F 6D 31 06 32 00 06 FA 00 03 E8 03 03 A0 0F 04 40 1F 06 80 3E 06 \
1261 01 FA 06 05 04 54 65 73 74 06 32 00 00 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E 00";
1262
1263 #[test]
1264 fn renames_the_owners_new_preset() {
1265 let list = decode_custom_eq_list(&hex(OWNER_AFTER_W11C)).unwrap();
1266 let test = list.iter().find(|p| p.id == 0x05).unwrap();
1267 let gym = renamed(test, "Gym").unwrap();
1268 assert_eq!(
1269 (gym.id, gym.bands.clone(), gym.name.clone()),
1270 (0x05, test.bands.clone(), b"Gym".to_vec())
1271 );
1272 // Spec: realme_t310.md#custom-eq-rename (the expected frame, with seq 06)
1273 let frame =
1274 crate::devices::realme_t310::frame::encode(CUSTOM_EQ_WRITE, 0x06, &custom_eq_save_payload(&gym).unwrap())
1275 .unwrap();
1276 assert_eq!(
1277 frame,
1278 hex("AA 22 00 00 18 04 06 1B 00 02 FA 06 05 03 47 79 6D 06 \
1279 32 00 00 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E 00")
1280 );
1281 assert!(renamed(test, "").is_err());
1282 }
1283
1284 #[test]
1285 fn deletes_the_owners_preset_as_listed() {
1286 let list = decode_custom_eq_list(&hex(OWNER_AFTER_W11C)).unwrap();
1287 let mut gym = list.iter().find(|p| p.id == 0x05).unwrap().clone();
1288 gym.name = b"Gym".to_vec();
1289 // Spec: realme_t310.md#custom-eq-delete (the expected frame, with seq 06)
1290 let frame =
1291 crate::devices::realme_t310::frame::encode(CUSTOM_EQ_WRITE, 0x06, &custom_eq_delete_payload(&gym).unwrap())
1292 .unwrap();
1293 assert_eq!(
1294 frame,
1295 hex("AA 22 00 00 18 04 06 1B 00 03 FA 06 05 03 47 79 6D 06 \
1296 32 00 00 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E 00")
1297 );
1298 let before = vec![list[0].clone(), gym.clone()];
1299 assert!(deleted(&before, &gym, &before[..1]));
1300 assert!(!deleted(&before, &gym, &before));
1301 assert!(!deleted(&before, &gym, &before[1..]), "the wrong one went");
1302 }
1303
1304 #[test]
1305 fn preset_names() {
1306 assert_eq!(custom_eq_name("Gym").unwrap(), b"Gym");
1307 assert_eq!(custom_eq_name("Bass é").unwrap().len(), 7);
1308 for bad in ["", " Gym", "Gym ", "a\tb", "seventeen bytes!!"] {
1309 assert!(matches!(custom_eq_name(bad), Err(Error::Usage(_))), "{bad:?}");
1310 }
1311 }
1312
1313 #[test]
1314 fn finds_the_created_preset() {
1315 let preset = |id, name: &str| CustomEq {
1316 selected: false,
1317 min: -6,
1318 max: 6,
1319 id,
1320 name: name.as_bytes().to_vec(),
1321 bands: Vec::new(),
1322 };
1323 let list = [preset(4, "Custom1"), preset(5, "Test")];
1324 assert_eq!(find_created(&list, b"Test", 5).map(|p| p.id), Some(5));
1325 // Renumbered after the reply: the name still finds it.
1326 assert_eq!(find_created(&list, b"Test", 9).map(|p| p.id), Some(5));
1327 assert!(find_created(&list, b"Other", 5).is_none());
1328 let twins = [preset(4, "Test"), preset(5, "Test")];
1329 assert_eq!(find_created(&twins, b"Test", 5).map(|p| p.id), Some(5));
1330 assert!(find_created(&twins, b"Test", 9).is_none());
1331 assert_eq!(decode_custom_eq_write_reply(&hex("00 05")).unwrap(), 5);
1332 assert!(decode_custom_eq_write_reply(&hex("01 05")).is_err());
1333 }
1334}