Skip to main content

rusty_modbus_frame/
rtu_tcp.rs

1//! RTU-over-TCP codec for Modbus framing.
2//!
3//! Carries RTU frames inside a TCP stream. Since TCP has no inter-character
4//! silence for frame boundary detection, this codec uses a CRC-scanning
5//! approach: it buffers incoming data and tries progressively longer candidate
6//! frames (starting at 4 bytes) until a valid CRC-16 is found. This is how
7//! many production RTU-over-TCP implementations work.
8
9use bytes::{BufMut, BytesMut};
10use rusty_modbus_types::{MAX_PDU_SIZE, MAX_RTU_ADU_SIZE};
11use tokio_util::codec::{Decoder, Encoder};
12
13use crate::crc::{crc16, crc16_update};
14use crate::error::FrameError;
15use crate::frame::{Frame, FrameHeader};
16
17/// Minimum RTU frame size: `unit_id`(1) + FC(1) + CRC(2).
18const MIN_RTU_FRAME: usize = 4;
19const MIN_PDU_LENGTH: usize = 1;
20
21/// RTU-over-TCP codec.
22///
23/// Uses CRC-based length detection since TCP has no inter-character silence
24/// for frame boundaries. For each decode attempt, candidate frame lengths from
25/// 4 up to `MAX_RTU_ADU_SIZE` (or buffer length, whichever is smaller) are
26/// tested until a valid CRC-16 is found.
27#[derive(Debug, Default)]
28pub struct RtuOverTcpCodec;
29
30impl Decoder for RtuOverTcpCodec {
31    type Item = Frame;
32    type Error = FrameError;
33
34    fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
35        if src.len() < MIN_RTU_FRAME {
36            return Ok(None);
37        }
38
39        let max_len = src.len().min(MAX_RTU_ADU_SIZE);
40
41        // Scan candidate frame lengths from smallest to largest. Keep the CRC
42        // of each candidate data prefix incrementally instead of recomputing
43        // it from the start for every possible frame boundary.
44        let mut crc = 0xFFFF;
45        crc = crc16_update(crc, src[0]);
46        crc = crc16_update(crc, src[1]);
47        for candidate_len in MIN_RTU_FRAME..=max_len {
48            let data_end = candidate_len - 2;
49            let actual = u16::from_le_bytes([src[data_end], src[data_end + 1]]);
50            if crc == actual {
51                let unit_id = src[0];
52
53                let adu = src.split_to(candidate_len).freeze();
54                let pdu = adu.slice(1..adu.len() - 2);
55
56                return Ok(Some(Frame {
57                    header: FrameHeader::Rtu { unit_id },
58                    pdu,
59                }));
60            }
61
62            if candidate_len < max_len {
63                crc = crc16_update(crc, src[data_end]);
64            }
65        }
66
67        // If the buffer has grown beyond MAX_RTU_ADU_SIZE without a valid CRC
68        // match, the data is corrupt or mis-framed — discard what we cannot use.
69        if src.len() > MAX_RTU_ADU_SIZE {
70            return Err(FrameError::Truncated);
71        }
72
73        // Not enough data yet — ask for more.
74        Ok(None)
75    }
76}
77
78impl Encoder<Frame> for RtuOverTcpCodec {
79    type Error = FrameError;
80
81    fn encode(&mut self, item: Frame, dst: &mut BytesMut) -> Result<(), Self::Error> {
82        let unit_id = match item.header {
83            FrameHeader::Rtu { unit_id } => unit_id,
84            FrameHeader::Mbap(h) => h.unit_id,
85        };
86        validate_outgoing_pdu(item.pdu.len())?;
87
88        // Reserve space: unit_id(1) + PDU + CRC(2).
89        dst.reserve(1 + item.pdu.len() + 2);
90
91        dst.put_u8(unit_id);
92        dst.put_slice(&item.pdu);
93
94        // CRC-16 over [unit_id, pdu...].
95        let crc_start = dst.len() - 1 - item.pdu.len();
96        let crc = crc16(&dst[crc_start..]);
97        dst.put_u16_le(crc);
98
99        Ok(())
100    }
101}
102
103fn validate_outgoing_pdu(pdu_len: usize) -> Result<(), FrameError> {
104    if pdu_len < MIN_PDU_LENGTH {
105        return Err(FrameError::InvalidPduLength {
106            length: pdu_len,
107            minimum: MIN_PDU_LENGTH,
108        });
109    }
110    if pdu_len > MAX_PDU_SIZE {
111        return Err(FrameError::PduLengthOverflow {
112            length: pdu_len,
113            maximum: MAX_PDU_SIZE,
114        });
115    }
116    Ok(())
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::crc::verify_crc;
123
124    /// Build a valid RTU frame: [`unit_id`, pdu..., `crc_lo`, `crc_hi`].
125    fn make_rtu_frame(unit_id: u8, pdu: &[u8]) -> Vec<u8> {
126        let mut buf = vec![unit_id];
127        buf.extend_from_slice(pdu);
128        let crc = crc16(&buf);
129        buf.extend_from_slice(&crc.to_le_bytes());
130        buf
131    }
132
133    #[test]
134    fn decode_single_frame() {
135        let raw = make_rtu_frame(0x01, &[0x03, 0x00, 0x00, 0x00, 0x0A]);
136        let mut buf = BytesMut::from(&raw[..]);
137        let mut codec = RtuOverTcpCodec;
138
139        let frame = codec.decode(&mut buf).unwrap().unwrap();
140        assert_eq!(frame.unit_id(), 0x01);
141        assert_eq!(&frame.pdu[..], &[0x03, 0x00, 0x00, 0x00, 0x0A]);
142        assert!(buf.is_empty());
143    }
144
145    #[test]
146    fn decode_two_back_to_back_frames() {
147        let frame1 = make_rtu_frame(0x01, &[0x03, 0x02, 0x00, 0x64]);
148        let frame2 = make_rtu_frame(0x02, &[0x06, 0x00, 0x01, 0x00, 0x03]);
149
150        let mut buf = BytesMut::new();
151        buf.extend_from_slice(&frame1);
152        buf.extend_from_slice(&frame2);
153
154        let mut codec = RtuOverTcpCodec;
155
156        let f1 = codec.decode(&mut buf).unwrap().unwrap();
157        assert_eq!(f1.unit_id(), 0x01);
158
159        let f2 = codec.decode(&mut buf).unwrap().unwrap();
160        assert_eq!(f2.unit_id(), 0x02);
161
162        assert!(buf.is_empty());
163    }
164
165    #[test]
166    fn decode_incomplete_returns_none() {
167        let raw = make_rtu_frame(0x01, &[0x03, 0x00]);
168        // Feed only the first 3 bytes (incomplete).
169        let mut buf = BytesMut::from(&raw[..3]);
170        let mut codec = RtuOverTcpCodec;
171
172        assert!(codec.decode(&mut buf).unwrap().is_none());
173    }
174
175    #[test]
176    fn decode_partial_then_complete() {
177        let raw = make_rtu_frame(0x01, &[0x03, 0x02, 0xAB, 0xCD]);
178        let mut buf = BytesMut::new();
179        let mut codec = RtuOverTcpCodec;
180
181        // Feed partial data.
182        buf.extend_from_slice(&raw[..4]);
183        assert!(codec.decode(&mut buf).unwrap().is_none());
184
185        // Feed the rest.
186        buf.extend_from_slice(&raw[4..]);
187        let frame = codec.decode(&mut buf).unwrap().unwrap();
188        assert_eq!(frame.unit_id(), 0x01);
189        assert_eq!(&frame.pdu[..], &[0x03, 0x02, 0xAB, 0xCD]);
190    }
191
192    #[test]
193    fn encode_roundtrip() {
194        let original_pdu = vec![0x03, 0x02, 0x00, 0x64];
195        let frame = Frame {
196            header: FrameHeader::Rtu { unit_id: 0x01 },
197            pdu: bytes::Bytes::from(original_pdu.clone()),
198        };
199
200        let mut dst = BytesMut::new();
201        let mut codec = RtuOverTcpCodec;
202        codec.encode(frame, &mut dst).unwrap();
203
204        // Decode the encoded frame.
205        let decoded = codec.decode(&mut dst).unwrap().unwrap();
206        assert_eq!(decoded.unit_id(), 0x01);
207        assert_eq!(&decoded.pdu[..], &original_pdu[..]);
208    }
209
210    #[test]
211    fn encode_rejects_empty_pdu() {
212        let frame = Frame {
213            header: FrameHeader::Rtu { unit_id: 0x01 },
214            pdu: bytes::Bytes::new(),
215        };
216
217        let mut dst = BytesMut::new();
218        let mut codec = RtuOverTcpCodec;
219
220        let err = codec.encode(frame, &mut dst).unwrap_err();
221        assert!(matches!(err, FrameError::InvalidPduLength { .. }));
222    }
223
224    #[test]
225    fn encode_rejects_oversized_pdu() {
226        let frame = Frame {
227            header: FrameHeader::Rtu { unit_id: 0x01 },
228            pdu: bytes::Bytes::from(vec![0x03; MAX_PDU_SIZE + 1]),
229        };
230
231        let mut dst = BytesMut::new();
232        let mut codec = RtuOverTcpCodec;
233
234        let err = codec.encode(frame, &mut dst).unwrap_err();
235        assert!(matches!(err, FrameError::PduLengthOverflow { .. }));
236    }
237
238    #[test]
239    fn decode_exception_response() {
240        // Exception: unit_id=0x01, FC=0x83 (0x03|0x80), exception_code=0x02
241        let raw = make_rtu_frame(0x01, &[0x83, 0x02]);
242        let mut buf = BytesMut::from(&raw[..]);
243        let mut codec = RtuOverTcpCodec;
244
245        let frame = codec.decode(&mut buf).unwrap().unwrap();
246        assert_eq!(frame.unit_id(), 0x01);
247        assert_eq!(&frame.pdu[..], &[0x83, 0x02]);
248    }
249
250    #[test]
251    fn overflow_returns_error() {
252        // Fill buffer with random-looking data beyond MAX_RTU_ADU_SIZE that
253        // won't accidentally form a valid CRC.
254        let mut buf = BytesMut::new();
255        buf.extend_from_slice(&vec![0xAA; MAX_RTU_ADU_SIZE + 1]);
256        let mut codec = RtuOverTcpCodec;
257
258        let err = codec.decode(&mut buf).unwrap_err();
259        assert!(matches!(err, FrameError::Truncated));
260    }
261
262    #[test]
263    fn max_len_crc_miss_keeps_buffering() {
264        let raw = crc_miss_buffer(MAX_RTU_ADU_SIZE);
265        let mut buf = BytesMut::from(&raw[..]);
266        let mut codec = RtuOverTcpCodec;
267
268        assert!(codec.decode(&mut buf).unwrap().is_none());
269        assert_eq!(buf.len(), MAX_RTU_ADU_SIZE);
270    }
271
272    fn crc_miss_buffer(len: usize) -> Vec<u8> {
273        for salt in 0u8..=u8::MAX {
274            let candidate: Vec<u8> = (0..len)
275                .map(|i| {
276                    let byte = u8::try_from(i % 251).expect("modulo 251 fits u8");
277                    byte.wrapping_mul(37).wrapping_add(0xA5 ^ salt)
278                })
279                .collect();
280            if (MIN_RTU_FRAME..=len).all(|candidate_len| !verify_crc(&candidate[..candidate_len])) {
281                return candidate;
282            }
283        }
284        unreachable!("salted deterministic buffers should produce a CRC-miss case");
285    }
286}