Krolyxon
Commit c98214a

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310codec.rs

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