Krolyxon
Commit e9c4031

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310mod.rs

512 lines · 18.1 KB · Rust

1//! realme Buds T310. Spec: docs/protocol/realme_t310.md
2//!
3//! `frame.rs` and `link.rs` hold the protocol family's framing and request/reply logic;
4//! `codec.rs` holds everything specific to the T310.
5
6use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
7
8use crate::bluez::PairedDevice;
9use crate::bt::Uuid;
10use crate::devices::{Device, Session};
11use crate::error::{Error, Kind, Result};
12use crate::status::{NoiseControl, NoiseMode, Status, TouchControls};
13use crate::transport::Transport;
14
15mod codec;
16mod frame;
17mod link;
18
19use codec::Commands;
20use link::Link;
21
22/// After a write is acked: how long to keep reading back, and the pause between reads.
23/// Spec: realme_t310.md#write-read-back
24const READ_BACK_WINDOW: Duration = Duration::from_secs(2);
25/// Spec: realme_t310.md#write-read-back
26const READ_BACK_PAUSE: Duration = Duration::from_millis(300);
27
28/// Spec: realme_t310.md#identity
29const BLUETOOTH_NAME: &str = "realme Buds T310";
30
31/// Control services, in the order to try. Spec: realme_t310.md#transport
32const SERVICES: [Uuid; 2] = [
33 Uuid::from_u128(0x0000_079A_D102_11E1_9B23_0002_5B00_A5A5),
34 Uuid::from_u128(0x0000_1107_D102_11E1_9B23_0002_5B00_A5A5),
35];
36
37/// Quiet time after connecting, before the first request. Spec: realme_t310.md#init
38const SETTLE: Duration = Duration::from_millis(300);
39
40pub struct RealmeT310;
41
42impl Device for RealmeT310 {
43 fn name(&self) -> &'static str {
44 BLUETOOTH_NAME
45 }
46
47 fn matches(&self, device: &PairedDevice) -> bool {
48 device.name == BLUETOOTH_NAME
49 }
50
51 fn rfcomm_uuids(&self) -> &'static [Uuid] {
52 &SERVICES
53 }
54
55 fn open(&self, transport: Box<dyn Transport>) -> Result<Box<dyn Session>> {
56 Ok(Box::new(T310Session::open(Link::new(transport, unix_time))?))
57 }
58}
59
60fn unix_time() -> u32 {
61 let seconds = SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.as_secs());
62 u32::try_from(seconds).unwrap_or(u32::MAX)
63}
64
65struct T310Session {
66 link: Link,
67 commands: Commands,
68}
69
70/// Collects what a series of best-effort reads produced.
71#[derive(Default)]
72struct Reads {
73 warnings: Vec<String>,
74 answered: usize,
75}
76
77impl T310Session {
78 /// The session start. Spec: realme_t310.md#init
79 fn open(mut link: Link) -> Result<Self> {
80 link.pump_until(Instant::now() + SETTLE)?;
81
82 let bitmap = link.request(codec::HANDSHAKE, &[]).map_err(|error| match error {
83 // Provisional until checkpoint HW1: a link that connects but stays silent is most
84 // likely held by another app. Spec: realme_t310.md#open
85 Error::Timeout(_) => Error::ChannelBusy("connected, but the buds did not answer the handshake".to_owned()),
86 other => other,
87 })?;
88 let commands = Commands::parse(&bitmap)?;
89
90 let product = codec::decode_product_id(&link.request(codec::PRODUCT_ID, &[])?)?;
91 if product != codec::T310_PRODUCT_ID {
92 return Err(Error::WrongModel {
93 expected: BLUETOOTH_NAME,
94 found: format!("{product:06X}"),
95 });
96 }
97
98 let offered = codec::decode_events_offered(&link.request(codec::EVENTS_OFFERED, &[])?)?;
99 let subscription = codec::subscribe_payload(&offered, &commands);
100 if commands.allows(codec::SUBSCRIBE) && subscription.len() > 1 {
101 // Some buds never acknowledge this, so budctl does not wait. Spec: realme_t310.md#subscribe
102 link.send(codec::SUBSCRIBE, &subscription)?;
103 }
104 Ok(Self { link, commands })
105 }
106
107 /// Read and decode one value, if the buds offer the command. A read the buds answer badly
108 /// (timeout, refusal, garbage) becomes a warning so the rest still shows; a lost link stops
109 /// everything.
110 fn read<T>(
111 &mut self,
112 reads: &mut Reads,
113 what: &str,
114 cmd: u16,
115 payload: &[u8],
116 decode: fn(&[u8]) -> Result<T>,
117 ) -> Result<Option<T>> {
118 if !self.commands.allows(cmd) {
119 return Ok(None);
120 }
121 match self.link.request(cmd, payload).and_then(|reply| decode(&reply)) {
122 Ok(value) => {
123 reads.answered += 1;
124 Ok(Some(value))
125 }
126 Err(error) if error.kind() == Kind::Failed => {
127 reads.warnings.push(format!("{what}: {error}"));
128 Ok(None)
129 }
130 Err(error) => Err(error),
131 }
132 }
133}
134
135impl Session for T310Session {
136 fn status(&mut self) -> Result<Status> {
137 let mut reads = Reads::default();
138 let switches = self.read(
139 &mut reads,
140 "switches",
141 codec::SWITCHES,
142 &codec::SWITCH_QUERY,
143 codec::decode_switches,
144 )?;
145 let noise_control = self.read(
146 &mut reads,
147 "noise control",
148 codec::NOISE_CONTROL,
149 &codec::NOISE_MODE_QUERY,
150 codec::decode_noise_mode,
151 )?;
152 let battery = self.read(&mut reads, "battery", codec::BATTERY, &[], codec::decode_battery)?;
153 let eq = self.read(&mut reads, "EQ", codec::EQ_PRESET, &[], codec::decode_eq)?;
154 // No bass-level read: the T310 does not answer an empty 0x0124 (HW1). It returns with W6.
155 // Spec: realme_t310.md#bass-read
156 let codec = self.read(&mut reads, "codec", codec::CODEC, &[], codec::decode_codec)?;
157 let firmware = self.read(&mut reads, "firmware", codec::FIRMWARE, &[], codec::decode_firmware)?;
158
159 if reads.answered == 0 && !reads.warnings.is_empty() {
160 return Err(Error::Timeout(reads.warnings.join("; ")));
161 }
162 Ok(Status {
163 battery: battery.unwrap_or_default(),
164 noise_control,
165 eq,
166 bass_level: None,
167 switches: switches.unwrap_or_default(),
168 codec,
169 firmware,
170 warnings: reads.warnings,
171 })
172 }
173
174 /// Spec: realme_t310.md#anc-write, #write-read-back
175 fn set_noise_control(&mut self, mode: NoiseMode) -> Result<NoiseControl> {
176 if !self.commands.allows(codec::NOISE_CONTROL_WRITE) || !self.commands.allows(codec::NOISE_CONTROL) {
177 return Err(Error::Unsupported(
178 "these buds do not offer noise-control settings".to_owned(),
179 ));
180 }
181 let reply = self
182 .link
183 .request(codec::NOISE_CONTROL_WRITE, &codec::noise_write_payload(mode))?;
184 codec::check_write_reply(&reply, "the noise-control change")?;
185
186 // Accepted is not applied: read back until the new mode shows, or give up.
187 let wanted = codec::noise_write_mask(mode);
188 let give_up = Instant::now() + READ_BACK_WINDOW;
189 loop {
190 let reply = self.link.request(codec::NOISE_CONTROL, &codec::NOISE_MODE_QUERY)?;
191 if codec::decode_noise_mask(&reply)? == wanted {
192 return codec::decode_noise_mode(&reply);
193 }
194 if Instant::now() >= give_up {
195 let now = codec::decode_noise_mode(&reply)?;
196 return Err(Error::NotApplied(format!(
197 "the buds accepted noise control {}, but still report {}",
198 mode.name(),
199 now.mode
200 )));
201 }
202 self.link.pump_until(Instant::now() + READ_BACK_PAUSE)?;
203 }
204 }
205
206 fn touch_controls(&mut self) -> Result<TouchControls> {
207 if !self.commands.allows(codec::GESTURES) {
208 return Err(Error::Unsupported(
209 "these buds do not offer touch-control settings".to_owned(),
210 ));
211 }
212 let gestures = codec::decode_gestures(&self.link.request(codec::GESTURES, &[])?)?;
213 let mut reads = Reads::default();
214 let hold_cycle = self.read(
215 &mut reads,
216 "hold cycle",
217 codec::NOISE_CONTROL,
218 &codec::HOLD_CYCLE_QUERY,
219 codec::decode_hold_cycle,
220 )?;
221 Ok(TouchControls {
222 gestures,
223 hold_cycle,
224 warnings: reads.warnings,
225 })
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use crate::status::Charge;
233 use crate::transport::mock::{Script, Step, hex};
234
235 fn tx(cmd: u16, seq: u8, payload: &str) -> Step {
236 Step::Tx(frame::encode(cmd, seq, &hex(payload)).unwrap())
237 }
238
239 fn rx(cmd: u16, seq: u8, payload: &str) -> Step {
240 Step::Rx(frame::encode(cmd, seq, &hex(payload)).unwrap())
241 }
242
243 fn fixed_clock() -> u32 {
244 1_750_000_000
245 }
246
247 fn open(script: &Script) -> Result<T310Session> {
248 T310Session::open(Link::new(script.transport(), fixed_clock))
249 }
250
251 /// The handshake of the owner's T310 (HW1); seq 01-04 are used up.
252 fn handshake() -> Vec<Step> {
253 vec![
254 tx(0x0100, 0x01, ""),
255 rx(0x8100, 0x01, codec::T310_BITMAP_REPLY),
256 tx(0x0103, 0x02, ""),
257 rx(0x8103, 0x02, "00 12 3C 06"),
258 tx(0x0200, 0x03, ""),
259 rx(0x8200, 0x03, "00 09 01 02 03 04 05 06 0D F1 F4"),
260 tx(0x0205, 0x04, "04 01 02 03 04"),
261 rx(0x8205, 0x04, "00 04 01 00 02 00 03 00 04 00"),
262 ]
263 }
264
265 #[test]
266 fn full_status() {
267 let mut steps = handshake();
268 let mut firmware = String::from("00 03");
269 for byte in b"1,2,120,2,2,120,3,2,105" {
270 firmware.push_str(&format!(" {byte:02X}"));
271 }
272 steps.extend([
273 tx(0x010D, 0x05, "06 05 06 11 1A 1B 1D"),
274 rx(0x810D, 0x05, "00 06 05 00 06 00 11 01 1A 00 1B 01 1D 01"),
275 tx(0x010C, 0x06, "01 01"),
276 rx(0x810C, 0x06, "00 01 01 08"),
277 tx(0x0106, 0x07, ""),
278 rx(0x8106, 0x07, "00 03 01 50 02 E4 03 3C"),
279 tx(0x010F, 0x08, ""),
280 rx(0x810F, 0x08, "00 01"),
281 tx(0x0114, 0x09, ""),
282 rx(0x8114, 0x09, "00 02"),
283 tx(0x0105, 0x0A, ""),
284 rx(0x8105, 0x0A, &firmware),
285 ]);
286 let script = Script::new(steps);
287 let status = open(&script).unwrap().status().unwrap();
288 script.assert_done();
289
290 assert_eq!(
291 status.battery.left,
292 Some(Charge {
293 level: 80,
294 charging: false
295 })
296 );
297 assert_eq!(
298 status.battery.right,
299 Some(Charge {
300 level: 100,
301 charging: true
302 })
303 );
304 assert_eq!(
305 status.battery.case,
306 Some(Charge {
307 level: 60,
308 charging: false
309 })
310 );
311 let noise = status.noise_control.unwrap();
312 assert_eq!((noise.mode.as_str(), noise.level.as_deref()), ("anc", Some("deep")));
313 assert_eq!(status.eq.as_deref(), Some("deep-bass"));
314 assert_eq!(status.bass_level, None);
315 assert_eq!(status.codec.as_deref(), Some("AAC"));
316 assert_eq!(status.firmware.as_deref(), Some("120.120.105"));
317 let game = status.switches.iter().find(|s| s.key == "game").unwrap();
318 assert!(!game.on);
319 assert_eq!(status.switches.len(), 5);
320 assert!(status.warnings.is_empty());
321 }
322
323 #[test]
324 fn answers_the_time_request_during_the_settle() {
325 let mut steps = vec![
326 rx(0x0500, 0x01, ""),
327 Step::Tx(hex("AA 0C 00 00 00 85 01 05 00 00 80 E1 4E 68")),
328 ];
329 steps.extend(handshake());
330 // No 0x8205 ack this time: it may never arrive, and `open` does not wait for it.
331 // Spec: realme_t310.md#subscribe
332 steps.pop();
333 let script = Script::new(steps);
334 open(&script).unwrap();
335 script.assert_done();
336 }
337
338 #[test]
339 fn a_silent_handshake_reports_a_busy_channel() {
340 let script = Script::new(vec![tx(0x0100, 0x01, "")]);
341 assert!(matches!(open(&script), Err(Error::ChannelBusy(_))));
342 }
343
344 #[test]
345 fn other_models_are_refused() {
346 let script = Script::new(vec![
347 tx(0x0100, 0x01, ""),
348 rx(0x8100, 0x01, codec::T310_BITMAP_REPLY),
349 tx(0x0103, 0x02, ""),
350 rx(0x8103, 0x02, "00 14 54 06"),
351 ]);
352 match open(&script) {
353 Err(Error::WrongModel { found, .. }) => assert_eq!(found, "065414"),
354 other => panic!("expected WrongModel, got {:?}", other.err()),
355 }
356 }
357
358 #[test]
359 fn commands_missing_from_the_bitmap_are_never_sent() {
360 // A bitmap with no bits: only always-allowed commands go out, and no subscription.
361 let script = Script::new(vec![
362 tx(0x0100, 0x01, ""),
363 rx(0x8100, 0x01, "00"),
364 tx(0x0103, 0x02, ""),
365 rx(0x8103, 0x02, "00 12 3C 06"),
366 tx(0x0200, 0x03, ""),
367 rx(0x8200, 0x03, "00 02 01 02"),
368 tx(0x010D, 0x04, "06 05 06 11 1A 1B 1D"),
369 rx(0x810D, 0x04, "00 01 06 01"),
370 tx(0x0106, 0x05, ""),
371 // no battery reply: becomes a warning
372 ]);
373 let status = open(&script).unwrap().status().unwrap();
374 script.assert_done();
375 assert_eq!(status.switches.len(), 1);
376 assert!(status.noise_control.is_none() && status.firmware.is_none());
377 assert_eq!(status.warnings.len(), 1);
378 assert!(status.warnings.first().unwrap().starts_with("battery"));
379 }
380
381 #[test]
382 fn a_lost_link_stops_the_status() {
383 let mut steps = handshake();
384 steps.extend([tx(0x010D, 0x05, "06 05 06 11 1A 1B 1D"), Step::Close]);
385 let script = Script::new(steps);
386 assert!(matches!(open(&script).unwrap().status(), Err(Error::NotConnected(_))));
387 }
388
389 #[test]
390 fn touch_controls_and_hold_cycle() {
391 let mut steps = handshake();
392 steps.extend([
393 tx(0x0108, 0x05, ""),
394 rx(0x8108, 0x05, "00 02 01 01 02 01 02 01 04 08"),
395 tx(0x010C, 0x06, "02 01"),
396 rx(0x810C, 0x06, "00 02 01 0A"),
397 ]);
398 let script = Script::new(steps);
399 let touch = open(&script).unwrap().touch_controls().unwrap();
400 script.assert_done();
401 assert_eq!(touch.gestures.len(), 2);
402 assert_eq!(touch.hold_cycle.unwrap(), ["transparency", "anc"]);
403 }
404
405 /// The owner's `budctl --debug touch` (HW1), byte for byte. The buds' own frames (slice
406 /// byte 08) arrive glued to replies and need no answer.
407 #[test]
408 fn replays_the_hw1_touch_session() {
409 let script = Script::new(vec![
410 Step::Tx(hex("AA 07 00 00 00 01 01 00 00")),
411 Step::Rx(hex(
412 "AA 10 00 00 00 81 01 09 00 00 BF 1F 5A 20 45 04 40 00 AA 07 08 00 01 05 0F 00 00",
413 )),
414 Step::Tx(hex("AA 07 00 00 03 01 02 00 00")),
415 Step::Rx(hex("AA 0B 00 00 03 81 02 04 00 00 12 3C 06")),
416 Step::Tx(hex("AA 07 00 00 00 02 03 00 00")),
417 Step::Rx(hex("AA 12 00 00 00 82 03 0B 00 00 09 01 02 03 04 05 06 0D F1 F4")),
418 Step::Tx(hex("AA 0C 00 00 05 02 04 05 00 04 01 02 03 04")),
419 Step::Rx(hex(
420 "AA 11 00 00 05 82 04 0A 00 00 04 01 00 02 00 03 00 04 00 AA 0F 08 00 04 02 10 08 00 01 03 02 64 01 64 03 00",
421 )),
422 Step::Tx(hex("AA 07 00 00 08 01 05 00 00")),
423 Step::Rx(hex(
424 "AA 21 00 00 08 81 05 1A 00 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",
425 )),
426 Step::Tx(hex("AA 09 00 00 0C 01 06 02 00 02 01")),
427 Step::Rx(hex("AA 0B 00 00 0C 81 06 04 00 00 02 01 0A")),
428 ]);
429 let touch = open(&script).unwrap().touch_controls().unwrap();
430 script.assert_done();
431 let table: Vec<(&str, &str, &str)> = touch
432 .gestures
433 .iter()
434 .map(|g| (g.side.as_str(), g.gesture.as_str(), g.action.as_str()))
435 .collect();
436 assert_eq!(
437 table,
438 [
439 ("left", "double-tap", "play-pause"),
440 ("left", "triple-tap", "next"),
441 ("left", "hold", "noise-cycle"),
442 ("right", "double-tap", "play-pause"),
443 ("right", "triple-tap", "next"),
444 ("right", "hold", "noise-cycle"),
445 ]
446 );
447 assert_eq!(touch.hold_cycle.unwrap(), ["transparency", "anc"]);
448 assert!(touch.warnings.is_empty());
449 }
450
451 #[test]
452 fn sets_noise_control_and_reads_it_back() {
453 let mut steps = handshake();
454 steps.extend([
455 tx(0x0404, 0x05, "01 01 02"),
456 rx(0x8404, 0x05, "00"),
457 // The buds announce the change; it needs no answer.
458 rx(0x0204, 0x10, "03 01 01 02"),
459 tx(0x010C, 0x06, "01 01"),
460 rx(0x810C, 0x06, "00 01 01 02"),
461 ]);
462 let script = Script::new(steps);
463 let now = open(&script)
464 .unwrap()
465 .set_noise_control(NoiseMode::Transparency)
466 .unwrap();
467 script.assert_done();
468 assert_eq!((now.mode.as_str(), now.level), ("transparency", None));
469 }
470
471 #[test]
472 fn reads_back_again_until_the_change_shows() {
473 let mut steps = handshake();
474 steps.extend([
475 tx(0x0404, 0x05, "01 01 01"),
476 rx(0x8404, 0x05, "00"),
477 tx(0x010C, 0x06, "01 01"),
478 rx(0x810C, 0x06, "00 01 01 08"),
479 tx(0x010C, 0x07, "01 01"),
480 rx(0x810C, 0x07, "00 01 01 01"),
481 ]);
482 let script = Script::new(steps);
483 let now = open(&script).unwrap().set_noise_control(NoiseMode::Off).unwrap();
484 script.assert_done();
485 assert_eq!(now.mode, "off");
486 }
487
488 #[test]
489 fn a_rejected_noise_control_change_is_not_read_back() {
490 let mut steps = handshake();
491 steps.extend([tx(0x0404, 0x05, "01 01 08"), rx(0x8404, 0x05, "0E")]);
492 let script = Script::new(steps);
493 let result = open(&script).unwrap().set_noise_control(NoiseMode::Anc);
494 script.assert_done();
495 assert!(matches!(result, Err(Error::Rejected { status: 0x0E, .. })));
496 }
497
498 #[test]
499 fn noise_control_is_not_written_without_its_bitmap_bit() {
500 let script = Script::new(vec![
501 tx(0x0100, 0x01, ""),
502 rx(0x8100, 0x01, "00"),
503 tx(0x0103, 0x02, ""),
504 rx(0x8103, 0x02, "00 12 3C 06"),
505 tx(0x0200, 0x03, ""),
506 rx(0x8200, 0x03, "00 00"),
507 ]);
508 let result = open(&script).unwrap().set_noise_control(NoiseMode::Off);
509 script.assert_done();
510 assert!(matches!(result, Err(Error::Unsupported(_))));
511 }
512}