Skip to main content

bacnet_objects/device/
mod.rs

1//! Device object (type 8) per ASHRAE 135-2020 Clause 12.11.
2//!
3//! The Device object is required in every BACnet device and exposes
4//! device-level properties such as vendor info, protocol support,
5//! and configuration parameters.
6
7use std::borrow::Cow;
8use std::collections::HashMap;
9
10use bacnet_types::constructed::BACnetCOVSubscription;
11use bacnet_types::enums::{ErrorClass, ErrorCode, ObjectType, PropertyIdentifier, Segmentation};
12use bacnet_types::error::Error;
13use bacnet_types::primitives::{Date, ObjectIdentifier, PropertyValue, Time};
14
15use crate::common::read_property_list_property;
16use crate::traits::BACnetObject;
17
18/// Build a BACnet bitstring representing supported object types.
19/// Each type N sets bit at byte N/8, position 7-(N%8) (MSB-first within each byte).
20fn compute_object_types_supported(types: &[u32]) -> Vec<u8> {
21    let max_type = types.iter().copied().max().unwrap_or(0) as usize;
22    let num_bytes = (max_type / 8) + 1;
23    let mut bitstring = vec![0u8; num_bytes];
24    for &t in types {
25        let byte_idx = (t as usize) / 8;
26        let bit_pos = 7 - ((t as usize) % 8);
27        if byte_idx < bitstring.len() {
28            bitstring[byte_idx] |= 1 << bit_pos;
29        }
30    }
31    bitstring
32}
33
34/// Configuration for creating a Device object.
35pub struct DeviceConfig {
36    /// Device instance number (0..4194303).
37    pub instance: u32,
38    /// Device object name.
39    pub name: String,
40    /// Vendor name string.
41    pub vendor_name: String,
42    /// ASHRAE-assigned vendor identifier.
43    pub vendor_id: u16,
44    /// Model name string.
45    pub model_name: String,
46    /// Firmware revision string.
47    pub firmware_revision: String,
48    /// Application software version string.
49    pub application_software_version: String,
50    /// Maximum APDU length accepted (typically 1476 for BIP).
51    pub max_apdu_length: u32,
52    /// Segmentation support level.
53    pub segmentation_supported: Segmentation,
54    /// APDU timeout in milliseconds.
55    pub apdu_timeout: u32,
56    /// Number of APDU retries.
57    pub apdu_retries: u32,
58}
59
60impl Default for DeviceConfig {
61    fn default() -> Self {
62        Self {
63            instance: 1,
64            name: "BACnet Device".into(),
65            vendor_name: "Rusty BACnet".into(),
66            vendor_id: 0,
67            model_name: "rusty-bacnet".into(),
68            firmware_revision: "0.1.0".into(),
69            application_software_version: "0.1.0".into(),
70            max_apdu_length: 1476,
71            segmentation_supported: Segmentation::NONE,
72            apdu_timeout: 6000,
73            apdu_retries: 3,
74        }
75    }
76}
77
78/// BACnet Device object.
79pub struct DeviceObject {
80    oid: ObjectIdentifier,
81    properties: HashMap<PropertyIdentifier, PropertyValue>,
82    /// Cached object list for array-indexed reads.
83    object_list: Vec<ObjectIdentifier>,
84    /// Protocol_Object_Types_Supported — bitstring indicating which object
85    /// types this device supports (one bit per type, MSB-first within each byte).
86    protocol_object_types_supported: Vec<u8>,
87    /// Protocol_Services_Supported — bitstring indicating which services
88    /// this device supports (one bit per service, MSB-first within each byte).
89    protocol_services_supported: Vec<u8>,
90    /// Active COV subscriptions maintained by the server.
91    active_cov_subscriptions: Vec<BACnetCOVSubscription>,
92}
93
94impl DeviceObject {
95    /// Create a new Device object from configuration.
96    pub fn new(config: DeviceConfig) -> Result<Self, Error> {
97        let oid = ObjectIdentifier::new(ObjectType::DEVICE, config.instance)?;
98        let mut properties = HashMap::new();
99
100        properties.insert(
101            PropertyIdentifier::OBJECT_IDENTIFIER,
102            PropertyValue::ObjectIdentifier(oid),
103        );
104        properties.insert(
105            PropertyIdentifier::OBJECT_NAME,
106            PropertyValue::CharacterString(config.name),
107        );
108        properties.insert(
109            PropertyIdentifier::OBJECT_TYPE,
110            PropertyValue::Enumerated(ObjectType::DEVICE.to_raw()),
111        );
112        properties.insert(
113            PropertyIdentifier::SYSTEM_STATUS,
114            PropertyValue::Enumerated(0), // operational
115        );
116        properties.insert(
117            PropertyIdentifier::VENDOR_NAME,
118            PropertyValue::CharacterString(config.vendor_name),
119        );
120        properties.insert(
121            PropertyIdentifier::VENDOR_IDENTIFIER,
122            PropertyValue::Unsigned(config.vendor_id as u64),
123        );
124        properties.insert(
125            PropertyIdentifier::MODEL_NAME,
126            PropertyValue::CharacterString(config.model_name),
127        );
128        properties.insert(
129            PropertyIdentifier::FIRMWARE_REVISION,
130            PropertyValue::CharacterString(config.firmware_revision),
131        );
132        properties.insert(
133            PropertyIdentifier::APPLICATION_SOFTWARE_VERSION,
134            PropertyValue::CharacterString(config.application_software_version),
135        );
136        properties.insert(
137            PropertyIdentifier::PROTOCOL_VERSION,
138            PropertyValue::Unsigned(1),
139        );
140        properties.insert(
141            PropertyIdentifier::PROTOCOL_REVISION,
142            PropertyValue::Unsigned(22), // Revision 22 (2020)
143        );
144        properties.insert(
145            PropertyIdentifier::MAX_APDU_LENGTH_ACCEPTED,
146            PropertyValue::Unsigned(config.max_apdu_length as u64),
147        );
148        properties.insert(
149            PropertyIdentifier::SEGMENTATION_SUPPORTED,
150            PropertyValue::Enumerated(config.segmentation_supported.to_raw() as u32),
151        );
152        properties.insert(
153            PropertyIdentifier::APDU_TIMEOUT,
154            PropertyValue::Unsigned(config.apdu_timeout as u64),
155        );
156        properties.insert(
157            PropertyIdentifier::NUMBER_OF_APDU_RETRIES,
158            PropertyValue::Unsigned(config.apdu_retries as u64),
159        );
160        properties.insert(
161            PropertyIdentifier::DATABASE_REVISION,
162            PropertyValue::Unsigned(0),
163        );
164        properties.insert(
165            PropertyIdentifier::DESCRIPTION,
166            PropertyValue::CharacterString(String::new()),
167        );
168
169        // Device_Address_Binding — starts empty; populated as devices are discovered.
170        properties.insert(
171            PropertyIdentifier::DEVICE_ADDRESS_BINDING,
172            PropertyValue::List(Vec::new()),
173        );
174
175        // Placeholder values updated by the server's time sync or system clock.
176        properties.insert(
177            PropertyIdentifier::LOCAL_DATE,
178            PropertyValue::Date(Date {
179                year: 126, // 2026 - 1900
180                month: 3,
181                day: 18,
182                day_of_week: 3, // Wednesday
183            }),
184        );
185        properties.insert(
186            PropertyIdentifier::LOCAL_TIME,
187            PropertyValue::Time(Time {
188                hour: 12,
189                minute: 0,
190                second: 0,
191                hundredths: 0,
192            }),
193        );
194
195        // UTC_Offset: signed integer minutes from UTC (e.g., -300 for EST).
196        properties.insert(
197            PropertyIdentifier::UTC_OFFSET,
198            PropertyValue::Signed(0), // UTC
199        );
200
201        // Last_Restart_Reason: 0=unknown, 1=coldstart, 2=warmstart, etc.
202        properties.insert(
203            PropertyIdentifier::LAST_RESTART_REASON,
204            PropertyValue::Enumerated(0), // unknown
205        );
206
207        // Device_UUID: 16-byte UUID stored as OctetString. Default: all zeros.
208        properties.insert(
209            PropertyIdentifier::DEVICE_UUID,
210            PropertyValue::OctetString(vec![0u8; 16]),
211        );
212
213        // Max_Segments_Accepted — only included when segmentation is supported.
214        if config.segmentation_supported != Segmentation::NONE {
215            properties.insert(
216                PropertyIdentifier::MAX_SEGMENTS_ACCEPTED,
217                PropertyValue::Unsigned(65), // default: more than 64 segments
218            );
219        }
220
221        // Protocol_Object_Types_Supported: bitstring with one bit per
222        // implemented object type.  Computed from the full set of types
223        // that have concrete struct implementations in this crate.
224        let protocol_object_types_supported = compute_object_types_supported(&[
225            ObjectType::ANALOG_INPUT.to_raw(),
226            ObjectType::ANALOG_OUTPUT.to_raw(),
227            ObjectType::ANALOG_VALUE.to_raw(),
228            ObjectType::BINARY_INPUT.to_raw(),
229            ObjectType::BINARY_OUTPUT.to_raw(),
230            ObjectType::BINARY_VALUE.to_raw(),
231            ObjectType::CALENDAR.to_raw(),
232            ObjectType::COMMAND.to_raw(),
233            ObjectType::DEVICE.to_raw(),
234            ObjectType::EVENT_ENROLLMENT.to_raw(),
235            ObjectType::FILE.to_raw(),
236            ObjectType::GROUP.to_raw(),
237            ObjectType::LOOP.to_raw(),
238            ObjectType::MULTI_STATE_INPUT.to_raw(),
239            ObjectType::MULTI_STATE_OUTPUT.to_raw(),
240            ObjectType::NOTIFICATION_CLASS.to_raw(),
241            ObjectType::PROGRAM.to_raw(),
242            ObjectType::SCHEDULE.to_raw(),
243            ObjectType::AVERAGING.to_raw(),
244            ObjectType::MULTI_STATE_VALUE.to_raw(),
245            ObjectType::TREND_LOG.to_raw(),
246            ObjectType::LIFE_SAFETY_POINT.to_raw(),
247            ObjectType::LIFE_SAFETY_ZONE.to_raw(),
248            ObjectType::ACCUMULATOR.to_raw(),
249            ObjectType::PULSE_CONVERTER.to_raw(),
250            ObjectType::EVENT_LOG.to_raw(),
251            ObjectType::GLOBAL_GROUP.to_raw(),
252            ObjectType::TREND_LOG_MULTIPLE.to_raw(),
253            ObjectType::LOAD_CONTROL.to_raw(),
254            ObjectType::STRUCTURED_VIEW.to_raw(),
255            ObjectType::ACCESS_DOOR.to_raw(),
256            ObjectType::TIMER.to_raw(),
257            ObjectType::ACCESS_CREDENTIAL.to_raw(),
258            ObjectType::ACCESS_POINT.to_raw(),
259            ObjectType::ACCESS_RIGHTS.to_raw(),
260            ObjectType::ACCESS_USER.to_raw(),
261            ObjectType::ACCESS_ZONE.to_raw(),
262            ObjectType::CREDENTIAL_DATA_INPUT.to_raw(),
263            ObjectType::BITSTRING_VALUE.to_raw(),
264            ObjectType::CHARACTERSTRING_VALUE.to_raw(),
265            ObjectType::DATEPATTERN_VALUE.to_raw(),
266            ObjectType::DATE_VALUE.to_raw(),
267            ObjectType::DATETIMEPATTERN_VALUE.to_raw(),
268            ObjectType::DATETIME_VALUE.to_raw(),
269            ObjectType::INTEGER_VALUE.to_raw(),
270            ObjectType::LARGE_ANALOG_VALUE.to_raw(),
271            ObjectType::OCTETSTRING_VALUE.to_raw(),
272            ObjectType::POSITIVE_INTEGER_VALUE.to_raw(),
273            ObjectType::TIMEPATTERN_VALUE.to_raw(),
274            ObjectType::TIME_VALUE.to_raw(),
275            ObjectType::NOTIFICATION_FORWARDER.to_raw(),
276            ObjectType::ALERT_ENROLLMENT.to_raw(),
277            ObjectType::CHANNEL.to_raw(),
278            ObjectType::LIGHTING_OUTPUT.to_raw(),
279            ObjectType::BINARY_LIGHTING_OUTPUT.to_raw(),
280            ObjectType::NETWORK_PORT.to_raw(),
281            ObjectType::ELEVATOR_GROUP.to_raw(),
282            ObjectType::ESCALATOR.to_raw(),
283            ObjectType::LIFT.to_raw(),
284            ObjectType::STAGING.to_raw(),
285            ObjectType::AUDIT_REPORTER.to_raw(),
286            ObjectType::AUDIT_LOG.to_raw(),
287            ObjectType::COLOR.to_raw(),
288            ObjectType::COLOR_TEMPERATURE.to_raw(),
289        ]);
290
291        // Protocol_Services_Supported: 6 bytes (48 bits).  Bits set for
292        // services we handle:
293        //   0=AcknowledgeAlarm, 2=ConfirmedEventNotification,
294        //   5=SubscribeCOV, 12=ReadProperty, 14=ReadPropertyMultiple,
295        //   15=WriteProperty, 16=WritePropertyMultiple,
296        //   26=IAm, 27=IHave, 29=UnconfirmedCOVNotification,
297        //   31=WhoHas, 32=WhoIs
298        //   Byte 0: bits 0,2,5 → 0xA4
299        //   Byte 1: bits 12,14,15 → 0x0B
300        //   Byte 2: bit 16 → 0x80
301        //   Byte 3: bits 26,27,29,31 → 0x35
302        //   Byte 4: bit 32 → 0x80
303        //   Byte 5: 0x00
304        let protocol_services_supported = vec![0xA4, 0x0B, 0x80, 0x35, 0x80, 0x00];
305
306        Ok(Self {
307            oid,
308            properties,
309            object_list: vec![oid], // Device itself is always in the list
310            protocol_object_types_supported,
311            protocol_services_supported,
312            active_cov_subscriptions: Vec::new(),
313        })
314    }
315
316    /// Update the object-list with the current database contents.
317    pub fn set_object_list(&mut self, oids: Vec<ObjectIdentifier>) {
318        self.object_list = oids;
319    }
320
321    /// Get the device instance number.
322    pub fn instance(&self) -> u32 {
323        self.oid.instance_number()
324    }
325
326    /// Set the description string.
327    pub fn set_description(&mut self, desc: impl Into<String>) {
328        self.properties.insert(
329            PropertyIdentifier::DESCRIPTION,
330            PropertyValue::CharacterString(desc.into()),
331        );
332    }
333
334    /// Replace the entire active COV subscriptions list.
335    pub fn set_active_cov_subscriptions(&mut self, subs: Vec<BACnetCOVSubscription>) {
336        self.active_cov_subscriptions = subs;
337    }
338
339    /// Add a single COV subscription.
340    pub fn add_cov_subscription(&mut self, sub: BACnetCOVSubscription) {
341        self.active_cov_subscriptions.push(sub);
342    }
343}
344
345impl BACnetObject for DeviceObject {
346    fn object_identifier(&self) -> ObjectIdentifier {
347        self.oid
348    }
349
350    fn object_name(&self) -> &str {
351        match self.properties.get(&PropertyIdentifier::OBJECT_NAME) {
352            Some(PropertyValue::CharacterString(s)) => s,
353            _ => "Unknown",
354        }
355    }
356
357    fn read_property(
358        &self,
359        property: PropertyIdentifier,
360        array_index: Option<u32>,
361    ) -> Result<PropertyValue, Error> {
362        if property == PropertyIdentifier::OBJECT_LIST {
363            return match array_index {
364                None => {
365                    let elements = self
366                        .object_list
367                        .iter()
368                        .map(|oid| PropertyValue::ObjectIdentifier(*oid))
369                        .collect();
370                    Ok(PropertyValue::List(elements))
371                }
372                Some(0) => {
373                    // Index 0 = array length per BACnet convention
374                    Ok(PropertyValue::Unsigned(self.object_list.len() as u64))
375                }
376                Some(idx) => {
377                    let i = (idx - 1) as usize; // BACnet arrays are 1-based
378                    if i < self.object_list.len() {
379                        Ok(PropertyValue::ObjectIdentifier(self.object_list[i]))
380                    } else {
381                        Err(Error::Protocol {
382                            class: ErrorClass::PROPERTY.to_raw() as u32,
383                            code: ErrorCode::INVALID_ARRAY_INDEX.to_raw() as u32,
384                        })
385                    }
386                }
387            };
388        }
389
390        if property == PropertyIdentifier::PROPERTY_LIST {
391            return read_property_list_property(&self.property_list(), array_index);
392        }
393
394        if property == PropertyIdentifier::PROTOCOL_OBJECT_TYPES_SUPPORTED {
395            let num_bytes = self.protocol_object_types_supported.len();
396            let total_bits = num_bytes * 8;
397            // Find highest set bit to determine actual used bits
398            let mut max_type = 0u32;
399            for (byte_idx, &byte) in self.protocol_object_types_supported.iter().enumerate() {
400                for bit in 0..8 {
401                    if byte & (1 << (7 - bit)) != 0 {
402                        max_type = (byte_idx * 8 + bit) as u32;
403                    }
404                }
405            }
406            let used_bits = max_type as usize + 1;
407            let unused = (total_bits - used_bits) as u8;
408            return Ok(PropertyValue::BitString {
409                unused_bits: unused,
410                data: self.protocol_object_types_supported.clone(),
411            });
412        }
413
414        if property == PropertyIdentifier::PROTOCOL_SERVICES_SUPPORTED {
415            // 6 bytes = 48 bits; 41 defined (services 0-40), 7 unused bits
416            return Ok(PropertyValue::BitString {
417                unused_bits: 7,
418                data: self.protocol_services_supported.clone(),
419            });
420        }
421
422        if property == PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS {
423            let elements: Vec<PropertyValue> = self
424                .active_cov_subscriptions
425                .iter()
426                .map(|sub| {
427                    let mut entry = vec![
428                        PropertyValue::ObjectIdentifier(
429                            sub.monitored_property_reference.object_identifier,
430                        ),
431                        PropertyValue::Unsigned(sub.recipient.process_identifier as u64),
432                        PropertyValue::Boolean(sub.issue_confirmed_notifications),
433                        PropertyValue::Unsigned(sub.time_remaining as u64),
434                    ];
435                    if let Some(inc) = sub.cov_increment {
436                        entry.push(PropertyValue::Real(inc));
437                    }
438                    PropertyValue::List(entry)
439                })
440                .collect();
441            return Ok(PropertyValue::List(elements));
442        }
443
444        self.properties
445            .get(&property)
446            .cloned()
447            .ok_or(Error::Protocol {
448                class: ErrorClass::PROPERTY.to_raw() as u32,
449                code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
450            })
451    }
452
453    fn write_property(
454        &mut self,
455        property: PropertyIdentifier,
456        _array_index: Option<u32>,
457        value: PropertyValue,
458        _priority: Option<u8>,
459    ) -> Result<(), Error> {
460        if property == PropertyIdentifier::DESCRIPTION {
461            if let PropertyValue::CharacterString(_) = &value {
462                self.properties.insert(property, value);
463                return Ok(());
464            }
465            return Err(Error::Protocol {
466                class: ErrorClass::PROPERTY.to_raw() as u32,
467                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
468            });
469        }
470        Err(Error::Protocol {
471            class: ErrorClass::PROPERTY.to_raw() as u32,
472            code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
473        })
474    }
475
476    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
477        let mut props: Vec<PropertyIdentifier> = self.properties.keys().copied().collect();
478        props.push(PropertyIdentifier::OBJECT_LIST);
479        props.push(PropertyIdentifier::PROPERTY_LIST);
480        props.push(PropertyIdentifier::PROTOCOL_OBJECT_TYPES_SUPPORTED);
481        props.push(PropertyIdentifier::PROTOCOL_SERVICES_SUPPORTED);
482        props.push(PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS);
483        props.sort_by_key(|p| p.to_raw());
484        Cow::Owned(props)
485    }
486}
487
488#[cfg(test)]
489mod tests;