bacnet-objects 0.12.0

BACnet object model: traits, database, and standard object types
Documentation
use super::DeviceObject;
use std::borrow::Cow;

use bacnet_types::enums::PropertyIdentifier as P;

use crate::property_metadata::{
    PropertyConformance::{Optional, RequiredRead},
    PropertyMetadata,
    PropertyWriteCapability::{Always, ReadOnly},
};

// The full implemented set, ordered by raw identifier. Conditional rows keep
// their optional base code; effective sets below select only readable rows.
const BASE: &[PropertyMetadata] = &[
    PropertyMetadata::new(P::APDU_TIMEOUT, RequiredRead, None, ReadOnly),
    PropertyMetadata::new(
        P::APPLICATION_SOFTWARE_VERSION,
        RequiredRead,
        None,
        ReadOnly,
    ),
    PropertyMetadata::new(P::DAYLIGHT_SAVINGS_STATUS, Optional, None, ReadOnly),
    PropertyMetadata::new(P::DESCRIPTION, Optional, None, Always),
    PropertyMetadata::new(P::DEVICE_ADDRESS_BINDING, RequiredRead, None, ReadOnly),
    PropertyMetadata::new(P::FIRMWARE_REVISION, RequiredRead, None, ReadOnly),
    PropertyMetadata::new(P::LOCAL_DATE, Optional, None, ReadOnly),
    PropertyMetadata::new(P::LOCAL_TIME, Optional, None, ReadOnly),
    PropertyMetadata::new(P::MAX_APDU_LENGTH_ACCEPTED, RequiredRead, None, ReadOnly),
    PropertyMetadata::new(P::MODEL_NAME, RequiredRead, None, ReadOnly),
    PropertyMetadata::new(P::NUMBER_OF_APDU_RETRIES, RequiredRead, None, ReadOnly),
    PropertyMetadata::new(P::OBJECT_IDENTIFIER, RequiredRead, None, ReadOnly),
    PropertyMetadata::new(P::OBJECT_LIST, RequiredRead, None, ReadOnly),
    PropertyMetadata::new(P::OBJECT_NAME, RequiredRead, None, ReadOnly),
    PropertyMetadata::new(P::OBJECT_TYPE, RequiredRead, None, ReadOnly),
    PropertyMetadata::new(
        P::PROTOCOL_OBJECT_TYPES_SUPPORTED,
        RequiredRead,
        None,
        ReadOnly,
    ),
    PropertyMetadata::new(P::PROTOCOL_SERVICES_SUPPORTED, RequiredRead, None, ReadOnly),
    PropertyMetadata::new(P::PROTOCOL_VERSION, RequiredRead, None, ReadOnly),
    PropertyMetadata::new(P::SEGMENTATION_SUPPORTED, RequiredRead, None, ReadOnly),
    PropertyMetadata::new(P::SYSTEM_STATUS, RequiredRead, None, ReadOnly),
    PropertyMetadata::new(P::UTC_OFFSET, Optional, None, ReadOnly),
    PropertyMetadata::new(P::VENDOR_IDENTIFIER, RequiredRead, None, ReadOnly),
    PropertyMetadata::new(P::VENDOR_NAME, RequiredRead, None, ReadOnly),
    PropertyMetadata::new(P::PROTOCOL_REVISION, RequiredRead, None, ReadOnly),
    PropertyMetadata::new(P::ACTIVE_COV_SUBSCRIPTIONS, Optional, None, ReadOnly),
    PropertyMetadata::new(P::DATABASE_REVISION, RequiredRead, None, ReadOnly),
    PropertyMetadata::new(P::MAX_SEGMENTS_ACCEPTED, Optional, None, ReadOnly),
    PropertyMetadata::new(P::LAST_RESTART_REASON, Optional, None, ReadOnly),
    PropertyMetadata::new(P::PROPERTY_LIST, RequiredRead, None, ReadOnly),
    // Table 12-13 footnote 18: included below only for a declared Multiple
    // executor. The standalone value is empty; runtime views own live state.
    PropertyMetadata::new(
        P::ACTIVE_COV_MULTIPLE_SUBSCRIPTIONS,
        Optional,
        None,
        ReadOnly,
    ),
    PropertyMetadata::new(P::DEVICE_UUID, Optional, None, ReadOnly),
];

// Precompute clock/segment base sets without caching clock availability.
// Declared-service filtering below allocates only for narrower COV profiles.
const fn effective<const N: usize>(clock: bool, segments: bool) -> [PropertyMetadata; N] {
    let mut rows = [BASE[0]; N];
    let mut source = 0;
    let mut target = 0;
    while source < BASE.len() {
        let property = BASE[source].property_identifier;
        let clock_row = matches!(
            property,
            P::LOCAL_DATE | P::LOCAL_TIME | P::UTC_OFFSET | P::DAYLIGHT_SAVINGS_STATUS
        );
        let segment_row = matches!(property, P::MAX_SEGMENTS_ACCEPTED);
        if (!clock_row || clock) && (!segment_row || segments) {
            rows[target] = BASE[source];
            target += 1;
        }
        source += 1;
    }
    assert!(target == N);
    rows
}

const CLOCKLESS: &[PropertyMetadata] = &effective::<26>(false, false);
const CLOCKLESS_SEGMENTED: &[PropertyMetadata] = &effective::<27>(false, true);
const CLOCKED: &[PropertyMetadata] = &effective::<30>(true, false);

pub(super) fn for_object(object: &DeviceObject) -> Cow<'_, [PropertyMetadata]> {
    let clock = object.clock_frame().is_some();
    let segments = object.properties.contains_key(&P::MAX_SEGMENTS_ACCEPTED);
    let base = match (clock, segments) {
        (false, false) => CLOCKLESS,
        (false, true) => CLOCKLESS_SEGMENTED,
        (true, false) => CLOCKED,
        (true, true) => BASE,
    };
    let mut rows = if base
        .iter()
        .all(|row| object.cov_property_present(row.property_identifier))
    {
        Cow::Borrowed(base)
    } else {
        Cow::Owned(
            base.iter()
                .copied()
                .filter(|row| object.cov_property_present(row.property_identifier))
                .collect(),
        )
    };
    if object.audit_recipient_present() {
        rows.to_mut().push(PropertyMetadata::new(
            P::AUDIT_NOTIFICATION_RECIPIENT,
            Optional,
            Some(crate::property_metadata::PropertyPresenceCondition::AuditReporting),
            Always,
        ));
    }
    rows
}