use alloc::vec::Vec;
use core::fmt;
pub const HEADER_LEN: u8 = 0x06;
pub const PROTOCOL_VERSION_10: u8 = 0x10;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u16)]
pub enum ServiceType {
SearchRequest = 0x0201,
SearchResponse = 0x0202,
DescriptionRequest = 0x0203,
DescriptionResponse = 0x0204,
ConnectRequest = 0x0205,
ConnectResponse = 0x0206,
ConnectionStateRequest = 0x0207,
ConnectionStateResponse = 0x0208,
DisconnectRequest = 0x0209,
DisconnectResponse = 0x020A,
SearchRequestExtended = 0x020B,
SearchResponseExtended = 0x020C,
DeviceConfigurationRequest = 0x0310,
DeviceConfigurationAck = 0x0311,
TunnelingRequest = 0x0420,
TunnelingAck = 0x0421,
RoutingIndication = 0x0530,
RoutingLostMessage = 0x0531,
RoutingBusy = 0x0532,
}
impl ServiceType {
pub const fn from_raw(raw: u16) -> Option<Self> {
Some(match raw {
0x0201 => Self::SearchRequest,
0x0202 => Self::SearchResponse,
0x0203 => Self::DescriptionRequest,
0x0204 => Self::DescriptionResponse,
0x0205 => Self::ConnectRequest,
0x0206 => Self::ConnectResponse,
0x0207 => Self::ConnectionStateRequest,
0x0208 => Self::ConnectionStateResponse,
0x0209 => Self::DisconnectRequest,
0x020A => Self::DisconnectResponse,
0x020B => Self::SearchRequestExtended,
0x020C => Self::SearchResponseExtended,
0x0310 => Self::DeviceConfigurationRequest,
0x0311 => Self::DeviceConfigurationAck,
0x0420 => Self::TunnelingRequest,
0x0421 => Self::TunnelingAck,
0x0530 => Self::RoutingIndication,
0x0531 => Self::RoutingLostMessage,
0x0532 => Self::RoutingBusy,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum HostProtocol {
Ipv4Udp = 0x01,
Ipv4Tcp = 0x02,
}
impl HostProtocol {
pub const fn from_raw(raw: u8) -> Option<Self> {
Some(match raw {
0x01 => Self::Ipv4Udp,
0x02 => Self::Ipv4Tcp,
_ => return None,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KnxIpError {
TooShort,
InvalidHeaderLength,
InvalidProtocolVersion,
LengthMismatch,
UnknownServiceType(u16),
FrameTooLong(usize),
}
impl fmt::Display for KnxIpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooShort => f.write_str("KNXnet/IP frame too short"),
Self::InvalidHeaderLength => f.write_str("invalid KNXnet/IP header length"),
Self::InvalidProtocolVersion => f.write_str("invalid KNXnet/IP protocol version"),
Self::LengthMismatch => f.write_str("KNXnet/IP frame length mismatch"),
Self::UnknownServiceType(st) => write!(f, "unknown KNXnet/IP service type: {st:#06x}"),
Self::FrameTooLong(len) => write!(f, "KNXnet/IP frame too long: {len} bytes"),
}
}
}
impl core::error::Error for KnxIpError {}
#[derive(Clone, PartialEq, Eq)]
pub struct KnxIpFrame {
pub service_type: ServiceType,
pub body: Vec<u8>,
}
impl KnxIpFrame {
pub fn parse(data: &[u8]) -> Result<Self, KnxIpError> {
if data.len() < HEADER_LEN as usize {
return Err(KnxIpError::TooShort);
}
if data[0] != HEADER_LEN {
return Err(KnxIpError::InvalidHeaderLength);
}
if data[1] != PROTOCOL_VERSION_10 {
return Err(KnxIpError::InvalidProtocolVersion);
}
let service_raw = u16::from_be_bytes([data[2], data[3]]);
let total_len = u16::from_be_bytes([data[4], data[5]]) as usize;
if data.len() != total_len {
return Err(KnxIpError::LengthMismatch);
}
let service_type = ServiceType::from_raw(service_raw)
.ok_or(KnxIpError::UnknownServiceType(service_raw))?;
Ok(Self {
service_type,
body: data[HEADER_LEN as usize..total_len].to_vec(),
})
}
pub fn try_to_bytes(&self) -> Result<Vec<u8>, KnxIpError> {
let total_len = HEADER_LEN as usize + self.body.len();
let total_len_u16 =
u16::try_from(total_len).map_err(|_| KnxIpError::FrameTooLong(total_len))?;
let mut buf = Vec::with_capacity(total_len);
buf.push(HEADER_LEN);
buf.push(PROTOCOL_VERSION_10);
buf.extend_from_slice(&(self.service_type as u16).to_be_bytes());
buf.extend_from_slice(&total_len_u16.to_be_bytes());
buf.extend_from_slice(&self.body);
Ok(buf)
}
pub fn to_bytes(&self) -> Vec<u8> {
match self.try_to_bytes() {
Ok(bytes) => bytes,
Err(KnxIpError::FrameTooLong(len)) => {
panic!("KNXnet/IP frame length {len} exceeds u16::MAX")
}
Err(_) => unreachable!("serializing a well-typed frame cannot fail for this reason"),
}
}
}
impl fmt::Debug for KnxIpFrame {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("KnxIpFrame")
.field("service_type", &self.service_type)
.field("body_len", &self.body.len())
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ConnectionHeader {
pub channel_id: u8,
pub sequence_counter: u8,
pub status: u8,
}
impl ConnectionHeader {
pub const LEN: u8 = 4;
pub const fn parse(data: &[u8]) -> Option<Self> {
if data.len() < Self::LEN as usize {
return None;
}
Some(Self {
channel_id: data[1],
sequence_counter: data[2],
status: data[3],
})
}
pub const fn to_bytes(self) -> [u8; 4] {
[
Self::LEN,
self.channel_id,
self.sequence_counter,
self.status,
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Hpai {
pub protocol: HostProtocol,
pub ip: [u8; 4],
pub port: u16,
}
impl Hpai {
pub const LEN: u8 = 8;
pub const fn parse(data: &[u8]) -> Option<Self> {
if data.len() < Self::LEN as usize || data[0] != Self::LEN {
return None;
}
let Some(protocol) = HostProtocol::from_raw(data[1]) else {
return None;
};
Some(Self {
protocol,
ip: [data[2], data[3], data[4], data[5]],
port: u16::from_be_bytes([data[6], data[7]]),
})
}
pub const fn nat_udp(port: u16) -> Self {
Self {
protocol: HostProtocol::Ipv4Udp,
ip: [0, 0, 0, 0],
port,
}
}
pub fn is_unspecified(self) -> bool {
self.ip == [0, 0, 0, 0]
}
pub const fn to_bytes(self) -> [u8; 8] {
let port = self.port.to_be_bytes();
[
Self::LEN,
self.protocol as u8,
self.ip[0],
self.ip[1],
self.ip[2],
self.ip[3],
port[0],
port[1],
]
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::cast_possible_truncation
)]
mod tests {
use alloc::vec;
use super::*;
#[test]
fn parse_routing_indication() {
let mut frame_data = Vec::new();
frame_data.push(0x06); frame_data.push(0x10); frame_data.extend_from_slice(&0x0530u16.to_be_bytes()); let cemi = [
0x29, 0x00, 0xBC, 0xE0, 0x11, 0x01, 0x08, 0x01, 0x01, 0x00, 0x81,
];
let total_len = u16::from(HEADER_LEN) + cemi.len() as u16;
frame_data.extend_from_slice(&total_len.to_be_bytes());
frame_data.extend_from_slice(&cemi);
let frame = KnxIpFrame::parse(&frame_data).unwrap();
assert_eq!(frame.service_type, ServiceType::RoutingIndication);
assert_eq!(frame.body, cemi);
}
#[test]
fn roundtrip_tunneling_request() {
let cemi = [
0x29, 0x00, 0xBC, 0xE0, 0x11, 0x01, 0x08, 0x01, 0x01, 0x00, 0x81,
];
let ch = ConnectionHeader {
channel_id: 1,
sequence_counter: 5,
status: 0,
};
let mut body = Vec::new();
body.extend_from_slice(&ch.to_bytes());
body.extend_from_slice(&cemi);
let frame = KnxIpFrame {
service_type: ServiceType::TunnelingRequest,
body,
};
let bytes = frame.to_bytes();
let reparsed = KnxIpFrame::parse(&bytes).unwrap();
assert_eq!(reparsed.service_type, ServiceType::TunnelingRequest);
let ch2 = ConnectionHeader::parse(&reparsed.body).unwrap();
assert_eq!(ch2.channel_id, 1);
assert_eq!(ch2.sequence_counter, 5);
assert_eq!(ch2.status, 0);
}
#[test]
fn roundtrip_tunneling_ack() {
let ch = ConnectionHeader {
channel_id: 1,
sequence_counter: 5,
status: 0,
};
let frame = KnxIpFrame {
service_type: ServiceType::TunnelingAck,
body: ch.to_bytes().to_vec(),
};
let bytes = frame.to_bytes();
assert_eq!(
bytes.len(),
HEADER_LEN as usize + ConnectionHeader::LEN as usize
);
let reparsed = KnxIpFrame::parse(&bytes).unwrap();
assert_eq!(reparsed.service_type, ServiceType::TunnelingAck);
}
#[test]
fn parse_hpai() {
let data = [0x08, 0x01, 192, 168, 1, 50, 0x0E, 0x57]; let hpai = Hpai::parse(&data).unwrap();
assert_eq!(hpai.protocol, HostProtocol::Ipv4Udp);
assert_eq!(hpai.ip, [192, 168, 1, 50]);
assert_eq!(hpai.port, 3671);
assert!(!hpai.is_unspecified());
assert_eq!(hpai.to_bytes(), data);
}
#[test]
fn hpai_nat_udp_roundtrip() {
let hpai = Hpai::nat_udp(3671);
assert_eq!(hpai.protocol, HostProtocol::Ipv4Udp);
assert!(hpai.is_unspecified());
assert_eq!(Hpai::parse(&hpai.to_bytes()), Some(hpai));
}
#[test]
fn parse_too_short() {
assert!(KnxIpFrame::parse(&[0x06, 0x10]).is_err());
}
#[test]
fn parse_bad_header_length() {
let data = [0x05, 0x10, 0x05, 0x30, 0x00, 0x06];
assert!(matches!(
KnxIpFrame::parse(&data),
Err(KnxIpError::InvalidHeaderLength)
));
}
#[test]
fn parse_bad_version() {
let data = [0x06, 0x11, 0x05, 0x30, 0x00, 0x06];
assert!(matches!(
KnxIpFrame::parse(&data),
Err(KnxIpError::InvalidProtocolVersion)
));
}
#[test]
fn parse_unknown_service() {
let data = [0x06, 0x10, 0xFF, 0xFF, 0x00, 0x06];
assert!(matches!(
KnxIpFrame::parse(&data),
Err(KnxIpError::UnknownServiceType(0xFFFF))
));
}
#[test]
fn parse_rejects_trailing_bytes() {
let data = [0x06, 0x10, 0x05, 0x30, 0x00, 0x06, 0x00];
assert!(matches!(
KnxIpFrame::parse(&data),
Err(KnxIpError::LengthMismatch)
));
}
#[test]
fn try_to_bytes_rejects_oversized_frame() {
let frame = KnxIpFrame {
service_type: ServiceType::RoutingIndication,
body: vec![0; usize::from(u16::MAX)],
};
assert!(matches!(
frame.try_to_bytes(),
Err(KnxIpError::FrameTooLong(_))
));
}
#[test]
fn connection_header_roundtrip() {
let ch = ConnectionHeader {
channel_id: 42,
sequence_counter: 7,
status: 0,
};
let bytes = ch.to_bytes();
assert_eq!(bytes, [4, 42, 7, 0]);
let parsed = ConnectionHeader::parse(&bytes).unwrap();
assert_eq!(parsed, ch);
}
}