tokio_modbus/client/
mod.rs

1// SPDX-FileCopyrightText: Copyright (c) 2017-2025 slowtec GmbH <post@slowtec.de>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Modbus clients
5
6use std::{borrow::Cow, io};
7
8use async_trait::async_trait;
9
10use crate::{frame::*, slave::*, Result};
11
12#[cfg(feature = "rtu")]
13pub mod rtu;
14
15#[cfg(feature = "tcp")]
16pub mod tcp;
17
18#[cfg(feature = "sync")]
19pub mod sync;
20
21/// Transport independent asynchronous client trait
22#[async_trait]
23pub trait Client: SlaveContext + Send {
24    /// Invokes a _Modbus_ function.
25    async fn call(&mut self, request: Request<'_>) -> Result<Response>;
26
27    /// Disconnects the client.
28    ///
29    /// Permanently disconnects the client by shutting down the
30    /// underlying stream in a graceful manner (`AsyncDrop`).
31    ///
32    /// Dropping the client without explicitly disconnecting it
33    /// beforehand should also work and free all resources. The
34    /// actual behavior might depend on the underlying transport
35    /// protocol (RTU/TCP) that is used by the client.
36    async fn disconnect(&mut self) -> io::Result<()>;
37}
38
39/// Asynchronous _Modbus_ reader
40#[async_trait]
41pub trait Reader: Client {
42    /// Read multiple coils (0x01)
43    async fn read_coils(&mut self, addr: Address, cnt: Quantity) -> Result<Vec<Coil>>;
44
45    /// Read multiple discrete inputs (0x02)
46    async fn read_discrete_inputs(&mut self, addr: Address, cnt: Quantity) -> Result<Vec<Coil>>;
47
48    /// Read multiple holding registers (0x03)
49    async fn read_holding_registers(&mut self, addr: Address, cnt: Quantity) -> Result<Vec<Word>>;
50
51    /// Read multiple input registers (0x04)
52    async fn read_input_registers(&mut self, addr: Address, cnt: Quantity) -> Result<Vec<Word>>;
53
54    /// Read and write multiple holding registers (0x17)
55    ///
56    /// The write operation is performed before the read unlike
57    /// the name of the operation might suggest!
58    async fn read_write_multiple_registers(
59        &mut self,
60        read_addr: Address,
61        read_count: Quantity,
62        write_addr: Address,
63        write_data: &[Word],
64    ) -> Result<Vec<Word>>;
65
66    /// Read device identification (0x2B / 0x0E)
67    async fn read_device_identification(
68        &mut self,
69        read_code: ReadCode,
70        object_id: ObjectId,
71    ) -> Result<ReadDeviceIdentificationResponse>;
72}
73
74/// Asynchronous Modbus writer
75#[async_trait]
76pub trait Writer: Client {
77    /// Write a single coil (0x05)
78    async fn write_single_coil(&mut self, addr: Address, coil: Coil) -> Result<()>;
79
80    /// Write a single holding register (0x06)
81    async fn write_single_register(&mut self, addr: Address, word: Word) -> Result<()>;
82
83    /// Write multiple coils (0x0F)
84    async fn write_multiple_coils(&mut self, addr: Address, coils: &'_ [Coil]) -> Result<()>;
85
86    /// Write multiple holding registers (0x10)
87    async fn write_multiple_registers(&mut self, addr: Address, words: &[Word]) -> Result<()>;
88
89    /// Set or clear individual bits of a holding register (0x16)
90    async fn masked_write_register(
91        &mut self,
92        addr: Address,
93        and_mask: Word,
94        or_mask: Word,
95    ) -> Result<()>;
96}
97
98/// Asynchronous Modbus client context
99#[allow(missing_debug_implementations)]
100pub struct Context {
101    client: Box<dyn Client>,
102}
103
104impl From<Box<dyn Client>> for Context {
105    fn from(client: Box<dyn Client>) -> Self {
106        Self { client }
107    }
108}
109
110impl From<Context> for Box<dyn Client> {
111    fn from(val: Context) -> Self {
112        val.client
113    }
114}
115
116#[async_trait]
117impl Client for Context {
118    async fn call(&mut self, request: Request<'_>) -> Result<Response> {
119        self.client.call(request).await
120    }
121
122    async fn disconnect(&mut self) -> io::Result<()> {
123        self.client.disconnect().await
124    }
125}
126
127impl SlaveContext for Context {
128    fn set_slave(&mut self, slave: Slave) {
129        self.client.set_slave(slave);
130    }
131}
132
133#[async_trait]
134impl Reader for Context {
135    async fn read_coils<'a>(&'a mut self, addr: Address, cnt: Quantity) -> Result<Vec<Coil>> {
136        self.client
137            .call(Request::ReadCoils(addr, cnt))
138            .await
139            .map(|result| {
140                result.map(|response| match response {
141                    Response::ReadCoils(mut coils) => {
142                        debug_assert!(coils.len() >= cnt.into());
143                        coils.truncate(cnt.into());
144                        coils
145                    }
146                    _ => unreachable!("call() should reject mismatching responses"),
147                })
148            })
149    }
150
151    async fn read_discrete_inputs<'a>(
152        &'a mut self,
153        addr: Address,
154        cnt: Quantity,
155    ) -> Result<Vec<Coil>> {
156        self.client
157            .call(Request::ReadDiscreteInputs(addr, cnt))
158            .await
159            .map(|result| {
160                result.map(|response| match response {
161                    Response::ReadDiscreteInputs(mut coils) => {
162                        debug_assert!(coils.len() >= cnt.into());
163                        coils.truncate(cnt.into());
164                        coils
165                    }
166                    _ => unreachable!("call() should reject mismatching responses"),
167                })
168            })
169    }
170
171    async fn read_input_registers<'a>(
172        &'a mut self,
173        addr: Address,
174        cnt: Quantity,
175    ) -> Result<Vec<Word>> {
176        self.client
177            .call(Request::ReadInputRegisters(addr, cnt))
178            .await
179            .map(|result| {
180                result.map(|response| match response {
181                    Response::ReadInputRegisters(words) => {
182                        debug_assert_eq!(words.len(), cnt.into());
183                        words
184                    }
185                    _ => unreachable!("call() should reject mismatching responses"),
186                })
187            })
188    }
189
190    async fn read_holding_registers<'a>(
191        &'a mut self,
192        addr: Address,
193        cnt: Quantity,
194    ) -> Result<Vec<Word>> {
195        self.client
196            .call(Request::ReadHoldingRegisters(addr, cnt))
197            .await
198            .map(|result| {
199                result.map(|response| match response {
200                    Response::ReadHoldingRegisters(words) => {
201                        debug_assert_eq!(words.len(), cnt.into());
202                        words
203                    }
204                    _ => unreachable!("call() should reject mismatching responses"),
205                })
206            })
207    }
208
209    async fn read_write_multiple_registers<'a>(
210        &'a mut self,
211        read_addr: Address,
212        read_count: Quantity,
213        write_addr: Address,
214        write_data: &[Word],
215    ) -> Result<Vec<Word>> {
216        self.client
217            .call(Request::ReadWriteMultipleRegisters(
218                read_addr,
219                read_count,
220                write_addr,
221                Cow::Borrowed(write_data),
222            ))
223            .await
224            .map(|result| {
225                result.map(|response| match response {
226                    Response::ReadWriteMultipleRegisters(words) => {
227                        debug_assert_eq!(words.len(), read_count.into());
228                        words
229                    }
230                    _ => unreachable!("call() should reject mismatching responses"),
231                })
232            })
233    }
234
235    async fn read_device_identification(
236        &mut self,
237        read_code: ReadCode,
238        object_id: ObjectId,
239    ) -> Result<ReadDeviceIdentificationResponse> {
240        self.client
241            .call(Request::ReadDeviceIdentification(read_code, object_id))
242            .await
243            .map(|result| {
244                result.map(|response| match response {
245                    Response::ReadDeviceIdentification(response) => response,
246                    _ => unreachable!("call() should reject mismatching responses"),
247                })
248            })
249    }
250}
251
252#[async_trait]
253impl Writer for Context {
254    async fn write_single_coil<'a>(&'a mut self, addr: Address, coil: Coil) -> Result<()> {
255        self.client
256            .call(Request::WriteSingleCoil(addr, coil))
257            .await
258            .map(|result| {
259                result.map(|response| match response {
260                    Response::WriteSingleCoil(rsp_addr, rsp_coil) => {
261                        debug_assert_eq!(addr, rsp_addr);
262                        debug_assert_eq!(coil, rsp_coil);
263                    }
264                    _ => unreachable!("call() should reject mismatching responses"),
265                })
266            })
267    }
268
269    async fn write_multiple_coils<'a>(&'a mut self, addr: Address, coils: &[Coil]) -> Result<()> {
270        let cnt = coils.len();
271        self.client
272            .call(Request::WriteMultipleCoils(addr, Cow::Borrowed(coils)))
273            .await
274            .map(|result| {
275                result.map(|response| match response {
276                    Response::WriteMultipleCoils(rsp_addr, rsp_cnt) => {
277                        debug_assert_eq!(addr, rsp_addr);
278                        debug_assert_eq!(cnt, rsp_cnt.into());
279                    }
280                    _ => unreachable!("call() should reject mismatching responses"),
281                })
282            })
283    }
284
285    async fn write_single_register<'a>(&'a mut self, addr: Address, word: Word) -> Result<()> {
286        self.client
287            .call(Request::WriteSingleRegister(addr, word))
288            .await
289            .map(|result| {
290                result.map(|response| match response {
291                    Response::WriteSingleRegister(rsp_addr, rsp_word) => {
292                        debug_assert_eq!(addr, rsp_addr);
293                        debug_assert_eq!(word, rsp_word);
294                    }
295                    _ => unreachable!("call() should reject mismatching responses"),
296                })
297            })
298    }
299
300    async fn write_multiple_registers<'a>(
301        &'a mut self,
302        addr: Address,
303        data: &[Word],
304    ) -> Result<()> {
305        let cnt = data.len();
306        self.client
307            .call(Request::WriteMultipleRegisters(addr, Cow::Borrowed(data)))
308            .await
309            .map(|result| {
310                result.map(|response| match response {
311                    Response::WriteMultipleRegisters(rsp_addr, rsp_cnt) => {
312                        debug_assert_eq!(addr, rsp_addr);
313                        debug_assert_eq!(cnt, rsp_cnt.into());
314                    }
315                    _ => unreachable!("call() should reject mismatching responses"),
316                })
317            })
318    }
319
320    async fn masked_write_register<'a>(
321        &'a mut self,
322        addr: Address,
323        and_mask: Word,
324        or_mask: Word,
325    ) -> Result<()> {
326        self.client
327            .call(Request::MaskWriteRegister(addr, and_mask, or_mask))
328            .await
329            .map(|result| {
330                result.map(|response| match response {
331                    Response::MaskWriteRegister(rsp_addr, rsp_and_mask, rsp_or_mask) => {
332                        debug_assert_eq!(addr, rsp_addr);
333                        debug_assert_eq!(and_mask, rsp_and_mask);
334                        debug_assert_eq!(or_mask, rsp_or_mask);
335                    }
336                    _ => unreachable!("call() should reject mismatching responses"),
337                })
338            })
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use crate::{Error, Result};
345
346    use super::*;
347    use std::{io, sync::Mutex};
348
349    #[derive(Default, Debug)]
350    pub(crate) struct ClientMock {
351        slave: Option<Slave>,
352        last_request: Mutex<Option<Request<'static>>>,
353        next_response: Option<Result<Response>>,
354    }
355
356    #[allow(dead_code)]
357    impl ClientMock {
358        pub(crate) fn slave(&self) -> Option<Slave> {
359            self.slave
360        }
361
362        pub(crate) fn last_request(&self) -> &Mutex<Option<Request<'static>>> {
363            &self.last_request
364        }
365
366        pub(crate) fn set_next_response(&mut self, next_response: Result<Response>) {
367            self.next_response = Some(next_response);
368        }
369    }
370
371    #[async_trait]
372    impl Client for ClientMock {
373        async fn call(&mut self, request: Request<'_>) -> Result<Response> {
374            *self.last_request.lock().unwrap() = Some(request.into_owned());
375            match self.next_response.take().unwrap() {
376                Ok(response) => Ok(response),
377                Err(Error::Transport(err)) => {
378                    Err(io::Error::new(err.kind(), format!("{err}")).into())
379                }
380                Err(err) => Err(err),
381            }
382        }
383
384        async fn disconnect(&mut self) -> io::Result<()> {
385            Ok(())
386        }
387    }
388
389    impl SlaveContext for ClientMock {
390        fn set_slave(&mut self, slave: Slave) {
391            self.slave = Some(slave);
392        }
393    }
394
395    #[test]
396    fn read_some_coils() {
397        // The protocol will always return entire bytes with, i.e.
398        // a multiple of 8 coils.
399        let response_coils = [true, false, false, true, false, true, false, true];
400        for num_coils in 1..8 {
401            let mut client = Box::<ClientMock>::default();
402            client.set_next_response(Ok(Ok(Response::ReadCoils(response_coils.to_vec()))));
403            let mut context = Context { client };
404            context.set_slave(Slave(1));
405            let coils = futures::executor::block_on(context.read_coils(1, num_coils))
406                .unwrap()
407                .unwrap();
408            assert_eq!(&response_coils[0..num_coils as usize], &coils[..]);
409        }
410    }
411
412    #[test]
413    fn read_some_discrete_inputs() {
414        // The protocol will always return entire bytes with, i.e.
415        // a multiple of 8 coils.
416        let response_inputs = [true, false, false, true, false, true, false, true];
417        for num_inputs in 1..8 {
418            let mut client = Box::<ClientMock>::default();
419            client.set_next_response(Ok(Ok(Response::ReadDiscreteInputs(
420                response_inputs.to_vec(),
421            ))));
422            let mut context = Context { client };
423            context.set_slave(Slave(1));
424            let inputs = futures::executor::block_on(context.read_discrete_inputs(1, num_inputs))
425                .unwrap()
426                .unwrap();
427            assert_eq!(&response_inputs[0..num_inputs as usize], &inputs[..]);
428        }
429    }
430}