Krolyxon
Commit c7666c2

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310codec.rs

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