bacnet-server 0.15.0

BACnet server: APDU dispatch and service handlers
Documentation
use super::*;
use bacnet_objects::binary::BinaryValueObject;
use bacnet_objects::event_enrollment::EventEnrollmentObject;
use bacnet_objects::traits::BACnetObject;
use bacnet_services::wpm::WritePropertyMultipleError;
use bacnet_types::enums::EventType;

fn malformed_indexed_value_after_description(oid: ObjectIdentifier, description: &str) -> BytesMut {
    let mut encoded_description = BytesMut::new();
    bacnet_encoding::primitives::encode_property_value(
        &mut encoded_description,
        &PropertyValue::CharacterString(description.into()),
    )
    .unwrap();
    let request = WritePropertyMultipleRequest {
        list_of_write_access_specs: vec![WriteAccessSpecification {
            object_identifier: oid,
            list_of_properties: vec![BACnetPropertyValue {
                property_identifier: PropertyIdentifier::DESCRIPTION,
                property_array_index: None,
                value: encoded_description.to_vec(),
                priority: None,
            }],
        }],
    };
    let mut encoded = BytesMut::new();
    request.encode(&mut encoded).unwrap();
    assert_eq!(encoded.last(), Some(&0x1f));
    encoded.truncate(encoded.len() - 1);
    bacnet_encoding::primitives::encode_ctx_unsigned(
        &mut encoded,
        0,
        PropertyIdentifier::OBJECT_TYPE.to_raw() as u64,
    );
    bacnet_encoding::primitives::encode_ctx_unsigned(&mut encoded, 1, 4);
    encoded
}

#[tokio::test]
async fn event_enrollment_prefix_commit_returns_exact_error_through_server_dispatch() {
    let mut db = clocked_test_database();
    let object = EventEnrollmentObject::new(7, "dispatch-ee", EventType::OUT_OF_RANGE).unwrap();
    let oid = object.object_identifier();
    db.add(Box::new(object)).unwrap();
    let fixture = DispatchFixture::new(db, std::iter::empty()).await;

    let mut description = BytesMut::new();
    bacnet_encoding::primitives::encode_property_value(
        &mut description,
        &PropertyValue::CharacterString("committed through dispatch".into()),
    )
    .unwrap();
    let mut acked = BytesMut::new();
    bacnet_encoding::primitives::encode_property_value(
        &mut acked,
        &PropertyValue::BitString {
            unused_bits: 5,
            data: vec![0xe0],
        },
    )
    .unwrap();
    let request = WritePropertyMultipleRequest {
        list_of_write_access_specs: vec![WriteAccessSpecification {
            object_identifier: oid,
            list_of_properties: vec![
                BACnetPropertyValue {
                    property_identifier: PropertyIdentifier::DESCRIPTION,
                    property_array_index: None,
                    value: description.to_vec(),
                    priority: None,
                },
                BACnetPropertyValue {
                    property_identifier: PropertyIdentifier::ACKED_TRANSITIONS,
                    property_array_index: None,
                    value: acked.to_vec(),
                    priority: None,
                },
            ],
        }],
    };
    let mut encoded = BytesMut::new();
    request.encode(&mut encoded).unwrap();

    fixture
        .dispatch(
            0x43,
            ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE,
            encoded.freeze(),
        )
        .await;

    let apdus = fixture.take_apdus();
    assert_eq!(
        apdus.len(),
        1,
        "the no-subscription fixture emits only Result(-)"
    );
    let Apdu::Error(error_pdu) = &apdus[0] else {
        panic!("expected service-16 Error, got {:?}", apdus[0]);
    };
    assert_eq!(
        error_pdu.service_choice,
        ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE
    );
    assert_eq!(error_pdu.error_class, ErrorClass::PROPERTY);
    assert_eq!(error_pdu.error_code, ErrorCode::WRITE_ACCESS_DENIED);
    let formal = WritePropertyMultipleError::from_error_pdu(error_pdu).unwrap();
    assert_eq!(formal.first_failed_write_attempt.object_identifier, oid);
    assert_eq!(
        formal.first_failed_write_attempt.property_identifier,
        PropertyIdentifier::ACKED_TRANSITIONS.to_raw()
    );
    assert_eq!(formal.first_failed_write_attempt.property_array_index, None);
    assert_eq!(
        fixture
            .db
            .read()
            .await
            .get(&oid)
            .unwrap()
            .read_property(PropertyIdentifier::DESCRIPTION, None)
            .unwrap(),
        PropertyValue::CharacterString("committed through dispatch".into())
    );
}

#[tokio::test]
async fn malformed_value_after_prefix_returns_exact_formal_error_through_dispatch() {
    let mut db = clocked_test_database();
    let object = BinaryValueObject::new(8, "malformed-prefix").unwrap();
    let oid = object.object_identifier();
    db.add(Box::new(object)).unwrap();
    let fixture = DispatchFixture::new(db, std::iter::empty()).await;
    let description = "prefix survives malformed value";
    let encoded = malformed_indexed_value_after_description(oid, description);

    fixture
        .dispatch(
            0x46,
            ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE,
            encoded.freeze(),
        )
        .await;

    let apdus = fixture.take_apdus();
    assert_eq!(apdus.len(), 1);
    let Apdu::Error(error_pdu) = &apdus[0] else {
        panic!("expected service-16 Error, got {:?}", apdus[0]);
    };
    assert_eq!(
        error_pdu.service_choice,
        ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE
    );
    assert_eq!(error_pdu.error_class, ErrorClass::SERVICES);
    assert_eq!(error_pdu.error_code, ErrorCode::INVALID_TAG);
    let formal = WritePropertyMultipleError::from_error_pdu(error_pdu).unwrap();
    assert_eq!(formal.first_failed_write_attempt.object_identifier, oid);
    assert_eq!(
        formal.first_failed_write_attempt.property_identifier,
        PropertyIdentifier::OBJECT_TYPE.to_raw()
    );
    assert_eq!(
        formal.first_failed_write_attempt.property_array_index,
        Some(4)
    );
    assert_eq!(
        fixture
            .db
            .read()
            .await
            .get(&oid)
            .unwrap()
            .read_property(PropertyIdentifier::DESCRIPTION, None)
            .unwrap(),
        PropertyValue::CharacterString(description.into())
    );
}

#[tokio::test]
async fn failed_wpm_sends_formal_error_before_committed_prefix_cov() {
    let fixture = DispatchFixture::new(
        life_safety_db(),
        [
            subscription(
                Some(PropertyIdentifier::OPERATION_EXPECTED),
                CovNotificationKind::Single,
                1,
            ),
            subscription(None, CovNotificationKind::Single, 2),
        ],
    )
    .await;
    let mut boolean = BytesMut::new();
    bacnet_encoding::primitives::encode_property_value(&mut boolean, &PropertyValue::Boolean(true))
        .unwrap();
    let mut enumerated = BytesMut::new();
    bacnet_encoding::primitives::encode_property_value(
        &mut enumerated,
        &PropertyValue::Enumerated(ObjectType::LIFE_SAFETY_POINT.to_raw()),
    )
    .unwrap();
    let request = WritePropertyMultipleRequest {
        list_of_write_access_specs: vec![WriteAccessSpecification {
            object_identifier: point_oid(),
            list_of_properties: vec![
                BACnetPropertyValue {
                    property_identifier: PropertyIdentifier::OUT_OF_SERVICE,
                    property_array_index: None,
                    value: boolean.to_vec(),
                    priority: None,
                },
                BACnetPropertyValue {
                    property_identifier: PropertyIdentifier::OBJECT_TYPE,
                    property_array_index: None,
                    value: enumerated.to_vec(),
                    priority: None,
                },
            ],
        }],
    };
    let mut encoded = BytesMut::new();
    request.encode(&mut encoded).unwrap();

    fixture
        .dispatch(
            0x44,
            ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE,
            encoded.freeze(),
        )
        .await;

    let apdus = fixture.take_apdus();
    assert_eq!(apdus.len(), 3);
    let Apdu::Error(error_pdu) = &apdus[0] else {
        panic!("confirmed WPM Result(-) must precede notifications");
    };
    assert_eq!(error_pdu.error_class, ErrorClass::PROPERTY);
    assert_eq!(error_pdu.error_code, ErrorCode::WRITE_ACCESS_DENIED);
    let formal = WritePropertyMultipleError::from_error_pdu(error_pdu).unwrap();
    assert_eq!(
        formal.first_failed_write_attempt.object_identifier,
        point_oid()
    );
    assert_eq!(
        formal.first_failed_write_attempt.property_identifier,
        PropertyIdentifier::OBJECT_TYPE.to_raw()
    );
    assert_eq!(formal.first_failed_write_attempt.property_array_index, None);
    let payloads: Vec<_> = apdus[1..].iter().map(single_properties).collect();
    assert!(payloads.contains(&vec![
        PropertyIdentifier::OPERATION_EXPECTED,
        PropertyIdentifier::STATUS_FLAGS,
    ]));
    assert!(payloads.contains(&vec![
        PropertyIdentifier::PRESENT_VALUE,
        PropertyIdentifier::STATUS_FLAGS,
    ]));
    assert_eq!(
        fixture
            .db
            .read()
            .await
            .get(&point_oid())
            .unwrap()
            .read_property(PropertyIdentifier::OUT_OF_SERVICE, None)
            .unwrap(),
        PropertyValue::Boolean(true)
    );
}

#[tokio::test]
async fn failed_wpm_sends_generic_cov_for_non_life_safety_prefix_only() {
    let mut db = clocked_test_database();
    let object = BinaryValueObject::new(1, "binary").unwrap();
    let oid = object.object_identifier();
    db.add(Box::new(object)).unwrap();
    let fixture = DispatchFixture::new(
        db,
        [CovSubscription {
            subscriber_mac: MacAddr::from_slice(&[127, 0, 0, 1, 0xBA, 3]),
            subscriber_network: None,
            subscriber_process_identifier: 3,
            monitored_object_identifier: oid,
            issue_confirmed_notifications: false,
            expires_at: None,
            last_notified_observation: None,
            monitored_property: None,
            monitored_property_array_index: None,
            cov_increment: None,
            notification_kind: CovNotificationKind::Single,
            timestamped: false,
        }],
    )
    .await;
    let mut on = BytesMut::new();
    bacnet_encoding::primitives::encode_property_value(&mut on, &PropertyValue::Enumerated(1))
        .unwrap();
    let request = WritePropertyMultipleRequest {
        list_of_write_access_specs: vec![WriteAccessSpecification {
            object_identifier: oid,
            list_of_properties: vec![
                BACnetPropertyValue {
                    property_identifier: PropertyIdentifier::PRESENT_VALUE,
                    property_array_index: None,
                    value: on.to_vec(),
                    priority: Some(8),
                },
                BACnetPropertyValue {
                    property_identifier: PropertyIdentifier::OBJECT_TYPE,
                    property_array_index: None,
                    value: on.to_vec(),
                    priority: None,
                },
            ],
        }],
    };
    let mut encoded = BytesMut::new();
    request.encode(&mut encoded).unwrap();

    fixture
        .dispatch(
            0x45,
            ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE,
            encoded.freeze(),
        )
        .await;

    let apdus = fixture.take_apdus();
    assert_eq!(apdus.len(), 2, "one Error and one committed-prefix COV");
    assert!(matches!(apdus[0], Apdu::Error(_)));
    assert_eq!(
        single_properties(&apdus[1]),
        vec![
            PropertyIdentifier::PRESENT_VALUE,
            PropertyIdentifier::STATUS_FLAGS,
        ]
    );
}