umsh_ulcp/
gatt.rs

1//! Service-agnostic frame segmentation and reassembly for GATT links.
2//!
3//! This implements the GATT Frame Transport from
4//! `docs/protocol/src/ulcp-ble.md`: one SAR header octet per ATT
5//! value, no HDLC escaping or checksum, and bounded reassembly state.
6
7/// SAR value for a frame contained in one segment.
8pub const SAR_COMPLETE: u8 = 0;
9/// SAR value for the first segment of a multi-segment frame.
10pub const SAR_FIRST: u8 = 1;
11/// SAR value for a middle segment of a multi-segment frame.
12pub const SAR_CONT: u8 = 2;
13/// SAR value for the final segment of a multi-segment frame.
14pub const SAR_LAST: u8 = 3;
15
16const SAR_SHIFT: u8 = 6;
17const RESERVED_MASK: u8 = 0x3f;
18
19/// Maximum reassembled frame size for the ULCP GATT Service.
20pub const MAX_FRAME: usize = 512;
21
22/// Return a UMSH UUID with `slot` spliced into the second UUID group.
23pub const fn uuid(slot: u16) -> u128 {
24    0x21EB_6B15_0000_4CCF_92E4_A079_171B_EC97u128 | ((slot as u128) << 80)
25}
26
27/// ULCP GATT Service UUID.
28pub const SERVICE_UUID: u128 = uuid(0x0001);
29/// ULCP Frame In characteristic UUID.
30pub const FRAME_IN_UUID: u128 = uuid(0x0002);
31/// ULCP Frame Out characteristic UUID.
32pub const FRAME_OUT_UUID: u128 = uuid(0x0003);
33
34/// One header-prefixed GATT frame segment.
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub struct Segment<'a> {
37    sar: u8,
38    payload: &'a [u8],
39}
40
41impl<'a> Segment<'a> {
42    /// Encoded segment header octet.
43    pub const fn header(&self) -> u8 {
44        self.sar << SAR_SHIFT
45    }
46
47    /// Frame bytes carried by this segment.
48    pub const fn payload(&self) -> &'a [u8] {
49        self.payload
50    }
51
52    /// Write the header and payload to `out`, returning the encoded length.
53    pub fn write_to(&self, out: &mut [u8]) -> Result<usize, EncodeError> {
54        let len = self.payload.len() + 1;
55        if out.len() < len {
56            return Err(EncodeError::BufferTooSmall);
57        }
58        out[0] = self.header();
59        out[1..len].copy_from_slice(self.payload);
60        Ok(len)
61    }
62}
63
64/// Segment encoding failure.
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum EncodeError {
67    /// The caller-provided destination cannot hold the segment.
68    BufferTooSmall,
69}
70
71/// Iterator returned by [`segments`].
72pub struct Segments<'a> {
73    frame: &'a [u8],
74    seg_payload: usize,
75    offset: usize,
76    emitted_empty: bool,
77}
78
79impl<'a> Iterator for Segments<'a> {
80    type Item = Segment<'a>;
81
82    fn next(&mut self) -> Option<Self::Item> {
83        if self.frame.is_empty() {
84            if self.emitted_empty {
85                return None;
86            }
87            self.emitted_empty = true;
88            return Some(Segment {
89                sar: SAR_COMPLETE,
90                payload: self.frame,
91            });
92        }
93        if self.offset >= self.frame.len() {
94            return None;
95        }
96
97        let start = self.offset;
98        let end = start.saturating_add(self.seg_payload).min(self.frame.len());
99        self.offset = end;
100        let sar = if self.frame.len() <= self.seg_payload {
101            SAR_COMPLETE
102        } else if start == 0 {
103            SAR_FIRST
104        } else if end == self.frame.len() {
105            SAR_LAST
106        } else {
107            SAR_CONT
108        };
109        Some(Segment {
110            sar,
111            payload: &self.frame[start..end],
112        })
113    }
114
115    fn size_hint(&self) -> (usize, Option<usize>) {
116        let remaining = if self.frame.is_empty() {
117            usize::from(!self.emitted_empty)
118        } else {
119            (self.frame.len() - self.offset).div_ceil(self.seg_payload)
120        };
121        (remaining, Some(remaining))
122    }
123}
124
125impl ExactSizeIterator for Segments<'_> {}
126
127/// Split one frame into segments carrying at most `seg_payload` frame bytes.
128///
129/// `seg_payload` is the negotiated usable ATT value size minus the header
130/// octet (`ATT_MTU - 3 - 1`). It must be nonzero in every build profile.
131pub fn segments(frame: &[u8], seg_payload: usize) -> Segments<'_> {
132    assert!(seg_payload >= 1, "GATT segment payload must be nonzero");
133    Segments {
134        frame,
135        seg_payload,
136        offset: 0,
137        emitted_empty: false,
138    }
139}
140
141/// Segment decoding/reassembly failure.
142#[derive(Clone, Copy, Debug, PartialEq, Eq)]
143pub enum DecodeError {
144    /// Reserved header bits were nonzero.
145    ReservedBits,
146    /// A continuation/final segment arrived without a partial frame.
147    Orphan,
148    /// The reassembled frame exceeded the configured bound.
149    TooLong,
150    /// The ATT value did not contain a header octet.
151    Runt,
152}
153
154/// Bounded reassembly state for one GATT characteristic.
155pub struct Reassembler<const N: usize> {
156    buf: [u8; N],
157    len: usize,
158    in_progress: bool,
159    discarding_overflow: bool,
160}
161
162impl<const N: usize> Default for Reassembler<N> {
163    fn default() -> Self {
164        Self::new()
165    }
166}
167
168impl<const N: usize> Reassembler<N> {
169    /// Construct an idle reassembler.
170    pub const fn new() -> Self {
171        Self {
172            buf: [0; N],
173            len: 0,
174            in_progress: false,
175            discarding_overflow: false,
176        }
177    }
178
179    /// Discard all partial state.
180    pub fn reset(&mut self) {
181        self.len = 0;
182        self.in_progress = false;
183        self.discarding_overflow = false;
184    }
185
186    /// Push one complete ATT value.
187    ///
188    /// Returns a borrowed completed frame, a one-shot error, or `None` while a
189    /// segmented frame remains in progress. Every error resets normal partial
190    /// state; overflow additionally ignores continuations until a new start.
191    pub fn push(&mut self, segment: &[u8]) -> Option<Result<&[u8], DecodeError>> {
192        let Some((&header, payload)) = segment.split_first() else {
193            self.reset();
194            return Some(Err(DecodeError::Runt));
195        };
196        if header & RESERVED_MASK != 0 {
197            self.reset();
198            return Some(Err(DecodeError::ReservedBits));
199        }
200        let sar = header >> SAR_SHIFT;
201
202        if matches!(sar, SAR_COMPLETE | SAR_FIRST) {
203            self.reset();
204        } else if self.discarding_overflow {
205            return None;
206        } else if !self.in_progress {
207            return Some(Err(DecodeError::Orphan));
208        }
209
210        if payload.len() > N.saturating_sub(self.len) {
211            self.len = 0;
212            self.in_progress = false;
213            self.discarding_overflow = true;
214            return Some(Err(DecodeError::TooLong));
215        }
216        self.buf[self.len..self.len + payload.len()].copy_from_slice(payload);
217        self.len += payload.len();
218
219        match sar {
220            SAR_COMPLETE => {
221                self.in_progress = false;
222                Some(Ok(&self.buf[..self.len]))
223            }
224            SAR_FIRST | SAR_CONT => {
225                self.in_progress = true;
226                None
227            }
228            SAR_LAST => {
229                self.in_progress = false;
230                Some(Ok(&self.buf[..self.len]))
231            }
232            _ => unreachable!(),
233        }
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    fn encoded(segment: Segment<'_>) -> std::vec::Vec<u8> {
242        let mut out = std::vec![0; segment.payload().len() + 1];
243        let len = segment.write_to(&mut out).unwrap();
244        out.truncate(len);
245        out
246    }
247
248    fn round_trip(frame: &[u8], size: usize) {
249        let mut decoder = Reassembler::<MAX_FRAME>::new();
250        let mut result = None;
251        for segment in segments(frame, size) {
252            if let Some(decoded) = decoder.push(&encoded(segment)) {
253                result = Some(decoded.unwrap().to_vec());
254            }
255        }
256        assert_eq!(result.as_deref(), Some(frame));
257    }
258
259    #[test]
260    fn round_trip_common_mtu_payloads() {
261        let frame: std::vec::Vec<u8> = (0..512).map(|n| n as u8).collect();
262        for size in [19, 243, 511] {
263            round_trip(&frame, size);
264        }
265    }
266
267    #[test]
268    fn complete_exact_empty_and_one_byte_payloads() {
269        round_trip(b"one segment", 19);
270        round_trip(&[7; 38], 19);
271        round_trip(&[], 19);
272        round_trip(&[1, 2, 3], 1);
273        let empty = segments(&[], 1).next().unwrap();
274        assert_eq!(empty.header(), 0);
275        assert!(empty.payload().is_empty());
276    }
277
278    #[test]
279    fn empty_middle_segment_is_legal() {
280        let mut r = Reassembler::<8>::new();
281        assert_eq!(r.push(&[SAR_FIRST << 6, 1]), None);
282        assert_eq!(r.push(&[SAR_CONT << 6]), None);
283        assert_eq!(r.push(&[SAR_LAST << 6, 2]), Some(Ok(&[1, 2][..])));
284    }
285
286    #[test]
287    fn rejects_reserved_orphan_and_runt() {
288        let mut r = Reassembler::<8>::new();
289        assert_eq!(r.push(&[1, 2]), Some(Err(DecodeError::ReservedBits)));
290        assert_eq!(r.push(&[SAR_CONT << 6, 2]), Some(Err(DecodeError::Orphan)));
291        assert_eq!(r.push(&[SAR_LAST << 6]), Some(Err(DecodeError::Orphan)));
292        assert_eq!(r.push(&[]), Some(Err(DecodeError::Runt)));
293        assert_eq!(r.push(&[SAR_FIRST << 6, 1]), None);
294        assert_eq!(r.push(&[]), Some(Err(DecodeError::Runt)));
295        assert_eq!(r.push(&[SAR_LAST << 6, 2]), Some(Err(DecodeError::Orphan)));
296    }
297
298    #[test]
299    fn first_discards_partial_and_restarts() {
300        let mut r = Reassembler::<8>::new();
301        assert_eq!(r.push(&[SAR_FIRST << 6, 1]), None);
302        assert_eq!(r.push(&[SAR_FIRST << 6, 2]), None);
303        assert_eq!(r.push(&[SAR_LAST << 6, 3]), Some(Ok(&[2, 3][..])));
304    }
305
306    #[test]
307    fn overflow_reports_once_then_recovers_at_start() {
308        let mut r = Reassembler::<3>::new();
309        assert_eq!(r.push(&[SAR_FIRST << 6, 1, 2]), None);
310        assert_eq!(
311            r.push(&[SAR_CONT << 6, 3, 4]),
312            Some(Err(DecodeError::TooLong))
313        );
314        assert_eq!(r.push(&[SAR_LAST << 6, 5]), None);
315        assert_eq!(r.push(&[SAR_COMPLETE << 6, 9]), Some(Ok(&[9][..])));
316    }
317
318    #[test]
319    #[should_panic(expected = "GATT segment payload must be nonzero")]
320    fn zero_segment_payload_panics_unconditionally() {
321        let _ = segments(b"frame", 0);
322    }
323
324    #[test]
325    fn uuid_literals_match_spec() {
326        assert_eq!(SERVICE_UUID, 0x21EB6B1500014CCF92E4A079171BEC97);
327        assert_eq!(FRAME_IN_UUID, 0x21EB6B1500024CCF92E4A079171BEC97);
328        assert_eq!(FRAME_OUT_UUID, 0x21EB6B1500034CCF92E4A079171BEC97);
329    }
330}