use crate::transport::ModbusError;
pub(crate) const FN_READ_HOLDING: u8 = 0x03;
pub(crate) const FN_WRITE_SINGLE: u8 = 0x06;
pub(crate) const FN_WRITE_MULTIPLE: u8 = 0x10;
pub const EXCEPTION_BIT: u8 = 0x80;
pub const MAX_ADU: usize = 256;
pub const MAX_READ_REGS: usize = 125;
pub const MAX_WRITE_REGS: usize = 123;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum FrameError {
InvalidLength(usize),
BufferTooSmall { needed: usize, actual: usize },
}
impl core::fmt::Display for FrameError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::InvalidLength(n) => write!(f, "invalid register count {n}"),
Self::BufferTooSmall { needed, actual } => {
write!(f, "buffer too small (need {needed}, have {actual})")
}
}
}
}
impl core::error::Error for FrameError {}
pub fn crc16_modbus(data: &[u8]) -> u16 {
let mut crc: u16 = 0xFFFF;
for &b in data {
crc ^= b as u16;
for _ in 0..8 {
if crc & 1 != 0 {
crc = (crc >> 1) ^ 0xA001;
} else {
crc >>= 1;
}
}
}
crc
}
fn append_crc(buf: &mut [u8], len: usize) {
let crc = crc16_modbus(&buf[..len]);
buf[len] = crc as u8;
buf[len + 1] = (crc >> 8) as u8;
}
pub fn build_read_request(slave: u8, addr: u16, count: u16) -> [u8; 8] {
let mut req = [0u8; 8];
req[0] = slave;
req[1] = FN_READ_HOLDING;
req[2..4].copy_from_slice(&addr.to_be_bytes());
req[4..6].copy_from_slice(&count.to_be_bytes());
append_crc(&mut req, 6);
req
}
pub fn build_write_single_request(slave: u8, addr: u16, value: u16) -> [u8; 8] {
let mut req = [0u8; 8];
req[0] = slave;
req[1] = FN_WRITE_SINGLE;
req[2..4].copy_from_slice(&addr.to_be_bytes());
req[4..6].copy_from_slice(&value.to_be_bytes());
append_crc(&mut req, 6);
req
}
pub fn build_write_multiple_request(
slave: u8,
addr: u16,
values: &[u16],
out: &mut [u8],
) -> Result<usize, FrameError> {
if values.is_empty() || values.len() > MAX_WRITE_REGS {
return Err(FrameError::InvalidLength(values.len()));
}
let bc = 2 * values.len();
let len = 7 + bc + 2;
if out.len() < len {
return Err(FrameError::BufferTooSmall {
needed: len,
actual: out.len(),
});
}
out[0] = slave;
out[1] = FN_WRITE_MULTIPLE;
out[2..4].copy_from_slice(&addr.to_be_bytes());
out[4..6].copy_from_slice(&(values.len() as u16).to_be_bytes());
out[6] = bc as u8;
for (i, v) in values.iter().enumerate() {
out[7 + 2 * i..9 + 2 * i].copy_from_slice(&v.to_be_bytes());
}
append_crc(out, 7 + bc);
Ok(len)
}
fn check_crc(resp: &[u8], len: usize) -> Result<(), ModbusError> {
if resp.len() < len {
return Err(ModbusError::ShortResponse(resp.len()));
}
let got = u16::from_le_bytes([resp[len - 2], resp[len - 1]]);
let calc = crc16_modbus(&resp[..len - 2]);
if got == calc {
Ok(())
} else {
Err(ModbusError::BadCrc)
}
}
fn check_exception(resp: &[u8], slave: u8) -> Result<(), ModbusError> {
if resp.len() < 5 {
return Err(ModbusError::ShortResponse(resp.len()));
}
if resp[0] != slave {
return Err(ModbusError::BadSlave(resp[0]));
}
if resp[1] & EXCEPTION_BIT != 0 {
check_crc(resp, 5)?;
return Err(ModbusError::Exception(resp[2]));
}
Ok(())
}
pub fn parse_read_response(resp: &[u8], slave: u8, out: &mut [u16]) -> Result<(), ModbusError> {
check_exception(resp, slave)?;
let count = out.len();
let expected_len = 5 + 2 * count;
if resp[1] != FN_READ_HOLDING || resp[2] as usize != 2 * count {
return Err(ModbusError::BadHeader);
}
check_crc(resp, expected_len)?;
for (i, slot) in out.iter_mut().enumerate() {
*slot = u16::from_be_bytes([resp[3 + 2 * i], resp[4 + 2 * i]]);
}
Ok(())
}
pub fn parse_write_single_response(resp: &[u8], req: &[u8; 8]) -> Result<(), ModbusError> {
check_exception(resp, req[0])?;
check_crc(resp, 8)?;
if resp.len() < 8 || resp[..8] != req[..] {
return Err(ModbusError::BadHeader);
}
Ok(())
}
pub fn parse_write_multiple_response(
resp: &[u8],
slave: u8,
addr: u16,
qty: u16,
) -> Result<(), ModbusError> {
check_exception(resp, slave)?;
check_crc(resp, 8)?;
if resp[1] != FN_WRITE_MULTIPLE
|| u16::from_be_bytes([resp[2], resp[3]]) != addr
|| u16::from_be_bytes([resp[4], resp[5]]) != qty
{
return Err(ModbusError::BadHeader);
}
Ok(())
}
#[cfg(test)]
mod tests;