Krolyxon
Commit f5d5748

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310codec.rs

1251 lines · 48.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 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 id in a `0x0418` reply: `00 <id>`. Spec: realme_t310.md#custom-eq
639pub(super) fn decode_custom_eq_write_reply(reply: &[u8]) -> Result<u8> {
640 let mut r = ok_reply(reply, "the custom EQ write")?;
641 r.u8()
642}
643
644/// The preset a create made, in the list read after it: the one with that name and the id the
645/// buds answered, else the only one with that name. Spec: realme_t310.md#custom-eq-create
646pub(super) fn find_created<'a>(list: &'a [CustomEq], name: &[u8], id: u8) -> Option<&'a CustomEq> {
647 let mut named = list.iter().filter(|p| p.name == name);
648 list.iter()
649 .find(|p| p.name == name && p.id == id)
650 .or_else(|| match (named.next(), named.next()) {
651 (Some(only), None) => Some(only),
652 _ => None,
653 })
654}
655
656fn custom_eq_payload(action: u8, preset: &CustomEq) -> Result<Vec<u8>> {
657 let too_long = |what: &str| Error::Protocol(format!("the preset's {what} does not fit in a byte"));
658 let mut payload = vec![action];
659 payload.extend([preset.min.to_le_bytes()[0], preset.max.to_le_bytes()[0], preset.id]);
660 payload.push(u8::try_from(preset.name.len()).map_err(|_| too_long("name"))?);
661 payload.extend(&preset.name);
662 payload.push(u8::try_from(preset.bands.len()).map_err(|_| too_long("band count"))?);
663 for band in &preset.bands {
664 payload.extend(band.hz.to_le_bytes());
665 payload.push(band.gain.to_le_bytes()[0]);
666 }
667 Ok(payload)
668}
669
670/// One gesture-table entry: `<dev> <btn> <act> <fn>`. Spec: realme_t310.md#gestures
671pub(super) type GestureEntry = [u8; 4];
672
673/// The entries of a gesture-table reply, in the order the buds sent them.
674/// Spec: realme_t310.md#gestures
675pub(super) fn decode_gesture_entries(reply: &[u8]) -> Result<Vec<GestureEntry>> {
676 let mut r = ok_reply(reply, "the gesture table")?;
677 let mut entries = Vec::new();
678 for _ in 0..r.u8()? {
679 entries.push([r.u8()?, r.u8()?, r.u8()?, r.u8()?]);
680 }
681 Ok(entries)
682}
683
684/// Spec: realme_t310.md#gestures
685pub(super) fn name_gesture(&[side, group, act, function]: &GestureEntry) -> Gesture {
686 Gesture {
687 side: name_of(&SIDES, side).map_or_else(|| format!("device-0x{side:02X}"), str::to_owned),
688 gesture: name_of(&ACTS, act).map_or_else(|| format!("act-0x{act:02X}"), str::to_owned),
689 action: name_of(&FUNCTIONS, function).map_or_else(|| format!("fn-0x{function:02X}"), str::to_owned),
690 group: (group != MAIN_GROUP).then_some(group),
691 }
692}
693
694/// Spec: realme_t310.md#gestures
695pub(super) fn decode_gestures(reply: &[u8]) -> Result<Vec<Gesture>> {
696 Ok(decode_gesture_entries(reply)?.iter().map(name_gesture).collect())
697}
698
699/// What `touch <side>` changes: `both` is the left and the right bud's entry.
700/// Spec: realme_t310.md#gesture-choices
701const TOUCH_SIDES: [(&str, &[u8]); 3] = [("left", &[0x01]), ("right", &[0x02]), ("both", &[0x01, 0x02])];
702
703/// The actions realme Link offers per gesture on the T310, in its order. budctl offers no
704/// others. Spec: realme_t310.md#gesture-choices
705const TOUCH_CHOICES: [(u8, &[u8]); 3] = [
706 // double tap: none, play/pause, next, voice assistant
707 (0x02, &[0x00, 0x01, 0x06, 0x03]),
708 // triple tap: none, previous, next, voice assistant, volume up, volume down, game mode
709 (0x03, &[0x00, 0x05, 0x06, 0x03, 0x0B, 0x0C, 0x11]),
710 // hold: none, noise-control cycle, voice assistant, game mode
711 (0x04, &[0x00, 0x08, 0x03, 0x11]),
712];
713
714/// The actions each gesture can be given, by name, in realme Link's order.
715/// Spec: realme_t310.md#gesture-choices
716pub(super) fn touch_choices() -> Vec<GestureChoice> {
717 TOUCH_CHOICES
718 .iter()
719 .filter_map(|&(act, functions)| {
720 Some(GestureChoice {
721 gesture: name_of(&ACTS, act)?.to_owned(),
722 actions: functions
723 .iter()
724 .filter_map(|&f| name_of(&FUNCTIONS, f).map(str::to_owned))
725 .collect(),
726 })
727 })
728 .collect()
729}
730
731/// The modes a hold cycle can include, by name. Spec: realme_t310.md#hold-cycle-write
732pub(super) fn cycle_modes() -> Vec<String> {
733 CYCLE_MODES.iter().map(|&(_, name)| name.to_owned()).collect()
734}
735
736/// A checked `touch <side> <gesture> <action>`: the bud ids, the act and the fn to write.
737#[derive(Debug, PartialEq, Eq)]
738pub(super) struct TouchTarget {
739 pub(super) devices: &'static [u8],
740 pub(super) act: u8,
741 pub(super) function: u8,
742}
743
744/// Check the names against what the T310 offers. Every failure is a usage error that lists
745/// the valid names. Spec: realme_t310.md#gesture-choices
746pub(super) fn touch_target(side: &str, gesture: &str, action: &str) -> Result<TouchTarget> {
747 let list = |names: Vec<&str>| names.join(", ");
748 let Some(&(_, devices)) = TOUCH_SIDES.iter().find(|(name, _)| *name == side) else {
749 return Err(Error::Usage(format!(
750 "no side called {side}; use one of: {}",
751 list(TOUCH_SIDES.iter().map(|(name, _)| *name).collect())
752 )));
753 };
754 let gesture_names = || {
755 list(
756 TOUCH_CHOICES
757 .iter()
758 .filter_map(|(act, _)| name_of(&ACTS, *act))
759 .collect(),
760 )
761 };
762 let Some(&(act, choices)) = TOUCH_CHOICES
763 .iter()
764 .find(|(act, _)| name_of(&ACTS, *act) == Some(gesture))
765 else {
766 return Err(Error::Usage(format!(
767 "these buds have no {gesture} gesture to set; use one of: {}",
768 gesture_names()
769 )));
770 };
771 let Some(&function) = choices.iter().find(|f| name_of(&FUNCTIONS, **f) == Some(action)) else {
772 return Err(Error::Usage(format!(
773 "{gesture} cannot be set to {action} on these buds; use one of: {}",
774 list(choices.iter().filter_map(|f| name_of(&FUNCTIONS, *f)).collect())
775 )));
776 };
777 Ok(TouchTarget { devices, act, function })
778}
779
780/// The table with the target's entries changed, and nothing else. Every targeted entry must
781/// already be in the table: budctl never adds entries. Spec: realme_t310.md#gesture-write
782pub(super) fn retarget_gestures(table: &[GestureEntry], target: &TouchTarget) -> Result<Vec<GestureEntry>> {
783 for &device in target.devices {
784 if !table
785 .iter()
786 .any(|&[d, group, act, _]| d == device && group == MAIN_GROUP && act == target.act)
787 {
788 let entry = name_gesture(&[device, MAIN_GROUP, target.act, 0]);
789 return Err(Error::Unsupported(format!(
790 "the buds report no {} {} entry, so budctl will not write one",
791 entry.side, entry.gesture
792 )));
793 }
794 }
795 Ok(table
796 .iter()
797 .map(|&[device, group, act, function]| {
798 if target.devices.contains(&device) && group == MAIN_GROUP && act == target.act {
799 [device, group, act, target.function]
800 } else {
801 [device, group, act, function]
802 }
803 })
804 .collect())
805}
806
807/// `<count> (<dev> <btn> <act> <fn>)…`: the whole table. Spec: realme_t310.md#gesture-write
808pub(super) fn gesture_write_payload(table: &[GestureEntry]) -> Result<Vec<u8>> {
809 let count = u8::try_from(table.len())
810 .map_err(|_| Error::Protocol(format!("a gesture table of {} entries does not fit", table.len())))?;
811 let mut payload = vec![count];
812 payload.extend(table.iter().flatten());
813 Ok(payload)
814}
815
816/// The handshake reply of the owner's T310 (HW1). Spec: realme_t310.md#bitmap-t310
817#[cfg(test)]
818pub(super) const T310_BITMAP_REPLY: &str = "00 BF 1F 5A 20 45 04 40 00";
819
820#[cfg(test)]
821mod tests {
822 use super::*;
823 use crate::transport::mock::hex;
824
825 #[test]
826 fn t310_bitmap_allows_its_commands_and_nothing_else() {
827 let commands = Commands::parse(&hex(T310_BITMAP_REPLY)).unwrap();
828 // Every gated command in the T310's logged command set (realme_t310.md#bitmap-t310).
829 for cmd in [
830 0x0100, 0x0103, 0x0105, 0x0106, 0x0108, 0x0109, 0x010C, 0x010D, 0x010F, 0x0114, 0x0122, 0x0124, 0x0205,
831 0x0400, 0x0401, 0x0403, 0x0404, 0x0405, 0x0406, 0x0418, 0x041B, 0x0200,
832 ] {
833 assert!(commands.allows(cmd), "0x{cmd:04X} should be allowed");
834 }
835 // Commands the T310 lacks, and commands budctl has no gate for.
836 for cmd in [0x012F, 0x041E, 0x0413, 0x0130, 0x0427, 0x0423, 0x012A, 0x0422, 0x9999] {
837 assert!(!commands.allows(cmd), "0x{cmd:04X} should be refused");
838 }
839 }
840
841 #[test]
842 fn a_short_bitmap_allows_only_the_always_commands() {
843 let commands = Commands::parse(&hex("00")).unwrap();
844 assert!(commands.allows(BATTERY) && commands.allows(SWITCHES) && commands.allows(HANDSHAKE));
845 assert!(!commands.allows(FIRMWARE) && !commands.allows(NOISE_CONTROL));
846 assert!(matches!(
847 Commands::parse(&hex("01 FF")),
848 Err(Error::Rejected { status: 1, .. })
849 ));
850 }
851
852 #[test]
853 fn product_id_is_three_bytes_little_endian() {
854 assert_eq!(decode_product_id(&hex("00 12 3C 06")).unwrap(), T310_PRODUCT_ID);
855 assert_eq!(decode_product_id(&hex("00 14 54 06")).unwrap(), 0x06_54_14);
856 assert!(decode_product_id(&hex("00 12 3C")).is_err());
857 }
858
859 #[test]
860 fn subscribes_to_offered_events_only() {
861 let commands = Commands::parse(&hex(T310_BITMAP_REPLY)).unwrap();
862 let buds4_offer = hex("00 09 01 02 03 04 08 0B F1 F2 F3");
863 let offered = decode_events_offered(&buds4_offer).unwrap();
864 assert_eq!(subscribe_payload(&offered, &commands), hex("04 01 02 03 04"));
865 assert_eq!(subscribe_payload(&[0x02, 0x01], &commands), hex("02 01 02"));
866 let no_fit_test = Commands::parse(&hex("00 BD 0D")).unwrap();
867 assert_eq!(subscribe_payload(&offered, &no_fit_test), hex("03 01 02 03"));
868 }
869
870 #[test]
871 fn battery_levels_charging_and_unknown_parts() {
872 let battery = decode_battery(&hex("00 03 01 64 02 E4 03 00")).unwrap();
873 assert_eq!(
874 battery.left,
875 Some(Charge {
876 level: 100,
877 charging: false
878 })
879 );
880 assert_eq!(
881 battery.right,
882 Some(Charge {
883 level: 100,
884 charging: true
885 })
886 );
887 assert_eq!(battery.case, None);
888 assert_eq!(decode_battery(&hex("00 00")).unwrap(), Battery::default());
889 assert!(decode_battery(&hex("00 03 01 64")).is_err());
890 }
891
892 #[test]
893 fn firmware_shows_kind_two_versions() {
894 let mut reply = hex("00 04");
895 reply.extend(b"1,2,138,2,2,138,3,1,01,3,2,105");
896 assert_eq!(decode_firmware(&reply).unwrap(), "138.138.105");
897 // The owner's T310 (HW1): both buds report the same full version.
898 let mut t310 = hex("00 02");
899 t310.extend(b"1,2,1.1.0.43,2,2,1.1.0.43");
900 assert_eq!(decode_firmware(&t310).unwrap(), "1.1.0.43");
901 let mut differ = hex("00 02");
902 differ.extend(b"1,2,1.1.0.43,2,2,1.1.0.44");
903 assert_eq!(decode_firmware(&differ).unwrap(), "1.1.0.43 / 1.1.0.44");
904 let mut odd = hex("00 01");
905 odd.extend(b"V1.0.2");
906 assert_eq!(decode_firmware(&odd).unwrap(), "V1.0.2");
907 }
908
909 #[test]
910 fn noise_control_modes() {
911 let mode = |reply: &str| decode_noise_mode(&hex(reply)).unwrap();
912 assert_eq!(mode("00 01 01 01").mode, "off");
913 assert_eq!(mode("00 01 01 02 00").mode, "transparency");
914 let deep = mode("00 01 01 08");
915 assert_eq!((deep.mode.as_str(), deep.level.as_deref()), ("anc", Some("deep")));
916 assert_eq!(mode("00 01 01 10").level.as_deref(), Some("medium"));
917 assert_eq!(mode("00 01 01 00 08").mode, "unknown (0x800)");
918 assert!(decode_noise_mode(&hex("00 02 01 08")).is_err());
919 assert!(decode_noise_mode(&hex("00 01 01")).is_err());
920 assert!(matches!(
921 decode_noise_mode(&hex("0E 01 01 08")),
922 Err(Error::Rejected { status: 0x0E, .. })
923 ));
924 }
925
926 #[test]
927 fn noise_control_write_payloads() {
928 assert_eq!(noise_write_payload(NoiseMode::Off), hex("01 01 01"));
929 assert_eq!(noise_write_payload(NoiseMode::Transparency), hex("01 01 02"));
930 assert_eq!(noise_write_payload(NoiseMode::Anc(AncLevel::Light)), hex("01 01 04"));
931 assert_eq!(noise_write_payload(NoiseMode::Anc(AncLevel::Medium)), hex("01 01 10"));
932 assert_eq!(noise_write_payload(NoiseMode::Anc(AncLevel::Deep)), hex("01 01 08"));
933 // What is written is what a read reports: every write mask decodes to its own mode.
934 for level in [AncLevel::Light, AncLevel::Medium, AncLevel::Deep] {
935 let mut reply = hex("00 01 01");
936 reply.extend(noise_write_payload(NoiseMode::Anc(level)).iter().skip(2));
937 let read = decode_noise_mode(&reply).unwrap();
938 assert_eq!((read.mode.as_str(), read.level.as_deref()), ("anc", Some(level.name())));
939 }
940 // The family's documented frame for "off" (realme_t310.md#frame-whole).
941 let frame =
942 crate::devices::realme_t310::frame::encode(NOISE_CONTROL_WRITE, 0x06, &noise_write_payload(NoiseMode::Off))
943 .unwrap();
944 assert_eq!(frame, hex("AA 0A 00 00 04 04 06 03 00 01 01 01"));
945 }
946
947 #[test]
948 fn switch_writes() {
949 assert_eq!(switch_id("game"), Some(0x06));
950 assert_eq!(switch_id("dual"), Some(0x11));
951 assert_eq!(switch_id("turbo"), None);
952 assert_eq!(switch_write_payload(0x06, true), [0x06, 0x01]);
953 assert_eq!(switch_write_payload(0x06, false), [0x06, 0x00]);
954 }
955
956 #[test]
957 fn eq_presets_by_name() {
958 assert_eq!(eq_id("pure-raw"), Some(0x00));
959 assert_eq!(eq_id("pure-bass"), Some(0x03));
960 assert_eq!(eq_id("custom-7"), None);
961 assert_eq!(eq_names(), ["pure-raw", "deep-bass", "pure-vocals", "pure-bass"]);
962 // Every preset reads back under the name it was selected by.
963 for name in eq_names() {
964 let reply = [0x00, eq_id(name).unwrap()];
965 assert_eq!(decode_eq(&reply).unwrap(), name);
966 }
967 }
968
969 #[test]
970 fn find_payloads() {
971 assert_eq!(find_payload(true), [0x01]);
972 assert_eq!(find_payload(false), [0x00]);
973 }
974
975 #[test]
976 fn write_replies() {
977 assert!(check_write_reply(&hex("00"), "the write").is_ok());
978 assert!(matches!(
979 check_write_reply(&hex("0E"), "the write"),
980 Err(Error::Rejected { status: 0x0E, .. })
981 ));
982 assert!(check_write_reply(&[], "the write").is_err());
983 }
984
985 #[test]
986 fn hold_cycle_lists_modes() {
987 assert_eq!(decode_hold_cycle(&hex("00 02 01 0A")).unwrap(), ["transparency", "anc"]);
988 assert_eq!(
989 decode_hold_cycle(&hex("00 02 01 0B 00")).unwrap(),
990 ["off", "transparency", "anc"]
991 );
992 assert_eq!(decode_hold_cycle(&hex("00 02 01 14")).unwrap(), ["unknown (0x14)"]);
993 }
994
995 #[test]
996 fn switches_from_report_22_values() {
997 let switches = decode_switches(&hex("00 06 05 00 06 00 11 01 1A 00 1B 01 1D 01")).unwrap();
998 let on: Vec<(&str, bool)> = switches.iter().map(|s| (s.key, s.on)).collect();
999 assert_eq!(
1000 on,
1001 [
1002 ("game", false),
1003 ("dual", true),
1004 ("wind", false),
1005 ("spatial", true),
1006 ("bass-boost", true)
1007 ]
1008 );
1009 assert!(decode_switches(&hex("00 02 06 01")).is_err());
1010 }
1011
1012 #[test]
1013 fn eq_and_codec() {
1014 assert_eq!(decode_eq(&hex("00 00")).unwrap(), "pure-raw");
1015 assert_eq!(decode_eq(&hex("00 03")).unwrap(), "pure-bass");
1016 assert_eq!(decode_eq(&hex("00 05")).unwrap(), "custom-5");
1017 assert_eq!(decode_codec(&hex("00 02")).unwrap(), "AAC");
1018 assert_eq!(decode_codec(&hex("00 09")).unwrap(), "unknown (0x09)");
1019 }
1020
1021 #[test]
1022 fn gesture_table_names_known_values() {
1023 let gestures = decode_gestures(&hex("00 03 01 01 02 01 02 01 04 08 04 06 02 1D")).unwrap();
1024 let rows: Vec<(&str, &str, &str, Option<u8>)> = gestures
1025 .iter()
1026 .map(|g| (g.side.as_str(), g.gesture.as_str(), g.action.as_str(), g.group))
1027 .collect();
1028 assert_eq!(
1029 rows,
1030 [
1031 ("left", "double-tap", "play-pause", None),
1032 ("right", "hold", "noise-cycle", None),
1033 ("both", "double-tap", "answer-call", Some(6)),
1034 ]
1035 );
1036 let odd = decode_gestures(&hex("00 01 09 01 07 42")).unwrap();
1037 assert_eq!(odd.first().map(|g| g.action.as_str()), Some("fn-0x42"));
1038 assert!(decode_gestures(&hex("00 02 01 01 02 01")).is_err());
1039 }
1040
1041 /// The owner's table (HW1): per bud, double tap play/pause, triple tap next, hold cycle.
1042 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";
1043
1044 #[test]
1045 fn touch_targets_follow_the_t310_choices() {
1046 assert_eq!(
1047 touch_target("left", "triple-tap", "volume-up").unwrap(),
1048 TouchTarget {
1049 devices: &[0x01],
1050 act: 0x03,
1051 function: 0x0B
1052 }
1053 );
1054 assert_eq!(touch_target("both", "hold", "none").unwrap().devices, [0x01, 0x02]);
1055 let usage = |side, gesture, action| match touch_target(side, gesture, action) {
1056 Err(Error::Usage(message)) => message,
1057 other => panic!("expected a usage error, got {other:?}"),
1058 };
1059 assert!(usage("middle", "hold", "none").contains("left, right, both"));
1060 assert!(usage("left", "single-tap", "none").contains("double-tap, triple-tap, hold"));
1061 // Offered by the family, but not by realme Link for this gesture.
1062 assert!(usage("left", "double-tap", "volume-up").contains("none, play-pause, next, voice-assistant"));
1063 assert!(usage("left", "hold", "previous").contains("none, noise-cycle, voice-assistant, game-mode"));
1064 }
1065
1066 #[test]
1067 fn retargeting_changes_only_the_targeted_entries() {
1068 let table = decode_gesture_entries(&hex(HW1_GESTURES)).unwrap();
1069 let left = touch_target("left", "triple-tap", "volume-up").unwrap();
1070 let payload = gesture_write_payload(&retarget_gestures(&table, &left).unwrap()).unwrap();
1071 assert_eq!(
1072 payload,
1073 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")
1074 );
1075 // The frame the spec gives for this change (realme_t310.md#gesture-write).
1076 let frame = crate::devices::realme_t310::frame::encode(GESTURE_WRITE, 0x06, &payload).unwrap();
1077 assert_eq!(
1078 crate::debug::hex(&frame),
1079 "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"
1080 );
1081 let both = touch_target("both", "hold", "game-mode").unwrap();
1082 let payload = gesture_write_payload(&retarget_gestures(&table, &both).unwrap()).unwrap();
1083 assert_eq!(
1084 payload,
1085 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")
1086 );
1087 // Unchanged, the payload is the reply without its status byte.
1088 let same = touch_target("right", "double-tap", "play-pause").unwrap();
1089 let payload = gesture_write_payload(&retarget_gestures(&table, &same).unwrap()).unwrap();
1090 assert_eq!(payload, hex(HW1_GESTURES).get(1..).unwrap());
1091 }
1092
1093 #[test]
1094 fn entries_the_buds_did_not_report_are_never_written() {
1095 // Only the left bud's entries.
1096 let table = decode_gesture_entries(&hex("00 03 01 01 02 01 01 01 03 06 01 01 04 08")).unwrap();
1097 let both = touch_target("both", "hold", "none").unwrap();
1098 assert!(matches!(retarget_gestures(&table, &both), Err(Error::Unsupported(_))));
1099 // An on-call row (group 06) is not a main-group entry.
1100 let table = decode_gesture_entries(&hex("00 01 01 06 02 1D")).unwrap();
1101 let left = touch_target("left", "double-tap", "none").unwrap();
1102 assert!(matches!(retarget_gestures(&table, &left), Err(Error::Unsupported(_))));
1103 }
1104
1105 #[test]
1106 fn hold_cycle_writes() {
1107 assert_eq!(hold_cycle_mask(&["transparency", "anc"]).unwrap(), 0x0A);
1108 assert_eq!(hold_cycle_mask(&["anc", "off", "transparency"]).unwrap(), 0x0B);
1109 assert_eq!(hold_cycle_write_payload(0x0B), hex("02 01 0B"));
1110 assert_eq!(hold_cycle_write_payload(0x09), hex("02 01 09"));
1111 let usage = |modes: &[&str]| matches!(hold_cycle_mask(modes), Err(Error::Usage(_)));
1112 assert!(usage(&["anc"]));
1113 assert!(usage(&[]));
1114 assert!(usage(&["anc", "anc"]));
1115 assert!(usage(&["anc", "deep"]));
1116 // What is written reads back as the same modes.
1117 let mut reply = hex("00");
1118 reply.extend(hold_cycle_write_payload(0x0B));
1119 assert_eq!(decode_hold_cycle_mask(&reply).unwrap(), 0x0B);
1120 assert_eq!(decode_hold_cycle(&reply).unwrap(), ["off", "transparency", "anc"]);
1121 // The frame the spec gives (realme_t310.md#hold-cycle-write).
1122 let frame =
1123 crate::devices::realme_t310::frame::encode(NOISE_CONTROL_WRITE, 0x06, &hold_cycle_write_payload(0x0B))
1124 .unwrap();
1125 assert_eq!(crate::debug::hex(&frame), "AA 0A 00 00 04 04 06 03 00 02 01 0B");
1126 }
1127
1128 #[test]
1129 fn custom_eq_list() {
1130 assert!(decode_custom_eqs(&hex("00 00")).unwrap().is_empty());
1131 // One preset "Me", selected, range FA 06, two bands: 62 Hz +3, 16 kHz -2.
1132 let reply = hex("00 01 01 FA 06 04 02 4D 65 02 3E 00 03 80 3E FE");
1133 let presets = decode_custom_eqs(&reply).unwrap();
1134 assert_eq!(
1135 presets,
1136 [EqPreset {
1137 name: "Me".to_owned(),
1138 id: 0x04,
1139 custom: true,
1140 bands: vec![EqBand { hz: 62, gain: 3 }, EqBand { hz: 16000, gain: -2 }],
1141 range: Some((-6, 6)),
1142 }]
1143 );
1144 // Cut short inside the bands.
1145 assert!(decode_custom_eqs(&hex("00 01 01 FA 06 04 02 4D 65 02 3E 00")).is_err());
1146 assert_eq!(builtin_eqs().len(), 4);
1147 }
1148
1149 #[test]
1150 fn bass_level_payloads() {
1151 assert_eq!(bass_write_payload(0).unwrap(), [0xFB, 0x05, 0x00]);
1152 assert_eq!(bass_write_payload(5).unwrap(), [0xFB, 0x05, 0x05]);
1153 assert_eq!(bass_write_payload(-2).unwrap(), [0xFB, 0x05, 0xFE]);
1154 assert_eq!(bass_write_payload(-5).unwrap(), [0xFB, 0x05, 0xFB]);
1155 assert!(matches!(bass_write_payload(6), Err(Error::Usage(_))));
1156 assert!(matches!(bass_write_payload(-6), Err(Error::Usage(_))));
1157 // The frame the spec gives (realme_t310.md#bass-write).
1158 let frame = crate::devices::realme_t310::frame::encode(BASS_LEVEL_WRITE, 0x05, &bass_write_payload(3).unwrap())
1159 .unwrap();
1160 assert_eq!(crate::debug::hex(&frame), "AA 0A 00 00 1B 04 05 03 00 FB 05 03");
1161 }
1162
1163 /// The owner's custom EQ list (W11a): "Custom1", id 04, 6 flat bands from 50 Hz.
1164 const OWNER_CUSTOM_EQS: &str =
1165 "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";
1166
1167 #[test]
1168 fn retunes_the_owners_custom_preset() {
1169 let list = decode_custom_eq_list(&hex(OWNER_CUSTOM_EQS)).unwrap();
1170 let custom1 = list.first().unwrap();
1171 assert!(custom1.selected);
1172 assert_eq!((custom1.min, custom1.max, custom1.id), (-6, 6, 4));
1173 assert_eq!(custom_eq_id("custom-4"), Some(4));
1174 assert_eq!(custom_eq_id("pure-bass"), None);
1175 assert_eq!(custom_eq_id("custom-x"), None);
1176
1177 // Unchanged, the save carries the preset as listed.
1178 assert_eq!(
1179 custom_eq_save_payload(&retune(custom1, &[]).unwrap()).unwrap(),
1180 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")
1181 );
1182 let tuned = retune(custom1, &[EqBand { hz: 50, gain: 3 }, EqBand { hz: 16000, gain: -2 }]).unwrap();
1183 let payload = custom_eq_save_payload(&tuned).unwrap();
1184 assert_eq!(
1185 payload,
1186 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")
1187 );
1188 // The frame the spec gives (realme_t310.md#custom-eq).
1189 let frame = crate::devices::realme_t310::frame::encode(CUSTOM_EQ_WRITE, 0x06, &payload).unwrap();
1190 assert_eq!(
1191 crate::debug::hex(&frame),
1192 "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"
1193 );
1194
1195 let usage = |changes: &[EqBand]| matches!(retune(custom1, changes), Err(Error::Usage(_)));
1196 assert!(usage(&[EqBand { hz: 62, gain: 1 }]));
1197 assert!(usage(&[EqBand { hz: 50, gain: 7 }]));
1198 assert!(usage(&[EqBand { hz: 50, gain: -7 }]));
1199 assert!(usage(&[EqBand { hz: 50, gain: 1 }, EqBand { hz: 50, gain: 2 }]));
1200 }
1201
1202 #[test]
1203 fn creates_a_flat_copy_of_the_owners_preset() {
1204 let list = decode_custom_eq_list(&hex(OWNER_CUSTOM_EQS)).unwrap();
1205 let new = new_custom_eq("Test", &list[0]).unwrap();
1206 assert_eq!((new.id, new.min, new.max), (0x00, -6, 6));
1207 assert!(new.bands.iter().all(|b| b.gain == 0));
1208 let hz: Vec<u16> = new.bands.iter().map(|b| b.hz).collect();
1209 assert_eq!(hz, [50, 250, 1000, 4000, 8000, 16000]);
1210 // Spec: realme_t310.md#custom-eq-create (the expected frame, with seq 06)
1211 let frame =
1212 crate::devices::realme_t310::frame::encode(CUSTOM_EQ_WRITE, 0x06, &custom_eq_create_payload(&new).unwrap())
1213 .unwrap();
1214 assert_eq!(
1215 frame,
1216 hex("AA 23 00 00 18 04 06 1C 00 01 FA 06 00 04 54 65 73 74 06 \
1217 32 00 00 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E 00")
1218 );
1219 }
1220
1221 #[test]
1222 fn preset_names() {
1223 assert_eq!(custom_eq_name("Gym").unwrap(), b"Gym");
1224 assert_eq!(custom_eq_name("Bass é").unwrap().len(), 7);
1225 for bad in ["", " Gym", "Gym ", "a\tb", "seventeen bytes!!"] {
1226 assert!(matches!(custom_eq_name(bad), Err(Error::Usage(_))), "{bad:?}");
1227 }
1228 }
1229
1230 #[test]
1231 fn finds_the_created_preset() {
1232 let preset = |id, name: &str| CustomEq {
1233 selected: false,
1234 min: -6,
1235 max: 6,
1236 id,
1237 name: name.as_bytes().to_vec(),
1238 bands: Vec::new(),
1239 };
1240 let list = [preset(4, "Custom1"), preset(5, "Test")];
1241 assert_eq!(find_created(&list, b"Test", 5).map(|p| p.id), Some(5));
1242 // Renumbered after the reply: the name still finds it.
1243 assert_eq!(find_created(&list, b"Test", 9).map(|p| p.id), Some(5));
1244 assert!(find_created(&list, b"Other", 5).is_none());
1245 let twins = [preset(4, "Test"), preset(5, "Test")];
1246 assert_eq!(find_created(&twins, b"Test", 5).map(|p| p.id), Some(5));
1247 assert!(find_created(&twins, b"Test", 9).is_none());
1248 assert_eq!(decode_custom_eq_write_reply(&hex("00 05")).unwrap(), 5);
1249 assert!(decode_custom_eq_write_reply(&hex("01 05")).is_err());
1250 }
1251}