Skip to main content

bacnet_objects/elevator/
lift.rs

1//! The Lift object (type 59, Clause 12.59).
2
3use bacnet_types::constructed::{
4    BACnetAssignedLandingCalls, BACnetDeviceObjectReference, BACnetLandingDoorStatus,
5    BACnetLiftCarCallList,
6};
7use bacnet_types::enums::{
8    DoorStatus, EngineeringUnits, LiftCarDirection, LiftCarDoorCommand, LiftCarDriveStatus,
9    LiftCarMode, LiftFault, ObjectType, PropertyIdentifier, Reliability,
10};
11use bacnet_types::error::Error;
12use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
13use std::borrow::Cow;
14
15use super::car_state::{self, CarState};
16use super::doors::CarDoors;
17use super::energy_meter::{energy_meter_accessors, EnergyMeter};
18use super::membership::{group_membership_accessors, GroupMembership};
19use super::{checked, decode_fault_signals, direction_in_range, metadata};
20use crate::common::{self, read_common_properties};
21use crate::traits::BACnetObject;
22
23/// BACnet Lift object — represents a single lift/elevator car.
24///
25/// The object serves the rows of its table (Clause 12.59, Table 12-77) that
26/// it implements, each with its table datatype. The per-door arrays
27/// (Car_Door_Status, Landing_Door_Status, Assigned_Landing_Calls,
28/// Making_Car_Call, Registered_Car_Call and Car_Door_Command) hold one
29/// element per car door and always have the same size, as Clause 12.59
30/// requires. They, and Car_Assigned_Direction, Car_Door_Zone, Car_Mode,
31/// Next_Stopping_Floor and Car_Drive_Status, are set by the application in
32/// service and take WriteProperty only while Out_Of_Service is TRUE.
33pub struct LiftObject {
34    oid: ObjectIdentifier,
35    name: String,
36    description: String,
37    /// Elevator_Group, Group_ID and Installation_ID.
38    membership: GroupMembership,
39    /// Floor_Text: one label per universal floor number, floor 1 first.
40    floor_text: Vec<String>,
41    /// Car_Position: the universal floor number of the car (Unsigned8).
42    car_position: u8,
43    /// Car moving direction (BACnetLiftCarDirection, Clause 21); proprietary
44    /// extensions (Clause 23.1) are preserved as raw values.
45    pub(super) car_moving_direction: LiftCarDirection,
46    /// The per-door arrays, one element per car door.
47    doors: CarDoors,
48    /// Car_Assigned_Direction, Car_Door_Zone, Car_Mode, Next_Stopping_Floor
49    /// and Car_Drive_Status.
50    car: CarState,
51    /// Car_Load (REAL), in `car_load_units`.
52    car_load: f32,
53    /// Car_Load_Units, present because Car_Load is.
54    car_load_units: EngineeringUnits,
55    /// Passenger alarm state (Boolean).
56    passenger_alarm: bool,
57    /// Energy_Meter and Energy_Meter_Ref.
58    energy_meter: EnergyMeter,
59    status_flags: StatusFlags,
60    out_of_service: bool,
61    reliability: Reliability,
62    /// Fault signal set (BACnetLiftFault, Clause 21).
63    fault_signals: Vec<LiftFault>,
64}
65
66impl LiftObject {
67    /// Create a new Lift object with the given number of floors.
68    ///
69    /// Floor text is initialized to "Floor 1", "Floor 2", etc. The car stands
70    /// at floor 1 with one door, whose status is UNKNOWN and which reports no
71    /// landing doors, calls or door command, and Car_Load is 0.0 percent.
72    /// Car_Assigned_Direction, Car_Mode and Car_Drive_Status start UNKNOWN,
73    /// Car_Door_Zone FALSE and Next_Stopping_Floor 1.
74    pub fn new(instance: u32, name: impl Into<String>, num_floors: usize) -> Result<Self, Error> {
75        let oid = ObjectIdentifier::new(ObjectType::LIFT, instance)?;
76        let floor_text = (1..=num_floors).map(|i| format!("Floor {i}")).collect();
77        Ok(Self {
78            oid,
79            name: name.into(),
80            description: String::new(),
81            membership: GroupMembership::new()?,
82            floor_text,
83            car_position: 1,
84            car_moving_direction: LiftCarDirection::STOPPED,
85            doors: CarDoors::new(),
86            car: CarState::new(),
87            car_load: 0.0,
88            car_load_units: EngineeringUnits::PERCENT,
89            passenger_alarm: false,
90            energy_meter: EnergyMeter::new()?,
91            status_flags: StatusFlags::empty(),
92            out_of_service: false,
93            reliability: Reliability::NO_FAULT_DETECTED,
94            fault_signals: Vec::new(),
95        })
96    }
97
98    group_membership_accessors!("lift");
99    energy_meter_accessors!("lift");
100
101    /// The status of each car door, served as Car_Door_Status.
102    pub fn car_door_status(&self) -> &[DoorStatus] {
103        &self.doors.status
104    }
105
106    /// Set the status of each car door, one element per door.
107    ///
108    /// Its size is the number of car doors, so a list of a different size
109    /// changes that number, and the other per-door arrays follow: a removed
110    /// door's elements are dropped, and an added door starts with no landing
111    /// doors, no assigned or registered calls, a Making_Car_Call of 0 and a
112    /// Car_Door_Command of NONE. A WriteProperty, accepted only while
113    /// Out_Of_Service is TRUE, can't change the size, so this is the only way
114    /// to. A status outside BACnetDoorStatus (a reserved value, or one above
115    /// 65535) is refused with VALUE_OUT_OF_RANGE and nothing changes.
116    pub fn set_car_door_status(&mut self, status: Vec<DoorStatus>) -> Result<(), Error> {
117        self.doors.set_status(status)
118    }
119
120    /// The landing doors of each car door, served as Landing_Door_Status.
121    pub fn landing_door_status(&self) -> &[BACnetLandingDoorStatus] {
122        &self.doors.landing
123    }
124
125    /// Set the landing doors of each car door, one element per car door.
126    ///
127    /// A list whose size differs from Car_Door_Status, or a landing door
128    /// status outside BACnetDoorStatus, is refused with VALUE_OUT_OF_RANGE
129    /// and the property is left unchanged. A WriteProperty, accepted only
130    /// while Out_Of_Service is TRUE, follows the same rules.
131    pub fn set_landing_door_status(
132        &mut self,
133        status: Vec<BACnetLandingDoorStatus>,
134    ) -> Result<(), Error> {
135        self.doors.set_landing(status)
136    }
137
138    /// The landing calls assigned to each car door, served as
139    /// Assigned_Landing_Calls.
140    pub fn assigned_landing_calls(&self) -> &[BACnetAssignedLandingCalls] {
141        &self.doors.landing_calls
142    }
143
144    /// Set the landing calls assigned to each car door, one element per car
145    /// door.
146    ///
147    /// A list whose size differs from Car_Door_Status, or a call whose
148    /// direction is outside BACnetLiftCarDirection, is refused with
149    /// VALUE_OUT_OF_RANGE and the property is left unchanged. A
150    /// WriteProperty, accepted only while Out_Of_Service is TRUE, follows the
151    /// same rules.
152    pub fn set_assigned_landing_calls(
153        &mut self,
154        calls: Vec<BACnetAssignedLandingCalls>,
155    ) -> Result<(), Error> {
156        self.doors.set_landing_calls(calls)
157    }
158
159    /// The last car call made for each car door, served as Making_Car_Call:
160    /// a universal floor number, or 0 when no call has been made.
161    pub fn making_car_call(&self) -> &[u8] {
162        &self.doors.making_car_call
163    }
164
165    /// Set the last car call made for each car door, one element per car
166    /// door.
167    ///
168    /// A list whose size differs from Car_Door_Status is refused with
169    /// VALUE_OUT_OF_RANGE and the property is left unchanged. A
170    /// WriteProperty, accepted only while Out_Of_Service is TRUE, follows the
171    /// same rule.
172    pub fn set_making_car_call(&mut self, floors: Vec<u8>) -> Result<(), Error> {
173        self.doors.set_making_car_call(floors)
174    }
175
176    /// The floors with a car call registered for each car door, served as
177    /// Registered_Car_Call.
178    pub fn registered_car_call(&self) -> &[BACnetLiftCarCallList] {
179        &self.doors.registered_car_call
180    }
181
182    /// Set the floors with a car call registered for each car door, one
183    /// element per car door.
184    ///
185    /// A list whose size differs from Car_Door_Status is refused with
186    /// VALUE_OUT_OF_RANGE and the property is left unchanged. A
187    /// WriteProperty, accepted only while Out_Of_Service is TRUE, follows the
188    /// same rule.
189    pub fn set_registered_car_call(
190        &mut self,
191        calls: Vec<BACnetLiftCarCallList>,
192    ) -> Result<(), Error> {
193        self.doors.set_registered_car_call(calls)
194    }
195
196    /// The pending command of each car door, served as Car_Door_Command.
197    pub fn car_door_command(&self) -> &[LiftCarDoorCommand] {
198        &self.doors.command
199    }
200
201    /// Set the pending command of each car door, one element per car door,
202    /// for instance back to NONE once a command has run.
203    ///
204    /// A list whose size differs from Car_Door_Status, or a command other
205    /// than NONE, OPEN or CLOSE, is refused with VALUE_OUT_OF_RANGE and the
206    /// property is left unchanged. A WriteProperty, accepted only while
207    /// Out_Of_Service is TRUE, follows the same rules.
208    pub fn set_car_door_command(&mut self, commands: Vec<LiftCarDoorCommand>) -> Result<(), Error> {
209        self.doors.set_command(commands)
210    }
211
212    /// The direction the car is assigned to move, served as
213    /// Car_Assigned_Direction.
214    pub fn car_assigned_direction(&self) -> LiftCarDirection {
215        self.car.assigned_direction
216    }
217
218    /// Set the direction the car is assigned to move.
219    ///
220    /// A direction outside BACnetLiftCarDirection is refused with
221    /// VALUE_OUT_OF_RANGE and the property is left unchanged.
222    pub fn set_car_assigned_direction(&mut self, direction: LiftCarDirection) -> Result<(), Error> {
223        self.car.assigned_direction = checked(direction, direction_in_range)?;
224        Ok(())
225    }
226
227    /// Whether the car sits in its door zone, served as Car_Door_Zone.
228    pub fn car_door_zone(&self) -> bool {
229        self.car.door_zone
230    }
231
232    /// Set whether the car sits in its door zone, close enough to a landing
233    /// that its doors may begin to open.
234    pub fn set_car_door_zone(&mut self, in_zone: bool) {
235        self.car.door_zone = in_zone;
236    }
237
238    /// The car's operating mode, served as Car_Mode.
239    pub fn car_mode(&self) -> LiftCarMode {
240        self.car.mode
241    }
242
243    /// Set the car's operating mode.
244    ///
245    /// A mode outside BACnetLiftCarMode is refused with VALUE_OUT_OF_RANGE
246    /// and the property is left unchanged.
247    pub fn set_car_mode(&mut self, mode: LiftCarMode) -> Result<(), Error> {
248        self.car.mode = checked(mode, car_state::mode_in_range)?;
249        Ok(())
250    }
251
252    /// The floor of the car's next stop (universal numbering), served as
253    /// Next_Stopping_Floor.
254    pub fn next_stopping_floor(&self) -> u8 {
255        self.car.next_stopping_floor
256    }
257
258    /// Set the floor of the car's next stop, or, while it stands still, the
259    /// floor it is at.
260    pub fn set_next_stopping_floor(&mut self, floor: u8) {
261        self.car.next_stopping_floor = floor;
262    }
263
264    /// The state of the car's motor drive, served as Car_Drive_Status.
265    pub fn car_drive_status(&self) -> LiftCarDriveStatus {
266        self.car.drive_status
267    }
268
269    /// Set the state of the car's motor drive.
270    ///
271    /// A status outside BACnetLiftCarDriveStatus is refused with
272    /// VALUE_OUT_OF_RANGE and the property is left unchanged.
273    pub fn set_car_drive_status(&mut self, status: LiftCarDriveStatus) -> Result<(), Error> {
274        self.car.drive_status = checked(status, car_state::drive_status_in_range)?;
275        Ok(())
276    }
277
278    /// The units of Car_Load, served as Car_Load_Units.
279    pub fn car_load_units(&self) -> EngineeringUnits {
280        self.car_load_units
281    }
282
283    /// Set the units Car_Load is measured in. A new Lift uses PERCENT.
284    ///
285    /// Car_Load_Units is read-only over the network. A value above 65535,
286    /// outside BACnetEngineeringUnits, is refused with VALUE_OUT_OF_RANGE and
287    /// the property is left unchanged.
288    pub fn set_car_load_units(&mut self, units: EngineeringUnits) -> Result<(), Error> {
289        if units.to_raw() > 65_535 {
290            return Err(common::value_out_of_range_error());
291        }
292        self.car_load_units = units;
293        Ok(())
294    }
295}
296
297impl BACnetObject for LiftObject {
298    fn object_identifier(&self) -> ObjectIdentifier {
299        self.oid
300    }
301
302    fn object_name(&self) -> &str {
303        &self.name
304    }
305
306    fn read_property(
307        &self,
308        property: PropertyIdentifier,
309        array_index: Option<u32>,
310    ) -> Result<PropertyValue, Error> {
311        if let Some(result) = read_common_properties!(self, property, array_index) {
312            return result;
313        }
314        if let Some(value) = self.membership.read(property) {
315            return Ok(value);
316        }
317        if let Some(value) = self.energy_meter.read(property) {
318            return Ok(value);
319        }
320        if let Some(value) = self.car.read(property) {
321            return Ok(value);
322        }
323        if let Some(result) = self.doors.read(property, array_index) {
324            return result;
325        }
326        match property {
327            p if p == PropertyIdentifier::OBJECT_TYPE => {
328                Ok(PropertyValue::Enumerated(ObjectType::LIFT.to_raw()))
329            }
330            p if p == PropertyIdentifier::FLOOR_TEXT => common::read_array(
331                self.floor_text
332                    .iter()
333                    .cloned()
334                    .map(PropertyValue::CharacterString)
335                    .collect(),
336                array_index,
337            ),
338            p if p == PropertyIdentifier::CAR_POSITION => {
339                Ok(PropertyValue::Unsigned(self.car_position.into()))
340            }
341            p if p == PropertyIdentifier::CAR_MOVING_DIRECTION => Ok(PropertyValue::Enumerated(
342                self.car_moving_direction.to_raw(),
343            )),
344            p if p == PropertyIdentifier::CAR_LOAD => Ok(PropertyValue::Real(self.car_load)),
345            p if p == PropertyIdentifier::CAR_LOAD_UNITS => {
346                Ok(PropertyValue::Enumerated(self.car_load_units.to_raw()))
347            }
348            p if p == PropertyIdentifier::PASSENGER_ALARM => {
349                Ok(PropertyValue::Boolean(self.passenger_alarm))
350            }
351            p if p == PropertyIdentifier::FAULT_SIGNALS => Ok(PropertyValue::List(
352                self.fault_signals
353                    .iter()
354                    .map(|fault| PropertyValue::Enumerated(fault.to_raw()))
355                    .collect(),
356            )),
357            _ => Err(common::unknown_property_error()),
358        }
359    }
360
361    fn write_property(
362        &mut self,
363        property: PropertyIdentifier,
364        array_index: Option<u32>,
365        value: PropertyValue,
366        _priority: Option<u8>,
367    ) -> Result<(), Error> {
368        if let Some(result) =
369            common::write_out_of_service(&mut self.out_of_service, property, &value)
370        {
371            return result;
372        }
373        if let Some(result) = common::write_description(&mut self.description, property, &value) {
374            return result;
375        }
376        if let Some(result) = self.energy_meter.write(property, &value) {
377            return result;
378        }
379        // Simulation writes (items (c) and (d) of the Out_Of_Service
380        // description), refused in service. None can change the car door
381        // count, so the per-door arrays keep the same size.
382        if let Some(result) = self.car.write(property, &value, self.out_of_service) {
383            return result;
384        }
385        if let Some(result) = self
386            .doors
387            .write(property, array_index, &value, self.out_of_service)
388        {
389            return result;
390        }
391        match property {
392            p if p == PropertyIdentifier::CAR_POSITION => {
393                if let PropertyValue::Unsigned(v) = value {
394                    // Car_Position is an Unsigned8 (Table 12-77).
395                    self.car_position =
396                        u8::try_from(v).map_err(|_| common::value_out_of_range_error())?;
397                    Ok(())
398                } else {
399                    Err(common::invalid_data_type_error())
400                }
401            }
402            p if p == PropertyIdentifier::CAR_MOVING_DIRECTION => {
403                if let PropertyValue::Enumerated(v) = value {
404                    self.car_moving_direction =
405                        checked(LiftCarDirection::from_raw(v), direction_in_range)?;
406                    Ok(())
407                } else {
408                    Err(common::invalid_data_type_error())
409                }
410            }
411            p if p == PropertyIdentifier::CAR_LOAD => {
412                if let PropertyValue::Real(v) = value {
413                    common::reject_non_finite(v)?;
414                    self.car_load = v;
415                    Ok(())
416                } else {
417                    Err(common::invalid_data_type_error())
418                }
419            }
420            p if p == PropertyIdentifier::PASSENGER_ALARM => {
421                if let PropertyValue::Boolean(v) = value {
422                    self.passenger_alarm = v;
423                    Ok(())
424                } else {
425                    Err(common::invalid_data_type_error())
426                }
427            }
428            p if p == PropertyIdentifier::FAULT_SIGNALS => {
429                self.fault_signals =
430                    decode_fault_signals(value, LiftFault::ALL_NAMED, LiftFault::from_raw)?;
431                Ok(())
432            }
433            _ => Err(crate::common::unhandled_write_error(
434                self.property_metadata().as_ref(),
435                property,
436                array_index,
437            )),
438        }
439    }
440
441    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
442        metadata::for_lift_object(self)
443    }
444
445    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
446        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
447    }
448}