use crate::error::ModbusError;
use core::convert::Infallible;
pub(crate) type TcpResponse = heapless::Vec<u8, 512>;
pub(crate) type RtuResponse = heapless::Vec<u8, 512>;
const MODBUS_PROTO: [u8; 2] = [0, 0];
pub fn crc(data: &[u8]) -> [u8; 2] {
let mut crc: u16 = 0xFFFF;
for &byte in data {
crc ^= byte as u16;
for _ in 0..8 {
crc = if crc & 1 != 0 {
(crc >> 1) ^ 0xA001
} else {
crc >> 1
};
}
}
crc.to_le_bytes()
}
pub fn check_crc(frame: &[u8]) -> Result<(), ModbusError<Infallible, Infallible>> {
if frame.len() < 4 {
return Err(ModbusError::PayloadTooShort);
}
let (body, rx_crc) = frame.split_at(frame.len() - 2);
if crc(body) == rx_crc {
Ok(())
} else {
Err(ModbusError::Crc)
}
}
pub fn rtu_to_tcp(
rtu: &[u8],
transaction_id: u16,
) -> Result<TcpResponse, ModbusError<Infallible, Infallible>> {
if rtu.len() < 4 {
return Err(ModbusError::PayloadTooShort);
}
let (pdu, _crc) = rtu.split_at(rtu.len() - 2);
let len_bytes = (pdu.len() as u16).to_be_bytes();
let mut out = TcpResponse::new();
out.extend_from_slice(&transaction_id.to_be_bytes())
.and(out.extend_from_slice(&MODBUS_PROTO))
.and(out.extend_from_slice(&len_bytes))
.and(out.extend_from_slice(pdu))
.map_err(|_| ModbusError::ConversionSlice)?;
Ok(out)
}
pub fn tcp_to_rtu(tcp: &[u8]) -> Result<(RtuResponse, u16), ModbusError<Infallible, Infallible>> {
if tcp.len() < 7 {
return Err(ModbusError::PayloadTooShort);
}
let transaction_id = u16::from_be_bytes([tcp[0], tcp[1]]);
let pdu = &tcp[6..];
let mut out = RtuResponse::new();
out.extend_from_slice(pdu)
.and(out.extend_from_slice(&crc(pdu)))
.map_err(|_| ModbusError::ConversionSlice)?;
Ok((out, transaction_id))
}
pub fn tcp_resp_to_rtu(
tcp: &[u8],
expected_tid: u16,
) -> Result<RtuResponse, ModbusError<Infallible, Infallible>> {
if tcp.len() < 7 {
return Err(ModbusError::PayloadTooShort);
}
let rx_tid = u16::from_be_bytes([tcp[0], tcp[1]]);
if rx_tid != expected_tid {
return Err(ModbusError::InvalidTransactionId);
}
let pdu_len = tcp[5] as usize;
if tcp.len() < 6 + pdu_len {
return Err(ModbusError::PayloadTooShort);
}
let pdu = &tcp[6..6 + pdu_len];
let mut out = RtuResponse::new();
out.extend_from_slice(pdu)
.and(out.extend_from_slice(&crc(pdu)))
.map_err(|_| ModbusError::ConversionSlice)?;
Ok(out)
}
pub fn rtu_resp_to_tcp(
rtu: &[u8],
transaction_id: u16,
) -> Result<TcpResponse, ModbusError<Infallible, Infallible>> {
if rtu.len() < 4 {
return Err(ModbusError::PayloadTooShort);
}
let (pdu, _crc) = rtu.split_at(rtu.len() - 2);
let len_bytes = (pdu.len() as u16).to_be_bytes();
let mut out = TcpResponse::new();
out.extend_from_slice(&transaction_id.to_be_bytes())
.and(out.extend_from_slice(&MODBUS_PROTO))
.and(out.extend_from_slice(&len_bytes))
.and(out.extend_from_slice(pdu))
.map_err(|_| ModbusError::ConversionSlice)?;
Ok(out)
}
pub fn rtu_response_remaining(header: &[u8; 3]) -> usize {
match header[1] {
0x80..=0xFF => 2,
0x05 | 0x06 | 0x0F | 0x10 => 5,
_ => header[2] as usize + 2,
}
}
pub(crate) fn parse_tcp_request(tcp: &[u8]) -> Option<(u8, u8, u16, u16)> {
if tcp.len() < 12 {
return None;
}
let unit_id = tcp[6];
let fc = tcp[7];
let start = u16::from_be_bytes([tcp[8], tcp[9]]);
let qty = u16::from_be_bytes([tcp[10], tcp[11]]);
Some((unit_id, fc, start, qty))
}
pub(crate) fn parse_rtu_request(rtu: &[u8]) -> Option<(u8, u8, u16, u16)> {
if rtu.len() < 8 {
return None;
}
let unit_id = rtu[0];
let fc = rtu[1];
let start = u16::from_be_bytes([rtu[2], rtu[3]]);
let qty = u16::from_be_bytes([rtu[4], rtu[5]]);
Some((unit_id, fc, start, qty))
}
#[cfg(test)]
mod tests {
use super::*;
fn make_rtu_request() -> [u8; 8] {
let body = [0x01u8, 0x03, 0x00, 0x00, 0x00, 0x02];
let [lo, hi] = crc(&body);
[body[0], body[1], body[2], body[3], body[4], body[5], lo, hi]
}
#[test]
fn crc_self_check_residual() {
let body = [0x01u8, 0x03, 0x00, 0x00, 0x00, 0x02];
let c = crc(&body);
let mut full = [0u8; 8];
full[..6].copy_from_slice(&body);
full[6..].copy_from_slice(&c);
assert_eq!(crc(&full), [0x00, 0x00]);
}
#[test]
fn crc_single_zero_byte() {
assert_eq!(crc(&[0x00]), [0xBF, 0x40]);
}
#[test]
fn crc_all_ff_single_byte() {
assert_eq!(crc(&[0xFF]), [0xFF, 0x00]);
}
#[test]
fn remaining_read_fc_zero_byte_count() {
assert_eq!(rtu_response_remaining(&[0x01, 0x03, 0x00]), 2);
}
#[test]
fn remaining_read_fc_max_byte_count() {
assert_eq!(rtu_response_remaining(&[0x01, 0x03, 0xFF]), 257);
}
#[test]
fn remaining_write_single_coil_fc05() {
assert_eq!(rtu_response_remaining(&[0x01, 0x05, 0x00]), 5);
}
#[test]
fn remaining_write_single_register_fc06() {
assert_eq!(rtu_response_remaining(&[0x01, 0x06, 0x00]), 5);
}
#[test]
fn remaining_write_multiple_coils_fc0f() {
assert_eq!(rtu_response_remaining(&[0x01, 0x0F, 0x00]), 5);
}
#[test]
fn remaining_write_multiple_registers_fc10() {
assert_eq!(rtu_response_remaining(&[0x01, 0x10, 0x00]), 5);
}
#[test]
fn remaining_exception_response() {
assert_eq!(rtu_response_remaining(&[0x01, 0x83, 0x02]), 2);
assert_eq!(rtu_response_remaining(&[0x01, 0x85, 0x02]), 2);
assert_eq!(rtu_response_remaining(&[0x01, 0x90, 0x02]), 2);
}
#[test]
fn rtu_to_tcp_then_tcp_to_rtu_recovers_pdu() {
let frame = make_rtu_request();
let tcp = rtu_to_tcp(&frame, 0x0042).unwrap();
let (rtu2, tid) = tcp_to_rtu(&tcp).unwrap();
assert_eq!(&frame[..frame.len() - 2], &rtu2[..rtu2.len() - 2]);
assert_eq!(tid, 0x0042);
}
#[test]
fn rtu_resp_to_tcp_then_tcp_resp_to_rtu_recovers_pdu() {
let body = [0x01u8, 0x03, 0x02, 0x00, 0x01];
let [lo, hi] = crc(&body);
let rtu_resp: &[u8] = &[body[0], body[1], body[2], body[3], body[4], lo, hi];
let tcp = rtu_resp_to_tcp(rtu_resp, 0x0007).unwrap();
let rtu2 = tcp_resp_to_rtu(&tcp, 0x0007).unwrap();
assert_eq!(&rtu_resp[..rtu_resp.len() - 2], &rtu2[..rtu2.len() - 2]);
}
#[test]
fn rtu_to_tcp_transaction_id_survives_u16_max() {
let frame = make_rtu_request();
let tcp = rtu_to_tcp(&frame, u16::MAX).unwrap();
let (_, tid) = tcp_to_rtu(&tcp).unwrap();
assert_eq!(tid, u16::MAX);
}
#[test]
fn tcp_resp_to_rtu_rejects_truncated_pdu() {
let frame: [u8; 9] = [0x00, 0x01, 0x00, 0x00, 0x00, 10, 0x01, 0x03, 0x04];
assert!(matches!(
tcp_resp_to_rtu(&frame, 0x0001),
Err(ModbusError::PayloadTooShort)
));
}
#[test]
fn tcp_to_rtu_tid_zero_is_preserved() {
let frame = make_rtu_request();
let tcp = rtu_to_tcp(&frame, 0x0000).unwrap();
let (_, tid) = tcp_to_rtu(&tcp).unwrap();
assert_eq!(tid, 0x0000);
}
}