Skip to main content

rustmod_core/frame/
rtu.rs

1use crate::encoding::Writer;
2use crate::{DecodeError, EncodeError, UnitId};
3
4const fn build_crc16_table() -> [u16; 256] {
5    let mut table = [0u16; 256];
6    let mut i = 0;
7    while i < 256 {
8        let mut crc = i as u16;
9        let mut bit = 0;
10        while bit < 8 {
11            if (crc & 0x0001) != 0 {
12                crc = (crc >> 1) ^ 0xA001;
13            } else {
14                crc >>= 1;
15            }
16            bit += 1;
17        }
18        table[i] = crc;
19        i += 1;
20    }
21    table
22}
23
24const CRC16_TABLE: [u16; 256] = build_crc16_table();
25
26/// Compute the Modbus RTU CRC-16 checksum over the given data.
27pub fn crc16(data: &[u8]) -> u16 {
28    let mut crc = 0xFFFFu16;
29    for byte in data {
30        let idx = ((crc ^ (*byte as u16)) & 0x00FF) as usize;
31        crc = (crc >> 8) ^ CRC16_TABLE[idx];
32    }
33    crc
34}
35
36/// Encode a complete Modbus RTU frame (address + PDU + CRC) into the writer.
37pub fn encode_frame(w: &mut Writer<'_>, address: UnitId, pdu: &[u8]) -> Result<(), EncodeError> {
38    if pdu.is_empty() {
39        return Err(EncodeError::InvalidLength);
40    }
41
42    let addr = address.as_u8();
43    w.write_u8(addr)?;
44    w.write_all(pdu)?;
45
46    let mut tmp = [0u8; 254];
47    if pdu.len() > 253 {
48        return Err(EncodeError::ValueOutOfRange);
49    }
50    tmp[0] = addr;
51    tmp[1..1 + pdu.len()].copy_from_slice(pdu);
52    let crc = crc16(&tmp[..1 + pdu.len()]);
53    w.write_all(&crc.to_le_bytes())?;
54    Ok(())
55}
56
57/// Decode a Modbus RTU frame, verifying the CRC and returning the unit ID and PDU slice.
58pub fn decode_frame(data: &[u8]) -> Result<(UnitId, &[u8]), DecodeError> {
59    if data.len() < 4 {
60        return Err(DecodeError::InvalidLength);
61    }
62
63    let payload = &data[..data.len() - 2];
64    let expected = crc16(payload);
65    let got = u16::from_le_bytes([data[data.len() - 2], data[data.len() - 1]]);
66    if expected != got {
67        return Err(DecodeError::InvalidCrc);
68    }
69
70    let address = UnitId::new(payload[0]);
71    let pdu = &payload[1..];
72    if pdu.is_empty() {
73        return Err(DecodeError::InvalidLength);
74    }
75    Ok((address, pdu))
76}
77
78#[cfg(test)]
79mod tests {
80    use super::{crc16, decode_frame, encode_frame};
81    use crate::encoding::Writer;
82    use crate::{DecodeError, UnitId};
83
84    #[test]
85    fn crc16_known_vector() {
86        let frame_wo_crc = [0x01u8, 0x03, 0x00, 0x00, 0x00, 0x0A];
87        assert_eq!(crc16(&frame_wo_crc), 0xCDC5);
88    }
89
90    #[test]
91    fn rtu_roundtrip() {
92        let mut buf = [0u8; 32];
93        let mut w = Writer::new(&mut buf);
94        encode_frame(&mut w, UnitId::new(0x11), &[0x03, 0x00, 0x6B, 0x00, 0x03]).unwrap();
95        let written = w.as_written();
96
97        let (address, pdu) = decode_frame(written).unwrap();
98        assert_eq!(address, UnitId::new(0x11));
99        assert_eq!(pdu, &[0x03, 0x00, 0x6B, 0x00, 0x03]);
100    }
101
102    #[test]
103    fn detects_bad_crc() {
104        let bad = [0x11u8, 0x03, 0x00, 0x6B, 0x00, 0x03, 0x00, 0x00];
105        assert_eq!(decode_frame(&bad).unwrap_err(), DecodeError::InvalidCrc);
106    }
107}