| 1 | //! Requests and replies over one link, for the OPPO/OnePlus/realme protocol family. |
| 2 | //! Spec: realme_t310.md#seq, #buds-requests |
| 3 | //! |
| 4 | //! Holds no T310 tables. If a second model of this family is added, this file moves to |
| 5 | //! `devices/oppo_family/` together with `frame.rs`. |
| 6 | |
| 7 | use std::time::{Duration, Instant}; |
| 8 | |
| 9 | use super::frame::{self, Frame, Reassembler}; |
| 10 | use crate::debug; |
| 11 | use crate::error::{Error, Result}; |
| 12 | use crate::transport::Transport; |
| 13 | |
| 14 | /// A reply's command id is the request's with this bit set. Spec: realme_t310.md#seq |
| 15 | const REPLY: u16 = 0x8000; |
| 16 | /// budctl's sequence numbers. Spec: realme_t310.md#seq |
| 17 | const FIRST_SEQ: u8 = 0x01; |
| 18 | /// Spec: realme_t310.md#seq |
| 19 | const LAST_SEQ: u8 = 0xBF; |
| 20 | /// At least this long between two frames budctl sends. Spec: realme_t310.md#init |
| 21 | const SEND_GAP: Duration = Duration::from_millis(200); |
| 22 | /// How long to wait for a reply. |
| 23 | const REPLY_TIMEOUT: Duration = Duration::from_secs(3); |
| 24 | |
| 25 | /// Spec: realme_t310.md#time-request |
| 26 | const TIME_REQUEST: u16 = 0x0500; |
| 27 | /// Spec: realme_t310.md#buds-requests |
| 28 | const APP_LIST_REQUEST: u16 = 0x050C; |
| 29 | /// Spec: realme_t310.md#buds-requests |
| 30 | const APP_LIST_ANSWER: &[u8] = &[0x00, 0x01, 0x00]; |
| 31 | /// Spec: realme_t310.md#buds-requests |
| 32 | const CAPABILITIES_REQUEST: u16 = 0x051C; |
| 33 | /// Spec: realme_t310.md#buds-requests |
| 34 | const CAPABILITIES_ANSWER: &[u8] = &[0x00, 0x00, 0x00]; |
| 35 | /// Spec: realme_t310.md#buds-requests |
| 36 | const SPATIAL_MODE_REQUEST: u16 = 0x051D; |
| 37 | /// Spec: realme_t310.md#buds-requests |
| 38 | const SPATIAL_MODE_ANSWER: &[u8] = &[0x01]; |
| 39 | /// Command groups the buds use; a request outside them gets `UNKNOWN_ANSWER`. |
| 40 | /// Spec: realme_t310.md#seq, #buds-requests |
| 41 | const KNOWN_GROUPS: [u16; 6] = [0x01, 0x02, 0x03, 0x04, 0x05, 0x0F]; |
| 42 | /// Spec: realme_t310.md#buds-requests |
| 43 | const UNKNOWN_ANSWER: &[u8] = &[0x01]; |
| 44 | |
| 45 | pub(super) struct Link { |
| 46 | transport: Box<dyn Transport>, |
| 47 | frames: Reassembler, |
| 48 | next_seq: u8, |
| 49 | last_send: Option<Instant>, |
| 50 | /// Unix time in seconds, for the buds' time request. Replaced in tests. |
| 51 | clock: fn() -> u32, |
| 52 | } |
| 53 | |
| 54 | impl Link { |
| 55 | pub(super) fn new(transport: Box<dyn Transport>, clock: fn() -> u32) -> Self { |
| 56 | Self { |
| 57 | transport, |
| 58 | frames: Reassembler::default(), |
| 59 | next_seq: FIRST_SEQ, |
| 60 | last_send: None, |
| 61 | clock, |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | /// Handle whatever the buds send until `deadline`, answering their requests. |
| 66 | /// This is how budctl waits: never with a plain sleep. |
| 67 | pub(super) fn pump_until(&mut self, deadline: Instant) -> Result<()> { |
| 68 | while let Some(frame) = self.next_frame(deadline)? { |
| 69 | self.handle_unsolicited(frame)?; |
| 70 | } |
| 71 | Ok(()) |
| 72 | } |
| 73 | |
| 74 | /// Like [`Self::pump_until`], but stop at the first frame `wanted` picks and return what it |
| 75 | /// returned. `None` when `deadline` passes first. |
| 76 | pub(super) fn wait_for<T>(&mut self, deadline: Instant, wanted: impl Fn(&Frame) -> Option<T>) -> Result<Option<T>> { |
| 77 | while let Some(frame) = self.next_frame(deadline)? { |
| 78 | if let Some(found) = wanted(&frame) { |
| 79 | return Ok(Some(found)); |
| 80 | } |
| 81 | self.handle_unsolicited(frame)?; |
| 82 | } |
| 83 | Ok(None) |
| 84 | } |
| 85 | |
| 86 | /// Send a request and return the payload of its reply. |
| 87 | pub(super) fn request(&mut self, cmd: u16, payload: &[u8]) -> Result<Vec<u8>> { |
| 88 | let seq = self.send(cmd, payload)?; |
| 89 | let deadline = Instant::now() + REPLY_TIMEOUT; |
| 90 | loop { |
| 91 | match self.next_frame(deadline)? { |
| 92 | None => return Err(Error::Timeout(format!("no reply to command 0x{cmd:04X}"))), |
| 93 | Some(frame) if frame.cmd == cmd | REPLY && frame.seq == seq => return Ok(frame.payload), |
| 94 | Some(frame) => self.handle_unsolicited(frame)?, |
| 95 | } |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | /// Send a request without waiting for its reply. A reply that comes later is skipped. |
| 100 | pub(super) fn send(&mut self, cmd: u16, payload: &[u8]) -> Result<u8> { |
| 101 | if let Some(last) = self.last_send { |
| 102 | self.pump_until(last + SEND_GAP)?; |
| 103 | } |
| 104 | let seq = self.next_seq; |
| 105 | self.next_seq = if seq >= LAST_SEQ { FIRST_SEQ } else { seq + 1 }; |
| 106 | debug::note(|| format!("send 0x{cmd:04X} seq {seq:02X} [{}]", debug::hex(payload))); |
| 107 | self.transport.send(&frame::encode(cmd, seq, payload)?)?; |
| 108 | self.last_send = Some(Instant::now()); |
| 109 | Ok(seq) |
| 110 | } |
| 111 | |
| 112 | fn next_frame(&mut self, deadline: Instant) -> Result<Option<Frame>> { |
| 113 | loop { |
| 114 | if let Some(frame) = self.frames.next_frame() { |
| 115 | debug::note(|| { |
| 116 | format!( |
| 117 | "recv 0x{:04X} seq {:02X} [{}]", |
| 118 | frame.cmd, |
| 119 | frame.seq, |
| 120 | debug::hex(&frame.payload) |
| 121 | ) |
| 122 | }); |
| 123 | return Ok(Some(frame)); |
| 124 | } |
| 125 | let discarded = self.frames.take_discarded(); |
| 126 | if discarded > 0 { |
| 127 | debug::note(|| format!("discarded {discarded} bytes that were not a frame")); |
| 128 | } |
| 129 | match self.transport.recv(deadline)? { |
| 130 | Some(bytes) => self.frames.push(&bytes), |
| 131 | None => return Ok(None), |
| 132 | } |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | /// A frame nobody is waiting for: answer it if the buds expect an answer, else skip it. |
| 137 | /// Spec: realme_t310.md#buds-requests, #pushes |
| 138 | fn handle_unsolicited(&mut self, frame: Frame) -> Result<()> { |
| 139 | if frame.cmd & REPLY != 0 { |
| 140 | debug::note(|| format!("skipping late reply 0x{:04X}", frame.cmd)); |
| 141 | return Ok(()); |
| 142 | } |
| 143 | let answer = match frame.cmd { |
| 144 | TIME_REQUEST => { |
| 145 | let mut answer = vec![0x00]; |
| 146 | answer.extend((self.clock)().to_le_bytes()); |
| 147 | answer |
| 148 | } |
| 149 | APP_LIST_REQUEST => APP_LIST_ANSWER.to_vec(), |
| 150 | CAPABILITIES_REQUEST => CAPABILITIES_ANSWER.to_vec(), |
| 151 | SPATIAL_MODE_REQUEST => SPATIAL_MODE_ANSWER.to_vec(), |
| 152 | cmd if !KNOWN_GROUPS.contains(&(cmd >> 8)) => UNKNOWN_ANSWER.to_vec(), |
| 153 | _ => return Ok(()), // pushes and messages that need no answer |
| 154 | }; |
| 155 | debug::note(|| format!("answer 0x{:04X} with [{}]", frame.cmd, debug::hex(&answer))); |
| 156 | // Answered at once: some firmware drops the link when this takes too long. |
| 157 | self.transport |
| 158 | .send(&frame::encode(frame.cmd | REPLY, frame.seq, &answer)?) |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | #[cfg(test)] |
| 163 | mod tests { |
| 164 | use super::*; |
| 165 | use crate::transport::mock::{Script, Step, hex}; |
| 166 | |
| 167 | fn fixed_clock() -> u32 { |
| 168 | 1_750_000_000 |
| 169 | } |
| 170 | |
| 171 | #[test] |
| 172 | fn answers_the_time_request_while_waiting() { |
| 173 | // Vector: realme_t310.md#buds-requests |
| 174 | let script = Script::new(vec![ |
| 175 | Step::Tx(hex("AA 07 00 00 06 01 01 00 00")), |
| 176 | Step::Rx(hex("AA 07 00 00 00 05 02 00 00")), |
| 177 | Step::Tx(hex("AA 0C 00 00 00 85 02 05 00 00 80 E1 4E 68")), |
| 178 | Step::Rx(hex("AA 0A 00 00 06 81 01 03 00 00 01 01")), |
| 179 | ]); |
| 180 | let mut link = Link::new(script.transport(), fixed_clock); |
| 181 | assert_eq!(link.request(0x0106, &[]).unwrap(), hex("00 01 01")); |
| 182 | script.assert_done(); |
| 183 | } |
| 184 | |
| 185 | #[test] |
| 186 | fn skips_pushes_and_stale_replies() { |
| 187 | let script = Script::new(vec![ |
| 188 | Step::Tx(hex("AA 07 00 00 06 01 01 00 00")), |
| 189 | Step::Rx(hex("AA 0C 00 00 04 02 FF 05 00 03 01 01 08 00")), |
| 190 | Step::Rx(hex("AA 08 00 00 06 81 77 01 00 00")), |
| 191 | Step::Rx(hex("AA 08 00 00 06 81 01 01 00 00")), |
| 192 | ]); |
| 193 | let mut link = Link::new(script.transport(), fixed_clock); |
| 194 | assert_eq!(link.request(0x0106, &[]).unwrap(), hex("00")); |
| 195 | script.assert_done(); |
| 196 | } |
| 197 | |
| 198 | #[test] |
| 199 | fn unknown_requests_get_the_unknown_answer() { |
| 200 | let script = Script::new(vec![ |
| 201 | Step::Rx(hex("AA 07 00 00 34 12 09 00 00")), |
| 202 | Step::Tx(hex("AA 08 00 00 34 92 09 01 00 01")), |
| 203 | ]); |
| 204 | let mut link = Link::new(script.transport(), fixed_clock); |
| 205 | link.pump_until(Instant::now()).unwrap(); |
| 206 | script.assert_done(); |
| 207 | } |
| 208 | |
| 209 | #[test] |
| 210 | fn silence_is_a_timeout_and_a_drop_is_not_connected() { |
| 211 | let script = Script::new(vec![Step::Tx(hex("AA 07 00 00 06 01 01 00 00"))]); |
| 212 | let mut link = Link::new(script.transport(), fixed_clock); |
| 213 | assert!(matches!(link.request(0x0106, &[]), Err(Error::Timeout(_)))); |
| 214 | |
| 215 | let script = Script::new(vec![Step::Tx(hex("AA 07 00 00 06 01 01 00 00")), Step::Close]); |
| 216 | let mut link = Link::new(script.transport(), fixed_clock); |
| 217 | assert!(matches!(link.request(0x0106, &[]), Err(Error::NotConnected(_)))); |
| 218 | } |
| 219 | |
| 220 | #[test] |
| 221 | fn sequence_numbers_wrap_inside_budctls_range() { |
| 222 | let mut steps = Vec::new(); |
| 223 | for seq in (FIRST_SEQ..=LAST_SEQ).chain([FIRST_SEQ]) { |
| 224 | steps.push(Step::Tx(frame::encode(0x0100, seq, &[]).unwrap())); |
| 225 | } |
| 226 | let script = Script::new(steps); |
| 227 | let mut link = Link::new(script.transport(), fixed_clock); |
| 228 | for _ in FIRST_SEQ..=LAST_SEQ + 1 { |
| 229 | link.send(0x0100, &[]).unwrap(); |
| 230 | } |
| 231 | script.assert_done(); |
| 232 | } |
| 233 | } |