bacnet-server 0.15.0

BACnet server: APDU dispatch and service handlers
Documentation
use super::*;
use bacnet_objects::{
    access_control::AccessDoorObject, file::FileObject, network_port::NetworkPortObject,
};

fn write(
    property: PropertyIdentifier,
    value: PropertyValue,
    priority: Option<u8>,
) -> BACnetPropertyValue {
    BACnetPropertyValue {
        property_identifier: property,
        property_array_index: None,
        value: encode_value(&value),
        priority,
    }
}

fn prefix_failure(
    db: &mut ObjectDatabase,
    oid: ObjectIdentifier,
    mut prefix: Vec<BACnetPropertyValue>,
    suffix: BACnetPropertyValue,
) {
    prefix.push(write(
        PropertyIdentifier::OBJECT_IDENTIFIER,
        PropertyValue::ObjectIdentifier(oid),
        None,
    ));
    prefix.push(suffix);
    let WritePropertyMultipleOutcome::Error {
        error,
        first_failed_write_attempt,
        committed_oids,
    } = detailed(db, &encode_request(oid, prefix))
    else {
        panic!("expected WPM failure")
    };
    assert_protocol(error, ErrorClass::PROPERTY, ErrorCode::WRITE_ACCESS_DENIED);
    assert_reference(
        &first_failed_write_attempt,
        oid,
        PropertyIdentifier::OBJECT_IDENTIFIER,
        None,
    );
    assert_eq!(committed_oids, vec![oid]);
}

fn read(
    db: &ObjectDatabase,
    oid: ObjectIdentifier,
    property: PropertyIdentifier,
    index: Option<u32>,
) -> PropertyValue {
    db.get(&oid)
        .unwrap()
        .read_property(property, index)
        .unwrap()
}

#[test]
fn wpm_file_resize_and_network_configuration_keep_committed_state() {
    let mut db = ObjectDatabase::new();
    let mut file = FileObject::new(1, "File", "binary").unwrap();
    file.set_data(vec![1, 2, 3]);
    let file_oid = file.object_identifier();
    db.add(Box::new(file)).unwrap();
    prefix_failure(
        &mut db,
        file_oid,
        vec![write(
            PropertyIdentifier::FILE_SIZE,
            PropertyValue::Unsigned(2),
            None,
        )],
        write(
            PropertyIdentifier::FILE_SIZE,
            PropertyValue::Unsigned(1),
            None,
        ),
    );
    assert_eq!(
        read(&db, file_oid, PropertyIdentifier::FILE_SIZE, None),
        PropertyValue::Unsigned(2)
    );
    assert_eq!(
        db.get(&file_oid)
            .unwrap()
            .file_storage_internal()
            .unwrap()
            .read_stream(0, 10)
            .unwrap()
            .data,
        vec![1, 2]
    );
    assert_eq!(
        read(&db, file_oid, PropertyIdentifier::ARCHIVE, None),
        PropertyValue::Boolean(false)
    );

    let mut port = NetworkPortObject::new_bip(1, "Port", Default::default()).unwrap();
    let port_oid = port.object_identifier();
    let before = port
        .read_property(PropertyIdentifier::BACNET_IP_UDP_PORT, None)
        .unwrap();
    assert!(
        port.write_property(
            PropertyIdentifier::BACNET_IP_UDP_PORT,
            None,
            PropertyValue::Unsigned(65536),
            None
        )
        .is_err()
    );
    assert_eq!(
        port.read_property(PropertyIdentifier::BACNET_IP_UDP_PORT, None)
            .unwrap(),
        before
    );
    assert_eq!(
        port.read_property(PropertyIdentifier::CHANGES_PENDING, None)
            .unwrap(),
        PropertyValue::Boolean(false)
    );
    db.add(Box::new(port)).unwrap();
    let outcome = detailed(
        &mut db,
        &encode_request(
            port_oid,
            vec![
                write(
                    PropertyIdentifier::DESCRIPTION,
                    PropertyValue::CharacterString("committed".into()),
                    None,
                ),
                write(
                    PropertyIdentifier::IP_ADDRESS,
                    PropertyValue::OctetString(vec![10, 0, 0, 1]),
                    None,
                ),
                write(
                    PropertyIdentifier::DESCRIPTION,
                    PropertyValue::CharacterString("suffix".into()),
                    None,
                ),
            ],
        ),
    );
    let WritePropertyMultipleOutcome::Error {
        error,
        first_failed_write_attempt,
        committed_oids,
    } = outcome
    else {
        panic!("expected readonly config failure")
    };
    assert_protocol(error, ErrorClass::PROPERTY, ErrorCode::WRITE_ACCESS_DENIED);
    assert_reference(
        &first_failed_write_attempt,
        port_oid,
        PropertyIdentifier::IP_ADDRESS,
        None,
    );
    assert_eq!(committed_oids, vec![port_oid]);
    assert_eq!(
        read(&db, port_oid, PropertyIdentifier::DESCRIPTION, None),
        PropertyValue::CharacterString("committed".into())
    );
    assert_eq!(
        read(&db, port_oid, PropertyIdentifier::IP_ADDRESS, None),
        PropertyValue::OctetString(vec![0; 4])
    );
    assert_eq!(
        read(&db, port_oid, PropertyIdentifier::CHANGES_PENDING, None),
        PropertyValue::Boolean(false)
    );
}

#[test]
fn wpm_command_priority_and_fallback_remain_owned_by_committed_prefix() {
    let objects: Vec<(
        Box<dyn BACnetObject>,
        PropertyValue,
        PropertyValue,
        PropertyValue,
    )> = vec![
        (
            Box::new(MultiStateOutputObject::new(1, "MSO", 3).unwrap()),
            PropertyValue::Unsigned(3),
            PropertyValue::Unsigned(2),
            PropertyValue::Unsigned(1),
        ),
        (
            Box::new(MultiStateValueObject::new(1, "MSV", 3).unwrap()),
            PropertyValue::Unsigned(3),
            PropertyValue::Unsigned(2),
            PropertyValue::Unsigned(1),
        ),
        (
            Box::new(AccessDoorObject::new(1, "Door").unwrap()),
            PropertyValue::Enumerated(1),
            PropertyValue::Enumerated(2),
            PropertyValue::Enumerated(0),
        ),
    ];
    for (object, fallback, commanded, suffix) in objects {
        let mut db = ObjectDatabase::new();
        let oid = object.object_identifier();
        db.add(object).unwrap();
        prefix_failure(
            &mut db,
            oid,
            vec![
                write(
                    PropertyIdentifier::RELINQUISH_DEFAULT,
                    fallback.clone(),
                    None,
                ),
                write(
                    PropertyIdentifier::PRESENT_VALUE,
                    commanded.clone(),
                    Some(8),
                ),
            ],
            write(PropertyIdentifier::PRESENT_VALUE, suffix, Some(1)),
        );
        assert_eq!(
            read(&db, oid, PropertyIdentifier::PRESENT_VALUE, None),
            commanded
        );
        assert_eq!(
            read(&db, oid, PropertyIdentifier::PRIORITY_ARRAY, Some(8)),
            commanded
        );
        assert_eq!(
            read(&db, oid, PropertyIdentifier::PRIORITY_ARRAY, Some(1)),
            PropertyValue::Null
        );
        assert_eq!(
            read(&db, oid, PropertyIdentifier::RELINQUISH_DEFAULT, None),
            fallback
        );
        db.get_mut(&oid)
            .unwrap()
            .write_property_from(
                PropertyIdentifier::PRESENT_VALUE,
                None,
                PropertyValue::Null,
                Some(8),
                &crate::command_source::test_origin(),
            )
            .unwrap();
        assert_eq!(
            read(&db, oid, PropertyIdentifier::PRESENT_VALUE, None),
            fallback
        );
    }
}