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    pub function: Function,
98    pub manufacturer_id: ManufacturerId,
99    pub data: &'a [u8],
100}
101
102#[derive(Debug, Clone, Copy, PartialEq)]
103#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
104pub struct ManufacturerId {
105    pub manufacturer_code: ManufacturerCode,
106    pub identification_number: IdentificationNumber,
107    pub device_type: DeviceType,
108    pub version: u8,
109    pub is_unique_globally: bool,
110}
111
112impl TryFrom<&[u8]> for ManufacturerId {
113    type Error = FrameError;
114    fn try_from(data: &[u8]) -> Result<Self, FrameError> {
115        let mut iter = data.iter();
116        Ok(ManufacturerId {
117            manufacturer_code: ManufacturerCode::from_id(u16::from_le_bytes([
118                *iter.next().ok_or(FrameError::TooShort)?,
119                *iter.next().ok_or(FrameError::TooShort)?,
120            ]))
121            .map_err(|_| FrameError::TooShort)?,
122            identification_number: IdentificationNumber::from_bcd_hex_digits([
123                *iter.next().ok_or(FrameError::TooShort)?,
124                *iter.next().ok_or(FrameError::TooShort)?,
125                *iter.next().ok_or(FrameError::TooShort)?,
126                *iter.next().ok_or(FrameError::TooShort)?,
127            ])
128            .map_err(|_| FrameError::TooShort)?,
129            version: *iter.next().ok_or(FrameError::TooShort)?,
130            // In wireless M-Bus, device type encoding depends on the CI (Control Information) field:
131            // - For unencrypted frames (CI=0x7A): use full device type byte
132            // - For encrypted frames (CI=0xA0-0xAF): device type is in upper nibble,
133            //   lower nibble contains encryption mode information
134            device_type: {
135                let device_byte = *iter.next().ok_or(FrameError::TooShort)?;
136                // Peek ahead at the CI field (at offset 8 from start of ManufacturerId data)
137                let ci_byte = *data.get(8).ok_or(FrameError::TooShort)?;
138                let device_type_code = if (0xA0..=0xAF).contains(&ci_byte) {
139                    // Encrypted frame: extract upper nibble only
140                    (device_byte >> 4) & 0x0F
141                } else {
142                    // Unencrypted frame: use full byte
143                    device_byte
144                };
145                DeviceType::from(device_type_code)
146            },
147            is_unique_globally: false, /*todo not sure about this field*/
148        })
149    }
150}
151
152#[derive(Debug, Copy, Clone, PartialEq)]
153#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
154pub enum FrameError {
155    EmptyData,
156    TooShort,
157    WrongLength { expected: usize, actual: usize },
158}
159
160impl<'a> TryFrom<&'a [u8]> for WirelessFrame<'a> {
161    type Error = FrameError;
162
163    fn try_from(data: &'a [u8]) -> Result<Self, FrameError> {
164        let length = data.len();
165        let length_byte = *data.first().ok_or(FrameError::EmptyData)? as usize;
166        let _c_field = *data.get(1).ok_or(FrameError::TooShort)? as usize;
167        let manufacturer_id = ManufacturerId::try_from(&data[2..])?;
168
169        // In wireless M-Bus, the L-field contains the number of bytes following the L-field
170        if length_byte + 1 == length {
171            let data_end = trailing_frame_crc_start(data).unwrap_or(length);
172            return Ok(WirelessFrame {
173                function: Function::SndNk { prm: false },
174                manufacturer_id,
175                data: &data[10..data_end],
176            });
177        }
178
179        Err(FrameError::WrongLength {
180            expected: length_byte + 1,
181            actual: data.len(),
182        })
183    }
184}
185
186#[cfg(test)]
187mod test {
188    use super::*;
189
190    #[test]
191    fn test_dummy() {
192        let _id = 33225544;
193        let _medium = 7; // water
194        let _man = "SEN";
195        let _version = 104;
196        let frame: &[u8] = &[
197            0x18, 0x44, 0xAE, 0x4C, 0x44, 0x55, 0x22, 0x33, 0x68, 0x07, 0x7A, 0x55, 0x00, 0x00,
198            0x00, 0x00, 0x04, 0x13, 0x89, 0xE2, 0x01, 0x00, 0x02, 0x3B, 0x00, 0x00,
199        ];
200        let parsed = WirelessFrame::try_from(frame);
201        println!("{:#?}", parsed);
202    }
203
204    #[test]
205    fn test_trailing_frame_crc_is_not_payload() {
206        let frame = [
207            0x14, 0x44, 0xAE, 0x0C, 0x78, 0x56, 0x34, 0x12, 0x01, 0x07, 0x8C, 0x20, 0x27, 0x78,
208            0x0B, 0x13, 0x43, 0x65, 0x87, 0x7A, 0xC5,
209        ];
210
211        assert_eq!(trailing_frame_crc_start(&frame), Some(19));
212
213        let parsed = WirelessFrame::try_from(frame.as_slice()).expect("valid wireless frame");
214        assert_eq!(
215            parsed.data,
216            &[0x8C, 0x20, 0x27, 0x78, 0x0B, 0x13, 0x43, 0x65, 0x87]
217        );
218    }
219}