Krolyxon
Commit a21a864

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310codec.rs

1106 lines · 42.8 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 })
486 .collect()
487}
488
489/// One custom preset as the list holds it. Spec: realme_t310.md#custom-eq
490#[derive(Debug, Clone, PartialEq, Eq)]
491pub(super) struct CustomEq {
492 pub(super) selected: bool,
493 /// Gain range, dB.
494 pub(super) min: i8,
495 pub(super) max: i8,
496 pub(super) id: u8,
497 /// The stored name, byte for byte, so a save writes it back unchanged.
498 pub(super) name: Vec<u8>,
499 pub(super) bands: Vec<EqBand>,
500}
501
502impl CustomEq {
503 pub(super) fn preset(&self) -> EqPreset {
504 EqPreset {
505 name: String::from_utf8_lossy(&self.name).into_owned(),
506 id: self.id,
507 custom: true,
508 bands: self.bands.clone(),
509 }
510 }
511}
512
513/// `00 <count>`, then per preset `<selected> <min> <max> <id> <nameLen> <name> <bands>
514/// (<freq u16 LE> <gain s8>)…`. Spec: realme_t310.md#custom-eq
515pub(super) fn decode_custom_eq_list(reply: &[u8]) -> Result<Vec<CustomEq>> {
516 let mut r = ok_reply(reply, "the custom EQ list")?;
517 let mut presets = Vec::new();
518 for _ in 0..r.u8()? {
519 let selected = r.u8()? == 0x01;
520 let (min, max, id) = (r.i8()?, r.i8()?, r.u8()?);
521 let name_len = r.u8()?;
522 let name = r.take(usize::from(name_len))?.to_vec();
523 let mut bands = Vec::new();
524 for _ in 0..r.u8()? {
525 bands.push(EqBand {
526 hz: r.u16_le()?,
527 gain: r.i8()?,
528 });
529 }
530 presets.push(CustomEq {
531 selected,
532 min,
533 max,
534 id,
535 name,
536 bands,
537 });
538 }
539 Ok(presets)
540}
541
542/// Spec: realme_t310.md#custom-eq
543pub(super) fn decode_custom_eqs(reply: &[u8]) -> Result<Vec<EqPreset>> {
544 Ok(decode_custom_eq_list(reply)?.iter().map(CustomEq::preset).collect())
545}
546
547/// The id in `custom-<id>`, the name [`decode_eq`] gives a custom preset.
548/// Spec: realme_t310.md#eq-read
549pub(super) fn custom_eq_id(name: &str) -> Option<u8> {
550 name.strip_prefix("custom-")?.parse().ok()
551}
552
553/// The preset with the given bands' gains changed, and nothing else. Every band must be one the
554/// preset has, at most once, with a gain inside the preset's range.
555/// Spec: realme_t310.md#custom-eq
556pub(super) fn retune(preset: &CustomEq, changes: &[EqBand]) -> Result<CustomEq> {
557 let mut tuned = preset.clone();
558 for (i, change) in changes.iter().enumerate() {
559 if changes.iter().take(i).any(|earlier| earlier.hz == change.hz) {
560 return Err(Error::Usage(format!("the {} Hz band is given twice", change.hz)));
561 }
562 let Some(band) = tuned.bands.iter_mut().find(|b| b.hz == change.hz) else {
563 let have: Vec<String> = preset.bands.iter().map(|b| b.hz.to_string()).collect();
564 return Err(Error::Usage(format!(
565 "custom-{} has no {} Hz band; its bands are {} Hz",
566 preset.id,
567 change.hz,
568 have.join(", ")
569 )));
570 };
571 if !(preset.min..=preset.max).contains(&change.gain) {
572 return Err(Error::Usage(format!(
573 "a gain of {:+} dB is outside {:+}..{:+} dB",
574 change.gain, preset.min, preset.max
575 )));
576 }
577 band.gain = change.gain;
578 }
579 Ok(tuned)
580}
581
582/// `02 <min> <max> <id> <nameLen> <name> <bands> (<freq u16 LE> <gain s8>)…`: save the whole
583/// preset and select it. Spec: realme_t310.md#custom-eq
584pub(super) fn custom_eq_save_payload(preset: &CustomEq) -> Result<Vec<u8>> {
585 let too_long = |what: &str| Error::Protocol(format!("the preset's {what} does not fit in a byte"));
586 let mut payload = vec![CUSTOM_EQ_SAVE];
587 payload.extend([preset.min.to_le_bytes()[0], preset.max.to_le_bytes()[0], preset.id]);
588 payload.push(u8::try_from(preset.name.len()).map_err(|_| too_long("name"))?);
589 payload.extend(&preset.name);
590 payload.push(u8::try_from(preset.bands.len()).map_err(|_| too_long("band count"))?);
591 for band in &preset.bands {
592 payload.extend(band.hz.to_le_bytes());
593 payload.push(band.gain.to_le_bytes()[0]);
594 }
595 Ok(payload)
596}
597
598/// One gesture-table entry: `<dev> <btn> <act> <fn>`. Spec: realme_t310.md#gestures
599pub(super) type GestureEntry = [u8; 4];
600
601/// The entries of a gesture-table reply, in the order the buds sent them.
602/// Spec: realme_t310.md#gestures
603pub(super) fn decode_gesture_entries(reply: &[u8]) -> Result<Vec<GestureEntry>> {
604 let mut r = ok_reply(reply, "the gesture table")?;
605 let mut entries = Vec::new();
606 for _ in 0..r.u8()? {
607 entries.push([r.u8()?, r.u8()?, r.u8()?, r.u8()?]);
608 }
609 Ok(entries)
610}
611
612/// Spec: realme_t310.md#gestures
613pub(super) fn name_gesture(&[side, group, act, function]: &GestureEntry) -> Gesture {
614 Gesture {
615 side: name_of(&SIDES, side).map_or_else(|| format!("device-0x{side:02X}"), str::to_owned),
616 gesture: name_of(&ACTS, act).map_or_else(|| format!("act-0x{act:02X}"), str::to_owned),
617 action: name_of(&FUNCTIONS, function).map_or_else(|| format!("fn-0x{function:02X}"), str::to_owned),
618 group: (group != MAIN_GROUP).then_some(group),
619 }
620}
621
622/// Spec: realme_t310.md#gestures
623pub(super) fn decode_gestures(reply: &[u8]) -> Result<Vec<Gesture>> {
624 Ok(decode_gesture_entries(reply)?.iter().map(name_gesture).collect())
625}
626
627/// What `touch <side>` changes: `both` is the left and the right bud's entry.
628/// Spec: realme_t310.md#gesture-choices
629const TOUCH_SIDES: [(&str, &[u8]); 3] = [("left", &[0x01]), ("right", &[0x02]), ("both", &[0x01, 0x02])];
630
631/// The actions realme Link offers per gesture on the T310, in its order. budctl offers no
632/// others. Spec: realme_t310.md#gesture-choices
633const TOUCH_CHOICES: [(u8, &[u8]); 3] = [
634 // double tap: none, play/pause, next, voice assistant
635 (0x02, &[0x00, 0x01, 0x06, 0x03]),
636 // triple tap: none, previous, next, voice assistant, volume up, volume down, game mode
637 (0x03, &[0x00, 0x05, 0x06, 0x03, 0x0B, 0x0C, 0x11]),
638 // hold: none, noise-control cycle, voice assistant, game mode
639 (0x04, &[0x00, 0x08, 0x03, 0x11]),
640];
641
642/// A checked `touch <side> <gesture> <action>`: the bud ids, the act and the fn to write.
643#[derive(Debug, PartialEq, Eq)]
644pub(super) struct TouchTarget {
645 pub(super) devices: &'static [u8],
646 pub(super) act: u8,
647 pub(super) function: u8,
648}
649
650/// Check the names against what the T310 offers. Every failure is a usage error that lists
651/// the valid names. Spec: realme_t310.md#gesture-choices
652pub(super) fn touch_target(side: &str, gesture: &str, action: &str) -> Result<TouchTarget> {
653 let list = |names: Vec<&str>| names.join(", ");
654 let Some(&(_, devices)) = TOUCH_SIDES.iter().find(|(name, _)| *name == side) else {
655 return Err(Error::Usage(format!(
656 "no side called {side}; use one of: {}",
657 list(TOUCH_SIDES.iter().map(|(name, _)| *name).collect())
658 )));
659 };
660 let gesture_names = || {
661 list(
662 TOUCH_CHOICES
663 .iter()
664 .filter_map(|(act, _)| name_of(&ACTS, *act))
665 .collect(),
666 )
667 };
668 let Some(&(act, choices)) = TOUCH_CHOICES
669 .iter()
670 .find(|(act, _)| name_of(&ACTS, *act) == Some(gesture))
671 else {
672 return Err(Error::Usage(format!(
673 "these buds have no {gesture} gesture to set; use one of: {}",
674 gesture_names()
675 )));
676 };
677 let Some(&function) = choices.iter().find(|f| name_of(&FUNCTIONS, **f) == Some(action)) else {
678 return Err(Error::Usage(format!(
679 "{gesture} cannot be set to {action} on these buds; use one of: {}",
680 list(choices.iter().filter_map(|f| name_of(&FUNCTIONS, *f)).collect())
681 )));
682 };
683 Ok(TouchTarget { devices, act, function })
684}
685
686/// The table with the target's entries changed, and nothing else. Every targeted entry must
687/// already be in the table: budctl never adds entries. Spec: realme_t310.md#gesture-write
688pub(super) fn retarget_gestures(table: &[GestureEntry], target: &TouchTarget) -> Result<Vec<GestureEntry>> {
689 for &device in target.devices {
690 if !table
691 .iter()
692 .any(|&[d, group, act, _]| d == device && group == MAIN_GROUP && act == target.act)
693 {
694 let entry = name_gesture(&[device, MAIN_GROUP, target.act, 0]);
695 return Err(Error::Unsupported(format!(
696 "the buds report no {} {} entry, so budctl will not write one",
697 entry.side, entry.gesture
698 )));
699 }
700 }
701 Ok(table
702 .iter()
703 .map(|&[device, group, act, function]| {
704 if target.devices.contains(&device) && group == MAIN_GROUP && act == target.act {
705 [device, group, act, target.function]
706 } else {
707 [device, group, act, function]
708 }
709 })
710 .collect())
711}
712
713/// `<count> (<dev> <btn> <act> <fn>)…`: the whole table. Spec: realme_t310.md#gesture-write
714pub(super) fn gesture_write_payload(table: &[GestureEntry]) -> Result<Vec<u8>> {
715 let count = u8::try_from(table.len())
716 .map_err(|_| Error::Protocol(format!("a gesture table of {} entries does not fit", table.len())))?;
717 let mut payload = vec![count];
718 payload.extend(table.iter().flatten());
719 Ok(payload)
720}
721
722/// The handshake reply of the owner's T310 (HW1). Spec: realme_t310.md#bitmap-t310
723#[cfg(test)]
724pub(super) const T310_BITMAP_REPLY: &str = "00 BF 1F 5A 20 45 04 40 00";
725
726#[cfg(test)]
727mod tests {
728 use super::*;
729 use crate::transport::mock::hex;
730
731 #[test]
732 fn t310_bitmap_allows_its_commands_and_nothing_else() {
733 let commands = Commands::parse(&hex(T310_BITMAP_REPLY)).unwrap();
734 // Every gated command in the T310's logged command set (realme_t310.md#bitmap-t310).
735 for cmd in [
736 0x0100, 0x0103, 0x0105, 0x0106, 0x0108, 0x0109, 0x010C, 0x010D, 0x010F, 0x0114, 0x0122, 0x0124, 0x0205,
737 0x0400, 0x0401, 0x0403, 0x0404, 0x0405, 0x0406, 0x0418, 0x041B, 0x0200,
738 ] {
739 assert!(commands.allows(cmd), "0x{cmd:04X} should be allowed");
740 }
741 // Commands the T310 lacks, and commands budctl has no gate for.
742 for cmd in [0x012F, 0x041E, 0x0413, 0x0130, 0x0427, 0x0423, 0x012A, 0x0422, 0x9999] {
743 assert!(!commands.allows(cmd), "0x{cmd:04X} should be refused");
744 }
745 }
746
747 #[test]
748 fn a_short_bitmap_allows_only_the_always_commands() {
749 let commands = Commands::parse(&hex("00")).unwrap();
750 assert!(commands.allows(BATTERY) && commands.allows(SWITCHES) && commands.allows(HANDSHAKE));
751 assert!(!commands.allows(FIRMWARE) && !commands.allows(NOISE_CONTROL));
752 assert!(matches!(
753 Commands::parse(&hex("01 FF")),
754 Err(Error::Rejected { status: 1, .. })
755 ));
756 }
757
758 #[test]
759 fn product_id_is_three_bytes_little_endian() {
760 assert_eq!(decode_product_id(&hex("00 12 3C 06")).unwrap(), T310_PRODUCT_ID);
761 assert_eq!(decode_product_id(&hex("00 14 54 06")).unwrap(), 0x06_54_14);
762 assert!(decode_product_id(&hex("00 12 3C")).is_err());
763 }
764
765 #[test]
766 fn subscribes_to_offered_events_only() {
767 let commands = Commands::parse(&hex(T310_BITMAP_REPLY)).unwrap();
768 let buds4_offer = hex("00 09 01 02 03 04 08 0B F1 F2 F3");
769 let offered = decode_events_offered(&buds4_offer).unwrap();
770 assert_eq!(subscribe_payload(&offered, &commands), hex("04 01 02 03 04"));
771 assert_eq!(subscribe_payload(&[0x02, 0x01], &commands), hex("02 01 02"));
772 let no_fit_test = Commands::parse(&hex("00 BD 0D")).unwrap();
773 assert_eq!(subscribe_payload(&offered, &no_fit_test), hex("03 01 02 03"));
774 }
775
776 #[test]
777 fn battery_levels_charging_and_unknown_parts() {
778 let battery = decode_battery(&hex("00 03 01 64 02 E4 03 00")).unwrap();
779 assert_eq!(
780 battery.left,
781 Some(Charge {
782 level: 100,
783 charging: false
784 })
785 );
786 assert_eq!(
787 battery.right,
788 Some(Charge {
789 level: 100,
790 charging: true
791 })
792 );
793 assert_eq!(battery.case, None);
794 assert_eq!(decode_battery(&hex("00 00")).unwrap(), Battery::default());
795 assert!(decode_battery(&hex("00 03 01 64")).is_err());
796 }
797
798 #[test]
799 fn firmware_shows_kind_two_versions() {
800 let mut reply = hex("00 04");
801 reply.extend(b"1,2,138,2,2,138,3,1,01,3,2,105");
802 assert_eq!(decode_firmware(&reply).unwrap(), "138.138.105");
803 // The owner's T310 (HW1): both buds report the same full version.
804 let mut t310 = hex("00 02");
805 t310.extend(b"1,2,1.1.0.43,2,2,1.1.0.43");
806 assert_eq!(decode_firmware(&t310).unwrap(), "1.1.0.43");
807 let mut differ = hex("00 02");
808 differ.extend(b"1,2,1.1.0.43,2,2,1.1.0.44");
809 assert_eq!(decode_firmware(&differ).unwrap(), "1.1.0.43 / 1.1.0.44");
810 let mut odd = hex("00 01");
811 odd.extend(b"V1.0.2");
812 assert_eq!(decode_firmware(&odd).unwrap(), "V1.0.2");
813 }
814
815 #[test]
816 fn noise_control_modes() {
817 let mode = |reply: &str| decode_noise_mode(&hex(reply)).unwrap();
818 assert_eq!(mode("00 01 01 01").mode, "off");
819 assert_eq!(mode("00 01 01 02 00").mode, "transparency");
820 let deep = mode("00 01 01 08");
821 assert_eq!((deep.mode.as_str(), deep.level.as_deref()), ("anc", Some("deep")));
822 assert_eq!(mode("00 01 01 10").level.as_deref(), Some("medium"));
823 assert_eq!(mode("00 01 01 00 08").mode, "unknown (0x800)");
824 assert!(decode_noise_mode(&hex("00 02 01 08")).is_err());
825 assert!(decode_noise_mode(&hex("00 01 01")).is_err());
826 assert!(matches!(
827 decode_noise_mode(&hex("0E 01 01 08")),
828 Err(Error::Rejected { status: 0x0E, .. })
829 ));
830 }
831
832 #[test]
833 fn noise_control_write_payloads() {
834 assert_eq!(noise_write_payload(NoiseMode::Off), hex("01 01 01"));
835 assert_eq!(noise_write_payload(NoiseMode::Transparency), hex("01 01 02"));
836 assert_eq!(noise_write_payload(NoiseMode::Anc(AncLevel::Light)), hex("01 01 04"));
837 assert_eq!(noise_write_payload(NoiseMode::Anc(AncLevel::Medium)), hex("01 01 10"));
838 assert_eq!(noise_write_payload(NoiseMode::Anc(AncLevel::Deep)), hex("01 01 08"));
839 // What is written is what a read reports: every write mask decodes to its own mode.
840 for level in [AncLevel::Light, AncLevel::Medium, AncLevel::Deep] {
841 let mut reply = hex("00 01 01");
842 reply.extend(noise_write_payload(NoiseMode::Anc(level)).iter().skip(2));
843 let read = decode_noise_mode(&reply).unwrap();
844 assert_eq!((read.mode.as_str(), read.level.as_deref()), ("anc", Some(level.name())));
845 }
846 // The family's documented frame for "off" (realme_t310.md#frame-whole).
847 let frame =
848 crate::devices::realme_t310::frame::encode(NOISE_CONTROL_WRITE, 0x06, &noise_write_payload(NoiseMode::Off))
849 .unwrap();
850 assert_eq!(frame, hex("AA 0A 00 00 04 04 06 03 00 01 01 01"));
851 }
852
853 #[test]
854 fn switch_writes() {
855 assert_eq!(switch_id("game"), Some(0x06));
856 assert_eq!(switch_id("dual"), Some(0x11));
857 assert_eq!(switch_id("turbo"), None);
858 assert_eq!(switch_write_payload(0x06, true), [0x06, 0x01]);
859 assert_eq!(switch_write_payload(0x06, false), [0x06, 0x00]);
860 }
861
862 #[test]
863 fn eq_presets_by_name() {
864 assert_eq!(eq_id("pure-raw"), Some(0x00));
865 assert_eq!(eq_id("pure-bass"), Some(0x03));
866 assert_eq!(eq_id("custom-7"), None);
867 assert_eq!(eq_names(), ["pure-raw", "deep-bass", "pure-vocals", "pure-bass"]);
868 // Every preset reads back under the name it was selected by.
869 for name in eq_names() {
870 let reply = [0x00, eq_id(name).unwrap()];
871 assert_eq!(decode_eq(&reply).unwrap(), name);
872 }
873 }
874
875 #[test]
876 fn find_payloads() {
877 assert_eq!(find_payload(true), [0x01]);
878 assert_eq!(find_payload(false), [0x00]);
879 }
880
881 #[test]
882 fn write_replies() {
883 assert!(check_write_reply(&hex("00"), "the write").is_ok());
884 assert!(matches!(
885 check_write_reply(&hex("0E"), "the write"),
886 Err(Error::Rejected { status: 0x0E, .. })
887 ));
888 assert!(check_write_reply(&[], "the write").is_err());
889 }
890
891 #[test]
892 fn hold_cycle_lists_modes() {
893 assert_eq!(decode_hold_cycle(&hex("00 02 01 0A")).unwrap(), ["transparency", "anc"]);
894 assert_eq!(
895 decode_hold_cycle(&hex("00 02 01 0B 00")).unwrap(),
896 ["off", "transparency", "anc"]
897 );
898 assert_eq!(decode_hold_cycle(&hex("00 02 01 14")).unwrap(), ["unknown (0x14)"]);
899 }
900
901 #[test]
902 fn switches_from_report_22_values() {
903 let switches = decode_switches(&hex("00 06 05 00 06 00 11 01 1A 00 1B 01 1D 01")).unwrap();
904 let on: Vec<(&str, bool)> = switches.iter().map(|s| (s.key, s.on)).collect();
905 assert_eq!(
906 on,
907 [
908 ("game", false),
909 ("dual", true),
910 ("wind", false),
911 ("spatial", true),
912 ("bass-boost", true)
913 ]
914 );
915 assert!(decode_switches(&hex("00 02 06 01")).is_err());
916 }
917
918 #[test]
919 fn eq_and_codec() {
920 assert_eq!(decode_eq(&hex("00 00")).unwrap(), "pure-raw");
921 assert_eq!(decode_eq(&hex("00 03")).unwrap(), "pure-bass");
922 assert_eq!(decode_eq(&hex("00 05")).unwrap(), "custom-5");
923 assert_eq!(decode_codec(&hex("00 02")).unwrap(), "AAC");
924 assert_eq!(decode_codec(&hex("00 09")).unwrap(), "unknown (0x09)");
925 }
926
927 #[test]
928 fn gesture_table_names_known_values() {
929 let gestures = decode_gestures(&hex("00 03 01 01 02 01 02 01 04 08 04 06 02 1D")).unwrap();
930 let rows: Vec<(&str, &str, &str, Option<u8>)> = gestures
931 .iter()
932 .map(|g| (g.side.as_str(), g.gesture.as_str(), g.action.as_str(), g.group))
933 .collect();
934 assert_eq!(
935 rows,
936 [
937 ("left", "double-tap", "play-pause", None),
938 ("right", "hold", "noise-cycle", None),
939 ("both", "double-tap", "answer-call", Some(6)),
940 ]
941 );
942 let odd = decode_gestures(&hex("00 01 09 01 07 42")).unwrap();
943 assert_eq!(odd.first().map(|g| g.action.as_str()), Some("fn-0x42"));
944 assert!(decode_gestures(&hex("00 02 01 01 02 01")).is_err());
945 }
946
947 /// The owner's table (HW1): per bud, double tap play/pause, triple tap next, hold cycle.
948 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";
949
950 #[test]
951 fn touch_targets_follow_the_t310_choices() {
952 assert_eq!(
953 touch_target("left", "triple-tap", "volume-up").unwrap(),
954 TouchTarget {
955 devices: &[0x01],
956 act: 0x03,
957 function: 0x0B
958 }
959 );
960 assert_eq!(touch_target("both", "hold", "none").unwrap().devices, [0x01, 0x02]);
961 let usage = |side, gesture, action| match touch_target(side, gesture, action) {
962 Err(Error::Usage(message)) => message,
963 other => panic!("expected a usage error, got {other:?}"),
964 };
965 assert!(usage("middle", "hold", "none").contains("left, right, both"));
966 assert!(usage("left", "single-tap", "none").contains("double-tap, triple-tap, hold"));
967 // Offered by the family, but not by realme Link for this gesture.
968 assert!(usage("left", "double-tap", "volume-up").contains("none, play-pause, next, voice-assistant"));
969 assert!(usage("left", "hold", "previous").contains("none, noise-cycle, voice-assistant, game-mode"));
970 }
971
972 #[test]
973 fn retargeting_changes_only_the_targeted_entries() {
974 let table = decode_gesture_entries(&hex(HW1_GESTURES)).unwrap();
975 let left = touch_target("left", "triple-tap", "volume-up").unwrap();
976 let payload = gesture_write_payload(&retarget_gestures(&table, &left).unwrap()).unwrap();
977 assert_eq!(
978 payload,
979 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")
980 );
981 // The frame the spec gives for this change (realme_t310.md#gesture-write).
982 let frame = crate::devices::realme_t310::frame::encode(GESTURE_WRITE, 0x06, &payload).unwrap();
983 assert_eq!(
984 crate::debug::hex(&frame),
985 "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"
986 );
987 let both = touch_target("both", "hold", "game-mode").unwrap();
988 let payload = gesture_write_payload(&retarget_gestures(&table, &both).unwrap()).unwrap();
989 assert_eq!(
990 payload,
991 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")
992 );
993 // Unchanged, the payload is the reply without its status byte.
994 let same = touch_target("right", "double-tap", "play-pause").unwrap();
995 let payload = gesture_write_payload(&retarget_gestures(&table, &same).unwrap()).unwrap();
996 assert_eq!(payload, hex(HW1_GESTURES).get(1..).unwrap());
997 }
998
999 #[test]
1000 fn entries_the_buds_did_not_report_are_never_written() {
1001 // Only the left bud's entries.
1002 let table = decode_gesture_entries(&hex("00 03 01 01 02 01 01 01 03 06 01 01 04 08")).unwrap();
1003 let both = touch_target("both", "hold", "none").unwrap();
1004 assert!(matches!(retarget_gestures(&table, &both), Err(Error::Unsupported(_))));
1005 // An on-call row (group 06) is not a main-group entry.
1006 let table = decode_gesture_entries(&hex("00 01 01 06 02 1D")).unwrap();
1007 let left = touch_target("left", "double-tap", "none").unwrap();
1008 assert!(matches!(retarget_gestures(&table, &left), Err(Error::Unsupported(_))));
1009 }
1010
1011 #[test]
1012 fn hold_cycle_writes() {
1013 assert_eq!(hold_cycle_mask(&["transparency", "anc"]).unwrap(), 0x0A);
1014 assert_eq!(hold_cycle_mask(&["anc", "off", "transparency"]).unwrap(), 0x0B);
1015 assert_eq!(hold_cycle_write_payload(0x0B), hex("02 01 0B"));
1016 assert_eq!(hold_cycle_write_payload(0x09), hex("02 01 09"));
1017 let usage = |modes: &[&str]| matches!(hold_cycle_mask(modes), Err(Error::Usage(_)));
1018 assert!(usage(&["anc"]));
1019 assert!(usage(&[]));
1020 assert!(usage(&["anc", "anc"]));
1021 assert!(usage(&["anc", "deep"]));
1022 // What is written reads back as the same modes.
1023 let mut reply = hex("00");
1024 reply.extend(hold_cycle_write_payload(0x0B));
1025 assert_eq!(decode_hold_cycle_mask(&reply).unwrap(), 0x0B);
1026 assert_eq!(decode_hold_cycle(&reply).unwrap(), ["off", "transparency", "anc"]);
1027 // The frame the spec gives (realme_t310.md#hold-cycle-write).
1028 let frame =
1029 crate::devices::realme_t310::frame::encode(NOISE_CONTROL_WRITE, 0x06, &hold_cycle_write_payload(0x0B))
1030 .unwrap();
1031 assert_eq!(crate::debug::hex(&frame), "AA 0A 00 00 04 04 06 03 00 02 01 0B");
1032 }
1033
1034 #[test]
1035 fn custom_eq_list() {
1036 assert!(decode_custom_eqs(&hex("00 00")).unwrap().is_empty());
1037 // One preset "Me", selected, range FA 06, two bands: 62 Hz +3, 16 kHz -2.
1038 let reply = hex("00 01 01 FA 06 04 02 4D 65 02 3E 00 03 80 3E FE");
1039 let presets = decode_custom_eqs(&reply).unwrap();
1040 assert_eq!(
1041 presets,
1042 [EqPreset {
1043 name: "Me".to_owned(),
1044 id: 0x04,
1045 custom: true,
1046 bands: vec![EqBand { hz: 62, gain: 3 }, EqBand { hz: 16000, gain: -2 }],
1047 }]
1048 );
1049 // Cut short inside the bands.
1050 assert!(decode_custom_eqs(&hex("00 01 01 FA 06 04 02 4D 65 02 3E 00")).is_err());
1051 assert_eq!(builtin_eqs().len(), 4);
1052 }
1053
1054 #[test]
1055 fn bass_level_payloads() {
1056 assert_eq!(bass_write_payload(0).unwrap(), [0xFB, 0x05, 0x00]);
1057 assert_eq!(bass_write_payload(5).unwrap(), [0xFB, 0x05, 0x05]);
1058 assert_eq!(bass_write_payload(-2).unwrap(), [0xFB, 0x05, 0xFE]);
1059 assert_eq!(bass_write_payload(-5).unwrap(), [0xFB, 0x05, 0xFB]);
1060 assert!(matches!(bass_write_payload(6), Err(Error::Usage(_))));
1061 assert!(matches!(bass_write_payload(-6), Err(Error::Usage(_))));
1062 // The frame the spec gives (realme_t310.md#bass-write).
1063 let frame = crate::devices::realme_t310::frame::encode(BASS_LEVEL_WRITE, 0x05, &bass_write_payload(3).unwrap())
1064 .unwrap();
1065 assert_eq!(crate::debug::hex(&frame), "AA 0A 00 00 1B 04 05 03 00 FB 05 03");
1066 }
1067
1068 /// The owner's custom EQ list (W11a): "Custom1", id 04, 6 flat bands from 50 Hz.
1069 const OWNER_CUSTOM_EQS: &str =
1070 "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";
1071
1072 #[test]
1073 fn retunes_the_owners_custom_preset() {
1074 let list = decode_custom_eq_list(&hex(OWNER_CUSTOM_EQS)).unwrap();
1075 let custom1 = list.first().unwrap();
1076 assert!(custom1.selected);
1077 assert_eq!((custom1.min, custom1.max, custom1.id), (-6, 6, 4));
1078 assert_eq!(custom_eq_id("custom-4"), Some(4));
1079 assert_eq!(custom_eq_id("pure-bass"), None);
1080 assert_eq!(custom_eq_id("custom-x"), None);
1081
1082 // Unchanged, the save carries the preset as listed.
1083 assert_eq!(
1084 custom_eq_save_payload(&retune(custom1, &[]).unwrap()).unwrap(),
1085 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")
1086 );
1087 let tuned = retune(custom1, &[EqBand { hz: 50, gain: 3 }, EqBand { hz: 16000, gain: -2 }]).unwrap();
1088 let payload = custom_eq_save_payload(&tuned).unwrap();
1089 assert_eq!(
1090 payload,
1091 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")
1092 );
1093 // The frame the spec gives (realme_t310.md#custom-eq).
1094 let frame = crate::devices::realme_t310::frame::encode(CUSTOM_EQ_WRITE, 0x06, &payload).unwrap();
1095 assert_eq!(
1096 crate::debug::hex(&frame),
1097 "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"
1098 );
1099
1100 let usage = |changes: &[EqBand]| matches!(retune(custom1, changes), Err(Error::Usage(_)));
1101 assert!(usage(&[EqBand { hz: 62, gain: 1 }]));
1102 assert!(usage(&[EqBand { hz: 50, gain: 7 }]));
1103 assert!(usage(&[EqBand { hz: 50, gain: -7 }]));
1104 assert!(usage(&[EqBand { hz: 50, gain: 1 }, EqBand { hz: 50, gain: 2 }]));
1105 }
1106}