use super::*;
use bacnet_objects::accumulator::{AccumulatorObject, PulseConverterObject};
use bacnet_objects::analog::AnalogInputObject;
use bacnet_services::common::BACnetPropertyValue;
use bacnet_services::wpm::{WriteAccessSpecification, WritePropertyMultipleRequest};
use bacnet_types::enums::{EngineeringUnits, Reliability};
use bacnet_types::error::RemoteError;
const INPUT: PropertyIdentifier = PropertyIdentifier::INPUT_REFERENCE;
const ACC_1_PV: [u8; 7] = [0x0C, 0x05, 0xC0, 0x00, 0x01, 0x19, 0x55];
const AI_1_PV: [u8; 7] = [0x0C, 0x00, 0x00, 0x00, 0x01, 0x19, 0x55];
const ACC_2_PV: [u8; 7] = [0x0C, 0x05, 0xC0, 0x00, 0x02, 0x19, 0x55];
const UNSET: [u8; 7] = [0x0C, 0x05, 0xFF, 0xFF, 0xFF, 0x19, 0x55];
const FAULT: u8 = 0x40;
fn pc1() -> ObjectIdentifier {
ObjectIdentifier::new(ObjectType::PULSE_CONVERTER, 1).unwrap()
}
fn database() -> ObjectDatabase {
let mut db = ObjectDatabase::new();
db.add(Box::new(
PulseConverterObject::new(1, "PC-1", EngineeringUnits::NO_UNITS).unwrap(),
))
.unwrap();
db.add(Box::new(
AccumulatorObject::new(1, "ACC-1", EngineeringUnits::NO_UNITS).unwrap(),
))
.unwrap();
db.add(Box::new(
AnalogInputObject::new(1, "AI-1", EngineeringUnits::NO_UNITS).unwrap(),
))
.unwrap();
db
}
fn write(db: &mut ObjectDatabase, octets: &[u8]) {
let request = WritePropertyRequest {
object_identifier: pc1(),
property_identifier: INPUT,
property_array_index: None,
property_value: octets.to_vec(),
priority: None,
};
let mut bytes = BytesMut::new();
request.encode(&mut bytes).unwrap();
handle_write_property(db, &bytes).unwrap();
}
fn write_multiple(db: &mut ObjectDatabase, octets: &[u8]) {
let request = WritePropertyMultipleRequest {
list_of_write_access_specs: vec![WriteAccessSpecification {
object_identifier: pc1(),
list_of_properties: vec![BACnetPropertyValue {
property_identifier: INPUT,
property_array_index: None,
value: octets.to_vec(),
priority: None,
}],
}],
};
let mut bytes = BytesMut::new();
request.encode(&mut bytes).unwrap();
handle_write_property_multiple(db, &bytes).unwrap();
}
fn state(db: &ObjectDatabase) -> (Reliability, u8) {
let read = |property: PropertyIdentifier| {
let request = ReadPropertyRequest {
object_identifier: pc1(),
property_identifier: property,
property_array_index: None,
};
let mut bytes = BytesMut::new();
request.encode(&mut bytes);
let mut ack = BytesMut::new();
handle_read_property(db, &bytes, &mut ack).unwrap();
ReadPropertyACK::decode(&ack).unwrap().property_value
};
let reliability = read(PropertyIdentifier::RELIABILITY);
assert_eq!(reliability[0], 0x91, "an Enumerated");
let flags = read(PropertyIdentifier::STATUS_FLAGS);
assert_eq!(&flags[..2], &[0x82, 0x04], "four-bit Status_Flags");
(Reliability::from_raw(u32::from(reliability[1])), flags[2])
}
#[test]
fn a_written_reference_is_judged_as_the_write_commits() {
let mut db = database();
assert_eq!(state(&db), (Reliability::NO_FAULT_DETECTED, 0));
for (octets, reliability, flags, what) in [
(&AI_1_PV, Reliability::CONFIGURATION_ERROR, FAULT, "a REAL"),
(&ACC_1_PV, Reliability::NO_FAULT_DETECTED, 0, "an Unsigned"),
(
&ACC_2_PV,
Reliability::CONFIGURATION_ERROR,
FAULT,
"a missing object",
),
(&UNSET, Reliability::NO_FAULT_DETECTED, 0, "no reference"),
] {
write(&mut db, octets);
assert_eq!(state(&db), (reliability, flags), "WriteProperty of {what}");
write_multiple(&mut db, &ACC_1_PV);
write_multiple(&mut db, octets);
assert_eq!(state(&db), (reliability, flags), "WPM of {what}");
}
}
#[test]
fn a_null_leaves_the_reference_and_its_verdict_as_they_are() {
let mut db = database();
write(&mut db, &AI_1_PV);
write(&mut db, &[0x00]);
assert_eq!(state(&db), (Reliability::CONFIGURATION_ERROR, FAULT));
}
#[test]
fn deleting_and_recreating_the_named_object_moves_the_fault() {
let mut db = database();
write(&mut db, &ACC_1_PV);
assert_eq!(state(&db), (Reliability::NO_FAULT_DETECTED, 0));
let accumulator = ObjectIdentifier::new(ObjectType::ACCUMULATOR, 1).unwrap();
db.remove(&accumulator).unwrap();
assert_eq!(state(&db), (Reliability::CONFIGURATION_ERROR, FAULT));
assert_eq!(db.take_membership_work_internal().changed, [pc1()]);
db.add(Box::new(
AccumulatorObject::new(1, "ACC-1", EngineeringUnits::NO_UNITS).unwrap(),
))
.unwrap();
assert_eq!(state(&db), (Reliability::NO_FAULT_DETECTED, 0));
assert_eq!(db.take_membership_work_internal().changed, [pc1()]);
}
fn write_to(
db: &mut ObjectDatabase,
object: ObjectIdentifier,
property: PropertyIdentifier,
octets: &[u8],
) -> Result<ObjectIdentifier, Error> {
let request = WritePropertyRequest {
object_identifier: object,
property_identifier: property,
property_array_index: None,
property_value: octets.to_vec(),
priority: None,
};
let mut bytes = BytesMut::new();
request.encode(&mut bytes).unwrap();
handle_write_property(db, &bytes)
}
fn refused_with(result: Result<ObjectIdentifier, Error>, expected: ErrorCode) {
assert!(
matches!(result, Err(Error::Remote(RemoteError::Error { class, code }))
if class == ErrorClass::PROPERTY
&& code == expected),
"expected PROPERTY / {expected:?}, got {result:?}"
);
}
#[test]
fn reliability_takes_a_simulated_value_only_out_of_service() {
let mut db = database();
write(&mut db, &AI_1_PV);
assert_eq!(state(&db), (Reliability::CONFIGURATION_ERROR, FAULT));
let over_range = [0x91, 0x02];
let reliability = PropertyIdentifier::RELIABILITY;
refused_with(
write_to(&mut db, pc1(), reliability, &over_range),
ErrorCode::WRITE_ACCESS_DENIED,
);
let out_of_service = PropertyIdentifier::OUT_OF_SERVICE;
write_to(&mut db, pc1(), out_of_service, &[0x11]).unwrap();
write_to(&mut db, pc1(), reliability, &over_range).unwrap();
write_to(&mut db, pc1(), reliability, &[0x00]).unwrap();
assert_eq!(state(&db), (Reliability::OVER_RANGE, FAULT | 0x10));
write_to(&mut db, pc1(), out_of_service, &[0x10]).unwrap();
assert_eq!(state(&db), (Reliability::CONFIGURATION_ERROR, FAULT));
}
#[test]
fn an_accumulator_serves_prescale_only_once_set() {
let mut db = database();
let acc = ObjectIdentifier::new(ObjectType::ACCUMULATOR, 1).unwrap();
let prescale = PropertyIdentifier::PRESCALE;
let read = |db: &ObjectDatabase, property| {
let request = ReadPropertyRequest {
object_identifier: acc,
property_identifier: property,
property_array_index: None,
};
let mut bytes = BytesMut::new();
request.encode(&mut bytes);
let mut ack = BytesMut::new();
handle_read_property(db, &bytes, &mut ack)
.map(|_| ReadPropertyACK::decode(&ack).unwrap().property_value)
};
refused_with(
read(&db, prescale).map(|_| acc),
ErrorCode::UNKNOWN_PROPERTY,
);
let list = read(&db, PropertyIdentifier::PROPERTY_LIST).unwrap();
assert!(
!list
.chunks(2)
.any(|enumerated| enumerated == [0x91, prescale.to_raw() as u8])
);
refused_with(
write_to(&mut db, acc, prescale, &[0x00]),
ErrorCode::UNKNOWN_PROPERTY,
);
let mut configured = AccumulatorObject::new(1, "ACC-1", EngineeringUnits::NO_UNITS).unwrap();
configured.set_prescale(bacnet_types::constructed::BACnetPrescale {
multiplier: 5,
modulo_divide: 100,
});
db.add(Box::new(configured)).unwrap();
assert_eq!(read(&db, prescale).unwrap(), [0x09, 5, 0x19, 100]);
refused_with(
write_to(&mut db, acc, prescale, &[0x00]),
ErrorCode::WRITE_ACCESS_DENIED,
);
}