Skip to main content

wireless_mbus_link_layer/
lib.rs

1use m_bus_core::{DeviceType, Function, IdentificationNumber, ManufacturerCode};
2
3/// CRC-16/EN13757 used in wireless M-Bus Format A frames.
4/// Polynomial: 0x3D65, Init: 0x0000, XorOut: 0xFFFF, RefIn: false, RefOut: false.
5fn crc16_en13757(data: &[u8]) -> u16 {
6    let mut crc: u16 = 0x0000;
7    for &byte in data {
8        crc ^= (byte as u16) << 8;
9        for _ in 0..8 {
10            if crc & 0x8000 != 0 {
11                crc = (crc << 1) ^ 0x3D65;
12            } else {
13                crc <<= 1;
14            }
15        }
16    }
17    crc ^ 0xFFFF
18}
19
20/// Return the start offset of a trailing frame CRC when the final two bytes
21/// validate against every preceding byte.
22pub fn trailing_frame_crc_start(data: &[u8]) -> Option<usize> {
23    let crc_start = data.len().checked_sub(2)?;
24    if crc_start < 10 {
25        return None;
26    }
27
28    let expected = u16::from_be_bytes([data[crc_start], data[crc_start + 1]]);
29    (crc16_en13757(&data[..crc_start]) == expected).then_some(crc_start)
30}
31
32/// Strip Format A CRCs from a wireless M-Bus frame.
33///
34/// Format A frames have CRC-16 checksums embedded in the data:
35/// - Block 1: first 10 bytes (L, C, M, M, ID, ID, ID, ID, Ver, Type) + 2 CRC bytes
36/// - Block 2+: up to 16 bytes of data + 2 CRC bytes each
37///
38/// Writes the stripped frame into `output` and returns the resulting slice,
39/// or `None` if the frame doesn't have valid Format A CRCs.
40/// The L-field is corrected to reflect the stripped payload size.
41pub fn strip_format_a_crcs<'a>(data: &[u8], output: &'a mut [u8]) -> Option<&'a [u8]> {
42    if data.len() < 12 || output.len() < data.len() {
43        return None;
44    }
45
46    // Check block 1: first 10 bytes + 2 CRC
47    if crc16_en13757(&data[0..10]) != u16::from_be_bytes([data[10], data[11]]) {
48        return None;
49    }
50
51    let mut out_pos = 10;
52    output[..10].copy_from_slice(&data[..10]);
53
54    let mut pos = 12;
55    while pos < data.len() {
56        let remaining = data.len() - pos;
57        if remaining < 3 {
58            output[out_pos..out_pos + remaining].copy_from_slice(&data[pos..pos + remaining]);
59            out_pos += remaining;
60            break;
61        }
62
63        let max_data_len = 16.min(remaining - 2);
64        let mut found = false;
65
66        for data_len in (1..=max_data_len).rev() {
67            let crc_start = pos + data_len;
68            if crc_start + 2 > data.len() {
69                continue;
70            }
71            if crc16_en13757(&data[pos..crc_start])
72                == u16::from_be_bytes([data[crc_start], data[crc_start + 1]])
73            {
74                output[out_pos..out_pos + data_len].copy_from_slice(&data[pos..crc_start]);
75                out_pos += data_len;
76                pos = crc_start + 2;
77                found = true;
78                break;
79            }
80        }
81
82        if !found {
83            let remaining = data.len() - pos;
84            output[out_pos..out_pos + remaining].copy_from_slice(&data[pos..]);
85            out_pos += remaining;
86            break;
87        }
88    }
89
90    output[0] = (out_pos - 1) as u8;
91    Some(&output[..out_pos])
92}
93
94#[derive(Debug, Clone, Copy, PartialEq)]
95#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
96pub struct WirelessFrame<'a> {
97    /// Raw wireless M-Bus C-field.
98    pub control_field: u8,
99    /// Decoded C-field when it maps to a function currently known by the
100    /// shared M-Bus core. Unknown but otherwise valid C-fields are preserved
101    /// through `control_field` instead of making the frame unparseable.
102    pub function: Option<Function>,
103    pub manufacturer_id: ManufacturerId,
104    #[cfg_attr(
105        feature = "serde",
106        serde(serialize_with = "m_bus_core::serde_hex::serialize")
107    )]
108    pub data: &'a [u8],
109}
110
111#[derive(Debug, Clone, Copy, PartialEq)]
112#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
113pub struct ManufacturerId {
114    pub manufacturer_code: ManufacturerCode,
115    pub identification_number: IdentificationNumber,
116    pub device_type: DeviceType,
117    pub version: u8,
118    pub is_unique_globally: bool,
119}
120
121impl TryFrom<&[u8]> for ManufacturerId {
122    type Error = FrameError;
123    fn try_from(data: &[u8]) -> Result<Self, FrameError> {
124        let mut iter = data.iter();
125        Ok(ManufacturerId {
126            manufacturer_code: ManufacturerCode::from_id(u16::from_le_bytes([
127                *iter.next().ok_or(FrameError::TooShort)?,
128                *iter.next().ok_or(FrameError::TooShort)?,
129            ]))
130            .map_err(|_| FrameError::TooShort)?,
131            identification_number: IdentificationNumber::from_bcd_hex_digits([
132                *iter.next().ok_or(FrameError::TooShort)?,
133                *iter.next().ok_or(FrameError::TooShort)?,
134                *iter.next().ok_or(FrameError::TooShort)?,
135                *iter.next().ok_or(FrameError::TooShort)?,
136            ])
137            .map_err(|_| FrameError::TooShort)?,
138            version: *iter.next().ok_or(FrameError::TooShort)?,
139            // In wireless M-Bus, device type encoding depends on the CI (Control Information) field:
140            // - For unencrypted frames (CI=0x7A): use full device type byte
141            // - For encrypted frames (CI=0xA0-0xAF): device type is in upper nibble,
142            //   lower nibble contains encryption mode information
143            device_type: {
144                let device_byte = *iter.next().ok_or(FrameError::TooShort)?;
145                // Peek ahead at the CI field (at offset 8 from start of ManufacturerId data)
146                let ci_byte = *data.get(8).ok_or(FrameError::TooShort)?;
147                let device_type_code = if (0xA0..=0xAF).contains(&ci_byte) {
148                    // Encrypted frame: extract upper nibble only
149                    (device_byte >> 4) & 0x0F
150                } else {
151                    // Unencrypted frame: use full byte
152                    device_byte
153                };
154                DeviceType::from(device_type_code)
155            },
156            is_unique_globally: false, /*todo not sure about this field*/
157        })
158    }
159}
160
161#[derive(Debug, Copy, Clone, PartialEq)]
162#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
163pub enum FrameError {
164    EmptyData,
165    TooShort,
166    WrongLength { expected: usize, actual: usize },
167}
168
169impl<'a> TryFrom<&'a [u8]> for WirelessFrame<'a> {
170    type Error = FrameError;
171
172    fn try_from(data: &'a [u8]) -> Result<Self, FrameError> {
173        let length = data.len();
174        let length_byte = *data.first().ok_or(FrameError::EmptyData)? as usize;
175        let control_field = *data.get(1).ok_or(FrameError::TooShort)?;
176        let manufacturer_id = ManufacturerId::try_from(&data[2..])?;
177
178        // In wireless M-Bus, the L-field contains the number of bytes following the L-field
179        if length_byte + 1 == length {
180            let data_end = trailing_frame_crc_start(data).unwrap_or(length);
181            return Ok(WirelessFrame {
182                control_field,
183                function: Function::try_from(control_field).ok(),
184                manufacturer_id,
185                data: &data[10..data_end],
186            });
187        }
188
189        Err(FrameError::WrongLength {
190            expected: length_byte + 1,
191            actual: data.len(),
192        })
193    }
194}
195
196#[cfg(test)]
197mod test {
198    use super::*;
199
200    #[test]
201    fn test_dummy() {
202        let _id = 33225544;
203        let _medium = 7; // water
204        let _man = "SEN";
205        let _version = 104;
206        let frame: &[u8] = &[
207            0x18, 0x44, 0xAE, 0x4C, 0x44, 0x55, 0x22, 0x33, 0x68, 0x07, 0x7A, 0x55, 0x00, 0x00,
208            0x00, 0x00, 0x04, 0x13, 0x89, 0xE2, 0x01, 0x00, 0x02, 0x3B, 0x00, 0x00,
209        ];
210        let parsed = WirelessFrame::try_from(frame);
211        println!("{:#?}", parsed);
212    }
213
214    #[test]
215    fn test_trailing_frame_crc_is_not_payload() {
216        let frame = [
217            0x14, 0x44, 0xAE, 0x0C, 0x78, 0x56, 0x34, 0x12, 0x01, 0x07, 0x8C, 0x20, 0x27, 0x78,
218            0x0B, 0x13, 0x43, 0x65, 0x87, 0x7A, 0xC5,
219        ];
220
221        assert_eq!(trailing_frame_crc_start(&frame), Some(19));
222
223        let parsed = WirelessFrame::try_from(frame.as_slice()).expect("valid wireless frame");
224        assert_eq!(
225            parsed.data,
226            &[0x8C, 0x20, 0x27, 0x78, 0x0B, 0x13, 0x43, 0x65, 0x87]
227        );
228    }
229
230    #[test]
231    fn c_field_is_decoded_and_preserved() {
232        let frame = [
233            0x18, 0x44, 0xAE, 0x4C, 0x44, 0x55, 0x22, 0x33, 0x68, 0x07, 0x7A, 0x55, 0x00, 0x00,
234            0x00, 0x00, 0x04, 0x13, 0x89, 0xE2, 0x01, 0x00, 0x02, 0x3B, 0x00,
235        ];
236        let parsed = WirelessFrame::try_from(frame.as_slice()).expect("valid wireless frame");
237        assert_eq!(parsed.control_field, 0x44);
238        assert_eq!(parsed.function, Some(Function::SndNr));
239    }
240
241    #[test]
242    fn unknown_c_field_does_not_invalidate_frame() {
243        let frame = [
244            0x18, 0x45, 0xAE, 0x4C, 0x44, 0x55, 0x22, 0x33, 0x68, 0x07, 0x7A, 0x55, 0x00, 0x00,
245            0x00, 0x00, 0x04, 0x13, 0x89, 0xE2, 0x01, 0x00, 0x02, 0x3B, 0x00,
246        ];
247        let parsed = WirelessFrame::try_from(frame.as_slice()).expect("valid wireless frame");
248        assert_eq!(parsed.control_field, 0x45);
249        assert_eq!(parsed.function, None);
250    }
251}