Skip to main content

mbus_async/runtime/
serial_client.rs

1//! Async Modbus serial client.
2//!
3//! [`AsyncSerialClient`] is a thin wrapper around [`AsyncClientCore`] that adds
4//! serial-specific constructors (RTU, ASCII, and injection of a custom
5//! transport).  All Modbus request methods are inherited transparently through
6//! the [`std::ops::Deref`] implementation that resolves to `AsyncClientCore`.
7//!
8//! # Note on pipeline depth
9//!
10//! Serial Modbus is a strict request-reply protocol, so `ClientServices` is
11//! always built with a pipeline depth of 1 (`ClientServices::<_, _, 1>`).
12
13use super::*;
14use std::ops::Deref;
15
16/// Async Modbus serial client facade.
17///
18/// Supports both RTU and ASCII framing.  All Modbus request methods
19/// (`read_holding_registers`, `write_single_coil`, etc.) are available directly
20/// on this type via [`Deref`] to [`AsyncClientCore`].
21pub struct AsyncSerialClient {
22    core: AsyncClientCore,
23}
24
25impl Deref for AsyncSerialClient {
26    type Target = AsyncClientCore;
27
28    fn deref(&self) -> &Self::Target {
29        &self.core
30    }
31}
32
33// ── Constructors ─────────────────────────────────────────────────────────────────────
34
35impl AsyncSerialClient {
36    /// Deprecated constructor alias.
37    ///
38    /// Use [`AsyncSerialClient::new_rtu`] and then call `client.connect().await?`.
39    #[cfg(feature = "serial-rtu")]
40    #[deprecated(note = "use AsyncSerialClient::new_rtu(...) and then client.connect().await")]
41    pub fn connect_rtu(serial_config: ModbusSerialConfig) -> Result<Self, AsyncError> {
42        Self::new_rtu(serial_config)
43    }
44
45    /// Deprecated constructor alias.
46    ///
47    /// Use [`AsyncSerialClient::new_rtu_with_poll_interval`] and then call
48    /// `client.connect().await?`.
49    #[cfg(feature = "serial-rtu")]
50    #[deprecated(
51        note = "use AsyncSerialClient::new_rtu_with_poll_interval(...) and then client.connect().await"
52    )]
53    pub fn connect_rtu_with_poll_interval(
54        serial_config: ModbusSerialConfig,
55        poll_interval: Duration,
56    ) -> Result<Self, AsyncError> {
57        Self::new_rtu_with_poll_interval(serial_config, poll_interval)
58    }
59
60    /// Deprecated constructor alias.
61    ///
62    /// Use [`AsyncSerialClient::new_ascii`] and then call
63    /// `client.connect().await?`.
64    #[cfg(feature = "serial-ascii")]
65    #[deprecated(note = "use AsyncSerialClient::new_ascii(...) and then client.connect().await")]
66    pub fn connect_ascii(serial_config: ModbusSerialConfig) -> Result<Self, AsyncError> {
67        Self::new_ascii(serial_config)
68    }
69
70    /// Deprecated constructor alias.
71    ///
72    /// Use [`AsyncSerialClient::new_ascii_with_poll_interval`] and then call
73    /// `client.connect().await?`.
74    #[cfg(feature = "serial-ascii")]
75    #[deprecated(
76        note = "use AsyncSerialClient::new_ascii_with_poll_interval(...) and then client.connect().await"
77    )]
78    pub fn connect_ascii_with_poll_interval(
79        serial_config: ModbusSerialConfig,
80        poll_interval: Duration,
81    ) -> Result<Self, AsyncError> {
82        Self::new_ascii_with_poll_interval(serial_config, poll_interval)
83    }
84
85    /// Deprecated constructor alias.
86    ///
87    /// Use [`AsyncSerialClient::new_with_transport`] and then call
88    /// `client.connect().await?`.
89    #[deprecated(
90        note = "use AsyncSerialClient::new_with_transport(...) and then client.connect().await"
91    )]
92    pub fn connect_with_transport<TRANSPORT>(
93        transport: TRANSPORT,
94        config: ModbusConfig,
95        poll_interval: Duration,
96    ) -> Result<Self, AsyncError>
97    where
98        TRANSPORT: Transport + Send + 'static,
99    {
100        Self::new_with_transport(transport, config, poll_interval)
101    }
102
103    /// Creates an async Modbus RTU serial client without connecting.
104    ///
105    /// Validates that `serial_config.mode` is [`SerialMode::Rtu`]. Uses a
106    /// 20 ms polling interval. Call [`AsyncClientCore::connect`] on the returned
107    /// client before sending requests.
108    #[cfg(feature = "serial-rtu")]
109    pub fn new_rtu(serial_config: ModbusSerialConfig) -> Result<Self, AsyncError> {
110        if serial_config.mode != SerialMode::Rtu {
111            return Err(AsyncError::Mbus(MbusError::InvalidConfiguration));
112        }
113
114        let transport = StdRtuTransport::new();
115        let config = ModbusConfig::Serial(serial_config);
116        Self::from_transport_config(transport, config, Duration::from_millis(20))
117    }
118
119    /// Creates an async Modbus RTU serial client with a custom `poll_interval`.
120    ///
121    /// Validates that `serial_config.mode` is [`SerialMode::Rtu`]. Call
122    /// [`AsyncClientCore::connect`] on the returned client before sending
123    /// requests.
124    #[cfg(feature = "serial-rtu")]
125    pub fn new_rtu_with_poll_interval(
126        serial_config: ModbusSerialConfig,
127        poll_interval: Duration,
128    ) -> Result<Self, AsyncError> {
129        if serial_config.mode != SerialMode::Rtu {
130            return Err(AsyncError::Mbus(MbusError::InvalidConfiguration));
131        }
132
133        let transport = StdRtuTransport::new();
134        let config = ModbusConfig::Serial(serial_config);
135        Self::from_transport_config(transport, config, poll_interval)
136    }
137
138    /// Creates an async Modbus ASCII serial client without connecting.
139    ///
140    /// Validates that `serial_config.mode` is [`SerialMode::Ascii`]. Uses a
141    /// 20 ms polling interval. Call [`AsyncClientCore::connect`] on the returned
142    /// client before sending requests.
143    #[cfg(feature = "serial-ascii")]
144    pub fn new_ascii(serial_config: ModbusSerialConfig) -> Result<Self, AsyncError> {
145        if serial_config.mode != SerialMode::Ascii {
146            return Err(AsyncError::Mbus(MbusError::InvalidConfiguration));
147        }
148
149        let transport = StdAsciiTransport::new();
150        let config = ModbusConfig::Serial(serial_config);
151        Self::from_transport_config(transport, config, Duration::from_millis(20))
152    }
153
154    /// Creates an async Modbus ASCII serial client with a custom `poll_interval`.
155    ///
156    /// Validates that `serial_config.mode` is [`SerialMode::Ascii`]. Call
157    /// [`AsyncClientCore::connect`] on the returned client before sending
158    /// requests.
159    #[cfg(feature = "serial-ascii")]
160    pub fn new_ascii_with_poll_interval(
161        serial_config: ModbusSerialConfig,
162        poll_interval: Duration,
163    ) -> Result<Self, AsyncError> {
164        if serial_config.mode != SerialMode::Ascii {
165            return Err(AsyncError::Mbus(MbusError::InvalidConfiguration));
166        }
167
168        let transport = StdAsciiTransport::new();
169        let config = ModbusConfig::Serial(serial_config);
170        Self::from_transport_config(transport, config, poll_interval)
171    }
172
173    /// Creates an async serial client from a caller-provided transport without
174    /// connecting.
175    ///
176    /// This is the escape hatch for custom serial drivers and integration tests
177    /// that inject a mock transport.  The `config` must be
178    /// `ModbusConfig::Serial(_)` or the call returns
179    /// `AsyncError::Mbus(MbusError::InvalidTransport)`. Call
180    /// [`AsyncClientCore::connect`] on the returned client before sending
181    /// requests.
182    pub fn new_with_transport<TRANSPORT>(
183        transport: TRANSPORT,
184        config: ModbusConfig,
185        poll_interval: Duration,
186    ) -> Result<Self, AsyncError>
187    where
188        TRANSPORT: Transport + Send + 'static,
189    {
190        if !matches!(config, ModbusConfig::Serial(_)) {
191            return Err(AsyncError::Mbus(MbusError::InvalidTransport));
192        }
193
194        let pending = Arc::new(Mutex::new(HashMap::new()));
195        #[cfg(feature = "traffic")]
196        let traffic_handler = Arc::new(Mutex::new(None));
197        #[cfg(feature = "traffic")]
198        let (traffic_sender, traffic_receiver) = mpsc::channel();
199        let app = AsyncApp {
200            pending: pending.clone(),
201            #[cfg(feature = "traffic")]
202            traffic_sender,
203        };
204
205        // Serial is always single-in-flight (pipeline depth 1).
206        let client = ClientServices::<_, _, 1>::new(transport, app, config)?;
207        let (sender, receiver) = mpsc::channel();
208
209        thread::spawn(move || run_worker(client, pending, receiver, poll_interval));
210        #[cfg(feature = "traffic")]
211        {
212            let dispatcher_handler = traffic_handler.clone();
213            thread::spawn(move || run_traffic_dispatcher(traffic_receiver, dispatcher_handler));
214        }
215
216        #[cfg(feature = "traffic")]
217        {
218            Ok(Self {
219                core: AsyncClientCore::new(sender, traffic_handler),
220            })
221        }
222
223        #[cfg(not(feature = "traffic"))]
224        {
225            Ok(Self {
226                core: AsyncClientCore::new(sender),
227            })
228        }
229    }
230
231    /// Internal constructor used by the RTU/ASCII helpers.
232    #[cfg(any(feature = "serial-rtu", feature = "serial-ascii"))]
233    fn from_transport_config<const ASCII: bool>(
234        transport: StdSerialTransport<ASCII>,
235        config: ModbusConfig,
236        poll_interval: Duration,
237    ) -> Result<Self, AsyncError> {
238        Self::new_with_transport(transport, config, poll_interval)
239    }
240}