Krolyxon
Commit 90075f3

Branches

Refs →

budctlcratesbudctl-coresrcdevicesrealme_t310mod.rs

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