#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
use core::ffi::CStr;
use core::ffi::c_char;
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
use mbus_core::transport::BackoffStrategy;
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
use mbus_core::transport::JitterStrategy;
#[cfg(any(feature = "serial-rtu", feature = "serial-ascii"))]
use mbus_core::transport::{BaudRate, DataBits, ModbusSerialConfig, Parity, SerialMode};
#[cfg(feature = "network-tcp")]
use mbus_core::transport::ModbusConfig;
#[cfg(feature = "network-tcp")]
use mbus_core::transport::ModbusTcpConfig;
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
use crate::c::error::MbusStatusCode;
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MbusBackoffStrategy {
MbusBackoffImmediate = 0,
MbusBackoffFixed,
MbusBackoffExponential,
MbusBackoffLinear,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MbusSerialMode {
MbusSerialRtu = 0,
MbusSerialAscii,
}
#[repr(C)]
pub struct MbusTcpConfig {
pub host: *const c_char,
pub port: u16,
pub connection_timeout_ms: u32,
pub response_timeout_ms: u32,
pub retries: u8,
pub backoff_strategy: MbusBackoffStrategy,
pub backoff_base_delay_ms: u32,
pub backoff_max_delay_ms: u32,
pub jitter_percent: u8,
}
#[repr(C)]
pub struct MbusSerialConfig {
pub port_name: *const c_char,
pub baud_rate: u32,
pub mode: MbusSerialMode,
pub response_timeout_ms: u32,
pub retries: u8,
pub backoff_strategy: MbusBackoffStrategy,
pub backoff_base_delay_ms: u32,
pub backoff_max_delay_ms: u32,
pub jitter_percent: u8,
}
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
fn map_backoff(strategy: MbusBackoffStrategy, base: u32, max: u32) -> BackoffStrategy {
match strategy {
MbusBackoffStrategy::MbusBackoffImmediate => BackoffStrategy::Immediate,
MbusBackoffStrategy::MbusBackoffFixed => BackoffStrategy::Fixed { delay_ms: base },
MbusBackoffStrategy::MbusBackoffExponential => BackoffStrategy::Exponential {
base_delay_ms: base,
max_delay_ms: max,
},
MbusBackoffStrategy::MbusBackoffLinear => BackoffStrategy::Linear {
initial_delay_ms: base,
increment_ms: base,
max_delay_ms: max,
},
}
}
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
fn map_jitter(percent: u8) -> JitterStrategy {
if percent == 0 {
JitterStrategy::None
} else {
JitterStrategy::Percentage { percent }
}
}
#[cfg(feature = "network-tcp")]
pub(super) unsafe fn tcp_config_from_c(
cfg: *const MbusTcpConfig,
) -> Result<ModbusConfig, MbusStatusCode> {
if cfg.is_null() {
return Err(MbusStatusCode::MbusErrNullPointer);
}
let cfg = unsafe { &*cfg };
if cfg.host.is_null() {
return Err(MbusStatusCode::MbusErrNullPointer);
}
let host_str = unsafe { CStr::from_ptr(cfg.host) }
.to_str()
.map_err(|_| MbusStatusCode::MbusErrInvalidUtf8)?;
let inner = ModbusTcpConfig::new(host_str, cfg.port).map_err(MbusStatusCode::from)?;
Ok(ModbusConfig::Tcp(ModbusTcpConfig {
connection_timeout_ms: cfg.connection_timeout_ms,
response_timeout_ms: cfg.response_timeout_ms,
retry_attempts: cfg.retries,
retry_backoff_strategy: map_backoff(
cfg.backoff_strategy,
cfg.backoff_base_delay_ms,
cfg.backoff_max_delay_ms,
),
retry_jitter_strategy: map_jitter(cfg.jitter_percent),
retry_random_fn: None,
..inner
}))
}
#[cfg(any(feature = "serial-rtu", feature = "serial-ascii"))]
pub(super) unsafe fn serial_config_from_c(
cfg: *const MbusSerialConfig,
) -> Result<ModbusSerialConfig, MbusStatusCode> {
if cfg.is_null() {
return Err(MbusStatusCode::MbusErrNullPointer);
}
let cfg = unsafe { &*cfg };
if cfg.port_name.is_null() {
return Err(MbusStatusCode::MbusErrNullPointer);
}
let port_str = unsafe { CStr::from_ptr(cfg.port_name) }
.to_str()
.map_err(|_| MbusStatusCode::MbusErrInvalidUtf8)?;
let mode = match cfg.mode {
MbusSerialMode::MbusSerialRtu => SerialMode::Rtu,
MbusSerialMode::MbusSerialAscii => SerialMode::Ascii,
};
let port_path = heapless::String::<64>::try_from(port_str)
.map_err(|_| MbusStatusCode::MbusErrBufferTooSmall)?;
Ok(ModbusSerialConfig {
port_path,
mode,
baud_rate: BaudRate::Custom(cfg.baud_rate),
data_bits: DataBits::Eight,
parity: Parity::None,
stop_bits: 1,
response_timeout_ms: cfg.response_timeout_ms,
retry_attempts: cfg.retries,
retry_backoff_strategy: map_backoff(
cfg.backoff_strategy,
cfg.backoff_base_delay_ms,
cfg.backoff_max_delay_ms,
),
retry_jitter_strategy: map_jitter(cfg.jitter_percent),
retry_random_fn: None,
})
}
#[cfg(test)]
mod tests {
use super::*;
use core::ffi::c_char;
#[test]
fn tcp_null_cfg_returns_null_pointer_error() {
let result = unsafe { tcp_config_from_c(core::ptr::null()) };
assert!(matches!(result, Err(MbusStatusCode::MbusErrNullPointer)));
}
#[test]
fn tcp_null_host_returns_null_pointer_error() {
let cfg = MbusTcpConfig {
host: core::ptr::null(),
port: 502,
connection_timeout_ms: 1000,
response_timeout_ms: 1000,
retries: 3,
backoff_strategy: MbusBackoffStrategy::MbusBackoffImmediate,
backoff_base_delay_ms: 0,
backoff_max_delay_ms: 0,
jitter_percent: 0,
};
let result = unsafe { tcp_config_from_c(&cfg) };
assert!(matches!(result, Err(MbusStatusCode::MbusErrNullPointer)));
}
#[test]
fn tcp_host_too_long_returns_error() {
let long: [u8; 66] = {
let mut a = [b'a'; 66];
a[65] = 0;
a
};
let cfg = MbusTcpConfig {
host: long.as_ptr() as *const c_char,
port: 502,
connection_timeout_ms: 1000,
response_timeout_ms: 1000,
retries: 3,
backoff_strategy: MbusBackoffStrategy::MbusBackoffImmediate,
backoff_base_delay_ms: 0,
backoff_max_delay_ms: 0,
jitter_percent: 0,
};
let result = unsafe { tcp_config_from_c(&cfg) };
assert!(
result.is_err(),
"expected error for host > 63 bytes, got Ok"
);
}
#[test]
fn tcp_invalid_utf8_host_returns_error() {
let bad: [u8; 2] = [0xFF, 0x00];
let cfg = MbusTcpConfig {
host: bad.as_ptr() as *const c_char,
port: 502,
connection_timeout_ms: 1000,
response_timeout_ms: 1000,
retries: 3,
backoff_strategy: MbusBackoffStrategy::MbusBackoffImmediate,
backoff_base_delay_ms: 0,
backoff_max_delay_ms: 0,
jitter_percent: 0,
};
let result = unsafe { tcp_config_from_c(&cfg) };
assert!(matches!(result, Err(MbusStatusCode::MbusErrInvalidUtf8)));
}
#[test]
fn tcp_valid_config_round_trips() {
let host = b"192.168.1.1\0";
let cfg = MbusTcpConfig {
host: host.as_ptr() as *const c_char,
port: 502,
connection_timeout_ms: 2000,
response_timeout_ms: 3000,
retries: 5,
backoff_strategy: MbusBackoffStrategy::MbusBackoffFixed,
backoff_base_delay_ms: 100,
backoff_max_delay_ms: 0,
jitter_percent: 10,
};
let result = unsafe { tcp_config_from_c(&cfg) };
assert!(
result.is_ok(),
"expected Ok for valid config, got {:?}",
result
);
if let Ok(ModbusConfig::Tcp(inner)) = result {
assert_eq!(inner.port, 502);
assert_eq!(inner.response_timeout_ms, 3000);
assert_eq!(inner.retry_attempts, 5);
} else {
panic!("expected ModbusConfig::Tcp");
}
}
#[test]
fn serial_null_cfg_returns_null_pointer_error() {
let result = unsafe { serial_config_from_c(core::ptr::null()) };
assert!(matches!(result, Err(MbusStatusCode::MbusErrNullPointer)));
}
#[test]
fn serial_null_port_name_returns_null_pointer_error() {
let cfg = MbusSerialConfig {
port_name: core::ptr::null(),
baud_rate: 9600,
mode: MbusSerialMode::MbusSerialRtu,
response_timeout_ms: 1000,
retries: 3,
backoff_strategy: MbusBackoffStrategy::MbusBackoffImmediate,
backoff_base_delay_ms: 0,
backoff_max_delay_ms: 0,
jitter_percent: 0,
};
let result = unsafe { serial_config_from_c(&cfg) };
assert!(matches!(result, Err(MbusStatusCode::MbusErrNullPointer)));
}
#[test]
fn serial_port_name_too_long_returns_error() {
let long: [u8; 66] = {
let mut a = [b'x'; 66];
a[65] = 0;
a
};
let cfg = MbusSerialConfig {
port_name: long.as_ptr() as *const c_char,
baud_rate: 9600,
mode: MbusSerialMode::MbusSerialRtu,
response_timeout_ms: 1000,
retries: 3,
backoff_strategy: MbusBackoffStrategy::MbusBackoffImmediate,
backoff_base_delay_ms: 0,
backoff_max_delay_ms: 0,
jitter_percent: 0,
};
let result = unsafe { serial_config_from_c(&cfg) };
assert!(result.is_err(), "expected error for port_name > 63 bytes");
}
#[test]
fn serial_invalid_utf8_port_name_returns_error() {
let bad: [u8; 2] = [0xFF, 0x00];
let cfg = MbusSerialConfig {
port_name: bad.as_ptr() as *const c_char,
baud_rate: 9600,
mode: MbusSerialMode::MbusSerialRtu,
response_timeout_ms: 1000,
retries: 3,
backoff_strategy: MbusBackoffStrategy::MbusBackoffImmediate,
backoff_base_delay_ms: 0,
backoff_max_delay_ms: 0,
jitter_percent: 0,
};
let result = unsafe { serial_config_from_c(&cfg) };
assert!(matches!(result, Err(MbusStatusCode::MbusErrInvalidUtf8)));
}
#[test]
fn serial_valid_config_round_trips() {
let port = b"/dev/ttyUSB0\0";
let cfg = MbusSerialConfig {
port_name: port.as_ptr() as *const c_char,
baud_rate: 115200,
mode: MbusSerialMode::MbusSerialAscii,
response_timeout_ms: 500,
retries: 2,
backoff_strategy: MbusBackoffStrategy::MbusBackoffImmediate,
backoff_base_delay_ms: 0,
backoff_max_delay_ms: 0,
jitter_percent: 0,
};
let result = unsafe { serial_config_from_c(&cfg) };
assert!(
result.is_ok(),
"expected Ok for valid config, got {:?}",
result
);
let inner = result.unwrap();
assert_eq!(inner.response_timeout_ms, 500);
assert_eq!(inner.retry_attempts, 2);
}
#[test]
fn map_backoff_immediate_variant() {
let s = map_backoff(MbusBackoffStrategy::MbusBackoffImmediate, 0, 0);
assert!(matches!(s, BackoffStrategy::Immediate));
}
#[test]
fn map_backoff_fixed_variant() {
let s = map_backoff(MbusBackoffStrategy::MbusBackoffFixed, 50, 0);
assert!(matches!(s, BackoffStrategy::Fixed { delay_ms: 50 }));
}
#[test]
fn map_jitter_zero_is_none() {
assert!(matches!(map_jitter(0), JitterStrategy::None));
}
#[test]
fn map_jitter_nonzero_is_percentage() {
assert!(matches!(
map_jitter(25),
JitterStrategy::Percentage { percent: 25 }
));
}
}