use knx_rs_core::address::IndividualAddress;
use knx_rs_core::cemi::CemiFrame;
#[derive(Debug)]
pub struct TpFrame {
data: [u8; 64],
len: usize,
}
impl TpFrame {
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 7 || data.len() > 64 {
return None;
}
let mut frame = Self {
data: [0u8; 64],
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 {
(self.data[0] & super::commands::L_DATA_MASK) == super::commands::L_DATA_EXTENDED_IND
}
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] & 0x0F
}
}
pub fn calc_crc(&self) -> u8 {
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) -> Self {
let payload = cemi.payload();
let npdu_len = cemi.npdu_length();
let mut frame = Self {
data: [0u8; 64],
len: 0,
};
if npdu_len <= 15 {
let ctrl = cemi.as_bytes()[2]; frame.data[0] = ctrl;
let src = cemi.source_address().to_bytes();
frame.data[1] = src[0];
frame.data[2] = src[1];
let dst_raw = cemi.destination_address_raw().to_be_bytes();
frame.data[3] = dst_raw[0];
frame.data[4] = dst_raw[1];
let ctrl2 = cemi.as_bytes()[3]; frame.data[5] = (ctrl2 & 0xF0) | (npdu_len & 0x0F);
let apdu_len = payload.len();
frame.data[6..6 + apdu_len].copy_from_slice(payload);
frame.len = 6 + apdu_len + 1; } else {
frame.data[0] = cemi.as_bytes()[2]; frame.data[1] = cemi.as_bytes()[3]; let src = cemi.source_address().to_bytes();
frame.data[2] = src[0];
frame.data[3] = src[1];
let dst_raw = cemi.destination_address_raw().to_be_bytes();
frame.data[4] = dst_raw[0];
frame.data[5] = dst_raw[1];
frame.data[6] = npdu_len;
let apdu_len = payload.len();
frame.data[7..7 + apdu_len].copy_from_slice(payload);
frame.len = 7 + apdu_len + 1;
}
let crc = CemiFrame::calc_crc_tp(&frame.data[..frame.len - 1]);
frame.data[frame.len - 1] = crc;
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);
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 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());
}
}