use knx_rs_core::address::IndividualAddress;
use knx_rs_core::cemi::CemiFrame;
use crate::commands::{L_DATA_EXTENDED_IND, L_DATA_MASK};
pub const MAX_FRAME_LEN: usize = 64;
pub const HEADER_STD: usize = 6;
pub const HEADER_EXT: usize = 7;
pub const CRC_LEN: usize = 1;
pub const APDU_LEN_MASK: u8 = 0x0F;
pub const CTRL_FRAME_TYPE: u8 = 0x80;
#[must_use]
pub const fn is_extended_ctrl(ctrl: u8) -> bool {
(ctrl & L_DATA_MASK) == L_DATA_EXTENDED_IND
}
#[derive(Debug)]
pub struct TpFrame {
data: [u8; MAX_FRAME_LEN],
len: usize,
}
impl TpFrame {
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < HEADER_STD + CRC_LEN || data.len() > MAX_FRAME_LEN {
return None;
}
let mut frame = Self {
data: [0u8; MAX_FRAME_LEN],
len: data.len(),
};
frame.data[..data.len()].copy_from_slice(data);
Some(frame)
}
pub fn as_bytes(&self) -> &[u8] {
&self.data[..self.len]
}
pub const fn len(&self) -> usize {
self.len
}
pub const fn is_empty(&self) -> bool {
self.len == 0
}
pub const fn is_extended(&self) -> bool {
is_extended_ctrl(self.data[0])
}
pub const fn source(&self) -> IndividualAddress {
let raw = if self.is_extended() {
u16::from_be_bytes([self.data[2], self.data[3]])
} else {
u16::from_be_bytes([self.data[1], self.data[2]])
};
IndividualAddress::from_raw(raw)
}
pub const fn destination_raw(&self) -> u16 {
if self.is_extended() {
u16::from_be_bytes([self.data[4], self.data[5]])
} else {
u16::from_be_bytes([self.data[3], self.data[4]])
}
}
pub const fn apdu_size(&self) -> u8 {
if self.is_extended() {
self.data[6]
} else {
self.data[5] & APDU_LEN_MASK
}
}
pub fn calc_crc(&self) -> u8 {
if self.len == 0 {
return 0;
}
CemiFrame::calc_crc_tp(&self.data[..self.len - 1])
}
pub fn is_valid(&self) -> bool {
self.len >= 7 && self.data[self.len - 1] == self.calc_crc()
}
pub fn from_cemi(cemi: &CemiFrame) -> Option<Self> {
let payload = cemi.payload();
let npdu_len = cemi.npdu_length();
let apdu_len = payload.len();
let mut frame = Self {
data: [0u8; MAX_FRAME_LEN],
len: 0,
};
let src = cemi.source_address().to_bytes();
let dst_raw = cemi.destination_address_raw().to_be_bytes();
let ctrl1 = cemi.as_bytes()[2];
let ctrl2 = cemi.as_bytes()[3];
if npdu_len <= APDU_LEN_MASK {
if HEADER_STD + apdu_len + CRC_LEN > MAX_FRAME_LEN {
return None;
}
frame.data[0] = ctrl1 | CTRL_FRAME_TYPE;
frame.data[1] = src[0];
frame.data[2] = src[1];
frame.data[3] = dst_raw[0];
frame.data[4] = dst_raw[1];
frame.data[5] = (ctrl2 & 0xF0) | (npdu_len & APDU_LEN_MASK);
frame.data[HEADER_STD..HEADER_STD + apdu_len].copy_from_slice(payload);
frame.len = HEADER_STD + apdu_len + CRC_LEN;
} else {
if HEADER_EXT + apdu_len + CRC_LEN > MAX_FRAME_LEN {
return None;
}
frame.data[0] = ctrl1 & !CTRL_FRAME_TYPE;
frame.data[1] = ctrl2;
frame.data[2] = src[0];
frame.data[3] = src[1];
frame.data[4] = dst_raw[0];
frame.data[5] = dst_raw[1];
frame.data[6] = npdu_len;
frame.data[HEADER_EXT..HEADER_EXT + apdu_len].copy_from_slice(payload);
frame.len = HEADER_EXT + apdu_len + CRC_LEN;
}
let crc = CemiFrame::calc_crc_tp(&frame.data[..frame.len - 1]);
frame.data[frame.len - 1] = crc;
Some(frame)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use knx_rs_core::address::{DestinationAddress, GroupAddress, IndividualAddress};
use knx_rs_core::message::MessageCode;
use knx_rs_core::types::Priority;
#[test]
fn cemi_to_tp_standard_frame() {
let cemi = CemiFrame::new_l_data(
MessageCode::LDataInd,
IndividualAddress::from_raw(0x1101),
DestinationAddress::Group(GroupAddress::from_raw(0x0801)),
Priority::Low,
&[0x00, 0x81],
);
let tp = TpFrame::from_cemi(&cemi).unwrap();
assert!(!tp.is_extended());
assert_eq!(tp.source().raw(), 0x1101);
assert_eq!(tp.destination_raw(), 0x0801);
assert_eq!(tp.apdu_size(), 1);
assert!(tp.is_valid());
}
#[test]
fn cemi_to_tp_extended_frame_roundtrips() {
let mut apdu = [0u8; 20];
apdu[0] = 0x00;
apdu[1] = 0x80; let cemi = CemiFrame::new_l_data(
MessageCode::LDataInd,
IndividualAddress::from_raw(0x1101),
DestinationAddress::Group(GroupAddress::from_raw(0x0801)),
Priority::Low,
&apdu,
);
let tp = TpFrame::from_cemi(&cemi).unwrap();
assert!(tp.is_extended());
assert_eq!(tp.source().raw(), 0x1101);
assert_eq!(tp.destination_raw(), 0x0801);
assert_eq!(tp.apdu_size() as usize, apdu.len() - 1);
assert!(tp.is_valid());
}
#[test]
fn from_cemi_rejects_oversized_apdu() {
let apdu = [0xAAu8; 80];
let cemi = CemiFrame::new_l_data(
MessageCode::LDataInd,
IndividualAddress::from_raw(0x1101),
DestinationAddress::Group(GroupAddress::from_raw(0x0801)),
Priority::Low,
&apdu,
);
assert!(TpFrame::from_cemi(&cemi).is_none());
}
#[test]
fn tp_frame_from_bytes() {
let mut data = [0xBC, 0x11, 0x01, 0x08, 0x01, 0xE1, 0x00, 0x81, 0x00];
data[8] = CemiFrame::calc_crc_tp(&data[..8]);
let frame = TpFrame::from_bytes(&data).unwrap();
assert!(!frame.is_extended());
assert_eq!(frame.source().raw(), 0x1101);
assert_eq!(frame.destination_raw(), 0x0801);
assert!(frame.is_valid());
}
#[test]
fn crc_validation() {
let data = [0xBC, 0x11, 0x01, 0x08, 0x01, 0xE1, 0x00, 0x81, 0xFF]; let frame = TpFrame::from_bytes(&data).unwrap();
assert!(!frame.is_valid());
}
#[test]
fn too_short_rejected() {
assert!(TpFrame::from_bytes(&[0x00; 3]).is_none());
}
}