#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::{format, vec, vec::Vec};
#[cfg(feature = "std")]
use std::vec::Vec;
use crc::{Crc, CRC_16_MODBUS};
use embedded_io_async::{Read, Write};
use heapless::Vec as HVec;
use crate::error::{ModbusError, ModbusResult};
use crate::protocol::{ModbusFunction, ModbusRequest, ModbusResponse};
const MAX_FRAME: usize = 256;
static CRC_MODBUS: Crc<u16> = Crc::<u16>::new(&CRC_16_MODBUS);
pub struct EmbeddedRtuTransport<RW> {
io: RW,
}
impl<RW> EmbeddedRtuTransport<RW>
where
RW: Read + Write,
{
pub fn new(io: RW) -> Self {
Self { io }
}
pub fn into_inner(self) -> RW {
self.io
}
pub async fn request(&mut self, request: &ModbusRequest) -> ModbusResult<ModbusResponse> {
let frame = self.encode_request(request)?;
self.write_frame(&frame).await?;
let response_buf = self.read_response(request).await?;
self.decode_response(response_buf)
}
pub fn encode_request(
&self,
request: &ModbusRequest,
) -> ModbusResult<HVec<u8, MAX_FRAME>> {
let mut frame: HVec<u8, MAX_FRAME> = HVec::new();
push(&mut frame, request.slave_id)?;
push(&mut frame, request.function.to_u8())?;
match request.function {
ModbusFunction::ReadCoils
| ModbusFunction::ReadDiscreteInputs
| ModbusFunction::ReadHoldingRegisters
| ModbusFunction::ReadInputRegisters => {
extend(&mut frame, &request.address.to_be_bytes())?;
extend(&mut frame, &request.quantity.to_be_bytes())?;
}
ModbusFunction::WriteSingleCoil => {
extend(&mut frame, &request.address.to_be_bytes())?;
let coil_value: u16 =
if !request.data.is_empty() && request.data[0] != 0 { 0xFF00 } else { 0x0000 };
extend(&mut frame, &coil_value.to_be_bytes())?;
}
ModbusFunction::WriteSingleRegister => {
extend(&mut frame, &request.address.to_be_bytes())?;
if request.data.len() >= 2 {
extend(&mut frame, &request.data[0..2])?;
} else {
extend(&mut frame, &[0u8, 0u8])?;
}
}
ModbusFunction::WriteMultipleCoils | ModbusFunction::WriteMultipleRegisters => {
extend(&mut frame, &request.address.to_be_bytes())?;
extend(&mut frame, &request.quantity.to_be_bytes())?;
let byte_count = u8::try_from(request.data.len())
.map_err(|_| ModbusError::invalid_data("data payload too large"))?;
push(&mut frame, byte_count)?;
extend(&mut frame, &request.data)?;
}
}
let crc = CRC_MODBUS.checksum(&frame);
extend(&mut frame, &crc.to_le_bytes())?;
Ok(frame)
}
pub fn decode_response(&self, frame: Vec<u8>) -> ModbusResult<ModbusResponse> {
if frame.len() < 4 {
return Err(ModbusError::frame("RTU frame too short"));
}
let pdu_len = frame.len() - 2; let received_crc = u16::from_le_bytes([frame[pdu_len], frame[pdu_len + 1]]);
let calculated_crc = CRC_MODBUS.checksum(&frame[..pdu_len]);
if received_crc != calculated_crc {
return Err(ModbusError::frame(format!(
"CRC mismatch: expected 0x{:04X}, got 0x{:04X}",
calculated_crc, received_crc
)));
}
let slave_id = frame[0];
let function_code = frame[1];
if function_code & 0x80 != 0 {
if frame.len() < 5 {
return Err(ModbusError::frame("Invalid exception response"));
}
let original_fn = function_code & 0x7F;
let exception_code = frame[2];
return Ok(ModbusResponse::new_exception(
slave_id,
ModbusFunction::from_u8(original_fn)?,
exception_code,
));
}
let function = ModbusFunction::from_u8(function_code)?;
let data_start = 2usize;
let data_len = pdu_len.saturating_sub(2);
Ok(ModbusResponse::new_from_frame(
frame, slave_id, function, data_start, data_len,
))
}
async fn write_frame(&mut self, frame: &[u8]) -> ModbusResult<()> {
self.io
.write_all(frame)
.await
.map_err(|_| ModbusError::io("embedded write error"))
}
async fn read_response(&mut self, request: &ModbusRequest) -> ModbusResult<Vec<u8>> {
let expected = expected_response_len(request);
let mut buf = vec![0u8; expected];
self.io
.read_exact(&mut buf)
.await
.map_err(|_| ModbusError::io("embedded read error"))?;
Ok(buf)
}
}
fn expected_response_len(request: &ModbusRequest) -> usize {
match request.function {
ModbusFunction::ReadCoils | ModbusFunction::ReadDiscreteInputs => {
let data_bytes = usize::from(request.quantity.div_ceil(8));
1 + 1 + 1 + data_bytes + 2
}
ModbusFunction::ReadHoldingRegisters | ModbusFunction::ReadInputRegisters => {
let data_bytes = usize::from(request.quantity) * 2;
1 + 1 + 1 + data_bytes + 2
}
ModbusFunction::WriteSingleCoil | ModbusFunction::WriteSingleRegister => 1 + 1 + 2 + 2 + 2,
ModbusFunction::WriteMultipleCoils | ModbusFunction::WriteMultipleRegisters => {
1 + 1 + 2 + 2 + 2
}
}
}
#[inline]
fn push(buf: &mut HVec<u8, MAX_FRAME>, byte: u8) -> ModbusResult<()> {
buf.push(byte)
.map_err(|_| ModbusError::frame("RTU frame buffer overflow"))
}
#[inline]
fn extend(buf: &mut HVec<u8, MAX_FRAME>, bytes: &[u8]) -> ModbusResult<()> {
buf.extend_from_slice(bytes)
.map_err(|_| ModbusError::frame("RTU frame buffer overflow"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::{ModbusFunction, ModbusRequest};
struct MockIo {
read_buf: Vec<u8>,
read_pos: usize,
pub written: Vec<u8>,
}
impl MockIo {
fn new(read_data: Vec<u8>) -> Self {
Self {
read_buf: read_data,
read_pos: 0,
written: Vec::new(),
}
}
}
impl embedded_io_async::ErrorType for MockIo {
type Error = MockError;
}
#[derive(Debug)]
struct MockError;
impl embedded_io_async::Error for MockError {
fn kind(&self) -> embedded_io_async::ErrorKind {
embedded_io_async::ErrorKind::Other
}
}
impl Read for MockIo {
async fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
let remaining = self.read_buf.len() - self.read_pos;
if remaining == 0 {
return Err(MockError);
}
let n = buf.len().min(remaining);
buf[..n].copy_from_slice(&self.read_buf[self.read_pos..self.read_pos + n]);
self.read_pos += n;
Ok(n)
}
}
impl Write for MockIo {
async fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
self.written.extend_from_slice(buf);
Ok(buf.len())
}
}
fn make_fc03_response(slave_id: u8, regs: &[u16]) -> Vec<u8> {
let mut frame: Vec<u8> = Vec::new();
frame.push(slave_id);
frame.push(0x03); frame.push((regs.len() * 2) as u8);
for &r in regs {
frame.extend_from_slice(&r.to_be_bytes());
}
let crc = CRC_MODBUS.checksum(&frame);
frame.extend_from_slice(&crc.to_le_bytes());
frame
}
fn make_exception_frame(slave_id: u8, fc: u8, exc_code: u8) -> Vec<u8> {
let mut frame: Vec<u8> = vec![slave_id, fc | 0x80, exc_code];
let crc = CRC_MODBUS.checksum(&frame);
frame.extend_from_slice(&crc.to_le_bytes());
frame
}
#[test]
fn test_encode_read_holding_registers() {
let transport = EmbeddedRtuTransport::new(MockIo::new(vec![]));
let req = ModbusRequest::new_read(1, ModbusFunction::ReadHoldingRegisters, 0x0000, 10);
let frame = transport.encode_request(&req).unwrap();
assert_eq!(frame.len(), 8);
assert_eq!(frame[0], 1); assert_eq!(frame[1], 0x03); assert_eq!(frame[2], 0x00); assert_eq!(frame[3], 0x00); assert_eq!(frame[4], 0x00); assert_eq!(frame[5], 0x0A);
let pdu_len = frame.len() - 2;
let expected_crc = CRC_MODBUS.checksum(&frame[..pdu_len]);
let frame_crc = u16::from_le_bytes([frame[pdu_len], frame[pdu_len + 1]]);
assert_eq!(expected_crc, frame_crc);
}
#[test]
fn test_encode_write_single_register() {
let transport = EmbeddedRtuTransport::new(MockIo::new(vec![]));
let req = ModbusRequest::new_write(
2,
ModbusFunction::WriteSingleRegister,
0x0064, vec![0x12, 0x34],
);
let frame = transport.encode_request(&req).unwrap();
assert_eq!(frame[0], 2); assert_eq!(frame[1], 0x06); assert_eq!(frame[2], 0x00);
assert_eq!(frame[3], 0x64); assert_eq!(frame[4], 0x12);
assert_eq!(frame[5], 0x34);
let pdu_len = frame.len() - 2;
let expected_crc = CRC_MODBUS.checksum(&frame[..pdu_len]);
let frame_crc = u16::from_le_bytes([frame[pdu_len], frame[pdu_len + 1]]);
assert_eq!(expected_crc, frame_crc);
}
#[test]
fn test_encode_write_coil_on() {
let transport = EmbeddedRtuTransport::new(MockIo::new(vec![]));
let req = ModbusRequest::new_write(
1,
ModbusFunction::WriteSingleCoil,
0,
vec![1], );
let frame = transport.encode_request(&req).unwrap();
assert_eq!(frame[4], 0xFF);
assert_eq!(frame[5], 0x00);
}
#[test]
fn test_encode_write_coil_off() {
let transport = EmbeddedRtuTransport::new(MockIo::new(vec![]));
let req = ModbusRequest::new_write(1, ModbusFunction::WriteSingleCoil, 0, vec![0]);
let frame = transport.encode_request(&req).unwrap();
assert_eq!(frame[4], 0x00);
assert_eq!(frame[5], 0x00);
}
#[test]
fn test_decode_fc03_response_roundtrip() {
let transport = EmbeddedRtuTransport::new(MockIo::new(vec![]));
let regs = [0x0001u16, 0x0002, 0x0003];
let frame = make_fc03_response(1, ®s);
let response = transport.decode_response(frame).unwrap();
assert_eq!(response.slave_id, 1);
assert_eq!(response.function, ModbusFunction::ReadHoldingRegisters);
assert!(!response.is_exception());
let parsed = response.parse_registers().unwrap();
assert_eq!(parsed, regs);
}
#[test]
fn test_decode_bad_crc_rejected() {
let transport = EmbeddedRtuTransport::new(MockIo::new(vec![]));
let mut frame = make_fc03_response(1, &[0xABCDu16]);
let last = frame.len() - 1;
frame[last] ^= 0xFF;
let result = transport.decode_response(frame);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, ModbusError::Frame { .. }));
}
#[test]
fn test_decode_exception_response() {
let transport = EmbeddedRtuTransport::new(MockIo::new(vec![]));
let frame = make_exception_frame(1, 0x03, 0x02);
let response = transport.decode_response(frame).unwrap();
assert!(response.is_exception());
assert_eq!(response.slave_id, 1);
assert_eq!(response.function, ModbusFunction::ReadHoldingRegisters);
}
#[test]
fn test_decode_frame_too_short() {
let transport = EmbeddedRtuTransport::new(MockIo::new(vec![]));
let short = vec![0x01, 0x03, 0x04]; assert!(transport.decode_response(short).is_err());
}
#[tokio::test]
async fn test_request_roundtrip_fc03() {
let regs = [0x1234u16, 0x5678];
let response_frame = make_fc03_response(1, ®s);
let mock = MockIo::new(response_frame.clone());
let mut transport = EmbeddedRtuTransport::new(mock);
let req = ModbusRequest::new_read(1, ModbusFunction::ReadHoldingRegisters, 0, 2);
let response = transport.request(&req).await.unwrap();
assert!(!response.is_exception());
let parsed = response.parse_registers().unwrap();
assert_eq!(parsed, regs);
let written = &transport.io.written;
assert!(!written.is_empty());
assert_eq!(written[0], 1); assert_eq!(written[1], 0x03); }
#[tokio::test]
async fn test_request_bad_crc_error() {
let mut frame = make_fc03_response(1, &[0xBEEFu16]);
let last = frame.len() - 1;
frame[last] ^= 0xFF;
let mock = MockIo::new(frame);
let mut transport = EmbeddedRtuTransport::new(mock);
let req = ModbusRequest::new_read(1, ModbusFunction::ReadHoldingRegisters, 0, 1);
assert!(transport.request(&req).await.is_err());
}
}