modbus_bridge/client.rs
1//! [`Client`] — owns the RTU serial port and creates [`ClientSession`]s.
2//!
3//! In client mode the roles of RTU and TCP are reversed relative to [`Bridge`](crate::Bridge):
4//! the serial bus acts as the *request source* (an RTU master talks to this device) and
5//! the TCP stream connects to an *upstream Modbus TCP server*.
6
7use crate::{
8 client_builder::ClientBuilder, client_session::ClientSession, rtu::ModbusRtu, NoDelay,
9};
10use embedded_hal::digital::OutputPin;
11
12/// Modbus RTU→TCP client.
13///
14/// Owns the serial port (`S`) and RS-485 TX-enable pin (`TX`). Connect to an
15/// upstream Modbus TCP server by calling [`connect`](Client::connect) with a TCP stream.
16///
17/// The optional third parameter `D` is a delay provider for I/O timeouts.
18/// It defaults to [`NoDelay`](crate::NoDelay).
19///
20/// # Examples
21///
22/// ```rust,ignore
23/// use modbus_bridge::{Client, BridgeError, BridgeEvent};
24///
25/// let mut client = Client::builder()
26/// .rtu(uart, tx_en_pin)
27/// .build();
28///
29/// // tcp_stream connects to the upstream Modbus TCP server
30/// let mut session = client.connect(tcp_stream);
31/// loop {
32/// match session.next().await {
33/// Ok(BridgeEvent::Transaction(t)) => log::info!("modbus: {t}"),
34/// Ok(BridgeEvent::Warning(w)) => log::warn!("modbus: {w}"),
35/// Err(BridgeError::RtuClosed) => break, // RTU master disconnected
36/// Err(e) => { log::error!("{e}"); break; }
37/// }
38/// }
39/// let tcp_stream = session.into_stream();
40/// ```
41pub struct Client<S, TX, D = NoDelay> {
42 pub(crate) rtu: ModbusRtu<S, TX>,
43 pub(crate) rtu_timeout_ms: Option<u32>,
44 pub(crate) tcp_timeout_ms: Option<u32>,
45 pub(crate) delay: D,
46}
47
48impl<S, TX, D> Client<S, TX, D> {
49 /// Returns a [`ClientBuilder`](crate::ClientBuilder) for constructing a `Client`.
50 pub fn builder() -> ClientBuilder<(), (), NoDelay> {
51 ClientBuilder::new()
52 }
53
54 pub(crate) fn from_parts(
55 serial: S,
56 tx_en: TX,
57 delay: D,
58 rtu_timeout_ms: Option<u32>,
59 tcp_timeout_ms: Option<u32>,
60 ) -> Self {
61 Self {
62 rtu: ModbusRtu::new(serial, tx_en),
63 rtu_timeout_ms,
64 tcp_timeout_ms,
65 delay,
66 }
67 }
68
69 /// Consumes the client and returns the inner serial port, TX-enable pin, and delay provider.
70 pub fn into_inner(self) -> (S, TX, D) {
71 let (s, tx) = self.rtu.into_inner();
72 (s, tx, self.delay)
73 }
74}
75
76#[cfg(feature = "async")]
77impl<S, TX, D> Client<S, TX, D>
78where
79 S: embedded_io_async::Read + embedded_io_async::Write,
80 TX: OutputPin,
81{
82 /// Creates a [`ClientSession`](crate::ClientSession) connected to an upstream TCP server.
83 ///
84 /// Takes ownership of `stream` and mutably borrows the client for the lifetime
85 /// of the returned [`ClientSession`](crate::ClientSession).
86 ///
87 /// # Examples
88 ///
89 /// ```rust,ignore
90 /// let mut session = client.connect(tcp_stream);
91 /// loop {
92 /// match session.next().await {
93 /// Ok(event) => { /* handle event */ }
94 /// Err(_) => break,
95 /// }
96 /// }
97 /// let tcp_stream = session.into_stream();
98 /// ```
99 pub fn connect<TS>(&mut self, stream: TS) -> ClientSession<'_, S, TX, TS, D>
100 where
101 TS: embedded_io_async::Read + embedded_io_async::Write,
102 {
103 ClientSession::new(self, stream)
104 }
105}
106
107#[cfg(feature = "sync")]
108impl<S, TX, D> Client<S, TX, D>
109where
110 S: embedded_io::Read + embedded_io::Write,
111 TX: OutputPin,
112{
113 /// Creates a [`ClientSession`](crate::ClientSession) connected to an upstream TCP server.
114 pub fn connect<TS>(&mut self, stream: TS) -> ClientSession<'_, S, TX, TS, D>
115 where
116 TS: embedded_io::Read + embedded_io::Write,
117 {
118 ClientSession::new(self, stream)
119 }
120}