Skip to main content

rustbac_client/
walk.rs

1//! Device discovery walk — reads the object list and common properties for
2//! every object on a BACnet device.
3
4use crate::{BacnetClient, ClientDataValue, ClientError};
5use rustbac_core::types::{ObjectId, ObjectType, PropertyId};
6use rustbac_datalink::{DataLink, DataLinkAddress};
7
8/// Summary of a single object on a device.
9#[derive(Debug, Clone)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub struct ObjectSummary {
12    pub object_id: ObjectId,
13    pub object_name: Option<String>,
14    pub object_type: ObjectType,
15    pub present_value: Option<ClientDataValue>,
16    pub description: Option<String>,
17    pub units: Option<u32>,
18    pub status_flags: Option<ClientDataValue>,
19}
20
21/// Metadata read from the Device object during a walk.
22#[derive(Debug, Clone, Default)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub struct DeviceInfo {
25    pub vendor_name: Option<String>,
26    pub model_name: Option<String>,
27    pub firmware_revision: Option<String>,
28}
29
30/// Result of a full device walk.
31#[derive(Debug, Clone)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
33pub struct DeviceWalkResult {
34    pub device_id: ObjectId,
35    pub device_info: DeviceInfo,
36    pub objects: Vec<ObjectSummary>,
37}
38
39/// Walk a BACnet device: read its object list, then batch-read common
40/// properties for each object.
41pub async fn walk_device<D: DataLink>(
42    client: &BacnetClient<D>,
43    addr: DataLinkAddress,
44    device_id: ObjectId,
45) -> Result<DeviceWalkResult, ClientError> {
46    // 1. Read the object list.
47    let object_list_value = client
48        .read_property(addr, device_id, PropertyId::ObjectList)
49        .await?;
50
51    let object_ids = extract_object_ids(&object_list_value);
52
53    // 2. For each object, read common properties via ReadPropertyMultiple.
54    let properties = &[
55        PropertyId::ObjectName,
56        PropertyId::ObjectType,
57        PropertyId::PresentValue,
58        PropertyId::Description,
59        PropertyId::Units,
60        PropertyId::StatusFlags,
61    ];
62
63    let mut objects = Vec::with_capacity(object_ids.len());
64    for &oid in &object_ids {
65        let props = client.read_property_multiple(addr, oid, properties).await;
66
67        let summary = match props {
68            Ok(prop_values) => build_summary(oid, &prop_values),
69            Err(_) => ObjectSummary {
70                object_id: oid,
71                object_name: None,
72                object_type: oid.object_type(),
73                present_value: None,
74                description: None,
75                units: None,
76                status_flags: None,
77            },
78        };
79        objects.push(summary);
80    }
81
82    // 3. Read device metadata (vendor, model, firmware) from the Device object.
83    let device_info = read_device_info(client, addr, device_id).await;
84
85    Ok(DeviceWalkResult {
86        device_id,
87        device_info,
88        objects,
89    })
90}
91
92async fn read_device_info<D: DataLink>(
93    client: &BacnetClient<D>,
94    addr: DataLinkAddress,
95    device_id: ObjectId,
96) -> DeviceInfo {
97    let info_props = &[
98        PropertyId::VendorName,
99        PropertyId::ModelName,
100        PropertyId::FirmwareRevision,
101    ];
102
103    let prop_values = match client
104        .read_property_multiple(addr, device_id, info_props)
105        .await
106    {
107        Ok(v) => v,
108        Err(_) => return DeviceInfo::default(),
109    };
110
111    let mut info = DeviceInfo::default();
112    for (pid, val) in &prop_values {
113        if let ClientDataValue::CharacterString(s) = val {
114            match pid {
115                PropertyId::VendorName => info.vendor_name = Some(s.clone()),
116                PropertyId::ModelName => info.model_name = Some(s.clone()),
117                PropertyId::FirmwareRevision => info.firmware_revision = Some(s.clone()),
118                _ => {}
119            }
120        }
121    }
122    info
123}
124
125fn extract_object_ids(value: &ClientDataValue) -> Vec<ObjectId> {
126    match value {
127        ClientDataValue::ObjectId(oid) => vec![*oid],
128        ClientDataValue::Constructed { values, .. } => values
129            .iter()
130            .filter_map(|v| {
131                if let ClientDataValue::ObjectId(oid) = v {
132                    Some(*oid)
133                } else {
134                    None
135                }
136            })
137            .collect(),
138        _ => vec![],
139    }
140}
141
142fn build_summary(oid: ObjectId, props: &[(PropertyId, ClientDataValue)]) -> ObjectSummary {
143    let mut summary = ObjectSummary {
144        object_id: oid,
145        object_name: None,
146        object_type: oid.object_type(),
147        present_value: None,
148        description: None,
149        units: None,
150        status_flags: None,
151    };
152
153    for (pid, val) in props {
154        match pid {
155            PropertyId::ObjectName => {
156                if let ClientDataValue::CharacterString(s) = val {
157                    summary.object_name = Some(s.clone());
158                }
159            }
160            PropertyId::ObjectType => {
161                if let ClientDataValue::Enumerated(v) = val {
162                    summary.object_type = ObjectType::from_u16(*v as u16);
163                }
164            }
165            PropertyId::PresentValue => {
166                summary.present_value = Some(val.clone());
167            }
168            PropertyId::Description => {
169                if let ClientDataValue::CharacterString(s) = val {
170                    summary.description = Some(s.clone());
171                }
172            }
173            PropertyId::Units => {
174                if let ClientDataValue::Enumerated(v) = val {
175                    summary.units = Some(*v);
176                }
177            }
178            PropertyId::StatusFlags => {
179                summary.status_flags = Some(val.clone());
180            }
181            _ => {}
182        }
183    }
184
185    summary
186}