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;
9use std::sync::Arc;
10
11use bacnet_encoding::primitives::encode_timestamp_choice;
12use bacnet_types::enums::{
13    BackupAndRestoreState, DeviceStatus, ErrorClass, ErrorCode, ObjectType, PropertyIdentifier,
14    Segmentation, ServiceSupported,
15};
16use bacnet_types::error::Error;
17use bacnet_types::primitives::{BACnetTimeStamp, ObjectIdentifier, PropertyValue};
18use bytes::BytesMut;
19
20use crate::clock::{ClockFrame, ClockReader, UNSPECIFIED_DATETIME};
21use crate::common::{
22    invalid_data_type_error, property_is_not_an_array_error, read_array,
23    read_property_list_property, value_out_of_range_error,
24};
25use crate::object_profile::{ObjectProfile, ProfileState, TagsPersistence};
26use crate::traits::BACnetObject;
27
28mod audit_recipient;
29mod metadata;
30pub use audit_recipient::{AuditRecipientChangeSink, AuditWriteSource, DeviceAuthority};
31
32/// Every service the bundled `bacnet-server` dispatch executes, as
33/// `BACnetServicesSupported` bit positions (Clause 21).
34///
35/// Default source for `Protocol_Services_Supported`, which Clause 12.11 ties
36/// to services *executed* — the server's initiate-only services (I-Am,
37/// I-Have, COV/event notifications) are deliberately absent. Kept in lockstep
38/// with the dispatch arms by `bacnet-server`'s executed-services cross-check
39/// test; deployments with a different dispatch surface override via
40/// [`DeviceObject::set_services_supported`].
41pub const EXECUTED_SERVICES: &[ServiceSupported] = &[
42    ServiceSupported::ACKNOWLEDGE_ALARM,
43    // Received notifications go to the Notification Forwarder objects
44    // (Clause 12.51); the server acknowledges each confirmed one.
45    ServiceSupported::CONFIRMED_EVENT_NOTIFICATION,
46    ServiceSupported::GET_ALARM_SUMMARY,
47    ServiceSupported::GET_ENROLLMENT_SUMMARY,
48    ServiceSupported::SUBSCRIBE_COV,
49    ServiceSupported::ATOMIC_READ_FILE,
50    ServiceSupported::ATOMIC_WRITE_FILE,
51    ServiceSupported::ADD_LIST_ELEMENT,
52    ServiceSupported::REMOVE_LIST_ELEMENT,
53    ServiceSupported::CREATE_OBJECT,
54    ServiceSupported::DELETE_OBJECT,
55    ServiceSupported::READ_PROPERTY,
56    ServiceSupported::READ_PROPERTY_MULTIPLE,
57    ServiceSupported::WRITE_PROPERTY,
58    ServiceSupported::WRITE_PROPERTY_MULTIPLE,
59    ServiceSupported::DEVICE_COMMUNICATION_CONTROL,
60    ServiceSupported::CONFIRMED_TEXT_MESSAGE,
61    ServiceSupported::REINITIALIZE_DEVICE,
62    ServiceSupported::UNCONFIRMED_EVENT_NOTIFICATION,
63    ServiceSupported::UNCONFIRMED_TEXT_MESSAGE,
64    ServiceSupported::TIME_SYNCHRONIZATION,
65    ServiceSupported::WHO_HAS,
66    ServiceSupported::WHO_IS,
67    ServiceSupported::READ_RANGE,
68    ServiceSupported::UTC_TIME_SYNCHRONIZATION,
69    // Executes authorized silence/unsilence operations. Built-in reset execution
70    // stays unsupported pending application-executor and replay semantics.
71    ServiceSupported::LIFE_SAFETY_OPERATION,
72    ServiceSupported::SUBSCRIBE_COV_PROPERTY,
73    ServiceSupported::GET_EVENT_INFORMATION,
74    // Applied to the local Channel objects (Clause 15.11).
75    ServiceSupported::WRITE_GROUP,
76    ServiceSupported::SUBSCRIBE_COV_PROPERTY_MULTIPLE,
77    ServiceSupported::CONFIRMED_AUDIT_NOTIFICATION,
78    ServiceSupported::UNCONFIRMED_AUDIT_NOTIFICATION,
79    ServiceSupported::AUDIT_LOG_QUERY,
80];
81
82/// Number of bits in the `BACnetServicesSupported` production: bits 0..=48
83/// (you-Are is the highest defined bit, Clause 21).
84const SERVICES_SUPPORTED_BITS: usize = 49;
85
86/// Build the `Protocol_Services_Supported` bit string from a service set,
87/// sized for the full production (7 octets, 7 unused bits) and packed
88/// MSB-first per Clause 20.2.10: bit N at byte N/8, position 7-(N%8).
89fn compute_services_supported(services: &[ServiceSupported]) -> Vec<u8> {
90    let mut bits = vec![0u8; SERVICES_SUPPORTED_BITS.div_ceil(8)];
91    for service in services {
92        let n = service.to_raw() as usize;
93        if n < SERVICES_SUPPORTED_BITS {
94            bits[n / 8] |= 0x80 >> (n % 8);
95        }
96    }
97    bits
98}
99
100/// Build a BACnet bitstring representing supported object types.
101/// Each type N sets bit at byte N/8, position 7-(N%8) (MSB-first within each byte).
102fn compute_object_types_supported(types: &[u32]) -> Vec<u8> {
103    let max_type = types.iter().copied().max().unwrap_or(0) as usize;
104    let num_bytes = (max_type / 8) + 1;
105    let mut bitstring = vec![0u8; num_bytes];
106    for &t in types {
107        let byte_idx = (t as usize) / 8;
108        let bit_pos = 7 - ((t as usize) % 8);
109        if byte_idx < bitstring.len() {
110            bitstring[byte_idx] |= 1 << bit_pos;
111        }
112    }
113    bitstring
114}
115
116/// Configuration for creating a Device object.
117pub struct DeviceConfig {
118    /// One source for Device identity and runtime declarations.
119    pub identity: crate::DeviceIdentity,
120    /// Vendor name string.
121    pub vendor_name: String,
122    /// Model name string.
123    pub model_name: String,
124    /// Firmware revision string.
125    pub firmware_revision: String,
126    /// Application software version string.
127    pub application_software_version: String,
128    /// APDU timeout in milliseconds.
129    pub apdu_timeout: u32,
130    /// Number of APDU retries.
131    pub apdu_retries: u32,
132    /// Whether the device performs the backup and restore procedures (Clause 19.1), which
133    /// adds Configuration_Files, Last_Restore_Time, Backup_Failure_Timeout and
134    /// Backup_And_Restore_State. The application carries out the procedures themselves.
135    pub backup_and_restore: bool,
136    /// Initial Backup_Failure_Timeout in seconds. Only served with `backup_and_restore`.
137    pub backup_failure_timeout: u16,
138}
139
140impl Default for DeviceConfig {
141    fn default() -> Self {
142        Self {
143            identity: crate::DeviceIdentity::default(),
144            vendor_name: "Rusty BACnet".into(),
145            model_name: "rusty-bacnet".into(),
146            firmware_revision: "0.1.0".into(),
147            application_software_version: "0.1.0".into(),
148            apdu_timeout: 6000,
149            apdu_retries: 3,
150            backup_and_restore: false,
151            backup_failure_timeout: 60,
152        }
153    }
154}
155
156fn timestamp_value(stamp: &BACnetTimeStamp) -> PropertyValue {
157    let mut encoded = BytesMut::new();
158    encode_timestamp_choice(&mut encoded, stamp)
159        .expect("BACnetTimeStamp CHOICE encoding is infallible");
160    PropertyValue::ApplicationData(encoded.to_vec())
161}
162
163/// BACnet Device object.
164pub struct DeviceObject {
165    oid: ObjectIdentifier,
166    recipient: audit_recipient::RecipientState,
167    properties: HashMap<PropertyIdentifier, PropertyValue>,
168    profile: ProfileState,
169    /// Cached object list for array-indexed reads.
170    object_list: Vec<ObjectIdentifier>,
171    /// Protocol_Object_Types_Supported — bitstring indicating which object
172    /// types this device supports (one bit per type, MSB-first within each byte).
173    protocol_object_types_supported: Vec<u8>,
174    /// Configured executed services before clock-availability filtering.
175    configured_services_supported: Vec<ServiceSupported>,
176    /// Shared dynamic clock sample source. `None` is explicitly clockless.
177    clock: Option<Arc<dyn ClockReader>>,
178}
179
180impl DeviceObject {
181    /// Create a new Device object from configuration.
182    pub fn new(config: DeviceConfig) -> Result<Self, Error> {
183        config.identity.validate()?;
184        let oid = config.identity.device_oid();
185        let mut properties = HashMap::new();
186
187        properties.insert(
188            PropertyIdentifier::OBJECT_IDENTIFIER,
189            PropertyValue::ObjectIdentifier(oid),
190        );
191        properties.insert(
192            PropertyIdentifier::OBJECT_NAME,
193            PropertyValue::CharacterString(config.identity.name().to_owned()),
194        );
195        properties.insert(
196            PropertyIdentifier::OBJECT_TYPE,
197            PropertyValue::Enumerated(ObjectType::DEVICE.to_raw()),
198        );
199        properties.insert(
200            PropertyIdentifier::SYSTEM_STATUS,
201            PropertyValue::Enumerated(DeviceStatus::OPERATIONAL.to_raw()),
202        );
203        properties.insert(
204            PropertyIdentifier::VENDOR_NAME,
205            PropertyValue::CharacterString(config.vendor_name),
206        );
207        properties.insert(
208            PropertyIdentifier::VENDOR_IDENTIFIER,
209            PropertyValue::Unsigned(config.identity.vendor_id() as u64),
210        );
211        properties.insert(
212            PropertyIdentifier::MODEL_NAME,
213            PropertyValue::CharacterString(config.model_name),
214        );
215        properties.insert(
216            PropertyIdentifier::FIRMWARE_REVISION,
217            PropertyValue::CharacterString(config.firmware_revision),
218        );
219        properties.insert(
220            PropertyIdentifier::APPLICATION_SOFTWARE_VERSION,
221            PropertyValue::CharacterString(config.application_software_version),
222        );
223        properties.insert(
224            PropertyIdentifier::PROTOCOL_VERSION,
225            PropertyValue::Unsigned(1),
226        );
227        properties.insert(
228            PropertyIdentifier::PROTOCOL_REVISION,
229            PropertyValue::Unsigned(22), // Revision 22 (2020)
230        );
231        properties.insert(
232            PropertyIdentifier::MAX_APDU_LENGTH_ACCEPTED,
233            PropertyValue::Unsigned(config.identity.max_apdu_length() as u64),
234        );
235        properties.insert(
236            PropertyIdentifier::SEGMENTATION_SUPPORTED,
237            PropertyValue::Enumerated(config.identity.segmentation().to_raw() as u32),
238        );
239        properties.insert(
240            PropertyIdentifier::APDU_TIMEOUT,
241            PropertyValue::Unsigned(config.apdu_timeout as u64),
242        );
243        properties.insert(
244            PropertyIdentifier::NUMBER_OF_APDU_RETRIES,
245            PropertyValue::Unsigned(config.apdu_retries as u64),
246        );
247        properties.insert(
248            PropertyIdentifier::DATABASE_REVISION,
249            PropertyValue::Unsigned(0),
250        );
251        properties.insert(
252            PropertyIdentifier::DESCRIPTION,
253            PropertyValue::CharacterString(String::new()),
254        );
255
256        // Device_Address_Binding: stored empty. A running server serves its
257        // device bindings in place of this value (bacnet-server, #1369).
258        properties.insert(
259            PropertyIdentifier::DEVICE_ADDRESS_BINDING,
260            PropertyValue::List(Vec::new()),
261        );
262
263        // Last_Restart_Reason: 0=unknown, 1=coldstart, 2=warmstart, etc.
264        properties.insert(
265            PropertyIdentifier::LAST_RESTART_REASON,
266            PropertyValue::Enumerated(0), // unknown
267        );
268
269        // Device_UUID: 16-byte UUID stored as OctetString. Default: all zeros.
270        properties.insert(
271            PropertyIdentifier::DEVICE_UUID,
272            PropertyValue::OctetString(config.identity.device_uuid().to_vec()),
273        );
274
275        // Table 12-13 footnotes 7 and 8: present only on a device that performs the backup
276        // and restore procedures. The application owns the File objects Configuration_Files
277        // names, so the list starts empty.
278        if config.backup_and_restore {
279            if config.backup_failure_timeout == 0 {
280                return Err(Error::Encoding(
281                    "Backup_Failure_Timeout must be positive when backup and restore is on".into(),
282                ));
283            }
284            properties.insert(
285                PropertyIdentifier::CONFIGURATION_FILES,
286                PropertyValue::List(Vec::new()),
287            );
288            // No restore has happened yet.
289            let (date, time) = UNSPECIFIED_DATETIME;
290            properties.insert(
291                PropertyIdentifier::LAST_RESTORE_TIME,
292                timestamp_value(&BACnetTimeStamp::DateTime { date, time }),
293            );
294            properties.insert(
295                PropertyIdentifier::BACKUP_FAILURE_TIMEOUT,
296                PropertyValue::Unsigned(u64::from(config.backup_failure_timeout)),
297            );
298            properties.insert(
299                PropertyIdentifier::BACKUP_AND_RESTORE_STATE,
300                PropertyValue::Enumerated(BackupAndRestoreState::IDLE.to_raw()),
301            );
302        }
303
304        // Max_Segments_Accepted — only included when segmentation is supported.
305        if config.identity.segmentation() != Segmentation::NONE {
306            if config.identity.apdu_segment_timeout_ms() == 0 {
307                return Err(Error::Encoding(
308                    "APDU_Segment_Timeout must be positive when segmentation is supported".into(),
309                ));
310            }
311            properties.insert(
312                PropertyIdentifier::APDU_SEGMENT_TIMEOUT,
313                PropertyValue::Unsigned(config.identity.apdu_segment_timeout_ms()),
314            );
315            let max_segments_accepted = if config.identity.segmentation() == Segmentation::TRANSMIT
316            {
317                1
318            } else {
319                65
320            };
321            properties.insert(
322                PropertyIdentifier::MAX_SEGMENTS_ACCEPTED,
323                PropertyValue::Unsigned(max_segments_accepted),
324            );
325        }
326
327        // Protocol_Object_Types_Supported: bitstring with one bit per
328        // implemented object type.  Computed from the full set of types
329        // that have concrete struct implementations in this crate.
330        let protocol_object_types_supported = compute_object_types_supported(&[
331            ObjectType::ANALOG_INPUT.to_raw(),
332            ObjectType::ANALOG_OUTPUT.to_raw(),
333            ObjectType::ANALOG_VALUE.to_raw(),
334            ObjectType::BINARY_INPUT.to_raw(),
335            ObjectType::BINARY_OUTPUT.to_raw(),
336            ObjectType::BINARY_VALUE.to_raw(),
337            ObjectType::CALENDAR.to_raw(),
338            ObjectType::COMMAND.to_raw(),
339            ObjectType::DEVICE.to_raw(),
340            ObjectType::EVENT_ENROLLMENT.to_raw(),
341            ObjectType::FILE.to_raw(),
342            ObjectType::GROUP.to_raw(),
343            ObjectType::LOOP.to_raw(),
344            ObjectType::MULTI_STATE_INPUT.to_raw(),
345            ObjectType::MULTI_STATE_OUTPUT.to_raw(),
346            ObjectType::NOTIFICATION_CLASS.to_raw(),
347            ObjectType::PROGRAM.to_raw(),
348            ObjectType::SCHEDULE.to_raw(),
349            ObjectType::AVERAGING.to_raw(),
350            ObjectType::MULTI_STATE_VALUE.to_raw(),
351            ObjectType::TREND_LOG.to_raw(),
352            ObjectType::LIFE_SAFETY_POINT.to_raw(),
353            ObjectType::LIFE_SAFETY_ZONE.to_raw(),
354            ObjectType::ACCUMULATOR.to_raw(),
355            ObjectType::PULSE_CONVERTER.to_raw(),
356            ObjectType::EVENT_LOG.to_raw(),
357            ObjectType::GLOBAL_GROUP.to_raw(),
358            ObjectType::TREND_LOG_MULTIPLE.to_raw(),
359            ObjectType::LOAD_CONTROL.to_raw(),
360            ObjectType::STRUCTURED_VIEW.to_raw(),
361            ObjectType::ACCESS_DOOR.to_raw(),
362            ObjectType::TIMER.to_raw(),
363            ObjectType::ACCESS_CREDENTIAL.to_raw(),
364            ObjectType::ACCESS_POINT.to_raw(),
365            ObjectType::ACCESS_RIGHTS.to_raw(),
366            ObjectType::ACCESS_USER.to_raw(),
367            ObjectType::ACCESS_ZONE.to_raw(),
368            ObjectType::CREDENTIAL_DATA_INPUT.to_raw(),
369            ObjectType::BITSTRING_VALUE.to_raw(),
370            ObjectType::CHARACTERSTRING_VALUE.to_raw(),
371            ObjectType::DATEPATTERN_VALUE.to_raw(),
372            ObjectType::DATE_VALUE.to_raw(),
373            ObjectType::DATETIMEPATTERN_VALUE.to_raw(),
374            ObjectType::DATETIME_VALUE.to_raw(),
375            ObjectType::INTEGER_VALUE.to_raw(),
376            ObjectType::LARGE_ANALOG_VALUE.to_raw(),
377            ObjectType::OCTETSTRING_VALUE.to_raw(),
378            ObjectType::POSITIVE_INTEGER_VALUE.to_raw(),
379            ObjectType::TIMEPATTERN_VALUE.to_raw(),
380            ObjectType::TIME_VALUE.to_raw(),
381            ObjectType::NOTIFICATION_FORWARDER.to_raw(),
382            ObjectType::ALERT_ENROLLMENT.to_raw(),
383            ObjectType::CHANNEL.to_raw(),
384            ObjectType::LIGHTING_OUTPUT.to_raw(),
385            ObjectType::BINARY_LIGHTING_OUTPUT.to_raw(),
386            ObjectType::NETWORK_PORT.to_raw(),
387            ObjectType::ELEVATOR_GROUP.to_raw(),
388            ObjectType::ESCALATOR.to_raw(),
389            ObjectType::LIFT.to_raw(),
390            ObjectType::STAGING.to_raw(),
391            ObjectType::AUDIT_LOG.to_raw(),
392            ObjectType::AUDIT_REPORTER.to_raw(),
393            ObjectType::COLOR.to_raw(),
394            ObjectType::COLOR_TEMPERATURE.to_raw(),
395        ]);
396
397        Ok(Self {
398            oid,
399            recipient: Default::default(),
400            profile: ProfileState::default(),
401            properties,
402            object_list: vec![oid], // Device itself is always in the list
403            protocol_object_types_supported,
404            configured_services_supported: EXECUTED_SERVICES.to_vec(),
405            clock: None,
406        })
407    }
408
409    /// Build with application-owned Tags storage. Saved Tags override configured
410    /// Tags only while [`Self::set_profile`] provisions the row. Loaded data must
411    /// satisfy the shared Tags rules and opt-in 1 MiB snapshot limit.
412    pub fn with_tags_persistence(
413        config: DeviceConfig,
414        persistence: Arc<dyn TagsPersistence>,
415    ) -> Result<Self, Error> {
416        let mut object = Self::new(config)?;
417        object.profile = ProfileState::persistent(object.oid, persistence)?;
418        Ok(object)
419    }
420
421    /// Provision independent optional Tags, Profile_Location and Profile_Name
422    /// rows before registration. Ordinary servers permit Tags writes; an endpoint
423    /// still applies its narrower Device write allowlist. The profile text is
424    /// network read-only. Invalid configuration leaves the old profile unchanged.
425    pub fn set_profile(&mut self, profile: ObjectProfile) -> Result<(), Error> {
426        self.profile.provision(profile)
427    }
428
429    /// Wait for queued save attempts; each write reports its own outcome.
430    /// This is not a success receipt. Unstaged synchronous writes block their caller.
431    pub fn wait_for_tag_saves(&self) {
432        self.profile.wait_for_saves();
433    }
434
435    /// Update the object-list with the current database contents.
436    pub fn set_object_list(&mut self, oids: Vec<ObjectIdentifier>) {
437        self.object_list = oids;
438    }
439
440    /// Set `System_Status`, the device's reported operating state.
441    pub fn set_system_status(&mut self, status: DeviceStatus) {
442        self.properties.insert(
443            PropertyIdentifier::SYSTEM_STATUS,
444            PropertyValue::Enumerated(status.to_raw()),
445        );
446    }
447
448    /// Set `Database_Revision`, which a client compares against its cached copy to see whether
449    /// objects were created, deleted, renamed or re-identified, or a restore happened.
450    pub fn set_database_revision(&mut self, revision: u32) {
451        self.properties.insert(
452            PropertyIdentifier::DATABASE_REVISION,
453            PropertyValue::Unsigned(u64::from(revision)),
454        );
455    }
456
457    /// Name the File objects that hold this device's configuration (`Configuration_Files`).
458    /// Ignored unless the device was built with [`DeviceConfig::backup_and_restore`].
459    pub fn set_configuration_files(&mut self, files: Vec<ObjectIdentifier>) {
460        self.set_backup_property(
461            PropertyIdentifier::CONFIGURATION_FILES,
462            PropertyValue::List(
463                files
464                    .into_iter()
465                    .map(PropertyValue::ObjectIdentifier)
466                    .collect(),
467            ),
468        );
469    }
470
471    /// Set `Backup_And_Restore_State`, where the device is in a backup or restore.
472    /// Ignored unless the device was built with [`DeviceConfig::backup_and_restore`].
473    pub fn set_backup_and_restore_state(&mut self, state: BackupAndRestoreState) {
474        self.set_backup_property(
475            PropertyIdentifier::BACKUP_AND_RESTORE_STATE,
476            PropertyValue::Enumerated(state.to_raw()),
477        );
478    }
479
480    /// Set `Last_Restore_Time`, when the last restore completed.
481    /// Ignored unless the device was built with [`DeviceConfig::backup_and_restore`].
482    pub fn set_last_restore_time(&mut self, time: BACnetTimeStamp) {
483        self.set_backup_property(
484            PropertyIdentifier::LAST_RESTORE_TIME,
485            timestamp_value(&time),
486        );
487    }
488
489    fn set_backup_property(&mut self, property: PropertyIdentifier, value: PropertyValue) {
490        if let Some(current) = self.properties.get_mut(&property) {
491            *current = value;
492        }
493    }
494
495    fn write_backup_failure_timeout(
496        &mut self,
497        array_index: Option<u32>,
498        value: PropertyValue,
499    ) -> Result<(), Error> {
500        if array_index.is_some() {
501            return Err(property_is_not_an_array_error());
502        }
503        // Clause 15.9: relinquishing a non-commandable writable property
504        // succeeds without changing its value when no other error exists.
505        match value {
506            PropertyValue::Null => Ok(()),
507            PropertyValue::Unsigned(seconds) if (1..=u64::from(u16::MAX)).contains(&seconds) => {
508                self.properties.insert(
509                    PropertyIdentifier::BACKUP_FAILURE_TIMEOUT,
510                    PropertyValue::Unsigned(seconds),
511                );
512                Ok(())
513            }
514            PropertyValue::Unsigned(_) => Err(value_out_of_range_error()),
515            _ => Err(invalid_data_type_error()),
516        }
517    }
518
519    /// Replace the advertised executed-service set (`Protocol_Services_Supported`,
520    /// Clause 12.11). For deployments whose dispatch surface differs from the
521    /// bundled server's [`EXECUTED_SERVICES`]. This is a standalone declaration,
522    /// not a runtime dispatch switch. Raw COV presence follows these services;
523    /// a running server overrides served Device data with its own execution view.
524    ///
525    /// The production is closed at you-Are (bit 48); values past it are not
526    /// representable and are dropped.
527    pub fn set_services_supported(&mut self, services: &[ServiceSupported]) {
528        self.configured_services_supported = services.to_vec();
529    }
530
531    /// Get the device instance number.
532    pub fn instance(&self) -> u32 {
533        self.oid.instance_number()
534    }
535
536    /// Set the description string.
537    pub fn set_description(&mut self, desc: impl Into<String>) {
538        self.properties.insert(
539            PropertyIdentifier::DESCRIPTION,
540            PropertyValue::CharacterString(desc.into()),
541        );
542    }
543
544    /// Sync the durable Device UUID (Clause 12.11 DEVICE_UUID).
545    ///
546    /// RB-16 endpoint sync (not a fork): the caller owns the 16-byte UUID
547    /// durably (Annex AB.1.5.3); this setter only stores the supplied bytes
548    /// as an OctetString. No generation, no persistence, no version/variant
549    /// checks — startup validation elsewhere rejects only all-zero where a
550    /// real SC identity is required.
551    pub fn set_device_uuid(&mut self, uuid: [u8; 16]) {
552        self.properties.insert(
553            PropertyIdentifier::DEVICE_UUID,
554            PropertyValue::OctetString(uuid.to_vec()),
555        );
556    }
557
558    fn clock_frame(&self) -> Option<ClockFrame> {
559        self.clock.as_ref()?.read_clock()
560    }
561
562    fn cov_property_present(&self, property: PropertyIdentifier) -> bool {
563        match property {
564            PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS => {
565                self.configured_services_supported.iter().any(|service| {
566                    matches!(
567                        *service,
568                        ServiceSupported::SUBSCRIBE_COV | ServiceSupported::SUBSCRIBE_COV_PROPERTY
569                    )
570                })
571            }
572            PropertyIdentifier::ACTIVE_COV_MULTIPLE_SUBSCRIPTIONS => self
573                .configured_services_supported
574                .contains(&ServiceSupported::SUBSCRIBE_COV_PROPERTY_MULTIPLE),
575            _ => true,
576        }
577    }
578
579    fn services_supported(&self) -> Vec<u8> {
580        let clock_available = self.clock_frame().is_some();
581        let services = self
582            .configured_services_supported
583            .iter()
584            .copied()
585            .filter(|service| {
586                clock_available
587                    || (*service != ServiceSupported::TIME_SYNCHRONIZATION
588                        && *service != ServiceSupported::UTC_TIME_SYNCHRONIZATION)
589            })
590            .collect::<Vec<_>>();
591        compute_services_supported(&services)
592    }
593}
594
595impl BACnetObject for DeviceObject {
596    fn device_authority_internal(&mut self) -> Option<DeviceAuthority<'_>> {
597        Some(DeviceAuthority(self))
598    }
599
600    fn object_identifier(&self) -> ObjectIdentifier {
601        self.oid
602    }
603
604    fn object_name(&self) -> &str {
605        match self.properties.get(&PropertyIdentifier::OBJECT_NAME) {
606            Some(PropertyValue::CharacterString(s)) => s,
607            _ => "Unknown",
608        }
609    }
610
611    fn read_property(
612        &self,
613        property: PropertyIdentifier,
614        array_index: Option<u32>,
615    ) -> Result<PropertyValue, Error> {
616        if property == PropertyIdentifier::AUDIT_NOTIFICATION_RECIPIENT {
617            return self.read_audit_recipient(array_index);
618        }
619        if property == PropertyIdentifier::OBJECT_LIST {
620            return match array_index {
621                None => {
622                    let elements = self
623                        .object_list
624                        .iter()
625                        .map(|oid| PropertyValue::ObjectIdentifier(*oid))
626                        .collect();
627                    Ok(PropertyValue::List(elements))
628                }
629                Some(0) => {
630                    // Index 0 = array length per BACnet convention
631                    Ok(PropertyValue::Unsigned(self.object_list.len() as u64))
632                }
633                Some(idx) => {
634                    let i = (idx - 1) as usize; // BACnet arrays are 1-based
635                    if i < self.object_list.len() {
636                        Ok(PropertyValue::ObjectIdentifier(self.object_list[i]))
637                    } else {
638                        Err(Error::Protocol {
639                            class: ErrorClass::PROPERTY,
640                            code: ErrorCode::INVALID_ARRAY_INDEX,
641                        })
642                    }
643                }
644            };
645        }
646
647        if property == PropertyIdentifier::PROPERTY_LIST {
648            return read_property_list_property(&self.property_list(), array_index);
649        }
650
651        if property == PropertyIdentifier::CONFIGURATION_FILES {
652            if let Some(PropertyValue::List(files)) = self.properties.get(&property) {
653                return read_array(files.clone(), array_index);
654            }
655        }
656
657        if matches!(
658            property,
659            PropertyIdentifier::LOCAL_DATE
660                | PropertyIdentifier::LOCAL_TIME
661                | PropertyIdentifier::UTC_OFFSET
662                | PropertyIdentifier::DAYLIGHT_SAVINGS_STATUS
663        ) {
664            let frame = self.clock_frame().ok_or(Error::Protocol {
665                class: ErrorClass::PROPERTY,
666                code: ErrorCode::UNKNOWN_PROPERTY,
667            })?;
668            return Ok(match property {
669                PropertyIdentifier::LOCAL_DATE => PropertyValue::Date(frame.local_date),
670                PropertyIdentifier::LOCAL_TIME => PropertyValue::Time(frame.local_time),
671                PropertyIdentifier::UTC_OFFSET => {
672                    PropertyValue::Signed(i32::from(frame.utc_offset))
673                }
674                PropertyIdentifier::DAYLIGHT_SAVINGS_STATUS => {
675                    PropertyValue::Boolean(frame.daylight_savings_status)
676                }
677                _ => unreachable!(),
678            });
679        }
680
681        if property == PropertyIdentifier::PROTOCOL_OBJECT_TYPES_SUPPORTED {
682            let num_bytes = self.protocol_object_types_supported.len();
683            let total_bits = num_bytes * 8;
684            // Find highest set bit to determine actual used bits
685            let mut max_type = 0u32;
686            for (byte_idx, &byte) in self.protocol_object_types_supported.iter().enumerate() {
687                for bit in 0..8 {
688                    if byte & (1 << (7 - bit)) != 0 {
689                        max_type = (byte_idx * 8 + bit) as u32;
690                    }
691                }
692            }
693            let used_bits = max_type as usize + 1;
694            let unused = (total_bits - used_bits) as u8;
695            return Ok(PropertyValue::BitString {
696                unused_bits: unused,
697                data: self.protocol_object_types_supported.clone(),
698            });
699        }
700
701        if property == PropertyIdentifier::PROTOCOL_SERVICES_SUPPORTED {
702            let services_supported = self.services_supported();
703            let unused = (services_supported.len() * 8 - SERVICES_SUPPORTED_BITS) as u8;
704            return Ok(PropertyValue::BitString {
705                unused_bits: unused,
706                data: services_supported,
707            });
708        }
709
710        if matches!(
711            property,
712            PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS
713                | PropertyIdentifier::ACTIVE_COV_MULTIPLE_SUBSCRIPTIONS
714        ) {
715            // Standalone default: the object holds no subscription state. A
716            // running `BACnetServer` owns both live lists in its COV
717            // subscription table and projects them for network reads and
718            // `read_local`.
719            return if self.cov_property_present(property) {
720                Ok(PropertyValue::ApplicationData(Vec::new()))
721            } else {
722                Err(Error::Protocol {
723                    class: ErrorClass::PROPERTY,
724                    code: ErrorCode::UNKNOWN_PROPERTY,
725                })
726            };
727        }
728
729        if let Some(value) = self.profile.read(property, array_index) {
730            return value;
731        }
732        self.properties
733            .get(&property)
734            .cloned()
735            .ok_or(Error::Protocol {
736                class: ErrorClass::PROPERTY,
737                code: ErrorCode::UNKNOWN_PROPERTY,
738            })
739    }
740
741    fn write_property(
742        &mut self,
743        property: PropertyIdentifier,
744        array_index: Option<u32>,
745        value: PropertyValue,
746        _priority: Option<u8>,
747    ) -> Result<(), Error> {
748        if property == PropertyIdentifier::AUDIT_NOTIFICATION_RECIPIENT {
749            return self.write_audit_recipient(array_index, value, _priority, None);
750        }
751        if let Some(result) = self.profile.write(property, array_index, &value) {
752            return result;
753        }
754        if property == PropertyIdentifier::DESCRIPTION {
755            if array_index.is_some() {
756                return Err(Error::Protocol {
757                    class: ErrorClass::PROPERTY,
758                    code: ErrorCode::PROPERTY_IS_NOT_AN_ARRAY,
759                });
760            }
761            // Clause 15.9: relinquishing a non-commandable writable property
762            // succeeds without changing its value when no other error exists.
763            if value == PropertyValue::Null {
764                return Ok(());
765            }
766            if let PropertyValue::CharacterString(_) = &value {
767                self.properties.insert(property, value);
768                return Ok(());
769            }
770            return Err(Error::Protocol {
771                class: ErrorClass::PROPERTY,
772                code: ErrorCode::INVALID_DATA_TYPE,
773            });
774        }
775        if property == PropertyIdentifier::BACKUP_FAILURE_TIMEOUT
776            && self.properties.contains_key(&property)
777        {
778            return self.write_backup_failure_timeout(array_index, value);
779        }
780        Err(crate::common::unhandled_write_error(
781            self.property_metadata().as_ref(),
782            property,
783            array_index,
784        ))
785    }
786
787    fn durable_writes_internal(&mut self) -> Option<&mut dyn crate::durable::DurableWrites> {
788        self.profile.capability()
789    }
790
791    fn advance_monotonic_time_internal(&mut self, now: std::time::Duration) -> bool {
792        self.profile.expire(now);
793        false
794    }
795
796    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
797        metadata::for_object(self)
798    }
799
800    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
801        // Preserve Device's legacy inclusion of PROPERTY_LIST. The wire reader
802        // still removes it and the identity rows as before.
803        Cow::Owned(
804            self.property_metadata()
805                .iter()
806                .map(|row| row.property_identifier)
807                .collect(),
808        )
809    }
810
811    /// Device is not createable or deleteable at runtime.
812    fn is_createable(&self) -> bool {
813        false
814    }
815    fn is_deleteable(&self) -> bool {
816        false
817    }
818
819    fn bind_clock_internal(&mut self, clock: Option<Arc<dyn ClockReader>>) {
820        self.clock = clock;
821    }
822}
823
824#[cfg(test)]
825mod tests;
826
827#[cfg(test)]
828mod service_profile_tests;