use std::future::Future;
use std::net::SocketAddr;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{broadcast, Semaphore};
use tokio::time::timeout;
#[cfg(feature = "rtu")]
use tokio_serial;
use tracing::{debug, error, info, warn};
use crate::constants::{
MAX_READ_COILS, MAX_READ_REGISTERS, MAX_RW_READ_REGISTERS, MAX_RW_WRITE_REGISTERS,
MAX_WRITE_COILS, MAX_WRITE_REGISTERS,
};
use crate::error::{ModbusError, ModbusResult};
use crate::register_bank::{ModbusRegisterBank, RegisterBankStats};
const MAX_TCP_FRAME_SIZE: usize = 260;
const MBAP_HEADER_SIZE: usize = 6;
pub trait ModbusServer: Send + Sync {
fn start(&mut self) -> impl std::future::Future<Output = ModbusResult<()>> + Send;
fn stop(&mut self) -> impl std::future::Future<Output = ModbusResult<()>> + Send;
fn is_running(&self) -> bool;
fn get_stats(&self) -> ServerStats;
fn get_register_bank(&self) -> Option<Arc<ModbusRegisterBank>>;
}
pub type ServiceFuture<'a> = Pin<Box<dyn Future<Output = ModbusResult<Vec<u8>>> + Send + 'a>>;
pub trait ModbusService: Send + Sync {
fn handle_pdu<'a>(&'a self, function_code: u8, data: &'a [u8]) -> ServiceFuture<'a>;
}
impl ModbusService for ModbusRegisterBank {
fn handle_pdu<'a>(&'a self, function_code: u8, data: &'a [u8]) -> ServiceFuture<'a> {
Box::pin(async move {
match function_code {
0x01 => ModbusTcpServer::handle_read_01(data, self).await,
0x02 => ModbusTcpServer::handle_read_02(data, self).await,
0x03 => ModbusTcpServer::handle_read_03(data, self).await,
0x04 => ModbusTcpServer::handle_read_04(data, self).await,
0x05 => ModbusTcpServer::handle_write_05(data, self).await,
0x06 => ModbusTcpServer::handle_write_06(data, self).await,
0x0F => ModbusTcpServer::handle_write_0f(data, self).await,
0x10 => ModbusTcpServer::handle_write_10(data, self).await,
0x16 => ModbusTcpServer::handle_write_16(data, self).await,
0x17 => ModbusTcpServer::handle_read_write_17(data, self).await,
0x08 if data.len() >= 4 && data[0] == 0 && data[1] == 0 => {
let mut response = Vec::with_capacity(1 + data.len());
response.push(0x08);
response.extend_from_slice(data);
Ok(response)
}
_ => {
warn!("Unsupported function code: 0x{:02X}", function_code);
Err(ModbusError::invalid_function(function_code))
}
}
})
}
}
#[derive(Debug, Clone, Default)]
pub struct DeviceIdentity {
objects: Vec<(u8, Vec<u8>)>,
}
impl DeviceIdentity {
pub fn basic(vendor_name: &str, product_code: &str, revision: &str) -> Self {
Self::default()
.with_object(0x00, vendor_name.as_bytes())
.with_object(0x01, product_code.as_bytes())
.with_object(0x02, revision.as_bytes())
}
pub fn with_object(mut self, id: u8, value: &[u8]) -> Self {
let value = value[..value.len().min(255)].to_vec();
match self
.objects
.binary_search_by_key(&id, |(obj_id, _)| *obj_id)
{
Ok(pos) => self.objects[pos].1 = value,
Err(pos) => self.objects.insert(pos, (id, value)),
}
self
}
fn conformity_level(&self) -> u8 {
let max_id = self.objects.last().map(|(id, _)| *id).unwrap_or(0);
let stream_level = if max_id <= 0x02 {
0x01
} else if max_id <= 0x06 {
0x02
} else {
0x03
};
stream_level | 0x80
}
#[doc(hidden)]
pub fn handle_request_fuzz(&self, data: &[u8]) -> ModbusResult<Vec<u8>> {
self.handle_request(data)
}
fn handle_request(&self, data: &[u8]) -> ModbusResult<Vec<u8>> {
use crate::constants::{MAX_PDU_SIZE, MEI_READ_DEVICE_ID};
if data.len() != 3 || data[0] != MEI_READ_DEVICE_ID {
return Err(ModbusError::invalid_data(
"Invalid device identification request",
));
}
let read_code = data[1];
let object_id = data[2];
let selected: Vec<&(u8, Vec<u8>)> = match read_code {
1..=3 => {
let max_id = match read_code {
1 => 0x02,
2 => 0x06,
_ => 0xFF,
};
let start = if self.objects.iter().any(|(id, _)| *id == object_id) {
object_id
} else {
0
};
self.objects
.iter()
.filter(|(id, _)| *id >= start && *id <= max_id)
.collect()
}
4 => match self.objects.iter().find(|(id, _)| *id == object_id) {
Some(object) => vec![object],
None => return Err(ModbusError::invalid_address(u16::from(object_id), 1)),
},
_ => {
return Err(ModbusError::invalid_data(
"Invalid ReadDeviceId code (must be 1-4)",
))
}
};
let mut response = vec![
0x2B,
MEI_READ_DEVICE_ID,
read_code,
self.conformity_level(),
0x00, 0x00, 0x00, ];
let mut count: u8 = 0;
for (id, value) in selected {
if response.len() + 2 + value.len() > MAX_PDU_SIZE {
response[4] = 0xFF; response[5] = *id; break;
}
response.push(*id);
response.push(value.len() as u8);
response.extend_from_slice(value);
count += 1;
}
response[6] = count;
Ok(response)
}
}
struct WithDeviceIdentity {
inner: Arc<dyn ModbusService>,
identity: Arc<DeviceIdentity>,
}
impl ModbusService for WithDeviceIdentity {
fn handle_pdu<'a>(&'a self, function_code: u8, data: &'a [u8]) -> ServiceFuture<'a> {
if function_code == 0x2B {
let result = self.identity.handle_request(data);
Box::pin(async move { result })
} else {
self.inner.handle_pdu(function_code, data)
}
}
}
fn effective_service(
service: &Arc<dyn ModbusService>,
identity: &Option<Arc<DeviceIdentity>>,
) -> Arc<dyn ModbusService> {
match identity {
Some(identity) => Arc::new(WithDeviceIdentity {
inner: service.clone(),
identity: identity.clone(),
}),
None => service.clone(),
}
}
#[derive(Debug, Clone, Default)]
pub struct ServerStats {
pub connections_count: u64,
pub total_requests: u64,
pub successful_requests: u64,
pub failed_requests: u64,
pub bytes_received: u64,
pub bytes_sent: u64,
pub uptime_seconds: u64,
pub register_bank_stats: Option<RegisterBankStats>,
}
#[derive(Debug, Clone)]
pub struct ModbusTcpServerConfig {
pub bind_address: SocketAddr,
pub max_connections: usize,
pub request_timeout: Duration,
pub register_bank: Option<Arc<ModbusRegisterBank>>,
}
impl Default for ModbusTcpServerConfig {
fn default() -> Self {
Self {
bind_address: "127.0.0.1:502".parse().unwrap(),
max_connections: 100,
request_timeout: Duration::from_secs(30),
register_bank: None,
}
}
}
pub struct ModbusTcpServer {
config: ModbusTcpServerConfig,
register_bank: Arc<ModbusRegisterBank>,
service: Arc<dyn ModbusService>,
device_identity: Option<Arc<DeviceIdentity>>,
stats: Arc<Mutex<ServerStats>>,
shutdown_tx: Option<broadcast::Sender<()>>,
is_running: Arc<AtomicBool>,
start_time: Option<std::time::Instant>,
}
impl ModbusTcpServer {
pub fn new(bind_address: &str) -> ModbusResult<Self> {
let addr = bind_address
.parse()
.map_err(|e| ModbusError::invalid_data(format!("Invalid bind address: {}", e)))?;
let config = ModbusTcpServerConfig {
bind_address: addr,
..Default::default()
};
Self::with_config(config)
}
pub fn with_config(config: ModbusTcpServerConfig) -> ModbusResult<Self> {
let register_bank = config
.register_bank
.clone()
.unwrap_or_else(|| Arc::new(ModbusRegisterBank::new()));
Ok(Self {
config,
service: register_bank.clone(),
register_bank,
device_identity: None,
stats: Arc::new(Mutex::new(ServerStats::default())),
shutdown_tx: None,
is_running: Arc::new(AtomicBool::new(false)),
start_time: None,
})
}
pub fn set_register_bank(&mut self, register_bank: Arc<ModbusRegisterBank>) {
self.service = register_bank.clone();
self.register_bank = register_bank;
}
pub fn set_service(&mut self, service: Arc<dyn ModbusService>) {
self.service = service;
}
pub fn set_device_identity(&mut self, identity: DeviceIdentity) {
self.device_identity = Some(Arc::new(identity));
}
async fn handle_client(
mut stream: TcpStream,
service: Arc<dyn ModbusService>,
stats: Arc<Mutex<ServerStats>>,
mut shutdown_rx: broadcast::Receiver<()>,
request_timeout: Duration,
) {
let peer_addr = stream
.peer_addr()
.map(|addr| addr.to_string())
.unwrap_or_else(|_| "unknown".to_string());
info!("📡 New client connected: {}", peer_addr);
if let Ok(mut stats) = stats.lock() {
stats.connections_count += 1;
}
loop {
tokio::select! {
_ = shutdown_rx.recv() => {
debug!("Shutdown signal received for client {}", peer_addr);
break;
}
result = timeout(request_timeout, Self::read_tcp_frame(&mut stream)) => {
match result {
Ok(Ok(frame)) => {
if let Ok(mut stats) = stats.lock() {
stats.total_requests += 1;
stats.bytes_received += frame.len() as u64;
}
match Self::handle_request(&frame, service.as_ref()).await {
Ok(response_data) => {
if let Err(e) = stream.write_all(&response_data).await {
error!("Failed to send response to {}: {}", peer_addr, e);
break;
} else {
if let Ok(mut stats) = stats.lock() {
stats.successful_requests += 1;
stats.bytes_sent += response_data.len() as u64;
}
}
}
Err(e) => {
error!("Error processing request from {}: {}", peer_addr, e);
let exception_code = Self::exception_code_for_error(&e);
if let Ok(error_response) =
Self::create_error_response(&frame, exception_code)
{
let _ = stream.write_all(&error_response).await;
if let Ok(mut stats) = stats.lock() {
stats.bytes_sent += error_response.len() as u64;
}
}
if let Ok(mut stats) = stats.lock() {
stats.failed_requests += 1;
}
}
}
}
Ok(Err(e)) => {
error!("Read error from {}: {}", peer_addr, e);
break;
}
Err(_) => {
warn!("Read timeout from {}", peer_addr);
break;
}
}
}
}
}
info!("🔌 Client {} disconnected", peer_addr);
}
async fn read_tcp_frame(stream: &mut TcpStream) -> ModbusResult<Vec<u8>> {
let mut header = [0u8; MBAP_HEADER_SIZE];
stream.read_exact(&mut header).await?;
let protocol_id = u16::from_be_bytes([header[2], header[3]]);
if protocol_id != 0 {
return Err(ModbusError::frame(format!(
"Invalid protocol ID: {:04X}",
protocol_id
)));
}
let length = u16::from_be_bytes([header[4], header[5]]);
if !(2..=254).contains(&length) {
return Err(ModbusError::frame(format!(
"Invalid MBAP length: {} (must be 2-254)",
length
)));
}
let total_len = MBAP_HEADER_SIZE + usize::from(length);
if total_len > MAX_TCP_FRAME_SIZE {
return Err(ModbusError::frame("TCP frame too large"));
}
let mut frame = vec![0u8; total_len];
frame[..MBAP_HEADER_SIZE].copy_from_slice(&header);
stream.read_exact(&mut frame[MBAP_HEADER_SIZE..]).await?;
Ok(frame)
}
async fn handle_request(data: &[u8], service: &dyn ModbusService) -> ModbusResult<Vec<u8>> {
if data.len() < MBAP_HEADER_SIZE + 2 {
return Err(ModbusError::frame("Invalid TCP frame length"));
}
let transaction_id = u16::from_be_bytes([data[0], data[1]]);
let protocol_id = u16::from_be_bytes([data[2], data[3]]);
if protocol_id != 0 {
return Err(ModbusError::frame(format!(
"Invalid protocol ID: {:04X}",
protocol_id
)));
}
let length = u16::from_be_bytes([data[4], data[5]]);
let expected_len = MBAP_HEADER_SIZE + usize::from(length);
if !(2..=254).contains(&length) || data.len() != expected_len {
return Err(ModbusError::frame("Invalid TCP frame length"));
}
let unit_id = data[6];
let function_code = data[7];
let pdu_data = &data[8..];
debug!("Processing function code: 0x{:02X}", function_code);
let pdu_response = service.handle_pdu(function_code, pdu_data).await?;
Self::create_success_response(transaction_id, unit_id, &pdu_response)
}
fn create_success_response(
transaction_id: u16,
unit_id: u8,
pdu_response: &[u8],
) -> ModbusResult<Vec<u8>> {
let length = u16::try_from(1 + pdu_response.len())
.map_err(|_| ModbusError::frame("TCP response too large"))?;
if length > 254 {
return Err(ModbusError::frame("TCP response too large"));
}
let mut response = Vec::with_capacity(MBAP_HEADER_SIZE + usize::from(length));
response.extend_from_slice(&transaction_id.to_be_bytes());
response.extend_from_slice(&0u16.to_be_bytes());
response.extend_from_slice(&length.to_be_bytes());
response.push(unit_id);
response.extend_from_slice(pdu_response);
Ok(response)
}
fn exception_code_for_error(error: &ModbusError) -> u8 {
match error {
ModbusError::InvalidFunction { .. } => 0x01,
ModbusError::InvalidAddress { .. } => 0x02,
ModbusError::InvalidData { .. } | ModbusError::Frame { .. } => 0x03,
_ => 0x04,
}
}
async fn handle_read_01(
data: &[u8],
register_bank: &ModbusRegisterBank,
) -> ModbusResult<Vec<u8>> {
if data.len() != 4 {
return Err(ModbusError::frame("Invalid read coils request"));
}
let address = u16::from_be_bytes([data[0], data[1]]);
let quantity = u16::from_be_bytes([data[2], data[3]]);
if quantity == 0 || usize::from(quantity) > MAX_READ_COILS {
return Err(ModbusError::invalid_data("Invalid read coils quantity"));
}
let coils = register_bank.read_01(address, quantity)?;
let byte_count = (quantity as usize).div_ceil(8);
let mut response = Vec::with_capacity(2 + byte_count);
response.push(0x01);
response.push(
u8::try_from(byte_count)
.map_err(|_| ModbusError::invalid_data("Read coils response too large"))?,
);
for chunk in coils.chunks(8) {
let mut byte = 0u8;
for (i, &coil) in chunk.iter().enumerate() {
if coil {
byte |= 1 << i;
}
}
response.push(byte);
}
Ok(response)
}
async fn handle_read_02(
data: &[u8],
register_bank: &ModbusRegisterBank,
) -> ModbusResult<Vec<u8>> {
if data.len() != 4 {
return Err(ModbusError::frame("Invalid read discrete inputs request"));
}
let address = u16::from_be_bytes([data[0], data[1]]);
let quantity = u16::from_be_bytes([data[2], data[3]]);
if quantity == 0 || usize::from(quantity) > MAX_READ_COILS {
return Err(ModbusError::invalid_data(
"Invalid read discrete inputs quantity",
));
}
let inputs = register_bank.read_02(address, quantity)?;
let byte_count = (quantity as usize).div_ceil(8);
let mut response = Vec::with_capacity(2 + byte_count);
response.push(0x02);
response.push(
u8::try_from(byte_count).map_err(|_| {
ModbusError::invalid_data("Read discrete inputs response too large")
})?,
);
for chunk in inputs.chunks(8) {
let mut byte = 0u8;
for (i, &input) in chunk.iter().enumerate() {
if input {
byte |= 1 << i;
}
}
response.push(byte);
}
Ok(response)
}
async fn handle_read_03(
data: &[u8],
register_bank: &ModbusRegisterBank,
) -> ModbusResult<Vec<u8>> {
if data.len() != 4 {
return Err(ModbusError::frame("invalid frame"));
}
let address = u16::from_be_bytes([data[0], data[1]]);
let quantity = u16::from_be_bytes([data[2], data[3]]);
if quantity == 0 || usize::from(quantity) > MAX_READ_REGISTERS {
return Err(ModbusError::invalid_data(
"Invalid read holding registers quantity",
));
}
let registers = register_bank.read_03(address, quantity)?;
let data_len = (quantity as usize) * 2;
let mut response = Vec::with_capacity(2 + data_len);
response.push(0x03);
response.push(
u8::try_from(data_len).map_err(|_| {
ModbusError::invalid_data("Read holding registers response too large")
})?,
);
for ®ister in ®isters {
response.extend_from_slice(®ister.to_be_bytes());
}
Ok(response)
}
async fn handle_read_04(
data: &[u8],
register_bank: &ModbusRegisterBank,
) -> ModbusResult<Vec<u8>> {
if data.len() != 4 {
return Err(ModbusError::frame("invalid frame"));
}
let address = u16::from_be_bytes([data[0], data[1]]);
let quantity = u16::from_be_bytes([data[2], data[3]]);
if quantity == 0 || usize::from(quantity) > MAX_READ_REGISTERS {
return Err(ModbusError::invalid_data(
"Invalid read input registers quantity",
));
}
let registers = register_bank.read_04(address, quantity)?;
let data_len = (quantity as usize) * 2;
let mut response = Vec::with_capacity(2 + data_len);
response.push(0x04);
response.push(
u8::try_from(data_len).map_err(|_| {
ModbusError::invalid_data("Read input registers response too large")
})?,
);
for ®ister in ®isters {
response.extend_from_slice(®ister.to_be_bytes());
}
Ok(response)
}
async fn handle_write_05(
data: &[u8],
register_bank: &ModbusRegisterBank,
) -> ModbusResult<Vec<u8>> {
if data.len() != 4 {
return Err(ModbusError::frame("invalid frame"));
}
let address = u16::from_be_bytes([data[0], data[1]]);
let value_bytes = u16::from_be_bytes([data[2], data[3]]);
if value_bytes != 0xFF00 && value_bytes != 0x0000 {
return Err(ModbusError::invalid_data("Invalid coil write value"));
}
let coil_value = value_bytes == 0xFF00;
register_bank.write_05(address, coil_value)?;
let mut response = vec![0x05];
response.extend_from_slice(&address.to_be_bytes());
response.extend_from_slice(&value_bytes.to_be_bytes());
Ok(response)
}
async fn handle_write_06(
data: &[u8],
register_bank: &ModbusRegisterBank,
) -> ModbusResult<Vec<u8>> {
if data.len() != 4 {
return Err(ModbusError::frame("invalid frame"));
}
let address = u16::from_be_bytes([data[0], data[1]]);
let value = u16::from_be_bytes([data[2], data[3]]);
register_bank.write_06(address, value)?;
let mut response = vec![0x06];
response.extend_from_slice(&address.to_be_bytes());
response.extend_from_slice(&value.to_be_bytes());
Ok(response)
}
async fn handle_write_0f(
data: &[u8],
register_bank: &ModbusRegisterBank,
) -> ModbusResult<Vec<u8>> {
if data.len() < 5 {
return Err(ModbusError::frame("invalid frame"));
}
let address = u16::from_be_bytes([data[0], data[1]]);
let quantity = u16::from_be_bytes([data[2], data[3]]);
let byte_count = data[4] as usize;
if quantity == 0 || usize::from(quantity) > MAX_WRITE_COILS {
return Err(ModbusError::invalid_data(
"Invalid write multiple coils quantity",
));
}
if byte_count != usize::from(quantity).div_ceil(8) {
return Err(ModbusError::frame("invalid frame"));
}
if data.len() != 5 + byte_count {
return Err(ModbusError::frame("invalid frame"));
}
let mut coils = Vec::with_capacity(quantity as usize);
for i in 0..quantity {
let byte_index = 5 + (i / 8) as usize;
let bit_index = i % 8;
let bit_value = (data[byte_index] & (1 << bit_index)) != 0;
coils.push(bit_value);
}
register_bank.write_0f(address, &coils)?;
let mut response = Vec::with_capacity(5);
response.push(0x0F);
response.extend_from_slice(&address.to_be_bytes());
response.extend_from_slice(&quantity.to_be_bytes());
Ok(response)
}
async fn handle_write_10(
data: &[u8],
register_bank: &ModbusRegisterBank,
) -> ModbusResult<Vec<u8>> {
if data.len() < 5 {
return Err(ModbusError::frame("invalid frame"));
}
let address = u16::from_be_bytes([data[0], data[1]]);
let quantity = u16::from_be_bytes([data[2], data[3]]);
let byte_count = data[4] as usize;
if quantity == 0 || usize::from(quantity) > MAX_WRITE_REGISTERS {
return Err(ModbusError::invalid_data(
"Invalid write multiple registers quantity",
));
}
if data.len() != 5 + byte_count || byte_count != (quantity as usize * 2) {
return Err(ModbusError::frame("invalid frame"));
}
let mut registers = Vec::with_capacity(quantity as usize);
for i in 0..quantity {
let byte_offset = 5 + (i as usize * 2);
let value = u16::from_be_bytes([data[byte_offset], data[byte_offset + 1]]);
registers.push(value);
}
register_bank.write_10(address, ®isters)?;
let mut response = Vec::with_capacity(5);
response.push(0x10);
response.extend_from_slice(&address.to_be_bytes());
response.extend_from_slice(&quantity.to_be_bytes());
Ok(response)
}
async fn handle_write_16(
data: &[u8],
register_bank: &ModbusRegisterBank,
) -> ModbusResult<Vec<u8>> {
if data.len() != 6 {
return Err(ModbusError::frame("invalid frame"));
}
let address = u16::from_be_bytes([data[0], data[1]]);
let and_mask = u16::from_be_bytes([data[2], data[3]]);
let or_mask = u16::from_be_bytes([data[4], data[5]]);
let current = register_bank.read_03(address, 1)?[0];
let result = (current & and_mask) | (or_mask & !and_mask);
register_bank.write_06(address, result)?;
let mut response = Vec::with_capacity(7);
response.push(0x16);
response.extend_from_slice(data);
Ok(response)
}
async fn handle_read_write_17(
data: &[u8],
register_bank: &ModbusRegisterBank,
) -> ModbusResult<Vec<u8>> {
if data.len() < 9 {
return Err(ModbusError::frame("invalid frame"));
}
let read_address = u16::from_be_bytes([data[0], data[1]]);
let read_quantity = u16::from_be_bytes([data[2], data[3]]);
let write_address = u16::from_be_bytes([data[4], data[5]]);
let write_quantity = u16::from_be_bytes([data[6], data[7]]);
let byte_count = usize::from(data[8]);
if read_quantity == 0 || usize::from(read_quantity) > MAX_RW_READ_REGISTERS {
return Err(ModbusError::invalid_data(
"Invalid read/write multiple read quantity",
));
}
if write_quantity == 0 || usize::from(write_quantity) > MAX_RW_WRITE_REGISTERS {
return Err(ModbusError::invalid_data(
"Invalid read/write multiple write quantity",
));
}
if byte_count != usize::from(write_quantity) * 2 || data.len() != 9 + byte_count {
return Err(ModbusError::frame("invalid frame"));
}
let values: Vec<u16> = data[9..]
.chunks_exact(2)
.map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
.collect();
register_bank.write_10(write_address, &values)?;
let registers = register_bank.read_03(read_address, read_quantity)?;
let data_len = registers.len() * 2;
let mut response = Vec::with_capacity(2 + data_len);
response.push(0x17);
response.push(
u8::try_from(data_len)
.map_err(|_| ModbusError::invalid_data("Read/write response too large"))?,
);
for ®ister in ®isters {
response.extend_from_slice(®ister.to_be_bytes());
}
Ok(response)
}
fn create_error_response(request: &[u8], exception_code: u8) -> ModbusResult<Vec<u8>> {
if request.len() < MBAP_HEADER_SIZE + 2 {
return Err(ModbusError::frame("Request too short for error response"));
}
let transaction_id = u16::from_be_bytes([request[0], request[1]]);
let protocol_id = 0u16;
let length = 3u16; let unit_id = request[6];
let function_code = request[7] | 0x80;
let mut response = Vec::with_capacity(MBAP_HEADER_SIZE + 3);
response.extend_from_slice(&transaction_id.to_be_bytes());
response.extend_from_slice(&protocol_id.to_be_bytes());
response.extend_from_slice(&length.to_be_bytes());
response.push(unit_id);
response.push(function_code);
response.push(exception_code);
Ok(response)
}
}
impl ModbusServer for ModbusTcpServer {
async fn start(&mut self) -> ModbusResult<()> {
if self.is_running.load(Ordering::Relaxed) {
return Err(ModbusError::protocol("Server is already running"));
}
info!(
"🚀 Starting Modbus TCP server on {}",
self.config.bind_address
);
let listener = TcpListener::bind(self.config.bind_address)
.await
.map_err(|e| {
ModbusError::connection(format!(
"Failed to bind to {}: {}",
self.config.bind_address, e
))
})?;
let (shutdown_tx, _) = broadcast::channel(1);
self.shutdown_tx = Some(shutdown_tx.clone());
self.start_time = Some(std::time::Instant::now());
info!("✅ Modbus TCP server started successfully");
info!("📊 Server configuration:");
info!(" - Bind address: {}", self.config.bind_address);
info!(" - Max connections: {}", self.config.max_connections);
info!(" - Request timeout: {:?}", self.config.request_timeout);
let service = effective_service(&self.service, &self.device_identity);
let stats = self.stats.clone();
let request_timeout = self.config.request_timeout;
let connection_limit = Arc::new(Semaphore::new(self.config.max_connections));
let is_running_flag = self.is_running.clone();
let mut shutdown_rx = shutdown_tx.subscribe();
self.is_running.store(true, Ordering::Relaxed);
tokio::spawn(async move {
loop {
tokio::select! {
result = listener.accept() => {
match result {
Ok((stream, addr)) => {
debug!("Accepted connection from {}", addr);
let permit = match connection_limit.clone().try_acquire_owned() {
Ok(permit) => permit,
Err(_) => {
warn!("Rejecting {}: max connections reached", addr);
continue;
}
};
let service = service.clone();
let stats = stats.clone();
let shutdown_rx = shutdown_tx.subscribe();
tokio::spawn(async move {
let _permit = permit;
Self::handle_client(stream, service, stats, shutdown_rx, request_timeout).await;
});
}
Err(e) => {
error!("Failed to accept connection: {}", e);
}
}
}
_ = shutdown_rx.recv() => {
info!("Shutdown signal received, stopping server");
break;
}
}
}
is_running_flag.store(false, Ordering::Relaxed);
});
Ok(())
}
async fn stop(&mut self) -> ModbusResult<()> {
if let Some(shutdown_tx) = &self.shutdown_tx {
let _ = shutdown_tx.send(());
}
self.is_running.store(false, Ordering::Relaxed);
info!("⏹️ Modbus TCP server stopped");
Ok(())
}
fn is_running(&self) -> bool {
self.is_running.load(Ordering::Relaxed)
}
fn get_stats(&self) -> ServerStats {
let mut stats = self
.stats
.lock()
.map(|stats| stats.clone())
.unwrap_or_default();
if let Some(start_time) = self.start_time {
stats.uptime_seconds = start_time.elapsed().as_secs();
}
stats.register_bank_stats = Some(self.register_bank.get_stats());
stats
}
fn get_register_bank(&self) -> Option<Arc<ModbusRegisterBank>> {
Some(self.register_bank.clone())
}
}
#[cfg(feature = "rtu")]
#[derive(Debug, Clone)]
pub struct ModbusRtuServerConfig {
pub port: String,
pub baud_rate: u32,
pub slave_id: u8,
pub data_bits: tokio_serial::DataBits,
pub stop_bits: tokio_serial::StopBits,
pub parity: tokio_serial::Parity,
pub timeout: Duration,
pub frame_gap: Duration,
pub register_bank: Option<Arc<ModbusRegisterBank>>,
}
#[cfg(feature = "rtu")]
impl Default for ModbusRtuServerConfig {
fn default() -> Self {
Self {
port: "/dev/ttyUSB0".to_string(),
baud_rate: 9600,
slave_id: 1,
data_bits: tokio_serial::DataBits::Eight,
stop_bits: tokio_serial::StopBits::One,
parity: tokio_serial::Parity::None,
timeout: Duration::from_secs(1),
frame_gap: Duration::from_millis(4), register_bank: None,
}
}
}
#[cfg(feature = "rtu")]
pub struct ModbusRtuServer {
config: ModbusRtuServerConfig,
register_bank: Arc<ModbusRegisterBank>,
service: Arc<dyn ModbusService>,
device_identity: Option<Arc<DeviceIdentity>>,
stats: Arc<Mutex<ServerStats>>,
shutdown_tx: Option<broadcast::Sender<()>>,
is_running: Arc<AtomicBool>,
start_time: Option<std::time::Instant>,
}
#[cfg(feature = "rtu")]
impl ModbusRtuServer {
pub fn new(port: &str, baud_rate: u32) -> ModbusResult<Self> {
let config = ModbusRtuServerConfig {
port: port.to_string(),
baud_rate,
frame_gap: crate::transport::rtu_frame_gap(baud_rate),
..Default::default()
};
Self::with_config(config)
}
pub fn with_config(config: ModbusRtuServerConfig) -> ModbusResult<Self> {
let register_bank = config
.register_bank
.clone()
.unwrap_or_else(|| Arc::new(ModbusRegisterBank::new()));
Ok(Self {
config,
service: register_bank.clone(),
register_bank,
device_identity: None,
stats: Arc::new(Mutex::new(ServerStats::default())),
shutdown_tx: None,
is_running: Arc::new(AtomicBool::new(false)),
start_time: None,
})
}
pub fn set_register_bank(&mut self, register_bank: Arc<ModbusRegisterBank>) {
self.service = register_bank.clone();
self.register_bank = register_bank;
}
pub fn set_service(&mut self, service: Arc<dyn ModbusService>) {
self.service = service;
}
pub fn set_device_identity(&mut self, identity: DeviceIdentity) {
self.device_identity = Some(Arc::new(identity));
}
fn calculate_crc(data: &[u8]) -> u16 {
use crc::{Crc, CRC_16_MODBUS};
const CRC_MODBUS: Crc<u16> = Crc::<u16>::new(&CRC_16_MODBUS);
CRC_MODBUS.checksum(data)
}
async fn process_frame(
frame: &[u8],
own_slave_id: u8,
service: &dyn ModbusService,
) -> Option<Vec<u8>> {
if frame.len() < 4 {
return None;
}
let crc_split = frame.len() - 2;
let received_crc = u16::from_le_bytes([frame[crc_split], frame[crc_split + 1]]);
let calculated_crc = Self::calculate_crc(&frame[..crc_split]);
if received_crc != calculated_crc {
warn!(
"Ignoring RTU frame with bad CRC: expected {:04X}, got {:04X}",
calculated_crc, received_crc
);
return None;
}
let slave_id = frame[0];
if slave_id != 0 && slave_id != own_slave_id {
return None;
}
let function_code = frame[1];
let pdu_data = &frame[2..crc_split];
let result = service.handle_pdu(function_code, pdu_data).await;
if slave_id == 0 {
return None;
}
match result {
Ok(pdu) => {
let mut response = Vec::with_capacity(1 + pdu.len() + 2);
response.push(slave_id);
response.extend_from_slice(&pdu);
let crc = Self::calculate_crc(&response);
response.extend_from_slice(&crc.to_le_bytes());
Some(response)
}
Err(e) => {
let exception_code = ModbusTcpServer::exception_code_for_error(&e);
Self::create_rtu_error_response(slave_id, function_code, exception_code).ok()
}
}
}
#[doc(hidden)]
pub async fn process_frame_fuzz(frame: &[u8], own_slave_id: u8) -> Option<Vec<u8>> {
let bank = ModbusRegisterBank::new();
Self::process_frame(frame, own_slave_id, &bank).await
}
fn create_rtu_error_response(
slave_id: u8,
function_code: u8,
exception_code: u8,
) -> ModbusResult<Vec<u8>> {
let mut response = Vec::new();
response.push(slave_id);
response.push(function_code | 0x80); response.push(exception_code);
let crc = Self::calculate_crc(&response);
response.extend_from_slice(&crc.to_le_bytes());
Ok(response)
}
async fn handle_rtu_communication(
mut port: tokio_serial::SerialStream,
own_slave_id: u8,
service: Arc<dyn ModbusService>,
stats: Arc<Mutex<ServerStats>>,
mut shutdown_rx: broadcast::Receiver<()>,
frame_gap: Duration,
) {
info!("🔌 RTU server communication started");
let mut buffer = vec![0u8; 256];
let mut frame_buffer = Vec::new();
let mut last_activity = std::time::Instant::now();
loop {
tokio::select! {
_ = shutdown_rx.recv() => {
debug!("Shutdown signal received for RTU server");
break;
}
result = tokio::time::timeout(Duration::from_millis(100), port.read(&mut buffer)) => {
match result {
Ok(Ok(bytes_read)) if bytes_read > 0 => {
let now = std::time::Instant::now();
if now.duration_since(last_activity) > frame_gap && !frame_buffer.is_empty() {
Self::process_accumulated_frame(
&frame_buffer,
&mut port,
own_slave_id,
service.as_ref(),
&stats
).await;
frame_buffer.clear();
}
frame_buffer.extend_from_slice(&buffer[..bytes_read]);
last_activity = now;
if let Ok(mut stats) = stats.lock() {
stats.bytes_received += bytes_read as u64;
}
}
Ok(Ok(_)) => {
}
Ok(Err(e)) => {
error!("RTU read error: {}", e);
break;
}
Err(_) => {
let now = std::time::Instant::now();
if !frame_buffer.is_empty() && now.duration_since(last_activity) > frame_gap {
Self::process_accumulated_frame(
&frame_buffer,
&mut port,
own_slave_id,
service.as_ref(),
&stats
).await;
frame_buffer.clear();
}
}
}
}
}
}
info!("🔌 RTU server communication stopped");
}
async fn process_accumulated_frame(
frame: &[u8],
port: &mut tokio_serial::SerialStream,
own_slave_id: u8,
service: &dyn ModbusService,
stats: &Arc<Mutex<ServerStats>>,
) {
if let Ok(mut stats) = stats.lock() {
stats.total_requests += 1;
}
let Some(response) = Self::process_frame(frame, own_slave_id, service).await else {
return;
};
if let Err(e) = port.write_all(&response).await {
error!("Failed to write response: {}", e);
if let Ok(mut stats) = stats.lock() {
stats.failed_requests += 1;
}
} else if let Ok(mut stats) = stats.lock() {
stats.successful_requests += 1;
stats.bytes_sent += response.len() as u64;
}
}
}
#[cfg(feature = "rtu")]
impl ModbusServer for ModbusRtuServer {
async fn start(&mut self) -> ModbusResult<()> {
if self.is_running.load(Ordering::Relaxed) {
return Err(ModbusError::protocol("RTU Server is already running"));
}
info!("🚀 Starting Modbus RTU server on {}", self.config.port);
let port = tokio_serial::SerialStream::open(
&tokio_serial::new(&self.config.port, self.config.baud_rate)
.data_bits(self.config.data_bits)
.stop_bits(self.config.stop_bits)
.parity(self.config.parity)
.timeout(self.config.timeout),
)
.map_err(|e| {
ModbusError::connection(format!(
"Failed to open serial port {}: {}",
self.config.port, e
))
})?;
let (shutdown_tx, _) = broadcast::channel(1);
self.shutdown_tx = Some(shutdown_tx.clone());
self.start_time = Some(std::time::Instant::now());
self.is_running.store(true, Ordering::Relaxed);
info!("✅ Modbus RTU server started successfully");
info!("📊 Server configuration:");
info!(" - Port: {}", self.config.port);
info!(" - Baud rate: {}", self.config.baud_rate);
info!(" - Slave ID: {}", self.config.slave_id);
info!(" - Data bits: {:?}", self.config.data_bits);
info!(" - Stop bits: {:?}", self.config.stop_bits);
info!(" - Parity: {:?}", self.config.parity);
info!(" - Timeout: {:?}", self.config.timeout);
let service = effective_service(&self.service, &self.device_identity);
let stats = self.stats.clone();
let frame_gap = self.config.frame_gap;
let own_slave_id = self.config.slave_id;
let is_running_flag = self.is_running.clone();
let shutdown_rx = shutdown_tx.subscribe();
tokio::spawn(async move {
Self::handle_rtu_communication(
port,
own_slave_id,
service,
stats,
shutdown_rx,
frame_gap,
)
.await;
is_running_flag.store(false, Ordering::Relaxed);
});
Ok(())
}
async fn stop(&mut self) -> ModbusResult<()> {
if let Some(shutdown_tx) = &self.shutdown_tx {
let _ = shutdown_tx.send(());
}
self.is_running.store(false, Ordering::Relaxed);
info!("⏹️ Modbus RTU server stopped");
Ok(())
}
fn is_running(&self) -> bool {
self.is_running.load(Ordering::Relaxed)
}
fn get_stats(&self) -> ServerStats {
let mut stats = self
.stats
.lock()
.map(|stats| stats.clone())
.unwrap_or_default();
if let Some(start_time) = self.start_time {
stats.uptime_seconds = start_time.elapsed().as_secs();
}
stats.register_bank_stats = Some(self.register_bank.get_stats());
stats
}
fn get_register_bank(&self) -> Option<Arc<ModbusRegisterBank>> {
Some(self.register_bank.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "rtu")]
use std::time::Duration;
#[test]
fn test_tcp_server_creation() {
let result = ModbusTcpServer::new("127.0.0.1:5020");
assert!(result.is_ok());
let server = result.unwrap();
assert!(!server.is_running());
assert!(server.get_register_bank().is_some());
}
#[tokio::test]
async fn test_tcp_handle_request_returns_complete_mbap_frame() {
let register_bank = Arc::new(ModbusRegisterBank::new());
register_bank.write_06(0, 0x1234).unwrap();
let request = [
0x12, 0x34, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03, 0x00, 0x00, 0x00, 0x01, ];
let response = ModbusTcpServer::handle_request(&request, register_bank.as_ref())
.await
.unwrap();
assert_eq!(
response,
vec![
0x12, 0x34, 0x00, 0x00, 0x00, 0x05, 0x01, 0x03, 0x02, 0x12, 0x34,
]
);
}
#[tokio::test]
async fn test_tcp_handle_request_rejects_read_trailing_bytes() {
let register_bank = Arc::new(ModbusRegisterBank::new());
let request = [
0x12, 0x34, 0x00, 0x00, 0x00, 0x07, 0x01, 0x03, 0x00, 0x00, 0x00, 0x01, 0x99, ];
let err = ModbusTcpServer::handle_request(&request, register_bank.as_ref())
.await
.unwrap_err();
assert!(matches!(err, ModbusError::Frame { .. }));
}
#[tokio::test]
async fn test_tcp_handle_request_rejects_write_trailing_bytes() {
let register_bank = Arc::new(ModbusRegisterBank::new());
let request = [
0x12, 0x34, 0x00, 0x00, 0x00, 0x0A, 0x01, 0x10, 0x00, 0x00, 0x00, 0x01, 0x02, 0x12, 0x34, 0x99, ];
let err = ModbusTcpServer::handle_request(&request, register_bank.as_ref())
.await
.unwrap_err();
assert!(matches!(err, ModbusError::Frame { .. }));
}
#[cfg(feature = "rtu")]
#[test]
fn test_rtu_server_creation() {
let result = ModbusRtuServer::new("/dev/ttyUSB0", 9600);
assert!(result.is_ok());
let server = result.unwrap();
assert!(!server.is_running());
assert!(server.get_register_bank().is_some());
}
#[cfg(feature = "rtu")]
#[test]
fn test_rtu_server_configuration() {
let config = ModbusRtuServerConfig {
port: "/dev/ttyUSB0".to_string(),
baud_rate: 19200,
slave_id: 17,
data_bits: tokio_serial::DataBits::Eight,
stop_bits: tokio_serial::StopBits::Two,
parity: tokio_serial::Parity::Even,
timeout: Duration::from_secs(2),
frame_gap: Duration::from_millis(5),
register_bank: None,
};
let result = ModbusRtuServer::with_config(config);
assert!(result.is_ok());
let server = result.unwrap();
assert!(!server.is_running());
}
#[cfg(feature = "rtu")]
#[tokio::test]
async fn test_rtu_server_lifecycle() {
let mut server = ModbusRtuServer::new("/dev/ttyUSB0", 9600).unwrap();
assert!(!server.is_running());
let start_result = server.start().await;
if start_result.is_ok() {
tokio::time::sleep(Duration::from_millis(10)).await;
let stop_result = server.stop().await;
assert!(stop_result.is_ok());
} else {
println!(
"RTU server start failed (expected without serial port): {:?}",
start_result.err()
);
}
}
#[cfg(feature = "rtu")]
#[test]
fn test_crc_calculation() {
let test_data = vec![0x01, 0x03, 0x00, 0x00, 0x00, 0x02];
let crc = ModbusRtuServer::calculate_crc(&test_data);
assert_eq!(crc, ModbusRtuServer::calculate_crc(&test_data));
let test_data2 = vec![0x01, 0x04, 0x00, 0x00, 0x00, 0x01];
let crc2 = ModbusRtuServer::calculate_crc(&test_data2);
assert_ne!(crc, crc2);
}
#[tokio::test]
async fn test_tcp_handle_request_mask_write() {
let register_bank = Arc::new(ModbusRegisterBank::new());
register_bank.write_06(4, 0x0012).unwrap();
let request = [
0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x01, 0x16, 0x00, 0x04, 0x00, 0xF2, 0x00, 0x25,
];
let response = ModbusTcpServer::handle_request(&request, register_bank.as_ref())
.await
.unwrap();
assert_eq!(&response[7..], &[0x16, 0x00, 0x04, 0x00, 0xF2, 0x00, 0x25]);
assert_eq!(register_bank.read_03(4, 1).unwrap(), vec![0x0017]);
}
#[tokio::test]
async fn test_tcp_handle_request_read_write_multiple() {
let register_bank = Arc::new(ModbusRegisterBank::new());
register_bank.write_06(0, 0x0AAA).unwrap();
let request = [
0x00, 0x01, 0x00, 0x00, 0x00, 0x0D, 0x01, 0x17, 0x00, 0x00, 0x00, 0x01, 0x00, 0x0A, 0x00, 0x01, 0x02, 0x12, 0x34, ];
let response = ModbusTcpServer::handle_request(&request, register_bank.as_ref())
.await
.unwrap();
assert_eq!(&response[7..], &[0x17, 0x02, 0x0A, 0xAA]);
assert_eq!(register_bank.read_03(10, 1).unwrap(), vec![0x1234]);
}
struct FixedService;
impl ModbusService for FixedService {
fn handle_pdu<'a>(&'a self, function_code: u8, _data: &'a [u8]) -> ServiceFuture<'a> {
Box::pin(async move {
match function_code {
0x03 => Ok(vec![0x03, 0x02, 0xBE, 0xEF]),
other => Err(ModbusError::invalid_function(other)),
}
})
}
}
#[tokio::test]
async fn test_custom_service_backs_tcp_dispatch() {
let request = [
0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03, 0x00, 0x00, 0x00, 0x01,
];
let response = ModbusTcpServer::handle_request(&request, &FixedService)
.await
.unwrap();
assert_eq!(&response[7..], &[0x03, 0x02, 0xBE, 0xEF]);
let bad = [
0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x04, 0x00, 0x00, 0x00, 0x01,
];
assert!(ModbusTcpServer::handle_request(&bad, &FixedService)
.await
.is_err());
}
#[tokio::test]
async fn test_tcp_handle_request_diagnostics_echo() {
let register_bank = Arc::new(ModbusRegisterBank::new());
let request = [
0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x08, 0x00, 0x00, 0xA5, 0x37, ];
let response = ModbusTcpServer::handle_request(&request, register_bank.as_ref())
.await
.unwrap();
assert_eq!(&response[7..], &[0x08, 0x00, 0x00, 0xA5, 0x37]);
let bad = [
0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x08, 0x00, 0x01, 0x00, 0x00,
];
assert!(
ModbusTcpServer::handle_request(&bad, register_bank.as_ref())
.await
.is_err()
);
}
#[tokio::test]
async fn test_device_identity_stream_read() {
let identity = DeviceIdentity::basic("VendorX", "PC-1", "V2.1");
let response = identity.handle_request(&[0x0E, 0x01, 0x00]).unwrap();
assert_eq!(&response[..3], &[0x2B, 0x0E, 0x01]);
assert_eq!(response[3], 0x81); assert_eq!(response[4], 0x00); assert_eq!(response[6], 3); assert_eq!(&response[7..9], &[0x00, 7]); assert_eq!(&response[9..16], b"VendorX");
}
#[tokio::test]
async fn test_device_identity_individual_read() {
let identity = DeviceIdentity::basic("VendorX", "PC-1", "V2.1");
let response = identity.handle_request(&[0x0E, 0x04, 0x01]).unwrap();
assert_eq!(response[6], 1);
assert_eq!(&response[7..9], &[0x01, 4]);
assert_eq!(&response[9..13], b"PC-1");
let err = identity.handle_request(&[0x0E, 0x04, 0x77]).unwrap_err();
assert_eq!(ModbusTcpServer::exception_code_for_error(&err), 0x02);
}
#[tokio::test]
async fn test_device_identity_served_through_service_wrapper() {
let bank = Arc::new(ModbusRegisterBank::new());
bank.write_06(0, 0x1234).unwrap();
let service: Arc<dyn ModbusService> = bank.clone();
let identity = Some(Arc::new(DeviceIdentity::basic("V", "P", "1.0")));
let wrapped = effective_service(&service, &identity);
let request = [
0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x01, 0x2B, 0x0E, 0x01, 0x00,
];
let response = ModbusTcpServer::handle_request(&request, wrapped.as_ref())
.await
.unwrap();
assert_eq!(response[7], 0x2B);
assert_eq!(response[13], 3);
let read = [
0x00, 0x02, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03, 0x00, 0x00, 0x00, 0x01,
];
let response = ModbusTcpServer::handle_request(&read, wrapped.as_ref())
.await
.unwrap();
assert_eq!(&response[7..], &[0x03, 0x02, 0x12, 0x34]);
}
#[cfg(feature = "rtu")]
fn rtu_frame(body: &[u8]) -> Vec<u8> {
let mut frame = body.to_vec();
let crc = ModbusRtuServer::calculate_crc(&frame);
frame.extend_from_slice(&crc.to_le_bytes());
frame
}
#[cfg(feature = "rtu")]
#[tokio::test]
async fn test_rtu_process_frame_ignores_short_noise() {
let bank = Arc::new(ModbusRegisterBank::new());
for noise in [
&[][..],
&[0x01][..],
&[0x01, 0x03][..],
&[0x01, 0x03, 0x00][..],
] {
assert!(
ModbusRtuServer::process_frame(noise, 1, bank.as_ref())
.await
.is_none(),
"noise frame {noise:02X?} must be ignored"
);
}
}
#[cfg(feature = "rtu")]
#[tokio::test]
async fn test_rtu_process_frame_ignores_bad_crc() {
let bank = Arc::new(ModbusRegisterBank::new());
let mut frame = rtu_frame(&[0x00, 0x06, 0x00, 0x05, 0xAB, 0xCD]);
let last = frame.len() - 1;
frame[last] ^= 0xFF;
assert!(ModbusRtuServer::process_frame(&frame, 1, bank.as_ref())
.await
.is_none());
assert_eq!(bank.read_03(5, 1).unwrap(), vec![0x0000]);
}
#[cfg(feature = "rtu")]
#[tokio::test]
async fn test_rtu_process_frame_respects_configured_slave_id() {
let bank = Arc::new(ModbusRegisterBank::new());
let frame = rtu_frame(&[0x02, 0x03, 0x00, 0x00, 0x00, 0x01]);
assert!(ModbusRtuServer::process_frame(&frame, 1, bank.as_ref())
.await
.is_none());
assert!(ModbusRtuServer::process_frame(&frame, 2, bank.as_ref())
.await
.is_some());
}
#[cfg(feature = "rtu")]
#[tokio::test]
async fn test_rtu_process_frame_broadcast_write_executes_silently() {
let bank = Arc::new(ModbusRegisterBank::new());
let frame = rtu_frame(&[0x00, 0x06, 0x00, 0x05, 0xAB, 0xCD]);
let response = ModbusRtuServer::process_frame(&frame, 1, bank.as_ref()).await;
assert!(response.is_none(), "broadcast must never be answered");
assert_eq!(bank.read_03(5, 1).unwrap(), vec![0xABCD]);
}
#[cfg(feature = "rtu")]
#[tokio::test]
async fn test_rtu_process_frame_valid_read_response() {
let bank = Arc::new(ModbusRegisterBank::new());
bank.write_06(0, 0x1234).unwrap();
let frame = rtu_frame(&[0x01, 0x03, 0x00, 0x00, 0x00, 0x01]);
let response = ModbusRtuServer::process_frame(&frame, 1, bank.as_ref())
.await
.expect("valid unicast read must be answered");
assert_eq!(&response[..5], &[0x01, 0x03, 0x02, 0x12, 0x34]);
let split = response.len() - 2;
let crc = u16::from_le_bytes([response[split], response[split + 1]]);
assert_eq!(crc, ModbusRtuServer::calculate_crc(&response[..split]));
}
#[cfg(feature = "rtu")]
#[tokio::test]
async fn test_rtu_process_frame_error_yields_exception_response() {
let bank = Arc::new(ModbusRegisterBank::new());
let frame = rtu_frame(&[0x01, 0x03, 0x00, 0x00, 0x00, 0x00]);
let response = ModbusRtuServer::process_frame(&frame, 1, bank.as_ref())
.await
.expect("error must produce an exception response");
assert_eq!(response[0], 0x01); assert_eq!(response[1], 0x83); assert_eq!(response[2], 0x03); let split = response.len() - 2;
let crc = u16::from_le_bytes([response[split], response[split + 1]]);
assert_eq!(crc, ModbusRtuServer::calculate_crc(&response[..split]));
}
#[cfg(feature = "rtu")]
#[test]
fn test_rtu_error_response() {
let result = ModbusRtuServer::create_rtu_error_response(0x01, 0x03, 0x01);
assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response[0], 0x01); assert_eq!(response[1], 0x83); assert_eq!(response[2], 0x01); assert_eq!(response.len(), 5); }
#[tokio::test]
async fn test_server_stats() {
let server = ModbusTcpServer::new("127.0.0.1:5021").unwrap();
let stats = server.get_stats();
assert_eq!(stats.connections_count, 0);
assert_eq!(stats.total_requests, 0);
assert_eq!(stats.successful_requests, 0);
assert_eq!(stats.failed_requests, 0);
}
#[test]
fn test_register_bank_integration() {
let register_bank = Arc::new(ModbusRegisterBank::new());
register_bank.write_05(0, true).unwrap();
register_bank.write_06(0, 0x1234).unwrap();
let mut server = ModbusTcpServer::new("127.0.0.1:5022").unwrap();
server.set_register_bank(register_bank.clone());
let server_bank = server.get_register_bank().unwrap();
let coils = server_bank.read_coils(0, 1).unwrap();
let registers = server_bank.read_holding_registers(0, 1).unwrap();
assert!(coils[0]);
assert_eq!(registers[0], 0x1234);
}
#[tokio::test]
async fn test_register_operations() {
let register_bank = Arc::new(ModbusRegisterBank::new());
register_bank.write_05(0, true).unwrap();
register_bank.write_06(0, 0x1234).unwrap();
let coils = register_bank.read_coils(0, 1).unwrap();
assert_eq!(coils, vec![true]);
let registers = register_bank.read_holding_registers(0, 1).unwrap();
assert_eq!(registers, vec![0x1234]);
}
}