modbus_core/codec/tcp/
client.rs

1// SPDX-FileCopyrightText: Copyright (c) 2018-2025 slowtec GmbH <post@slowtec.de>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Modbus TCP client (master) specific functions.
5use super::*;
6
7/// Encode an TCP request.
8pub fn encode_request(adu: RequestAdu, buf: &mut [u8]) -> Result<usize> {
9    let RequestAdu { hdr, pdu } = adu;
10    if buf.len() < 2 {
11        return Err(Error::BufferSize);
12    }
13    let len = pdu.encode(&mut buf[7..])?;
14    if buf.len() < len + 7 {
15        return Err(Error::BufferSize);
16    }
17    buf[..2].copy_from_slice(&hdr.transaction_id.to_be_bytes());
18    buf[2..4].fill(0);
19    buf[4..6].copy_from_slice(&(1 + len as u16).to_be_bytes());
20    buf[6] = hdr.unit_id;
21    Ok(len + 7)
22}
23
24/// Decode an TCP response.
25pub fn decode_response(buf: &[u8]) -> Result<Option<ResponseAdu<'_>>> {
26    if buf.is_empty() {
27        return Ok(None);
28    }
29    decode(DecoderType::Response, buf)
30        .and_then(|frame| {
31            let Some((
32                DecodedFrame {
33                    transaction_id,
34                    unit_id,
35                    pdu,
36                },
37                _frame_pos,
38            )) = frame
39            else {
40                return Ok(None);
41            };
42            let hdr = Header {
43                transaction_id,
44                unit_id,
45            };
46            // Decoding of the PDU should are unlikely to fail due
47            // to transmission errors, because the frame's bytes
48            // have already been verified with the CRC.
49            let response = ExceptionResponse::try_from(pdu)
50                .map(|er| ResponsePdu(Err(er)))
51                .or_else(|_| Response::try_from(pdu).map(|r| ResponsePdu(Ok(r))))
52                .map(|pdu| Some(ResponseAdu { hdr, pdu }));
53            #[cfg(feature = "log")]
54            if let Err(error) = response {
55                // Unrecoverable error
56                log::error!("Failed to decode Response PDU: {error}");
57            }
58            response
59        })
60        .map_err(|_| {
61            // Decoding the transport frame is non-destructive and must
62            // never fail!
63            unreachable!();
64        })
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn decode_empty_response() {
73        let req = decode_response(&[]).unwrap();
74        assert!(req.is_none());
75    }
76
77    #[test]
78    fn decode_partly_received_response() {
79        let buf = &[
80            0x12, // slave address
81            0x16, // function code
82        ];
83        let req = decode_response(buf).unwrap();
84        assert!(req.is_none());
85    }
86
87    #[test]
88    fn encode_write_single_register_request() {
89        let mut buf = [0u8; 255];
90        let sz = encode_request(
91            RequestAdu {
92                hdr: Header {
93                    transaction_id: 0x1234,
94                    unit_id: 0x12,
95                },
96                pdu: RequestPdu(Request::WriteSingleRegister(0x2222, 0xABCD)),
97            },
98            &mut buf,
99        )
100        .expect("Error encoding request");
101
102        let req = &buf[..sz];
103        assert_eq!(
104            req,
105            &[
106                0x12, // transaction id
107                0x34, // transaction id
108                0x00, // protocol id
109                0x00, // protocol id
110                0x00, // length high byte
111                0x06, // length low byte
112                0x12, // slave address
113                0x06, // function code
114                0x22, // addr
115                0x22, // addr
116                0xAB, // value
117                0xCD, // value
118            ]
119        );
120    }
121
122    #[test]
123    fn decode_write_single_register_response() {
124        use crate::frame::Response;
125        let rsp = &[
126            0x12, 0x34, 0x00, 0x00, 0x00, 0x06, 0x12, 0x06, 0x22, 0x22, 0xAB, 0xCD,
127        ];
128
129        assert!(matches!(
130            decode_response(rsp),
131            Ok(Some(ResponseAdu {
132                hdr: Header {
133                    transaction_id: 0x1234,
134                    unit_id: 0x12
135                },
136                pdu: ResponsePdu(Ok(Response::WriteSingleRegister(0x2222, 0xABCD)))
137            }))
138        ));
139    }
140
141    #[test]
142    fn decode_malformed_write_single_register_response() {
143        let rsp = &[0x12, 0x06, 0x22, 0x22, 0xAB, 0x65, 0x9E];
144
145        assert!(matches!(decode_response(rsp), Ok(None)));
146    }
147
148    #[test]
149    fn decode_bad_crc_write_single_register_response() {
150        let rsp = &[0x12, 0x06, 0x22, 0x22, 0xAB, 0xCD, 0x5F, 0xBE];
151
152        assert!(matches!(decode_response(rsp), Ok(None)));
153    }
154}