use bacnet_rs::{
service::{ReadPropertyMultipleRequest, ReadAccessSpecification, PropertyReference, ConfirmedServiceChoice, WhoIsRequest, IAmRequest, UnconfirmedServiceChoice},
object::{ObjectIdentifier, ObjectType},
network::Npdu,
app::{Apdu, MaxSegments, MaxApduSize},
property::decode_units,
};
use std::{
net::{SocketAddr, UdpSocket},
time::{Duration, Instant},
env,
};
#[derive(Debug)]
struct ObjectInfo {
object_identifier: ObjectIdentifier,
object_name: Option<String>,
description: Option<String>,
present_value: Option<String>,
units: Option<String>,
object_type_name: String,
}
impl ObjectInfo {
fn new(object_identifier: ObjectIdentifier) -> Self {
let object_type_name = match object_identifier.object_type {
ObjectType::Device => "Device",
ObjectType::AnalogInput => "Analog Input",
ObjectType::AnalogOutput => "Analog Output",
ObjectType::AnalogValue => "Analog Value",
ObjectType::BinaryInput => "Binary Input",
ObjectType::BinaryOutput => "Binary Output",
ObjectType::BinaryValue => "Binary Value",
ObjectType::MultiStateInput => "Multi-State Input",
ObjectType::MultiStateOutput => "Multi-State Output",
ObjectType::MultiStateValue => "Multi-State Value",
ObjectType::Calendar => "Calendar",
ObjectType::Command => "Command",
ObjectType::EventEnrollment => "Event Enrollment",
ObjectType::File => "File",
ObjectType::Group => "Group",
ObjectType::Loop => "Loop",
ObjectType::NotificationClass => "Notification Class",
ObjectType::Program => "Program",
ObjectType::Schedule => "Schedule",
ObjectType::Averaging => "Averaging",
ObjectType::TrendLog => "Trend Log",
ObjectType::LifeSafetyPoint => "Life Safety Point",
ObjectType::LifeSafetyZone => "Life Safety Zone",
ObjectType::Accumulator => "Accumulator",
ObjectType::PulseConverter => "Pulse Converter",
ObjectType::EventLog => "Event Log",
ObjectType::GlobalGroup => "Global Group",
ObjectType::TrendLogMultiple => "Trend Log Multiple",
ObjectType::LoadControl => "Load Control",
ObjectType::StructuredView => "Structured View",
ObjectType::AccessDoor => "Access Door",
}.to_string();
Self {
object_identifier,
object_name: None,
description: None,
present_value: None,
units: None,
object_type_name,
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
eprintln!("Usage: {} <target_device_ip>", args[0]);
eprintln!("Example: {} 10.161.1.211", args[0]);
std::process::exit(1);
}
let target_ip = &args[1];
let target_addr: SocketAddr = format!("{}:47808", target_ip).parse()?;
println!("BACnet Device Objects Discovery");
println!("==============================\n");
println!("Target device: {}", target_addr);
let local_addr = "0.0.0.0:0"; let socket = UdpSocket::bind(local_addr)?;
socket.set_read_timeout(Some(Duration::from_secs(5)))?;
println!("Connected from: {}", socket.local_addr()?);
println!();
println!("Step 0: Discovering device ID...");
let device_id = discover_device_id(&socket, target_addr)?;
println!("Found device ID: {}", device_id);
println!();
println!("Step 1: Reading device object list...");
let device_objects = read_device_object_list(&socket, target_addr, device_id)?;
if device_objects.is_empty() {
println!("No objects found in device object list!");
return Ok(());
}
println!("Found {} objects in device", device_objects.len());
println!();
println!("Step 2: Reading object properties...");
let objects_info = read_objects_properties(&socket, target_addr, &device_objects)?;
println!();
println!("Device Objects Summary");
println!("=====================");
let mut objects_by_type: std::collections::HashMap<String, Vec<&ObjectInfo>> = std::collections::HashMap::new();
for obj in &objects_info {
objects_by_type.entry(obj.object_type_name.clone()).or_default().push(obj);
}
for (object_type, objects) in objects_by_type {
println!("\n{} Objects ({}):", object_type, objects.len());
println!("{}", "-".repeat(object_type.len() + 15));
for obj in objects {
println!(" {} Instance {}", obj.object_type_name, obj.object_identifier.instance);
if let Some(name) = &obj.object_name {
println!(" Object Name: {}", name);
} else {
println!(" Object Name: [Not Available]");
}
if let Some(desc) = &obj.description {
println!(" Description: {}", desc);
} else {
println!(" Description: [Not Available]");
}
match obj.object_identifier.object_type {
ObjectType::AnalogInput | ObjectType::AnalogOutput | ObjectType::AnalogValue |
ObjectType::BinaryInput | ObjectType::BinaryOutput | ObjectType::BinaryValue |
ObjectType::MultiStateInput | ObjectType::MultiStateOutput | ObjectType::MultiStateValue => {
if let Some(value) = &obj.present_value {
println!(" Present Value: {}", value);
} else {
println!(" Present Value: [Not Available]");
}
}
_ => {}
}
match obj.object_identifier.object_type {
ObjectType::AnalogInput | ObjectType::AnalogOutput | ObjectType::AnalogValue => {
if let Some(units) = &obj.units {
println!(" Units: {}", units);
} else {
println!(" Units: [Not Available]");
}
}
_ => {}
}
println!(" Object Type: {}", obj.object_type_name);
println!();
}
}
println!("Total objects discovered: {}", objects_info.len());
Ok(())
}
fn discover_device_id(socket: &UdpSocket, target_addr: SocketAddr) -> Result<u32, Box<dyn std::error::Error>> {
let whois = WhoIsRequest::new();
let mut buffer = Vec::new();
whois.encode(&mut buffer)?;
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(UnconfirmedServiceChoice::WhoIs as u8);
apdu.extend_from_slice(&buffer);
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;
socket.send_to(&bvlc_message, target_addr)?;
let mut recv_buffer = [0u8; 1500];
let start_time = Instant::now();
while start_time.elapsed() < Duration::from_secs(3) {
match socket.recv_from(&mut recv_buffer) {
Ok((len, source)) => {
if source == target_addr {
if let Some(device_id) = process_iam_response(&recv_buffer[..len]) {
return Ok(device_id);
}
}
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
continue;
}
Err(_) => {
continue;
}
}
}
Err("Failed to discover device ID - no I-Am response received".into())
}
fn process_iam_response(data: &[u8]) -> Option<u32> {
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;
if data.len() <= npdu_start {
return None;
}
let (_npdu, npdu_len) = Npdu::decode(&data[npdu_start..]).ok()?;
let apdu_start = npdu_start + npdu_len;
if data.len() <= apdu_start {
return None;
}
let apdu = &data[apdu_start..];
if apdu.len() < 2 || apdu[0] != 0x10 {
return None;
}
let service_choice = apdu[1];
if service_choice != UnconfirmedServiceChoice::IAm as u8 {
return None;
}
if apdu.len() <= 2 {
return None;
}
match IAmRequest::decode(&apdu[2..]) {
Ok(iam) => Some(iam.device_identifier.instance),
Err(_) => None,
}
}
fn read_device_object_list(socket: &UdpSocket, 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(76); 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 = send_confirmed_request(socket, target_addr, invoke_id, ConfirmedServiceChoice::ReadPropertyMultiple as u8, &encode_rpm_request(&rpm_request)?)?;
let object_list = parse_object_list_response(&response_data)?;
println!("Device object list contains {} objects", object_list.len());
for (i, obj) in object_list.iter().enumerate() {
if i < 10 { println!(" {}: {} Instance {}", i + 1, get_object_type_name(obj.object_type), obj.instance);
} else if i == 10 {
println!(" ... and {} more objects", object_list.len() - 10);
break;
}
}
Ok(object_list)
}
fn read_objects_properties(socket: &UdpSocket, target_addr: SocketAddr, objects: &[ObjectIdentifier]) -> Result<Vec<ObjectInfo>, Box<dyn std::error::Error>> {
let mut objects_info = Vec::new();
let batch_size = 5;
for chunk in objects.chunks(batch_size) {
println!("Reading properties for {} objects...", chunk.len());
let mut read_specs = Vec::new();
for obj in chunk {
let mut property_refs = Vec::new();
property_refs.push(PropertyReference::new(77)); property_refs.push(PropertyReference::new(28));
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(85)); }
_ => {}
}
match obj.object_type {
ObjectType::AnalogInput | ObjectType::AnalogOutput | ObjectType::AnalogValue => {
property_refs.push(PropertyReference::new(117)); }
_ => {}
}
read_specs.push(ReadAccessSpecification::new(*obj, property_refs));
}
let rpm_request = ReadPropertyMultipleRequest::new(read_specs);
let invoke_id = (objects_info.len() / batch_size + 2) as u8; match send_confirmed_request(socket, target_addr, invoke_id, ConfirmedServiceChoice::ReadPropertyMultiple as u8, &encode_rpm_request(&rpm_request)?) {
Ok(response_data) => {
match parse_rpm_response(&response_data, chunk) {
Ok(mut batch_info) => {
objects_info.append(&mut batch_info);
println!(" Successfully read properties for {} objects", chunk.len());
}
Err(e) => {
println!(" Warning: Failed to parse response for batch: {}", e);
for obj in chunk {
objects_info.push(ObjectInfo::new(*obj));
}
}
}
}
Err(e) => {
println!(" Warning: Failed to read properties for batch: {}", e);
for obj in chunk {
objects_info.push(ObjectInfo::new(*obj));
}
}
}
std::thread::sleep(Duration::from_millis(100));
}
Ok(objects_info)
}
fn send_confirmed_request(socket: &UdpSocket, target_addr: SocketAddr, invoke_id: u8, service_choice: u8, 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;
socket.send_to(&bvlc_message, target_addr)?;
let mut recv_buffer = [0u8; 1500];
let start_time = Instant::now();
while start_time.elapsed() < Duration::from_secs(5) {
match socket.recv_from(&mut recv_buffer) {
Ok((len, source)) => {
if source == target_addr {
if let Some(response_data) = 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(format!("Error receiving response: {}", e).into());
}
}
}
Err("Timeout waiting for response".into())
}
fn process_confirmed_response(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;
if data.len() <= npdu_start {
return None;
}
let (_npdu, npdu_len) = Npdu::decode(&data[npdu_start..]).ok()?;
let apdu_start = npdu_start + npdu_len;
if data.len() <= apdu_start {
return None;
}
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
}
}
Apdu::Error { invoke_id, error_class, error_code, .. } => {
if invoke_id == expected_invoke_id {
println!(" Error response: class={}, code={}", error_class, error_code);
}
None
}
_ => None,
}
}
fn encode_rpm_request(request: &ReadPropertyMultipleRequest) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut buffer = Vec::new();
for spec in &request.read_access_specifications {
let object_id = encode_object_id(spec.object_identifier.object_type as u16, spec.object_identifier.instance);
buffer.push(0x0C); buffer.extend_from_slice(&object_id.to_be_bytes());
buffer.push(0x1E);
for prop_ref in &spec.property_references {
buffer.push(0x09); buffer.push(prop_ref.property_identifier as u8);
if let Some(array_index) = prop_ref.property_array_index {
buffer.push(0x19); buffer.push(array_index as u8);
}
}
buffer.push(0x1F); }
Ok(buffer)
}
fn parse_object_list_response(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 (obj_type, instance) = decode_object_id(obj_id);
if obj_type == 8 {
pos += 4;
continue;
}
if let Ok(object_type) = ObjectType::try_from(obj_type) {
objects.push(ObjectIdentifier::new(object_type, instance));
}
pos += 4;
} else {
pos += 1;
}
}
Ok(objects)
}
fn parse_rpm_response(data: &[u8], objects: &[ObjectIdentifier]) -> Result<Vec<ObjectInfo>, Box<dyn std::error::Error>> {
let mut objects_info = Vec::new();
for obj in objects {
objects_info.push(ObjectInfo::new(*obj));
}
let mut pos = 0;
let mut current_obj_index = 0;
while pos < data.len() && current_obj_index < objects_info.len() {
while pos < data.len() && data[pos] != 0x0C {
pos += 1;
}
if pos >= data.len() {
break;
}
pos += 5;
while pos < data.len() && data[pos] != 0x1E {
pos += 1;
}
if pos >= data.len() {
break;
}
pos += 1;
if pos < data.len() && data[pos] == 0x29 { pos += 1;
}
if pos < data.len() && data[pos] == 0x4D { pos += 1;
}
if pos < data.len() && data[pos] == 0x4E { pos += 1;
}
if pos < data.len() && data[pos] == 0x75 { if let Some((name, consumed)) = extract_character_string(&data[pos..]) {
objects_info[current_obj_index].object_name = Some(name);
pos += consumed;
}
}
while pos < data.len() && data[pos] != 0x44 && data[pos] != 0x11 && data[pos] != 0x1F {
pos += 1;
}
match objects_info[current_obj_index].object_identifier.object_type {
ObjectType::AnalogInput | ObjectType::AnalogOutput | ObjectType::AnalogValue => {
if pos < data.len() && data[pos] == 0x44 { if let Some((value, consumed)) = extract_present_value(&data[pos..], objects_info[current_obj_index].object_identifier.object_type) {
objects_info[current_obj_index].present_value = Some(value);
pos += consumed;
}
}
}
ObjectType::BinaryInput | ObjectType::BinaryOutput | ObjectType::BinaryValue => {
if pos < data.len() && data[pos] == 0x11 { if let Some((value, consumed)) = extract_present_value(&data[pos..], objects_info[current_obj_index].object_identifier.object_type) {
objects_info[current_obj_index].present_value = Some(value);
pos += consumed;
}
}
}
_ => {}
}
while pos < data.len() && data[pos] != 0x91 && data[pos] != 0x1F {
pos += 1;
}
if pos < data.len() && data[pos] == 0x91 { if let Some((units, consumed)) = extract_units(&data[pos..]) {
objects_info[current_obj_index].units = Some(units);
pos += consumed;
}
}
while pos < data.len() && data[pos] != 0x1F {
pos += 1;
}
if pos < data.len() && data[pos] == 0x1F {
pos += 1; }
current_obj_index += 1;
}
Ok(objects_info)
}
fn extract_character_string(data: &[u8]) -> Option<(String, usize)> {
if data.len() < 2 || data[0] != 0x75 { return None;
}
let length = data[1] as usize;
if data.len() < 2 + length || length == 0 {
return None;
}
let encoding = data[2];
let string_data = &data[3..2 + length];
let string = match encoding {
0 => {
String::from_utf8_lossy(string_data).to_string()
}
4 => {
if string_data.len() % 2 != 0 {
return None; }
let mut utf16_chars = Vec::new();
for chunk in string_data.chunks_exact(2) {
let char_code = u16::from_be_bytes([chunk[0], chunk[1]]);
utf16_chars.push(char_code);
}
String::from_utf16_lossy(&utf16_chars)
}
_ => {
String::from_utf8_lossy(string_data).to_string()
}
};
Some((string, 2 + length))
}
fn extract_present_value(data: &[u8], object_type: ObjectType) -> Option<(String, usize)> {
if data.is_empty() {
return None;
}
match object_type {
ObjectType::AnalogInput | ObjectType::AnalogOutput | ObjectType::AnalogValue => {
if data.len() >= 5 && data[0] == 0x44 { let bytes = [data[1], data[2], data[3], data[4]];
let value = f32::from_be_bytes(bytes);
Some((format!("{:.2}", value), 5))
} else {
None
}
}
ObjectType::BinaryInput | ObjectType::BinaryOutput | ObjectType::BinaryValue => {
if data.len() >= 2 && data[0] == 0x11 { let value = data[1] != 0;
Some((if value { "Active".to_string() } else { "Inactive".to_string() }, 2))
} else {
None
}
}
ObjectType::MultiStateInput | ObjectType::MultiStateOutput | ObjectType::MultiStateValue => {
if data.len() >= 2 && data[0] == 0x21 { let value = data[1];
Some((format!("State {}", value), 2))
} else {
None
}
}
_ => Some(("N/A".to_string(), 1))
}
}
fn extract_units(data: &[u8]) -> Option<(String, usize)> {
decode_units(data)
}
fn encode_object_id(object_type: u16, instance: u32) -> u32 {
((object_type as u32) << 22) | (instance & 0x3FFFFF)
}
fn decode_object_id(encoded: u32) -> (u16, u32) {
let object_type = ((encoded >> 22) & 0x3FF) as u16;
let instance = encoded & 0x3FFFFF;
(object_type, instance)
}
fn get_object_type_name(object_type: ObjectType) -> &'static str {
match object_type {
ObjectType::Device => "Device",
ObjectType::AnalogInput => "Analog Input",
ObjectType::AnalogOutput => "Analog Output",
ObjectType::AnalogValue => "Analog Value",
ObjectType::BinaryInput => "Binary Input",
ObjectType::BinaryOutput => "Binary Output",
ObjectType::BinaryValue => "Binary Value",
ObjectType::MultiStateInput => "Multi-State Input",
ObjectType::MultiStateOutput => "Multi-State Output",
ObjectType::MultiStateValue => "Multi-State Value",
ObjectType::Calendar => "Calendar",
ObjectType::Command => "Command",
ObjectType::EventEnrollment => "Event Enrollment",
ObjectType::File => "File",
ObjectType::Group => "Group",
ObjectType::Loop => "Loop",
ObjectType::NotificationClass => "Notification Class",
ObjectType::Program => "Program",
ObjectType::Schedule => "Schedule",
ObjectType::Averaging => "Averaging",
ObjectType::TrendLog => "Trend Log",
ObjectType::LifeSafetyPoint => "Life Safety Point",
ObjectType::LifeSafetyZone => "Life Safety Zone",
ObjectType::Accumulator => "Accumulator",
ObjectType::PulseConverter => "Pulse Converter",
ObjectType::EventLog => "Event Log",
ObjectType::GlobalGroup => "Global Group",
ObjectType::TrendLogMultiple => "Trend Log Multiple",
ObjectType::LoadControl => "Load Control",
ObjectType::StructuredView => "Structured View",
ObjectType::AccessDoor => "Access Door",
}
}