#[cfg(feature = "std")]
use std::error::Error;
#[cfg(feature = "std")]
use std::{
collections::HashMap,
fmt,
net::{IpAddr, SocketAddr},
time::Instant,
};
#[cfg(not(feature = "std"))]
use core::fmt;
#[cfg(not(feature = "std"))]
use alloc::string::String;
#[cfg(not(feature = "std"))]
use core::time::Duration;
#[cfg(feature = "std")]
use std::time::Duration;
#[cfg(feature = "std")]
pub type Result<T> = std::result::Result<T, TransportError>;
#[cfg(not(feature = "std"))]
pub type Result<T> = core::result::Result<T, TransportError>;
#[derive(Debug)]
pub enum TransportError {
#[cfg(feature = "std")]
IoError(std::io::Error),
InvalidBvll(String),
RegistrationFailed,
NotConnected,
InvalidConfiguration(String),
Timeout(String),
RequestNotFound(u8),
}
impl fmt::Display for TransportError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
#[cfg(feature = "std")]
TransportError::IoError(e) => write!(f, "I/O error: {}", e),
TransportError::InvalidBvll(msg) => write!(f, "Invalid BVLL: {}", msg),
TransportError::RegistrationFailed => write!(f, "Foreign device registration failed"),
TransportError::NotConnected => write!(f, "Transport not connected"),
TransportError::InvalidConfiguration(msg) => {
write!(f, "Invalid configuration: {}", msg)
}
TransportError::Timeout(msg) => write!(f, "Timeout: {}", msg),
TransportError::RequestNotFound(invoke_id) => {
write!(f, "Request not found: invoke ID {}", invoke_id)
}
}
}
}
#[cfg(feature = "std")]
impl Error for TransportError {}
#[cfg(feature = "std")]
impl From<std::io::Error> for TransportError {
fn from(error: std::io::Error) -> Self {
TransportError::IoError(error)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum BvllType {
BacnetIp = 0x81,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum BvllFunction {
OriginalUnicastNpdu = 0x0A,
OriginalBroadcastNpdu = 0x0B,
SecureBvll = 0x0C,
DistributeBroadcastToNetwork = 0x09,
RegisterForeignDevice = 0x05,
ReadBroadcastDistributionTable = 0x02,
ReadBroadcastDistributionTableAck = 0x03,
ForwardedNpdu = 0x04,
WriteBroadcastDistributionTable = 0x01,
ReadForeignDeviceTable = 0x06,
ReadForeignDeviceTableAck = 0x07,
DeleteForeignDeviceTableEntry = 0x08,
Result = 0x00,
}
#[derive(Debug, Clone)]
pub struct BvllHeader {
pub bvll_type: BvllType,
pub function: BvllFunction,
pub length: u16,
}
#[cfg(feature = "std")]
#[derive(Debug, Clone)]
pub struct ForeignDeviceRegistration {
pub bbmd_address: SocketAddr,
pub ttl: u16,
pub last_registration: Instant,
}
#[cfg(feature = "std")]
#[derive(Debug, Clone)]
pub struct BdtEntry {
pub address: IpAddr,
pub port: u16,
pub mask: IpAddr,
}
#[cfg(feature = "std")]
pub trait Transport: Send + Sync {
fn send(&mut self, data: &[u8], dest: &SocketAddr) -> Result<()>;
fn receive(&mut self) -> Result<(Vec<u8>, SocketAddr)>;
fn receive_timeout(&mut self, timeout: Duration) -> Result<(Vec<u8>, SocketAddr)>;
fn send_confirmed_request(
&mut self,
dest: SocketAddr,
data: &[u8],
timeout: Option<Duration>,
) -> Result<u8>;
fn check_timeouts(&mut self) -> Vec<u8>;
fn complete_request(&mut self, invoke_id: u8) -> Option<Duration>;
fn local_address(&self) -> Result<SocketAddr>;
fn is_connected(&self) -> bool;
}
#[cfg(feature = "std")]
#[derive(Debug, Clone)]
pub struct BacnetIpConfig {
pub bind_address: SocketAddr,
pub broadcast_enabled: bool,
pub foreign_device: Option<ForeignDeviceRegistration>,
pub bdt: Vec<BdtEntry>,
pub buffer_size: usize,
pub read_timeout: Option<Duration>,
pub write_timeout: Option<Duration>,
pub request_timeout: Duration,
pub registration_timeout: Duration,
}
#[cfg(feature = "std")]
impl Default for BacnetIpConfig {
fn default() -> Self {
Self {
bind_address: "0.0.0.0:47808"
.parse()
.expect("hardcoded address should be valid"),
broadcast_enabled: true,
foreign_device: None,
bdt: Vec::new(),
buffer_size: 1500,
read_timeout: Some(Duration::from_secs(5)),
write_timeout: Some(Duration::from_secs(5)),
request_timeout: Duration::from_secs(10),
registration_timeout: Duration::from_secs(30),
}
}
}
pub mod constants {
use super::Duration;
pub const BACNET_IP_PORT: u16 = 0xBAC0;
pub const MAX_BVLL_LENGTH: usize = 1497;
pub const BVLL_HEADER_SIZE: usize = 4;
pub const DEFAULT_FD_TTL: u16 = 900;
pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
pub const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(5);
pub const DEFAULT_WRITE_TIMEOUT: Duration = Duration::from_secs(5);
pub const DEFAULT_REGISTRATION_TIMEOUT: Duration = Duration::from_secs(30);
pub const MAX_CONCURRENT_REQUESTS: usize = 255;
}
impl BvllHeader {
pub fn new(function: BvllFunction, length: u16) -> Self {
Self {
bvll_type: BvllType::BacnetIp,
function,
length,
}
}
pub fn encode(&self) -> [u8; 4] {
[
self.bvll_type as u8,
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(TransportError::InvalidBvll("Header too short".into()));
}
let bvll_type = match data[0] {
0x81 => BvllType::BacnetIp,
_ => return Err(TransportError::InvalidBvll("Invalid BVLL type".into())),
};
let function = match data[1] {
0x00 => BvllFunction::Result,
0x01 => BvllFunction::WriteBroadcastDistributionTable,
0x02 => BvllFunction::ReadBroadcastDistributionTable,
0x03 => BvllFunction::ReadBroadcastDistributionTableAck,
0x04 => BvllFunction::ForwardedNpdu,
0x05 => BvllFunction::RegisterForeignDevice,
0x06 => BvllFunction::ReadForeignDeviceTable,
0x07 => BvllFunction::ReadForeignDeviceTableAck,
0x08 => BvllFunction::DeleteForeignDeviceTableEntry,
0x09 => BvllFunction::DistributeBroadcastToNetwork,
0x0A => BvllFunction::OriginalUnicastNpdu,
0x0B => BvllFunction::OriginalBroadcastNpdu,
0x0C => BvllFunction::SecureBvll,
_ => return Err(TransportError::InvalidBvll("Invalid BVLL function".into())),
};
let length = ((data[2] as u16) << 8) | (data[3] as u16);
Ok(Self {
bvll_type,
function,
length,
})
}
}
#[derive(Debug, Clone)]
pub struct BvllMessage {
pub header: BvllHeader,
pub data: Vec<u8>,
}
impl BvllMessage {
pub fn new(function: BvllFunction, data: Vec<u8>) -> Self {
let length = (constants::BVLL_HEADER_SIZE + data.len()) as u16;
Self {
header: BvllHeader::new(function, length),
data,
}
}
pub fn encode(&self) -> Vec<u8> {
let mut result = Vec::new();
result.extend_from_slice(&self.header.encode());
result.extend_from_slice(&self.data);
result
}
pub fn decode(data: &[u8]) -> Result<Self> {
let header = BvllHeader::decode(data)?;
if data.len() < header.length as usize {
return Err(TransportError::InvalidBvll("Message too short".into()));
}
let message_data = data[constants::BVLL_HEADER_SIZE..header.length as usize].to_vec();
Ok(Self {
header,
data: message_data,
})
}
}
#[cfg(feature = "std")]
use std::net::UdpSocket;
#[cfg(feature = "std")]
#[derive(Debug)]
pub struct TimeoutManager {
timeouts: HashMap<u8, (Instant, Duration)>,
next_invoke_id: u8,
}
#[cfg(feature = "std")]
impl TimeoutManager {
pub fn new() -> Self {
Self {
timeouts: HashMap::new(),
next_invoke_id: 1,
}
}
pub fn start_request(&mut self, timeout: Duration) -> u8 {
let invoke_id = self.next_invoke_id;
self.next_invoke_id = self.next_invoke_id.wrapping_add(1);
if self.next_invoke_id == 0 {
self.next_invoke_id = 1; }
self.timeouts.insert(invoke_id, (Instant::now(), timeout));
invoke_id
}
pub fn complete_request(&mut self, invoke_id: u8) -> Option<Duration> {
self.timeouts
.remove(&invoke_id)
.map(|(start_time, _)| start_time.elapsed())
}
pub fn check_timeouts(&mut self) -> Vec<u8> {
let mut timed_out = Vec::new();
let now = Instant::now();
self.timeouts.retain(|&invoke_id, (start_time, timeout)| {
if now.duration_since(*start_time) > *timeout {
timed_out.push(invoke_id);
false
} else {
true
}
});
timed_out
}
pub fn active_count(&self) -> usize {
self.timeouts.len()
}
pub fn remaining_time(&self, invoke_id: u8) -> Option<Duration> {
self.timeouts.get(&invoke_id).map(|(start_time, timeout)| {
let elapsed = start_time.elapsed();
if elapsed < *timeout {
*timeout - elapsed
} else {
Duration::from_secs(0)
}
})
}
pub fn clear(&mut self) {
self.timeouts.clear();
}
pub fn active_invoke_ids(&self) -> Vec<u8> {
self.timeouts.keys().copied().collect()
}
}
#[cfg(feature = "std")]
impl Default for TimeoutManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "std")]
pub struct BacnetIpTransport {
socket: UdpSocket,
config: BacnetIpConfig,
buffer: Vec<u8>,
active_requests: std::collections::HashMap<u8, (Instant, Duration)>,
next_invoke_id: u8,
}
#[cfg(feature = "std")]
impl BacnetIpTransport {
pub fn new(config: BacnetIpConfig) -> Result<Self> {
let socket = UdpSocket::bind(config.bind_address)?;
if config.broadcast_enabled {
socket.set_broadcast(true)?;
}
socket.set_read_timeout(config.read_timeout)?;
socket.set_write_timeout(config.write_timeout)?;
let buffer = vec![0u8; config.buffer_size];
Ok(Self {
socket,
config,
buffer,
active_requests: HashMap::new(),
next_invoke_id: 1,
})
}
pub fn new_default(bind_addr: &str) -> Result<Self> {
let config = BacnetIpConfig {
bind_address: bind_addr
.parse()
.map_err(|_| TransportError::InvalidConfiguration("Invalid bind address".into()))?,
..Default::default()
};
Self::new(config)
}
pub fn send_bvll(&mut self, message: BvllMessage, dest: SocketAddr) -> Result<()> {
let encoded = message.encode();
self.socket.send_to(&encoded, dest)?;
Ok(())
}
pub fn send_npdu_unicast(&mut self, npdu: &[u8], dest: SocketAddr) -> Result<()> {
let message = BvllMessage::new(BvllFunction::OriginalUnicastNpdu, npdu.to_vec());
self.send_bvll(message, dest)
}
pub fn send_npdu_broadcast(&mut self, npdu: &[u8], dest: SocketAddr) -> Result<()> {
let message = BvllMessage::new(BvllFunction::OriginalBroadcastNpdu, npdu.to_vec());
self.send_bvll(message, dest)
}
pub fn receive_bvll(&mut self) -> Result<(BvllMessage, SocketAddr)> {
let (len, src) = self.socket.recv_from(&mut self.buffer)?;
let message = BvllMessage::decode(&self.buffer[..len])?;
Ok((message, src))
}
pub fn receive_bvll_timeout(&mut self, timeout: Duration) -> Result<(BvllMessage, SocketAddr)> {
let original_timeout = self.socket.read_timeout()?;
self.socket.set_read_timeout(Some(timeout))?;
let result = self.receive_bvll();
self.socket.set_read_timeout(original_timeout)?;
result
}
pub fn register_foreign_device(&mut self, bbmd_addr: SocketAddr, ttl: u16) -> Result<()> {
let mut data = Vec::new();
data.extend_from_slice(&ttl.to_be_bytes());
let message = BvllMessage::new(BvllFunction::RegisterForeignDevice, data);
self.send_bvll(message, bbmd_addr)?;
let start_time = Instant::now();
while start_time.elapsed() < self.config.registration_timeout {
if let Ok((response, src)) = self.receive_bvll_timeout(Duration::from_millis(100)) {
if src == bbmd_addr && response.header.function == BvllFunction::Result {
if !response.data.is_empty() {
let result_code = u16::from_be_bytes([response.data[0], response.data[1]]);
if result_code == 0 {
self.config.foreign_device = Some(ForeignDeviceRegistration {
bbmd_address: bbmd_addr,
ttl,
last_registration: Instant::now(),
});
return Ok(());
} else {
return Err(TransportError::RegistrationFailed);
}
}
}
}
}
self.config.foreign_device = Some(ForeignDeviceRegistration {
bbmd_address: bbmd_addr,
ttl,
last_registration: Instant::now(),
});
Ok(())
}
pub fn send_foreign_device_heartbeat(&mut self) -> Result<()> {
if let Some(ref fd_reg) = self.config.foreign_device {
let elapsed = fd_reg.last_registration.elapsed().as_secs() as u16;
if elapsed >= fd_reg.ttl / 2 {
self.register_foreign_device(fd_reg.bbmd_address, fd_reg.ttl)?;
}
}
Ok(())
}
pub fn config(&self) -> &BacnetIpConfig {
&self.config
}
pub fn update_config(&mut self, config: BacnetIpConfig) -> Result<()> {
if config.bind_address != self.config.bind_address {
return Err(TransportError::InvalidConfiguration(
"Cannot change bind address on existing transport".into(),
));
}
if config.read_timeout != self.config.read_timeout {
self.socket.set_read_timeout(config.read_timeout)?;
}
if config.write_timeout != self.config.write_timeout {
self.socket.set_write_timeout(config.write_timeout)?;
}
self.config = config;
Ok(())
}
pub fn send_confirmed_request(
&mut self,
dest: SocketAddr,
data: &[u8],
timeout: Option<Duration>,
) -> Result<u8> {
let invoke_id = self.next_invoke_id;
self.next_invoke_id = self.next_invoke_id.wrapping_add(1);
if self.next_invoke_id == 0 {
self.next_invoke_id = 1; }
let request_timeout = timeout.unwrap_or(self.config.request_timeout);
self.active_requests
.insert(invoke_id, (Instant::now(), request_timeout));
self.send_npdu_unicast(data, dest)?;
Ok(invoke_id)
}
pub fn check_timeouts(&mut self) -> Vec<u8> {
let mut timed_out = Vec::new();
let now = Instant::now();
self.active_requests
.retain(|&invoke_id, (start_time, timeout)| {
if now.duration_since(*start_time) > *timeout {
timed_out.push(invoke_id);
false } else {
true }
});
timed_out
}
pub fn complete_request(&mut self, invoke_id: u8) -> Option<Duration> {
self.active_requests
.remove(&invoke_id)
.map(|(start_time, _)| start_time.elapsed())
}
pub fn active_request_count(&self) -> usize {
self.active_requests.len()
}
pub fn get_requests_by_remaining_time(&self) -> Vec<(u8, Duration)> {
let now = Instant::now();
let mut requests: Vec<(u8, Duration)> = self
.active_requests
.iter()
.map(|(&invoke_id, (start_time, timeout))| {
let elapsed = now.duration_since(*start_time);
if elapsed < *timeout {
(invoke_id, *timeout - elapsed)
} else {
(invoke_id, Duration::from_secs(0))
}
})
.collect();
requests.sort_by_key(|(_, remaining)| *remaining);
requests
}
}
#[cfg(feature = "std")]
impl Transport for BacnetIpTransport {
fn send(&mut self, data: &[u8], dest: &SocketAddr) -> Result<()> {
self.send_npdu_unicast(data, *dest)
}
fn receive(&mut self) -> Result<(Vec<u8>, SocketAddr)> {
let (message, src) = self.receive_bvll()?;
Ok((message.data, src))
}
fn receive_timeout(&mut self, timeout: Duration) -> Result<(Vec<u8>, SocketAddr)> {
let (message, src) = self.receive_bvll_timeout(timeout)?;
Ok((message.data, src))
}
fn send_confirmed_request(
&mut self,
dest: SocketAddr,
data: &[u8],
timeout: Option<Duration>,
) -> Result<u8> {
self.send_confirmed_request(dest, data, timeout)
}
fn check_timeouts(&mut self) -> Vec<u8> {
self.check_timeouts()
}
fn complete_request(&mut self, invoke_id: u8) -> Option<Duration> {
self.complete_request(invoke_id)
}
fn local_address(&self) -> Result<SocketAddr> {
Ok(self.socket.local_addr()?)
}
fn is_connected(&self) -> bool {
true }
}
#[cfg(feature = "std")]
#[derive(Default)]
pub struct BroadcastManager {
bdt: Vec<BdtEntry>,
}
#[cfg(feature = "std")]
impl BroadcastManager {
pub fn new() -> Self {
Self::default()
}
pub fn add_bdt_entry(&mut self, entry: BdtEntry) {
self.bdt.push(entry);
}
pub fn remove_bdt_entry(&mut self, address: IpAddr) {
self.bdt.retain(|entry| entry.address != address);
}
pub fn get_bdt_entries(&self) -> &[BdtEntry] {
&self.bdt
}
pub fn encode_bdt(&self) -> Vec<u8> {
let mut data = Vec::new();
for entry in &self.bdt {
match entry.address {
IpAddr::V4(addr) => {
data.extend_from_slice(&addr.octets());
data.extend_from_slice(&entry.port.to_be_bytes());
if let IpAddr::V4(mask) = entry.mask {
data.extend_from_slice(&mask.octets());
} else {
data.extend_from_slice(&[255, 255, 255, 255]); }
}
IpAddr::V6(_) => {
}
}
}
data
}
#[allow(clippy::manual_is_multiple_of)]
pub fn decode_bdt(&mut self, data: &[u8]) -> Result<()> {
self.bdt.clear();
let entry_size = 10; if data.len() % entry_size != 0 {
return Err(TransportError::InvalidBvll(
"Invalid BDT data length".into(),
));
}
for chunk in data.chunks_exact(entry_size) {
let ip_bytes = [chunk[0], chunk[1], chunk[2], chunk[3]];
let address = IpAddr::V4(ip_bytes.into());
let port = u16::from_be_bytes([chunk[4], chunk[5]]);
let mask_bytes = [chunk[6], chunk[7], chunk[8], chunk[9]];
let mask = IpAddr::V4(mask_bytes.into());
self.bdt.push(BdtEntry {
address,
port,
mask,
});
}
Ok(())
}
}
#[cfg(feature = "std")]
#[derive(Debug, Clone)]
pub struct TimeoutConfig {
pub read_timeout: Duration,
pub write_timeout: Duration,
pub request_timeout: Duration,
pub registration_timeout: Duration,
pub discovery_timeout: Duration,
pub property_read_timeout: Duration,
pub file_transfer_timeout: Duration,
}
#[cfg(feature = "std")]
impl Default for TimeoutConfig {
fn default() -> Self {
Self {
read_timeout: Duration::from_secs(5),
write_timeout: Duration::from_secs(5),
request_timeout: Duration::from_secs(10),
registration_timeout: Duration::from_secs(30),
discovery_timeout: Duration::from_secs(3),
property_read_timeout: Duration::from_secs(5),
file_transfer_timeout: Duration::from_secs(60),
}
}
}
#[cfg(feature = "std")]
pub mod timeout_utils {
use super::*;
pub fn wait_for_condition<F>(
condition: F,
timeout: Duration,
check_interval: Duration,
) -> Result<()>
where
F: Fn() -> bool,
{
let start = Instant::now();
while start.elapsed() < timeout {
if condition() {
return Ok(());
}
std::thread::sleep(check_interval);
}
Err(TransportError::Timeout(
"Condition not met within timeout".into(),
))
}
pub fn retry_with_backoff<F, T, E>(
mut operation: F,
max_attempts: u32,
initial_delay: Duration,
max_delay: Duration,
backoff_multiplier: f64,
) -> std::result::Result<T, E>
where
F: FnMut() -> std::result::Result<T, E>,
{
let mut delay = initial_delay;
let mut last_error = None;
for attempt in 0..max_attempts {
match operation() {
Ok(result) => return Ok(result),
Err(e) => {
last_error = Some(e);
if attempt < max_attempts - 1 {
std::thread::sleep(delay);
delay = std::cmp::min(
Duration::from_millis(
(delay.as_millis() as f64 * backoff_multiplier) as u64,
),
max_delay,
);
}
}
}
}
Err(last_error.expect("retry loop should have at least one error"))
}
pub fn with_timeout<F, T>(operation: F, timeout: Duration) -> Result<T>
where
F: FnOnce() -> Result<T>,
{
let start = Instant::now();
let result = operation();
if start.elapsed() > timeout {
Err(TransportError::Timeout(format!(
"Operation took {:.2}s, timeout was {:.2}s",
start.elapsed().as_secs_f64(),
timeout.as_secs_f64()
)))
} else {
result
}
}
pub fn calculate_adaptive_timeout(
recent_times: &[Duration],
base_timeout: Duration,
safety_factor: f64,
) -> Duration {
if recent_times.is_empty() {
return base_timeout;
}
let avg = recent_times.iter().sum::<Duration>() / recent_times.len() as u32;
let variance: f64 = recent_times
.iter()
.map(|t| {
let diff = t.as_secs_f64() - avg.as_secs_f64();
diff * diff
})
.sum::<f64>()
/ recent_times.len() as f64;
let std_dev = variance.sqrt();
let adaptive_secs = avg.as_secs_f64() + (safety_factor * std_dev);
let adaptive_timeout = Duration::from_secs_f64(adaptive_secs);
std::cmp::max(adaptive_timeout, base_timeout)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bvll_header_encode_decode() {
let header = BvllHeader::new(BvllFunction::OriginalUnicastNpdu, 100);
let encoded = header.encode();
assert_eq!(encoded[0], 0x81); assert_eq!(encoded[1], 0x0A); assert_eq!(encoded[2], 0x00); assert_eq!(encoded[3], 0x64);
let decoded = BvllHeader::decode(&encoded).unwrap();
assert_eq!(decoded.bvll_type as u8, header.bvll_type as u8);
assert_eq!(decoded.function as u8, header.function as u8);
assert_eq!(decoded.length, header.length);
}
#[test]
fn test_bvll_message_encode_decode() {
let test_data = vec![0x01, 0x02, 0x03, 0x04];
let message = BvllMessage::new(BvllFunction::OriginalBroadcastNpdu, test_data.clone());
let encoded = message.encode();
assert_eq!(encoded.len(), 4 + test_data.len());
let decoded = BvllMessage::decode(&encoded).unwrap();
assert_eq!(
decoded.header.function as u8,
BvllFunction::OriginalBroadcastNpdu as u8
);
assert_eq!(decoded.data, test_data);
}
#[test]
fn test_bvll_function_decode() {
let test_cases = [
(0x00, BvllFunction::Result),
(0x0A, BvllFunction::OriginalUnicastNpdu),
(0x0B, BvllFunction::OriginalBroadcastNpdu),
(0x05, BvllFunction::RegisterForeignDevice),
];
for (code, expected) in test_cases.iter() {
let data = [0x81, *code, 0x00, 0x04];
let header = BvllHeader::decode(&data).unwrap();
assert_eq!(header.function as u8, *expected as u8);
}
}
#[test]
fn test_broadcast_manager() {
let mut manager = BroadcastManager::new();
let entry = BdtEntry {
address: "192.168.1.1".parse().unwrap(),
port: 47808,
mask: "255.255.255.0".parse().unwrap(),
};
manager.add_bdt_entry(entry.clone());
assert_eq!(manager.get_bdt_entries().len(), 1);
let encoded = manager.encode_bdt();
assert_eq!(encoded.len(), 10);
let mut new_manager = BroadcastManager::new();
new_manager.decode_bdt(&encoded).unwrap();
let decoded_entries = new_manager.get_bdt_entries();
assert_eq!(decoded_entries.len(), 1);
assert_eq!(decoded_entries[0].address, entry.address);
assert_eq!(decoded_entries[0].port, entry.port);
}
#[cfg(feature = "std")]
#[test]
fn test_bacnet_ip_config_default() {
let config = BacnetIpConfig::default();
assert_eq!(config.bind_address.port(), constants::BACNET_IP_PORT);
assert!(config.broadcast_enabled);
assert!(config.foreign_device.is_none());
assert_eq!(config.buffer_size, 1500);
assert_eq!(config.read_timeout, Some(constants::DEFAULT_READ_TIMEOUT));
assert_eq!(config.write_timeout, Some(constants::DEFAULT_WRITE_TIMEOUT));
assert_eq!(config.request_timeout, constants::DEFAULT_REQUEST_TIMEOUT);
assert_eq!(
config.registration_timeout,
constants::DEFAULT_REGISTRATION_TIMEOUT
);
}
#[test]
fn test_invalid_bvll_decode() {
let short_data = [0x81, 0x0A];
assert!(BvllHeader::decode(&short_data).is_err());
let invalid_type = [0x82, 0x0A, 0x00, 0x04];
assert!(BvllHeader::decode(&invalid_type).is_err());
let invalid_function = [0x81, 0xFF, 0x00, 0x04];
assert!(BvllHeader::decode(&invalid_function).is_err());
}
#[cfg(feature = "std")]
#[test]
fn test_timeout_tracking() {
let config = BacnetIpConfig::default();
let mut transport = BacnetIpTransport::new(config).unwrap();
let target = "127.0.0.1:47808".parse().unwrap();
let data = &[0x01, 0x02, 0x03];
let invoke_id1 = transport
.send_confirmed_request(target, data, None)
.unwrap();
let invoke_id2 = transport
.send_confirmed_request(target, data, None)
.unwrap();
assert_ne!(invoke_id1, invoke_id2);
assert_eq!(transport.active_request_count(), 2);
let elapsed = transport.complete_request(invoke_id1);
assert!(elapsed.is_some());
assert_eq!(transport.active_request_count(), 1);
let elapsed = transport.complete_request(255);
assert!(elapsed.is_none());
}
#[test]
fn test_timeout_constants() {
assert_eq!(constants::DEFAULT_REQUEST_TIMEOUT.as_secs(), 10);
assert_eq!(constants::DEFAULT_READ_TIMEOUT.as_secs(), 5);
assert_eq!(constants::DEFAULT_WRITE_TIMEOUT.as_secs(), 5);
assert_eq!(constants::DEFAULT_REGISTRATION_TIMEOUT.as_secs(), 30);
assert_eq!(constants::MAX_CONCURRENT_REQUESTS, 255);
}
}