Krolyxon
Commit 904ab9a

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310codec.rs

1109 lines · 42.9 KB · Rust

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