use std::sync::Arc;
use std::time::Duration;
use oms_modbus::codec::calculate_crc;
use oms_modbus::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
fn rtu_frame(slave: u8, pdu: &[u8]) -> Vec<u8> {
let mut data = vec![slave];
data.extend_from_slice(pdu);
let crc = calculate_crc(&data);
data.push(crc as u8);
data.push((crc >> 8) as u8);
data
}
fn parse_rtu_response(raw: &[u8]) -> (u8, Vec<u8>) {
assert!(raw.len() >= 5, "response too short: {} bytes", raw.len());
let crc_received = u16::from_le_bytes([raw[raw.len() - 2], raw[raw.len() - 1]]);
let crc_calc = calculate_crc(&raw[..raw.len() - 2]);
assert_eq!(
crc_received, crc_calc,
"CRC mismatch in response: received {crc_received:#06X}, calculated {crc_calc:#06X}"
);
let frame = &raw[..raw.len() - 2];
(frame[0], frame[1..].to_vec())
}
fn spawn_rtu_server(store: Arc<SlaveStore>) -> tokio::io::DuplexStream {
let (client_stream, server_stream) = tokio::io::duplex(1024);
let server = rtu::RtuServer::new(server_stream);
tokio::spawn(async move {
server.serve_forever(store).await.ok();
});
client_stream
}
#[tokio::test]
async fn serve_forever_read_holding_registers() {
let store = Arc::new(SlaveStore::with_holding_registers(&[(0, 42)]));
let mut stream = spawn_rtu_server(store);
let req = rtu_frame(1, &[0x03, 0x00, 0x00, 0x00, 0x01]);
stream.write_all(&req).await.unwrap();
let mut buf = [0u8; 256];
let n = tokio::time::timeout(Duration::from_secs(2), stream.read(&mut buf))
.await
.unwrap()
.unwrap();
let (slave, pdu) = parse_rtu_response(&buf[..n]);
assert_eq!(slave, 1);
assert_eq!(pdu, vec![0x03, 0x02, 0x00, 0x2A]);
}
#[tokio::test]
async fn serve_forever_write_single_register() {
let store = Arc::new(SlaveStore::with_holding_registers(&[(0, 0)]));
let mut stream = spawn_rtu_server(store.clone());
let req = rtu_frame(1, &[0x06, 0x00, 0x00, 0x03, 0xE7]);
stream.write_all(&req).await.unwrap();
let mut buf = [0u8; 256];
let n = tokio::time::timeout(Duration::from_secs(2), stream.read(&mut buf))
.await
.unwrap()
.unwrap();
let (_slave, pdu) = parse_rtu_response(&buf[..n]);
assert_eq!(pdu, vec![0x06, 0x00, 0x00, 0x03, 0xE7]);
assert_eq!(store.read_holding_register(0), 999);
}
#[tokio::test]
async fn serve_forever_multiple_function_codes() {
let store = Arc::new(SlaveStore::with_holding_registers(&[(0, 100)]));
let mut stream = spawn_rtu_server(store);
for (fc, pdu, expected_fc) in [
(1, vec![0x01, 0x00, 0x00, 0x00, 0x08], 0x01u8),
(2, vec![0x02, 0x00, 0x00, 0x00, 0x08], 0x02u8),
(4, vec![0x04, 0x00, 0x00, 0x00, 0x01], 0x04u8),
(5, vec![0x05, 0x00, 0x00, 0xFF, 0x00], 0x05u8),
(15, vec![0x0F, 0x00, 0x00, 0x00, 0x08, 0x01, 0xFF], 0x0Fu8),
] {
let req = rtu_frame(1, &pdu);
stream.write_all(&req).await.unwrap();
let mut buf = [0u8; 256];
let n = tokio::time::timeout(Duration::from_secs(2), stream.read(&mut buf))
.await
.unwrap()
.unwrap();
let (_slave, rsp_pdu) = parse_rtu_response(&buf[..n]);
assert_eq!(
rsp_pdu[0], expected_fc,
"FC {fc}: expected response FC {expected_fc:#04X}, got {:#04X}",
rsp_pdu[0]
);
}
}
#[tokio::test]
async fn serve_forever_crc_error_silent_discard() {
let store = Arc::new(SlaveStore::with_holding_registers(&[(0, 42)]));
let mut stream = spawn_rtu_server(store);
let pdu = [0x03, 0x00, 0x00, 0x00, 0x01];
let mut bad_frame = vec![1u8];
bad_frame.extend_from_slice(&pdu);
bad_frame.push(0xFF); bad_frame.push(0xFF); stream.write_all(&bad_frame).await.unwrap();
let mut buf = [0u8; 256];
let result = tokio::time::timeout(Duration::from_millis(200), stream.read(&mut buf)).await;
assert!(
result.is_err(),
"CRC error frame must produce no response (timeout)"
);
}
#[tokio::test]
async fn serve_forever_unknown_function_code() {
let store = Arc::new(SlaveStore::new());
let mut stream = spawn_rtu_server(store);
let req = rtu_frame(1, &[0x46, 0x00, 0x00]);
stream.write_all(&req).await.unwrap();
let mut buf = [0u8; 256];
let result = tokio::time::timeout(Duration::from_millis(200), stream.read(&mut buf)).await;
assert!(
result.is_err(),
"unknown FC frame must produce no response (timeout)"
);
}
#[tokio::test]
async fn serve_forever_service_exception() {
let store = Arc::new(SlaveStore::new());
let mut stream = spawn_rtu_server(store);
let req = rtu_frame(1, &[0x08, 0x00, 0xFF, 0x00, 0x00]);
stream.write_all(&req).await.unwrap();
let mut buf = [0u8; 256];
let n = tokio::time::timeout(Duration::from_secs(2), stream.read(&mut buf))
.await
.unwrap()
.unwrap();
let (_slave, pdu) = parse_rtu_response(&buf[..n]);
assert_eq!(
pdu[0], 0x88,
"expected exception FC 0x88, got {:#04X}",
pdu[0]
);
assert_eq!(
pdu[1], 0x03,
"expected IllegalDataValue (0x03), got {:#04X}",
pdu[1]
);
}
#[tokio::test]
async fn serve_forever_fragmented_frame_arrival() {
let store = Arc::new(SlaveStore::with_holding_registers(&[(0, 77)]));
let mut stream = spawn_rtu_server(store);
let frame = rtu_frame(1, &[0x03, 0x00, 0x00, 0x00, 0x01]);
for &byte in &frame {
stream.write_all(&[byte]).await.unwrap();
tokio::time::sleep(Duration::from_millis(1)).await;
}
let mut buf = [0u8; 256];
let n = tokio::time::timeout(Duration::from_secs(2), stream.read(&mut buf))
.await
.unwrap()
.unwrap();
let (_slave, pdu) = parse_rtu_response(&buf[..n]);
assert_eq!(pdu, vec![0x03, 0x02, 0x00, 0x4D]); }
#[tokio::test]
async fn serve_forever_diagnostic_request() {
let store = Arc::new(SlaveStore::new());
let mut stream = spawn_rtu_server(store);
let frame = rtu_frame(1, &[0x83, 0x02]);
assert_eq!(frame.len(), 5);
let req = rtu_frame(1, &[0x08, 0x00, 0x00, 0xAB, 0xCD]);
stream.write_all(&req).await.unwrap();
let mut buf = [0u8; 256];
let n = tokio::time::timeout(Duration::from_secs(2), stream.read(&mut buf))
.await
.unwrap()
.unwrap();
let (_slave, pdu) = parse_rtu_response(&buf[..n]);
assert_eq!(pdu.len(), 5, "5-byte PDU expected for Diagnostic echo");
assert_eq!(pdu[0], 0x08);
}
#[tokio::test]
async fn serve_forever_max_size_frame() {
let _regs: Vec<u16> = vec![0xABCD; 125];
let store = Arc::new(SlaveStore::with_holding_registers(
&(0..125u16).map(|i| (i, i)).collect::<Vec<_>>(),
));
let mut stream = spawn_rtu_server(store);
let req = rtu_frame(1, &[0x03, 0x00, 0x00, 0x00, 125u8]);
stream.write_all(&req).await.unwrap();
let mut buf = [0u8; 300];
let n = tokio::time::timeout(Duration::from_secs(2), stream.read(&mut buf))
.await
.unwrap()
.unwrap();
let (_slave, pdu) = parse_rtu_response(&buf[..n]);
assert_eq!(pdu[0], 0x03);
assert_eq!(pdu[1], 250, "125 registers × 2 = 250 bytes byte_count");
assert_eq!(pdu.len(), 252, "PDU: FC + byte_count + 250 data bytes");
}
#[tokio::test]
async fn serve_forever_partial_frame_no_crash() {
let store = Arc::new(SlaveStore::with_holding_registers(&[(0, 42)]));
let mut stream = spawn_rtu_server(store);
stream.write_all(&[0x01, 0x03, 0x00]).await.unwrap();
let mut buf = [0u8; 256];
let result = tokio::time::timeout(Duration::from_millis(200), stream.read(&mut buf)).await;
assert!(
result.is_err(),
"partial frame should produce no response (timeout)"
);
}
#[tokio::test]
async fn serve_forever_garbage_bytes_no_crash() {
let store = Arc::new(SlaveStore::with_holding_registers(&[(0, 42)]));
let mut stream = spawn_rtu_server(store);
stream
.write_all(&[0xFF, 0x00, 0xAA, 0x55, 0x11])
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(20)).await;
let valid = rtu_frame(1, &[0x03, 0x00, 0x00, 0x00, 0x01]);
stream.write_all(&valid).await.unwrap();
let mut buf = [0u8; 256];
let n = tokio::time::timeout(Duration::from_secs(2), stream.read(&mut buf))
.await
.unwrap()
.unwrap();
let (_slave, pdu) = parse_rtu_response(&buf[..n]);
assert_eq!(pdu, vec![0x03, 0x02, 0x00, 0x2A]);
}
#[tokio::test]
async fn serve_forever_recovers_after_partial_frame() {
let store = Arc::new(SlaveStore::with_holding_registers(&[(0, 77)]));
let mut stream = spawn_rtu_server(store);
stream.write_all(&[0x01, 0x03, 0x00]).await.unwrap();
tokio::time::sleep(Duration::from_millis(20)).await;
let valid = rtu_frame(1, &[0x03, 0x00, 0x00, 0x00, 0x01]);
stream.write_all(&valid).await.unwrap();
let mut buf = [0u8; 256];
let n = tokio::time::timeout(Duration::from_secs(2), stream.read(&mut buf))
.await
.unwrap()
.unwrap();
let (_slave, pdu) = parse_rtu_response(&buf[..n]);
assert_eq!(pdu, vec![0x03, 0x02, 0x00, 0x4D]); }
#[tokio::test]
async fn serve_forever_write_multiple_registers() {
let store = Arc::new(SlaveStore::with_holding_registers(&[
(0, 0),
(1, 0),
(2, 0),
]));
let mut stream = spawn_rtu_server(store.clone());
let req = rtu_frame(
1,
&[
0x10, 0x00, 0x00, 0x00, 0x03, 0x06, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03,
],
);
stream.write_all(&req).await.unwrap();
let mut buf = [0u8; 256];
let n = tokio::time::timeout(Duration::from_secs(2), stream.read(&mut buf))
.await
.unwrap()
.unwrap();
let (_slave, pdu) = parse_rtu_response(&buf[..n]);
assert_eq!(pdu, vec![0x10, 0x00, 0x00, 0x00, 0x03]);
assert_eq!(store.read_holding_register(0), 1);
assert_eq!(store.read_holding_register(1), 2);
assert_eq!(store.read_holding_register(2), 3);
}