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