Skip to content

Commit 7a2382f

Browse files
committed
Doc updates.
1 parent 09e9811 commit 7a2382f

File tree

2 files changed

+136
-1
lines changed

2 files changed

+136
-1
lines changed

docs/architecture.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,27 @@ MAC address format varies by transport:
106106

107107
`AnyTransport<S>` is a type-erased enum wrapping all transport types, enabling mixed-transport routing (e.g., BIP + MS/TP + Loopback on the same router).
108108

109+
### RS-485 Direction Control (MS/TP)
110+
111+
RS-485 is half-duplex — the transceiver's DE/RE pin must be toggled between transmit and receive. The stack supports three modes:
112+
113+
```
114+
┌──────────────────────────┐
115+
USB RS-485 Adapter ──────────>│ TokioSerialPort │ Auto-direction
116+
(FTDI, CH340, etc.) │ (no config needed) │ (hardware handles DE/RE)
117+
└──────────────────────────┘
118+
119+
UART + RTS → DE/RE ──────────>│ TokioSerialPort │ Kernel RS-485
120+
(DE wired to UART RTS pin) │ .enable_kernel_rs485() │ (TIOCSRS485 ioctl)
121+
└──────────────────────────┘
122+
123+
UART + GPIO → DE/RE ─────────>│ GpioDirectionPort<S> │ GPIO direction
124+
(Pi hat, GPIO pin for DE) │ wraps any SerialPort │ (gpiocdev, serial-gpio feature)
125+
└──────────────────────────┘
126+
```
127+
128+
`GpioDirectionPort` is a composable wrapper — it wraps any `SerialPort` and toggles a GPIO pin via the Linux character device API (`/dev/gpiochipN`) before and after each write. This keeps `TokioSerialPort` simple and platform-independent.
129+
109130
## Object Model
110131

111132
Every BACnet object implements the `BACnetObject` trait:

docs/rust-api.md

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,8 @@ Transport-layer implementations. All implement the `TransportPort` trait.
249249
| (default) | all | BIP (UDP/IPv4) |
250250
| `ipv6` | all | BIP6 (UDP/IPv6 multicast) |
251251
| `sc-tls` | all | BACnet/SC (WebSocket + TLS) + SC Hub |
252-
| `serial` | Linux | MS/TP (serial token-passing) |
252+
| `serial` | all | MS/TP (serial token-passing via `tokio-serial`) |
253+
| `serial-gpio` | Linux | MS/TP + GPIO direction control (adds `gpiocdev`) |
253254
| `ethernet` | Linux | BACnet Ethernet (BPF) |
254255

255256
### BIP (IPv4)
@@ -301,6 +302,82 @@ let addr = hub.start().await?; // Returns SocketAddr
301302

302303
The SC hub is a TLS WebSocket relay. Both clients and servers connect to it as spoke nodes. Messages are routed by VMAC address.
303304

305+
### MS/TP (Serial RS-485)
306+
307+
MS/TP is a token-passing protocol over RS-485 serial, commonly used for field-level BACnet devices. The serial I/O is abstracted behind the `SerialPort` trait, with three RS-485 direction control modes.
308+
309+
#### Auto-Direction (USB RS-485 Adapters)
310+
311+
Most USB RS-485 adapters (FTDI, CH340, CP2102) handle direction switching in hardware — no configuration needed.
312+
313+
```rust
314+
use bacnet_transport::mstp_serial::{TokioSerialPort, SerialConfig};
315+
316+
let serial = TokioSerialPort::open(&SerialConfig {
317+
port_name: "/dev/ttyUSB0".into(), // Linux
318+
// port_name: "/dev/cu.usbserial-xxx".into(), // macOS
319+
baud_rate: 76800,
320+
})?;
321+
322+
// Use with BACnetClient or BACnetServer via generic_builder
323+
let client = BACnetClient::generic_builder()
324+
.transport(MstpTransport::new(serial, 1, 127)) // station 1, max_master 127
325+
.build()
326+
.await?;
327+
```
328+
329+
#### Kernel RS-485 Mode (Linux, RTS-based)
330+
331+
When DE/RE is wired to the UART's RTS pin, the Linux kernel can toggle it automatically via the `TIOCSRS485` ioctl. Zero userspace overhead.
332+
333+
```rust
334+
let serial = TokioSerialPort::open(&config)?;
335+
serial.enable_kernel_rs485(
336+
false, // invert_rts: false = RTS HIGH during TX
337+
0, // delay_before_send_us
338+
0, // delay_after_send_us
339+
)?;
340+
```
341+
342+
#### GPIO Direction Control (RS-485 Hats)
343+
344+
For RS-485 hats where DE/RE is wired to a GPIO pin (e.g., Seeed Studio RS-485 Shield on Raspberry Pi with GPIO18), use `GpioDirectionPort` to wrap any `SerialPort`. Requires the `serial-gpio` feature.
345+
346+
```rust
347+
use bacnet_transport::mstp_serial::{GpioDirectionPort, TokioSerialPort, SerialConfig};
348+
349+
let serial = TokioSerialPort::open(&SerialConfig {
350+
port_name: "/dev/ttyS0".into(),
351+
baud_rate: 76800,
352+
})?;
353+
354+
// Wrap with GPIO direction control: gpiochip0, line 18, active-high
355+
let port = GpioDirectionPort::new(serial, "/dev/gpiochip0", 18, true)?;
356+
357+
// Or with explicit post-TX delay (microseconds before switching to RX):
358+
let port = GpioDirectionPort::with_post_tx_delay(
359+
serial, "/dev/gpiochip0", 18, true, 200,
360+
)?;
361+
```
362+
363+
The `GpioDirectionPort` wrapper:
364+
- Sets GPIO to receive mode (DE deasserted) on creation
365+
- Switches to TX mode before each `write()`
366+
- Waits for optional post-TX delay after write completes
367+
- Switches back to RX mode after each `write()`
368+
- Uses the Linux GPIO character device (`/dev/gpiochipN`) via `gpiocdev` — not deprecated sysfs
369+
370+
#### SerialPort Trait
371+
372+
The MS/TP state machine is hardware-agnostic. Custom serial implementations (e.g., for testing) can implement:
373+
374+
```rust
375+
pub trait SerialPort: Send + Sync + 'static {
376+
fn write(&self, data: &[u8]) -> impl Future<Output = Result<(), Error>> + Send;
377+
fn read(&self, buf: &mut [u8]) -> impl Future<Output = Result<usize, Error>> + Send;
378+
}
379+
```
380+
304381
### Loopback Transport
305382

306383
```rust
@@ -856,3 +933,40 @@ let client = BACnetClient::sc_builder()
856933
.build()
857934
.await?;
858935
```
936+
937+
### MS/TP with USB Adapter
938+
939+
```rust
940+
use bacnet_transport::mstp::MstpTransport;
941+
use bacnet_transport::mstp_serial::{TokioSerialPort, SerialConfig};
942+
943+
let serial = TokioSerialPort::open(&SerialConfig {
944+
port_name: "/dev/ttyUSB0".into(),
945+
baud_rate: 76800,
946+
})?;
947+
948+
let client = BACnetClient::generic_builder()
949+
.transport(MstpTransport::new(serial, 1, 127))
950+
.build()
951+
.await?;
952+
```
953+
954+
### MS/TP with Raspberry Pi RS-485 Hat (GPIO)
955+
956+
```rust
957+
use bacnet_transport::mstp::MstpTransport;
958+
use bacnet_transport::mstp_serial::{GpioDirectionPort, TokioSerialPort, SerialConfig};
959+
960+
let serial = TokioSerialPort::open(&SerialConfig {
961+
port_name: "/dev/ttyS0".into(),
962+
baud_rate: 76800,
963+
})?;
964+
965+
// Seeed Studio RS-485 Shield: GPIO18 for DE/RE, active-high
966+
let port = GpioDirectionPort::new(serial, "/dev/gpiochip0", 18, true)?;
967+
968+
let client = BACnetClient::generic_builder()
969+
.transport(MstpTransport::new(port, 1, 127))
970+
.build()
971+
.await?;
972+
```

0 commit comments

Comments
 (0)