use bytes::Bytes;
use rusty_modbus_frame::frame::{Frame, FrameHeader};
use rusty_modbus_types::{ExceptionCode, MAX_PDU_SIZE, MbapHeader, TransactionId};
#[must_use]
pub fn mbap_to_rtu(frame: &Frame) -> Frame {
let unit_id = frame.unit_id();
Frame {
header: FrameHeader::Rtu { unit_id },
pdu: frame.pdu.clone(),
}
}
#[must_use]
pub fn rtu_to_mbap(rtu_frame: &Frame, transaction_id: TransactionId, unit_id: u8) -> Frame {
if rtu_frame.pdu.is_empty() || rtu_frame.pdu.len() > MAX_PDU_SIZE {
return make_exception_frame(
transaction_id,
unit_id,
rtu_frame.pdu.first().copied().unwrap_or(0),
ExceptionCode::GatewayTargetDeviceFailedToRespond.code(),
);
}
let Ok(pdu_len) = u16::try_from(rtu_frame.pdu.len()) else {
return make_exception_frame(
transaction_id,
unit_id,
rtu_frame.pdu.first().copied().unwrap_or(0),
ExceptionCode::GatewayTargetDeviceFailedToRespond.code(),
);
};
let header = MbapHeader::new(transaction_id.0, unit_id, pdu_len);
Frame {
header: FrameHeader::Mbap(header),
pdu: rtu_frame.pdu.clone(),
}
}
#[must_use]
pub fn make_exception_frame(
transaction_id: TransactionId,
unit_id: u8,
function_code: u8,
exception_code: u8,
) -> Frame {
let pdu = Bytes::from(vec![function_code | 0x80, exception_code]);
let header = MbapHeader::new(transaction_id.0, unit_id, 2);
Frame {
header: FrameHeader::Mbap(header),
pdu,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mbap_to_rtu_preserves_pdu() {
let pdu = Bytes::from_static(&[0x03, 0x00, 0x00, 0x00, 0x01]);
let header = MbapHeader::new(42, 0x01, 5);
let mbap = Frame {
header: FrameHeader::Mbap(header),
pdu: pdu.clone(),
};
let rtu = mbap_to_rtu(&mbap);
assert_eq!(rtu.unit_id(), 0x01);
assert_eq!(rtu.pdu, pdu);
}
#[test]
fn rtu_to_mbap_sets_transaction_id() {
let pdu = Bytes::from_static(&[0x03, 0x02, 0x12, 0x34]);
let rtu = Frame {
header: FrameHeader::Rtu { unit_id: 0x05 },
pdu,
};
let mbap = rtu_to_mbap(&rtu, TransactionId(99), 0x05);
match mbap.header {
FrameHeader::Mbap(h) => {
assert_eq!(h.transaction_id.get(), 99);
assert_eq!(h.unit_id, 0x05);
}
FrameHeader::Rtu { .. } => panic!("expected MBAP header"),
}
}
#[test]
fn rtu_to_mbap_oversized_pdu_is_gateway_target_failure() {
let rtu = Frame {
header: FrameHeader::Rtu { unit_id: 0x05 },
pdu: Bytes::from(vec![0x03; MAX_PDU_SIZE + 1]),
};
let mbap = rtu_to_mbap(&rtu, TransactionId(99), 0x05);
match mbap.header {
FrameHeader::Mbap(h) => {
assert_eq!(h.transaction_id.get(), 99);
assert_eq!(h.unit_id, 0x05);
assert_eq!(h.pdu_length(), 2);
}
FrameHeader::Rtu { .. } => panic!("expected MBAP header"),
}
assert_eq!(
&mbap.pdu[..],
&[
0x83,
ExceptionCode::GatewayTargetDeviceFailedToRespond.code()
]
);
}
#[test]
fn rtu_to_mbap_empty_pdu_is_gateway_target_failure() {
let rtu = Frame {
header: FrameHeader::Rtu { unit_id: 0x05 },
pdu: Bytes::new(),
};
let mbap = rtu_to_mbap(&rtu, TransactionId(99), 0x05);
assert_eq!(
&mbap.pdu[..],
&[
0x80,
ExceptionCode::GatewayTargetDeviceFailedToRespond.code()
]
);
}
#[test]
fn exception_frame_format() {
let frame = make_exception_frame(TransactionId(1), 0x01, 0x03, 0x0B);
assert_eq!(&frame.pdu[..], &[0x83, 0x0B]);
}
}