Skip to main content

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}