use super::group::{ExpectedRead, assert_cases};
use super::*;
use PropertyIdentifier as P;
use bacnet_objects::analog::{AnalogInputObject, AnalogValueObject};
use bacnet_objects::group::GroupObject;
use bacnet_objects::traits::BACnetObject;
use bacnet_services::read_range::{RangeSpec, ReadRangeAck, ReadRangeRequest};
use bacnet_types::constructed::{PropertyReference, ReadAccessSpecification};
use bacnet_types::enums::EngineeringUnits;
use bacnet_types::primitives::PropertyValue;
fn oid(object_type: ObjectType, instance: u32) -> ObjectIdentifier {
ObjectIdentifier::new(object_type, instance).unwrap()
}
fn member(
object_identifier: ObjectIdentifier,
references: &[(P, Option<u32>)],
) -> ReadAccessSpecification {
ReadAccessSpecification {
object_identifier,
list_of_property_references: references
.iter()
.map(
|&(property_identifier, property_array_index)| PropertyReference {
property_identifier,
property_array_index,
},
)
.collect(),
}
}
fn database(members: Vec<ReadAccessSpecification>) -> (ObjectDatabase, ObjectIdentifier) {
let mut input = AnalogInputObject::new(1, "AI-1", EngineeringUnits::DEGREES_CELSIUS).unwrap();
input.set_present_value(21.5);
let mut group = GroupObject::new(7, "GRP-7").unwrap();
for member in members {
group.add_member(member).unwrap();
}
let group_oid = group.object_identifier();
let mut db = ObjectDatabase::new();
db.add(Box::new(input)).unwrap();
db.add(Box::new(
AnalogValueObject::new(2, "AV-2", EngineeringUnits::DEGREES_CELSIUS).unwrap(),
))
.unwrap();
db.add(Box::new(group)).unwrap();
(db, group_oid)
}
fn pinned_members() -> Vec<ReadAccessSpecification> {
vec![
member(
oid(ObjectType::ANALOG_INPUT, 1),
&[(P::PRESENT_VALUE, None), (P::STATUS_FLAGS, None)],
),
member(
oid(ObjectType::ANALOG_INPUT, 1),
&[(P::PRIORITY_ARRAY, None)],
),
member(
oid(ObjectType::ANALOG_INPUT, 9),
&[(P::PRESENT_VALUE, None)],
),
member(
oid(ObjectType::ANALOG_VALUE, 2),
&[(P::PRIORITY_ARRAY, Some(16))],
),
member(
oid(ObjectType::ANALOG_INPUT, 1),
&[(P::PRESENT_VALUE, Some(1))],
),
]
}
const MEMBERS: &[u8] = &[
0x0C, 0, 0, 0, 1, 0x1E, 0x09, 85, 0x09, 111, 0x1F, 0x0C, 0, 0, 0, 1, 0x1E, 0x09, 87, 0x1F, 0x0C, 0, 0, 0, 9, 0x1E, 0x09, 85, 0x1F, 0x0C, 0, 0x80, 0, 2, 0x1E, 0x09, 87, 0x19, 16, 0x1F, 0x0C, 0, 0, 0, 1, 0x1E, 0x09, 85, 0x19, 1, 0x1F, ];
const PRESENT_VALUE: &[u8] = &[
0x0C, 0, 0, 0, 1, 0x1E, 0x29, 85, 0x4E, 0x44, 0x41, 0xAC, 0, 0, 0x4F, 0x29, 111, 0x4E, 0x82, 4, 0, 0x4F, 0x1F, 0x0C, 0, 0, 0, 1, 0x1E, 0x29, 87, 0x5E, 0x91, 2, 0x91, 32, 0x5F, 0x1F, 0x0C, 0, 0, 0, 9, 0x1E, 0x29, 85, 0x5E, 0x91, 1, 0x91, 31, 0x5F, 0x1F, 0x0C, 0, 0x80, 0, 2, 0x1E, 0x29, 87, 0x39, 16, 0x4E, 0x00, 0x4F, 0x1F, 0x0C, 0, 0, 0, 1, 0x1E, 0x29, 85, 0x5E, 0x91, 2, 0x91, 50, 0x5F, 0x1F, ];
#[test]
fn group_present_value_holds_one_read_access_result_per_member() {
let (db, group) = database(pinned_members());
let cases: &[(P, Option<u32>, ExpectedRead)] = &[
(P::LIST_OF_GROUP_MEMBERS, None, Ok(MEMBERS)),
(P::PRESENT_VALUE, None, Ok(PRESENT_VALUE)),
(
P::PRESENT_VALUE,
Some(0),
Err(ErrorCode::PROPERTY_IS_NOT_AN_ARRAY),
),
(
P::LIST_OF_GROUP_MEMBERS,
Some(1),
Err(ErrorCode::PROPERTY_IS_NOT_AN_ARRAY),
),
];
assert_cases(&db, group, cases);
}
fn rpm_ack(db: &ObjectDatabase, spec: &ReadAccessSpecification) -> Vec<u8> {
let mut request = BytesMut::new();
ReadPropertyMultipleRequest {
list_of_read_access_specs: vec![spec.clone()],
}
.encode(&mut request)
.unwrap();
let mut ack = BytesMut::new();
handle_read_property_multiple(db, &request, &mut ack).unwrap();
ack.to_vec()
}
fn group_present_value(db: &ObjectDatabase, group: ObjectIdentifier) -> Vec<Vec<u8>> {
let mut request = BytesMut::new();
ReadPropertyRequest {
object_identifier: group,
property_identifier: P::PRESENT_VALUE,
property_array_index: None,
}
.encode(&mut request);
let mut response = BytesMut::new();
handle_read_property(db, &request, &mut response).unwrap();
let value = ReadPropertyACK::decode(&response).unwrap().property_value;
ReadPropertyMultipleACK::decode(&value)
.unwrap()
.list_of_read_access_results
.iter()
.map(|result| {
let mut encoded = BytesMut::new();
result.encode(&mut encoded);
encoded.to_vec()
})
.collect()
}
#[test]
fn each_member_reads_as_read_property_multiple_reads_its_specification() {
let device = bacnet_objects::device::DeviceObject::new(bacnet_objects::device::DeviceConfig {
identity: bacnet_objects::DeviceIdentity::new(100, 0)
.expect("valid Device identity")
.with_name("DEV-100"),
..Default::default()
})
.unwrap();
let members = vec![
member(oid(ObjectType::ANALOG_INPUT, 1), &[(P::ALL, None)]),
member(
oid(ObjectType::ANALOG_VALUE, 2),
&[(P::REQUIRED, None), (P::PRIORITY_ARRAY, Some(0))],
),
member(oid(ObjectType::ANALOG_VALUE, 2), &[(P::OPTIONAL, None)]),
member(oid(ObjectType::GROUP, 7), &[(P::OPTIONAL, None)]),
member(oid(ObjectType::DEVICE, 4194303), &[(P::OBJECT_NAME, None)]),
member(oid(ObjectType::ANALOG_INPUT, 9), &[(P::ALL, None)]),
];
let (mut db, group) = database(members.clone());
db.add(Box::new(device)).unwrap();
let elements = group_present_value(&db, group);
assert_eq!(elements.len(), members.len());
for (element, spec) in elements.iter().zip(&members) {
assert_eq!(element, &rpm_ack(&db, spec), "{spec:?}");
}
assert_eq!(elements[4][..5], [0x0C, 0x02, 0, 0, 100]);
}
#[test]
fn group_present_value_is_rebuilt_on_each_read() {
let input = oid(ObjectType::ANALOG_INPUT, 1);
let (mut db, group) = database(vec![member(input, &[(P::DESCRIPTION, None)])]);
let before = group_present_value(&db, group);
db.get_mut(&input)
.unwrap()
.write_property(
P::DESCRIPTION,
None,
PropertyValue::CharacterString("hot".into()),
None,
)
.unwrap();
let after = group_present_value(&db, group);
assert_eq!(
before,
[vec![
0x0C, 0, 0, 0, 1, 0x1E, 0x29, 28, 0x4E, 0x71, 0, 0x4F, 0x1F
]]
);
assert_eq!(
after,
[vec![
0x0C, 0, 0, 0, 1, 0x1E, 0x29, 28, 0x4E, 0x74, 0, b'h', b'o', b't', 0x4F, 0x1F,
]]
);
}
fn expanded_rows(db: &ObjectDatabase, specs: Vec<ReadAccessSpecification>) -> usize {
let mut request = BytesMut::new();
ReadPropertyMultipleRequest {
list_of_read_access_specs: specs,
}
.encode(&mut request)
.unwrap();
let mut ack = BytesMut::new();
handle_read_property_multiple(db, &request, &mut ack).unwrap();
ReadPropertyMultipleACK::decode(&ack)
.unwrap()
.list_of_read_access_results
.iter()
.map(|result| result.list_of_results.len())
.sum()
}
#[test]
fn member_rows_are_charged_to_the_rpm_work_budget_and_shared_between_groups() {
use crate::handlers::{ReadFailure, rpm_budget::handle_rpm_budgeted_observed};
let input = oid(ObjectType::ANALOG_INPUT, 1);
let value = oid(ObjectType::ANALOG_VALUE, 2);
let (mut db, seven) = database(vec![
member(input, &[(P::PRESENT_VALUE, None)]),
member(value, &[(P::ALL, None)]),
]);
let mut group = GroupObject::new(8, "GRP-8").unwrap();
group
.add_member(member(
input,
&[(P::DESCRIPTION, None), (P::OBJECT_NAME, None)],
))
.unwrap();
let eight = group.object_identifier();
db.add(Box::new(group)).unwrap();
let seven_members = 1 + expanded_rows(&db, vec![member(value, &[(P::ALL, None)])]);
let group_all = expanded_rows(&db, vec![member(seven, &[(P::ALL, None)])]);
let read_seven = member(seven, &[(P::PRESENT_VALUE, None)]);
let read_eight = member(eight, &[(P::PRESENT_VALUE, None)]);
let pair = 1 + seven_members + 1 + 2;
let budget = |max_result_elements| crate::server::ReadPropertyMultipleBudget {
max_result_elements,
max_service_ack_bytes: 16384,
};
let encode = |specs: &[ReadAccessSpecification]| {
let mut request = BytesMut::new();
ReadPropertyMultipleRequest {
list_of_read_access_specs: specs.to_vec(),
}
.encode(&mut request)
.unwrap();
request
};
let cases = [
(vec![read_seven.clone()], 1 + seven_members),
(
vec![member(seven, &[(P::ALL, None)])],
group_all + seven_members,
),
(vec![read_seven.clone(), read_eight.clone()], pair),
];
for (specs, work) in cases {
let request = encode(&specs);
let mut ack = BytesMut::new();
handle_rpm_budgeted_observed(&db, &request, &mut ack, budget(work), |_, _, _, _| {})
.unwrap();
let mut legacy = BytesMut::new();
handle_read_property_multiple(&db, &request, &mut legacy).unwrap();
assert_eq!(ack, legacy, "{specs:?}");
let mut prefix = BytesMut::from(&b"prefix"[..]);
let mut observed = 0;
let result = handle_rpm_budgeted_observed(
&db,
&request,
&mut prefix,
budget(work - 1),
|_, _, _, _| observed += 1,
);
assert!(matches!(result, Err(ReadFailure::Work)), "{specs:?}");
assert_eq!((&prefix[..], observed), (&b"prefix"[..], 0));
}
for alone in [read_seven, read_eight] {
let request = encode(std::slice::from_ref(&alone));
let mut ack = BytesMut::new();
handle_rpm_budgeted_observed(&db, &request, &mut ack, budget(pair - 1), |_, _, _, _| {})
.unwrap();
}
}
#[test]
fn read_range_pages_the_rebuilt_present_value() {
let (db, group) = database(pinned_members());
let mut request = BytesMut::new();
ReadRangeRequest {
object_identifier: group,
property_identifier: P::PRESENT_VALUE,
property_array_index: None,
range: Some(RangeSpec::ByPosition {
reference_index: 2,
count: 2,
}),
}
.encode(&mut request)
.unwrap();
let mut response = BytesMut::new();
handle_read_range(&db, &request, &mut response).unwrap();
let ack = ReadRangeAck::decode(&response).unwrap();
assert_eq!(ack.item_count, 2);
assert_eq!(ack.result_flags, (false, false, false));
assert_eq!(ack.item_data, PRESENT_VALUE[23..53]);
}