Skip to main content

mbus_async/runtime/
network_client.rs

1//! Async Modbus TCP client.
2//!
3//! [`AsyncTcpClient`] is a thin wrapper around [`AsyncClientCore`] that adds
4//! TCP-specific constructors. All Modbus request methods are inherited
5//! transparently through the [`std::ops::Deref`] implementation that resolves
6//! to `AsyncClientCore`.
7
8use super::*;
9use std::ops::Deref;
10
11/// Async Modbus TCP client facade.
12///
13/// All Modbus request methods (`read_holding_registers`, `write_single_coil`,
14/// etc.) are available directly on this type via [`Deref`] to
15/// [`AsyncClientCore`].
16///
17/// The constant generic parameter `N` is the compile-time pipeline depth
18/// forwarded to `ClientServices<_, _, N>` (default `9`).
19pub struct AsyncTcpClient<const N: usize = 9> {
20    core: AsyncClientCore,
21}
22
23impl<const N: usize> Deref for AsyncTcpClient<N> {
24    type Target = AsyncClientCore;
25
26    fn deref(&self) -> &Self::Target {
27        &self.core
28    }
29}
30
31// ── Default-pipeline constructors (N = 9) ───────────────────────────────────
32
33impl AsyncTcpClient<9> {
34    /// Deprecated constructor alias.
35    ///
36    /// Use [`AsyncTcpClient::new`] and then call `client.connect().await?`.
37    #[cfg(feature = "tcp")]
38    #[deprecated(note = "use AsyncTcpClient::new(...) and then client.connect().await")]
39    pub fn connect(host: &str, port: u16) -> Result<Self, AsyncError> {
40        Self::new(host, port)
41    }
42
43    /// Deprecated constructor alias.
44    ///
45    /// Use [`AsyncTcpClient::new_with_poll_interval`] and then call
46    /// `client.connect().await?`.
47    #[cfg(feature = "tcp")]
48    #[deprecated(
49        note = "use AsyncTcpClient::new_with_poll_interval(...) and then client.connect().await"
50    )]
51    pub fn connect_with_poll_interval(
52        host: &str,
53        port: u16,
54        poll_interval: Duration,
55    ) -> Result<Self, AsyncError> {
56        Self::new_with_poll_interval(host, port, poll_interval)
57    }
58
59    /// Creates an async TCP client for `host`:`port` without connecting.
60    ///
61    /// Uses the default pipeline depth of 9 and a 20 ms polling interval. Call
62    /// [`AsyncClientCore::connect`] on the returned client before sending
63    /// requests.
64    #[cfg(feature = "tcp")]
65    pub fn new(host: &str, port: u16) -> Result<Self, AsyncError> {
66        Self::new_with_pipeline(host, port)
67    }
68
69    /// Creates an async TCP client for `host`:`port` with a custom
70    /// `poll_interval`.
71    ///
72    /// Uses the default pipeline depth of 9. Call [`AsyncClientCore::connect`]
73    /// on the returned client before sending requests.
74    #[cfg(feature = "tcp")]
75    pub fn new_with_poll_interval(
76        host: &str,
77        port: u16,
78        poll_interval: Duration,
79    ) -> Result<Self, AsyncError> {
80        Self::new_with_pipeline_and_poll_interval(host, port, poll_interval)
81    }
82
83    /// Creates an async TCP client with a fully custom [`ModbusTcpConfig`] and
84    /// a custom `poll_interval`, using the default pipeline depth of 9.
85    ///
86    /// Use this when you need to override fields such as `response_timeout_ms`,
87    /// `retry_attempts`, or backoff strategy.
88    ///
89    /// Call [`AsyncClientCore::connect`] on the returned client before sending
90    /// requests.
91    #[cfg(feature = "tcp")]
92    pub fn new_with_config(
93        tcp_config: ModbusTcpConfig,
94        poll_interval: Duration,
95    ) -> Result<Self, AsyncError> {
96        let transport = StdTcpTransport::new();
97        Self::from_transport_config(transport, ModbusConfig::Tcp(tcp_config), poll_interval)
98    }
99}
100
101// ── Configurable-pipeline constructors ───────────────────────────────────────
102
103impl<const N: usize> AsyncTcpClient<N> {
104    /// Deprecated constructor alias.
105    ///
106    /// Use [`AsyncTcpClient::new_with_pipeline`] and then call
107    /// `client.connect().await?`.
108    #[cfg(feature = "tcp")]
109    #[deprecated(
110        note = "use AsyncTcpClient::new_with_pipeline(...) and then client.connect().await"
111    )]
112    pub fn connect_with_pipeline(host: &str, port: u16) -> Result<Self, AsyncError> {
113        Self::new_with_pipeline(host, port)
114    }
115
116    /// Deprecated constructor alias.
117    ///
118    /// Use [`AsyncTcpClient::new_with_pipeline_and_poll_interval`] and then call
119    /// `client.connect().await?`.
120    #[cfg(feature = "tcp")]
121    #[deprecated(
122        note = "use AsyncTcpClient::new_with_pipeline_and_poll_interval(...) and then client.connect().await"
123    )]
124    pub fn connect_with_pipeline_and_poll_interval(
125        host: &str,
126        port: u16,
127        poll_interval: Duration,
128    ) -> Result<Self, AsyncError> {
129        Self::new_with_pipeline_and_poll_interval(host, port, poll_interval)
130    }
131
132    /// Creates an async TCP client with compile-time pipeline depth `N`.
133    ///
134    /// Uses a 20 ms polling interval. Call [`AsyncClientCore::connect`] on the
135    /// returned client before sending requests.
136    #[cfg(feature = "tcp")]
137    pub fn new_with_pipeline(host: &str, port: u16) -> Result<Self, AsyncError> {
138        let transport = StdTcpTransport::new();
139        let config = ModbusConfig::Tcp(ModbusTcpConfig::new(host, port)?);
140        Self::from_transport_config(transport, config, Duration::from_millis(20))
141    }
142
143    /// Creates an async TCP client with compile-time pipeline depth `N` and a
144    /// custom `poll_interval`.
145    ///
146    /// Call [`AsyncClientCore::connect`] on the returned client before sending
147    /// requests.
148    #[cfg(feature = "tcp")]
149    pub fn new_with_pipeline_and_poll_interval(
150        host: &str,
151        port: u16,
152        poll_interval: Duration,
153    ) -> Result<Self, AsyncError> {
154        let transport = StdTcpTransport::new();
155        let config = ModbusConfig::Tcp(ModbusTcpConfig::new(host, port)?);
156        Self::from_transport_config(transport, config, poll_interval)
157    }
158
159    /// Creates an async TCP client with a fully custom [`ModbusTcpConfig`] and
160    /// a custom `poll_interval`.
161    ///
162    /// Use this when you need to override fields such as `response_timeout_ms`,
163    /// `retry_attempts`, or backoff strategy beyond what the convenience
164    /// constructors expose.
165    ///
166    /// Call [`AsyncClientCore::connect`] on the returned client before sending
167    /// requests.
168    #[cfg(feature = "tcp")]
169    pub fn new_with_config_and_pipeline(
170        tcp_config: ModbusTcpConfig,
171        poll_interval: Duration,
172    ) -> Result<Self, AsyncError> {
173        let transport = StdTcpTransport::new();
174        Self::from_transport_config(transport, ModbusConfig::Tcp(tcp_config), poll_interval)
175    }
176
177    /// Internal constructor: wires `transport` + `config` into a
178    /// `ClientServices` instance, spawns the worker thread, and wraps the
179    /// resulting channel in an [`AsyncClientCore`].
180    #[cfg(feature = "tcp")]
181    fn from_transport_config(
182        transport: StdTcpTransport,
183        config: ModbusConfig,
184        poll_interval: Duration,
185    ) -> Result<Self, AsyncError> {
186        let pending = Arc::new(Mutex::new(HashMap::new()));
187        #[cfg(feature = "traffic")]
188        let traffic_handler = Arc::new(Mutex::new(None));
189        #[cfg(feature = "traffic")]
190        let (traffic_sender, traffic_receiver) = mpsc::channel();
191        let app = AsyncApp {
192            pending: pending.clone(),
193            #[cfg(feature = "traffic")]
194            traffic_sender,
195        };
196
197        let client = ClientServices::<_, _, N>::new(transport, app, config)?;
198        let (sender, receiver) = mpsc::channel();
199
200        thread::spawn(move || run_worker(client, pending, receiver, poll_interval));
201        #[cfg(feature = "traffic")]
202        {
203            let dispatcher_handler = traffic_handler.clone();
204            thread::spawn(move || run_traffic_dispatcher(traffic_receiver, dispatcher_handler));
205        }
206
207        #[cfg(feature = "traffic")]
208        {
209            Ok(Self {
210                core: AsyncClientCore::new(sender, traffic_handler),
211            })
212        }
213
214        #[cfg(not(feature = "traffic"))]
215        {
216            Ok(Self {
217                core: AsyncClientCore::new(sender),
218            })
219        }
220    }
221}