use super::*;
use bacnet_objects::elevator::ElevatorGroupObject;
use bacnet_services::common::BACnetPropertyValue;
use bacnet_services::wpm::{WriteAccessSpecification, WritePropertyMultipleRequest};
use bacnet_types::constructed::{BACnetLandingCallStatus, LandingCallCommand};
use bacnet_types::enums::LiftCarDirection;
use bacnet_types::error::RemoteError;
const LCC: PropertyIdentifier = PropertyIdentifier::LANDING_CALL_CONTROL;
const LANDING_CALLS: PropertyIdentifier = PropertyIdentifier::LANDING_CALLS;
const UP_FROM_5: &[u8] = &[0x09, 0x05, 0x19, 0x03];
fn group_db() -> (ObjectDatabase, ObjectIdentifier) {
let mut db = ObjectDatabase::new();
let group = ElevatorGroupObject::new(1, "EG-1").unwrap();
let oid = group.object_identifier();
db.add(Box::new(group)).unwrap();
(db, oid)
}
fn encode_value(value: PropertyValue) -> Vec<u8> {
let mut buf = BytesMut::new();
encode_property_value(&mut buf, &value).unwrap();
buf.to_vec()
}
fn write(
db: &mut ObjectDatabase,
oid: ObjectIdentifier,
array_index: Option<u32>,
property_value: &[u8],
) -> Result<(), Error> {
let mut request = BytesMut::new();
WritePropertyRequest {
object_identifier: oid,
property_identifier: LCC,
property_array_index: array_index,
property_value: property_value.to_vec(),
priority: None,
}
.encode(&mut request)
.unwrap();
handle_write_property(db, &request).map(|_| ())
}
fn read_wire(db: &ObjectDatabase, oid: ObjectIdentifier, property: PropertyIdentifier) -> Vec<u8> {
let mut request = BytesMut::new();
ReadPropertyRequest {
object_identifier: oid,
property_identifier: property,
property_array_index: None,
}
.encode(&mut request);
let mut response = BytesMut::new();
handle_read_property(db, &request, &mut response).unwrap();
ReadPropertyACK::decode(&response).unwrap().property_value
}
fn control(db: &ObjectDatabase, oid: ObjectIdentifier) -> BACnetLandingCallStatus {
let bytes = read_wire(db, oid, LCC);
let (status, end) =
bacnet_encoding::constructed::decode_landing_call_status(&bytes, 0).unwrap();
assert_eq!(end, bytes.len());
status
}
fn assert_property_error(result: Result<(), Error>, expected: ErrorCode, context: &str) {
match result.expect_err(context) {
Error::Remote(RemoteError::Error { class, code }) => {
assert_eq!(class, ErrorClass::PROPERTY, "{context}");
assert_eq!(code, expected, "{context}: {expected:?}");
}
other => panic!("{context}: expected PROPERTY/{expected:?}, got {other:?}"),
}
}
#[test]
fn landing_call_control_writes_over_wp_and_reads_back_over_rp() {
let (mut db, oid) = group_db();
assert_eq!(read_wire(&db, oid, LCC), [0x09, 0x00, 0x19, 0x00]);
let cases: &[(&[u8], BACnetLandingCallStatus)] = &[
(
UP_FROM_5,
BACnetLandingCallStatus {
floor_number: 5,
command: LandingCallCommand::Direction(LiftCarDirection::UP),
floor_text: None,
},
),
(
&[
0x09, 0x0C, 0x29, 0x14, 0x3D, 0x06, 0x00, b'L', b'o', b'b', b'b', b'y',
],
BACnetLandingCallStatus {
floor_number: 12,
command: LandingCallCommand::Destination(20),
floor_text: Some("Lobby".into()),
},
),
(
&[0x09, 0x00, 0x1A, 0x04, 0x00],
BACnetLandingCallStatus {
floor_number: 0,
command: LandingCallCommand::Direction(LiftCarDirection::from_raw(1024)),
floor_text: None,
},
),
];
for (bytes, expected) in cases {
write(&mut db, oid, None, bytes).unwrap();
assert_eq!(read_wire(&db, oid, LCC), *bytes);
assert_eq!(control(&db, oid), *expected);
}
}
#[test]
fn landing_call_control_wp_refusals_preserve_the_last_call() {
let (mut db, oid) = group_db();
write(&mut db, oid, None, UP_FROM_5).unwrap();
let cases: Vec<(&str, Option<u32>, Vec<u8>, ErrorCode)> = vec![
(
"application Enumerated",
None,
encode_value(PropertyValue::Enumerated(1)),
ErrorCode::INVALID_DATA_TYPE,
),
(
"application Unsigned",
None,
encode_value(PropertyValue::Unsigned(1)),
ErrorCode::INVALID_DATA_TYPE,
),
(
"context floor then application direction",
None,
vec![0x09, 0x05, 0x91, 0x03],
ErrorCode::INVALID_DATA_TYPE,
),
(
"reserved direction 6",
None,
vec![0x09, 0x05, 0x19, 0x06],
ErrorCode::VALUE_OUT_OF_RANGE,
),
(
"direction 65536",
None,
vec![0x09, 0x05, 0x1B, 0x01, 0x00, 0x00],
ErrorCode::VALUE_OUT_OF_RANGE,
),
(
"floor 300, direction UP",
None,
vec![0x0A, 0x01, 0x2C, 0x19, 0x03],
ErrorCode::VALUE_OUT_OF_RANGE,
),
(
"destination 256",
None,
vec![0x09, 0x05, 0x2A, 0x01, 0x00],
ErrorCode::VALUE_OUT_OF_RANGE,
),
(
"direction 2^32",
None,
vec![0x09, 0x05, 0x1D, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00],
ErrorCode::VALUE_OUT_OF_RANGE,
),
(
"floor-number only",
None,
vec![0x09, 0x05],
ErrorCode::INVALID_DATA_ENCODING,
),
(
"oversized floor without a command",
None,
vec![0x0A, 0x01, 0x2C],
ErrorCode::INVALID_DATA_ENCODING,
),
(
"both command alternatives",
None,
vec![0x09, 0x05, 0x19, 0x03, 0x29, 0x14],
ErrorCode::INVALID_DATA_ENCODING,
),
(
"array index",
Some(1),
UP_FROM_5.to_vec(),
ErrorCode::PROPERTY_IS_NOT_AN_ARRAY,
),
];
let before = read_wire(&db, oid, LCC);
for (what, index, bytes, expected) in cases {
assert_property_error(write(&mut db, oid, index, &bytes), expected, what);
assert_eq!(read_wire(&db, oid, LCC), before, "{what}");
}
write(&mut db, oid, None, &encode_value(PropertyValue::Null)).unwrap();
assert_eq!(read_wire(&db, oid, LCC), before);
}
#[test]
fn landing_call_control_wpm_keeps_the_committed_prefix() {
let (mut db, oid) = group_db();
let property = |value: &[u8]| BACnetPropertyValue {
property_identifier: LCC,
property_array_index: None,
value: value.to_vec(),
priority: None,
};
let mut request = BytesMut::new();
WritePropertyMultipleRequest {
list_of_write_access_specs: vec![WriteAccessSpecification {
object_identifier: oid,
list_of_properties: vec![property(UP_FROM_5), property(&[0x09, 0x05, 0x19, 0x06])],
}],
}
.encode(&mut request)
.unwrap();
assert_property_error(
handle_write_property_multiple(&mut db, &request).map(|_| ()),
ErrorCode::VALUE_OUT_OF_RANGE,
"second call has a reserved direction",
);
assert_eq!(read_wire(&db, oid, LCC), UP_FROM_5);
}
#[test]
fn landing_calls_read_as_a_list_and_refuse_writes() {
let (mut db, oid) = group_db();
assert!(read_wire(&db, oid, LANDING_CALLS).is_empty());
let mut db_with_calls = ObjectDatabase::new();
let mut group = ElevatorGroupObject::new(2, "EG-2").unwrap();
group
.set_landing_calls(vec![
BACnetLandingCallStatus {
floor_number: 2,
command: LandingCallCommand::Direction(LiftCarDirection::DOWN),
floor_text: None,
},
BACnetLandingCallStatus {
floor_number: 9,
command: LandingCallCommand::Destination(1),
floor_text: Some("L".into()),
},
])
.unwrap();
let with_calls = group.object_identifier();
db_with_calls.add(Box::new(group)).unwrap();
let bytes = read_wire(&db_with_calls, with_calls, LANDING_CALLS);
assert_eq!(
bytes,
[
0x09, 0x02, 0x19, 0x04, 0x09, 0x09, 0x29, 0x01, 0x3A, 0x00, 0x4C
]
);
assert_eq!(
bacnet_encoding::constructed::decode_landing_call_status_list(&bytes)
.unwrap()
.len(),
2
);
let mut request = BytesMut::new();
WritePropertyRequest {
object_identifier: oid,
property_identifier: LANDING_CALLS,
property_array_index: None,
property_value: UP_FROM_5.to_vec(),
priority: None,
}
.encode(&mut request)
.unwrap();
assert_property_error(
handle_write_property(&mut db, &request).map(|_| ()),
ErrorCode::WRITE_ACCESS_DENIED,
"Landing_Calls write",
);
}