#[cfg(feature = "std")]
use std::{
net::{SocketAddr, UdpSocket},
time::{Duration, Instant},
};
#[cfg(not(feature = "std"))]
use alloc::{collections::BTreeMap as HashMap, string::String, vec::Vec};
use crate::{
app::{Apdu, MaxApduSize, MaxSegments},
network::Npdu,
object::{EngineeringUnits, ObjectIdentifier, ObjectType, PropertyIdentifier, Segmentation},
property::PropertyValue,
service::{
ConfirmedServiceChoice, IAmRequest, PropertyReference, ReadAccessSpecification,
ReadPropertyMultipleRequest, UnconfirmedServiceChoice, WhoIsRequest,
},
};
#[cfg(feature = "std")]
pub struct BacnetClient {
socket: UdpSocket,
timeout: Duration,
}
#[derive(Debug, Clone)]
pub struct DeviceInfo {
pub device_id: u32,
pub address: SocketAddr,
pub vendor_id: u16,
pub vendor_name: String,
pub max_apdu: u32,
pub segmentation: Segmentation,
}
#[derive(Debug, Clone)]
pub struct ObjectInfo {
pub object_identifier: ObjectIdentifier,
pub object_name: Option<String>,
pub description: Option<String>,
pub present_value: Option<PropertyValue>,
pub units: Option<EngineeringUnits>,
pub status_flags: Option<Vec<bool>>,
}
#[cfg(feature = "std")]
impl BacnetClient {
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
let socket = UdpSocket::bind("0.0.0.0:0")?;
socket.set_read_timeout(Some(Duration::from_secs(5)))?;
Ok(Self {
socket,
timeout: Duration::from_secs(5),
})
}
pub fn discover_device(
&self,
target_addr: SocketAddr,
) -> Result<DeviceInfo, Box<dyn std::error::Error>> {
let whois = WhoIsRequest::new();
let mut buffer = Vec::new();
whois.encode(&mut buffer)?;
let message =
self.create_unconfirmed_message(UnconfirmedServiceChoice::WhoIs as u8, &buffer);
self.socket.send_to(&message, target_addr)?;
let mut recv_buffer = [0u8; 1500];
let start_time = Instant::now();
while start_time.elapsed() < self.timeout {
match self.socket.recv_from(&mut recv_buffer) {
Ok((len, source)) => {
if source == target_addr {
if let Some(device_info) =
self.parse_iam_response(&recv_buffer[..len], source)
{
return Ok(device_info);
}
}
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
Err(e) => return Err(e.into()),
}
}
Err("Device discovery timeout".into())
}
pub fn read_object_list(
&self,
target_addr: SocketAddr,
device_id: u32,
) -> Result<Vec<ObjectIdentifier>, Box<dyn std::error::Error>> {
let device_object = ObjectIdentifier::new(ObjectType::Device, device_id);
let property_ref = PropertyReference::new(PropertyIdentifier::ObjectList); let read_spec = ReadAccessSpecification::new(device_object, vec![property_ref]);
let rpm_request = ReadPropertyMultipleRequest::new(vec![read_spec]);
let invoke_id = 1;
let response_data = self.send_confirmed_request(
target_addr,
invoke_id,
ConfirmedServiceChoice::ReadPropertyMultiple,
&self.encode_rpm_request(&rpm_request)?,
)?;
self.parse_object_list_response(&response_data)
}
pub fn read_objects_properties(
&self,
target_addr: SocketAddr,
objects: &[ObjectIdentifier],
) -> Result<Vec<ObjectInfo>, Box<dyn std::error::Error>> {
let mut objects_info = Vec::new();
let batch_size = 5;
for (batch_idx, chunk) in objects.chunks(batch_size).enumerate() {
let mut read_specs = Vec::new();
for obj in chunk {
let mut property_refs = Vec::new();
property_refs.push(PropertyReference::new(PropertyIdentifier::ObjectName)); property_refs.push(PropertyReference::new(PropertyIdentifier::Description));
match obj.object_type {
ObjectType::AnalogInput
| ObjectType::AnalogOutput
| ObjectType::AnalogValue
| ObjectType::BinaryInput
| ObjectType::BinaryOutput
| ObjectType::BinaryValue
| ObjectType::MultiStateInput
| ObjectType::MultiStateOutput
| ObjectType::MultiStateValue => {
property_refs
.push(PropertyReference::new(PropertyIdentifier::PresentValue)); property_refs.push(PropertyReference::new(PropertyIdentifier::StatusFlags));
}
_ => {}
}
match obj.object_type {
ObjectType::AnalogInput
| ObjectType::AnalogOutput
| ObjectType::AnalogValue => {
property_refs.push(PropertyReference::new(PropertyIdentifier::Units));
}
_ => {}
}
read_specs.push(ReadAccessSpecification::new(*obj, property_refs));
}
let rpm_request = ReadPropertyMultipleRequest::new(read_specs);
let invoke_id = (batch_idx + 2) as u8;
match self.send_confirmed_request(
target_addr,
invoke_id,
ConfirmedServiceChoice::ReadPropertyMultiple,
&self.encode_rpm_request(&rpm_request)?,
) {
Ok(response_data) => {
match self.parse_rpm_response(&response_data, chunk) {
Ok(mut batch_info) => objects_info.append(&mut batch_info),
Err(_) => {
for obj in chunk {
objects_info.push(ObjectInfo {
object_identifier: *obj,
object_name: None,
description: None,
present_value: None,
units: None,
status_flags: None,
});
}
}
}
}
Err(_) => {
for obj in chunk {
objects_info.push(ObjectInfo {
object_identifier: *obj,
object_name: None,
description: None,
present_value: None,
units: None,
status_flags: None,
});
}
}
}
std::thread::sleep(Duration::from_millis(100));
}
Ok(objects_info)
}
fn create_unconfirmed_message(&self, service_choice: u8, service_data: &[u8]) -> Vec<u8> {
let mut npdu = Npdu::new();
npdu.control.expecting_reply = false;
npdu.control.priority = 0;
let npdu_buffer = npdu.encode();
let mut apdu = vec![0x10]; apdu.push(service_choice);
apdu.extend_from_slice(service_data);
let mut message = npdu_buffer;
message.extend_from_slice(&apdu);
let mut bvlc_message = vec![0x81, 0x0A, 0x00, 0x00];
bvlc_message.extend_from_slice(&message);
let total_len = bvlc_message.len() as u16;
bvlc_message[2] = (total_len >> 8) as u8;
bvlc_message[3] = (total_len & 0xFF) as u8;
bvlc_message
}
fn send_confirmed_request(
&self,
target_addr: SocketAddr,
invoke_id: u8,
service_choice: ConfirmedServiceChoice,
service_data: &[u8],
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let apdu = Apdu::ConfirmedRequest {
segmented: false,
more_follows: false,
segmented_response_accepted: true,
max_segments: MaxSegments::Unspecified,
max_response_size: MaxApduSize::Up1476,
invoke_id,
sequence_number: None,
proposed_window_size: None,
service_choice,
service_data: service_data.to_vec(),
};
let apdu_data = apdu.encode();
let mut npdu = Npdu::new();
npdu.control.expecting_reply = true;
npdu.control.priority = 0;
let npdu_data = npdu.encode();
let mut message = npdu_data;
message.extend_from_slice(&apdu_data);
let mut bvlc_message = vec![0x81, 0x0A, 0x00, 0x00];
bvlc_message.extend_from_slice(&message);
let total_len = bvlc_message.len() as u16;
bvlc_message[2] = (total_len >> 8) as u8;
bvlc_message[3] = (total_len & 0xFF) as u8;
self.socket.send_to(&bvlc_message, target_addr)?;
let mut recv_buffer = [0u8; 1500];
let start_time = Instant::now();
while start_time.elapsed() < self.timeout {
match self.socket.recv_from(&mut recv_buffer) {
Ok((len, source)) => {
if source == target_addr {
if let Some(response_data) =
self.process_confirmed_response(&recv_buffer[..len], invoke_id)
{
return Ok(response_data);
}
}
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
Err(e) => return Err(e.into()),
}
}
Err("Request timeout".into())
}
fn parse_iam_response(&self, data: &[u8], source: SocketAddr) -> Option<DeviceInfo> {
if data.len() < 4 || data[0] != 0x81 {
return None;
}
let bvlc_length = ((data[2] as u16) << 8) | (data[3] as u16);
if data.len() != bvlc_length as usize {
return None;
}
let npdu_start = 4;
let (_npdu, npdu_len) = Npdu::decode(&data[npdu_start..]).ok()?;
let apdu_start = npdu_start + npdu_len;
let apdu = &data[apdu_start..];
if apdu.len() < 2 || apdu[0] != 0x10 || apdu[1] != UnconfirmedServiceChoice::IAm as u8 {
return None;
}
match IAmRequest::decode(&apdu[2..]) {
Ok(iam) => {
let vendor_name = crate::vendor::get_vendor_name(iam.vendor_identifier)
.unwrap_or("Unknown Vendor")
.to_string();
Some(DeviceInfo {
device_id: iam.device_identifier.instance,
address: source,
vendor_id: iam.vendor_identifier,
vendor_name,
max_apdu: iam.max_apdu_length_accepted,
segmentation: iam.segmentation_supported,
})
}
Err(_) => None,
}
}
fn process_confirmed_response(&self, data: &[u8], expected_invoke_id: u8) -> Option<Vec<u8>> {
if data.len() < 4 || data[0] != 0x81 {
return None;
}
let bvlc_length = ((data[2] as u16) << 8) | (data[3] as u16);
if data.len() != bvlc_length as usize {
return None;
}
let npdu_start = 4;
let (_npdu, npdu_len) = Npdu::decode(&data[npdu_start..]).ok()?;
let apdu_start = npdu_start + npdu_len;
let apdu = Apdu::decode(&data[apdu_start..]).ok()?;
match apdu {
Apdu::ComplexAck {
invoke_id,
service_data,
..
} => {
if invoke_id == expected_invoke_id {
Some(service_data)
} else {
None
}
}
_ => None,
}
}
fn encode_rpm_request(
&self,
request: &ReadPropertyMultipleRequest,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut buffer = Vec::new();
request.encode(&mut buffer)?;
Ok(buffer)
}
fn parse_object_list_response(
&self,
data: &[u8],
) -> Result<Vec<ObjectIdentifier>, Box<dyn std::error::Error>> {
let mut objects = Vec::new();
let mut pos = 0;
while pos + 5 <= data.len() {
if data[pos] == 0xC4 {
pos += 1;
let obj_id_bytes = [data[pos], data[pos + 1], data[pos + 2], data[pos + 3]];
let obj_id = u32::from_be_bytes(obj_id_bytes);
let identifier: ObjectIdentifier = obj_id.into();
if identifier.object_type != ObjectType::Device {
objects.push(identifier);
}
pos += 4;
} else {
pos += 1;
}
}
Ok(objects)
}
fn parse_rpm_response(
&self,
data: &[u8],
objects: &[ObjectIdentifier],
) -> Result<Vec<ObjectInfo>, Box<dyn std::error::Error>> {
let mut objects_info = Vec::new();
for obj in objects {
let mut object_info = ObjectInfo {
object_identifier: *obj,
object_name: None,
description: None,
present_value: None,
units: None,
status_flags: None,
};
if let Some(PropertyValue::CharacterString(s)) = extract_property_value(data, 77) {
object_info.object_name = Some(s);
}
if let Some(PropertyValue::CharacterString(s)) = extract_property_value(data, 28) {
object_info.description = Some(s);
}
if let Some(value) = extract_property_value(data, 85) {
object_info.present_value = Some(value);
}
objects_info.push(object_info);
}
Ok(objects_info)
}
}
fn extract_property_value(_data: &[u8], _property_id: u32) -> Option<PropertyValue> {
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_object_id_encoding() {
let object_id = ObjectIdentifier::new(ObjectType::AnalogInput, 123);
let encoded: u32 = match object_id.try_into() {
Ok(value) => value,
Err(_) => panic!("Object identifier encoding failed"),
};
let decoded = ObjectIdentifier::from(encoded);
assert_eq!(decoded.object_type, ObjectType::AnalogInput);
assert_eq!(decoded.instance, 123);
let object_id = ObjectIdentifier::new(ObjectType::Device, 5047);
let encoded: u32 = match object_id.try_into() {
Ok(value) => value,
Err(_) => panic!("Object identifier encoding failed"),
};
let decoded = ObjectIdentifier::from(encoded);
assert_eq!(decoded.object_type, ObjectType::Device);
assert_eq!(decoded.instance, 5047);
}
}