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    pub location: Option<String>,
29    pub description: Option<String>,
30    pub max_apdu_length: Option<u32>,
31    pub segmentation_supported: Option<u32>,
32    pub protocol_version: Option<u32>,
33    pub application_software_version: Option<String>,
34}
35
36/// Result of a full device walk.
37#[derive(Debug, Clone)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39pub struct DeviceWalkResult {
40    pub device_id: ObjectId,
41    pub device_info: DeviceInfo,
42    pub objects: Vec<ObjectSummary>,
43}
44
45/// Walk a BACnet device: read its object list, then batch-read common
46/// properties for each object.
47pub async fn walk_device<D: DataLink>(
48    client: &BacnetClient<D>,
49    addr: DataLinkAddress,
50    device_id: ObjectId,
51) -> Result<DeviceWalkResult, ClientError> {
52    // 1. Read the object list.
53    let object_list_value = client
54        .read_property(addr, device_id, PropertyId::ObjectList)
55        .await?;
56
57    let object_ids = extract_object_ids(&object_list_value);
58
59    // 2. For each object, read common properties via ReadPropertyMultiple.
60    let properties = &[
61        PropertyId::ObjectName,
62        PropertyId::ObjectType,
63        PropertyId::PresentValue,
64        PropertyId::Description,
65        PropertyId::Units,
66        PropertyId::StatusFlags,
67    ];
68
69    let mut objects = Vec::with_capacity(object_ids.len());
70    for &oid in &object_ids {
71        let props = client.read_property_multiple(addr, oid, properties).await;
72
73        let summary = match props {
74            Ok(prop_values) => build_summary(oid, &prop_values),
75            Err(_) => ObjectSummary {
76                object_id: oid,
77                object_name: None,
78                object_type: oid.object_type(),
79                present_value: None,
80                description: None,
81                units: None,
82                status_flags: None,
83            },
84        };
85        objects.push(summary);
86    }
87
88    // 3. Read device metadata (vendor, model, firmware) from the Device object.
89    let device_info = read_device_info(client, addr, device_id).await;
90
91    Ok(DeviceWalkResult {
92        device_id,
93        device_info,
94        objects,
95    })
96}
97
98async fn read_device_info<D: DataLink>(
99    client: &BacnetClient<D>,
100    addr: DataLinkAddress,
101    device_id: ObjectId,
102) -> DeviceInfo {
103    let info_props = &[
104        PropertyId::VendorName,
105        PropertyId::ModelName,
106        PropertyId::FirmwareRevision,
107        PropertyId::Location,
108        PropertyId::Description,
109        PropertyId::MaxApduLengthAccepted,
110        PropertyId::SegmentationSupported,
111        PropertyId::ProtocolVersion,
112        PropertyId::ApplicationSoftwareVersion,
113    ];
114
115    let prop_values = match client
116        .read_property_multiple(addr, device_id, info_props)
117        .await
118    {
119        Ok(v) => v,
120        Err(_) => return DeviceInfo::default(),
121    };
122
123    let mut info = DeviceInfo::default();
124    for (pid, val) in &prop_values {
125        match (pid, val) {
126            (PropertyId::VendorName, ClientDataValue::CharacterString(s)) => {
127                info.vendor_name = Some(s.clone());
128            }
129            (PropertyId::ModelName, ClientDataValue::CharacterString(s)) => {
130                info.model_name = Some(s.clone());
131            }
132            (PropertyId::FirmwareRevision, ClientDataValue::CharacterString(s)) => {
133                info.firmware_revision = Some(s.clone());
134            }
135            (PropertyId::Location, ClientDataValue::CharacterString(s)) => {
136                info.location = Some(s.clone());
137            }
138            (PropertyId::Description, ClientDataValue::CharacterString(s)) => {
139                info.description = Some(s.clone());
140            }
141            (PropertyId::MaxApduLengthAccepted, ClientDataValue::Unsigned(v)) => {
142                info.max_apdu_length = Some(*v);
143            }
144            (PropertyId::SegmentationSupported, ClientDataValue::Enumerated(v)) => {
145                info.segmentation_supported = Some(*v);
146            }
147            (PropertyId::ProtocolVersion, ClientDataValue::Unsigned(v)) => {
148                info.protocol_version = Some(*v);
149            }
150            (PropertyId::ApplicationSoftwareVersion, ClientDataValue::CharacterString(s)) => {
151                info.application_software_version = Some(s.clone());
152            }
153            _ => {}
154        }
155    }
156    info
157}
158
159fn extract_object_ids(value: &ClientDataValue) -> Vec<ObjectId> {
160    match value {
161        ClientDataValue::ObjectId(oid) => vec![*oid],
162        ClientDataValue::Constructed { values, .. } => values
163            .iter()
164            .filter_map(|v| {
165                if let ClientDataValue::ObjectId(oid) = v {
166                    Some(*oid)
167                } else {
168                    None
169                }
170            })
171            .collect(),
172        _ => vec![],
173    }
174}
175
176fn build_summary(oid: ObjectId, props: &[(PropertyId, ClientDataValue)]) -> ObjectSummary {
177    let mut summary = ObjectSummary {
178        object_id: oid,
179        object_name: None,
180        object_type: oid.object_type(),
181        present_value: None,
182        description: None,
183        units: None,
184        status_flags: None,
185    };
186
187    for (pid, val) in props {
188        match pid {
189            PropertyId::ObjectName => {
190                if let ClientDataValue::CharacterString(s) = val {
191                    summary.object_name = Some(s.clone());
192                }
193            }
194            PropertyId::ObjectType => {
195                if let ClientDataValue::Enumerated(v) = val {
196                    summary.object_type = ObjectType::from_u16(*v as u16);
197                }
198            }
199            PropertyId::PresentValue => {
200                summary.present_value = Some(val.clone());
201            }
202            PropertyId::Description => {
203                if let ClientDataValue::CharacterString(s) = val {
204                    summary.description = Some(s.clone());
205                }
206            }
207            PropertyId::Units => {
208                if let ClientDataValue::Enumerated(v) = val {
209                    summary.units = Some(*v);
210                }
211            }
212            PropertyId::StatusFlags => {
213                summary.status_flags = Some(val.clone());
214            }
215            _ => {}
216        }
217    }
218
219    summary
220}