pub mod checksum;
use core::str::FromStr;
use crate::{data_unit::common::MAX_ADU_FRAME_LEN, errors::MbusError};
use heapless::{String, Vec};
const MODBUS_TCP_DEFAULT_PORT: u16 = 502;
pub type RetryRandomFn = fn() -> u32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackoffStrategy {
Immediate,
Fixed {
delay_ms: u32,
},
Exponential {
base_delay_ms: u32,
max_delay_ms: u32,
},
Linear {
initial_delay_ms: u32,
increment_ms: u32,
max_delay_ms: u32,
},
}
impl Default for BackoffStrategy {
fn default() -> Self {
Self::Immediate
}
}
impl BackoffStrategy {
pub fn delay_ms_for_retry(&self, retry_attempt: u8) -> u32 {
let attempt = retry_attempt.max(1);
match self {
BackoffStrategy::Immediate => 0,
BackoffStrategy::Fixed { delay_ms } => *delay_ms,
BackoffStrategy::Exponential {
base_delay_ms,
max_delay_ms,
} => {
let shift = (attempt.saturating_sub(1)).min(31);
let factor = 1u32 << shift;
base_delay_ms.saturating_mul(factor).min(*max_delay_ms)
}
BackoffStrategy::Linear {
initial_delay_ms,
increment_ms,
max_delay_ms,
} => {
let growth = increment_ms.saturating_mul((attempt.saturating_sub(1)) as u32);
initial_delay_ms.saturating_add(growth).min(*max_delay_ms)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum JitterStrategy {
#[default]
None,
Percentage {
percent: u8,
},
BoundedMs {
max_jitter_ms: u32,
},
}
impl JitterStrategy {
pub fn apply(self, base_delay_ms: u32, random_fn: Option<RetryRandomFn>) -> u32 {
let delta = match self {
JitterStrategy::None => return base_delay_ms,
JitterStrategy::Percentage { percent } => {
if percent == 0 || base_delay_ms == 0 {
return base_delay_ms;
}
base_delay_ms.saturating_mul((percent.min(100)) as u32) / 100
}
JitterStrategy::BoundedMs { max_jitter_ms } => {
if max_jitter_ms == 0 {
return base_delay_ms;
}
max_jitter_ms
}
};
let random = match random_fn {
Some(cb) => cb(),
None => return base_delay_ms,
};
let span = delta.saturating_mul(2).saturating_add(1);
if span == 0 {
return base_delay_ms;
}
let offset = (random % span) as i64 - delta as i64;
let jittered = base_delay_ms as i64 + offset;
jittered.max(0) as u32
}
}
#[derive(Debug)]
pub enum ModbusConfig {
Tcp(ModbusTcpConfig),
Serial(ModbusSerialConfig),
}
impl ModbusConfig {
pub fn retry_attempts(&self) -> u8 {
match self {
ModbusConfig::Tcp(config) => config.retry_attempts,
ModbusConfig::Serial(config) => config.retry_attempts,
}
}
pub fn retry_backoff_strategy(&self) -> BackoffStrategy {
match self {
ModbusConfig::Tcp(config) => config.retry_backoff_strategy,
ModbusConfig::Serial(config) => config.retry_backoff_strategy,
}
}
pub fn retry_jitter_strategy(&self) -> JitterStrategy {
match self {
ModbusConfig::Tcp(config) => config.retry_jitter_strategy,
ModbusConfig::Serial(config) => config.retry_jitter_strategy,
}
}
pub fn retry_random_fn(&self) -> Option<RetryRandomFn> {
match self {
ModbusConfig::Tcp(config) => config.retry_random_fn,
ModbusConfig::Serial(config) => config.retry_random_fn,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub enum Parity {
None,
#[default]
Even,
Odd,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum DataBits {
Five,
Six,
Seven,
#[default]
Eight,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum SerialMode {
#[default]
Rtu,
Ascii,
}
#[derive(Debug, Clone, Copy, Default)]
pub enum BaudRate {
Baud9600,
#[default]
Baud19200,
Custom(u32), }
#[derive(Debug)]
pub struct ModbusSerialConfig<const PORT_PATH_LEN: usize = 64> {
pub port_path: heapless::String<PORT_PATH_LEN>,
pub mode: SerialMode,
pub baud_rate: BaudRate,
pub data_bits: DataBits,
pub stop_bits: u8,
pub parity: Parity,
pub response_timeout_ms: u32,
pub retry_attempts: u8,
pub retry_backoff_strategy: BackoffStrategy,
pub retry_jitter_strategy: JitterStrategy,
pub retry_random_fn: Option<RetryRandomFn>,
}
#[derive(Debug, Clone)]
pub struct ModbusTcpConfig {
pub host: heapless::String<64>, pub port: u16,
pub connection_timeout_ms: u32,
pub response_timeout_ms: u32,
pub retry_attempts: u8,
pub retry_backoff_strategy: BackoffStrategy,
pub retry_jitter_strategy: JitterStrategy,
pub retry_random_fn: Option<RetryRandomFn>,
}
impl ModbusTcpConfig {
pub fn with_default_port(host: &str) -> Result<Self, MbusError> {
let host_string: String<64> =
String::from_str(host).map_err(|_| MbusError::BufferTooSmall)?; Self::new(&host_string, MODBUS_TCP_DEFAULT_PORT)
}
pub fn new(host: &str, port: u16) -> Result<Self, MbusError> {
let host_string = String::from_str(host).map_err(|_| MbusError::BufferTooSmall)?; Ok(Self {
host: host_string,
port,
connection_timeout_ms: 5000,
response_timeout_ms: 5000,
retry_attempts: 3,
retry_backoff_strategy: BackoffStrategy::Immediate,
retry_jitter_strategy: JitterStrategy::None,
retry_random_fn: None,
})
}
}
use core::fmt;
#[derive(Debug, PartialEq, Eq)]
pub enum TransportError {
ConnectionFailed,
ConnectionClosed,
IoError,
Timeout,
BufferTooSmall,
Unexpected,
InvalidConfiguration,
}
impl fmt::Display for TransportError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
TransportError::ConnectionFailed => write!(f, "Connection failed"),
TransportError::ConnectionClosed => write!(f, "Connection closed"),
TransportError::IoError => write!(f, "I/O error"),
TransportError::Timeout => write!(f, "Timeout"),
TransportError::BufferTooSmall => write!(f, "Buffer too small"),
TransportError::Unexpected => write!(f, "An unexpected error occurred"),
TransportError::InvalidConfiguration => write!(f, "Invalid configuration"),
}
}
}
impl core::error::Error for TransportError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransportType {
StdTcp,
StdSerial(SerialMode),
CustomTcp,
CustomSerial(SerialMode),
}
impl TransportType {
pub fn is_tcp_type(&self) -> bool {
matches!(self, TransportType::StdTcp | TransportType::CustomTcp)
}
pub fn is_serial_type(&self) -> bool {
matches!(
self,
TransportType::StdSerial(_) | TransportType::CustomSerial(_)
)
}
}
impl From<TransportError> for MbusError {
fn from(err: TransportError) -> Self {
match err {
TransportError::ConnectionFailed => MbusError::ConnectionFailed,
TransportError::ConnectionClosed => MbusError::ConnectionClosed,
TransportError::IoError => MbusError::IoError,
TransportError::Timeout => MbusError::Timeout,
TransportError::BufferTooSmall => MbusError::BufferTooSmall,
TransportError::Unexpected => MbusError::Unexpected,
TransportError::InvalidConfiguration => MbusError::InvalidConfiguration,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnitIdOrSlaveAddr(u8);
impl UnitIdOrSlaveAddr {
pub fn new(address: u8) -> Result<Self, MbusError> {
if (1..=247).contains(&address) {
return Ok(Self(address));
}
if 0 == address {
return Err(MbusError::InvalidBroadcastAddress);
}
Err(MbusError::InvalidSlaveAddress)
}
pub fn new_broadcast_address() -> Self {
Self(0)
}
pub fn is_broadcast(&self) -> bool {
self.0 == 0
}
pub fn get(&self) -> u8 {
self.0
}
}
impl Default for UnitIdOrSlaveAddr {
fn default() -> Self {
Self(255)
}
}
pub trait UidSaddrFrom {
fn from_u8(uid_saddr: u8) -> Self;
}
impl UidSaddrFrom for UnitIdOrSlaveAddr {
fn from_u8(value: u8) -> Self {
UnitIdOrSlaveAddr::new(value).unwrap_or_default()
}
}
impl From<UnitIdOrSlaveAddr> for u8 {
fn from(val: UnitIdOrSlaveAddr) -> Self {
val.get()
}
}
impl TryFrom<u8> for UnitIdOrSlaveAddr {
type Error = MbusError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
UnitIdOrSlaveAddr::new(value)
}
}
pub trait Transport {
type Error: Into<MbusError>;
fn connect(&mut self, config: &ModbusConfig) -> Result<(), Self::Error>;
fn disconnect(&mut self) -> Result<(), Self::Error>;
fn send(&mut self, adu: &[u8]) -> Result<(), Self::Error>;
fn recv(&mut self) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, Self::Error>;
fn is_connected(&self) -> bool;
fn transport_type(&self) -> TransportType;
}
pub trait TimeKeeper {
fn current_millis(&self) -> u64;
}