oms_modbus/bus_timing.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2//!
3//! Bus frame spacing — enforces minimum silence between Modbus frames.
4//
5// Critical for RS-485 multi-drop buses where sending too fast can crash
6// low-performance slave devices. RTU/ASCII spec requires ≥3.5 character
7// times of silence between frames. Users may also configure custom values
8// (50ms, 100ms, etc.) for particularly slow devices.
9
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::time::Duration;
12
13/// Tracks bus activity and enforces minimum frame spacing.
14///
15/// **Thread-safe**: uses `AtomicU64` for lock-free timestamp updates.
16/// Pass to [`crate::ClientOptions::with_bus_timing`] to activate.
17///
18/// # Frame spacing
19///
20/// 1. Every bus I/O event (read/write) calls [`touch`](BusTiming::touch)
21/// to record the last activity timestamp.
22/// 2. Before sending, [`wait_if_needed`](BusTiming::wait_if_needed) checks
23/// if enough time has elapsed. If not, it sleeps for the remaining gap.
24///
25/// # Examples
26///
27/// ```rust
28/// use std::time::Duration;
29/// use oms_modbus::BusTiming;
30///
31/// // RTU 3.5T @ 9600 baud ≈ 4.01ms
32/// let timing = BusTiming::rtu_35t(9600);
33///
34/// // Custom 100ms — for slow devices
35/// let timing = BusTiming::custom(Duration::from_millis(100));
36/// ```
37#[derive(Debug)]
38pub struct BusTiming {
39 /// Microsecond timestamp of the last bus event.
40 last_event_us: AtomicU64,
41 /// Minimum spacing between frames, in microseconds.
42 min_spacing_us: u64,
43}
44
45impl BusTiming {
46 /// Create with a custom minimum spacing.
47 ///
48 /// ```rust
49 /// use std::time::Duration;
50 /// use oms_modbus::BusTiming;
51 /// let timing = BusTiming::custom(Duration::from_millis(50));
52 /// ```
53 pub fn custom(min_spacing: Duration) -> Self {
54 let min_spacing_us = min_spacing.as_micros() as u64;
55 Self {
56 last_event_us: AtomicU64::new(0),
57 min_spacing_us,
58 }
59 }
60
61 /// RTU standard 3.5 character times at the given baud rate.
62 ///
63 /// Per *MODBUS over Serial Line Specification V1.02* §1.4:
64 /// - **≤ 19200 bps**: `3.5 × 11 bits / baud_rate` (the full formula).
65 /// - **> 19200 bps**: fixed minimum of **1.75 ms (1750 µs)**.
66 ///
67 /// Without the cap, 115200 baud would give only ~334 µs — too short for
68 /// real RS-485 transceivers and low-end MCU interrupt latency. Use
69 /// [`rtu_35t_raw`](Self::rtu_35t_raw) if you need the uncapped formula.
70 ///
71 /// | Baud | 3.5T spacing |
72 /// |--------|-------------|
73 /// | 9600 | ~4.01 ms |
74 /// | 19200 | ~2.01 ms |
75 /// | 38400 | 1.75 ms |
76 /// | 115200 | 1.75 ms |
77 /// | 921600 | 1.75 ms |
78 pub fn rtu_35t(baud_rate: u32) -> Self {
79 let min_spacing_us = if baud_rate > 19200 {
80 1750 // spec-mandated fixed interval for high baud rates
81 } else {
82 (3_500_000u64 * 11) / baud_rate.max(1) as u64
83 };
84 Self {
85 last_event_us: AtomicU64::new(0),
86 min_spacing_us,
87 }
88 }
89
90 /// Raw 3.5 character-time formula at any baud rate — **no 1.75 ms cap**.
91 ///
92 /// Unlike [`rtu_35t`](Self::rtu_35t), this always computes the raw formula,
93 /// even at high baud rates where the Modbus spec recommends a fixed
94 /// minimum of 1.75 ms. Baud rate 0 is silently clamped to 1.
95 /// Use this only if you understand the implications for your specific hardware.
96 ///
97 /// ```rust
98 /// use oms_modbus::BusTiming;
99 /// // 115200 baud → ~334 µs (raw formula, no cap)
100 /// let timing = BusTiming::rtu_35t_raw(115200);
101 /// assert_eq!(timing.min_spacing().as_micros(), 334);
102 /// ```
103 pub fn rtu_35t_raw(baud_rate: u32) -> Self {
104 let min_spacing_us = (3_500_000u64 * 11) / baud_rate.max(1) as u64;
105 Self {
106 last_event_us: AtomicU64::new(0),
107 min_spacing_us,
108 }
109 }
110
111 /// ASCII standard 3.5 character times — same timing as RTU.
112 pub fn ascii_35t(baud_rate: u32) -> Self {
113 Self::rtu_35t(baud_rate)
114 }
115
116 /// Record bus activity at the current instant.
117 ///
118 /// Call on every I/O read/write to keep the timestamp fresh.
119 /// Uses a monotonic, syscall-free clock for microsecond timestamps.
120 #[inline]
121 pub fn touch(&self) {
122 // Use max(1) so the stored value is never 0, which is reserved
123 // for "no previous activity" in wait_if_needed. Without this,
124 // touch + immediate wait within the same microsecond would hit
125 // the first-call fast path and skip the frame spacing check.
126 let now = now_micros().max(1);
127 self.last_event_us.store(now, Ordering::Relaxed);
128 }
129
130 /// Wait until the minimum spacing has elapsed since the last [`touch`](Self::touch).
131 ///
132 /// Returns immediately (no sleep) if:
133 /// - Enough time has already passed
134 /// - No previous activity was recorded (first call)
135 pub async fn wait_if_needed(&self) {
136 let last = self.last_event_us.load(Ordering::Relaxed);
137 if last == 0 {
138 return; // first call — no history
139 }
140 let now = now_micros();
141 let elapsed = now.saturating_sub(last);
142 if elapsed < self.min_spacing_us {
143 let remaining_us = self.min_spacing_us - elapsed;
144 tokio::time::sleep(Duration::from_micros(remaining_us)).await;
145 }
146 }
147
148 /// Current minimum spacing.
149 pub fn min_spacing(&self) -> Duration {
150 Duration::from_micros(self.min_spacing_us)
151 }
152}
153
154/// Monotonic timestamp in microseconds based on `std::time::Instant`.
155///
156/// Uses `std::time::Instant` (not `tokio::time::Instant`) because bus timing
157/// measures real hardware intervals — it should not be affected by
158/// `tokio::time::pause()` in tests. Also works in non-Tokio contexts
159/// (sync tests, pure-thread environments).
160fn now_micros() -> u64 {
161 static BASE: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
162 let base = BASE.get_or_init(std::time::Instant::now);
163 base.elapsed().as_micros() as u64
164}