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