use crate::adu::Adu;
use crate::error::ModbusError;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Pdu {
bytes: [u8; Pdu::MAX_LEN],
len: usize,
}
impl Pdu {
pub const MAX_LEN: usize = 253;
pub const MAX_WRITE_REGISTERS: usize = 123;
pub const MAX_WRITE_COILS: usize = 1968;
fn pair(function: u8, first: u16, second: u16) -> Pdu {
let mut bytes = [0u8; Self::MAX_LEN];
bytes[0] = function;
bytes[1..3].copy_from_slice(&first.to_be_bytes());
bytes[3..5].copy_from_slice(&second.to_be_bytes());
Pdu { bytes, len: 5 }
}
pub fn read_coils(start: u16, count: u16) -> Pdu {
Self::pair(0x01, start, count)
}
pub fn read_discrete_inputs(start: u16, count: u16) -> Pdu {
Self::pair(0x02, start, count)
}
pub fn read_holding_registers(start: u16, count: u16) -> Pdu {
Self::pair(0x03, start, count)
}
pub fn read_input_registers(start: u16, count: u16) -> Pdu {
Self::pair(0x04, start, count)
}
pub fn write_single_coil(address: u16, on: bool) -> Pdu {
Self::pair(0x05, address, if on { 0xFF00 } else { 0x0000 })
}
pub fn write_single_register(address: u16, value: u16) -> Pdu {
Self::pair(0x06, address, value)
}
pub fn write_multiple_registers(start: u16, values: &[u16]) -> Result<Pdu, ModbusError> {
let quantity = values.len();
if quantity == 0 || quantity > Self::MAX_WRITE_REGISTERS {
return Err(ModbusError::InvalidValueCount);
}
let byte_count = quantity * 2;
let mut bytes = [0u8; Self::MAX_LEN];
bytes[0] = 0x10;
bytes[1..3].copy_from_slice(&start.to_be_bytes());
bytes[3..5].copy_from_slice(&(quantity as u16).to_be_bytes());
bytes[5] = byte_count as u8;
for (i, &value) in values.iter().enumerate() {
bytes[6 + i * 2..8 + i * 2].copy_from_slice(&value.to_be_bytes());
}
Ok(Pdu {
bytes,
len: 6 + byte_count,
})
}
pub fn read_holding_registers_reply(values: &[u16]) -> Result<Pdu, ModbusError> {
Self::registers_reply(0x03, values)
}
pub fn read_input_registers_reply(values: &[u16]) -> Result<Pdu, ModbusError> {
Self::registers_reply(0x04, values)
}
fn registers_reply(function: u8, values: &[u16]) -> Result<Pdu, ModbusError> {
let quantity = values.len();
if quantity == 0 || quantity > Self::MAX_WRITE_REGISTERS {
return Err(ModbusError::InvalidValueCount);
}
let byte_count = quantity * 2;
let mut bytes = [0u8; Self::MAX_LEN];
bytes[0] = function;
bytes[1] = byte_count as u8;
for (i, &value) in values.iter().enumerate() {
bytes[2 + i * 2..4 + i * 2].copy_from_slice(&value.to_be_bytes());
}
Ok(Pdu {
bytes,
len: 2 + byte_count,
})
}
pub fn write_multiple_coils(start: u16, values: &[bool]) -> Result<Pdu, ModbusError> {
let quantity = values.len();
if quantity == 0 || quantity > Self::MAX_WRITE_COILS {
return Err(ModbusError::InvalidValueCount);
}
let byte_count = quantity.div_ceil(8);
let mut bytes = [0u8; Self::MAX_LEN];
bytes[0] = 0x0F;
bytes[1..3].copy_from_slice(&start.to_be_bytes());
bytes[3..5].copy_from_slice(&(quantity as u16).to_be_bytes());
bytes[5] = byte_count as u8;
for (i, &on) in values.iter().enumerate() {
if on {
bytes[6 + i / 8] |= 1u8 << (i % 8);
}
}
Ok(Pdu {
bytes,
len: 6 + byte_count,
})
}
pub fn raw(function: u8, data: &[u8]) -> Result<Pdu, ModbusError> {
let len = 1 + data.len();
if len > Self::MAX_LEN {
return Err(ModbusError::FrameTooLong);
}
let mut bytes = [0u8; Self::MAX_LEN];
bytes[0] = function;
bytes[1..len].copy_from_slice(data);
Ok(Pdu { bytes, len })
}
pub fn function_code(&self) -> u8 {
self.bytes[0]
}
pub fn as_bytes(&self) -> &[u8] {
&self.bytes[..self.len]
}
pub fn to_adu(&self, address: u8) -> Adu {
Adu::assemble(address, self.as_bytes())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn read_holding_registers_matches_the_spec_example() {
let pdu = Pdu::read_holding_registers(0x006B, 3);
assert_eq!(pdu.as_bytes(), &[0x03, 0x00, 0x6B, 0x00, 0x03]);
assert_eq!(pdu.function_code(), 0x03);
}
#[test]
fn write_single_coil_encodes_on_and_off() {
assert_eq!(
Pdu::write_single_coil(0x00AC, true).as_bytes(),
&[0x05, 0x00, 0xAC, 0xFF, 0x00]
);
assert_eq!(
Pdu::write_single_coil(0x00AC, false).as_bytes(),
&[0x05, 0x00, 0xAC, 0x00, 0x00]
);
}
#[test]
fn write_single_register_matches_the_spec_example() {
assert_eq!(
Pdu::write_single_register(0x0001, 0x0003).as_bytes(),
&[0x06, 0x00, 0x01, 0x00, 0x03]
);
}
#[test]
fn write_multiple_registers_matches_the_spec_example() {
let pdu = Pdu::write_multiple_registers(0x0001, &[0x000A, 0x0102]).unwrap();
assert_eq!(
pdu.as_bytes(),
&[0x10, 0x00, 0x01, 0x00, 0x02, 0x04, 0x00, 0x0A, 0x01, 0x02]
);
}
#[test]
fn write_multiple_coils_packs_bits_lsb_first() {
let values = [
true, false, true, true, false, false, true, true, true, false,
];
let pdu = Pdu::write_multiple_coils(0x0013, &values).unwrap();
assert_eq!(
pdu.as_bytes(),
&[0x0F, 0x00, 0x13, 0x00, 0x0A, 0x02, 0xCD, 0x01]
);
}
#[test]
fn an_empty_write_is_rejected() {
assert_eq!(
Pdu::write_multiple_registers(0, &[]),
Err(ModbusError::InvalidValueCount)
);
assert_eq!(
Pdu::write_multiple_coils(0, &[]),
Err(ModbusError::InvalidValueCount)
);
}
#[test]
fn an_oversized_write_is_rejected() {
let registers = [0u16; Pdu::MAX_WRITE_REGISTERS + 1];
assert_eq!(
Pdu::write_multiple_registers(0, ®isters),
Err(ModbusError::InvalidValueCount)
);
let coils = [false; Pdu::MAX_WRITE_COILS + 1];
assert_eq!(
Pdu::write_multiple_coils(0, &coils),
Err(ModbusError::InvalidValueCount)
);
}
#[test]
fn the_largest_write_still_fits_a_pdu() {
let registers = [0u16; Pdu::MAX_WRITE_REGISTERS];
let pdu = Pdu::write_multiple_registers(0, ®isters).unwrap();
assert!(pdu.as_bytes().len() <= Pdu::MAX_LEN);
}
#[test]
fn raw_carries_arbitrary_bytes() {
let pdu = Pdu::raw(0x2B, &[0x0E, 0x01, 0x00]).unwrap();
assert_eq!(pdu.as_bytes(), &[0x2B, 0x0E, 0x01, 0x00]);
}
#[test]
fn raw_rejects_an_oversized_pdu() {
let data = [0u8; Pdu::MAX_LEN];
assert_eq!(Pdu::raw(0x10, &data), Err(ModbusError::FrameTooLong));
}
#[test]
fn to_adu_appends_the_address_and_crc() {
let frame = Pdu::read_holding_registers(0x006B, 3).to_adu(0x11);
assert_eq!(
frame.as_bytes(),
&[0x11, 0x03, 0x00, 0x6B, 0x00, 0x03, 0x76, 0x87]
);
}
#[test]
fn a_read_registers_reply_is_the_frame_the_specification_shows() {
let reply = Pdu::read_holding_registers_reply(&[0x022B, 0x0000, 0x0064])
.expect("three registers fit a reply");
assert_eq!(
reply.as_bytes(),
&[0x03, 0x06, 0x02, 0x2B, 0x00, 0x00, 0x00, 0x64]
);
let inputs = Pdu::read_input_registers_reply(&[1]).expect("one register fits");
assert_eq!(inputs.as_bytes(), &[0x04, 0x02, 0x00, 0x01]);
}
#[test]
fn a_reply_refuses_a_register_count_it_cannot_carry() {
assert_eq!(
Pdu::read_holding_registers_reply(&[]),
Err(ModbusError::InvalidValueCount)
);
let too_many = [0u16; Pdu::MAX_WRITE_REGISTERS + 1];
assert_eq!(
Pdu::read_holding_registers_reply(&too_many),
Err(ModbusError::InvalidValueCount)
);
}
}