use super::*;
use crate::accumulator::{AccumulatorObject, PulseConverterObject};
use crate::analog::AnalogInputObject;
use crate::value_types::IntegerValueObject;
use bacnet_types::enums::Reliability;
use bacnet_types::primitives::StatusFlags;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use PropertyIdentifier as P;
fn oid(object_type: ObjectType, instance: u32) -> ObjectIdentifier {
ObjectIdentifier::new(object_type, instance).unwrap()
}
fn pc1() -> ObjectIdentifier {
oid(ObjectType::PULSE_CONVERTER, 1)
}
fn acc1() -> ObjectIdentifier {
oid(ObjectType::ACCUMULATOR, 1)
}
fn octets(reference: &BACnetObjectPropertyReference) -> PropertyValue {
let mut encoded = bytes::BytesMut::new();
bacnet_encoding::constructed::encode_object_property_reference(&mut encoded, reference);
PropertyValue::ApplicationData(encoded.to_vec())
}
fn present_value(target: ObjectIdentifier) -> BACnetObjectPropertyReference {
BACnetObjectPropertyReference::new(target, P::PRESENT_VALUE.to_raw())
}
fn reliability(db: &ObjectDatabase, oid: ObjectIdentifier) -> Reliability {
match db.get(&oid).unwrap().read_property(P::RELIABILITY, None) {
Ok(PropertyValue::Enumerated(raw)) => Reliability::from_raw(raw),
other => panic!("Reliability read {other:?}"),
}
}
fn fault(db: &ObjectDatabase, oid: ObjectIdentifier) -> bool {
match db.get(&oid).unwrap().read_property(P::STATUS_FLAGS, None) {
Ok(PropertyValue::BitString { data, .. }) => {
StatusFlags::from_bits_truncate(data[0] >> 4).contains(StatusFlags::FAULT)
}
other => panic!("Status_Flags read {other:?}"),
}
}
fn converter(reference: Option<BACnetObjectPropertyReference>) -> Box<PulseConverterObject> {
let mut pc = PulseConverterObject::new(1, "PC-1", 95).unwrap();
if let Some(reference) = reference {
pc.set_input_reference(reference);
}
Box::new(pc)
}
fn write_reference(db: &mut ObjectDatabase, reference: &BACnetObjectPropertyReference) -> bool {
db.get_mut(&pc1())
.unwrap()
.write_property(P::INPUT_REFERENCE, None, octets(reference), None)
.unwrap();
db.check_input_reference(&pc1())
}
#[test]
fn a_missing_object_faults_the_converter_until_it_is_added() {
let mut db = ObjectDatabase::new();
db.add(converter(Some(present_value(acc1())))).unwrap();
assert_eq!(reliability(&db, pc1()), Reliability::CONFIGURATION_ERROR);
assert!(fault(&db, pc1()));
assert!(db.take_membership_work_internal().is_empty());
db.add(Box::new(AccumulatorObject::new(1, "ACC-1", 95).unwrap()))
.unwrap();
assert_eq!(reliability(&db, pc1()), Reliability::NO_FAULT_DETECTED);
assert!(!fault(&db, pc1()));
assert_eq!(db.take_membership_work_internal().changed, [pc1()]);
assert!(db.take_membership_work_internal().is_empty());
db.remove(&acc1()).unwrap();
assert_eq!(reliability(&db, pc1()), Reliability::CONFIGURATION_ERROR);
assert_eq!(db.take_membership_work_internal().changed, [pc1()]);
}
#[test]
fn only_unsigned_and_integer_properties_are_inputs() {
let mut db = ObjectDatabase::new();
db.add(converter(None)).unwrap();
db.add(Box::new(AccumulatorObject::new(1, "ACC-1", 95).unwrap()))
.unwrap();
db.add(Box::new(AnalogInputObject::new(1, "AI-1", 95).unwrap()))
.unwrap();
db.add(Box::new(IntegerValueObject::new(1, "IV-1").unwrap()))
.unwrap();
assert_eq!(reliability(&db, pc1()), Reliability::NO_FAULT_DETECTED);
let indexed = |target, property: P, index| {
BACnetObjectPropertyReference::new_indexed(target, property.to_raw(), index)
};
let analog_input = oid(ObjectType::ANALOG_INPUT, 1);
let integer_value = oid(ObjectType::INTEGER_VALUE, 1);
for (reference, usable, what) in [
(present_value(acc1()), true, "an Unsigned"),
(present_value(integer_value), true, "an INTEGER"),
(present_value(analog_input), false, "a REAL"),
(
BACnetObjectPropertyReference::new(acc1(), P::OBJECT_NAME.to_raw()),
false,
"a CharacterString",
),
(
BACnetObjectPropertyReference::new(acc1(), P::HIGH_LIMIT.to_raw()),
false,
"a property the object doesn't have",
),
(
indexed(acc1(), P::PRESENT_VALUE, 1),
false,
"an index on a property that isn't an array",
),
(
indexed(integer_value, P::PRIORITY_ARRAY, 0),
false,
"an array's size, which counts no pulses",
),
(
indexed(integer_value, P::PRIORITY_ARRAY, 17),
false,
"an index past the end",
),
(
BACnetObjectPropertyReference::new(pc1(), P::COUNT.to_raw()),
false,
"the converter's own Count",
),
] {
write_reference(&mut db, &reference);
let expected = if usable {
Reliability::NO_FAULT_DETECTED
} else {
Reliability::CONFIGURATION_ERROR
};
assert_eq!(reliability(&db, pc1()), expected, "{what}");
assert_eq!(fault(&db, pc1()), !usable, "{what}");
}
}
#[test]
fn an_unset_reference_is_no_fault_and_clears_one() {
let mut db = ObjectDatabase::new();
db.add(converter(Some(present_value(acc1())))).unwrap();
assert_eq!(reliability(&db, pc1()), Reliability::CONFIGURATION_ERROR);
db.get_mut(&pc1())
.unwrap()
.write_property(
P::INPUT_REFERENCE,
None,
octets(&crate::reference::unset_reference(ObjectType::ACCUMULATOR)),
None,
)
.unwrap();
assert_eq!(reliability(&db, pc1()), Reliability::NO_FAULT_DETECTED);
assert!(!db.check_input_reference(&pc1()));
assert_eq!(reliability(&db, pc1()), Reliability::NO_FAULT_DETECTED);
}
#[test]
fn the_verdict_waits_out_of_service_and_applies_on_the_return() {
let mut db = ObjectDatabase::new();
db.add(converter(None)).unwrap();
let out_of_service = |db: &mut ObjectDatabase, value| {
db.get_mut(&pc1())
.unwrap()
.write_property(P::OUT_OF_SERVICE, None, PropertyValue::Boolean(value), None)
.unwrap();
};
out_of_service(&mut db, true);
assert!(!write_reference(&mut db, &present_value(acc1())));
assert_eq!(reliability(&db, pc1()), Reliability::NO_FAULT_DETECTED);
out_of_service(&mut db, false);
assert_eq!(reliability(&db, pc1()), Reliability::CONFIGURATION_ERROR);
assert!(fault(&db, pc1()));
}
fn count(db: &ObjectDatabase) -> PropertyValue {
db.get(&pc1())
.unwrap()
.read_property(P::COUNT, None)
.unwrap()
}
fn accumulator(instance: u32, value: u64) -> Box<AccumulatorObject> {
let mut object = AccumulatorObject::new(instance, format!("ACC-{instance}"), 95).unwrap();
object
.write_property(P::MAX_PRES_VALUE, None, PropertyValue::Unsigned(99), None)
.unwrap();
object.set_present_value(value);
Box::new(object)
}
#[test]
fn each_increase_of_an_accumulator_is_counted_across_its_wrap() {
let mut db = ObjectDatabase::new();
db.add(accumulator(1, 80)).unwrap();
db.add(converter(Some(present_value(acc1())))).unwrap();
assert!(db.count_pulse_inputs().is_empty());
assert_eq!(count(&db), PropertyValue::Unsigned(0));
db.add(accumulator(1, 87)).unwrap();
assert_eq!(db.count_pulse_inputs(), [pc1()]);
assert_eq!(count(&db), PropertyValue::Unsigned(7));
assert!(db.count_pulse_inputs().is_empty());
db.add(accumulator(1, 5)).unwrap();
assert_eq!(db.count_pulse_inputs(), [pc1()]);
assert_eq!(count(&db), PropertyValue::Unsigned(25));
db.remove(&acc1()).unwrap();
assert!(db.count_pulse_inputs().is_empty());
db.add(accumulator(1, 50)).unwrap();
assert!(db.count_pulse_inputs().is_empty());
db.add(accumulator(1, 51)).unwrap();
assert_eq!(db.count_pulse_inputs(), [pc1()]);
assert_eq!(count(&db), PropertyValue::Unsigned(26));
db.add(accumulator(2, 90)).unwrap();
write_reference(&mut db, &present_value(oid(ObjectType::ACCUMULATOR, 2)));
assert!(db.count_pulse_inputs().is_empty());
assert_eq!(count(&db), PropertyValue::Unsigned(26));
}
#[test]
fn a_decrease_of_any_other_source_only_sets_the_baseline_again() {
let mut db = ObjectDatabase::new();
db.add(accumulator(1, 0)).unwrap();
let max = BACnetObjectPropertyReference::new(acc1(), P::MAX_PRES_VALUE.to_raw());
db.add(converter(Some(max))).unwrap();
let set_max = |db: &mut ObjectDatabase, value| {
db.get_mut(&acc1())
.unwrap()
.write_property(
P::MAX_PRES_VALUE,
None,
PropertyValue::Unsigned(value),
None,
)
.unwrap();
};
assert!(db.count_pulse_inputs().is_empty());
set_max(&mut db, 120);
assert_eq!(db.count_pulse_inputs(), [pc1()]);
assert_eq!(count(&db), PropertyValue::Unsigned(21));
set_max(&mut db, 10);
assert!(db.count_pulse_inputs().is_empty());
set_max(&mut db, 14);
assert_eq!(db.count_pulse_inputs(), [pc1()]);
assert_eq!(count(&db), PropertyValue::Unsigned(25));
}
#[test]
fn a_priority_array_slot_is_judged_by_the_commanded_datatype() {
let mut db = ObjectDatabase::new();
db.add(Box::new(IntegerValueObject::new(1, "IV-1").unwrap()))
.unwrap();
let integer_value = oid(ObjectType::INTEGER_VALUE, 1);
let slot =
BACnetObjectPropertyReference::new_indexed(integer_value, P::PRIORITY_ARRAY.to_raw(), 8);
db.add(converter(Some(slot))).unwrap();
assert_eq!(reliability(&db, pc1()), Reliability::NO_FAULT_DETECTED);
assert!(db.count_pulse_inputs().is_empty());
let origin = crate::command_source::CommandOrigin::Local {
owner_device: oid(ObjectType::DEVICE, 1),
initiating_object: None,
};
for value in [3, 10] {
db.get_mut(&integer_value)
.unwrap()
.write_property_from(
P::PRESENT_VALUE,
None,
PropertyValue::Signed(value),
Some(8),
&origin,
)
.unwrap();
db.count_pulse_inputs();
}
assert_eq!(count(&db), PropertyValue::Unsigned(7));
assert_eq!(reliability(&db, pc1()), Reliability::NO_FAULT_DETECTED);
}
#[test]
fn the_counting_pass_judges_a_reference_changed_past_the_server() {
let mut db = ObjectDatabase::new();
db.add(accumulator(1, 0)).unwrap();
db.add(Box::new(AnalogInputObject::new(1, "AI-1", 95).unwrap()))
.unwrap();
db.add(converter(Some(present_value(acc1())))).unwrap();
assert_eq!(reliability(&db, pc1()), Reliability::NO_FAULT_DETECTED);
db.get_mut(&pc1())
.unwrap()
.write_property(
P::INPUT_REFERENCE,
None,
octets(&present_value(oid(ObjectType::ANALOG_INPUT, 1))),
None,
)
.unwrap();
assert_eq!(reliability(&db, pc1()), Reliability::NO_FAULT_DETECTED);
assert_eq!(db.count_pulse_inputs(), [pc1()]);
assert_eq!(reliability(&db, pc1()), Reliability::CONFIGURATION_ERROR);
assert!(db.count_pulse_inputs().is_empty());
}
#[test]
fn reliability_takes_a_clients_value_out_of_service_until_the_return() {
let mut db = ObjectDatabase::new();
db.add(converter(Some(present_value(acc1())))).unwrap();
assert_eq!(reliability(&db, pc1()), Reliability::CONFIGURATION_ERROR);
let write = |db: &mut ObjectDatabase, property, value| {
db.get_mut(&pc1())
.unwrap()
.write_property(property, None, value, None)
};
let simulated = PropertyValue::Enumerated(Reliability::OVER_RANGE.to_raw());
assert!(write(&mut db, P::RELIABILITY, simulated.clone()).is_err());
write(&mut db, P::OUT_OF_SERVICE, PropertyValue::Boolean(true)).unwrap();
write(&mut db, P::RELIABILITY, simulated).unwrap();
assert_eq!(reliability(&db, pc1()), Reliability::OVER_RANGE);
db.count_pulse_inputs();
assert_eq!(reliability(&db, pc1()), Reliability::OVER_RANGE);
write(&mut db, P::OUT_OF_SERVICE, PropertyValue::Boolean(false)).unwrap();
assert_eq!(reliability(&db, pc1()), Reliability::CONFIGURATION_ERROR);
}
#[test]
fn a_changed_converter_wakes_the_server_once_per_queued_change() {
let mut db = ObjectDatabase::new();
let wakes = Arc::new(AtomicUsize::new(0));
let counter = Arc::clone(&wakes);
db.set_membership_waker_internal(Some(Arc::new(move || {
counter.fetch_add(1, Ordering::SeqCst);
})));
db.add(converter(Some(present_value(acc1())))).unwrap();
db.add(Box::new(AnalogInputObject::new(1, "AI-1", 95).unwrap()))
.unwrap();
assert_eq!(wakes.load(Ordering::SeqCst), 0);
db.add(Box::new(AccumulatorObject::new(1, "ACC-1", 95).unwrap()))
.unwrap();
assert_eq!(wakes.load(Ordering::SeqCst), 1);
db.add(Box::new(AccumulatorObject::new(1, "ACC-1", 95).unwrap()))
.unwrap();
assert_eq!(wakes.load(Ordering::SeqCst), 1);
assert_eq!(db.take_membership_work_internal().changed, [pc1()]);
}