1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
//! Elevator Group, Escalator, and Lift objects per ASHRAE 135-2020.
//!
//! - ElevatorGroupObject (type 57) — Clause 12.58
//! - EscalatorObject (type 58) — Clause 12.60
//! - LiftObject (type 59) — Clause 12.59
use bacnet_types::constructed::BACnetLandingCallStatus;
use bacnet_types::enums::{LiftCarDirection, LiftGroupMode, ObjectType, PropertyIdentifier};
use bacnet_types::error::Error;
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
use std::{borrow::Cow, collections::HashSet, hash::Hash};
use crate::common;
use crate::traits::BACnetObject;
mod car_state;
mod door_values;
mod doors;
mod energy_meter;
mod escalator;
mod landing_calls;
mod lift;
mod membership;
mod metadata;
pub use escalator::EscalatorObject;
pub use lift::LiftObject;
/// Whether `raw` is in the domain of one of this family's extensible Clause 21
/// enumerations: one of its named values (`named` is its `ALL_NAMED` table,
/// `value` is `raw` as that type), or the proprietary range 1024..=65535 that
/// Clause 23.1 (Table 23-1) opens for each of them. Every other value is
/// reserved or too large, and a write of it is refused with
/// VALUE_OUT_OF_RANGE.
fn named_or_proprietary<T: Copy + PartialEq>(named: &[(&str, T)], value: T, raw: u32) -> bool {
named.iter().any(|&(_, named)| named == value) || (1024..=65_535).contains(&raw)
}
/// Whether `direction` is in BACnetLiftCarDirection, the datatype of
/// Car_Moving_Direction, Car_Assigned_Direction and the direction of each
/// landing call.
fn direction_in_range(direction: &LiftCarDirection) -> bool {
named_or_proprietary(LiftCarDirection::ALL_NAMED, *direction, direction.to_raw())
}
/// `value`, or VALUE_OUT_OF_RANGE when `in_range` refuses it.
fn checked<T>(value: T, in_range: fn(&T) -> bool) -> Result<T, Error> {
if in_range(&value) {
Ok(value)
} else {
Err(common::value_out_of_range_error())
}
}
/// Decode a Fault_Signals write, a BACnetLIST of one of this family's fault
/// enumerations (`named` is its `ALL_NAMED` table, `from_raw` its
/// constructor): one Enumerated or a list of them. A value that isn't
/// Enumerated is INVALID_DATA_TYPE; a fault outside the enumeration's domain
/// or a repeated fault (the list holds a set) is VALUE_OUT_OF_RANGE. The
/// caller stores nothing unless every element passes.
fn decode_fault_signals<T: Copy + Eq + Hash>(
value: PropertyValue,
named: &[(&str, T)],
from_raw: fn(u32) -> T,
) -> Result<Vec<T>, Error> {
let values = match value {
PropertyValue::Enumerated(v) => vec![PropertyValue::Enumerated(v)],
PropertyValue::List(values) => values,
_ => return Err(common::invalid_data_type_error()),
};
let mut faults = Vec::with_capacity(values.len());
let mut seen = HashSet::with_capacity(values.len());
// A refusal names its element (#1048).
for (index, value) in values.into_iter().enumerate() {
let PropertyValue::Enumerated(raw) = value else {
return Err(common::at_list_element(
common::invalid_data_type_error(),
index,
));
};
let fault = from_raw(raw);
if !named_or_proprietary(named, fault, raw) || !seen.insert(fault) {
return Err(common::at_list_element(
common::value_out_of_range_error(),
index,
));
}
faults.push(fault);
}
Ok(faults)
}
// ===========================================================================
// ElevatorGroupObject (type 57)
// ===========================================================================
/// BACnet Elevator Group object — manages a group of lifts.
///
/// The object serves only properties its table (Clause 12.58, Table 12-76)
/// defines. That table has no Status_Flags, Out_Of_Service or Reliability, so
/// reads and writes of those return UNKNOWN_PROPERTY.
pub struct ElevatorGroupObject {
oid: ObjectIdentifier,
name: String,
description: String,
/// The Positive Integer Value object holding the machine room's number.
machine_room_id: ObjectIdentifier,
/// Group identifier (Unsigned8).
group_id: u8,
/// List of lift ObjectIdentifiers in this group.
group_members: Vec<ObjectIdentifier>,
/// Group mode.
group_mode: LiftGroupMode,
/// Active landing calls, served as the Landing_Calls BACnetLIST.
landing_calls: Vec<BACnetLandingCallStatus>,
/// The last call written to Landing_Call_Control.
landing_call_control: BACnetLandingCallStatus,
}
impl ElevatorGroupObject {
/// Create a new Elevator Group object with default values.
pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
let oid = ObjectIdentifier::new(ObjectType::ELEVATOR_GROUP, instance)?;
Ok(Self {
oid,
name: name.into(),
description: String::new(),
machine_room_id: ObjectIdentifier::new(
ObjectType::POSITIVE_INTEGER_VALUE,
ObjectIdentifier::MAX_INSTANCE,
)?,
group_id: 0,
group_members: Vec::new(),
group_mode: LiftGroupMode::UNKNOWN,
landing_calls: Vec::new(),
landing_call_control: landing_calls::initial_landing_call_control(),
})
}
/// The Positive Integer Value object served as Machine_Room_ID. Until the
/// application sets one it names instance 4194303, which Clause 12.58
/// uses when the machine room has no identification number.
pub fn machine_room_id(&self) -> ObjectIdentifier {
self.machine_room_id
}
/// Choose which Positive Integer Value object supplies this group's
/// machine-room number; clients read the number from that object's
/// Present_Value (Clause 12.58).
///
/// Machine_Room_ID is read-only over the network, so this is the only way
/// to change it. A reference to any other object type is refused with
/// VALUE_OUT_OF_RANGE and the property is left unchanged.
pub fn set_machine_room_id(&mut self, oid: ObjectIdentifier) -> Result<(), Error> {
if oid.object_type() != ObjectType::POSITIVE_INTEGER_VALUE {
return Err(common::value_out_of_range_error());
}
self.machine_room_id = oid;
Ok(())
}
/// Add a lift member to this elevator group.
pub fn add_member(&mut self, oid: ObjectIdentifier) {
self.group_members.push(oid);
}
/// The last landing call written to Landing_Call_Control, or a placeholder
/// of floor 0 with direction UNKNOWN before any write.
pub fn landing_call_control(&self) -> &BACnetLandingCallStatus {
&self.landing_call_control
}
/// The active landing calls served as Landing_Calls.
pub fn landing_calls(&self) -> &[BACnetLandingCallStatus] {
&self.landing_calls
}
/// Replace the active landing calls served as Landing_Calls.
///
/// The application owns this list: a Landing_Call_Control write doesn't
/// add to it. A call with a direction outside BACnetLiftCarDirection is
/// refused with VALUE_OUT_OF_RANGE and the list is left unchanged.
pub fn set_landing_calls(&mut self, calls: Vec<BACnetLandingCallStatus>) -> Result<(), Error> {
calls.iter().try_for_each(landing_calls::validate)?;
self.landing_calls = calls;
Ok(())
}
}
impl BACnetObject for ElevatorGroupObject {
fn object_identifier(&self) -> ObjectIdentifier {
self.oid
}
fn object_name(&self) -> &str {
&self.name
}
fn read_property(
&self,
property: PropertyIdentifier,
array_index: Option<u32>,
) -> Result<PropertyValue, Error> {
// No read_common_properties!: it would serve Status_Flags,
// Out_Of_Service and Reliability, which Table 12-76 doesn't define.
match property {
p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
Ok(PropertyValue::ObjectIdentifier(self.oid))
}
p if p == PropertyIdentifier::OBJECT_NAME => {
Ok(PropertyValue::CharacterString(self.name.clone()))
}
p if p == PropertyIdentifier::DESCRIPTION => {
Ok(PropertyValue::CharacterString(self.description.clone()))
}
p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
ObjectType::ELEVATOR_GROUP.to_raw(),
)),
p if p == PropertyIdentifier::PROPERTY_LIST => {
common::read_property_list_property(&self.property_list(), array_index)
}
p if p == PropertyIdentifier::MACHINE_ROOM_ID => {
Ok(PropertyValue::ObjectIdentifier(self.machine_room_id))
}
p if p == PropertyIdentifier::GROUP_ID => {
Ok(PropertyValue::Unsigned(u64::from(self.group_id)))
}
// A BACnetARRAY (Table 12-76): the whole array, its size at
// index 0, or one member.
p if p == PropertyIdentifier::GROUP_MEMBERS => common::read_array(
self.group_members
.iter()
.copied()
.map(PropertyValue::ObjectIdentifier)
.collect(),
array_index,
),
p if p == PropertyIdentifier::GROUP_MODE => {
Ok(PropertyValue::Enumerated(self.group_mode.to_raw()))
}
p if p == PropertyIdentifier::LANDING_CALLS => Ok(PropertyValue::List(
self.landing_calls
.iter()
.map(landing_calls::encode)
.collect::<Result<Vec<_>, Error>>()?,
)),
p if p == PropertyIdentifier::LANDING_CALL_CONTROL => {
landing_calls::encode(&self.landing_call_control)
}
_ => Err(common::unknown_property_error()),
}
}
fn write_property(
&mut self,
property: PropertyIdentifier,
_array_index: Option<u32>,
value: PropertyValue,
_priority: Option<u8>,
) -> Result<(), Error> {
if let Some(result) = common::write_description(&mut self.description, property, &value) {
return result;
}
match property {
p if p == PropertyIdentifier::GROUP_ID => {
if let PropertyValue::Unsigned(v) = value {
// Group_ID is an Unsigned8 (Table 12-76).
self.group_id =
u8::try_from(v).map_err(|_| common::value_out_of_range_error())?;
Ok(())
} else {
Err(common::invalid_data_type_error())
}
}
p if p == PropertyIdentifier::GROUP_MODE => {
if let PropertyValue::Enumerated(v) = value {
self.group_mode = LiftGroupMode::from_raw(v);
Ok(())
} else {
Err(common::invalid_data_type_error())
}
}
p if p == PropertyIdentifier::LANDING_CALL_CONTROL => {
self.landing_call_control = landing_calls::decode_write(value)?;
Ok(())
}
_ => Err(crate::common::unhandled_write_error(
self.property_metadata().as_ref(),
property,
_array_index,
)),
}
}
fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
metadata::for_elevator_group_object(self)
}
fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
}
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests;