Skip to main content

rusty_modbus_frame/
rtu.rs

1//! RTU serial codec for Modbus framing.
2//!
3//! Frame boundaries on serial are determined by inter-character silence
4//! (3.5 character times), which is handled by the transport layer.
5//! This codec validates the CRC-16 and extracts the PDU from a complete frame.
6
7use bytes::{BufMut, BytesMut};
8use rusty_modbus_types::{MAX_PDU_SIZE, MAX_RTU_ADU_SIZE};
9use tokio_util::codec::{Decoder, Encoder};
10
11use crate::crc::{crc16, verify_crc};
12use crate::error::FrameError;
13use crate::frame::{Frame, FrameHeader};
14
15/// Minimum RTU frame size: `unit_id`(1) + FC(1) + CRC(2).
16const MIN_RTU_FRAME: usize = 4;
17const MIN_PDU_LENGTH: usize = 1;
18
19/// RTU codec for serial Modbus framing.
20///
21/// Frame boundaries are determined by inter-character silence (handled by the
22/// transport layer). This codec validates CRC and extracts the PDU.
23#[derive(Debug, Default)]
24pub struct RtuCodec;
25
26impl Decoder for RtuCodec {
27    type Item = Frame;
28    type Error = FrameError;
29
30    fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
31        // Need at least unit_id + FC + CRC_lo + CRC_hi.
32        if src.len() < MIN_RTU_FRAME {
33            return Ok(None);
34        }
35        if src.len() > MAX_RTU_ADU_SIZE {
36            return Err(FrameError::PduLengthOverflow {
37                length: src.len() - 3,
38                maximum: MAX_PDU_SIZE,
39            });
40        }
41
42        // For serial transport the entire frame is in the buffer (the transport
43        // delivers complete frames after silence detection). Validate CRC over
44        // the whole buffer.
45        if !verify_crc(src) {
46            let data_end = src.len() - 2;
47            let expected = crc16(&src[..data_end]);
48            let actual = u16::from_le_bytes([src[data_end], src[data_end + 1]]);
49            return Err(FrameError::CrcMismatch { expected, actual });
50        }
51
52        let unit_id = src[0];
53
54        // Consume the entire buffer and freeze into a Bytes handle.
55        let adu = src.split_to(src.len()).freeze();
56
57        // PDU is everything between the unit_id byte and the trailing 2-byte CRC.
58        let pdu = adu.slice(1..adu.len() - 2);
59
60        Ok(Some(Frame {
61            header: FrameHeader::Rtu { unit_id },
62            pdu,
63        }))
64    }
65}
66
67impl Encoder<Frame> for RtuCodec {
68    type Error = FrameError;
69
70    fn encode(&mut self, item: Frame, dst: &mut BytesMut) -> Result<(), Self::Error> {
71        let unit_id = match item.header {
72            FrameHeader::Rtu { unit_id } => unit_id,
73            FrameHeader::Mbap(h) => h.unit_id,
74        };
75        validate_outgoing_pdu(item.pdu.len())?;
76
77        // Reserve space: unit_id(1) + PDU + CRC(2).
78        dst.reserve(1 + item.pdu.len() + 2);
79
80        dst.put_u8(unit_id);
81        dst.put_slice(&item.pdu);
82
83        // CRC-16 is computed over [unit_id, pdu...].
84        // Build a temporary slice from what we just wrote.
85        let crc_start = dst.len() - 1 - item.pdu.len();
86        let crc = crc16(&dst[crc_start..]);
87        dst.put_u16_le(crc);
88
89        Ok(())
90    }
91}
92
93fn validate_outgoing_pdu(pdu_len: usize) -> Result<(), FrameError> {
94    if pdu_len < MIN_PDU_LENGTH {
95        return Err(FrameError::InvalidPduLength {
96            length: pdu_len,
97            minimum: MIN_PDU_LENGTH,
98        });
99    }
100    if pdu_len > MAX_PDU_SIZE {
101        return Err(FrameError::PduLengthOverflow {
102            length: pdu_len,
103            maximum: MAX_PDU_SIZE,
104        });
105    }
106    Ok(())
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    /// Build a valid RTU frame: [`unit_id`, pdu..., `crc_lo`, `crc_hi`].
114    fn make_rtu_frame(unit_id: u8, pdu: &[u8]) -> Vec<u8> {
115        let mut buf = vec![unit_id];
116        buf.extend_from_slice(pdu);
117        let crc = crc16(&buf);
118        buf.extend_from_slice(&crc.to_le_bytes());
119        buf
120    }
121
122    #[test]
123    fn decode_valid_frame() {
124        let raw = make_rtu_frame(0x01, &[0x03, 0x00, 0x00, 0x00, 0x0A]);
125        let mut buf = BytesMut::from(&raw[..]);
126        let mut codec = RtuCodec;
127
128        let frame = codec.decode(&mut buf).unwrap().unwrap();
129        assert_eq!(frame.unit_id(), 0x01);
130        assert_eq!(&frame.pdu[..], &[0x03, 0x00, 0x00, 0x00, 0x0A]);
131        assert!(buf.is_empty());
132    }
133
134    #[test]
135    fn decode_too_short() {
136        let mut buf = BytesMut::from(&[0x01, 0x03, 0xCD][..]);
137        let mut codec = RtuCodec;
138
139        assert!(codec.decode(&mut buf).unwrap().is_none());
140    }
141
142    #[test]
143    fn decode_bad_crc() {
144        let mut raw = make_rtu_frame(0x01, &[0x03, 0x00]);
145        // Corrupt the CRC.
146        let last = raw.len() - 1;
147        raw[last] ^= 0xFF;
148
149        let mut buf = BytesMut::from(&raw[..]);
150        let mut codec = RtuCodec;
151
152        let err = codec.decode(&mut buf).unwrap_err();
153        assert!(matches!(err, FrameError::CrcMismatch { .. }));
154    }
155
156    #[test]
157    fn encode_roundtrip() {
158        let original_pdu = vec![0x03, 0x00, 0x00, 0x00, 0x0A];
159        let frame = Frame {
160            header: FrameHeader::Rtu { unit_id: 0x01 },
161            pdu: bytes::Bytes::from(original_pdu.clone()),
162        };
163
164        let mut dst = BytesMut::new();
165        let mut codec = RtuCodec;
166        codec.encode(frame, &mut dst).unwrap();
167
168        // Decode the encoded frame.
169        let decoded = codec.decode(&mut dst).unwrap().unwrap();
170        assert_eq!(decoded.unit_id(), 0x01);
171        assert_eq!(&decoded.pdu[..], &original_pdu[..]);
172    }
173
174    #[test]
175    fn encode_rejects_empty_pdu() {
176        let frame = Frame {
177            header: FrameHeader::Rtu { unit_id: 0x01 },
178            pdu: bytes::Bytes::new(),
179        };
180
181        let mut dst = BytesMut::new();
182        let mut codec = RtuCodec;
183
184        let err = codec.encode(frame, &mut dst).unwrap_err();
185        assert!(matches!(err, FrameError::InvalidPduLength { .. }));
186    }
187
188    #[test]
189    fn encode_rejects_oversized_pdu() {
190        let frame = Frame {
191            header: FrameHeader::Rtu { unit_id: 0x01 },
192            pdu: bytes::Bytes::from(vec![0x03; MAX_PDU_SIZE + 1]),
193        };
194
195        let mut dst = BytesMut::new();
196        let mut codec = RtuCodec;
197
198        let err = codec.encode(frame, &mut dst).unwrap_err();
199        assert!(matches!(err, FrameError::PduLengthOverflow { .. }));
200    }
201
202    #[test]
203    fn decode_rejects_oversized_frame_even_with_valid_crc() {
204        let raw = make_rtu_frame(0x01, &vec![0x03; MAX_PDU_SIZE + 1]);
205        let mut buf = BytesMut::from(&raw[..]);
206        let mut codec = RtuCodec;
207
208        let err = codec.decode(&mut buf).unwrap_err();
209        assert!(matches!(err, FrameError::PduLengthOverflow { .. }));
210    }
211}