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
84// ── Configurable-pipeline constructors ───────────────────────────────────────
85
86impl<const N: usize> AsyncTcpClient<N> {
87    /// Deprecated constructor alias.
88    ///
89    /// Use [`AsyncTcpClient::new_with_pipeline`] and then call
90    /// `client.connect().await?`.
91    #[cfg(feature = "tcp")]
92    #[deprecated(
93        note = "use AsyncTcpClient::new_with_pipeline(...) and then client.connect().await"
94    )]
95    pub fn connect_with_pipeline(host: &str, port: u16) -> Result<Self, AsyncError> {
96        Self::new_with_pipeline(host, port)
97    }
98
99    /// Deprecated constructor alias.
100    ///
101    /// Use [`AsyncTcpClient::new_with_pipeline_and_poll_interval`] and then call
102    /// `client.connect().await?`.
103    #[cfg(feature = "tcp")]
104    #[deprecated(
105        note = "use AsyncTcpClient::new_with_pipeline_and_poll_interval(...) and then client.connect().await"
106    )]
107    pub fn connect_with_pipeline_and_poll_interval(
108        host: &str,
109        port: u16,
110        poll_interval: Duration,
111    ) -> Result<Self, AsyncError> {
112        Self::new_with_pipeline_and_poll_interval(host, port, poll_interval)
113    }
114
115    /// Creates an async TCP client with compile-time pipeline depth `N`.
116    ///
117    /// Uses a 20 ms polling interval. Call [`AsyncClientCore::connect`] on the
118    /// returned client before sending requests.
119    #[cfg(feature = "tcp")]
120    pub fn new_with_pipeline(host: &str, port: u16) -> Result<Self, AsyncError> {
121        let transport = StdTcpTransport::new();
122        let config = ModbusConfig::Tcp(ModbusTcpConfig::new(host, port)?);
123        Self::from_transport_config(transport, config, Duration::from_millis(20))
124    }
125
126    /// Creates an async TCP client with compile-time pipeline depth `N` and a
127    /// custom `poll_interval`.
128    ///
129    /// Call [`AsyncClientCore::connect`] on the returned client before sending
130    /// requests.
131    #[cfg(feature = "tcp")]
132    pub fn new_with_pipeline_and_poll_interval(
133        host: &str,
134        port: u16,
135        poll_interval: Duration,
136    ) -> Result<Self, AsyncError> {
137        let transport = StdTcpTransport::new();
138        let config = ModbusConfig::Tcp(ModbusTcpConfig::new(host, port)?);
139        Self::from_transport_config(transport, config, poll_interval)
140    }
141
142    /// Internal constructor: wires `transport` + `config` into a
143    /// `ClientServices` instance, spawns the worker thread, and wraps the
144    /// resulting channel in an [`AsyncClientCore`].
145    #[cfg(feature = "tcp")]
146    fn from_transport_config(
147        transport: StdTcpTransport,
148        config: ModbusConfig,
149        poll_interval: Duration,
150    ) -> Result<Self, AsyncError> {
151        let pending = Arc::new(Mutex::new(HashMap::new()));
152        #[cfg(feature = "traffic")]
153        let traffic_handler = Arc::new(Mutex::new(None));
154        #[cfg(feature = "traffic")]
155        let (traffic_sender, traffic_receiver) = mpsc::channel();
156        let app = AsyncApp {
157            pending: pending.clone(),
158            #[cfg(feature = "traffic")]
159            traffic_sender,
160        };
161
162        let client = ClientServices::<_, _, N>::new(transport, app, config)?;
163        let (sender, receiver) = mpsc::channel();
164
165        thread::spawn(move || run_worker(client, pending, receiver, poll_interval));
166        #[cfg(feature = "traffic")]
167        {
168            let dispatcher_handler = traffic_handler.clone();
169            thread::spawn(move || run_traffic_dispatcher(traffic_receiver, dispatcher_handler));
170        }
171
172        #[cfg(feature = "traffic")]
173        {
174            return Ok(Self {
175                core: AsyncClientCore::new(sender, traffic_handler),
176            });
177        }
178
179        #[cfg(not(feature = "traffic"))]
180        {
181            Ok(Self {
182                core: AsyncClientCore::new(sender),
183            })
184        }
185    }
186}