#[cfg(feature = "std")]
use std::{
io::ErrorKind,
net::{SocketAddr, ToSocketAddrs, UdpSocket},
time::{Duration, Instant},
};
#[cfg(not(feature = "std"))]
use alloc::{string::String, vec::Vec};
use crate::datalink::{DataLink, DataLinkAddress, DataLinkError, DataLinkType, Result};
pub const BACNET_IP_PORT: u16 = 47808;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum BvlcFunction {
OriginalUnicastNpdu = 0x0A,
OriginalBroadcastNpdu = 0x0B,
ForwardedNpdu = 0x04,
RegisterForeignDevice = 0x05,
ReadBroadcastDistributionTable = 0x02,
ReadBroadcastDistributionTableAck = 0x03,
ReadForeignDeviceTable = 0x06,
ReadForeignDeviceTableAck = 0x07,
DeleteForeignDeviceTableEntry = 0x08,
DistributeBroadcastToNetwork = 0x09,
ForwardedNpduFromDevice = 0x0C,
SecureBvll = 0x0D,
}
impl TryFrom<u8> for BvlcFunction {
type Error = DataLinkError;
fn try_from(value: u8) -> core::result::Result<Self, Self::Error> {
let function = match value {
0x0A => BvlcFunction::OriginalUnicastNpdu,
0x0B => BvlcFunction::OriginalBroadcastNpdu,
0x04 => BvlcFunction::ForwardedNpdu,
0x05 => BvlcFunction::RegisterForeignDevice,
0x02 => BvlcFunction::ReadBroadcastDistributionTable,
0x03 => BvlcFunction::ReadBroadcastDistributionTableAck,
0x06 => BvlcFunction::ReadForeignDeviceTable,
0x07 => BvlcFunction::ReadForeignDeviceTableAck,
0x08 => BvlcFunction::DeleteForeignDeviceTableEntry,
0x09 => BvlcFunction::DistributeBroadcastToNetwork,
0x0C => BvlcFunction::ForwardedNpduFromDevice,
0x0D => BvlcFunction::SecureBvll,
_ => return Err(DataLinkError::InvalidFrame),
};
Ok(function)
}
}
#[derive(Debug, Clone)]
pub struct BvlcHeader {
pub bvlc_type: u8,
pub function: BvlcFunction,
pub length: u16,
}
impl BvlcHeader {
pub fn new(function: BvlcFunction, length: u16) -> Self {
Self {
bvlc_type: 0x81, function,
length,
}
}
pub fn encode(&self) -> Vec<u8> {
vec![
self.bvlc_type,
self.function as u8,
(self.length >> 8) as u8,
(self.length & 0xFF) as u8,
]
}
pub fn decode(data: &[u8]) -> Result<Self> {
if data.len() < 4 {
return Err(DataLinkError::InvalidFrame);
}
let bvlc_type = data[0];
if bvlc_type != 0x81 {
return Err(DataLinkError::InvalidFrame);
}
let function = data[1].try_into()?;
let length = ((data[2] as u16) << 8) | (data[3] as u16);
Ok(BvlcHeader {
bvlc_type,
function,
length,
})
}
}
#[derive(Debug, Clone)]
#[cfg(feature = "std")]
pub struct BdtEntry {
pub address: SocketAddr,
pub mask: [u8; 4],
}
#[derive(Debug, Clone)]
#[cfg(feature = "std")]
pub struct FdtEntry {
pub address: SocketAddr,
pub ttl: u16,
pub registration_time: Instant,
}
#[cfg(feature = "std")]
pub struct BacnetIpDataLink {
socket: UdpSocket,
local_addr: SocketAddr,
bdt: Vec<BdtEntry>,
fdt: Vec<FdtEntry>,
broadcast_addr: SocketAddr,
}
#[cfg(feature = "std")]
impl BacnetIpDataLink {
pub fn new<A: ToSocketAddrs>(bind_addr: A) -> Result<Self> {
let socket = UdpSocket::bind(bind_addr).map_err(DataLinkError::IoError)?;
let local_addr = socket.local_addr().map_err(DataLinkError::IoError)?;
socket.set_broadcast(true).map_err(DataLinkError::IoError)?;
socket
.set_read_timeout(Some(Duration::from_millis(100)))
.map_err(DataLinkError::IoError)?;
let broadcast_addr = match local_addr {
SocketAddr::V4(addr) => {
let ip = addr.ip().octets();
let broadcast_ip = std::net::Ipv4Addr::new(ip[0], ip[1], ip[2], 255);
SocketAddr::new(broadcast_ip.into(), BACNET_IP_PORT)
}
SocketAddr::V6(_) => {
return Err(DataLinkError::UnsupportedType);
}
};
Ok(Self {
socket,
local_addr,
bdt: Vec::new(),
fdt: Vec::new(),
broadcast_addr,
})
}
pub fn send_unicast_npdu(&mut self, npdu: &[u8], dest: SocketAddr) -> Result<()> {
let header = BvlcHeader::new(BvlcFunction::OriginalUnicastNpdu, 4 + npdu.len() as u16);
let mut frame = header.encode();
frame.extend_from_slice(npdu);
self.socket
.send_to(&frame, dest)
.map_err(DataLinkError::IoError)?;
Ok(())
}
pub fn send_broadcast_npdu(&mut self, npdu: &[u8]) -> Result<()> {
let header = BvlcHeader::new(BvlcFunction::OriginalBroadcastNpdu, 4 + npdu.len() as u16);
let mut frame = header.encode();
frame.extend_from_slice(npdu);
self.socket
.send_to(&frame, self.broadcast_addr)
.map_err(DataLinkError::IoError)?;
for entry in &self.bdt {
let _ = self.socket.send_to(&frame, entry.address);
}
Ok(())
}
pub fn register_foreign_device(&mut self, bbmd_addr: SocketAddr, ttl: u16) -> Result<()> {
let header = BvlcHeader::new(BvlcFunction::RegisterForeignDevice, 6);
let mut frame = header.encode();
frame.extend_from_slice(&ttl.to_be_bytes());
self.socket
.send_to(&frame, bbmd_addr)
.map_err(DataLinkError::IoError)?;
Ok(())
}
pub fn add_bdt_entry(&mut self, address: SocketAddr, mask: [u8; 4]) {
self.bdt.push(BdtEntry { address, mask });
}
pub fn cleanup_fdt(&mut self) {
let now = Instant::now();
self.fdt.retain(|entry| {
now.duration_since(entry.registration_time).as_secs() < entry.ttl as u64
});
}
fn process_bvlc_message(&mut self, data: &[u8], source: SocketAddr) -> Result<Option<Vec<u8>>> {
let header = BvlcHeader::decode(data)?;
if data.len() != header.length as usize {
return Err(DataLinkError::InvalidFrame);
}
match header.function {
BvlcFunction::OriginalUnicastNpdu | BvlcFunction::OriginalBroadcastNpdu => {
if data.len() > 4 {
Ok(Some(data[4..].to_vec()))
} else {
Err(DataLinkError::InvalidFrame)
}
}
BvlcFunction::ForwardedNpdu => {
if data.len() > 10 {
Ok(Some(data[10..].to_vec()))
} else {
Err(DataLinkError::InvalidFrame)
}
}
BvlcFunction::RegisterForeignDevice => {
if data.len() == 6 {
let ttl = u16::from_be_bytes([data[4], data[5]]);
self.fdt.push(FdtEntry {
address: source,
ttl,
registration_time: Instant::now(),
});
}
Ok(None)
}
_ => {
Ok(None)
}
}
}
}
#[cfg(feature = "std")]
impl DataLink for BacnetIpDataLink {
fn send_frame(&mut self, frame: &[u8], dest: &DataLinkAddress) -> Result<()> {
match dest {
DataLinkAddress::Ip(addr) => self.send_unicast_npdu(frame, *addr),
DataLinkAddress::Broadcast => self.send_broadcast_npdu(frame),
_ => Err(DataLinkError::UnsupportedType),
}
}
fn receive_frame(&mut self) -> Result<(Vec<u8>, DataLinkAddress)> {
let mut buffer = [0u8; 1500];
match self.socket.recv_from(&mut buffer) {
Ok((len, source)) => {
let data = &buffer[..len];
if let Some(npdu) = self.process_bvlc_message(data, source)? {
Ok((npdu, DataLinkAddress::Ip(source)))
} else {
Err(DataLinkError::InvalidFrame)
}
}
Err(e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => {
Err(DataLinkError::IoError(e))
}
Err(e) => Err(DataLinkError::IoError(e)),
}
}
fn link_type(&self) -> DataLinkType {
DataLinkType::BacnetIp
}
fn local_address(&self) -> DataLinkAddress {
DataLinkAddress::Ip(self.local_addr)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bvlc_header_encode_decode() {
let header = BvlcHeader::new(BvlcFunction::OriginalUnicastNpdu, 1024);
let encoded = header.encode();
assert_eq!(encoded.len(), 4);
assert_eq!(encoded[0], 0x81);
assert_eq!(encoded[1], 0x0A);
assert_eq!(encoded[2], 0x04);
assert_eq!(encoded[3], 0x00);
let decoded = BvlcHeader::decode(&encoded).unwrap();
assert_eq!(decoded.bvlc_type, 0x81);
assert_eq!(decoded.function, BvlcFunction::OriginalUnicastNpdu);
assert_eq!(decoded.length, 1024);
}
#[cfg(feature = "std")]
#[test]
fn test_bacnet_ip_creation() {
let result = BacnetIpDataLink::new("127.0.0.1:0");
assert!(result.is_ok());
let datalink = result.unwrap();
assert_eq!(datalink.link_type(), DataLinkType::BacnetIp);
}
}