| 1 | //! Framing of the OPPO/OnePlus/realme protocol family. Spec: realme_t310.md#frame |
| 2 | //! |
| 3 | //! Holds no T310 tables. If a second model of this family is added, this file moves to |
| 4 | //! `devices/oppo_family/` (CLAUDE.md, "Device plugin contract"). |
| 5 | |
| 6 | use crate::error::{Error, Result}; |
| 7 | use crate::reader::Reader; |
| 8 | |
| 9 | /// Spec: realme_t310.md#frame |
| 10 | const START: u8 = 0xAA; |
| 11 | /// Spec: realme_t310.md#frame |
| 12 | const WHOLE: u8 = 0x00; |
| 13 | /// What the T310 puts in the slice byte of frames it starts itself (requests and pushes). The |
| 14 | /// rest is laid out as a whole message. Spec: realme_t310.md#frame-from-buds |
| 15 | const WHOLE_FROM_BUDS: u8 = 0x08; |
| 16 | /// Spec: realme_t310.md#frame-slices |
| 17 | const FIRST_SLICE: u8 = 0x01; |
| 18 | /// Spec: realme_t310.md#frame-slices |
| 19 | const MIDDLE_SLICE: u8 = 0x02; |
| 20 | /// Spec: realme_t310.md#frame-slices |
| 21 | const LAST_SLICE: u8 = 0x03; |
| 22 | /// Bytes after the length in a whole frame, before the payload: slice, 00, cmd (2), seq, |
| 23 | /// paylen (2). Spec: realme_t310.md#frame-whole |
| 24 | const WHOLE_HEADER: usize = 7; |
| 25 | /// Shortest slice frame: slice, 00, fsn, at least one byte. Spec: realme_t310.md#frame-receive |
| 26 | const MIN_SLICE_LEN: usize = 4; |
| 27 | /// Spec: realme_t310.md#frame-receive |
| 28 | const MAX_LEN: usize = 4096; |
| 29 | /// The length field is at most 3 bytes. Spec: realme_t310.md#frame-receive |
| 30 | const MAX_LEN_BYTES: usize = 3; |
| 31 | /// Senders slice messages that do not fit in this many bytes. budctl never slices, so it |
| 32 | /// refuses to build a bigger frame. Spec: realme_t310.md#frame-slices |
| 33 | const FRAME_BUDGET: usize = 660; |
| 34 | |
| 35 | /// One message: a command id, its sequence byte and its payload. |
| 36 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 37 | pub(super) struct Frame { |
| 38 | pub cmd: u16, |
| 39 | pub seq: u8, |
| 40 | pub payload: Vec<u8>, |
| 41 | } |
| 42 | |
| 43 | /// One whole (unsliced) frame. Spec: realme_t310.md#frame-whole |
| 44 | pub(super) fn encode(cmd: u16, seq: u8, payload: &[u8]) -> Result<Vec<u8>> { |
| 45 | let too_big = || Error::Protocol(format!("a {} byte payload does not fit in one frame", payload.len())); |
| 46 | let paylen = u16::try_from(payload.len()).map_err(|_| too_big())?; |
| 47 | let mut frame = vec![START]; |
| 48 | push_leb128(WHOLE_HEADER + payload.len(), &mut frame); |
| 49 | frame.extend([WHOLE, 0x00]); |
| 50 | frame.extend(cmd.to_le_bytes()); |
| 51 | frame.push(seq); |
| 52 | frame.extend(paylen.to_le_bytes()); |
| 53 | frame.extend(payload); |
| 54 | if frame.len() > FRAME_BUDGET { |
| 55 | return Err(too_big()); |
| 56 | } |
| 57 | Ok(frame) |
| 58 | } |
| 59 | |
| 60 | /// 7 bits per byte, low bits first; the top bit says another byte follows. |
| 61 | fn push_leb128(mut value: usize, out: &mut Vec<u8>) { |
| 62 | loop { |
| 63 | let low = u8::try_from(value & 0x7F).unwrap_or_default(); |
| 64 | value >>= 7; |
| 65 | if value == 0 { |
| 66 | out.push(low); |
| 67 | return; |
| 68 | } |
| 69 | out.push(low | 0x80); |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | enum Length { |
| 74 | /// The length and how many bytes it took. |
| 75 | Value(usize, usize), |
| 76 | /// More bytes are needed to finish the length. |
| 77 | Incomplete, |
| 78 | /// Not a valid length: more than three bytes. |
| 79 | Invalid, |
| 80 | } |
| 81 | |
| 82 | fn read_leb128(bytes: &[u8]) -> Length { |
| 83 | let mut value = 0; |
| 84 | for (i, &byte) in bytes.iter().take(MAX_LEN_BYTES).enumerate() { |
| 85 | value |= usize::from(byte & 0x7F) << (7 * i); |
| 86 | if byte & 0x80 == 0 { |
| 87 | return Length::Value(value, i + 1); |
| 88 | } |
| 89 | } |
| 90 | if bytes.len() < MAX_LEN_BYTES { |
| 91 | Length::Incomplete |
| 92 | } else { |
| 93 | Length::Invalid |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | /// Turns the byte stream from the buds back into messages. Spec: realme_t310.md#frame-receive |
| 98 | #[derive(Default)] |
| 99 | pub(super) struct Reassembler { |
| 100 | buffer: Vec<u8>, |
| 101 | /// Parts of a sliced message received so far. |
| 102 | sliced: Option<Vec<u8>>, |
| 103 | /// Bytes thrown away while looking for a frame start (shown with `--debug`). |
| 104 | discarded: usize, |
| 105 | } |
| 106 | |
| 107 | impl Reassembler { |
| 108 | pub(super) fn push(&mut self, bytes: &[u8]) { |
| 109 | self.buffer.extend_from_slice(bytes); |
| 110 | } |
| 111 | |
| 112 | /// How many bytes were discarded since the last call. |
| 113 | pub(super) fn take_discarded(&mut self) -> usize { |
| 114 | std::mem::take(&mut self.discarded) |
| 115 | } |
| 116 | |
| 117 | /// The next complete message, if one is buffered. |
| 118 | pub(super) fn next_frame(&mut self) -> Option<Frame> { |
| 119 | loop { |
| 120 | // Throw away everything before the next frame start. |
| 121 | let start = self.buffer.iter().position(|&b| b == START); |
| 122 | let skip = start.unwrap_or(self.buffer.len()); |
| 123 | self.discarded += skip; |
| 124 | self.buffer.drain(..skip); |
| 125 | if self.buffer.is_empty() { |
| 126 | return None; |
| 127 | } |
| 128 | |
| 129 | let (len, len_bytes) = match read_leb128(self.buffer.get(1..).unwrap_or_default()) { |
| 130 | Length::Value(len, len_bytes) => (len, len_bytes), |
| 131 | Length::Incomplete => return None, |
| 132 | Length::Invalid => { |
| 133 | self.drop_start(); |
| 134 | continue; |
| 135 | } |
| 136 | }; |
| 137 | let &slice = self.buffer.get(1 + len_bytes)?; |
| 138 | let min_len = if is_whole(slice) { WHOLE_HEADER } else { MIN_SLICE_LEN }; |
| 139 | if len < min_len || len > MAX_LEN { |
| 140 | self.drop_start(); |
| 141 | continue; |
| 142 | } |
| 143 | |
| 144 | let total = 1 + len_bytes + len; |
| 145 | if self.buffer.len() < total { |
| 146 | return None; |
| 147 | } |
| 148 | let body: Vec<u8> = self.buffer.drain(..total).skip(1 + len_bytes).collect(); |
| 149 | if let Some(frame) = self.body(&body) { |
| 150 | return Some(frame); |
| 151 | } |
| 152 | } |
| 153 | } |
| 154 | |
| 155 | /// Drop a false frame start and search on. |
| 156 | fn drop_start(&mut self) { |
| 157 | self.buffer.drain(..1); |
| 158 | self.discarded += 1; |
| 159 | } |
| 160 | |
| 161 | /// Spec: realme_t310.md#frame, #frame-slices |
| 162 | fn body(&mut self, body: &[u8]) -> Option<Frame> { |
| 163 | let mut r = Reader::new(body, "frame"); |
| 164 | let slice = r.u8().ok()?; |
| 165 | let _zero = r.u8().ok()?; |
| 166 | if is_whole(slice) { |
| 167 | return parse_message(r.rest()); |
| 168 | } |
| 169 | let _slice_number = r.u8().ok()?; |
| 170 | let part = r.rest(); |
| 171 | match slice { |
| 172 | FIRST_SLICE => self.sliced = Some(part.to_vec()), |
| 173 | MIDDLE_SLICE => { |
| 174 | if let Some(joined) = &mut self.sliced { |
| 175 | joined.extend_from_slice(part); |
| 176 | } |
| 177 | } |
| 178 | LAST_SLICE => { |
| 179 | let mut joined = self.sliced.take()?; |
| 180 | joined.extend_from_slice(part); |
| 181 | return parse_message(&joined); |
| 182 | } |
| 183 | _ => {} |
| 184 | } |
| 185 | None |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | /// Spec: realme_t310.md#frame-whole, #frame-from-buds |
| 190 | fn is_whole(slice: u8) -> bool { |
| 191 | slice == WHOLE || slice == WHOLE_FROM_BUDS |
| 192 | } |
| 193 | |
| 194 | /// `cmd(2, LE) seq paylen(2, LE) payload`. A payload shorter than `paylen` is kept as it is. |
| 195 | /// Spec: realme_t310.md#frame-receive |
| 196 | fn parse_message(bytes: &[u8]) -> Option<Frame> { |
| 197 | let mut r = Reader::new(bytes, "message"); |
| 198 | let cmd = r.u16_le().ok()?; |
| 199 | let seq = r.u8().ok()?; |
| 200 | let paylen = usize::from(r.u16_le().ok()?); |
| 201 | let rest = r.rest(); |
| 202 | let payload = rest.get(..paylen).unwrap_or(rest).to_vec(); |
| 203 | Some(Frame { cmd, seq, payload }) |
| 204 | } |
| 205 | |
| 206 | #[cfg(test)] |
| 207 | mod tests { |
| 208 | use super::*; |
| 209 | use crate::transport::mock::hex; |
| 210 | |
| 211 | fn frames(input: &[u8]) -> Vec<Frame> { |
| 212 | let mut r = Reassembler::default(); |
| 213 | r.push(input); |
| 214 | std::iter::from_fn(|| r.next_frame()).collect() |
| 215 | } |
| 216 | |
| 217 | fn frame(cmd: u16, seq: u8, payload: &str) -> Frame { |
| 218 | Frame { |
| 219 | cmd, |
| 220 | seq, |
| 221 | payload: hex(payload), |
| 222 | } |
| 223 | } |
| 224 | |
| 225 | // Vectors: realme_t310.md#frame-whole and #frame-long. |
| 226 | |
| 227 | #[test] |
| 228 | fn encodes_the_documented_examples() { |
| 229 | assert_eq!(encode(0x0100, 0x01, &[]).unwrap(), hex("AA 07 00 00 00 01 01 00 00")); |
| 230 | assert_eq!( |
| 231 | encode(0x0404, 0x06, &hex("01 01 01")).unwrap(), |
| 232 | hex("AA 0A 00 00 04 04 06 03 00 01 01 01") |
| 233 | ); |
| 234 | } |
| 235 | |
| 236 | #[test] |
| 237 | fn long_lengths_use_leb128() { |
| 238 | let payload = vec![0u8; 124]; |
| 239 | let encoded = encode(0x0418, 0x03, &payload).unwrap(); |
| 240 | assert_eq!(encoded.get(..3), Some(&hex("AA 83 01")[..])); |
| 241 | assert_eq!( |
| 242 | frames(&encoded), |
| 243 | vec![Frame { |
| 244 | cmd: 0x0418, |
| 245 | seq: 0x03, |
| 246 | payload |
| 247 | }] |
| 248 | ); |
| 249 | |
| 250 | let mut reply = hex("AA 81 02 00 00 22 81 05 FA 00"); |
| 251 | reply.extend(vec![0x11; 250]); |
| 252 | let decoded = frames(&reply); |
| 253 | assert_eq!(decoded.len(), 1); |
| 254 | assert_eq!(decoded.first().map(|f| f.payload.len()), Some(250)); |
| 255 | } |
| 256 | |
| 257 | #[test] |
| 258 | fn refuses_frames_that_would_need_slicing() { |
| 259 | assert!(encode(0x0418, 1, &[0; 700]).is_err()); |
| 260 | } |
| 261 | |
| 262 | #[test] |
| 263 | fn round_trips_every_payload_size() { |
| 264 | for len in 0..=640 { |
| 265 | let payload: Vec<u8> = (0..len).map(|i| u8::try_from(i % 251).unwrap()).collect(); |
| 266 | let encoded = encode(0x0106, 0x42, &payload).unwrap(); |
| 267 | assert_eq!( |
| 268 | frames(&encoded), |
| 269 | vec![Frame { |
| 270 | cmd: 0x0106, |
| 271 | seq: 0x42, |
| 272 | payload |
| 273 | }] |
| 274 | ); |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | #[test] |
| 279 | fn decodes_a_push() { |
| 280 | assert_eq!( |
| 281 | frames(&hex("AA 0C 00 00 04 02 FF 05 00 03 01 01 08 00")), |
| 282 | vec![frame(0x0204, 0xFF, "03 01 01 08 00")] |
| 283 | ); |
| 284 | } |
| 285 | |
| 286 | /// Captured on the owner's T310 (realme_t310.md#frame-from-buds): a reply and a request |
| 287 | /// from the buds in one read, then an ack and a battery push in another. |
| 288 | #[test] |
| 289 | fn decodes_frames_the_buds_start() { |
| 290 | assert_eq!( |
| 291 | frames(&hex( |
| 292 | "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" |
| 293 | )), |
| 294 | vec![ |
| 295 | frame(0x8100, 0x01, "00 BF 1F 5A 20 45 04 40 00"), |
| 296 | frame(0x0501, 0x0F, "") |
| 297 | ] |
| 298 | ); |
| 299 | assert_eq!( |
| 300 | frames(&hex( |
| 301 | "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" |
| 302 | )), |
| 303 | vec![ |
| 304 | frame(0x8205, 0x04, "00 04 01 00 02 00 03 00 04 00"), |
| 305 | frame(0x0204, 0x10, "01 03 02 64 01 64 03 00") |
| 306 | ] |
| 307 | ); |
| 308 | } |
| 309 | |
| 310 | #[test] |
| 311 | fn splits_and_joins_reads() { |
| 312 | let two = |
| 313 | hex("AA 0D 00 00 04 02 FF 06 00 01 02 01 64 02 64 AA 0F 00 00 04 02 0C 08 00 02 03 01 07 02 07 03 04"); |
| 314 | assert_eq!( |
| 315 | frames(&two), |
| 316 | vec![ |
| 317 | frame(0x0204, 0xFF, "01 02 01 64 02 64"), |
| 318 | frame(0x0204, 0x0C, "02 03 01 07 02 07 03 04") |
| 319 | ] |
| 320 | ); |
| 321 | |
| 322 | // The same bytes, one at a time. |
| 323 | let mut r = Reassembler::default(); |
| 324 | let mut out = Vec::new(); |
| 325 | for byte in &two { |
| 326 | r.push(&[*byte]); |
| 327 | out.extend(std::iter::from_fn(|| r.next_frame())); |
| 328 | } |
| 329 | assert_eq!(out.len(), 2); |
| 330 | } |
| 331 | |
| 332 | #[test] |
| 333 | fn skips_noise_and_bad_lengths() { |
| 334 | let real = "AA 0D 00 00 04 02 FF 06 00 01 02 01 64 02 64"; |
| 335 | let mut r = Reassembler::default(); |
| 336 | r.push(&hex(&format!("13 37 {real}"))); |
| 337 | assert_eq!(r.next_frame(), Some(frame(0x0204, 0xFF, "01 02 01 64 02 64"))); |
| 338 | assert_eq!(r.take_discarded(), 2); |
| 339 | |
| 340 | assert_eq!(frames(&hex(&format!("AA 80 80 80 {real}"))).len(), 1); |
| 341 | assert!(frames(&hex("AA 00")).is_empty()); |
| 342 | assert!(frames(&hex("AA 05 00 00 23 81 00")).is_empty()); |
| 343 | } |
| 344 | |
| 345 | #[test] |
| 346 | fn short_payload_is_cut_to_what_is_present() { |
| 347 | assert_eq!( |
| 348 | frames(&hex("AA 09 00 00 06 81 01 09 00 07 07")), |
| 349 | vec![frame(0x8106, 0x01, "07 07")] |
| 350 | ); |
| 351 | } |
| 352 | |
| 353 | #[test] |
| 354 | fn joins_slices_and_drops_orphans() { |
| 355 | // cmd 0x8108, seq 05, paylen 4, payload 00 01 02 03, split in three slices. |
| 356 | let sliced = hex("AA 07 01 00 00 08 81 05 04 AA 05 02 00 01 00 00 AA 06 03 00 02 01 02 03"); |
| 357 | assert_eq!(frames(&sliced), vec![frame(0x8108, 0x05, "00 01 02 03")]); |
| 358 | |
| 359 | // A last slice with no first slice is dropped; the next whole frame still decodes. |
| 360 | let orphan = hex("AA 06 03 00 02 00 02 03 AA 07 00 00 00 01 01 00 00"); |
| 361 | assert_eq!(frames(&orphan), vec![frame(0x0100, 0x01, "")]); |
| 362 | } |
| 363 | } |