1- //! Real serial port adapter for MS/TP using `tokio-serial`.
1+ //! Real serial port adapters for MS/TP using `tokio-serial`.
22//!
3- //! RS-485 is half-duplex — this assumes the hardware (USB-RS485 adapter)
4- //! handles direction switching automatically (which most modern adapters do).
3+ //! Provides three RS-485 direction control modes:
4+ //!
5+ //! - **Auto** (`TokioSerialPort`): Hardware handles direction switching.
6+ //! Works with USB-RS485 adapters (FTDI, CH340, CP2102) that toggle
7+ //! direction automatically.
8+ //!
9+ //! - **Kernel RS-485** (`TokioSerialPort::enable_kernel_rs485`): Uses the
10+ //! Linux `TIOCSRS485` ioctl so the kernel toggles the UART's RTS pin
11+ //! around each transmission. Zero userspace overhead. Requires DE/RE
12+ //! wired to the UART's RTS pin.
13+ //!
14+ //! - **GPIO** (`GpioDirectionPort`): Toggles an arbitrary GPIO pin for
15+ //! DE/RE control via the Linux GPIO character device (`/dev/gpiochipN`).
16+ //! Use this for RS-485 hats (like the Seeed Studio RS-485 Shield) where
17+ //! DE/RE is wired to a GPIO pin rather than the UART's RTS.
518
619use tokio:: io:: { AsyncReadExt , AsyncWriteExt } ;
720use tokio:: sync:: Mutex ;
@@ -13,7 +26,7 @@ use crate::mstp::SerialPort;
1326
1427/// Configuration for a serial port connection.
1528pub struct SerialConfig {
16- /// Serial port device name (e.g., "/dev/ttyUSB0" on Linux, "COM3 " on Windows ).
29+ /// Serial port device name (e.g., "/dev/ttyUSB0" on Linux, "/dev/cu.usbserial-xxx " on macOS ).
1730 pub port_name : String ,
1831 /// Baud rate. Common MS/TP values: 9600, 19200, 38400, 76800.
1932 pub baud_rate : u32 ,
@@ -30,7 +43,10 @@ impl Default for SerialConfig {
3043
3144/// A real serial port implementing the MS/TP [`SerialPort`] trait.
3245///
33- /// Wraps `tokio_serial::SerialStream` for async RS-485 I/O.
46+ /// Wraps `tokio_serial::SerialStream` for async RS-485 I/O. By default,
47+ /// assumes the hardware handles direction switching automatically (USB
48+ /// RS-485 adapters). On Linux, call [`enable_kernel_rs485`](Self::enable_kernel_rs485)
49+ /// to use kernel-managed RTS direction control.
3450pub struct TokioSerialPort {
3551 inner : Mutex < SerialStream > ,
3652}
@@ -45,6 +61,64 @@ impl TokioSerialPort {
4561 inner : Mutex :: new ( stream) ,
4662 } )
4763 }
64+
65+ /// Enable Linux kernel RS-485 mode via `TIOCSRS485` ioctl.
66+ ///
67+ /// The kernel will automatically toggle the UART's RTS pin to control
68+ /// the RS-485 transceiver direction. This is zero-overhead — no
69+ /// userspace GPIO toggling needed. Requires DE/RE wired to the UART's
70+ /// RTS pin (e.g., GPIO17 on Raspberry Pi).
71+ ///
72+ /// # Parameters
73+ /// - `invert_rts`: If true, RTS is LOW during transmission (for
74+ /// transceivers with active-low DE).
75+ /// - `delay_before_send_us`: Microseconds to wait after asserting RTS
76+ /// before transmitting. Covers transceiver enable time.
77+ /// - `delay_after_send_us`: Microseconds to wait after the last byte
78+ /// before deasserting RTS. Covers last-byte drain time.
79+ #[ cfg( target_os = "linux" ) ]
80+ pub fn enable_kernel_rs485 (
81+ & self ,
82+ invert_rts : bool ,
83+ delay_before_send_us : u32 ,
84+ delay_after_send_us : u32 ,
85+ ) -> Result < ( ) , Error > {
86+ use std:: os:: unix:: io:: AsRawFd ;
87+
88+ let stream = self . inner . try_lock ( ) . map_err ( |_| {
89+ Error :: Encoding ( "Cannot enable RS-485: serial port is in use" . to_string ( ) )
90+ } ) ?;
91+
92+ // struct serial_rs485 { u32 flags, u32 delay_rts_before_send, u32 delay_rts_after_send, u32[5] padding }
93+ const SER_RS485_ENABLED : u32 = 1 ;
94+ const SER_RS485_RTS_ON_SEND : u32 = 1 << 1 ;
95+ const SER_RS485_RTS_AFTER_SEND : u32 = 1 << 2 ;
96+
97+ let mut flags = SER_RS485_ENABLED ;
98+ if invert_rts {
99+ flags |= SER_RS485_RTS_AFTER_SEND ;
100+ } else {
101+ flags |= SER_RS485_RTS_ON_SEND ;
102+ }
103+
104+ let mut buf = [ 0u32 ; 8 ] ;
105+ buf[ 0 ] = flags;
106+ buf[ 1 ] = delay_before_send_us;
107+ buf[ 2 ] = delay_after_send_us;
108+
109+ // TIOCSRS485 = 0x542F
110+ let ret =
111+ unsafe { libc:: ioctl ( stream. as_raw_fd ( ) , 0x542F , buf. as_mut_ptr ( ) ) } ;
112+ if ret < 0 {
113+ return Err ( Error :: Encoding ( format ! (
114+ "TIOCSRS485 ioctl failed: {}" ,
115+ std:: io:: Error :: last_os_error( )
116+ ) ) ) ;
117+ }
118+
119+ tracing:: info!( "Kernel RS-485 mode enabled (invert_rts={invert_rts})" ) ;
120+ Ok ( ( ) )
121+ }
48122}
49123
50124impl SerialPort for TokioSerialPort {
@@ -64,3 +138,164 @@ impl SerialPort for TokioSerialPort {
64138 . map_err ( |e| Error :: Encoding ( format ! ( "Serial read failed: {e}" ) ) )
65139 }
66140}
141+
142+ // ---------------------------------------------------------------------------
143+ // GPIO direction control wrapper
144+ // ---------------------------------------------------------------------------
145+
146+ /// RS-485 direction control via a GPIO pin on the Linux GPIO character device.
147+ ///
148+ /// Wraps any [`SerialPort`] implementation and toggles a GPIO pin for
149+ /// DE/RE (Driver Enable / Receiver Enable) control around each write.
150+ ///
151+ /// # Usage
152+ ///
153+ /// ```no_run
154+ /// use bacnet_transport::mstp_serial::{GpioDirectionPort, TokioSerialPort, SerialConfig};
155+ ///
156+ /// let serial = TokioSerialPort::open(&SerialConfig {
157+ /// port_name: "/dev/ttyS0".into(),
158+ /// baud_rate: 76800,
159+ /// }).unwrap();
160+ ///
161+ /// // Seeed Studio RS-485 Shield: GPIO18 on /dev/gpiochip0, active-high
162+ /// let port = GpioDirectionPort::new(serial, "/dev/gpiochip0", 18, true).unwrap();
163+ /// ```
164+ ///
165+ /// The pin is set to receive mode (DE deasserted) on creation and after
166+ /// each write. This ensures the bus defaults to listening.
167+ #[ cfg( feature = "serial-gpio" ) ]
168+ pub struct GpioDirectionPort < S : SerialPort > {
169+ inner : S ,
170+ gpio : std:: sync:: Mutex < gpiocdev:: Request > ,
171+ line : u32 ,
172+ active_high : bool ,
173+ /// Baud-rate-dependent delay after flush to ensure the last byte
174+ /// has left the UART shift register before switching to RX mode.
175+ post_tx_delay_us : u64 ,
176+ }
177+
178+ #[ cfg( feature = "serial-gpio" ) ]
179+ impl < S : SerialPort > GpioDirectionPort < S > {
180+ /// Create a new GPIO direction-controlled serial port.
181+ ///
182+ /// # Parameters
183+ /// - `inner`: The underlying serial port for data I/O.
184+ /// - `gpio_chip`: Path to the GPIO chip device (e.g., "/dev/gpiochip0").
185+ /// - `line`: GPIO line number for DE/RE control (e.g., 18).
186+ /// - `active_high`: If true, GPIO HIGH enables the transmitter (most
187+ /// common — MAX485 DE pin is active-high). If false, GPIO LOW enables TX.
188+ pub fn new (
189+ inner : S ,
190+ gpio_chip : & str ,
191+ line : u32 ,
192+ active_high : bool ,
193+ ) -> Result < Self , Error > {
194+ Self :: with_post_tx_delay ( inner, gpio_chip, line, active_high, 0 )
195+ }
196+
197+ /// Create with an explicit post-TX delay in microseconds.
198+ ///
199+ /// After flushing the serial port, the wrapper waits this long before
200+ /// switching back to receive mode. This covers the time for the last
201+ /// byte to leave the UART's shift register. At 76800 baud, one byte
202+ /// takes ~130us. A delay of 200-500us is typically safe.
203+ ///
204+ /// If set to 0, no additional delay is added (suitable when the UART
205+ /// driver's flush fully drains the hardware FIFO).
206+ pub fn with_post_tx_delay (
207+ inner : S ,
208+ gpio_chip : & str ,
209+ line : u32 ,
210+ active_high : bool ,
211+ post_tx_delay_us : u64 ,
212+ ) -> Result < Self , Error > {
213+ use gpiocdev:: line:: Value ;
214+
215+ // Start in RX mode (DE deasserted).
216+ let rx_value = if active_high {
217+ Value :: Inactive
218+ } else {
219+ Value :: Active
220+ } ;
221+
222+ let request = gpiocdev:: Request :: builder ( )
223+ . on_chip ( gpio_chip)
224+ . with_line ( line)
225+ . as_output ( rx_value)
226+ . with_consumer ( "bacnet-mstp" )
227+ . request ( )
228+ . map_err ( |e| {
229+ Error :: Encoding ( format ! (
230+ "GPIO request failed for {gpio_chip} line {line}: {e}"
231+ ) )
232+ } ) ?;
233+
234+ tracing:: info!(
235+ "GPIO direction control: {gpio_chip} line {line} (active_high={active_high})"
236+ ) ;
237+
238+ Ok ( Self {
239+ inner,
240+ gpio : std:: sync:: Mutex :: new ( request) ,
241+ line,
242+ active_high,
243+ post_tx_delay_us,
244+ } )
245+ }
246+
247+ /// Set the transceiver to transmit mode (DE asserted).
248+ fn set_tx_mode ( & self ) -> Result < ( ) , Error > {
249+ use gpiocdev:: line:: Value ;
250+ let value = if self . active_high {
251+ Value :: Active
252+ } else {
253+ Value :: Inactive
254+ } ;
255+ self . gpio
256+ . lock ( )
257+ . unwrap_or_else ( |e| e. into_inner ( ) )
258+ . set_value ( self . line , value)
259+ . map_err ( |e| Error :: Encoding ( format ! ( "GPIO set TX mode failed: {e}" ) ) )
260+ }
261+
262+ /// Set the transceiver to receive mode (DE deasserted).
263+ fn set_rx_mode ( & self ) -> Result < ( ) , Error > {
264+ use gpiocdev:: line:: Value ;
265+ let value = if self . active_high {
266+ Value :: Inactive
267+ } else {
268+ Value :: Active
269+ } ;
270+ self . gpio
271+ . lock ( )
272+ . unwrap_or_else ( |e| e. into_inner ( ) )
273+ . set_value ( self . line , value)
274+ . map_err ( |e| Error :: Encoding ( format ! ( "GPIO set RX mode failed: {e}" ) ) )
275+ }
276+ }
277+
278+ #[ cfg( feature = "serial-gpio" ) ]
279+ impl < S : SerialPort > SerialPort for GpioDirectionPort < S > {
280+ async fn write ( & self , data : & [ u8 ] ) -> Result < ( ) , Error > {
281+ // Switch to TX mode before writing.
282+ self . set_tx_mode ( ) ?;
283+
284+ let result = self . inner . write ( data) . await ;
285+
286+ // Post-TX delay to let the last byte leave the shift register.
287+ if self . post_tx_delay_us > 0 {
288+ tokio:: time:: sleep ( tokio:: time:: Duration :: from_micros ( self . post_tx_delay_us ) ) . await ;
289+ }
290+
291+ // Always switch back to RX mode, even on write error.
292+ self . set_rx_mode ( ) ?;
293+
294+ result
295+ }
296+
297+ async fn read ( & self , buf : & mut [ u8 ] ) -> Result < usize , Error > {
298+ // Already in RX mode — just delegate.
299+ self . inner . read ( buf) . await
300+ }
301+ }
0 commit comments