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