Krolyxon
Commit 24133e0

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310codec.rs

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