mbus_core/transport/mod.rs
1//! # Modbus Transport Layer
2//!
3//! This module defines the abstractions and configurations required for transmitting
4//! Modbus Application Data Units (ADUs) over various physical and logical mediums.
5//!
6//! ## Core Concepts
7//! - **[`Transport`]**: A unified trait that abstracts the underlying communication
8//! (TCP, Serial, or Mock) from the high-level protocol logic.
9//! - **[`ModbusConfig`]**: A comprehensive configuration enum for setting up
10//! TCP/IP or Serial (RTU/ASCII) parameters.
11//! - **[`BackoffStrategy`]**: Poll-driven retry scheduling strategy used after timeouts.
12//! - **[`JitterStrategy`]**: Optional jitter added on top of retry backoff delays.
13//! - **[`RetryRandomFn`]**: Application-supplied random callback used only when jitter is enabled.
14//! - **[`UnitIdOrSlaveAddr`]**: A type-safe wrapper ensuring that Modbus addresses
15//! stay within the valid range (1-247) and handling broadcast (0) explicitly.
16//!
17//! ## Design Goals
18//! - **`no_std` Compatibility**: Uses `heapless` data structures and `core` traits
19//! to ensure the library can run on bare-metal embedded systems.
20//! - **Non-blocking I/O**: The `Transport::recv` interface is designed to be polled,
21//! allowing the client to remain responsive without requiring an OS-level thread.
22//! - **Scheduled retries**: Retry backoff/jitter values are consumed by higher layers
23//! to schedule retransmissions using timestamps, never by sleeping.
24//! - **Extensibility**: Users can implement the `Transport` trait to support
25//! custom hardware (e.g., specialized UART drivers or proprietary TCP stacks).
26//!
27//! ## Error Handling
28//! Errors are categorized into [`TransportError`], which can be seamlessly converted
29//! into the top-level [`MbusError`] used throughout the crate.
30
31pub mod checksum;
32use core::str::FromStr;
33
34use crate::{data_unit::common::MAX_ADU_FRAME_LEN, errors::MbusError};
35use heapless::{String, Vec};
36
37/// The default TCP port for Modbus communication.
38const MODBUS_TCP_DEFAULT_PORT: u16 = 502;
39
40/// Application-provided callback used to generate randomness for retry jitter.
41///
42/// The callback returns a raw `u32` value that is consumed by jitter logic.
43/// The distribution does not need to be cryptographically secure. A simple
44/// pseudo-random source from the target platform is sufficient.
45pub type RetryRandomFn = fn() -> u32;
46
47/// Retry delay strategy used after a request times out.
48///
49/// The delay is computed per retry attempt in a poll-driven manner. No internal
50/// sleeping or blocking waits are performed by the library.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum BackoffStrategy {
53 /// Retry immediately after timeout detection.
54 Immediate,
55 /// Retry using a constant delay in milliseconds.
56 Fixed {
57 /// Delay applied before each retry.
58 delay_ms: u32,
59 },
60 /// Retry with an exponential sequence: `base_delay_ms * 2^(attempt-1)`.
61 Exponential {
62 /// Base delay for the first retry attempt.
63 base_delay_ms: u32,
64 /// Upper bound used to clamp growth.
65 max_delay_ms: u32,
66 },
67 /// Retry with a linear sequence: `initial_delay_ms + (attempt-1) * increment_ms`.
68 Linear {
69 /// Delay for the first retry attempt.
70 initial_delay_ms: u32,
71 /// Increment added on every subsequent retry.
72 increment_ms: u32,
73 /// Upper bound used to clamp growth.
74 max_delay_ms: u32,
75 },
76}
77
78impl Default for BackoffStrategy {
79 fn default() -> Self {
80 Self::Immediate
81 }
82}
83
84impl BackoffStrategy {
85 /// Computes the base retry delay in milliseconds for a 1-based retry attempt index.
86 ///
87 /// `retry_attempt` is expected to start at `1` for the first retry after the
88 /// initial request timeout.
89 pub fn delay_ms_for_retry(&self, retry_attempt: u8) -> u32 {
90 let attempt = retry_attempt.max(1);
91 match self {
92 BackoffStrategy::Immediate => 0,
93 BackoffStrategy::Fixed { delay_ms } => *delay_ms,
94 BackoffStrategy::Exponential {
95 base_delay_ms,
96 max_delay_ms,
97 } => {
98 let shift = (attempt.saturating_sub(1)).min(31);
99 let factor = 1u32 << shift;
100 base_delay_ms.saturating_mul(factor).min(*max_delay_ms)
101 }
102 BackoffStrategy::Linear {
103 initial_delay_ms,
104 increment_ms,
105 max_delay_ms,
106 } => {
107 let growth = increment_ms.saturating_mul((attempt.saturating_sub(1)) as u32);
108 initial_delay_ms.saturating_add(growth).min(*max_delay_ms)
109 }
110 }
111 }
112}
113
114/// Jitter strategy applied on top of computed backoff delay.
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
116pub enum JitterStrategy {
117 /// Do not apply jitter.
118 #[default]
119 None,
120 /// Apply symmetric percentage jitter around the base delay.
121 ///
122 /// For example, with `percent = 20` and base `100ms`, the final delay is
123 /// in the range `[80ms, 120ms]`.
124 Percentage {
125 /// Maximum percentage variation from the base delay.
126 percent: u8,
127 },
128 /// Apply symmetric bounded jitter in milliseconds around the base delay.
129 ///
130 /// For example, with `max_jitter_ms = 15` and base `100ms`, the final delay is
131 /// in the range `[85ms, 115ms]`.
132 BoundedMs {
133 /// Maximum absolute jitter in milliseconds.
134 max_jitter_ms: u32,
135 },
136}
137
138impl JitterStrategy {
139 /// Applies jitter to `base_delay_ms` using an application-provided random callback.
140 ///
141 /// If jitter is disabled or no callback is provided, this method returns `base_delay_ms`.
142 pub fn apply(self, base_delay_ms: u32, random_fn: Option<RetryRandomFn>) -> u32 {
143 let delta = match self {
144 JitterStrategy::None => return base_delay_ms,
145 JitterStrategy::Percentage { percent } => {
146 if percent == 0 || base_delay_ms == 0 {
147 return base_delay_ms;
148 }
149 base_delay_ms.saturating_mul((percent.min(100)) as u32) / 100
150 }
151 JitterStrategy::BoundedMs { max_jitter_ms } => {
152 if max_jitter_ms == 0 {
153 return base_delay_ms;
154 }
155 max_jitter_ms
156 }
157 };
158
159 let random = match random_fn {
160 Some(cb) => cb(),
161 None => return base_delay_ms,
162 };
163
164 let span = delta.saturating_mul(2).saturating_add(1);
165 if span == 0 {
166 return base_delay_ms;
167 }
168
169 let offset = (random % span) as i64 - delta as i64;
170 let jittered = base_delay_ms as i64 + offset;
171 jittered.max(0) as u32
172 }
173}
174
175/// Top-level configuration for Modbus communication, supporting different transport layers.
176#[derive(Debug)]
177pub enum ModbusConfig {
178 /// Configuration for Modbus TCP/IP.
179 Tcp(ModbusTcpConfig),
180 /// Configuration for Modbus Serial (RTU or ASCII).
181 Serial(ModbusSerialConfig),
182}
183
184/// Implements common functionality for `ModbusConfig`.
185impl ModbusConfig {
186 /// Returns the number of retry attempts configured for the transport.
187 pub fn retry_attempts(&self) -> u8 {
188 match self {
189 ModbusConfig::Tcp(config) => config.retry_attempts,
190 ModbusConfig::Serial(config) => config.retry_attempts,
191 }
192 }
193
194 /// Returns the configured retry backoff strategy.
195 pub fn retry_backoff_strategy(&self) -> BackoffStrategy {
196 match self {
197 ModbusConfig::Tcp(config) => config.retry_backoff_strategy,
198 ModbusConfig::Serial(config) => config.retry_backoff_strategy,
199 }
200 }
201
202 /// Returns the configured retry jitter strategy.
203 pub fn retry_jitter_strategy(&self) -> JitterStrategy {
204 match self {
205 ModbusConfig::Tcp(config) => config.retry_jitter_strategy,
206 ModbusConfig::Serial(config) => config.retry_jitter_strategy,
207 }
208 }
209
210 /// Returns the optional application-provided random callback for jitter.
211 pub fn retry_random_fn(&self) -> Option<RetryRandomFn> {
212 match self {
213 ModbusConfig::Tcp(config) => config.retry_random_fn,
214 ModbusConfig::Serial(config) => config.retry_random_fn,
215 }
216 }
217}
218
219/// Parity bit configuration for serial communication.
220#[derive(Debug, Clone, Copy, Default)]
221pub enum Parity {
222 /// No parity bit is used.
223 None,
224 /// Even parity: the number of 1-bits in the data plus parity bit is even.
225 #[default]
226 Even,
227 /// Odd parity: the number of 1-bits in the data plus parity bit is odd.
228 Odd,
229}
230
231/// Number of data bits per serial character.
232#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
233pub enum DataBits {
234 /// 5 data bits.
235 Five,
236 /// 6 data bits.
237 Six,
238 /// 7 data bits.
239 Seven,
240 /// 8 data bits.
241 #[default]
242 Eight,
243}
244
245/// Configuration parameters for establishing a Modbus Serial connection.
246#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
247pub enum SerialMode {
248 /// Modbus RTU mode, which uses binary encoding and CRC error checking.
249 #[default]
250 Rtu,
251 /// Modbus ASCII mode, which uses ASCII encoding and LRC error checking.
252 Ascii,
253}
254
255/// Baud rate configuration for serial communication.
256#[derive(Debug, Clone, Copy, Default)]
257pub enum BaudRate {
258 /// Standard baud rate of 9600 bits per second.
259 Baud9600,
260 /// Standard baud rate of 19200 bits per second.
261 #[default]
262 Baud19200,
263 /// Custom baud rate.
264 Custom(u32), // Allow custom baud rates for flexibility
265}
266
267#[derive(Debug)]
268/// Configuration parameters for establishing a Modbus Serial connection.
269pub struct ModbusSerialConfig<const PORT_PATH_LEN: usize = 64> {
270 /// The path to the serial port (e.g., "/dev/ttyUSB0" or "COM1").
271 pub port_path: heapless::String<PORT_PATH_LEN>,
272 /// The serial mode to use (RTU or ASCII).
273 pub mode: SerialMode,
274 /// Communication speed in bits per second (e.g., 9600, 115200).
275 pub baud_rate: BaudRate,
276 /// Number of data bits per character (typically `DataBits::Eight` for RTU, `DataBits::Seven` for ASCII).
277 pub data_bits: DataBits,
278 /// Number of stop bits (This will be recalculated before calling the transport layer).
279 pub stop_bits: u8,
280 /// The parity checking mode.
281 pub parity: Parity,
282 /// Timeout for waiting for a response in milliseconds.
283 pub response_timeout_ms: u32,
284 /// Number of retries for failed operations.
285 pub retry_attempts: u8,
286 /// Backoff strategy used when scheduling retries.
287 pub retry_backoff_strategy: BackoffStrategy,
288 /// Optional jitter strategy applied on top of retry backoff delay.
289 pub retry_jitter_strategy: JitterStrategy,
290 /// Optional application-provided random callback used by jitter.
291 pub retry_random_fn: Option<RetryRandomFn>,
292}
293
294#[derive(Debug, Clone)]
295/// Configuration parameters for establishing a Modbus TCP connection.
296pub struct ModbusTcpConfig {
297 /// The hostname or IP address of the Modbus TCP server to connect to.
298 pub host: heapless::String<64>, // Increased capacity for host string to accommodate longer IP addresses/hostnames
299 /// The TCP port number on which the Modbus server is listening (default is typically 502).
300 pub port: u16,
301
302 // Optional parameters for connection management (can be set to default values if not needed)
303 /// Timeout for establishing a connection in milliseconds
304 pub connection_timeout_ms: u32,
305 /// Timeout for waiting for a response in milliseconds
306 pub response_timeout_ms: u32,
307 /// Number of retry attempts for failed operations
308 pub retry_attempts: u8,
309 /// Backoff strategy used when scheduling retries.
310 pub retry_backoff_strategy: BackoffStrategy,
311 /// Optional jitter strategy applied on top of retry backoff delay.
312 pub retry_jitter_strategy: JitterStrategy,
313 /// Optional application-provided random callback used by jitter.
314 pub retry_random_fn: Option<RetryRandomFn>,
315}
316
317/// The transport module defines the `Transport` trait and related types for managing Modbus TCP communication.
318impl ModbusTcpConfig {
319 /// Creates a new `ModbusTcpConfig` instance with the specified host and port.
320 /// # Arguments
321 /// * `host` - The hostname or IP address of the Modbus TCP server to connect to.
322 /// * `port` - The TCP port number on which the Modbus server is listening.
323 /// # Returns
324 /// A new `ModbusTcpConfig` instance with the provided host and port.
325 pub fn with_default_port(host: &str) -> Result<Self, MbusError> {
326 let host_string: String<64> =
327 String::from_str(host).map_err(|_| MbusError::BufferTooSmall)?; // Return error if host string is too long
328 Self::new(&host_string, MODBUS_TCP_DEFAULT_PORT)
329 }
330
331 /// Creates a new `ModbusTcpConfig` instance with the specified host and port.
332 /// # Arguments
333 /// * `host` - The hostname or IP address of the Modbus TCP server to connect to.
334 /// * `port` - The TCP port number on which the Modbus server is listening.
335 /// # Returns
336 /// A new `ModbusTcpConfig` instance with the provided host and port.
337 pub fn new(host: &str, port: u16) -> Result<Self, MbusError> {
338 let host_string = String::from_str(host).map_err(|_| MbusError::BufferTooSmall)?; // Return error if host string is too long
339 Ok(Self {
340 host: host_string,
341 port,
342 connection_timeout_ms: 5000,
343 response_timeout_ms: 5000,
344 retry_attempts: 3,
345 retry_backoff_strategy: BackoffStrategy::Immediate,
346 retry_jitter_strategy: JitterStrategy::None,
347 retry_random_fn: None,
348 })
349 }
350}
351
352use core::fmt;
353
354/// Represents errors that can occur at the Modbus TCP transport layer.
355#[derive(Debug, PartialEq, Eq)]
356pub enum TransportError {
357 /// The connection attempt failed.
358 ConnectionFailed,
359 /// The connection was unexpectedly closed.
360 ConnectionClosed,
361 /// An I/O error occurred during send or receive.
362 IoError,
363 /// A timeout occurred during a network operation.
364 Timeout,
365 /// The received data was too large for the buffer.
366 BufferTooSmall,
367 /// An unexpected error occurred.
368 Unexpected,
369 /// Invalid configuration
370 InvalidConfiguration,
371}
372
373impl fmt::Display for TransportError {
374 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
375 match self {
376 TransportError::ConnectionFailed => write!(f, "Connection failed"),
377 TransportError::ConnectionClosed => write!(f, "Connection closed"),
378 TransportError::IoError => write!(f, "I/O error"),
379 TransportError::Timeout => write!(f, "Timeout"),
380 TransportError::BufferTooSmall => write!(f, "Buffer too small"),
381 TransportError::Unexpected => write!(f, "An unexpected error occurred"),
382 TransportError::InvalidConfiguration => write!(f, "Invalid configuration"),
383 }
384 }
385}
386
387/// Implements the core standard `Error` trait for `TransportError`, allowing it to be used with Rust's error handling ecosystem.
388impl core::error::Error for TransportError {}
389
390/// An enumeration to specify the type of transport to use.
391#[derive(Debug, Clone, Copy, PartialEq, Eq)]
392pub enum TransportType {
393 /// Standard library TCP transport implementation.
394 StdTcp,
395 /// Standard library Serial transport implementation.
396 StdSerial(SerialMode),
397 /// Custom TCP transport implementation.
398 CustomTcp,
399 /// Custom Serial transport implementation.
400 CustomSerial(SerialMode),
401}
402
403impl TransportType {
404 /// Returns `true` if the transport type is TCP (StdTcp or CustomTcp).
405 pub fn is_tcp_type(&self) -> bool {
406 matches!(self, TransportType::StdTcp | TransportType::CustomTcp)
407 }
408
409 /// Returns `true` if the transport type is serial (RTU or ASCII).
410 pub fn is_serial_type(&self) -> bool {
411 matches!(
412 self,
413 TransportType::StdSerial(_) | TransportType::CustomSerial(_)
414 )
415 }
416}
417
418impl From<TransportError> for MbusError {
419 fn from(err: TransportError) -> Self {
420 match err {
421 TransportError::ConnectionFailed => MbusError::ConnectionFailed,
422 TransportError::ConnectionClosed => MbusError::ConnectionClosed,
423 TransportError::IoError => MbusError::IoError,
424 TransportError::Timeout => MbusError::Timeout,
425 TransportError::BufferTooSmall => MbusError::BufferTooSmall,
426 TransportError::Unexpected => MbusError::Unexpected,
427 TransportError::InvalidConfiguration => MbusError::InvalidConfiguration,
428 }
429 }
430}
431
432/// A type-safe wrapper for Modbus Unit Identifiers (TCP) and Slave Addresses (Serial).
433///
434/// ### Address Ranges:
435/// - **1 to 247**: Valid Unicast addresses for individual slave devices.
436/// - **0**: Reserved for **BROADCAST** operations.
437/// - **248 to 255**: Reserved/Invalid addresses.
438///
439/// ### ⚠️ Important: Broadcasting (Address 0)
440/// To prevent accidental broadcast requests (which are processed by all devices and
441/// **never** return a response), address `0` cannot be passed to the standard `new()`
442/// or `try_from()` constructors.
443///
444/// Developers **must** explicitly use [`UnitIdOrSlaveAddr::new_broadcast_address()`]
445/// to signal intent for a broadcast operation.
446///
447/// *Note: Broadcasts are generally only supported for Write operations on Serial transports.*
448#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449pub struct UnitIdOrSlaveAddr(u8);
450
451impl UnitIdOrSlaveAddr {
452 /// Creates a new `UnitIdOrSlaveAddr` instance with the specified address.
453 ///
454 /// ### Address Ranges:
455 /// - **1 to 247**: Valid Unicast addresses for individual slave devices.
456 /// - **0**: Reserved for **BROADCAST** operations.
457 /// - **248 to 255**: Reserved/Invalid addresses.
458 ///
459 /// ### ⚠️ Important: Broadcasting (Address 0)
460 /// To prevent accidental broadcast requests (which are processed by all devices and
461 /// **never** return a response), address `0` cannot be passed to the standard `new()`
462 /// or `try_from()` constructors.
463 ///
464 /// Developers **must** explicitly use [`UnitIdOrSlaveAddr::new_broadcast_address()`]
465 /// to signal intent for a broadcast operation.
466 ///
467 /// # Arguments:
468 /// - `address`: The `u8` value representing the Unit ID or Slave Address.
469 pub fn new(address: u8) -> Result<Self, MbusError> {
470 if (1..=247).contains(&address) {
471 return Ok(Self(address));
472 }
473
474 if 0 == address {
475 return Err(MbusError::InvalidBroadcastAddress);
476 }
477 Err(MbusError::InvalidSlaveAddress)
478 }
479
480 /// Creates a new `UnitIdOrSlaveAddr` instance representing the broadcast address (`0`).
481 ///
482 /// *Note: Broadcasts are generally only supported for Write operations on Serial transports.*
483 pub fn new_broadcast_address() -> Self {
484 Self(0)
485 }
486
487 /// Returns `true` if the address is the Modbus broadcast address (0).
488 pub fn is_broadcast(&self) -> bool {
489 self.0 == 0
490 }
491
492 /// Returns the raw `u8` value of the slave address.
493 ///
494 /// # Returns
495 /// The `u8` value representing the slave address.
496 pub fn get(&self) -> u8 {
497 self.0
498 }
499}
500
501impl Default for UnitIdOrSlaveAddr {
502 /// Provides a default value for initialization or error states.
503 ///
504 /// # ⚠️ Warning
505 /// This returns `255`, which is outside the valid Modbus slave address range (1-247).
506 /// It is intended to be used as a sentinel value to represent an uninitialized or
507 /// invalid address state that must be handled by the application logic.
508 /// This value will/should not be sent over the wire.
509 fn default() -> Self {
510 // 255 is in the reserved range (248-255) and serves as a safe
511 // "Null" or "Error" marker in this context.
512 Self(255)
513 }
514}
515
516/// A trait for types that can be created from a `u8` Unit ID or Slave Address.
517pub trait UidSaddrFrom {
518 /// Creates an instance from a raw stored `u8` Unit ID / Slave Address.
519 ///
520 /// This is intended for internal reconstruction paths where the value was
521 /// originally produced from a validated `UnitIdOrSlaveAddr` and later stored
522 /// as a raw `u8` (for example, queue bookkeeping fields).
523 ///
524 /// Do not use this for external or untrusted input parsing. For that use case,
525 /// use `UnitIdOrSlaveAddr::new(...)` or `TryFrom<u8>` so invalid values are
526 /// surfaced as errors.
527 fn from_u8(uid_saddr: u8) -> Self;
528}
529
530/// Implementation of `UidSaddrFrom` for `UnitIdOrSlaveAddr`.
531impl UidSaddrFrom for UnitIdOrSlaveAddr {
532 /// Creates an instance from an internal raw `u8` Unit ID / Slave Address.
533 ///
534 /// This helper is used in internal flows that reconstruct an address from
535 /// previously validated values serialized to `u8` for storage.
536 ///
537 /// If an invalid raw value is encountered (which should not occur in normal
538 /// operation), this returns the `Default` sentinel instead of panicking.
539 /// This makes corruption visible to upper layers without crashing.
540 ///
541 /// For external input validation, prefer `UnitIdOrSlaveAddr::new(...)` or
542 /// `TryFrom<u8>`.
543 ///
544 /// # Arguments
545 /// * `value` - The `u8` value representing the Unit ID or Slave Address.
546 ///
547 /// # Returns
548 /// A new `UnitIdOrSlaveAddr` instance.
549 fn from_u8(value: u8) -> Self {
550 UnitIdOrSlaveAddr::new(value).unwrap_or_default()
551 }
552}
553
554impl From<UnitIdOrSlaveAddr> for u8 {
555 /// Implementation of `From<UnitIdOrSlaveAddr>` for `u8`.
556 ///
557 /// This allows `UnitIdOrSlaveAddr` to be converted into a `u8` value.
558 ///
559 /// # Returns
560 /// The raw `u8` value of the `UnitIdOrSlaveAddr`.
561 ///
562 fn from(val: UnitIdOrSlaveAddr) -> Self {
563 val.get()
564 }
565}
566
567/// Implementation of `TryFrom` to allow safe conversion from a raw `u8`
568/// to a validated `UnitIdOrSlaveAddr`.
569impl TryFrom<u8> for UnitIdOrSlaveAddr {
570 type Error = MbusError;
571 /// Attempts to create a new `UnitIdOrSlaveAddr` from a raw `u8` value.
572 ///
573 fn try_from(value: u8) -> Result<Self, Self::Error> {
574 // Delegates to the new() constructor which performs range validation (1-247)
575 UnitIdOrSlaveAddr::new(value)
576 }
577}
578
579/// A unified trait defining the interface for any Modbus physical or network transport layer.
580///
581/// This trait abstracts the underlying communication medium (e.g., TCP socket, Serial COM port,
582/// or a mocked in-memory buffer) so that the higher-level Modbus Client Services can orchestrate
583/// transactions without needing to know the specifics of the hardware layer.
584///
585/// # Implementor Responsibilities
586/// Implementors of this trait must ensure:
587/// - **Connection Management**: Handling the initialization and teardown of the physical link.
588/// - **Framing**: Reading exactly one complete Modbus Application Data Unit (ADU) at a time for TCP.
589/// For TCP, this means parsing the MBAP header to determine the length. For Serial (RTU), this
590/// involves managing inter-frame timing silences or LRC/CRCs. In other words, just provide the available bytes;
591/// the protocol stack is intelligent enough to construct the full frame. If a timeout occurs, the stack will clear the buffer.
592pub trait Transport {
593 /// The specific error type returned by this transport implementation.
594 /// It must be convertible into the common `MbusError` for upper-layer processing.
595 type Error: Into<MbusError> + core::fmt::Debug;
596
597 /// Compile-time capability flag for Serial-style broadcast write semantics.
598 ///
599 /// Set this to `true` for transport implementations that can safely apply
600 /// Modbus broadcast writes (address `0`) with no response. Most transports
601 /// should keep the default `false`.
602 const SUPPORTS_BROADCAST_WRITES: bool = false;
603
604 /// Compile-time transport type metadata.
605 ///
606 /// Every implementation must declare its transport family here.
607 /// For transports whose serial mode (RTU / ASCII) is chosen at runtime,
608 /// set this to a representative value (e.g. `StdSerial(SerialMode::Rtu)`)
609 /// and override [`transport_type()`](Transport::transport_type) to return
610 /// the actual instance mode. The compile-time value is used by the server
611 /// for optimizations such as broadcast eligibility (`is_serial_type()`),
612 /// while the runtime method is authoritative for framing decisions.
613 const TRANSPORT_TYPE: TransportType;
614
615 /// Establishes the physical or logical connection to the Modbus server/slave.
616 ///
617 /// # Arguments
618 /// * `config` - A generalized `ModbusConfig` enum containing specific settings (like
619 /// IP/Port for TCP or Baud Rate/Parity for Serial connections).
620 ///
621 /// # Returns
622 /// - `Ok(())` if the underlying port was opened or socket successfully connected.
623 /// - `Err(Self::Error)` if the initialization fails (e.g., port busy, network unreachable).
624 fn connect(&mut self, config: &ModbusConfig) -> Result<(), Self::Error>;
625
626 /// Gracefully closes the active connection and releases underlying resources.
627 ///
628 /// After calling this method, subsequent calls to `send` or `recv` should fail until
629 /// `connect` is called again.
630 fn disconnect(&mut self) -> Result<(), Self::Error>;
631
632 /// Transmits a complete Modbus Application Data Unit (ADU) over the transport medium.
633 ///
634 /// The provided `adu` slice contains the fully formed byte frame, including all headers
635 /// (like MBAP for TCP) and footers (like CRC/LRC for Serial).
636 ///
637 /// # Arguments
638 /// * `adu` - A contiguous byte slice representing the packet to send.
639 fn send(&mut self, adu: &[u8]) -> Result<(), Self::Error>;
640
641 /// Receives available bytes from the transport medium in a **non-blocking** manner.
642 ///
643 /// # Implementation Details
644 /// - **TCP**: Implementors should ideally return a complete Modbus Application Data Unit (ADU).
645 /// - **Serial**: Implementors can return any number of available bytes. The protocol stack
646 /// is responsible for accumulating these fragments into a complete frame.
647 /// - **Timeouts**: If the protocol stack fails to assemble a full frame within the configured
648 /// `response_timeout_ms`, it will automatically clear its internal buffers.
649 ///
650 /// # Returns
651 /// - `Ok(Vec<u8, MAX_ADU_FRAME_LEN>)`: A non-empty heapless vector containing bytes read since
652 /// the last call.
653 /// - `Err(Self::Error)`: Returns `TransportError::Timeout` if no data is currently available,
654 /// or other errors if the connection is lost or hardware fails.
655 ///
656 /// Contract note: when no data is available in non-blocking mode, implementations must
657 /// return `Err(TransportError::Timeout)` (or transport-specific equivalent) and should not
658 /// return `Ok` with an empty vector.
659 fn recv(&mut self) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, Self::Error>;
660
661 /// Checks if the transport considers itself currently active and connected.
662 ///
663 /// Note: For connectionless or semi-connected states (like some RS-485 setups), this
664 /// might continually return `true` as long as the local port is open.
665 fn is_connected(&self) -> bool;
666}
667
668/// A trait for abstracting time-related operations, primarily for mocking in tests
669/// and providing a consistent interface for `no_std` environments.
670pub trait TimeKeeper {
671 /// A simple mock for current_millis for no_std compatibility in tests.
672 /// In a real no_std environment, this would come from a hardware timer.
673 /// Returns the current time in milliseconds.
674 ///
675 /// # Returns
676 /// The current time in milliseconds.
677 fn current_millis(&self) -> u64;
678}