use bytes::{BufMut, Bytes, BytesMut};
use thiserror::Error;
pub const RTU_MIN_FRAME_SIZE: usize = 4;
pub const RTU_MAX_FRAME_SIZE: usize = 256;
pub const RTU_MAX_PDU_SIZE: usize = 253;
const CRC_POLYNOMIAL: u16 = 0xA001;
const CRC_TABLE: [u16; 256] = generate_crc_table();
const fn generate_crc_table() -> [u16; 256] {
let mut table = [0u16; 256];
let mut i = 0usize;
while i < 256 {
let mut crc = i as u16;
let mut j = 0;
while j < 8 {
if crc & 0x0001 != 0 {
crc = (crc >> 1) ^ CRC_POLYNOMIAL;
} else {
crc >>= 1;
}
j += 1;
}
table[i] = crc;
i += 1;
}
table
}
#[inline]
pub fn calculate_crc(data: &[u8]) -> u16 {
let mut crc: u16 = 0xFFFF;
for byte in data {
let index = (crc ^ (*byte as u16)) & 0xFF;
crc = (crc >> 8) ^ CRC_TABLE[index as usize];
}
crc
}
#[inline]
pub fn calculate_crc_slow(data: &[u8]) -> u16 {
let mut crc: u16 = 0xFFFF;
for byte in data {
crc ^= *byte as u16;
for _ in 0..8 {
if crc & 0x0001 != 0 {
crc = (crc >> 1) ^ CRC_POLYNOMIAL;
} else {
crc >>= 1;
}
}
}
crc
}
#[inline]
pub fn verify_crc(frame: &[u8]) -> bool {
if frame.len() < RTU_MIN_FRAME_SIZE {
return false;
}
let data_len = frame.len() - 2;
let received_crc = u16::from_le_bytes([frame[data_len], frame[data_len + 1]]);
let calculated_crc = calculate_crc(&frame[..data_len]);
received_crc == calculated_crc
}
#[inline]
pub fn append_crc(buf: &mut BytesMut) {
let crc = calculate_crc(buf);
buf.put_u16_le(crc);
}
#[derive(Debug, Clone, Error)]
pub enum RtuFrameError {
#[error("Frame too short: {actual} bytes (minimum {minimum})")]
TooShort { actual: usize, minimum: usize },
#[error("Frame too long: {actual} bytes (maximum {maximum})")]
TooLong { actual: usize, maximum: usize },
#[error("CRC mismatch: expected 0x{expected:04X}, got 0x{received:04X}")]
CrcMismatch { expected: u16, received: u16 },
#[error("Invalid unit ID: {0}")]
InvalidUnitId(u8),
#[error("Invalid function code: 0x{0:02X}")]
InvalidFunctionCode(u8),
#[error("Incomplete frame: have {have} bytes, need {need}")]
Incomplete { have: usize, need: usize },
}
#[derive(Debug, Clone)]
pub struct RtuFrame {
pub unit_id: u8,
pub pdu: Vec<u8>,
pub crc: u16,
}
impl RtuFrame {
pub fn new(unit_id: u8, pdu: Vec<u8>) -> Self {
let mut data = Vec::with_capacity(1 + pdu.len());
data.push(unit_id);
data.extend_from_slice(&pdu);
let crc = calculate_crc(&data);
Self { unit_id, pdu, crc }
}
pub fn response(request: &RtuFrame, response_pdu: Vec<u8>) -> Self {
Self::new(request.unit_id, response_pdu)
}
pub fn exception(unit_id: u8, function_code: u8, exception_code: u8) -> Self {
Self::new(unit_id, vec![function_code | 0x80, exception_code])
}
pub fn function_code(&self) -> Option<u8> {
self.pdu.first().copied()
}
pub fn is_exception(&self) -> bool {
self.pdu.first().map(|fc| fc & 0x80 != 0).unwrap_or(false)
}
pub fn exception_code(&self) -> Option<u8> {
if self.is_exception() {
self.pdu.get(1).copied()
} else {
None
}
}
pub fn data(&self) -> &[u8] {
if self.pdu.len() > 1 {
&self.pdu[1..]
} else {
&[]
}
}
pub fn frame_size(&self) -> usize {
1 + self.pdu.len() + 2 }
pub fn encode(&self) -> Bytes {
let mut buf = BytesMut::with_capacity(self.frame_size());
self.encode_to(&mut buf);
buf.freeze()
}
pub fn encode_to(&self, buf: &mut BytesMut) {
buf.reserve(self.frame_size());
buf.put_u8(self.unit_id);
buf.put_slice(&self.pdu);
buf.put_u16_le(self.crc);
}
pub fn decode(data: &[u8]) -> Result<Self, RtuFrameError> {
if data.len() < RTU_MIN_FRAME_SIZE {
return Err(RtuFrameError::TooShort {
actual: data.len(),
minimum: RTU_MIN_FRAME_SIZE,
});
}
if data.len() > RTU_MAX_FRAME_SIZE {
return Err(RtuFrameError::TooLong {
actual: data.len(),
maximum: RTU_MAX_FRAME_SIZE,
});
}
let data_len = data.len() - 2;
let received_crc = u16::from_le_bytes([data[data_len], data[data_len + 1]]);
let calculated_crc = calculate_crc(&data[..data_len]);
if received_crc != calculated_crc {
return Err(RtuFrameError::CrcMismatch {
expected: calculated_crc,
received: received_crc,
});
}
let unit_id = data[0];
let pdu = data[1..data_len].to_vec();
if pdu.is_empty() {
return Err(RtuFrameError::TooShort {
actual: data.len(),
minimum: RTU_MIN_FRAME_SIZE + 1,
});
}
Ok(Self {
unit_id,
pdu,
crc: received_crc,
})
}
pub fn try_decode(data: &[u8]) -> Result<Option<Self>, RtuFrameError> {
if data.len() < RTU_MIN_FRAME_SIZE {
return Ok(None);
}
if let Some(expected_len) = estimate_frame_length(data) {
if data.len() < expected_len {
return Ok(None);
}
Self::decode(&data[..expected_len]).map(Some)
} else {
Ok(None)
}
}
}
fn estimate_frame_length(data: &[u8]) -> Option<usize> {
if data.len() < 2 {
return None;
}
let function_code = data[1];
if function_code & 0x80 != 0 {
return Some(5);
}
match function_code {
0x01 | 0x02 | 0x03 | 0x04 => {
if data.len() >= 3 {
let third_byte = data[2];
if data.len() >= 8 {
Some(8)
} else if data.len() >= 3 + third_byte as usize + 2 {
Some(3 + third_byte as usize + 2)
} else {
Some(8) }
} else {
Some(8) }
}
0x05 | 0x06 => {
Some(8)
}
0x16 => {
Some(10)
}
0x0F => {
if data.len() >= 7 {
let byte_count = data[6] as usize;
Some(7 + byte_count + 2)
} else {
None
}
}
0x10 => {
if data.len() >= 7 {
let byte_count = data[6] as usize;
Some(7 + byte_count + 2)
} else {
None
}
}
_ => None,
}
}
pub fn unpack_bits(bytes: &[u8], count: usize) -> Vec<bool> {
let mut result = Vec::with_capacity(count);
for i in 0..count {
let byte_index = i / 8;
let bit_index = i % 8;
if byte_index < bytes.len() {
result.push((bytes[byte_index] >> bit_index) & 1 != 0);
} else {
result.push(false);
}
}
result
}
pub fn pack_bits(bits: &[bool]) -> Vec<u8> {
let byte_count = (bits.len() + 7) / 8;
let mut result = vec![0u8; byte_count];
for (i, &bit) in bits.iter().enumerate() {
if bit {
result[i / 8] |= 1 << (i % 8);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_crc_calculation() {
let data = [0x01, 0x03, 0x00, 0x00, 0x00, 0x0A];
let crc = calculate_crc(&data);
assert_eq!(
crc, 0xCDC5,
"CRC mismatch: expected 0xCDC5, got 0x{:04X}",
crc
);
let data2 = [0x01, 0x03];
let crc2 = calculate_crc(&data2);
assert_eq!(crc2, 0x2140);
}
#[test]
fn test_crc_slow_matches_table() {
let data = [0x01, 0x03, 0x00, 0x00, 0x00, 0x0A, 0x12, 0x34, 0x56, 0x78];
assert_eq!(calculate_crc(&data), calculate_crc_slow(&data));
for len in 1..=10 {
let subset = &data[..len];
assert_eq!(
calculate_crc(subset),
calculate_crc_slow(subset),
"CRC mismatch for length {}",
len
);
}
}
#[test]
fn test_verify_crc_valid() {
let frame = [0x01, 0x03, 0x00, 0x00, 0x00, 0x0A, 0xC5, 0xCD];
assert!(verify_crc(&frame));
}
#[test]
fn test_verify_crc_invalid() {
let frame = [0x01, 0x03, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00];
assert!(!verify_crc(&frame));
assert!(!verify_crc(&[0x01, 0x03]));
}
#[test]
fn test_frame_creation() {
let pdu = vec![0x03, 0x00, 0x00, 0x00, 0x0A];
let frame = RtuFrame::new(1, pdu.clone());
assert_eq!(frame.unit_id, 1);
assert_eq!(frame.pdu, pdu);
assert_eq!(frame.function_code(), Some(0x03));
assert!(!frame.is_exception());
assert_eq!(frame.crc, 0xCDC5);
}
#[test]
fn test_frame_encode_decode() {
let original = RtuFrame::new(1, vec![0x03, 0x00, 0x00, 0x00, 0x0A]);
let encoded = original.encode();
let decoded = RtuFrame::decode(&encoded).unwrap();
assert_eq!(decoded.unit_id, original.unit_id);
assert_eq!(decoded.pdu, original.pdu);
assert_eq!(decoded.crc, original.crc);
}
#[test]
fn test_exception_frame() {
let frame = RtuFrame::exception(1, 0x03, 0x02);
assert!(frame.is_exception());
assert_eq!(frame.function_code(), Some(0x83));
assert_eq!(frame.exception_code(), Some(0x02));
}
#[test]
fn test_decode_errors() {
let result = RtuFrame::decode(&[0x01, 0x03]);
assert!(matches!(result, Err(RtuFrameError::TooShort { .. })));
let result = RtuFrame::decode(&[0x01, 0x03, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00]);
assert!(matches!(result, Err(RtuFrameError::CrcMismatch { .. })));
}
#[test]
fn test_pack_unpack_bits() {
let bits = vec![true, false, true, true, false, false, true, false, true];
let packed = pack_bits(&bits);
let unpacked = unpack_bits(&packed, bits.len());
assert_eq!(unpacked, bits);
}
#[test]
fn test_estimate_frame_length() {
let data = [0x01, 0x03, 0x00, 0x00, 0x00, 0x0A, 0xC5, 0xCD];
assert_eq!(estimate_frame_length(&data), Some(8));
let data = [0x01, 0x06, 0x00, 0x10, 0x00, 0x03, 0x00, 0x00];
assert_eq!(estimate_frame_length(&data), Some(8));
let data = [0x01, 0x83, 0x02, 0x00, 0x00];
assert_eq!(estimate_frame_length(&data), Some(5));
let data = [0x01, 0x03, 0x00, 0x00];
assert_eq!(estimate_frame_length(&data), Some(8));
}
}