Krolyxon
Commit 84c42fc

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310codec.rs

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