Skip to content

Commit 062a72b

Browse files
elrafoonflosse
authored andcommitted
feat: add modbus/tcp client implementation analogous to existing modbus/rtu client
1 parent 6e44adb commit 062a72b

2 files changed

Lines changed: 151 additions & 0 deletions

File tree

src/codec/tcp/client.rs

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

src/codec/tcp/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use super::*;
44
use byteorder::{BigEndian, ByteOrder};
55

6+
pub mod client;
67
pub mod server;
78
pub use crate::frame::tcp::*;
89

0 commit comments

Comments
 (0)