use bacnet_types::enums::NetworkPriority;
use bacnet_types::error::Error;
use bacnet_types::MacAddr;
use bytes::{BufMut, Bytes, BytesMut};
pub const BACNET_PROTOCOL_VERSION: u8 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct NpduAddress {
pub network: u16,
pub mac_address: MacAddr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Npdu {
pub is_network_message: bool,
pub expecting_reply: bool,
pub priority: NetworkPriority,
pub destination: Option<NpduAddress>,
pub source: Option<NpduAddress>,
pub hop_count: u8,
pub message_type: Option<u8>,
pub vendor_id: Option<u16>,
pub payload: Bytes,
}
impl Default for Npdu {
fn default() -> Self {
Self {
is_network_message: false,
expecting_reply: false,
priority: NetworkPriority::NORMAL,
destination: None,
source: None,
hop_count: 255,
message_type: None,
vendor_id: None,
payload: Bytes::new(),
}
}
}
pub fn encode_npdu(buf: &mut BytesMut, npdu: &Npdu) -> Result<(), Error> {
buf.put_u8(BACNET_PROTOCOL_VERSION);
let mut control: u8 = npdu.priority.to_raw() & 0x03;
if npdu.is_network_message {
control |= 0x80;
}
if npdu.destination.is_some() {
control |= 0x20;
}
if npdu.source.is_some() {
control |= 0x08;
}
if npdu.expecting_reply {
control |= 0x04;
}
buf.put_u8(control);
if let Some(dest) = &npdu.destination {
if dest.network == 0 {
return Err(Error::Encoding("NPDU DNET must not be 0".into()));
}
buf.put_u16(dest.network);
if dest.mac_address.len() > 255 {
return Err(Error::Encoding(
"NPDU destination MAC address exceeds 255 bytes".into(),
));
}
buf.put_u8(dest.mac_address.len() as u8);
buf.put_slice(&dest.mac_address);
}
if let Some(src) = &npdu.source {
if src.network == 0 || src.network == 0xFFFF {
return Err(Error::Encoding(format!(
"NPDU SNET must be 1..65534, got {}",
src.network
)));
}
if src.mac_address.is_empty() {
return Err(Error::Encoding("NPDU SLEN must not be 0".into()));
}
buf.put_u16(src.network);
if src.mac_address.len() > 255 {
return Err(Error::Encoding(
"NPDU source MAC address exceeds 255 bytes".into(),
));
}
buf.put_u8(src.mac_address.len() as u8);
buf.put_slice(&src.mac_address);
}
if npdu.destination.is_some() {
buf.put_u8(npdu.hop_count);
}
if npdu.is_network_message {
if let Some(msg_type) = npdu.message_type {
buf.put_u8(msg_type);
if msg_type >= 0x80 {
buf.put_u16(npdu.vendor_id.unwrap_or(0));
}
}
}
buf.put_slice(&npdu.payload);
Ok(())
}
pub fn decode_npdu(data: Bytes) -> Result<Npdu, Error> {
if data.len() < 2 {
return Err(Error::buffer_too_short(2, data.len()));
}
let version = data[0];
if version != BACNET_PROTOCOL_VERSION {
return Err(Error::decoding(
0,
format!("unsupported BACnet protocol version: {version}"),
));
}
let control = data[1];
let is_network_message = control & 0x80 != 0;
let has_destination = control & 0x20 != 0;
let has_source = control & 0x08 != 0;
let expecting_reply = control & 0x04 != 0;
let priority = NetworkPriority::from_raw(control & 0x03);
if control & 0x50 != 0 {
tracing::warn!(
control_byte = control,
"NPDU control byte has reserved bits set (bits 4 or 6)"
);
}
let mut offset = 2;
let mut destination = None;
let mut source = None;
let mut hop_count: u8 = 255;
if has_destination {
if offset + 3 > data.len() {
return Err(Error::decoding(
offset,
"NPDU too short for destination fields",
));
}
let dnet = u16::from_be_bytes([data[offset], data[offset + 1]]);
offset += 2;
let dlen = data[offset] as usize;
offset += 1;
if dlen > 0 && offset + dlen > data.len() {
return Err(Error::decoding(
offset,
format!("NPDU destination address truncated: DLEN={dlen}"),
));
}
let dadr = MacAddr::from_slice(&data[offset..offset + dlen]);
offset += dlen;
if dnet == 0 {
return Err(Error::decoding(
offset - dlen - 3, "NPDU destination network 0 is invalid",
));
}
destination = Some(NpduAddress {
network: dnet,
mac_address: dadr,
});
}
if has_source {
if offset + 3 > data.len() {
return Err(Error::decoding(offset, "NPDU too short for source fields"));
}
let snet = u16::from_be_bytes([data[offset], data[offset + 1]]);
offset += 2;
let slen = data[offset] as usize;
offset += 1;
if slen == 0 {
return Err(Error::decoding(offset - 1, "NPDU source SLEN=0 is invalid"));
}
if slen > 0 && offset + slen > data.len() {
return Err(Error::decoding(
offset,
format!("NPDU source address truncated: SLEN={slen}"),
));
}
let sadr = MacAddr::from_slice(&data[offset..offset + slen]);
offset += slen;
source = Some(NpduAddress {
network: snet,
mac_address: sadr,
});
if snet == 0 {
return Err(Error::decoding(
offset - slen - 3, "NPDU source network 0 is invalid",
));
}
if snet == 0xFFFF {
return Err(Error::decoding(
offset - slen - 3,
"NPDU source network 0xFFFF is invalid",
));
}
}
if has_destination {
if offset >= data.len() {
return Err(Error::decoding(offset, "NPDU too short for hop count"));
}
hop_count = data[offset];
offset += 1;
}
let mut message_type = None;
let mut vendor_id = None;
if is_network_message {
if offset >= data.len() {
return Err(Error::decoding(
offset,
"NPDU too short for network message type",
));
}
let msg_type = data[offset];
offset += 1;
message_type = Some(msg_type);
if msg_type >= 0x80 {
if offset + 2 > data.len() {
return Err(Error::decoding(
offset,
"NPDU too short for proprietary vendor ID",
));
}
vendor_id = Some(u16::from_be_bytes([data[offset], data[offset + 1]]));
offset += 2;
}
}
let payload = data.slice(offset..);
Ok(Npdu {
is_network_message,
expecting_reply,
priority,
destination,
source,
hop_count,
message_type,
vendor_id,
payload,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn encode_to_vec(npdu: &Npdu) -> Vec<u8> {
let mut buf = BytesMut::with_capacity(64);
encode_npdu(&mut buf, npdu).unwrap();
buf.to_vec()
}
#[test]
fn minimal_local_apdu() {
let npdu = Npdu {
payload: Bytes::from_static(&[0x10, 0x08]), ..Default::default()
};
let encoded = encode_to_vec(&npdu);
assert_eq!(encoded, vec![0x01, 0x00, 0x10, 0x08]);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded, npdu);
}
#[test]
fn expecting_reply_flag() {
let npdu = Npdu {
expecting_reply: true,
payload: Bytes::from_static(&[0xAA]),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
assert_eq!(encoded[1], 0x04); let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert!(decoded.expecting_reply);
}
#[test]
fn priority_encoding() {
for (prio, expected_bits) in [
(NetworkPriority::NORMAL, 0x00),
(NetworkPriority::URGENT, 0x01),
(NetworkPriority::CRITICAL_EQUIPMENT, 0x02),
(NetworkPriority::LIFE_SAFETY, 0x03),
] {
let npdu = Npdu {
priority: prio,
payload: Bytes::new(),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
assert_eq!(encoded[1] & 0x03, expected_bits);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded.priority, prio);
}
}
#[test]
fn destination_only_round_trip() {
let npdu = Npdu {
destination: Some(NpduAddress {
network: 1000,
mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x01, 0x01, 0xBA, 0xC0]),
}),
hop_count: 254,
payload: Bytes::from_static(&[0x10, 0x08]),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
assert_eq!(encoded[1] & 0x20, 0x20);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded, npdu);
}
#[test]
fn destination_broadcast() {
let npdu = Npdu {
destination: Some(NpduAddress {
network: 0xFFFF,
mac_address: MacAddr::new(),
}),
hop_count: 255,
payload: Bytes::from_static(&[0x10, 0x08]),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
assert_eq!(encoded.len(), 8);
assert_eq!(&encoded[2..4], &[0xFF, 0xFF]);
assert_eq!(encoded[4], 0);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded, npdu);
}
#[test]
fn source_only_round_trip() {
let npdu = Npdu {
source: Some(NpduAddress {
network: 500,
mac_address: MacAddr::from_slice(&[0x01]),
}),
payload: Bytes::from_static(&[0x30, 0x01, 0x0C]),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
assert_eq!(encoded[1] & 0x08, 0x08);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded, npdu);
}
#[test]
fn source_and_destination_round_trip() {
let npdu = Npdu {
expecting_reply: true,
destination: Some(NpduAddress {
network: 2000,
mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x02, 0x01, 0xBA, 0xC0]),
}),
source: Some(NpduAddress {
network: 1000,
mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x01, 0x01, 0xBA, 0xC0]),
}),
hop_count: 250,
payload: Bytes::from_static(&[0x00, 0x05, 0x01, 0x0C]),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
assert_eq!(encoded[1], 0x2C);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded, npdu);
}
#[test]
fn network_message_round_trip() {
let npdu = Npdu {
is_network_message: true,
message_type: Some(0x01), payload: Bytes::from_static(&[0x03, 0xE8]), ..Default::default()
};
let encoded = encode_to_vec(&npdu);
assert_eq!(encoded[1] & 0x80, 0x80);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded, npdu);
}
#[test]
fn proprietary_network_message_round_trip() {
let npdu = Npdu {
is_network_message: true,
message_type: Some(0x80), vendor_id: Some(999),
payload: Bytes::from_static(&[0xDE, 0xAD]),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded, npdu);
}
#[test]
fn wire_format_who_is_broadcast() {
let wire = [0x01, 0x20, 0xFF, 0xFF, 0x00, 0xFF, 0x10, 0x08];
let decoded = decode_npdu(Bytes::copy_from_slice(&wire)).unwrap();
assert!(!decoded.is_network_message);
assert!(!decoded.expecting_reply);
assert_eq!(decoded.priority, NetworkPriority::NORMAL);
assert_eq!(
decoded.destination,
Some(NpduAddress {
network: 0xFFFF,
mac_address: MacAddr::new(),
})
);
assert!(decoded.source.is_none());
assert_eq!(decoded.hop_count, 255);
assert_eq!(decoded.payload, vec![0x10, 0x08]);
let reencoded = encode_to_vec(&decoded);
assert_eq!(reencoded, wire);
}
#[test]
fn decode_too_short() {
assert!(decode_npdu(Bytes::new()).is_err());
assert!(decode_npdu(Bytes::from_static(&[0x01])).is_err());
}
#[test]
fn decode_wrong_version() {
assert!(decode_npdu(Bytes::from_static(&[0x02, 0x00])).is_err());
}
#[test]
fn decode_truncated_destination() {
assert!(decode_npdu(Bytes::from_static(&[0x01, 0x20, 0xFF])).is_err());
}
#[test]
fn decode_truncated_source() {
assert!(decode_npdu(Bytes::from_static(&[0x01, 0x08, 0x00])).is_err());
}
#[test]
fn npdu_network_zero() {
let npdu = Npdu {
destination: Some(NpduAddress {
network: 0,
mac_address: MacAddr::from_slice(&[0x01]),
}),
hop_count: 255,
payload: Bytes::from_static(&[0x10, 0x08]),
..Default::default()
};
let mut buf = BytesMut::new();
let result = encode_npdu(&mut buf, &npdu);
assert!(result.is_err());
let err = format!("{}", result.unwrap_err());
assert!(err.contains("DNET"), "got: {err}");
}
#[test]
fn npdu_network_fffe() {
let npdu = Npdu {
destination: Some(NpduAddress {
network: 0xFFFE,
mac_address: MacAddr::from_slice(&[0x01, 0x02]),
}),
hop_count: 200,
payload: Bytes::from_static(&[0xAA]),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded.destination.as_ref().unwrap().network, 0xFFFE);
assert_eq!(decoded.hop_count, 200);
}
#[test]
fn npdu_hop_count_zero() {
let npdu = Npdu {
destination: Some(NpduAddress {
network: 1000,
mac_address: MacAddr::new(),
}),
hop_count: 0,
payload: Bytes::from_static(&[0x10, 0x08]),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded.hop_count, 0);
}
#[test]
fn npdu_source_with_empty_mac() {
let npdu = Npdu {
source: Some(NpduAddress {
network: 500,
mac_address: MacAddr::new(),
}),
payload: Bytes::from_static(&[0xBB]),
..Default::default()
};
let mut buf = BytesMut::new();
let result = encode_npdu(&mut buf, &npdu);
assert!(result.is_err());
let err = format!("{}", result.unwrap_err());
assert!(err.contains("SLEN"), "got: {err}");
}
#[test]
fn npdu_destination_dlen_zero_broadcast_accepted() {
let npdu = Npdu {
destination: Some(NpduAddress {
network: 0xFFFF,
mac_address: MacAddr::new(),
}),
hop_count: 255,
payload: Bytes::from_static(&[0x10, 0x08]),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded.destination.as_ref().unwrap().network, 0xFFFF);
assert!(decoded.destination.as_ref().unwrap().mac_address.is_empty());
}
#[test]
fn npdu_destination_truncated_mac() {
let data = [0x01, 0x20, 0x03, 0xE8, 0x06, 0x01, 0x02];
assert!(decode_npdu(Bytes::copy_from_slice(&data)).is_err());
}
#[test]
fn npdu_source_truncated_mac() {
let data = [0x01, 0x08, 0x01, 0xF4, 0x04, 0x01]; assert!(decode_npdu(Bytes::copy_from_slice(&data)).is_err());
}
#[test]
fn npdu_missing_hop_count() {
let data = [0x01, 0x20, 0xFF, 0xFF, 0x00];
assert!(decode_npdu(Bytes::copy_from_slice(&data)).is_err());
}
#[test]
fn npdu_network_message_truncated_type() {
let data = [0x01, 0x80]; assert!(decode_npdu(Bytes::copy_from_slice(&data)).is_err());
}
#[test]
fn npdu_proprietary_message_truncated_vendor() {
let data = [0x01, 0x80, 0x80]; assert!(decode_npdu(Bytes::copy_from_slice(&data)).is_err());
}
#[test]
fn npdu_all_flags_round_trip() {
let npdu = Npdu {
is_network_message: false,
expecting_reply: true,
priority: NetworkPriority::LIFE_SAFETY,
destination: Some(NpduAddress {
network: 2000,
mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x02, 0x01, 0xBA, 0xC0]),
}),
source: Some(NpduAddress {
network: 1000,
mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x01, 0x01, 0xBA, 0xC0]),
}),
hop_count: 127,
payload: Bytes::from_static(&[0xDE, 0xAD, 0xBE, 0xEF]),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded, npdu);
}
#[test]
fn npdu_empty_payload() {
let npdu = Npdu {
payload: Bytes::new(),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert!(decoded.payload.is_empty());
}
#[test]
fn reject_snet_zero() {
let npdu = Npdu {
source: Some(NpduAddress {
network: 0,
mac_address: MacAddr::from_slice(&[0x01]),
}),
payload: Bytes::from_static(&[0x10, 0x08]),
..Default::default()
};
let mut buf = BytesMut::new();
let result = encode_npdu(&mut buf, &npdu);
assert!(result.is_err());
let err = format!("{}", result.unwrap_err());
assert!(err.contains("SNET"), "got: {err}");
}
#[test]
fn reserved_bits_warning_still_decodes() {
let mut data = vec![0x01, 0x40]; data.extend_from_slice(&[0x10, 0x08]);
let result = decode_npdu(Bytes::copy_from_slice(&data));
assert!(
result.is_ok(),
"reserved bits should not cause decode failure"
);
}
}