Skip to main content

bacnet_objects/elevator/
lift.rs

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