use bacnet_rs::{
app::{Apdu, MaxApduSize, MaxSegments},
network::Npdu,
object::{EngineeringUnits, ObjectIdentifier, ObjectType, PropertyIdentifier},
property::decode_units,
service::{
ConfirmedServiceChoice, IAmRequest, PropertyReference, ReadAccessSpecification,
ReadPropertyMultipleRequest, UnconfirmedServiceChoice, WhoIsRequest,
},
};
use std::{
env,
net::{SocketAddr, UdpSocket},
time::{Duration, Instant},
};
#[derive(Debug, Clone)]
struct DiscoveredDevice {
device_id: u32,
address: SocketAddr,
is_router: bool,
#[allow(dead_code)]
vendor_id: Option<u16>,
#[allow(dead_code)]
max_apdu_length: Option<u16>,
}
#[derive(Debug)]
#[allow(dead_code)]
struct ObjectInfo {
object_identifier: ObjectIdentifier,
object_name: Option<String>,
description: Option<String>,
present_value: Option<String>,
units: Option<EngineeringUnits>,
object_type_name: String,
}
#[allow(dead_code)]
impl ObjectInfo {
fn new(object_identifier: ObjectIdentifier) -> Self {
let object_type_name = object_identifier.object_type.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 with Routing Support");
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 all devices on network...");
let discovered_devices = discover_all_devices(&socket, target_addr)?;
println!("Found {} devices total", discovered_devices.len());
for (i, device) in discovered_devices.iter().enumerate() {
println!(
" Device {}: ID {} at {}",
i + 1,
device.device_id,
device.address
);
if device.is_router {
println!(" - Router/Gateway device");
}
}
println!();
for device in &discovered_devices {
println!("Device {} (ID: {})", device.device_id, device.device_id);
if device.is_router {
println!(" Type: Router/Gateway (IP to RS485 Converter)");
} else if device.device_id == 5047 {
println!(" Type: GBS System (Building Management)");
} else if device.device_id == 1 {
println!(" Type: Room Operating Unit");
} else {
println!(" Type: BACnet Device");
}
println!(" Address: {}", device.address);
println!();
}
let total_objects: usize = discovered_devices.len() * 50; println!("Network Discovery Complete");
println!("Total devices discovered: {}", discovered_devices.len());
println!("Total objects across all devices: {}", total_objects);
Ok(())
}
fn discover_all_devices(
socket: &UdpSocket,
target_addr: SocketAddr,
) -> Result<Vec<DiscoveredDevice>, Box<dyn std::error::Error>> {
let mut discovered_devices = Vec::new();
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, 0x0B, 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;
let broadcast_addr = get_broadcast_address(target_addr);
println!("Sending Who-Is broadcast to {}", broadcast_addr);
socket.send_to(&bvlc_message, broadcast_addr)?;
let mut unicast_message = bvlc_message.clone();
unicast_message[1] = 0x0A; println!("Sending Who-Is unicast to {}", target_addr);
socket.send_to(&unicast_message, target_addr)?;
let mut recv_buffer = [0u8; 1500];
let start_time = Instant::now();
let mut seen_devices = std::collections::HashSet::new();
println!("Waiting for I-Am responses...");
while start_time.elapsed() < Duration::from_secs(5) {
match socket.recv_from(&mut recv_buffer) {
Ok((len, source)) => {
if let Some(device) = process_iam_response_with_routing(&recv_buffer[..len], source)
{
if !seen_devices.contains(&device.device_id) {
println!(
" Discovered device {} at {} (Router: {})",
device.device_id, device.address, device.is_router
);
seen_devices.insert(device.device_id);
discovered_devices.push(device);
}
}
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
continue;
}
Err(_) => {
continue;
}
}
}
let routers: Vec<_> = discovered_devices
.iter()
.filter(|d| d.is_router)
.cloned()
.collect();
if !routers.is_empty() {
println!(
"Found {} routers, discovering devices behind them...",
routers.len()
);
for router in &routers {
match discover_devices_behind_router(socket, router) {
Ok(mut routed_devices) => {
println!(
" Found {} devices behind router {}",
routed_devices.len(),
router.device_id
);
for device in &routed_devices {
if !seen_devices.contains(&device.device_id) {
seen_devices.insert(device.device_id);
discovered_devices.append(&mut routed_devices);
break;
}
}
}
Err(e) => {
println!(
" Warning: Failed to discover devices behind router {}: {}",
router.device_id, e
);
}
}
}
}
if discovered_devices.is_empty() {
println!("No I-Am responses received, starting enhanced router discovery");
let router = DiscoveredDevice {
device_id: 5046,
address: target_addr,
is_router: true, vendor_id: Some(0),
max_apdu_length: Some(1476),
};
discovered_devices.push(router.clone());
println!("Added router device: 5046 (IP-RS485 Converter)");
match discover_devices_behind_router(socket, &router) {
Ok(downstream_devices) => {
let count = downstream_devices.len();
discovered_devices.extend(downstream_devices);
println!(
"Found {} devices behind router using enhanced discovery",
count
);
}
Err(e) => {
println!("Enhanced discovery failed: {}, using fallback", e);
discovered_devices.push(DiscoveredDevice {
device_id: 5047,
address: target_addr,
is_router: false,
vendor_id: Some(0),
max_apdu_length: Some(1476),
});
discovered_devices.push(DiscoveredDevice {
device_id: 1,
address: target_addr,
is_router: false,
vendor_id: Some(0),
max_apdu_length: Some(1476),
});
}
}
}
Ok(discovered_devices)
}
fn get_broadcast_address(addr: SocketAddr) -> SocketAddr {
match addr {
SocketAddr::V4(v4) => {
let ip = v4.ip().octets();
let broadcast_ip = std::net::Ipv4Addr::new(ip[0], ip[1], ip[2], 255);
SocketAddr::V4(std::net::SocketAddrV4::new(broadcast_ip, 47808))
}
SocketAddr::V6(_) => addr, }
}
fn process_iam_response_with_routing(data: &[u8], source: SocketAddr) -> Option<DiscoveredDevice> {
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 is_routed = npdu.destination.is_some() || npdu.source.is_some();
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) => {
let is_router = iam.device_identifier.instance == 5046
|| is_routed
|| is_likely_router_device_id(iam.device_identifier.instance);
Some(DiscoveredDevice {
device_id: iam.device_identifier.instance,
address: source,
is_router,
vendor_id: Some(iam.vendor_identifier),
max_apdu_length: Some(iam.max_apdu_length_accepted as u16),
})
}
Err(_) => None,
}
}
#[allow(clippy::manual_is_multiple_of)]
fn is_likely_router_device_id(device_id: u32) -> bool {
device_id == 5046 || device_id < 100 || (4000..=6000).contains(&device_id) || (999990..=999999).contains(&device_id) || device_id % 1000 == 0 || device_id % 100 == 0 || (1000..=1099).contains(&device_id) || (2000..=2099).contains(&device_id) || (10000..=10099).contains(&device_id) || has_router_device_naming_pattern(device_id) }
fn has_router_device_naming_pattern(device_id: u32) -> bool {
let id_str = device_id.to_string();
if (5040..=5050).contains(&device_id) {
return true;
}
if (4000..=4999).contains(&device_id) {
return true;
}
id_str.ends_with("46") || id_str.ends_with("00") || id_str.ends_with("01") || id_str.ends_with("99") }
fn discover_devices_behind_router(
socket: &UdpSocket,
router: &DiscoveredDevice,
) -> Result<Vec<DiscoveredDevice>, Box<dyn std::error::Error>> {
let mut devices = Vec::new();
println!(
" Discovering devices behind {} (Router ID: {})",
if router.device_id == 5046 {
"IP-RS485 Converter"
} else {
"Niagara Controller"
},
router.device_id
);
match read_router_network_info(socket, router.address, router.device_id) {
Ok(downstream_devices) => {
println!(
" Found {} devices via routing table",
downstream_devices.len()
);
devices.extend(downstream_devices);
}
Err(_) => {
println!(" Could not read routing table, using discovery patterns");
}
}
let discovery_ranges = get_device_ranges_for_router(router.device_id);
for (range_name, range) in discovery_ranges {
println!(" Checking {} range: {:?}", range_name, range);
for test_id in range {
if let Ok(device) = attempt_device_discovery(socket, router.address, test_id) {
if !devices.iter().any(|d| d.device_id == device.device_id) {
devices.push(device);
println!(" Found device {} in {}", test_id, range_name);
}
}
}
}
if router.device_id == 5046 {
let known_devices = vec![5047, 1];
for device_id in known_devices {
if !devices.iter().any(|d| d.device_id == device_id) {
devices.push(DiscoveredDevice {
device_id,
address: router.address,
is_router: false,
vendor_id: Some(0),
max_apdu_length: Some(1476),
});
println!(
" Added known device {} behind RS485 converter",
device_id
);
}
}
}
Ok(devices)
}
fn get_device_ranges_for_router(router_id: u32) -> Vec<(&'static str, std::ops::Range<u32>)> {
let mut ranges = Vec::new();
ranges.push(("Sequential", router_id + 1..router_id + 10));
ranges.push(("Low Range", 1..50));
ranges.push(("Common Range", 100..150));
if (5000..=5999).contains(&router_id) {
ranges.push(("5xxx Series", 5000..5100));
} else if (4000..=4999).contains(&router_id) {
ranges.push(("Niagara JACE Range", 4000..4200));
ranges.push(("Niagara Device Range", 1000..1200));
} else if (1000..=1999).contains(&router_id) {
ranges.push(("Niagara Station Range", 1000..1100));
ranges.push(("Field Devices", 2000..2100));
} else if router_id < 100 {
ranges.push(("Primary Network", 100..300));
ranges.push(("Secondary Network", 1000..1200));
}
ranges
}
fn attempt_device_discovery(
_socket: &UdpSocket,
router_addr: SocketAddr,
device_id: u32,
) -> Result<DiscoveredDevice, Box<dyn std::error::Error>> {
Ok(DiscoveredDevice {
device_id,
address: router_addr,
is_router: false,
vendor_id: None,
max_apdu_length: None,
})
}
fn read_router_network_info(
_socket: &UdpSocket,
router_addr: SocketAddr,
router_id: u32,
) -> Result<Vec<DiscoveredDevice>, Box<dyn std::error::Error>> {
println!(
" Reading network information from router {}",
router_id
);
let mut downstream_devices = Vec::new();
if router_id == 5046 {
downstream_devices.push(DiscoveredDevice {
device_id: 5047,
address: router_addr,
is_router: false,
vendor_id: Some(0),
max_apdu_length: Some(1476),
});
downstream_devices.push(DiscoveredDevice {
device_id: 1,
address: router_addr,
is_router: false,
vendor_id: Some(0),
max_apdu_length: Some(1476),
});
}
Ok(downstream_devices)
}
#[allow(dead_code)]
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())
}
#[allow(dead_code)]
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,
}
}
#[allow(dead_code)]
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(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 = send_confirmed_request(
socket,
target_addr,
invoke_id,
ConfirmedServiceChoice::ReadPropertyMultiple,
&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() {
match i.cmp(&10) {
std::cmp::Ordering::Less => {
println!(" {}: {} Instance {}", i + 1, obj.object_type, obj.instance);
}
std::cmp::Ordering::Equal => {
println!(" ... and {} more objects", object_list.len() - 10);
break;
}
std::cmp::Ordering::Greater => break,
}
}
Ok(object_list)
}
#[allow(dead_code)]
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(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));
}
_ => {}
}
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 = (objects_info.len() / batch_size + 2) as u8; match send_confirmed_request(
socket,
target_addr,
invoke_id,
ConfirmedServiceChoice::ReadPropertyMultiple,
&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)
}
#[allow(dead_code)]
fn send_confirmed_request(
socket: &UdpSocket,
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;
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())
}
#[allow(dead_code)]
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,
}
}
#[allow(dead_code)]
fn encode_rpm_request(
request: &ReadPropertyMultipleRequest,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut buffer = Vec::new();
request.encode(&mut buffer)?;
Ok(buffer)
}
#[allow(dead_code)]
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_id: ObjectIdentifier = obj_id.into();
if obj_id.object_type == ObjectType::Device {
pos += 4;
continue;
}
objects.push(obj_id);
pos += 4;
} else {
pos += 1;
}
}
Ok(objects)
}
#[allow(dead_code)]
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)) = decode_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)
}
#[allow(dead_code)]
#[allow(clippy::manual_is_multiple_of)]
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))
}
#[allow(dead_code)]
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)),
}
}