Skip to main content

serial_codec/
cmux.rs

1//! 3GPP TS 27.010 (GSM 07.10) basic-option frame encode/decode.
2//!
3//! Frame: `F9 | address | control | length (1–2 octets, EA) | info | FCS | F9`.
4//! No byte stuffing: the length field delimits the info, so payload bytes may be 0xF9.
5//! FCS is CRC-8 (reflected poly 0x07, i.e. 0xE0) over address+control+length; for UI
6//! frames the info field is included too.
7
8pub const FLAG: u8 = 0xF9;
9
10/// Frame types (control field with the P/F bit cleared).
11pub const SABM: u8 = 0x2F;
12pub const UA: u8 = 0x63;
13pub const DM: u8 = 0x0F;
14pub const DISC: u8 = 0x43;
15pub const UIH: u8 = 0xEF;
16pub const UI: u8 = 0x03;
17pub const PF: u8 = 0x10;
18
19/// Largest info field the basic option can describe: the 2-octet length field holds
20/// 15 bits (27.010 §5.2.1.5).
21pub const MAX_INFO: usize = 32_767;
22/// Default receive limit for the info field: room for a 1500-byte IP packet in PPP
23/// framing. A corrupt length octet (noise, a lost flag) then costs one dropped frame,
24/// not a stall until 32 KiB more arrive. Raise it with [`Deframer::with_max_info`] when
25/// `AT+CMUX` sets a larger N1.
26pub const DEFAULT_MAX_RX_INFO: usize = 1_509;
27/// Info bytes per transmitted frame. Basic option defaults N1 to 31 unless `AT+CMUX`
28/// sets it, so 31 is the only size every modem accepts.
29// ponytail: fixed N1=31; parse N1 from the AT+CMUX command if throughput matters.
30pub const TX_N1: usize = 31;
31
32const fn crc_table() -> [u8; 256] {
33    let mut table = [0u8; 256];
34    let mut i = 0;
35    while i < 256 {
36        let mut crc = i as u8;
37        let mut bit = 0;
38        while bit < 8 {
39            crc = if crc & 1 != 0 {
40                (crc >> 1) ^ 0xE0
41            } else {
42                crc >> 1
43            };
44            bit += 1;
45        }
46        table[i] = crc;
47        i += 1;
48    }
49    table
50}
51
52static CRC_TABLE: [u8; 256] = crc_table();
53
54pub fn fcs(bytes: &[u8]) -> u8 {
55    let crc = bytes
56        .iter()
57        .fold(0xFFu8, |crc, &b| CRC_TABLE[(crc ^ b) as usize]);
58    0xFF - crc
59}
60
61pub fn encode_address(dlci: u8, cr: bool) -> u8 {
62    ((dlci & 0x3F) << 2) | (u8::from(cr) << 1) | 0x01
63}
64
65pub fn decode_address(addr: u8) -> (u8, bool) {
66    ((addr >> 2) & 0x3F, addr & 0x02 != 0)
67}
68
69/// One frame. `cr` is the address C/R bit; as initiator we set it on commands and UIH.
70///
71/// # Panics
72/// If `info` is longer than [`MAX_INFO`] (use [`encode_uih`] to split data).
73pub fn encode_frame(dlci: u8, cr: bool, control: u8, info: &[u8]) -> Vec<u8> {
74    assert!(info.len() <= MAX_INFO, "cmux info field too long");
75    let mut header = vec![encode_address(dlci, cr), control];
76    let len = info.len();
77    if len <= 127 {
78        header.push(((len as u8) << 1) | 0x01);
79    } else {
80        header.push(((len & 0x7F) as u8) << 1);
81        header.push((len >> 7) as u8);
82    }
83    let check = if control & !PF == UI {
84        fcs(&[header.as_slice(), info].concat())
85    } else {
86        fcs(&header)
87    };
88    let mut frame = Vec::with_capacity(header.len() + len + 3);
89    frame.push(FLAG);
90    frame.extend_from_slice(&header);
91    frame.extend_from_slice(info);
92    frame.push(check);
93    frame.push(FLAG);
94    frame
95}
96
97/// UIH data frames for `payload`, split into [`TX_N1`]-sized info fields.
98pub fn encode_uih(dlci: u8, payload: &[u8]) -> Vec<u8> {
99    if payload.is_empty() {
100        return encode_frame(dlci, true, UIH, &[]);
101    }
102    payload
103        .chunks(TX_N1)
104        .flat_map(|chunk| encode_frame(dlci, true, UIH, chunk))
105        .collect()
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct DecodedFrame {
110    pub dlci: u8,
111    /// Address C/R bit.
112    pub cr: bool,
113    /// Control field with the P/F bit cleared (see [`SABM`], [`UA`], [`UIH`] …).
114    pub kind: u8,
115    pub payload: Vec<u8>,
116}
117
118/// Streaming deframer: keeps partial frames across reads and resyncs on garbage.
119pub struct Deframer {
120    buf: Vec<u8>,
121    max_info: usize,
122}
123
124impl Default for Deframer {
125    fn default() -> Self {
126        Self::with_max_info(DEFAULT_MAX_RX_INFO)
127    }
128}
129
130impl Deframer {
131    /// Frames whose length field exceeds `max_info` (the N1 given to `AT+CMUX`, at most
132    /// [`MAX_INFO`]) are dropped as corrupt.
133    pub fn with_max_info(max_info: usize) -> Self {
134        Self {
135            buf: Vec::new(),
136            max_info: max_info.clamp(1, MAX_INFO),
137        }
138    }
139
140    pub fn feed(&mut self, chunk: &[u8]) -> Vec<DecodedFrame> {
141        self.buf.extend_from_slice(chunk);
142        let mut out = Vec::new();
143        // Read offset into `buf`; consumed bytes are dropped once, at the end.
144        let mut pos = 0;
145        loop {
146            // Skip everything before the next flag.
147            match self.buf[pos..].iter().position(|&b| b == FLAG) {
148                Some(i) => pos += i,
149                None => {
150                    pos = self.buf.len();
151                    break;
152                }
153            }
154            // Collapse runs of flags (closing flag + opening flag, or idle fill).
155            let flags = self.buf[pos..].iter().take_while(|&&b| b == FLAG).count();
156            pos += flags - 1;
157            match self.parse_one(pos) {
158                Parse::Need => break,
159                // Not a frame start after all: skip this flag and rescan.
160                Parse::Bad => pos += 1,
161                Parse::Frame(frame, used) => {
162                    // Keep the closing flag: it may also open the next frame.
163                    pos += used - 1;
164                    out.push(frame);
165                }
166            }
167        }
168        self.buf.drain(..pos);
169        out
170    }
171
172    fn parse_one(&self, pos: usize) -> Parse {
173        let b = &self.buf[pos..];
174        if b.len() < 4 {
175            return Parse::Need;
176        }
177        let (addr, control) = (b[1], b[2]);
178        if addr & 0x01 == 0 {
179            return Parse::Bad;
180        }
181        let (len, len_octets) = if b[3] & 0x01 != 0 {
182            ((b[3] >> 1) as usize, 1)
183        } else {
184            if b.len() < 5 {
185                return Parse::Need;
186            }
187            (((b[3] >> 1) as usize) | ((b[4] as usize) << 7), 2)
188        };
189        if len > self.max_info {
190            return Parse::Bad;
191        }
192        let header_end = 3 + len_octets;
193        let total = header_end + len + 2; // + FCS + closing flag
194        if b.len() < total {
195            return Parse::Need;
196        }
197        let info = &b[header_end..header_end + len];
198        let kind = control & !PF;
199        let expected = if kind == UI {
200            fcs(&b[1..header_end + len])
201        } else {
202            fcs(&b[1..header_end])
203        };
204        if b[header_end + len] != expected || b[total - 1] != FLAG {
205            return Parse::Bad;
206        }
207        let (dlci, cr) = decode_address(addr);
208        Parse::Frame(
209            DecodedFrame {
210                dlci,
211                cr,
212                kind,
213                payload: info.to_vec(),
214            },
215            total,
216        )
217    }
218}
219
220enum Parse {
221    Need,
222    Bad,
223    Frame(DecodedFrame, usize),
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    /// 27.010 reference: SABM with P on DLCI 0 is `F9 03 3F 01 1C F9`.
231    #[test]
232    fn sabm_dlci0_matches_spec_vector() {
233        assert_eq!(
234            encode_frame(0, true, SABM | PF, &[]),
235            vec![0xF9, 0x03, 0x3F, 0x01, 0x1C, 0xF9]
236        );
237    }
238
239    #[test]
240    fn uih_roundtrip_with_flag_bytes_split_across_reads() {
241        let payload = b"AT\r\xF9\xF9 data";
242        let frame = encode_uih(1, payload);
243        let mut def = Deframer::default();
244        let (a, b) = frame.split_at(5);
245        assert!(def.feed(a).is_empty());
246        let frames = def.feed(b);
247        assert_eq!(frames.len(), 1);
248        assert_eq!(frames[0].dlci, 1);
249        assert_eq!(frames[0].kind, UIH);
250        assert_eq!(frames[0].payload, payload);
251    }
252
253    #[test]
254    fn long_payload_is_split_into_n1_frames_and_back_to_back_frames_share_flags() {
255        let payload: Vec<u8> = (0..100u8).collect();
256        let mut stream = encode_uih(2, &payload);
257        // A modem may send frames with a single shared flag between them.
258        let second = encode_uih(3, b"OK");
259        stream.extend_from_slice(&second[1..]);
260        let frames = Deframer::default().feed(&stream);
261        let data: Vec<u8> = frames
262            .iter()
263            .filter(|f| f.dlci == 2)
264            .flat_map(|f| f.payload.clone())
265            .collect();
266        assert_eq!(data, payload);
267        assert!(frames.iter().all(|f| f.payload.len() <= TX_N1));
268        assert_eq!(frames.last().unwrap().payload, b"OK");
269    }
270
271    #[test]
272    fn two_octet_length_roundtrip() {
273        let info = vec![0x55u8; 200];
274        let frame = encode_frame(5, true, UIH, &info);
275        assert_eq!(frame[3] & 0x01, 0, "EA must be 0 for a 2-octet length");
276        let frames = Deframer::default().feed(&frame);
277        assert_eq!(frames[0].payload, info);
278    }
279
280    #[test]
281    fn garbage_and_bad_fcs_are_skipped() {
282        let mut stream = vec![0x00, 0xF9, 0x12, 0xF9];
283        let mut bad = encode_uih(1, b"x");
284        let fcs_at = bad.len() - 2;
285        bad[fcs_at] ^= 0xFF;
286        stream.extend_from_slice(&bad);
287        stream.extend_from_slice(&encode_uih(1, b"good"));
288        let frames = Deframer::default().feed(&stream);
289        assert_eq!(frames.len(), 1);
290        assert_eq!(frames[0].payload, b"good");
291    }
292
293    /// The old resync did `Vec::remove(0)` per bad flag: 1 MiB of `F9 00` took 17 s.
294    #[test]
295    fn resync_over_a_large_noise_read_is_linear() {
296        let mut stream: Vec<u8> = [FLAG, 0x00].repeat(1 << 19);
297        stream.extend_from_slice(&encode_uih(1, b"good"));
298        let t = std::time::Instant::now();
299        let frames = Deframer::default().feed(&stream);
300        assert!(
301            t.elapsed() < std::time::Duration::from_secs(5),
302            "{:?}",
303            t.elapsed()
304        );
305        assert_eq!(frames.len(), 1);
306        assert_eq!(frames[0].payload, b"good");
307    }
308
309    #[test]
310    fn info_field_is_limited_to_15_bits() {
311        let info = vec![0x55u8; MAX_INFO];
312        let frames = Deframer::with_max_info(MAX_INFO).feed(&encode_frame(1, true, UIH, &info));
313        assert_eq!(frames[0].payload, info);
314        let too_long = std::panic::catch_unwind(|| encode_frame(1, true, UIH, &[0; MAX_INFO + 1]));
315        assert!(too_long.is_err(), "32768 bytes would encode as length 0");
316    }
317}