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
67/// Asynchronous Modbus writer
68#[async_trait]
69pub trait Writer: Client {
70    /// Write a single coil (0x05)
71    async fn write_single_coil(&mut self, addr: Address, coil: Coil) -> Result<()>;
72
73    /// Write a single holding register (0x06)
74    async fn write_single_register(&mut self, addr: Address, word: Word) -> Result<()>;
75
76    /// Write multiple coils (0x0F)
77    async fn write_multiple_coils(&mut self, addr: Address, coils: &'_ [Coil]) -> Result<()>;
78
79    /// Write multiple holding registers (0x10)
80    async fn write_multiple_registers(&mut self, addr: Address, words: &[Word]) -> Result<()>;
81
82    /// Set or clear individual bits of a holding register (0x16)
83    async fn masked_write_register(
84        &mut self,
85        addr: Address,
86        and_mask: Word,
87        or_mask: Word,
88    ) -> Result<()>;
89}
90
91/// Asynchronous Modbus client context
92#[allow(missing_debug_implementations)]
93pub struct Context {
94    client: Box<dyn Client>,
95}
96
97impl From<Box<dyn Client>> for Context {
98    fn from(client: Box<dyn Client>) -> Self {
99        Self { client }
100    }
101}
102
103impl From<Context> for Box<dyn Client> {
104    fn from(val: Context) -> Self {
105        val.client
106    }
107}
108
109#[async_trait]
110impl Client for Context {
111    async fn call(&mut self, request: Request<'_>) -> Result<Response> {
112        self.client.call(request).await
113    }
114
115    async fn disconnect(&mut self) -> io::Result<()> {
116        self.client.disconnect().await
117    }
118}
119
120impl SlaveContext for Context {
121    fn set_slave(&mut self, slave: Slave) {
122        self.client.set_slave(slave);
123    }
124}
125
126#[async_trait]
127impl Reader for Context {
128    async fn read_coils<'a>(&'a mut self, addr: Address, cnt: Quantity) -> Result<Vec<Coil>> {
129        self.client
130            .call(Request::ReadCoils(addr, cnt))
131            .await
132            .map(|result| {
133                result.map(|response| match response {
134                    Response::ReadCoils(mut coils) => {
135                        debug_assert!(coils.len() >= cnt.into());
136                        coils.truncate(cnt.into());
137                        coils
138                    }
139                    _ => unreachable!("call() should reject mismatching responses"),
140                })
141            })
142    }
143
144    async fn read_discrete_inputs<'a>(
145        &'a mut self,
146        addr: Address,
147        cnt: Quantity,
148    ) -> Result<Vec<Coil>> {
149        self.client
150            .call(Request::ReadDiscreteInputs(addr, cnt))
151            .await
152            .map(|result| {
153                result.map(|response| match response {
154                    Response::ReadDiscreteInputs(mut coils) => {
155                        debug_assert!(coils.len() >= cnt.into());
156                        coils.truncate(cnt.into());
157                        coils
158                    }
159                    _ => unreachable!("call() should reject mismatching responses"),
160                })
161            })
162    }
163
164    async fn read_input_registers<'a>(
165        &'a mut self,
166        addr: Address,
167        cnt: Quantity,
168    ) -> Result<Vec<Word>> {
169        self.client
170            .call(Request::ReadInputRegisters(addr, cnt))
171            .await
172            .map(|result| {
173                result.map(|response| match response {
174                    Response::ReadInputRegisters(words) => {
175                        debug_assert_eq!(words.len(), cnt.into());
176                        words
177                    }
178                    _ => unreachable!("call() should reject mismatching responses"),
179                })
180            })
181    }
182
183    async fn read_holding_registers<'a>(
184        &'a mut self,
185        addr: Address,
186        cnt: Quantity,
187    ) -> Result<Vec<Word>> {
188        self.client
189            .call(Request::ReadHoldingRegisters(addr, cnt))
190            .await
191            .map(|result| {
192                result.map(|response| match response {
193                    Response::ReadHoldingRegisters(words) => {
194                        debug_assert_eq!(words.len(), cnt.into());
195                        words
196                    }
197                    _ => unreachable!("call() should reject mismatching responses"),
198                })
199            })
200    }
201
202    async fn read_write_multiple_registers<'a>(
203        &'a mut self,
204        read_addr: Address,
205        read_count: Quantity,
206        write_addr: Address,
207        write_data: &[Word],
208    ) -> Result<Vec<Word>> {
209        self.client
210            .call(Request::ReadWriteMultipleRegisters(
211                read_addr,
212                read_count,
213                write_addr,
214                Cow::Borrowed(write_data),
215            ))
216            .await
217            .map(|result| {
218                result.map(|response| match response {
219                    Response::ReadWriteMultipleRegisters(words) => {
220                        debug_assert_eq!(words.len(), read_count.into());
221                        words
222                    }
223                    _ => unreachable!("call() should reject mismatching responses"),
224                })
225            })
226    }
227}
228
229#[async_trait]
230impl Writer for Context {
231    async fn write_single_coil<'a>(&'a mut self, addr: Address, coil: Coil) -> Result<()> {
232        self.client
233            .call(Request::WriteSingleCoil(addr, coil))
234            .await
235            .map(|result| {
236                result.map(|response| match response {
237                    Response::WriteSingleCoil(rsp_addr, rsp_coil) => {
238                        debug_assert_eq!(addr, rsp_addr);
239                        debug_assert_eq!(coil, rsp_coil);
240                    }
241                    _ => unreachable!("call() should reject mismatching responses"),
242                })
243            })
244    }
245
246    async fn write_multiple_coils<'a>(&'a mut self, addr: Address, coils: &[Coil]) -> Result<()> {
247        let cnt = coils.len();
248        self.client
249            .call(Request::WriteMultipleCoils(addr, Cow::Borrowed(coils)))
250            .await
251            .map(|result| {
252                result.map(|response| match response {
253                    Response::WriteMultipleCoils(rsp_addr, rsp_cnt) => {
254                        debug_assert_eq!(addr, rsp_addr);
255                        debug_assert_eq!(cnt, rsp_cnt.into());
256                    }
257                    _ => unreachable!("call() should reject mismatching responses"),
258                })
259            })
260    }
261
262    async fn write_single_register<'a>(&'a mut self, addr: Address, word: Word) -> Result<()> {
263        self.client
264            .call(Request::WriteSingleRegister(addr, word))
265            .await
266            .map(|result| {
267                result.map(|response| match response {
268                    Response::WriteSingleRegister(rsp_addr, rsp_word) => {
269                        debug_assert_eq!(addr, rsp_addr);
270                        debug_assert_eq!(word, rsp_word);
271                    }
272                    _ => unreachable!("call() should reject mismatching responses"),
273                })
274            })
275    }
276
277    async fn write_multiple_registers<'a>(
278        &'a mut self,
279        addr: Address,
280        data: &[Word],
281    ) -> Result<()> {
282        let cnt = data.len();
283        self.client
284            .call(Request::WriteMultipleRegisters(addr, Cow::Borrowed(data)))
285            .await
286            .map(|result| {
287                result.map(|response| match response {
288                    Response::WriteMultipleRegisters(rsp_addr, rsp_cnt) => {
289                        debug_assert_eq!(addr, rsp_addr);
290                        debug_assert_eq!(cnt, rsp_cnt.into());
291                    }
292                    _ => unreachable!("call() should reject mismatching responses"),
293                })
294            })
295    }
296
297    async fn masked_write_register<'a>(
298        &'a mut self,
299        addr: Address,
300        and_mask: Word,
301        or_mask: Word,
302    ) -> Result<()> {
303        self.client
304            .call(Request::MaskWriteRegister(addr, and_mask, or_mask))
305            .await
306            .map(|result| {
307                result.map(|response| match response {
308                    Response::MaskWriteRegister(rsp_addr, rsp_and_mask, rsp_or_mask) => {
309                        debug_assert_eq!(addr, rsp_addr);
310                        debug_assert_eq!(and_mask, rsp_and_mask);
311                        debug_assert_eq!(or_mask, rsp_or_mask);
312                    }
313                    _ => unreachable!("call() should reject mismatching responses"),
314                })
315            })
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use crate::{Error, Result};
322
323    use super::*;
324    use std::{io, sync::Mutex};
325
326    #[derive(Default, Debug)]
327    pub(crate) struct ClientMock {
328        slave: Option<Slave>,
329        last_request: Mutex<Option<Request<'static>>>,
330        next_response: Option<Result<Response>>,
331    }
332
333    #[allow(dead_code)]
334    impl ClientMock {
335        pub(crate) fn slave(&self) -> Option<Slave> {
336            self.slave
337        }
338
339        pub(crate) fn last_request(&self) -> &Mutex<Option<Request<'static>>> {
340            &self.last_request
341        }
342
343        pub(crate) fn set_next_response(&mut self, next_response: Result<Response>) {
344            self.next_response = Some(next_response);
345        }
346    }
347
348    #[async_trait]
349    impl Client for ClientMock {
350        async fn call(&mut self, request: Request<'_>) -> Result<Response> {
351            *self.last_request.lock().unwrap() = Some(request.into_owned());
352            match self.next_response.take().unwrap() {
353                Ok(response) => Ok(response),
354                Err(Error::Transport(err)) => {
355                    Err(io::Error::new(err.kind(), format!("{err}")).into())
356                }
357                Err(err) => Err(err),
358            }
359        }
360
361        async fn disconnect(&mut self) -> io::Result<()> {
362            Ok(())
363        }
364    }
365
366    impl SlaveContext for ClientMock {
367        fn set_slave(&mut self, slave: Slave) {
368            self.slave = Some(slave);
369        }
370    }
371
372    #[test]
373    fn read_some_coils() {
374        // The protocol will always return entire bytes with, i.e.
375        // a multiple of 8 coils.
376        let response_coils = [true, false, false, true, false, true, false, true];
377        for num_coils in 1..8 {
378            let mut client = Box::<ClientMock>::default();
379            client.set_next_response(Ok(Ok(Response::ReadCoils(response_coils.to_vec()))));
380            let mut context = Context { client };
381            context.set_slave(Slave(1));
382            let coils = futures::executor::block_on(context.read_coils(1, num_coils))
383                .unwrap()
384                .unwrap();
385            assert_eq!(&response_coils[0..num_coils as usize], &coils[..]);
386        }
387    }
388
389    #[test]
390    fn read_some_discrete_inputs() {
391        // The protocol will always return entire bytes with, i.e.
392        // a multiple of 8 coils.
393        let response_inputs = [true, false, false, true, false, true, false, true];
394        for num_inputs in 1..8 {
395            let mut client = Box::<ClientMock>::default();
396            client.set_next_response(Ok(Ok(Response::ReadDiscreteInputs(
397                response_inputs.to_vec(),
398            ))));
399            let mut context = Context { client };
400            context.set_slave(Slave(1));
401            let inputs = futures::executor::block_on(context.read_discrete_inputs(1, num_inputs))
402                .unwrap()
403                .unwrap();
404            assert_eq!(&response_inputs[0..num_inputs as usize], &inputs[..]);
405        }
406    }
407}