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;
pub const PDU_ADDRESS_OFFSET_1B: usize = 0;
pub const PDU_ADDRESS_OFFSET_2B: usize = PDU_ADDRESS_OFFSET_1B + 1;
pub const PDU_QUANTITY_OFFSET_1B: usize = 2;
pub const PDU_QUANTITY_OFFSET_2B: usize = PDU_QUANTITY_OFFSET_1B + 1;
pub const PDU_AND_MASK_OFFSET_1B: usize = 2;
pub const PDU_AND_MASK_OFFSET_2B: usize = PDU_AND_MASK_OFFSET_1B + 1;
pub const PDU_OR_MASK_OFFSET_1B: usize = 4;
pub const PDU_OR_MASK_OFFSET_2B: usize = PDU_OR_MASK_OFFSET_1B + 1;
pub const PDU_BYTE_COUNT_OFFSET: usize = 4;
pub const PDU_FC17_WRITE_ADDRESS_OFFSET_1B: usize = 4;
pub const PDU_FC17_WRITE_ADDRESS_OFFSET_2B: usize = PDU_FC17_WRITE_ADDRESS_OFFSET_1B + 1;
pub const PDU_FC17_WRITE_QUANTITY_OFFSET_1B: usize = 6;
pub const PDU_FC17_WRITE_QUANTITY_OFFSET_2B: usize = PDU_FC17_WRITE_QUANTITY_OFFSET_1B + 1;
pub const PDU_FC17_WRITE_BYTE_COUNT_OFFSET: usize = 8;
pub const PDU_FC17_WRITE_VALUES_OFFSET: usize = 9;
pub const PDU_SUB_FUNCTION_OFFSET_1B: usize = 0;
pub const PDU_SUB_FUNCTION_OFFSET_2B: usize = 1;
pub const PDU_MEI_TYPE_OFFSET: usize = 0;
pub const PDU_MEI_READ_CODE_OFFSET: usize = 1;
pub const PDU_MEI_CONFORMITY_LEVEL_OFFSET: usize = 2;
pub const PDU_MEI_MORE_FOLLOWS_OFFSET: usize = 3;
pub const PDU_MEI_NEXT_OBJECT_ID_OFFSET: usize = 4;
pub const PDU_MEI_NUM_OBJECTS_OFFSET: usize = 5;
pub const PDU_MEI_OBJECTS_DATA_OFFSET: usize = 6;
pub const PDU_FIFO_BYTE_COUNT_OFFSET_1B: usize = 0;
pub const PDU_FIFO_BYTE_COUNT_OFFSET_2B: usize = 1;
pub const PDU_FIFO_COUNT_OFFSET_1B: usize = 2;
pub const PDU_FIFO_COUNT_OFFSET_2B: usize = 3;
pub const PDU_FIFO_VALUES_OFFSET: usize = 4;
#[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, PartialEq, Eq)]
pub struct ReadWindow {
pub address: u16,
pub quantity: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WriteSingleU16Fields {
pub address: u16,
pub value: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WriteMultipleFields<'a> {
pub address: u16,
pub quantity: u16,
pub byte_count: u8,
pub values: &'a [u8],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MaskWriteRegisterFields {
pub address: u16,
pub and_mask: u16,
pub or_mask: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ByteCountPayload<'a> {
pub byte_count: u8,
pub payload: &'a [u8],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReadWriteMultipleFields<'a> {
pub read_address: u16,
pub read_quantity: u16,
pub write_address: u16,
pub write_quantity: u16,
pub write_byte_count: u8,
pub write_values: &'a [u8],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SubFunctionPayload<'a> {
pub sub_function: u16,
pub payload: &'a [u8],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct U16PairFields {
pub first: u16,
pub second: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MeiTypePayload<'a> {
pub mei_type_byte: u8,
pub payload: &'a [u8],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FifoPayload<'a> {
pub fifo_byte_count: u16,
pub fifo_count: u16,
pub values: &'a [u8],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadDeviceIdPduFields {
pub mei_type_byte: u8,
pub read_device_id_code_byte: u8,
pub conformity_level_byte: u8,
pub more_follows: bool,
pub next_object_id_byte: u8,
pub number_of_objects: u8,
pub objects_data: [u8; MAX_PDU_DATA_LEN],
pub payload_len: usize,
}
#[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) => match header.unit_id {
0 => UnitIdOrSlaveAddr::new_broadcast_address(),
unit_id => {
UnitIdOrSlaveAddr::try_from(unit_id).unwrap_or(UnitIdOrSlaveAddr::default())
}
},
AdditionalAddress::SlaveAddress(slave_address) => match slave_address.address() {
0 => UnitIdOrSlaveAddr::new_broadcast_address(),
address => {
UnitIdOrSlaveAddr::try_from(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 build_read_window(
fc: FunctionCode,
address: u16,
quantity: u16,
) -> Result<Self, MbusError> {
let mut data: heapless::Vec<u8, MAX_PDU_DATA_LEN> = heapless::Vec::new();
data.extend_from_slice(&address.to_be_bytes())
.map_err(|_| MbusError::BufferLenMissmatch)?;
data.extend_from_slice(&quantity.to_be_bytes())
.map_err(|_| MbusError::BufferLenMissmatch)?;
Ok(Pdu::new(fc, data, 4))
}
pub fn build_write_single_u16(
fc: FunctionCode,
address: u16,
value: u16,
) -> Result<Self, MbusError> {
let mut data: heapless::Vec<u8, MAX_PDU_DATA_LEN> = heapless::Vec::new();
data.extend_from_slice(&address.to_be_bytes())
.map_err(|_| MbusError::BufferLenMissmatch)?;
data.extend_from_slice(&value.to_be_bytes())
.map_err(|_| MbusError::BufferLenMissmatch)?;
Ok(Pdu::new(fc, data, 4))
}
pub fn build_write_multiple(
fc: FunctionCode,
address: u16,
quantity: u16,
values: &[u8],
) -> Result<Self, MbusError> {
let byte_count = values.len();
if byte_count > u8::MAX as usize {
return Err(MbusError::InvalidByteCount);
}
let data_len = 5 + byte_count;
if data_len > MAX_PDU_DATA_LEN {
return Err(MbusError::BufferTooSmall);
}
let mut data: heapless::Vec<u8, MAX_PDU_DATA_LEN> = heapless::Vec::new();
data.extend_from_slice(&address.to_be_bytes())
.map_err(|_| MbusError::BufferLenMissmatch)?;
data.extend_from_slice(&quantity.to_be_bytes())
.map_err(|_| MbusError::BufferLenMissmatch)?;
data.push(byte_count as u8)
.map_err(|_| MbusError::BufferLenMissmatch)?;
data.extend_from_slice(values)
.map_err(|_| MbusError::BufferLenMissmatch)?;
Ok(Pdu::new(fc, data, data_len as u8))
}
pub fn build_mask_write_register(
address: u16,
and_mask: u16,
or_mask: u16,
) -> Result<Self, MbusError> {
let mut data: heapless::Vec<u8, MAX_PDU_DATA_LEN> = heapless::Vec::new();
data.extend_from_slice(&address.to_be_bytes())
.map_err(|_| MbusError::BufferLenMissmatch)?;
data.extend_from_slice(&and_mask.to_be_bytes())
.map_err(|_| MbusError::BufferLenMissmatch)?;
data.extend_from_slice(&or_mask.to_be_bytes())
.map_err(|_| MbusError::BufferLenMissmatch)?;
Ok(Pdu::new(FunctionCode::MaskWriteRegister, data, 6))
}
pub fn build_read_write_multiple(
read_address: u16,
read_quantity: u16,
write_address: u16,
write_quantity: u16,
write_values: &[u8],
) -> Result<Self, MbusError> {
let byte_count = write_values.len();
if byte_count > u8::MAX as usize {
return Err(MbusError::InvalidByteCount);
}
let data_len = 9 + byte_count;
if data_len > MAX_PDU_DATA_LEN {
return Err(MbusError::BufferTooSmall);
}
let mut data: heapless::Vec<u8, MAX_PDU_DATA_LEN> = heapless::Vec::new();
data.extend_from_slice(&read_address.to_be_bytes())
.map_err(|_| MbusError::BufferLenMissmatch)?;
data.extend_from_slice(&read_quantity.to_be_bytes())
.map_err(|_| MbusError::BufferLenMissmatch)?;
data.extend_from_slice(&write_address.to_be_bytes())
.map_err(|_| MbusError::BufferLenMissmatch)?;
data.extend_from_slice(&write_quantity.to_be_bytes())
.map_err(|_| MbusError::BufferLenMissmatch)?;
data.push(byte_count as u8)
.map_err(|_| MbusError::BufferLenMissmatch)?;
data.extend_from_slice(write_values)
.map_err(|_| MbusError::BufferLenMissmatch)?;
Ok(Pdu::new(
FunctionCode::ReadWriteMultipleRegisters,
data,
data_len as u8,
))
}
pub fn build_sub_function(
fc: FunctionCode,
sub_function: u16,
words: &[u16],
) -> Result<Self, MbusError> {
let data_len = 2 + words.len() * 2;
if data_len > MAX_PDU_DATA_LEN {
return Err(MbusError::BufferTooSmall);
}
let mut data: heapless::Vec<u8, MAX_PDU_DATA_LEN> = heapless::Vec::new();
data.extend_from_slice(&sub_function.to_be_bytes())
.map_err(|_| MbusError::BufferLenMissmatch)?;
for &word in words {
data.extend_from_slice(&word.to_be_bytes())
.map_err(|_| MbusError::BufferLenMissmatch)?;
}
Ok(Pdu::new(fc, data, data_len as u8))
}
pub fn build_mei_type(
fc: FunctionCode,
mei_type: u8,
payload: &[u8],
) -> Result<Self, MbusError> {
let data_len = 1 + payload.len();
if data_len > MAX_PDU_DATA_LEN {
return Err(MbusError::BufferTooSmall);
}
let mut data: heapless::Vec<u8, MAX_PDU_DATA_LEN> = heapless::Vec::new();
data.push(mei_type)
.map_err(|_| MbusError::BufferLenMissmatch)?;
data.extend_from_slice(payload)
.map_err(|_| MbusError::BufferLenMissmatch)?;
Ok(Pdu::new(fc, data, data_len as u8))
}
pub fn build_u16_payload(fc: FunctionCode, value: u16) -> Result<Self, MbusError> {
let mut data: heapless::Vec<u8, MAX_PDU_DATA_LEN> = heapless::Vec::new();
data.extend_from_slice(&value.to_be_bytes())
.map_err(|_| MbusError::BufferLenMissmatch)?;
Ok(Pdu::new(fc, data, 2))
}
pub fn build_byte_payload(fc: FunctionCode, byte: u8) -> Result<Self, MbusError> {
let mut data: heapless::Vec<u8, MAX_PDU_DATA_LEN> = heapless::Vec::new();
data.push(byte).map_err(|_| MbusError::BufferLenMissmatch)?;
Ok(Pdu::new(fc, data, 1))
}
pub fn build_empty(fc: FunctionCode) -> Self {
Pdu::new(fc, heapless::Vec::new(), 0)
}
pub fn build_byte_count_payload(fc: FunctionCode, payload: &[u8]) -> Result<Self, MbusError> {
let byte_count = u8::try_from(payload.len()).map_err(|_| MbusError::InvalidByteCount)?;
let data_len = 1 + payload.len();
if data_len > MAX_PDU_DATA_LEN {
return Err(MbusError::BufferTooSmall);
}
let mut data: heapless::Vec<u8, MAX_PDU_DATA_LEN> = heapless::Vec::new();
data.push(byte_count)
.map_err(|_| MbusError::BufferLenMissmatch)?;
data.extend_from_slice(payload)
.map_err(|_| MbusError::BufferLenMissmatch)?;
Ok(Pdu::new(fc, data, data_len as u8))
}
#[cfg(feature = "diagnostics")]
pub fn build_diagnostics(sub_function: u16, result: u16) -> Result<Self, MbusError> {
let mut data: heapless::Vec<u8, MAX_PDU_DATA_LEN> = heapless::Vec::new();
data.extend_from_slice(&sub_function.to_be_bytes())
.map_err(|_| MbusError::BufferLenMissmatch)?;
data.extend_from_slice(&result.to_be_bytes())
.map_err(|_| MbusError::BufferLenMissmatch)?;
Ok(Pdu::new(FunctionCode::Diagnostics, data, 4))
}
#[cfg(feature = "fifo")]
pub fn build_fifo_payload(app_payload: &[u8]) -> Result<Self, MbusError> {
let byte_count =
u16::try_from(app_payload.len()).map_err(|_| MbusError::InvalidByteCount)?;
let data_len = 2 + app_payload.len();
if data_len > MAX_PDU_DATA_LEN {
return Err(MbusError::BufferTooSmall);
}
let mut data: heapless::Vec<u8, MAX_PDU_DATA_LEN> = heapless::Vec::new();
data.extend_from_slice(&byte_count.to_be_bytes())
.map_err(|_| MbusError::BufferLenMissmatch)?;
data.extend_from_slice(app_payload)
.map_err(|_| MbusError::BufferLenMissmatch)?;
Ok(Pdu::new(FunctionCode::ReadFifoQueue, data, data_len as u8))
}
#[inline]
fn read_address_quantity_pair(&self) -> Result<(u16, u16), MbusError> {
Ok((
u16::from_be_bytes([
self.data[PDU_ADDRESS_OFFSET_1B],
self.data[PDU_ADDRESS_OFFSET_2B],
]),
u16::from_be_bytes([
self.data[PDU_QUANTITY_OFFSET_1B],
self.data[PDU_QUANTITY_OFFSET_2B],
]),
))
}
pub fn read_window(&self) -> Result<ReadWindow, MbusError> {
if self.data_len != 4 {
return Err(MbusError::InvalidPduLength);
}
let (address, quantity) = self.read_address_quantity_pair()?;
Ok(ReadWindow { address, quantity })
}
pub fn write_single_u16_fields(&self) -> Result<WriteSingleU16Fields, MbusError> {
if self.data_len != 4 {
return Err(MbusError::InvalidPduLength);
}
Ok(WriteSingleU16Fields {
address: u16::from_be_bytes([
self.data[PDU_ADDRESS_OFFSET_1B],
self.data[PDU_ADDRESS_OFFSET_2B],
]),
value: u16::from_be_bytes([
self.data[PDU_QUANTITY_OFFSET_1B],
self.data[PDU_QUANTITY_OFFSET_2B],
]),
})
}
pub fn write_multiple_fields(&self) -> Result<WriteMultipleFields<'_>, MbusError> {
if self.data_len < 5 {
return Err(MbusError::InvalidPduLength);
}
let (address, quantity) = self.read_address_quantity_pair()?;
let byte_count = self.data[PDU_BYTE_COUNT_OFFSET];
let expected_len = 5usize
.checked_add(byte_count as usize)
.ok_or(MbusError::InvalidByteCount)?;
if self.data_len as usize != expected_len {
return Err(MbusError::InvalidByteCount);
}
Ok(WriteMultipleFields {
address,
quantity,
byte_count,
values: &self.data[5..expected_len],
})
}
pub fn mask_write_register_fields(&self) -> Result<MaskWriteRegisterFields, MbusError> {
if self.data_len != 6 {
return Err(MbusError::InvalidPduLength);
}
Ok(MaskWriteRegisterFields {
address: u16::from_be_bytes([
self.data[PDU_ADDRESS_OFFSET_1B],
self.data[PDU_ADDRESS_OFFSET_2B],
]),
and_mask: u16::from_be_bytes([
self.data[PDU_AND_MASK_OFFSET_1B],
self.data[PDU_AND_MASK_OFFSET_2B],
]),
or_mask: u16::from_be_bytes([
self.data[PDU_OR_MASK_OFFSET_1B],
self.data[PDU_OR_MASK_OFFSET_2B],
]),
})
}
pub fn byte_count_payload(&self) -> Result<ByteCountPayload<'_>, MbusError> {
if self.data_len < 1 {
return Err(MbusError::InvalidPduLength);
}
let byte_count = self.data[0];
let expected_len = 1usize
.checked_add(byte_count as usize)
.ok_or(MbusError::InvalidByteCount)?;
if self.data_len as usize != expected_len {
return Err(MbusError::InvalidByteCount);
}
Ok(ByteCountPayload {
byte_count,
payload: &self.data[1..expected_len],
})
}
pub fn read_write_multiple_fields(&self) -> Result<ReadWriteMultipleFields<'_>, MbusError> {
if (self.data_len as usize) < PDU_FC17_WRITE_VALUES_OFFSET {
return Err(MbusError::InvalidPduLength);
}
let (read_address, read_quantity) = self.read_address_quantity_pair()?;
let write_address = u16::from_be_bytes([
self.data[PDU_FC17_WRITE_ADDRESS_OFFSET_1B],
self.data[PDU_FC17_WRITE_ADDRESS_OFFSET_2B],
]);
let write_quantity = u16::from_be_bytes([
self.data[PDU_FC17_WRITE_QUANTITY_OFFSET_1B],
self.data[PDU_FC17_WRITE_QUANTITY_OFFSET_2B],
]);
let write_byte_count = self.data[PDU_FC17_WRITE_BYTE_COUNT_OFFSET];
let expected_len = PDU_FC17_WRITE_VALUES_OFFSET
.checked_add(write_byte_count as usize)
.ok_or(MbusError::InvalidByteCount)?;
if self.data_len as usize != expected_len {
return Err(MbusError::InvalidByteCount);
}
Ok(ReadWriteMultipleFields {
read_address,
read_quantity,
write_address,
write_quantity,
write_byte_count,
write_values: &self.data[PDU_FC17_WRITE_VALUES_OFFSET..expected_len],
})
}
pub fn single_byte_payload(&self) -> Result<u8, MbusError> {
if self.data_len != 1 {
return Err(MbusError::InvalidPduLength);
}
Ok(self.data[0])
}
pub fn sub_function_payload(&self) -> Result<SubFunctionPayload<'_>, MbusError> {
if self.data_len < 2 {
return Err(MbusError::InvalidPduLength);
}
if !self.data_len.is_multiple_of(2) {
return Err(MbusError::InvalidPduLength);
}
Ok(SubFunctionPayload {
sub_function: u16::from_be_bytes([
self.data[PDU_SUB_FUNCTION_OFFSET_1B],
self.data[PDU_SUB_FUNCTION_OFFSET_2B],
]),
payload: &self.data[2..self.data_len as usize],
})
}
pub fn u16_pair_fields(&self) -> Result<U16PairFields, MbusError> {
if self.data_len != 4 {
return Err(MbusError::InvalidPduLength);
}
Ok(U16PairFields {
first: u16::from_be_bytes([
self.data[PDU_ADDRESS_OFFSET_1B],
self.data[PDU_ADDRESS_OFFSET_2B],
]),
second: u16::from_be_bytes([
self.data[PDU_QUANTITY_OFFSET_1B],
self.data[PDU_QUANTITY_OFFSET_2B],
]),
})
}
pub fn mei_type_payload(&self) -> Result<MeiTypePayload<'_>, MbusError> {
if self.data_len < 1 {
return Err(MbusError::InvalidPduLength);
}
Ok(MeiTypePayload {
mei_type_byte: self.data[PDU_MEI_TYPE_OFFSET],
payload: &self.data[1..self.data_len as usize],
})
}
pub fn fifo_payload(&self) -> Result<FifoPayload<'_>, MbusError> {
if self.data_len < 4 {
return Err(MbusError::InvalidPduLength);
}
Ok(FifoPayload {
fifo_byte_count: u16::from_be_bytes([
self.data[PDU_FIFO_BYTE_COUNT_OFFSET_1B],
self.data[PDU_FIFO_BYTE_COUNT_OFFSET_2B],
]),
fifo_count: u16::from_be_bytes([
self.data[PDU_FIFO_COUNT_OFFSET_1B],
self.data[PDU_FIFO_COUNT_OFFSET_2B],
]),
values: &self.data[PDU_FIFO_VALUES_OFFSET..self.data_len as usize],
})
}
#[cfg(feature = "fifo")]
pub fn fifo_pointer(&self) -> Result<u16, MbusError> {
if self.data_len != 2 {
return Err(MbusError::InvalidPduLength);
}
Ok(u16::from_be_bytes([
self.data[PDU_ADDRESS_OFFSET_1B],
self.data[PDU_ADDRESS_OFFSET_2B],
]))
}
#[cfg(feature = "diagnostics")]
pub fn diagnostics_fields(&self) -> Result<(u16, u16), MbusError> {
if self.data_len < 4 {
return Err(MbusError::InvalidPduLength);
}
let sub_function = u16::from_be_bytes([
self.data[PDU_SUB_FUNCTION_OFFSET_1B],
self.data[PDU_SUB_FUNCTION_OFFSET_2B],
]);
let data_word = u16::from_be_bytes([self.data[2], self.data[3]]);
Ok((sub_function, data_word))
}
#[cfg(feature = "file-record")]
pub fn file_record_read_sub_requests(
&self,
) -> Result<
heapless::Vec<
crate::models::file_record::FileRecordReadSubRequest,
{ crate::models::file_record::MAX_SUB_REQUESTS_PER_PDU },
>,
MbusError,
> {
use crate::models::file_record::{FILE_RECORD_REF_TYPE, FileRecordReadSubRequest};
const FILE_RECORD_READ_SUB_REQUEST_LEN: usize = 7;
const FILE_RECORD_MAX_REQUEST_BYTE_COUNT: usize = 245;
let data_len = self.data_len as usize;
if data_len < 1 + FILE_RECORD_READ_SUB_REQUEST_LEN {
return Err(MbusError::InvalidPduLength);
}
let data = self.data.as_slice();
let byte_count = data[0] as usize;
if byte_count != data_len - 1 {
return Err(MbusError::InvalidByteCount);
}
if byte_count > FILE_RECORD_MAX_REQUEST_BYTE_COUNT {
return Err(MbusError::InvalidByteCount);
}
if !byte_count.is_multiple_of(FILE_RECORD_READ_SUB_REQUEST_LEN) {
return Err(MbusError::InvalidByteCount);
}
let mut out: heapless::Vec<
FileRecordReadSubRequest,
{ crate::models::file_record::MAX_SUB_REQUESTS_PER_PDU },
> = heapless::Vec::new();
let mut index = 1usize;
while index < data_len {
if data[index] != FILE_RECORD_REF_TYPE {
return Err(MbusError::InvalidValue);
}
let file_number = u16::from_be_bytes([data[index + 1], data[index + 2]]);
let record_number = u16::from_be_bytes([data[index + 3], data[index + 4]]);
let record_length = u16::from_be_bytes([data[index + 5], data[index + 6]]);
if record_length == 0 {
return Err(MbusError::InvalidQuantity);
}
out.push(FileRecordReadSubRequest {
file_number,
record_number,
record_length,
})
.map_err(|_| MbusError::TooManyFileReadSubRequests)?;
index += FILE_RECORD_READ_SUB_REQUEST_LEN;
}
Ok(out)
}
#[cfg(feature = "file-record")]
pub fn file_record_write_sub_requests<'a>(
&'a self,
) -> Result<
heapless::Vec<
crate::models::file_record::FileRecordWriteSubRequest<'a>,
{ crate::models::file_record::MAX_SUB_REQUESTS_PER_PDU },
>,
MbusError,
> {
use crate::models::file_record::{FILE_RECORD_REF_TYPE, FileRecordWriteSubRequest};
const FILE_RECORD_WRITE_SUB_REQUEST_HEADER_LEN: usize = 7;
const FILE_RECORD_MAX_REQUEST_BYTE_COUNT: usize = 245;
let data_len = self.data_len as usize;
if data_len < 1 + FILE_RECORD_WRITE_SUB_REQUEST_HEADER_LEN + 2 {
return Err(MbusError::InvalidPduLength);
}
let data = self.data.as_slice();
let byte_count = data[0] as usize;
if byte_count != data_len - 1 {
return Err(MbusError::InvalidByteCount);
}
if byte_count > FILE_RECORD_MAX_REQUEST_BYTE_COUNT {
return Err(MbusError::InvalidByteCount);
}
let mut out: heapless::Vec<
FileRecordWriteSubRequest,
{ crate::models::file_record::MAX_SUB_REQUESTS_PER_PDU },
> = heapless::Vec::new();
let mut index = 1usize;
while index < data_len {
if index + FILE_RECORD_WRITE_SUB_REQUEST_HEADER_LEN > data_len {
return Err(MbusError::InvalidPduLength);
}
if data[index] != FILE_RECORD_REF_TYPE {
return Err(MbusError::InvalidValue);
}
let file_number = u16::from_be_bytes([data[index + 1], data[index + 2]]);
let record_number = u16::from_be_bytes([data[index + 3], data[index + 4]]);
let record_length = u16::from_be_bytes([data[index + 5], data[index + 6]]);
if record_length == 0 {
return Err(MbusError::InvalidQuantity);
}
let data_bytes_len = record_length as usize * 2;
let end_index = index
.checked_add(FILE_RECORD_WRITE_SUB_REQUEST_HEADER_LEN + data_bytes_len)
.ok_or(MbusError::InvalidByteCount)?;
if end_index > data_len {
return Err(MbusError::InvalidByteCount);
}
out.push(FileRecordWriteSubRequest {
file_number,
record_number,
record_length,
record_data_bytes: &data
[index + FILE_RECORD_WRITE_SUB_REQUEST_HEADER_LEN..end_index],
})
.map_err(|_| MbusError::TooManyFileReadSubRequests)?;
index = end_index;
}
if out.is_empty() {
return Err(MbusError::InvalidPduLength);
}
Ok(out)
}
pub fn read_device_id_fields(&self) -> Result<ReadDeviceIdPduFields, MbusError> {
if (self.data_len as usize) < PDU_MEI_OBJECTS_DATA_OFFSET {
return Err(MbusError::InvalidPduLength);
}
let data = self.data.as_slice();
let number_of_objects = data[PDU_MEI_NUM_OBJECTS_OFFSET];
let mut offset = PDU_MEI_OBJECTS_DATA_OFFSET;
for _ in 0..number_of_objects as usize {
if offset + 2 > data.len() {
return Err(MbusError::InvalidPduLength);
}
let obj_len = data[offset + 1] as usize;
offset += 2;
if offset + obj_len > data.len() {
return Err(MbusError::InvalidPduLength);
}
offset += obj_len;
}
let payload_len = (self.data_len as usize) - PDU_MEI_OBJECTS_DATA_OFFSET;
if payload_len > MAX_PDU_DATA_LEN {
return Err(MbusError::BufferTooSmall);
}
let mut objects_data = [0u8; MAX_PDU_DATA_LEN];
if payload_len > 0 {
objects_data[..payload_len].copy_from_slice(&data[PDU_MEI_OBJECTS_DATA_OFFSET..]);
}
Ok(ReadDeviceIdPduFields {
mei_type_byte: data[PDU_MEI_TYPE_OFFSET],
read_device_id_code_byte: data[PDU_MEI_READ_CODE_OFFSET],
conformity_level_byte: data[PDU_MEI_CONFORMITY_LEVEL_OFFSET],
more_follows: data[PDU_MEI_MORE_FOLLOWS_OFFSET] == 0xFF,
next_object_id_byte: data[PDU_MEI_NEXT_OBJECT_ID_OFFSET],
number_of_objects,
objects_data,
payload_len,
})
}
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() {
return Err(MbusError::InvalidPduLength);
}
let error_code = if is_exception_code(bytes[0]) {
if bytes.len() < 2 {
return Err(MbusError::InvalidPduLength);
}
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_valid_no_data() {
let bytes = [0x11];
let pdu = Pdu::from_bytes(&bytes).expect("Should parse a function-code-only PDU");
assert_eq!(pdu.function_code, FunctionCode::ReportServerId);
assert_eq!(pdu.data_len, 0);
assert!(pdu.data.is_empty());
assert_eq!(pdu.error_code, None);
}
#[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_read_window_parses_expected_fields() {
let pdu = Pdu::from_bytes(&[0x03, 0x12, 0x34, 0x00, 0x02]).expect("valid pdu");
let parsed = pdu.read_window().expect("read window should parse");
assert_eq!(
parsed,
ReadWindow {
address: 0x1234,
quantity: 0x0002
}
);
}
#[test]
fn test_pdu_write_single_u16_fields_parses_expected_fields() {
let pdu = Pdu::from_bytes(&[0x06, 0x01, 0x02, 0xAB, 0xCD]).expect("valid pdu");
let parsed = pdu
.write_single_u16_fields()
.expect("write single fields should parse");
assert_eq!(
parsed,
WriteSingleU16Fields {
address: 0x0102,
value: 0xABCD
}
);
}
#[test]
fn test_pdu_write_multiple_fields_parses_expected_fields() {
let pdu = Pdu::from_bytes(&[0x10, 0x00, 0x20, 0x00, 0x02, 0x04, 0x12, 0x34, 0x56, 0x78])
.expect("valid pdu");
let parsed = pdu
.write_multiple_fields()
.expect("write multiple fields should parse");
assert_eq!(parsed.address, 0x0020);
assert_eq!(parsed.quantity, 0x0002);
assert_eq!(parsed.byte_count, 0x04);
assert_eq!(parsed.values, &[0x12, 0x34, 0x56, 0x78]);
}
#[test]
fn test_pdu_mask_write_register_fields_parses_expected_fields() {
let pdu = Pdu::from_bytes(&[0x16, 0x00, 0x10, 0xFF, 0x00, 0x00, 0x0F]).expect("valid pdu");
let parsed = pdu
.mask_write_register_fields()
.expect("mask write fields should parse");
assert_eq!(
parsed,
MaskWriteRegisterFields {
address: 0x0010,
and_mask: 0xFF00,
or_mask: 0x000F
}
);
}
#[test]
fn test_pdu_byte_count_payload_parses_expected_fields() {
let pdu = Pdu::from_bytes(&[0x03, 0x04, 0x12, 0x34, 0x56, 0x78]).expect("valid pdu");
let parsed = pdu
.byte_count_payload()
.expect("byte-count payload should parse");
assert_eq!(parsed.byte_count, 0x04);
assert_eq!(parsed.payload, &[0x12, 0x34, 0x56, 0x78]);
}
#[test]
fn test_pdu_write_multiple_fields_rejects_mismatched_byte_count() {
let pdu =
Pdu::from_bytes(&[0x10, 0x00, 0x20, 0x00, 0x02, 0x03, 0x12, 0x34]).expect("valid pdu");
let err = pdu
.write_multiple_fields()
.expect_err("byte count mismatch should error");
assert_eq!(err, MbusError::InvalidByteCount);
}
#[test]
fn test_pdu_byte_count_payload_rejects_mismatched_byte_count() {
let pdu = Pdu::from_bytes(&[0x03, 0x03, 0x12, 0x34]).expect("valid pdu");
let err = pdu
.byte_count_payload()
.expect_err("byte count mismatch should error");
assert_eq!(err, MbusError::InvalidByteCount);
}
#[test]
fn test_pdu_read_write_multiple_fields_parses_expected_fields() {
let pdu = Pdu::from_bytes(&[
0x17, 0x00, 0x10, 0x00, 0x02, 0x00, 0x20, 0x00, 0x02, 0x04, 0x12, 0x34, 0x56, 0x78,
])
.expect("valid pdu");
let parsed = pdu
.read_write_multiple_fields()
.expect("read/write multiple fields should parse");
assert_eq!(parsed.read_address, 0x0010);
assert_eq!(parsed.read_quantity, 0x0002);
assert_eq!(parsed.write_address, 0x0020);
assert_eq!(parsed.write_quantity, 0x0002);
assert_eq!(parsed.write_byte_count, 0x04);
assert_eq!(parsed.write_values, &[0x12, 0x34, 0x56, 0x78]);
}
#[test]
fn test_pdu_read_write_multiple_fields_rejects_short_pdu() {
let pdu = Pdu::from_bytes(&[0x17, 0x00, 0x10, 0x00, 0x02, 0x00]).expect("valid pdu");
let err = pdu
.read_write_multiple_fields()
.expect_err("PDU too short should error");
assert_eq!(err, MbusError::InvalidPduLength);
}
#[test]
fn test_pdu_read_write_multiple_fields_rejects_mismatched_byte_count() {
let pdu = Pdu::from_bytes(&[
0x17, 0x00, 0x10, 0x00, 0x02, 0x00, 0x20, 0x00, 0x02, 0x05, 0x12, 0x34, 0x56, 0x78,
])
.expect("valid pdu");
let err = pdu
.read_write_multiple_fields()
.expect_err("byte count mismatch should error");
assert_eq!(err, MbusError::InvalidByteCount);
}
#[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
);
}
}