#[cfg(not(feature = "std"))]
use alloc::{format, string::ToString, vec};
use crate::constants::{MAX_PDU_SIZE, MAX_WRITE_COILS, MAX_WRITE_REGISTERS};
use crate::error::{ModbusError, ModbusResult};
#[derive(Debug, Clone)]
pub struct ModbusPdu {
data: [u8; MAX_PDU_SIZE],
len: usize,
}
impl ModbusPdu {
#[inline]
pub fn new() -> Self {
Self {
data: [0; MAX_PDU_SIZE],
len: 0,
}
}
#[inline]
pub fn from_slice(data: &[u8]) -> ModbusResult<Self> {
#[cfg(feature = "std")]
tracing::debug!("Parsing PDU from slice: {} bytes", data.len());
if data.len() > MAX_PDU_SIZE {
return Err(ModbusError::Protocol {
message: format!("PDU too large: {} bytes (max {})", data.len(), MAX_PDU_SIZE),
});
}
let mut pdu = Self::new();
pdu.data[..data.len()].copy_from_slice(data);
pdu.len = data.len();
#[cfg(feature = "std")]
if let Some(fc) = pdu.function_code() {
let fc_desc = Self::function_code_description(fc);
if pdu.is_exception() {
let exc_code = pdu.exception_code().unwrap_or(0);
tracing::debug!(
"PDU parsed: FC={:02X} (Exception: {}), exception_code={:02X}",
fc,
fc_desc,
exc_code
);
} else {
tracing::debug!(
"PDU parsed: FC={:02X} ({}), data_len={}",
fc,
fc_desc,
pdu.len - 1
);
}
} else {
tracing::debug!("PDU parsed: empty PDU");
}
Ok(pdu)
}
#[inline]
pub fn push(&mut self, byte: u8) -> ModbusResult<()> {
if self.len >= MAX_PDU_SIZE {
return Err(ModbusError::Protocol {
message: "PDU buffer full".to_string(),
});
}
self.data[self.len] = byte;
self.len += 1;
Ok(())
}
#[inline]
pub fn push_u16(&mut self, value: u16) -> ModbusResult<()> {
self.push((value >> 8) as u8)?;
self.push((value & 0xFF) as u8)?;
Ok(())
}
#[inline]
pub fn extend(&mut self, data: &[u8]) -> ModbusResult<()> {
if self.len + data.len() > MAX_PDU_SIZE {
return Err(ModbusError::Protocol {
message: format!(
"PDU would exceed max size: {} + {} > {}",
self.len,
data.len(),
MAX_PDU_SIZE
),
});
}
self.data[self.len..self.len + data.len()].copy_from_slice(data);
self.len += data.len();
Ok(())
}
#[inline]
pub fn as_slice(&self) -> &[u8] {
&self.data[..self.len]
}
#[inline]
pub fn as_mut_slice(&mut self) -> &mut [u8] {
&mut self.data[..self.len]
}
#[inline]
pub fn len(&self) -> usize {
self.len
}
#[inline]
pub fn is_empty(&self) -> bool {
self.len == 0
}
#[inline]
pub fn clear(&mut self) {
self.len = 0;
}
#[inline]
pub fn function_code(&self) -> Option<u8> {
if self.len > 0 {
Some(self.data[0])
} else {
None
}
}
#[inline]
pub fn is_exception(&self) -> bool {
self.function_code()
.map(|fc| fc & 0x80 != 0)
.unwrap_or(false)
}
#[inline]
pub fn exception_code(&self) -> Option<u8> {
if self.is_exception() && self.len > 1 {
Some(self.data[1])
} else {
None
}
}
pub fn function_code_description(fc: u8) -> &'static str {
match fc & 0x7F {
0x01 => "Read Coils",
0x02 => "Read Discrete Inputs",
0x03 => "Read Holding Registers",
0x04 => "Read Input Registers",
0x05 => "Write Single Coil",
0x06 => "Write Single Register",
0x0F => "Write Multiple Coils",
0x10 => "Write Multiple Registers",
0x17 => "Read/Write Multiple Registers",
_ => "Unknown Function",
}
}
}
impl Default for ModbusPdu {
fn default() -> Self {
Self::new()
}
}
pub struct PduBuilder {
pdu: ModbusPdu,
}
impl Default for PduBuilder {
fn default() -> Self {
Self::new()
}
}
impl PduBuilder {
#[inline]
pub fn new() -> Self {
Self {
pdu: ModbusPdu::new(),
}
}
#[inline]
pub fn function_code(mut self, fc: u8) -> ModbusResult<Self> {
self.pdu.push(fc)?;
Ok(self)
}
#[inline]
pub fn address(mut self, addr: u16) -> ModbusResult<Self> {
self.pdu.push_u16(addr)?;
Ok(self)
}
#[inline]
pub fn quantity(mut self, qty: u16) -> ModbusResult<Self> {
self.pdu.push_u16(qty)?;
Ok(self)
}
#[inline]
pub fn byte(mut self, b: u8) -> ModbusResult<Self> {
self.pdu.push(b)?;
Ok(self)
}
#[inline]
pub fn data(mut self, data: &[u8]) -> ModbusResult<Self> {
self.pdu.extend(data)?;
Ok(self)
}
#[inline]
pub fn build(self) -> ModbusPdu {
#[cfg(feature = "std")]
if let Some(fc) = self.pdu.function_code() {
let fc_desc = ModbusPdu::function_code_description(fc);
tracing::debug!(
"PDU built: FC={:02X} ({}), total_len={}",
fc,
fc_desc,
self.pdu.len()
);
} else {
tracing::debug!("PDU built: empty PDU");
}
self.pdu
}
pub fn build_read_request(
fc: u8,
start_address: u16,
quantity: u16,
) -> ModbusResult<ModbusPdu> {
if !matches!(fc, 0x01..=0x04) {
return Err(ModbusError::InvalidFunction { code: fc });
}
Ok(PduBuilder::new()
.function_code(fc)?
.address(start_address)?
.quantity(quantity)?
.build())
}
pub fn build_write_single_coil(address: u16, value: bool) -> ModbusResult<ModbusPdu> {
let coil_value: u16 = if value { 0xFF00 } else { 0x0000 };
Ok(PduBuilder::new()
.function_code(0x05)?
.address(address)?
.quantity(coil_value)?
.build())
}
pub fn build_write_single_register(address: u16, value: u16) -> ModbusResult<ModbusPdu> {
Ok(PduBuilder::new()
.function_code(0x06)?
.address(address)?
.quantity(value)?
.build())
}
pub fn build_write_multiple_coils(address: u16, values: &[bool]) -> ModbusResult<ModbusPdu> {
if values.len() > MAX_WRITE_COILS {
return Err(ModbusError::invalid_data(
"write_multiple_coils: maximum 1968 coils per request",
));
}
let quantity = values.len() as u16;
let byte_count = values.len().div_ceil(8);
let mut coil_bytes = vec![0u8; byte_count];
for (i, &value) in values.iter().enumerate() {
if value {
coil_bytes[i / 8] |= 1 << (i % 8);
}
}
Ok(PduBuilder::new()
.function_code(0x0F)?
.address(address)?
.quantity(quantity)?
.byte(byte_count as u8)?
.data(&coil_bytes)?
.build())
}
pub fn build_write_multiple_registers(address: u16, values: &[u16]) -> ModbusResult<ModbusPdu> {
if values.len() > MAX_WRITE_REGISTERS {
return Err(ModbusError::invalid_data(
"write_multiple_registers: maximum 123 registers per request",
));
}
let quantity = values.len() as u16;
let byte_count = (values.len() * 2) as u8;
let mut builder = PduBuilder::new()
.function_code(0x10)?
.address(address)?
.quantity(quantity)?
.byte(byte_count)?;
for &value in values {
builder = builder
.byte((value >> 8) as u8)?
.byte((value & 0xFF) as u8)?;
}
Ok(builder.build())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pdu_basic_operations() {
let mut pdu = ModbusPdu::new();
assert_eq!(pdu.len(), 0);
assert!(pdu.is_empty());
pdu.push(0x03).unwrap();
assert_eq!(pdu.function_code(), Some(0x03));
assert!(!pdu.is_exception());
pdu.push_u16(0x0100).unwrap();
pdu.push_u16(0x000A).unwrap();
assert_eq!(pdu.len(), 5);
assert_eq!(pdu.as_slice(), &[0x03, 0x01, 0x00, 0x00, 0x0A]);
}
#[test]
fn test_pdu_builder() {
let pdu = PduBuilder::new()
.function_code(0x03)
.unwrap()
.address(0x0100)
.unwrap()
.quantity(0x000A)
.unwrap()
.build();
assert_eq!(pdu.len(), 5);
assert_eq!(pdu.as_slice(), &[0x03, 0x01, 0x00, 0x00, 0x0A]);
}
#[test]
fn test_exception_response() {
let mut pdu = ModbusPdu::new();
pdu.push(0x83).unwrap();
pdu.push(0x02).unwrap();
assert!(pdu.is_exception());
assert_eq!(pdu.exception_code(), Some(0x02));
}
#[test]
fn test_build_read_request() {
let pdu = PduBuilder::build_read_request(0x03, 0x006B, 3).unwrap();
assert_eq!(pdu.function_code(), Some(0x03));
let data = pdu.as_slice();
assert_eq!(data.len(), 5);
assert_eq!(data, &[0x03, 0x00, 0x6B, 0x00, 0x03]);
}
#[test]
fn test_build_write_single_coil() {
let pdu = PduBuilder::build_write_single_coil(0x00AC, true).unwrap();
assert_eq!(pdu.function_code(), Some(0x05));
assert_eq!(pdu.as_slice(), &[0x05, 0x00, 0xAC, 0xFF, 0x00]);
}
#[test]
fn test_build_write_single_register() {
let pdu = PduBuilder::build_write_single_register(0x0001, 0x0003).unwrap();
assert_eq!(pdu.function_code(), Some(0x06));
assert_eq!(pdu.as_slice(), &[0x06, 0x00, 0x01, 0x00, 0x03]);
}
#[test]
fn test_build_write_multiple_registers() {
let pdu = PduBuilder::build_write_multiple_registers(0x0001, &[0x000A, 0x0102]).unwrap();
assert_eq!(pdu.function_code(), Some(0x10));
assert_eq!(
pdu.as_slice(),
&[0x10, 0x00, 0x01, 0x00, 0x02, 0x04, 0x00, 0x0A, 0x01, 0x02]
);
}
#[test]
fn test_pdu_push_at_capacity_returns_error() {
use crate::constants::MAX_PDU_SIZE;
let mut pdu = ModbusPdu::new();
for i in 0..MAX_PDU_SIZE {
assert!(pdu.push(i as u8).is_ok(), "push {} should succeed", i);
}
let result = pdu.push(0xFF);
assert!(result.is_err(), "push when full should return Err");
}
#[test]
fn test_pdu_from_slice_rejects_oversized_input() {
use crate::constants::MAX_PDU_SIZE;
let oversized = vec![0u8; MAX_PDU_SIZE + 1];
let result = ModbusPdu::from_slice(&oversized);
assert!(
result.is_err(),
"from_slice with {} bytes should fail",
MAX_PDU_SIZE + 1
);
}
#[test]
fn test_pdu_from_slice_accepts_max_size() {
use crate::constants::MAX_PDU_SIZE;
let data = vec![0u8; MAX_PDU_SIZE];
let result = ModbusPdu::from_slice(&data);
assert!(
result.is_ok(),
"from_slice with MAX_PDU_SIZE bytes should succeed"
);
}
}