Skip to main content

oms_modbus/
client.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Modbus client (master) trait.
3
4use async_trait::async_trait;
5
6pub use crate::error::ModbusError;
7use crate::frame::{Request, Response};
8
9/// Convert a response that didn't match the expected variant into the
10/// appropriate error: `Exception` for exception responses, `Protocol`
11/// for unexpected success variants.
12#[inline]
13fn unexpected_response(rsp: Response) -> ModbusError {
14    match rsp {
15        Response::Exception(fc, ex) => ModbusError::exception(fc, u8::from(ex)),
16        other => ModbusError::protocol(format!("unexpected response: {other:?}")),
17    }
18}
19
20/// Transport-independent Modbus client interface.
21///
22/// Each transport implements [`call`](ModbusClient::call) — the only required
23/// method. All specific methods (`read_coils`, `write_single_register`, etc.)
24/// have default implementations that delegate to `call`.
25///
26/// `slave` is a per-request parameter — a single client instance handles
27/// the whole RS-485 bus, addressing different slaves on each call.
28///
29/// # Examples
30///
31/// ```no_run
32/// use oms_modbus::*;
33/// use std::time::Duration;
34///
35/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
36/// // TCP
37/// let client = tcp::TcpClient::connect_with_timeout(
38///     "192.168.1.10:502".parse()?, Duration::from_secs(3),
39/// ).await?;
40///
41/// // RTU serial
42/// # let port = tokio::io::duplex(64).0; // placeholder
43/// let client = rtu::RtuClient::with_timeout(port, Duration::from_secs(3));
44///
45/// // All function codes work the same way
46/// let regs = client.read_holding_registers(1, 0, 5).await?;
47/// client.write_single_register(1, 0, 42).await?;
48///
49/// // Share via Arc
50/// let shared: std::sync::Arc<dyn ModbusClient> = std::sync::Arc::new(client);
51/// # Ok(())
52/// # }
53/// ```
54#[async_trait]
55pub trait ModbusClient: Send + Sync {
56    /// Send a raw Modbus request to the given slave and return the response.
57    /// Transports must implement this; all other methods delegate to it.
58    async fn call(&self, slave: u8, request: Request<'_>) -> Result<Response, ModbusError>;
59
60    // ── Default implementations (delegate to `call`) ──────────────────────
61
62    /// Read coils (FC01) — returns the ON/OFF status of discrete outputs.
63    async fn read_coils(
64        &self,
65        slave: u8,
66        addr: u16,
67        quantity: u16,
68    ) -> Result<Vec<bool>, ModbusError> {
69        match self.call(slave, Request::ReadCoils(addr, quantity)).await? {
70            Response::ReadCoils(bits) => Ok(bits),
71            other => Err(unexpected_response(other)),
72        }
73    }
74    /// Read discrete inputs (FC02) — returns the ON/OFF status of digital inputs.
75    async fn read_discrete_inputs(
76        &self,
77        slave: u8,
78        addr: u16,
79        quantity: u16,
80    ) -> Result<Vec<bool>, ModbusError> {
81        match self
82            .call(slave, Request::ReadDiscreteInputs(addr, quantity))
83            .await?
84        {
85            Response::ReadDiscreteInputs(bits) => Ok(bits),
86            other => Err(unexpected_response(other)),
87        }
88    }
89    /// Read holding registers (FC03) — returns the contents of read/write registers.
90    async fn read_holding_registers(
91        &self,
92        slave: u8,
93        addr: u16,
94        quantity: u16,
95    ) -> Result<Vec<u16>, ModbusError> {
96        match self
97            .call(slave, Request::ReadHoldingRegisters(addr, quantity))
98            .await?
99        {
100            Response::ReadHoldingRegisters(regs) => Ok(regs),
101            other => Err(unexpected_response(other)),
102        }
103    }
104    /// Read input registers (FC04) — returns the contents of read-only registers.
105    async fn read_input_registers(
106        &self,
107        slave: u8,
108        addr: u16,
109        quantity: u16,
110    ) -> Result<Vec<u16>, ModbusError> {
111        match self
112            .call(slave, Request::ReadInputRegisters(addr, quantity))
113            .await?
114        {
115            Response::ReadInputRegisters(regs) => Ok(regs),
116            other => Err(unexpected_response(other)),
117        }
118    }
119    /// Write a single coil (FC05) — set a discrete output ON (`true`) or OFF (`false`).
120    async fn write_single_coil(
121        &self,
122        slave: u8,
123        addr: u16,
124        value: bool,
125    ) -> Result<(), ModbusError> {
126        match self
127            .call(slave, Request::WriteSingleCoil(addr, value))
128            .await?
129        {
130            Response::WriteSingleCoil(a, v) if a == addr && v == value => Ok(()),
131            other => Err(unexpected_response(other)),
132        }
133    }
134    /// Write a single holding register (FC06).
135    async fn write_single_register(
136        &self,
137        slave: u8,
138        addr: u16,
139        value: u16,
140    ) -> Result<(), ModbusError> {
141        match self
142            .call(slave, Request::WriteSingleRegister(addr, value))
143            .await?
144        {
145            Response::WriteSingleRegister(a, v) if a == addr && v == value => Ok(()),
146            other => Err(unexpected_response(other)),
147        }
148    }
149    /// Write multiple coils (FC15).
150    async fn write_multiple_coils(
151        &self,
152        slave: u8,
153        addr: u16,
154        values: &[bool],
155    ) -> Result<(), ModbusError> {
156        let req = Request::WriteMultipleCoils(addr, std::borrow::Cow::Borrowed(values));
157        match self.call(slave, req).await? {
158            Response::WriteMultipleCoils(a, n) if a == addr && n as usize == values.len() => Ok(()),
159            other => Err(unexpected_response(other)),
160        }
161    }
162    /// Write multiple holding registers (FC16).
163    async fn write_multiple_registers(
164        &self,
165        slave: u8,
166        addr: u16,
167        values: &[u16],
168    ) -> Result<(), ModbusError> {
169        let req = Request::WriteMultipleRegisters(addr, std::borrow::Cow::Borrowed(values));
170        match self.call(slave, req).await? {
171            Response::WriteMultipleRegisters(a, n) if a == addr && n as usize == values.len() => {
172                Ok(())
173            }
174            other => Err(unexpected_response(other)),
175        }
176    }
177
178    /// Write a single register using a mask (FC 22).
179    async fn mask_write_register(
180        &self,
181        slave: u8,
182        addr: u16,
183        and_mask: u16,
184        or_mask: u16,
185    ) -> Result<(), ModbusError> {
186        match self
187            .call(slave, Request::MaskWriteRegister(addr, and_mask, or_mask))
188            .await?
189        {
190            Response::MaskWriteRegister(a, and, or)
191                if a == addr && and == and_mask && or == or_mask =>
192            {
193                Ok(())
194            }
195            other => Err(unexpected_response(other)),
196        }
197    }
198
199    /// Send a Diagnostic request (FC 08).
200    async fn diagnostic(
201        &self,
202        slave: u8,
203        sub_function: u16,
204        data: u16,
205    ) -> Result<(u16, u16), ModbusError> {
206        match self
207            .call(slave, Request::Diagnostic(sub_function, data))
208            .await?
209        {
210            Response::Diagnostic(sf, d) => Ok((sf, d)),
211            other => Err(unexpected_response(other)),
212        }
213    }
214
215    /// Read and write multiple registers in a single transaction (FC 23).
216    async fn read_write_multiple_registers(
217        &self,
218        slave: u8,
219        read_addr: u16,
220        read_qty: u16,
221        write_addr: u16,
222        values: &[u16],
223    ) -> Result<Vec<u16>, ModbusError> {
224        let req = Request::ReadWriteMultipleRegisters(
225            read_addr,
226            read_qty,
227            write_addr,
228            std::borrow::Cow::Borrowed(values),
229        );
230        match self.call(slave, req).await? {
231            Response::ReadWriteMultipleRegisters(regs) => Ok(regs),
232            other => Err(unexpected_response(other)),
233        }
234    }
235}