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/// Strip Format A CRCs from a wireless M-Bus frame.
21///
22/// Format A frames have CRC-16 checksums embedded in the data:
23/// - Block 1: first 10 bytes (L, C, M, M, ID, ID, ID, ID, Ver, Type) + 2 CRC bytes
24/// - Block 2+: up to 16 bytes of data + 2 CRC bytes each
25///
26/// Writes the stripped frame into `output` and returns the resulting slice,
27/// or `None` if the frame doesn't have valid Format A CRCs.
28/// The L-field is corrected to reflect the stripped payload size.
29pub fn strip_format_a_crcs<'a>(data: &[u8], output: &'a mut [u8]) -> Option<&'a [u8]> {
30    if data.len() < 12 || output.len() < data.len() {
31        return None;
32    }
33
34    // Check block 1: first 10 bytes + 2 CRC
35    if crc16_en13757(&data[0..10]) != u16::from_be_bytes([data[10], data[11]]) {
36        return None;
37    }
38
39    let mut out_pos = 10;
40    output[..10].copy_from_slice(&data[..10]);
41
42    let mut pos = 12;
43    while pos < data.len() {
44        let remaining = data.len() - pos;
45        if remaining < 3 {
46            output[out_pos..out_pos + remaining].copy_from_slice(&data[pos..pos + remaining]);
47            out_pos += remaining;
48            break;
49        }
50
51        let max_data_len = 16.min(remaining - 2);
52        let mut found = false;
53
54        for data_len in (1..=max_data_len).rev() {
55            let crc_start = pos + data_len;
56            if crc_start + 2 > data.len() {
57                continue;
58            }
59            if crc16_en13757(&data[pos..crc_start])
60                == u16::from_be_bytes([data[crc_start], data[crc_start + 1]])
61            {
62                output[out_pos..out_pos + data_len].copy_from_slice(&data[pos..crc_start]);
63                out_pos += data_len;
64                pos = crc_start + 2;
65                found = true;
66                break;
67            }
68        }
69
70        if !found {
71            let remaining = data.len() - pos;
72            output[out_pos..out_pos + remaining].copy_from_slice(&data[pos..]);
73            out_pos += remaining;
74            break;
75        }
76    }
77
78    output[0] = (out_pos - 1) as u8;
79    Some(&output[..out_pos])
80}
81
82#[derive(Debug, Clone, Copy, PartialEq)]
83#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
84pub struct WirelessFrame<'a> {
85    pub function: Function,
86    pub manufacturer_id: ManufacturerId,
87    pub data: &'a [u8],
88}
89
90#[derive(Debug, Clone, Copy, PartialEq)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
92pub struct ManufacturerId {
93    pub manufacturer_code: ManufacturerCode,
94    pub identification_number: IdentificationNumber,
95    pub device_type: DeviceType,
96    pub version: u8,
97    pub is_unique_globally: bool,
98}
99
100impl TryFrom<&[u8]> for ManufacturerId {
101    type Error = FrameError;
102    fn try_from(data: &[u8]) -> Result<Self, FrameError> {
103        let mut iter = data.iter();
104        Ok(ManufacturerId {
105            manufacturer_code: ManufacturerCode::from_id(u16::from_le_bytes([
106                *iter.next().ok_or(FrameError::TooShort)?,
107                *iter.next().ok_or(FrameError::TooShort)?,
108            ]))
109            .map_err(|_| FrameError::TooShort)?,
110            identification_number: IdentificationNumber::from_bcd_hex_digits([
111                *iter.next().ok_or(FrameError::TooShort)?,
112                *iter.next().ok_or(FrameError::TooShort)?,
113                *iter.next().ok_or(FrameError::TooShort)?,
114                *iter.next().ok_or(FrameError::TooShort)?,
115            ])
116            .map_err(|_| FrameError::TooShort)?,
117            version: *iter.next().ok_or(FrameError::TooShort)?,
118            // In wireless M-Bus, device type encoding depends on the CI (Control Information) field:
119            // - For unencrypted frames (CI=0x7A): use full device type byte
120            // - For encrypted frames (CI=0xA0-0xAF): device type is in upper nibble,
121            //   lower nibble contains encryption mode information
122            device_type: {
123                let device_byte = *iter.next().ok_or(FrameError::TooShort)?;
124                // Peek ahead at the CI field (at offset 8 from start of ManufacturerId data)
125                let ci_byte = *data.get(8).ok_or(FrameError::TooShort)?;
126                let device_type_code = if (0xA0..=0xAF).contains(&ci_byte) {
127                    // Encrypted frame: extract upper nibble only
128                    (device_byte >> 4) & 0x0F
129                } else {
130                    // Unencrypted frame: use full byte
131                    device_byte
132                };
133                DeviceType::from(device_type_code)
134            },
135            is_unique_globally: false, /*todo not sure about this field*/
136        })
137    }
138}
139
140#[derive(Debug, Copy, Clone, PartialEq)]
141#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
142pub enum FrameError {
143    EmptyData,
144    TooShort,
145    WrongLength { expected: usize, actual: usize },
146}
147
148impl<'a> TryFrom<&'a [u8]> for WirelessFrame<'a> {
149    type Error = FrameError;
150
151    fn try_from(data: &'a [u8]) -> Result<Self, FrameError> {
152        let length = data.len();
153        let length_byte = *data.first().ok_or(FrameError::EmptyData)? as usize;
154        let _c_field = *data.get(1).ok_or(FrameError::TooShort)? as usize;
155        let manufacturer_id = ManufacturerId::try_from(&data[2..])?;
156
157        // In wireless M-Bus, the L-field contains the number of bytes following the L-field
158        if length_byte + 1 == length {
159            return Ok(WirelessFrame {
160                function: Function::SndNk { prm: false },
161                manufacturer_id,
162                data: &data[10..],
163            });
164        }
165
166        Err(FrameError::WrongLength {
167            expected: length_byte + 1,
168            actual: data.len(),
169        })
170    }
171}
172
173#[cfg(test)]
174mod test {
175    use super::*;
176
177    #[test]
178    fn test_dummy() {
179        let _id = 33225544;
180        let _medium = 7; // water
181        let _man = "SEN";
182        let _version = 104;
183        let frame: &[u8] = &[
184            0x18, 0x44, 0xAE, 0x4C, 0x44, 0x55, 0x22, 0x33, 0x68, 0x07, 0x7A, 0x55, 0x00, 0x00,
185            0x00, 0x00, 0x04, 0x13, 0x89, 0xE2, 0x01, 0x00, 0x02, 0x3B, 0x00, 0x00,
186        ];
187        let parsed = WirelessFrame::try_from(frame);
188        println!("{:#?}", parsed);
189    }
190}