use crate::errors::MbusError;
use crate::function_codes::public::FunctionCode;
use crate::transport::{SerialMode, TransportType, UnitIdOrSlaveAddr, checksum};
use heapless::Vec;
pub const MAX_PDU_DATA_LEN: usize = 252;
pub const MODBUS_PROTOCOL_ID: u16 = 0x0000;
pub const MAX_ADU_FRAME_LEN_TCP_RTU: usize = 260;
pub const MAX_ADU_FRAME_LEN_ASCII: usize = 513;
#[cfg(feature = "serial-ascii")]
pub const MAX_ADU_FRAME_LEN: usize = MAX_ADU_FRAME_LEN_ASCII;
#[cfg(not(feature = "serial-ascii"))]
pub const MAX_ADU_FRAME_LEN: usize = MAX_ADU_FRAME_LEN_TCP_RTU;
#[cfg(test)]
mod frame_len_tests {
use super::*;
#[test]
#[cfg(feature = "serial-ascii")]
fn test_max_adu_frame_len_ascii_enabled() {
assert_eq!(MAX_ADU_FRAME_LEN, 513);
}
#[test]
#[cfg(not(feature = "serial-ascii"))]
fn test_max_adu_frame_len_ascii_disabled() {
assert_eq!(MAX_ADU_FRAME_LEN, 260);
}
}
pub const MBAP_HEADER_SIZE: usize = 7;
pub const MIN_RTU_ADU_LEN: usize = 4;
pub const MIN_ASCII_ADU_LEN: usize = 9;
pub const RTU_CRC_SIZE: usize = 2;
pub const MBAP_TXN_ID_OFFSET_1B: usize = 0;
pub const MBAP_TXN_ID_OFFSET_2B: usize = MBAP_TXN_ID_OFFSET_1B + 1;
pub const MBAP_PROTO_ID_OFFSET_1B: usize = 2;
pub const MBAP_PROTO_ID_OFFSET_2B: usize = MBAP_PROTO_ID_OFFSET_1B + 1;
pub const MBAP_LENGTH_OFFSET_1B: usize = 4;
pub const MBAP_LENGTH_OFFSET_2B: usize = MBAP_LENGTH_OFFSET_1B + 1;
pub const MBAP_UNIT_ID_OFFSET: usize = 6;
pub const ASCII_START_SIZE: usize = 1;
pub const ASCII_END_SIZE: usize = 2;
pub const ERROR_BIT_MASK: u8 = 0x80;
pub const FUNCTION_CODE_MASK: u8 = 0x7F;
#[inline]
pub fn is_exception_code(function_code_byte: u8) -> bool {
function_code_byte & ERROR_BIT_MASK != 0
}
#[inline]
pub fn clear_exception_bit(function_code_byte: u8) -> u8 {
function_code_byte & FUNCTION_CODE_MASK
}
#[derive(Debug, Clone)]
pub struct Pdu {
function_code: FunctionCode,
error_code: Option<u8>,
data: heapless::Vec<u8, MAX_PDU_DATA_LEN>,
data_len: u8,
}
#[derive(Debug, Clone, Copy)]
pub struct MbapHeader {
pub transaction_id: u16,
pub protocol_id: u16,
pub length: u16,
pub unit_id: u8,
}
impl MbapHeader {
pub fn new(transaction_id: u16, length: u16, unit_id: u8) -> Self {
Self {
transaction_id,
protocol_id: 0,
length,
unit_id,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SlaveAddress(u8);
impl SlaveAddress {
pub fn new(address: u8) -> Result<Self, MbusError> {
if !(0..=247).contains(&address) {
return Err(MbusError::InvalidSlaveAddress);
}
Ok(Self(address))
}
pub fn address(&self) -> u8 {
self.0
}
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub enum AdditionalAddress {
MbapHeader(MbapHeader),
SlaveAddress(SlaveAddress),
}
#[derive(Debug, Clone)]
pub struct ModbusMessage {
pub additional_address: AdditionalAddress,
pub pdu: Pdu,
}
impl ModbusMessage {
pub fn new(additional_address: AdditionalAddress, pdu: Pdu) -> Self {
Self {
additional_address,
pdu,
}
}
pub fn additional_address(&self) -> &AdditionalAddress {
&self.additional_address
}
pub fn pdu(&self) -> &Pdu {
&self.pdu
}
pub fn unit_id_or_slave_addr(&self) -> UnitIdOrSlaveAddr {
match self.additional_address {
AdditionalAddress::MbapHeader(header) => {
UnitIdOrSlaveAddr::try_from(header.unit_id).unwrap_or(UnitIdOrSlaveAddr::default())
}
AdditionalAddress::SlaveAddress(slave_address) => {
UnitIdOrSlaveAddr::try_from(slave_address.address())
.unwrap_or(UnitIdOrSlaveAddr::default())
}
}
}
pub fn transaction_id(&self) -> u16 {
match self.additional_address {
AdditionalAddress::MbapHeader(header) => header.transaction_id,
AdditionalAddress::SlaveAddress(_) => 0,
}
}
pub fn function_code(&self) -> FunctionCode {
self.pdu.function_code()
}
pub fn data(&self) -> &heapless::Vec<u8, MAX_PDU_DATA_LEN> {
self.pdu.data()
}
pub fn data_len(&self) -> u8 {
self.pdu.data_len()
}
pub fn to_bytes(&self) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
let mut adu_bytes = Vec::new();
match &self.additional_address {
AdditionalAddress::MbapHeader(header) => {
adu_bytes
.extend_from_slice(&header.transaction_id.to_be_bytes())
.map_err(|_| MbusError::Unexpected)?;
adu_bytes
.extend_from_slice(&header.protocol_id.to_be_bytes())
.map_err(|_| MbusError::Unexpected)?;
adu_bytes
.extend_from_slice(&header.length.to_be_bytes())
.map_err(|_| MbusError::Unexpected)?;
adu_bytes
.push(header.unit_id)
.map_err(|_| MbusError::Unexpected)?;
}
AdditionalAddress::SlaveAddress(address) => {
adu_bytes
.push(address.address())
.map_err(|_| MbusError::Unexpected)?;
}
}
let pdu_bytes = self.pdu.to_bytes()?;
adu_bytes
.extend_from_slice(&pdu_bytes)
.map_err(|_| MbusError::Unexpected)?;
Ok(adu_bytes)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, MbusError> {
if bytes.len() < MBAP_HEADER_SIZE + 1 {
return Err(MbusError::InvalidAduLength); }
let transaction_id =
u16::from_be_bytes([bytes[MBAP_TXN_ID_OFFSET_1B], bytes[MBAP_TXN_ID_OFFSET_2B]]);
let protocol_id = u16::from_be_bytes([
bytes[MBAP_PROTO_ID_OFFSET_1B],
bytes[MBAP_PROTO_ID_OFFSET_2B],
]);
let length =
u16::from_be_bytes([bytes[MBAP_LENGTH_OFFSET_1B], bytes[MBAP_LENGTH_OFFSET_2B]]);
let unit_id = bytes[MBAP_UNIT_ID_OFFSET];
if protocol_id != MODBUS_PROTOCOL_ID {
return Err(MbusError::BasicParseError); }
const INITIAL_FRAME_LEN: usize = MBAP_HEADER_SIZE - 1; let expected_total_len_from_header = length as usize + INITIAL_FRAME_LEN;
if bytes.len() < expected_total_len_from_header {
return Err(MbusError::InvalidPduLength);
}
let frame_bytes = &bytes[..expected_total_len_from_header];
let pdu_bytes_slice = &frame_bytes[MBAP_HEADER_SIZE..];
let pdu = Pdu::from_bytes(pdu_bytes_slice)?;
let additional_addr = AdditionalAddress::MbapHeader(MbapHeader {
transaction_id,
protocol_id,
length,
unit_id,
});
Ok(ModbusMessage::new(additional_addr, pdu))
}
pub fn to_ascii_bytes(&self) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
let mut binary_data = self.to_bytes()?;
let lrc = checksum::lrc(&binary_data);
binary_data
.push(lrc)
.map_err(|_| MbusError::BufferTooSmall)?;
let mut ascii_data = Vec::new();
ascii_data
.push(b':')
.map_err(|_| MbusError::BufferTooSmall)?;
for byte in binary_data {
let high = (byte >> 4) & 0x0F;
let low = byte & 0x0F;
ascii_data
.push(nibble_to_hex(high))
.map_err(|_| MbusError::BufferTooSmall)?;
ascii_data
.push(nibble_to_hex(low))
.map_err(|_| MbusError::BufferTooSmall)?;
}
ascii_data
.push(b'\r')
.map_err(|_| MbusError::BufferTooSmall)?;
ascii_data
.push(b'\n')
.map_err(|_| MbusError::BufferTooSmall)?;
Ok(ascii_data)
}
pub fn from_rtu_bytes(frame: &[u8]) -> Result<Self, MbusError> {
if frame.len() < MIN_RTU_ADU_LEN {
return Err(MbusError::InvalidAduLength);
}
let data_len = frame.len() - RTU_CRC_SIZE;
let data_to_check = &frame[..data_len];
let received_crc = u16::from_le_bytes([frame[data_len], frame[data_len + 1]]);
let calculated_crc = checksum::crc16(data_to_check);
if calculated_crc != received_crc {
return Err(MbusError::ChecksumError); }
let slave_address = SlaveAddress::new(frame[0])?;
let pdu_bytes = &data_to_check[1..];
let pdu = Pdu::from_bytes(pdu_bytes)?;
Ok(ModbusMessage::new(
AdditionalAddress::SlaveAddress(slave_address),
pdu,
))
}
pub fn from_ascii_bytes(frame: &[u8]) -> Result<Self, MbusError> {
if frame.len() < MIN_ASCII_ADU_LEN {
return Err(MbusError::InvalidAduLength);
}
if frame[0] != b':' {
return Err(MbusError::BasicParseError); }
if frame[frame.len() - 2] != b'\r' || frame[frame.len() - 1] != b'\n' {
return Err(MbusError::BasicParseError); }
let hex_content = &frame[ASCII_START_SIZE..frame.len() - ASCII_END_SIZE];
if !hex_content.len().is_multiple_of(2) {
return Err(MbusError::BasicParseError); }
let mut binary_data: Vec<u8, 260> = Vec::new();
for chunk in hex_content.chunks(2) {
let byte = hex_pair_to_byte(chunk[0], chunk[1])?;
binary_data
.push(byte)
.map_err(|_| MbusError::BufferTooSmall)?;
}
if binary_data.len() < 2 {
return Err(MbusError::InvalidAduLength);
}
let data_len = binary_data.len() - 1;
let data_to_check = &binary_data[..data_len];
let received_lrc = binary_data[data_len];
let calculated_lrc = checksum::lrc(data_to_check);
if calculated_lrc != received_lrc {
return Err(MbusError::ChecksumError); }
let slave_address = SlaveAddress::new(binary_data[0])?;
let pdu_bytes = &binary_data[1..data_len];
let pdu = Pdu::from_bytes(pdu_bytes)?;
Ok(ModbusMessage::new(
AdditionalAddress::SlaveAddress(slave_address),
pdu,
))
}
}
impl Pdu {
pub fn new(
function_code: FunctionCode,
data: heapless::Vec<u8, MAX_PDU_DATA_LEN>,
data_len: u8,
) -> Self {
Self {
function_code,
error_code: None, data, data_len,
}
}
pub fn function_code(&self) -> FunctionCode {
self.function_code
}
pub fn data(&self) -> &Vec<u8, MAX_PDU_DATA_LEN> {
&self.data
}
pub fn data_len(&self) -> u8 {
self.data_len
}
pub fn error_code(&self) -> Option<u8> {
self.error_code
}
pub fn to_bytes(&self) -> Result<Vec<u8, 253>, MbusError> {
let mut pdu_bytes = Vec::new(); pdu_bytes
.push(self.function_code as u8)
.map_err(|_| MbusError::Unexpected)?;
pdu_bytes
.extend_from_slice(&self.data.as_slice()[..self.data_len as usize])
.map_err(|_| MbusError::BufferLenMissmatch)?;
Ok(pdu_bytes)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, MbusError> {
if bytes.is_empty() || bytes.len() < 2 {
return Err(MbusError::InvalidPduLength);
}
let error_code = if is_exception_code(bytes[0]) {
Some(bytes[1]) } else {
None
};
let function_code = clear_exception_bit(bytes[0]);
let function_code = FunctionCode::try_from(function_code)?;
let data_slice = &bytes[1..];
let data_len = data_slice.len();
if data_len > MAX_PDU_DATA_LEN {
return Err(MbusError::InvalidPduLength);
}
let mut data = heapless::Vec::new();
data.extend_from_slice(data_slice)
.map_err(|_| MbusError::BufferLenMissmatch)?;
Ok(Pdu {
function_code,
error_code,
data,
data_len: data_len as u8,
})
}
}
pub fn compile_adu_frame(
txn_id: u16,
unit_id: u8,
pdu: Pdu,
transport_type: TransportType,
) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
match transport_type {
TransportType::StdTcp | TransportType::CustomTcp => {
let pdu_bytes_len = pdu.to_bytes()?.len() as u16;
let mbap_header = MbapHeader::new(txn_id, pdu_bytes_len + 1, unit_id);
ModbusMessage::new(AdditionalAddress::MbapHeader(mbap_header), pdu).to_bytes()
}
TransportType::StdSerial(serial_mode) | TransportType::CustomSerial(serial_mode) => {
let slave_address = SlaveAddress(unit_id);
let adu_bytes = match serial_mode {
SerialMode::Rtu => {
let mut adu_bytes =
ModbusMessage::new(AdditionalAddress::SlaveAddress(slave_address), pdu)
.to_bytes()?;
let crc16 = checksum::crc16(adu_bytes.as_slice());
let crc_bytes = crc16.to_le_bytes();
adu_bytes
.extend_from_slice(&crc_bytes)
.map_err(|_| MbusError::Unexpected)?;
adu_bytes
}
SerialMode::Ascii => {
ModbusMessage::new(AdditionalAddress::SlaveAddress(slave_address), pdu)
.to_ascii_bytes()?
}
};
Ok(adu_bytes)
}
}
}
pub fn decompile_adu_frame(
frame: &[u8],
transport_type: TransportType,
) -> Result<ModbusMessage, MbusError> {
match transport_type {
TransportType::StdTcp | TransportType::CustomTcp => {
ModbusMessage::from_bytes(frame)
}
TransportType::StdSerial(serial_mode) | TransportType::CustomSerial(serial_mode) => {
match serial_mode {
SerialMode::Rtu => ModbusMessage::from_rtu_bytes(frame),
SerialMode::Ascii => ModbusMessage::from_ascii_bytes(frame),
}
}
}
}
pub fn derive_length_from_bytes(frame: &[u8], transport_type: TransportType) -> Option<usize> {
match transport_type {
TransportType::StdTcp | TransportType::CustomTcp => {
if frame.len() < 6 {
return None;
}
let protocol_id = u16::from_be_bytes([frame[2], frame[3]]);
if protocol_id != MODBUS_PROTOCOL_ID {
return Some(usize::MAX);
}
let length_field = u16::from_be_bytes([frame[4], frame[5]]) as usize;
Some(6 + length_field)
}
TransportType::StdSerial(SerialMode::Rtu)
| TransportType::CustomSerial(SerialMode::Rtu) => {
if frame.len() < 2 {
return None;
}
let fc = frame[1];
if is_exception_code(fc) {
return Some(5);
}
let check_crc = |len: usize| -> bool {
if frame.len() >= len && len >= MIN_RTU_ADU_LEN {
let data_len = len - RTU_CRC_SIZE;
let received_crc = u16::from_le_bytes([frame[data_len], frame[data_len + 1]]);
checksum::crc16(&frame[..data_len]) == received_crc
} else {
false
}
};
get_byte_count_from_frame(frame, fc, check_crc)
}
TransportType::StdSerial(SerialMode::Ascii)
| TransportType::CustomSerial(SerialMode::Ascii) => {
if frame.len() < MIN_ASCII_ADU_LEN {
return None;
}
frame.iter().position(|&b| b == b'\n').map(|pos| pos + 1)
}
}
}
fn get_byte_count_from_frame(
frame: &[u8],
fc: u8,
check_crc: impl Fn(usize) -> bool,
) -> Option<usize> {
let mut candidates = heapless::Vec::<usize, 4>::new();
let mut min_needed = usize::MAX;
let mut add_dyn = |cands: &mut heapless::Vec<usize, 4>, offset: usize, base: usize| {
if frame.len() > offset {
let _ = cands.push(base + frame[offset] as usize);
} else {
min_needed = core::cmp::min(min_needed, offset + 1);
}
};
match fc {
1..=4 => {
let _ = candidates.push(8);
add_dyn(&mut candidates, 2, 5);
}
5 | 6 | 8 => {
let _ = candidates.push(8);
}
7 => {
let _ = candidates.push(4);
let _ = candidates.push(5);
}
11 => {
let _ = candidates.push(4);
let _ = candidates.push(8);
}
12 | 17 => {
let _ = candidates.push(4);
add_dyn(&mut candidates, 2, 5);
}
15 | 16 => {
let _ = candidates.push(8);
add_dyn(&mut candidates, 6, 9);
}
20 | 21 => add_dyn(&mut candidates, 2, 5),
22 => {
let _ = candidates.push(10);
}
23 => {
add_dyn(&mut candidates, 2, 5);
add_dyn(&mut candidates, 10, 13);
}
24 => {
let _ = candidates.push(6);
if frame.len() >= 4 {
let byte_count = u16::from_be_bytes([frame[2], frame[3]]) as usize;
let _ = candidates.push(6 + byte_count);
} else {
min_needed = core::cmp::min(min_needed, 4);
}
}
43 => {
if check_crc(7) {
return Some(7);
}
for len in MIN_RTU_ADU_LEN..=frame.len() {
if check_crc(len) {
return Some(len);
}
}
return None;
}
_ => {
for len in MIN_RTU_ADU_LEN..=frame.len() {
if check_crc(len) {
return Some(len);
}
}
return None;
}
}
for &len in &candidates {
if check_crc(len) {
return Some(len);
}
}
let max_candidate = candidates.iter().copied().max().unwrap_or(0);
let target = if min_needed != usize::MAX {
core::cmp::max(min_needed, max_candidate)
} else {
max_candidate
};
if target > 0 { Some(target) } else { None }
}
fn nibble_to_hex(nibble: u8) -> u8 {
match nibble {
0..=9 => b'0' + nibble,
10..=15 => b'A' + (nibble - 10),
_ => b'?', }
}
fn hex_char_to_nibble(c: u8) -> Result<u8, MbusError> {
match c {
b'0'..=b'9' => Ok(c - b'0'),
b'A'..=b'F' => Ok(c - b'A' + 10),
b'a'..=b'f' => Ok(c - b'a' + 10),
_ => Err(MbusError::BasicParseError),
}
}
fn hex_pair_to_byte(high: u8, low: u8) -> Result<u8, MbusError> {
let h = hex_char_to_nibble(high)?;
let l = hex_char_to_nibble(low)?;
Ok((h << 4) | l)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::function_codes::public::FunctionCode;
use heapless::Vec;
#[test]
fn test_pdu_from_bytes_invalid_no_data() {
let bytes = [0x11];
let err = Pdu::from_bytes(&bytes).expect_err("Should return error for PDU with only FC");
assert_eq!(err, MbusError::InvalidPduLength);
}
#[test]
fn test_pdu_from_bytes_valid_read_coils_request() {
let bytes = [0x01, 0x00, 0x00, 0x00, 0x0A]; let pdu = Pdu::from_bytes(&bytes).expect("Should successfully parse Read Coils request");
assert_eq!(pdu.function_code, FunctionCode::ReadCoils);
assert_eq!(pdu.data_len, 4);
assert_eq!(pdu.data.as_slice(), &[0x00, 0x00, 0x00, 0x0A]);
}
#[test]
fn test_pdu_from_bytes_valid_read_holding_registers_response() {
let bytes = [0x03, 0x04, 0x12, 0x34, 0x56, 0x78]; let pdu = Pdu::from_bytes(&bytes)
.expect("Should successfully parse Read Holding Registers response");
assert_eq!(pdu.function_code, FunctionCode::ReadHoldingRegisters);
assert_eq!(pdu.data_len, 5); assert_eq!(pdu.data.as_slice(), &[0x04, 0x12, 0x34, 0x56, 0x78]);
}
#[test]
fn test_pdu_from_bytes_valid_max_data_length() {
let mut bytes_vec: Vec<u8, 253> = Vec::new();
let _ = bytes_vec.push(0x03); for i in 0..252 {
let _ = bytes_vec.push(i as u8);
}
let bytes = bytes_vec.as_slice();
let pdu = Pdu::from_bytes(bytes).expect("Should parse valid PDU with max data");
assert_eq!(pdu.function_code, FunctionCode::ReadHoldingRegisters);
assert_eq!(pdu.data_len, 252);
assert_eq!(pdu.data.as_slice(), &bytes[1..]);
}
#[test]
fn test_pdu_from_bytes_empty_slice_error() {
let bytes = [];
let err = Pdu::from_bytes(&bytes).expect_err("Should return error for empty slice");
assert_eq!(err, MbusError::InvalidPduLength);
}
#[test]
fn test_pdu_from_bytes_invalid_function_code_error() {
let bytes = [0x00, 0x01, 0x02];
let err = Pdu::from_bytes(&bytes).expect_err("Should return error for invalid FC 0x00");
assert_eq!(err, MbusError::UnsupportedFunction(0x00));
let bytes = [0xFF, 0x01, 0x02];
let err = Pdu::from_bytes(&bytes).expect_err("Should return error for invalid FC 0xFF");
assert_eq!(err, MbusError::UnsupportedFunction(0x7F));
}
#[test]
fn test_pdu_from_bytes_data_too_long_error() {
let mut bytes_vec: Vec<u8, 254> = Vec::new();
let _ = bytes_vec.push(0x03); for i in 0..253 {
let _ = bytes_vec.push(i as u8);
}
let bytes = bytes_vec.as_slice();
let err = Pdu::from_bytes(bytes).expect_err("Should return error for too much data");
assert_eq!(err, MbusError::InvalidPduLength);
}
#[test]
fn test_pdu_to_bytes_no_data() {
let pdu = Pdu::new(FunctionCode::ReportServerId, Vec::new(), 0);
let bytes = pdu.to_bytes().expect("Should convert PDU to bytes");
assert_eq!(bytes.as_slice(), &[0x11]);
}
#[test]
fn test_pdu_to_bytes_with_data() {
let mut data_vec = Vec::new();
data_vec
.extend_from_slice(&[0x00, 0x00, 0x00, 0x0A])
.unwrap();
let pdu = Pdu::new(FunctionCode::ReadCoils, data_vec, 4);
let bytes = pdu.to_bytes().expect("Should convert PDU to bytes");
assert_eq!(bytes.as_slice(), &[0x01, 0x00, 0x00, 0x00, 0x0A]);
}
#[test]
fn test_pdu_to_bytes_max_data() {
let mut data_vec = Vec::new();
for i in 0..252 {
data_vec.push(i as u8).unwrap();
}
let pdu = Pdu::new(FunctionCode::ReadHoldingRegisters, data_vec, 252);
let bytes = pdu
.to_bytes()
.expect("Should convert PDU to bytes with max data");
let mut expected_bytes_vec: Vec<u8, 253> = Vec::new();
let _ = expected_bytes_vec.push(0x03);
for i in 0..252 {
let _ = expected_bytes_vec.push(i as u8);
}
assert_eq!(bytes.as_slice(), expected_bytes_vec.as_slice());
}
#[test]
fn test_modbus_message_to_bytes_tcp() {
let mbap_header = MbapHeader {
transaction_id: 0x1234,
protocol_id: 0x0000,
length: 0x0005, unit_id: 0x01,
};
let mut pdu_data_vec: Vec<u8, MAX_PDU_DATA_LEN> = Vec::new();
pdu_data_vec.extend_from_slice(&[0x00, 0x00, 0x00]).unwrap();
let pdu = Pdu::new(
FunctionCode::ReadHoldingRegisters,
pdu_data_vec,
3, );
let modbus_message = ModbusMessage::new(AdditionalAddress::MbapHeader(mbap_header), pdu);
let adu_bytes = modbus_message
.to_bytes()
.expect("Failed to serialize ModbusMessage");
#[rustfmt::skip]
let expected_adu: [u8; 11] = [
0x12, 0x34, 0x00, 0x00, 0x00, 0x05, 0x01, 0x03, 0x00, 0x00, 0x00, ];
assert_eq!(adu_bytes.as_slice(), &expected_adu);
}
#[test]
fn test_function_code_try_from_valid() {
assert_eq!(
FunctionCode::try_from(0x01).unwrap(),
FunctionCode::ReadCoils
);
assert_eq!(
FunctionCode::try_from(0x08).unwrap(),
FunctionCode::Diagnostics
);
assert_eq!(
FunctionCode::try_from(0x2B).unwrap(),
FunctionCode::EncapsulatedInterfaceTransport
);
assert_eq!(
FunctionCode::try_from(0x18).unwrap(),
FunctionCode::ReadFifoQueue
);
assert_eq!(
FunctionCode::try_from(0x11).unwrap(),
FunctionCode::ReportServerId
);
}
#[test]
fn test_function_code_try_from_invalid() {
let err = FunctionCode::try_from(0x00).expect_err("Should error for invalid FC 0x00");
assert_eq!(err, MbusError::UnsupportedFunction(0x00));
let err =
FunctionCode::try_from(0x09).expect_err("Should error for invalid FC 0x09 (reserved)");
assert_eq!(err, MbusError::UnsupportedFunction(0x09));
let err = FunctionCode::try_from(0x64)
.expect_err("Should error for invalid FC 0x64 (private range, not public)");
assert_eq!(err, MbusError::UnsupportedFunction(0x64));
}
#[test]
fn test_pdu_round_trip_with_data() {
let original_bytes = [0x01, 0x00, 0x00, 0x00, 0x0A]; let pdu = Pdu::from_bytes(&original_bytes).expect("from_bytes failed");
let new_bytes = pdu.to_bytes().expect("to_bytes failed");
assert_eq!(original_bytes.as_slice(), new_bytes.as_slice());
}
#[test]
fn test_pdu_round_trip_max_data() {
let mut original_bytes_vec: Vec<u8, 253> = Vec::new();
let _ = original_bytes_vec.push(0x03); for i in 0..252 {
let _ = original_bytes_vec.push(i as u8);
}
let original_bytes = original_bytes_vec.as_slice();
let pdu = Pdu::from_bytes(original_bytes).expect("from_bytes failed");
let new_bytes = pdu.to_bytes().expect("to_bytes failed");
assert_eq!(original_bytes, new_bytes.as_slice());
}
#[test]
fn test_modbus_message_to_ascii_bytes_valid() {
let slave_addr = SlaveAddress::new(1).unwrap();
let mut data = Vec::new();
data.extend_from_slice(&[0x00, 0x00, 0x00, 0x0A]).unwrap();
let pdu = Pdu::new(FunctionCode::ReadCoils, data, 4);
let message = ModbusMessage::new(AdditionalAddress::SlaveAddress(slave_addr), pdu);
let ascii_bytes = message
.to_ascii_bytes()
.expect("Failed to convert to ASCII");
let expected = b":01010000000AF4\r\n";
assert_eq!(ascii_bytes.as_slice(), expected);
}
#[test]
fn test_modbus_message_to_ascii_bytes_max_capacity() {
let slave_addr = SlaveAddress::new(1).unwrap();
let mut data = Vec::new();
for _ in 0..125 {
data.push(0xAA).unwrap();
}
let pdu = Pdu::new(FunctionCode::ReadHoldingRegisters, data, 125);
let message = ModbusMessage::new(AdditionalAddress::SlaveAddress(slave_addr), pdu);
let ascii_bytes = message.to_ascii_bytes().expect("Should fit in buffer");
assert_eq!(ascii_bytes.len(), 259);
}
#[test]
fn test_modbus_message_to_ascii_bytes_large_payload() {
let slave_addr = SlaveAddress::new(1).unwrap();
let mut data = Vec::new();
for _ in 0..126 {
data.push(0xAA).unwrap();
}
let pdu = Pdu::new(FunctionCode::ReadHoldingRegisters, data, 126);
let message = ModbusMessage::new(AdditionalAddress::SlaveAddress(slave_addr), pdu);
let ascii_bytes = message.to_ascii_bytes().expect("Should fit in buffer");
assert_eq!(ascii_bytes.len(), 261);
}
#[test]
fn test_decompile_adu_frame_tcp_valid() {
let frame = [
0x12, 0x34, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03, 0x00, 0x01, 0x00, 0x02, ];
let msg = decompile_adu_frame(&frame, TransportType::StdTcp)
.expect("Should decode valid TCP frame");
assert_eq!(msg.function_code(), FunctionCode::ReadHoldingRegisters);
if let AdditionalAddress::MbapHeader(header) = msg.additional_address {
assert_eq!(header.transaction_id, 0x1234);
} else {
panic!("Expected MbapHeader");
}
}
#[test]
fn test_decompile_adu_frame_tcp_invalid() {
let frame = [0x00]; let err = decompile_adu_frame(&frame, TransportType::StdTcp).expect_err("Should fail");
assert_eq!(err, MbusError::InvalidAduLength);
}
#[test]
fn test_decompile_adu_frame_rtu_valid() {
let frame = [0x01, 0x03, 0x00, 0x6B, 0x00, 0x03, 0x74, 0x17];
let msg = decompile_adu_frame(&frame, TransportType::StdSerial(SerialMode::Rtu))
.expect("Valid RTU");
assert_eq!(msg.function_code(), FunctionCode::ReadHoldingRegisters);
}
#[test]
fn test_decompile_adu_frame_rtu_too_short() {
let frame = [0x01, 0x02, 0x03];
let err = decompile_adu_frame(&frame, TransportType::StdSerial(SerialMode::Rtu))
.expect_err("Too short");
assert_eq!(err, MbusError::InvalidAduLength);
}
#[test]
fn test_decompile_adu_frame_rtu_crc_mismatch() {
let frame = [0x01, 0x03, 0x00, 0x6B, 0x00, 0x03, 0x00, 0x00]; let err = decompile_adu_frame(&frame, TransportType::StdSerial(SerialMode::Rtu))
.expect_err("CRC mismatch");
assert_eq!(err, MbusError::ChecksumError);
}
#[test]
fn test_decompile_adu_frame_ascii_valid() {
let frame = b":010300000001FB\r\n";
let msg = decompile_adu_frame(frame, TransportType::StdSerial(SerialMode::Ascii))
.expect("Valid ASCII");
assert_eq!(msg.function_code(), FunctionCode::ReadHoldingRegisters);
}
#[test]
fn test_decompile_adu_frame_ascii_too_short() {
let frame = b":123\r\n";
let err = decompile_adu_frame(frame, TransportType::StdSerial(SerialMode::Ascii))
.expect_err("Too short");
assert_eq!(err, MbusError::InvalidAduLength);
}
#[test]
fn test_decompile_adu_frame_ascii_missing_start() {
let frame = b"010300000001FB\r\n";
let err = decompile_adu_frame(frame, TransportType::StdSerial(SerialMode::Ascii))
.expect_err("Missing start");
assert_eq!(err, MbusError::BasicParseError);
}
#[test]
fn test_decompile_adu_frame_ascii_missing_end() {
let frame = b":010300000001FB\r"; let err = decompile_adu_frame(frame, TransportType::StdSerial(SerialMode::Ascii))
.expect_err("Missing end");
assert_eq!(err, MbusError::BasicParseError);
}
#[test]
fn test_decompile_adu_frame_ascii_odd_hex() {
let frame = b":010300000001F\r\n"; let err = decompile_adu_frame(frame, TransportType::StdSerial(SerialMode::Ascii))
.expect_err("Odd hex");
assert_eq!(err, MbusError::BasicParseError);
}
#[test]
fn test_decompile_adu_frame_ascii_lrc_mismatch() {
let frame = b":01030000000100\r\n"; let err = decompile_adu_frame(frame, TransportType::StdSerial(SerialMode::Ascii))
.expect_err("LRC mismatch");
assert_eq!(err, MbusError::ChecksumError);
}
#[test]
fn test_decompile_adu_frame_ascii_buffer_overflow() {
let mut frame = Vec::<u8, 600>::new();
frame.push(b':').unwrap();
for _ in 0..261 {
frame.extend_from_slice(b"00").unwrap();
}
frame.extend_from_slice(b"\r\n").unwrap();
let err = decompile_adu_frame(&frame, TransportType::StdSerial(SerialMode::Ascii))
.expect_err("Buffer overflow");
assert_eq!(err, MbusError::BufferTooSmall);
}
#[test]
fn test_derive_length_tcp() {
let short_frame = [0x00, 0x01, 0x00, 0x00, 0x00];
assert_eq!(
derive_length_from_bytes(&short_frame, TransportType::StdTcp),
None
);
let full_frame = [
0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03, 0x00, 0x00, 0x00, 0x01,
];
assert_eq!(
derive_length_from_bytes(&full_frame, TransportType::StdTcp),
Some(12)
);
let garbage_frame = [
0x00, 0x01, 0xAA, 0xBB, 0x00, 0x06, 0x01, 0x03, 0x00, 0x00, 0x00, 0x01,
];
assert_eq!(
derive_length_from_bytes(&garbage_frame, TransportType::StdTcp),
Some(usize::MAX)
);
}
#[test]
fn test_derive_length_rtu_fixed() {
let request = [0x01, 0x05, 0x00, 0x0A, 0xFF, 0x00, 0x00, 0x00];
assert_eq!(
derive_length_from_bytes(&request, TransportType::StdSerial(SerialMode::Rtu)),
Some(8)
);
}
#[test]
fn test_derive_length_rtu_dynamic() {
let mut resp = [0x01, 0x03, 0x02, 0x12, 0x34, 0x00, 0x00];
let crc = checksum::crc16(&resp[..5]);
let crc_bytes = crc.to_le_bytes();
resp[5] = crc_bytes[0];
resp[6] = crc_bytes[1];
assert_eq!(
derive_length_from_bytes(&resp, TransportType::StdSerial(SerialMode::Rtu)),
Some(7)
);
assert_eq!(
derive_length_from_bytes(&resp[..4], TransportType::StdSerial(SerialMode::Rtu)),
Some(8)
);
}
#[test]
fn test_derive_length_rtu_exception() {
let exception = [0x01, 0x81, 0x02, 0x00, 0x00];
assert_eq!(
derive_length_from_bytes(&exception, TransportType::StdSerial(SerialMode::Rtu)),
Some(5)
);
}
#[test]
fn test_derive_length_rtu_forward_scan() {
let mut custom_frame = [0x01, 0x44, 0xAA, 0xBB, 0x00, 0x00];
let crc = checksum::crc16(&custom_frame[..4]);
let crc_bytes = crc.to_le_bytes();
custom_frame[4] = crc_bytes[0];
custom_frame[5] = crc_bytes[1];
assert_eq!(
derive_length_from_bytes(&custom_frame, TransportType::StdSerial(SerialMode::Rtu)),
Some(6)
);
assert_eq!(
derive_length_from_bytes(
&custom_frame[..4],
TransportType::StdSerial(SerialMode::Rtu)
),
None
);
}
#[test]
fn test_derive_length_ascii() {
let frame = b":010300000001FB\r\n";
assert_eq!(
derive_length_from_bytes(frame, TransportType::StdSerial(SerialMode::Ascii)),
Some(17)
);
let partial = b":010300000001F";
assert_eq!(
derive_length_from_bytes(partial, TransportType::StdSerial(SerialMode::Ascii)),
None
);
}
}