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 std::ops::Deref;
14use super::*;
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	/// Creates an async Modbus RTU serial client.
37	///
38	/// Validates that `serial_config.mode` is [`SerialMode::Rtu`].  Uses a
39	/// 20 ms polling interval.
40	#[cfg(feature = "serial-rtu")]
41	pub fn connect_rtu(serial_config: ModbusSerialConfig) -> Result<Self, AsyncError> {
42		if serial_config.mode != SerialMode::Rtu {
43			return Err(AsyncError::Mbus(MbusError::InvalidConfiguration));
44		}
45
46		let transport = StdSerialTransport::new(SerialMode::Rtu);
47		let config = ModbusConfig::Serial(serial_config);
48		Self::from_transport_config(transport, config, Duration::from_millis(20))
49	}
50
51	/// Creates an async Modbus RTU serial client with a custom `poll_interval`.
52	///
53	/// Validates that `serial_config.mode` is [`SerialMode::Rtu`].
54	#[cfg(feature = "serial-rtu")]
55	pub fn connect_rtu_with_poll_interval(
56		serial_config: ModbusSerialConfig,
57		poll_interval: Duration,
58	) -> Result<Self, AsyncError> {
59		if serial_config.mode != SerialMode::Rtu {
60			return Err(AsyncError::Mbus(MbusError::InvalidConfiguration));
61		}
62
63		let transport = StdSerialTransport::new(SerialMode::Rtu);
64		let config = ModbusConfig::Serial(serial_config);
65		Self::from_transport_config(transport, config, poll_interval)
66	}
67
68	/// Creates an async Modbus ASCII serial client.
69	///
70	/// Validates that `serial_config.mode` is [`SerialMode::Ascii`].  Uses a
71	/// 20 ms polling interval.
72	#[cfg(feature = "serial-ascii")]
73	pub fn connect_ascii(serial_config: ModbusSerialConfig) -> Result<Self, AsyncError> {
74		if serial_config.mode != SerialMode::Ascii {
75			return Err(AsyncError::Mbus(MbusError::InvalidConfiguration));
76		}
77
78		let transport = StdSerialTransport::new(SerialMode::Ascii);
79		let config = ModbusConfig::Serial(serial_config);
80		Self::from_transport_config(transport, config, Duration::from_millis(20))
81	}
82
83	/// Creates an async Modbus ASCII serial client with a custom `poll_interval`.
84	///
85	/// Validates that `serial_config.mode` is [`SerialMode::Ascii`].
86	#[cfg(feature = "serial-ascii")]
87	pub fn connect_ascii_with_poll_interval(
88		serial_config: ModbusSerialConfig,
89		poll_interval: Duration,
90	) -> Result<Self, AsyncError> {
91		if serial_config.mode != SerialMode::Ascii {
92			return Err(AsyncError::Mbus(MbusError::InvalidConfiguration));
93		}
94
95		let transport = StdSerialTransport::new(SerialMode::Ascii);
96		let config = ModbusConfig::Serial(serial_config);
97		Self::from_transport_config(transport, config, poll_interval)
98	}
99
100	/// Creates an async serial client from a caller-provided transport.
101	///
102	/// This is the escape hatch for custom serial drivers and integration tests
103	/// that inject a mock transport.  The `config` must be
104	/// `ModbusConfig::Serial(_)` or the call returns
105	/// `AsyncError::Mbus(MbusError::InvalidTransport)`.
106	pub fn connect_with_transport<TRANSPORT>(
107		transport: TRANSPORT,
108		config: ModbusConfig,
109		poll_interval: Duration,
110	) -> Result<Self, AsyncError>
111	where
112		TRANSPORT: Transport + Send + 'static,
113	{
114		if !matches!(config, ModbusConfig::Serial(_)) {
115			return Err(AsyncError::Mbus(MbusError::InvalidTransport));
116		}
117
118		let pending = Arc::new(Mutex::new(HashMap::new()));
119		let app = AsyncApp {
120			pending: pending.clone(),
121		};
122
123		// Serial is always single-in-flight (pipeline depth 1).
124		let client = ClientServices::<_, _, 1>::new(transport, app, config)?;
125		let (sender, receiver) = mpsc::channel();
126
127		thread::spawn(move || run_worker(client, pending, receiver, poll_interval));
128
129		Ok(Self {
130			core: AsyncClientCore::new(sender),
131		})
132	}
133
134	/// Internal constructor used by the RTU/ASCII helpers.
135	#[cfg(any(feature = "serial-rtu", feature = "serial-ascii"))]
136	fn from_transport_config(
137		transport: StdSerialTransport,
138		config: ModbusConfig,
139		poll_interval: Duration,
140	) -> Result<Self, AsyncError> {
141		Self::connect_with_transport(transport, config, poll_interval)
142	}
143}