Skip to main content

wireless_mbus_link_layer/
lib.rs

1#![cfg_attr(not(any(feature = "std", test)), no_std)]
2
3use m_bus_core::{DeviceType, Function, IdentificationNumber, ManufacturerCode};
4
5/// CRC-16/EN13757 used in wireless M-Bus Format A frames.
6/// Polynomial: 0x3D65, Init: 0x0000, XorOut: 0xFFFF, RefIn: false, RefOut: false.
7fn crc16_en13757(data: &[u8]) -> u16 {
8    let mut crc: u16 = 0x0000;
9    for &byte in data {
10        crc ^= (byte as u16) << 8;
11        for _ in 0..8 {
12            if crc & 0x8000 != 0 {
13                crc = (crc << 1) ^ 0x3D65;
14            } else {
15                crc <<= 1;
16            }
17        }
18    }
19    crc ^ 0xFFFF
20}
21
22/// Return the start offset of a trailing frame CRC when the final two bytes
23/// validate against every preceding byte.
24pub fn trailing_frame_crc_start(data: &[u8]) -> Option<usize> {
25    let crc_start = data.len().checked_sub(2)?;
26    if crc_start < 10 {
27        return None;
28    }
29
30    let expected = u16::from_be_bytes([data[crc_start], data[crc_start + 1]]);
31    (crc16_en13757(&data[..crc_start]) == expected).then_some(crc_start)
32}
33
34fn validate_format_a_header(data: &[u8]) -> Option<()> {
35    let header = data.get(..10)?;
36    let crc = data.get(10..12)?;
37    (crc16_en13757(header) == u16::from_be_bytes([crc[0], crc[1]])).then_some(())
38}
39
40/// A borrowed view of a Format A frame with its interleaved CRCs omitted.
41///
42/// The source is never rewritten or copied. [`Self::bytes`] yields the corrected
43/// length byte followed by the original non-CRC bytes. As with
44/// [`strip_format_a_crcs`], a valid first-block CRC is required; an unrecognized
45/// trailing block is retained verbatim for compatibility.
46///
47/// Construction scans CRC boundaries to determine the corrected length. No
48/// destination buffer is needed to subsequently consume the bytes:
49///
50/// ```
51/// use wireless_mbus_link_layer::FormatAFrame;
52/// let raw = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff];
53/// let view = FormatAFrame::new(&raw).unwrap();
54/// let mut bytes = view.bytes();
55/// assert_eq!(bytes.next(), Some(9));
56/// assert_eq!(bytes.count(), 9);
57/// ```
58#[derive(Clone, Debug)]
59pub struct FormatAFrame<'a> {
60    data: &'a [u8],
61    length: usize,
62}
63
64impl<'a> FormatAFrame<'a> {
65    #[must_use]
66    pub fn new(data: &'a [u8]) -> Option<Self> {
67        validate_format_a_header(data)?;
68        let length = FormatAChunks {
69            remaining: &data[12..],
70        }
71        .fold(10, |length, chunk| length + chunk.len());
72        Some(Self { data, length })
73    }
74
75    /// Number of bytes after removing the recognized CRCs.
76    #[must_use]
77    pub const fn len(&self) -> usize {
78        self.length
79    }
80
81    /// A Format A frame always contains its ten-byte link header.
82    #[must_use]
83    pub const fn is_empty(&self) -> bool {
84        false
85    }
86
87    /// Iterates over normalized bytes without a destination buffer.
88    #[must_use]
89    pub fn bytes(&self) -> FormatABytes<'a> {
90        FormatABytes {
91            length_byte: Some((self.length - 1) as u8),
92            current: self.data[1..10].iter(),
93            chunks: FormatAChunks {
94                remaining: &self.data[12..],
95            },
96            remaining: self.length,
97        }
98    }
99}
100
101#[derive(Clone, Debug)]
102struct FormatAChunks<'a> {
103    remaining: &'a [u8],
104}
105
106impl<'a> Iterator for FormatAChunks<'a> {
107    type Item = &'a [u8];
108
109    fn next(&mut self) -> Option<Self::Item> {
110        let data = self.remaining;
111        if data.is_empty() {
112            return None;
113        }
114        if data.len() >= 3 {
115            for length in (1..=16.min(data.len() - 2)).rev() {
116                let crc = u16::from_be_bytes([data[length], data[length + 1]]);
117                if crc16_en13757(&data[..length]) == crc {
118                    self.remaining = &data[length + 2..];
119                    return Some(&data[..length]);
120                }
121            }
122        }
123        self.remaining = &[];
124        Some(data)
125    }
126}
127
128/// Cloneable iterator over a borrowed Format A frame's normalized bytes.
129///
130/// Normalized data can be consumed incrementally. APIs accepting a contiguous
131/// slice still need caller-provided storage, filled with [`strip_format_a_crcs`].
132#[derive(Clone, Debug)]
133pub struct FormatABytes<'a> {
134    length_byte: Option<u8>,
135    current: core::slice::Iter<'a, u8>,
136    chunks: FormatAChunks<'a>,
137    remaining: usize,
138}
139
140impl Iterator for FormatABytes<'_> {
141    type Item = u8;
142
143    fn next(&mut self) -> Option<Self::Item> {
144        let byte = if let Some(length) = self.length_byte.take() {
145            length
146        } else {
147            loop {
148                if let Some(byte) = self.current.next() {
149                    break *byte;
150                }
151                self.current = self.chunks.next()?.iter();
152            }
153        };
154        self.remaining -= 1;
155        Some(byte)
156    }
157
158    fn size_hint(&self) -> (usize, Option<usize>) {
159        (self.remaining, Some(self.remaining))
160    }
161}
162
163impl ExactSizeIterator for FormatABytes<'_> {}
164impl core::iter::FusedIterator for FormatABytes<'_> {}
165
166/// Strip Format A CRCs into caller-provided contiguous storage.
167///
168/// Use [`FormatAFrame::bytes`] to consume normalized bytes lazily instead.
169pub fn strip_format_a_crcs<'a>(data: &[u8], output: &'a mut [u8]) -> Option<&'a [u8]> {
170    // Preserve the historical destination-size requirement.
171    if output.len() < data.len() {
172        return None;
173    }
174    validate_format_a_header(data)?;
175    output[..10].copy_from_slice(&data[..10]);
176    let mut length = 10;
177    for chunk in (FormatAChunks {
178        remaining: &data[12..],
179    }) {
180        output[length..length + chunk.len()].copy_from_slice(chunk);
181        length += chunk.len();
182    }
183    output[0] = (length - 1) as u8;
184    Some(&output[..length])
185}
186
187#[derive(Debug, Clone, Copy, PartialEq)]
188#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
189pub struct WirelessFrame<'a> {
190    /// Raw wireless M-Bus C-field.
191    pub control_field: u8,
192    /// Decoded C-field when it maps to a function currently known by the
193    /// shared M-Bus core. Unknown but otherwise valid C-fields are preserved
194    /// through `control_field` instead of making the frame unparseable.
195    pub function: Option<Function>,
196    pub manufacturer_id: ManufacturerId,
197    #[cfg_attr(
198        feature = "serde",
199        serde(serialize_with = "m_bus_core::serde_hex::serialize")
200    )]
201    pub data: &'a [u8],
202}
203
204#[derive(Debug, Clone, Copy, PartialEq)]
205#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
206pub struct ManufacturerId {
207    pub manufacturer_code: ManufacturerCode,
208    pub identification_number: IdentificationNumber,
209    pub device_type: DeviceType,
210    pub version: u8,
211    /// Bit 15 of the manufacturer field, which sits outside the three-letter
212    /// code: meters that set it flag the address as not globally unique.
213    pub is_unique_globally: bool,
214}
215
216impl TryFrom<&[u8]> for ManufacturerId {
217    type Error = FrameError;
218    fn try_from(data: &[u8]) -> Result<Self, FrameError> {
219        let mut iter = data.iter();
220        let manufacturer_field = u16::from_le_bytes([
221            *iter.next().ok_or(FrameError::TooShort)?,
222            *iter.next().ok_or(FrameError::TooShort)?,
223        ]);
224        Ok(ManufacturerId {
225            manufacturer_code: ManufacturerCode::from_id(manufacturer_field).map_err(|_| {
226                FrameError::InvalidManufacturerCode {
227                    code: manufacturer_field,
228                }
229            })?,
230            identification_number: IdentificationNumber::from_bcd_hex_digits([
231                *iter.next().ok_or(FrameError::TooShort)?,
232                *iter.next().ok_or(FrameError::TooShort)?,
233                *iter.next().ok_or(FrameError::TooShort)?,
234                *iter.next().ok_or(FrameError::TooShort)?,
235            ])
236            .map_err(|_| FrameError::TooShort)?,
237            version: *iter.next().ok_or(FrameError::TooShort)?,
238            // In wireless M-Bus, device type encoding depends on the CI (Control Information) field:
239            // - For unencrypted frames (CI=0x7A): use full device type byte
240            // - For encrypted frames (CI=0xA0-0xAF): device type is in upper nibble,
241            //   lower nibble contains encryption mode information
242            device_type: {
243                let device_byte = *iter.next().ok_or(FrameError::TooShort)?;
244                // Peek ahead at the CI field (at offset 8 from start of ManufacturerId data)
245                let ci_byte = *data.get(8).ok_or(FrameError::TooShort)?;
246                let device_type_code = if (0xA0..=0xAF).contains(&ci_byte) {
247                    // Encrypted frame: extract upper nibble only
248                    (device_byte >> 4) & 0x0F
249                } else {
250                    // Unencrypted frame: use full byte
251                    device_byte
252                };
253                DeviceType::from(device_type_code)
254            },
255            is_unique_globally: (manufacturer_field & !ManufacturerCode::CODE_MASK) == 0,
256        })
257    }
258}
259
260#[derive(Debug, Copy, Clone, PartialEq)]
261#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
262pub enum FrameError {
263    EmptyData,
264    TooShort,
265    /// The manufacturer field's low 15 bits are not three uppercase letters.
266    InvalidManufacturerCode {
267        code: u16,
268    },
269    WrongLength {
270        expected: usize,
271        actual: usize,
272    },
273}
274
275impl<'a> TryFrom<&'a [u8]> for WirelessFrame<'a> {
276    type Error = FrameError;
277
278    fn try_from(data: &'a [u8]) -> Result<Self, FrameError> {
279        let length = data.len();
280        let length_byte = *data.first().ok_or(FrameError::EmptyData)? as usize;
281        let control_field = *data.get(1).ok_or(FrameError::TooShort)?;
282        let manufacturer_id = ManufacturerId::try_from(&data[2..])?;
283
284        // In wireless M-Bus, the L-field contains the number of bytes following the L-field
285        if length_byte + 1 == length {
286            let data_end = trailing_frame_crc_start(data).unwrap_or(length);
287            return Ok(WirelessFrame {
288                control_field,
289                function: Function::try_from(control_field).ok(),
290                manufacturer_id,
291                data: &data[10..data_end],
292            });
293        }
294
295        Err(FrameError::WrongLength {
296            expected: length_byte + 1,
297            actual: data.len(),
298        })
299    }
300}
301
302#[cfg(test)]
303mod test {
304    use super::*;
305
306    #[test]
307    fn test_dummy() {
308        let _id = 33225544;
309        let _medium = 7; // water
310        let _man = "SEN";
311        let _version = 104;
312        let frame: &[u8] = &[
313            0x18, 0x44, 0xAE, 0x4C, 0x44, 0x55, 0x22, 0x33, 0x68, 0x07, 0x7A, 0x55, 0x00, 0x00,
314            0x00, 0x00, 0x04, 0x13, 0x89, 0xE2, 0x01, 0x00, 0x02, 0x3B, 0x00, 0x00,
315        ];
316        let parsed = WirelessFrame::try_from(frame);
317        println!("{:#?}", parsed);
318    }
319
320    #[test]
321    fn test_trailing_frame_crc_is_not_payload() {
322        let frame = [
323            0x14, 0x44, 0xAE, 0x0C, 0x78, 0x56, 0x34, 0x12, 0x01, 0x07, 0x8C, 0x20, 0x27, 0x78,
324            0x0B, 0x13, 0x43, 0x65, 0x87, 0x7A, 0xC5,
325        ];
326
327        assert_eq!(trailing_frame_crc_start(&frame), Some(19));
328
329        let parsed = WirelessFrame::try_from(frame.as_slice()).expect("valid wireless frame");
330        assert_eq!(
331            parsed.data,
332            &[0x8C, 0x20, 0x27, 0x78, 0x0B, 0x13, 0x43, 0x65, 0x87]
333        );
334    }
335
336    #[test]
337    fn c_field_is_decoded_and_preserved() {
338        let frame = [
339            0x18, 0x44, 0xAE, 0x4C, 0x44, 0x55, 0x22, 0x33, 0x68, 0x07, 0x7A, 0x55, 0x00, 0x00,
340            0x00, 0x00, 0x04, 0x13, 0x89, 0xE2, 0x01, 0x00, 0x02, 0x3B, 0x00,
341        ];
342        let parsed = WirelessFrame::try_from(frame.as_slice()).expect("valid wireless frame");
343        assert_eq!(parsed.control_field, 0x44);
344        assert_eq!(parsed.function, Some(Function::SndNr));
345    }
346
347    #[test]
348    fn manufacturer_field_with_top_bit_set_is_decoded() {
349        let frame = [
350            0x18, 0x44, 0x97, 0xA6, 0x44, 0x55, 0x22, 0x33, 0x68, 0x07, 0x7A, 0x55, 0x00, 0x00,
351            0x00, 0x00, 0x04, 0x13, 0x89, 0xE2, 0x01, 0x00, 0x02, 0x3B, 0x00,
352        ];
353        let parsed = WirelessFrame::try_from(frame.as_slice()).expect("valid wireless frame");
354        assert_eq!(
355            parsed.manufacturer_id.manufacturer_code.code,
356            ['I', 'T', 'W']
357        );
358        assert!(!parsed.manufacturer_id.is_unique_globally);
359
360        // Same code without the flag, which does mark a globally unique address.
361        let mut frame = frame;
362        frame[3] = 0x26;
363        let parsed = WirelessFrame::try_from(frame.as_slice()).expect("valid wireless frame");
364        assert_eq!(
365            parsed.manufacturer_id.manufacturer_code.code,
366            ['I', 'T', 'W']
367        );
368        assert!(parsed.manufacturer_id.is_unique_globally);
369    }
370
371    #[test]
372    fn undecodable_manufacturer_field_is_not_reported_as_too_short() {
373        let frame = [
374            0x18, 0x44, 0x00, 0x00, 0x44, 0x55, 0x22, 0x33, 0x68, 0x07, 0x7A, 0x55, 0x00, 0x00,
375            0x00, 0x00, 0x04, 0x13, 0x89, 0xE2, 0x01, 0x00, 0x02, 0x3B, 0x00,
376        ];
377        assert_eq!(
378            WirelessFrame::try_from(frame.as_slice()),
379            Err(FrameError::InvalidManufacturerCode { code: 0x0000 })
380        );
381    }
382
383    #[test]
384    fn unknown_c_field_does_not_invalidate_frame() {
385        let frame = [
386            0x18, 0x45, 0xAE, 0x4C, 0x44, 0x55, 0x22, 0x33, 0x68, 0x07, 0x7A, 0x55, 0x00, 0x00,
387            0x00, 0x00, 0x04, 0x13, 0x89, 0xE2, 0x01, 0x00, 0x02, 0x3B, 0x00,
388        ];
389        let parsed = WirelessFrame::try_from(frame.as_slice()).expect("valid wireless frame");
390        assert_eq!(parsed.control_field, 0x45);
391        assert_eq!(parsed.function, None);
392    }
393}
394
395#[cfg(test)]
396mod format_a_tests {
397    use super::*;
398
399    fn encode(payload: &[u8]) -> Vec<u8> {
400        let header = [0, 0x44, 0x49, 0x6A, 0x31, 0, 1, 0x55, 0x14, 0x37];
401        let mut frame = header.to_vec();
402        frame.extend(crc16_en13757(&header).to_be_bytes());
403        for chunk in payload.chunks(16) {
404            frame.extend(chunk);
405            frame.extend(crc16_en13757(chunk).to_be_bytes());
406        }
407        frame
408    }
409
410    #[test]
411    fn normalized_bytes_skip_crcs_across_block_boundaries() {
412        for length in [0, 1, 15, 16, 17, 31, 32, 33, 80, 240] {
413            let payload: Vec<u8> = (0..length).map(|n| (n * 17) as u8).collect();
414            let encoded = encode(&payload);
415            let original = encoded.clone();
416            let view = FormatAFrame::new(&encoded).unwrap();
417            let mut expected = vec![(length + 9) as u8];
418            expected.extend(&encoded[1..10]);
419            expected.extend(&payload);
420            assert_eq!(view.len(), expected.len());
421            assert!(!view.is_empty());
422            assert!(view.bytes().eq(expected.iter().copied()));
423            let mut bytes = view.bytes();
424            for (index, want) in expected.iter().enumerate() {
425                assert_eq!(bytes.len(), view.len() - index);
426                assert_eq!(bytes.size_hint(), (bytes.len(), Some(bytes.len())));
427                assert_eq!(bytes.next(), Some(*want));
428                assert!(bytes.clone().eq(expected.iter().skip(index + 1).copied()));
429            }
430            assert_eq!(bytes.next(), None);
431            assert_eq!(bytes.next(), None);
432            assert_eq!(bytes.len(), 0);
433            assert_eq!(encoded, original);
434            let mut output = vec![0; encoded.len()];
435            assert_eq!(
436                strip_format_a_crcs(&encoded, &mut output),
437                Some(expected.as_slice())
438            );
439        }
440    }
441
442    #[test]
443    fn tail_and_validation_match_the_buffer_api() {
444        for tail in [
445            &[][..],
446            &[0x33][..],
447            &[0x33, 0x44][..],
448            &[1, 2, 3, 4, 5][..],
449        ] {
450            let mut encoded = encode(&[]);
451            encoded.extend(tail);
452            let frame = FormatAFrame::new(&encoded).unwrap();
453            assert!(frame.bytes().skip(10).eq(tail.iter().copied()));
454            let mut too_small = vec![0xAA; encoded.len() - 1];
455            assert!(strip_format_a_crcs(&encoded, &mut too_small).is_none());
456            assert!(too_small.iter().all(|byte| *byte == 0xAA));
457        }
458        let encoded = encode(&[]);
459        for length in 0..12 {
460            assert!(FormatAFrame::new(&encoded[..length]).is_none());
461        }
462        let mut corrupt = encoded;
463        corrupt[10] ^= 1;
464        assert!(FormatAFrame::new(&corrupt).is_none());
465    }
466
467    #[test]
468    fn iterator_borrows_source_not_temporary_view() {
469        let encoded = encode(&[1, 2, 3]);
470        let bytes = {
471            let view = FormatAFrame::new(&encoded).unwrap();
472            view.bytes()
473        };
474        assert_eq!(bytes.skip(10).collect::<Vec<_>>(), [1, 2, 3]);
475    }
476}