Skip to main content

bacnet_objects/
command.rs

1//! Command object (type 7) per ASHRAE 135-2020 Clause 12.10.
2//!
3//! Writing Present_Value selects one of the Action lists, each a typed
4//! [`BACnetActionList`], and starts it. The object owns the bookkeeping:
5//! the range check, the busy interlock while a list runs, In_Process,
6//! All_Writes_Successful and each command's write-successful flag. The writes
7//! themselves are the bundled server's: it takes the queued [`CommandRun`]
8//! under the guard that committed the Present_Value write and makes each
9//! write once that guard is released.
10
11use crate::prelude::*;
12use alloc::borrow::Cow;
13use alloc::sync::Arc;
14use bacnet_encoding::constructed::encode_action_list;
15use bacnet_types::constructed::{BACnetActionCommand, BACnetActionList};
16use bacnet_types::enums::{ErrorClass, ErrorCode, ObjectType, PropertyIdentifier, Reliability};
17use bacnet_types::error::Error;
18use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
19use bytes::BytesMut;
20use core::sync::atomic::Ordering;
21
22use crate::sync::AtomicU64;
23
24use crate::common::{self, read_common_properties};
25use crate::traits::BACnetObject;
26
27mod metadata;
28
29/// The writes a Present_Value write queued, for the server to make: a
30/// Command object's selected list, or a Channel object's value for each of
31/// its members.
32#[doc(hidden)]
33#[derive(Debug, Clone, PartialEq)]
34pub struct CommandRun {
35    /// The commanding object; its generation guards every report back.
36    pub source: ObjectIdentifier,
37    /// The generation the write started.
38    pub generation: u64,
39    /// What the run writes.
40    pub plan: RunPlan,
41    /// The objects whose runs led to this one, outermost first. The object
42    /// leaves it empty; the server fills it in when one run's write starts
43    /// another, so a run that would start its own object again is caught.
44    pub chain: Arc<[ObjectIdentifier]>,
45}
46
47/// The next run generation. One counter serves every Command and Channel in
48/// the process, so a run left over from an object that was removed never
49/// matches the object that took its place.
50pub(crate) fn next_generation() -> u64 {
51    static NEXT: AtomicU64 = AtomicU64::new(1);
52    NEXT.fetch_add(1, Ordering::Relaxed)
53}
54
55/// What a [`CommandRun`] writes.
56#[doc(hidden)]
57#[derive(Debug, Clone, PartialEq)]
58pub enum RunPlan {
59    /// A Command object's selected list: its commands, in the order they're
60    /// made (Clause 12.10).
61    Actions(Vec<BACnetActionCommand>),
62    /// A Channel object's value and the members it goes to (Clause 12.53).
63    Channel(crate::channel::ChannelDistribution),
64}
65
66/// Why a write a [`CommandRun`] made, or meant to make, failed. A Channel
67/// reports the first failure of a run in its Reliability (Clause 12.53.9); a
68/// Command keeps only whether each write succeeded.
69#[doc(hidden)]
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum WriteFailure {
72    /// The target can't take the value as configured: the Channel can't
73    /// coerce the value to the member's datatype, or the target says the
74    /// object or property doesn't exist, or that it takes no value of this
75    /// datatype.
76    Configuration,
77    /// The target turned the write down for another reason, or the run
78    /// couldn't make it: it ended first, or the request couldn't be built or
79    /// sent from here.
80    Process,
81    /// The target's device couldn't be reached: no binding for it, initiation
82    /// disabled by DeviceCommunicationControl, or no answer to any attempt.
83    Communication,
84}
85
86impl WriteFailure {
87    /// The Reliability a Channel whose run failed this way reports.
88    pub fn reliability(self) -> Reliability {
89        match self {
90            Self::Configuration => Reliability::CONFIGURATION_ERROR,
91            Self::Process => Reliability::PROCESS_ERROR,
92            Self::Communication => Reliability::COMMUNICATION_FAILURE,
93        }
94    }
95}
96
97/// BACnet Command object: runs the action list Present_Value selects.
98pub struct CommandObject {
99    oid: ObjectIdentifier,
100    name: String,
101    description: String,
102    present_value: u64,
103    in_process: bool,
104    all_writes_successful: bool,
105    action: Vec<BACnetActionList>,
106    action_text: Option<Vec<String>>,
107    status_flags: StatusFlags,
108    reliability: Reliability,
109    /// A fresh [`next_generation`] at every Present_Value write and Action
110    /// change.
111    generation: u64,
112    /// The Action element (zero-based) whose commands are being made.
113    running: Option<usize>,
114    pending_run: Option<CommandRun>,
115}
116
117impl CommandObject {
118    /// Create a new Command object with default values.
119    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
120        let oid = ObjectIdentifier::new(ObjectType::COMMAND, instance)?;
121        Ok(Self {
122            oid,
123            name: name.into(),
124            description: String::new(),
125            present_value: 0,
126            in_process: false,
127            all_writes_successful: true,
128            action: Vec::new(),
129            action_text: None,
130            status_flags: StatusFlags::empty(),
131            reliability: Reliability::NO_FAULT_DETECTED,
132            generation: next_generation(),
133            running: None,
134            pending_run: None,
135        })
136    }
137
138    /// Replace the Action array: element N is the list Present_Value N
139    /// selects (Clause 12.10.8).
140    ///
141    /// Refused with PROPERTY / VALUE_OUT_OF_RANGE, leaving Action as it was,
142    /// when a command's device identifier isn't a Device object (#1308) or a
143    /// command can't be encoded: a priority outside 1..=16, or a value with no
144    /// encoding. A present Action_Text follows the new size (Clause 12.10.9),
145    /// keeping its leading texts and adding empty ones. A run in progress is
146    /// abandoned: its remaining writes aren't made and In_Process returns to
147    /// FALSE.
148    pub fn set_action(&mut self, action: Vec<BACnetActionList>) -> Result<(), Error> {
149        let mut scratch = BytesMut::new();
150        for list in &action {
151            for command in &list.commands {
152                crate::device_reference::check_device_member(command.device_identifier)?;
153            }
154            encode_action_list(&mut scratch, list)
155                .map_err(|_| common::value_out_of_range_error())?;
156            scratch.clear();
157        }
158        if let Some(texts) = &mut self.action_text {
159            texts.resize(action.len(), String::new());
160        }
161        self.action = action;
162        self.generation = next_generation();
163        self.in_process = false;
164        self.running = None;
165        self.pending_run = None;
166        Ok(())
167    }
168
169    /// Serve Action_Text, one description per Action list (Clause 12.10.9).
170    ///
171    /// Refused with PROPERTY / VALUE_OUT_OF_RANGE, leaving the property as it
172    /// was, unless there's exactly one text per Action element, so set Action
173    /// first. The property is read-only on the network.
174    pub fn set_action_text(&mut self, texts: Vec<String>) -> Result<(), Error> {
175        if texts.len() != self.action.len() {
176            return Err(common::value_out_of_range_error());
177        }
178        self.action_text = Some(texts);
179        Ok(())
180    }
181
182    /// Start the list `selected` names; zero or an empty list completes at
183    /// once (Clauses 12.10.5 to 12.10.8).
184    fn select(&mut self, selected: u64) -> Result<(), Error> {
185        // Refusing a write while a list runs also stops a Command whose list
186        // writes its own Present_Value from starting over (Clause 12.10).
187        if self.in_process {
188            return Err(common::protocol_error(ErrorClass::OBJECT, ErrorCode::BUSY));
189        }
190        let index = match usize::try_from(selected) {
191            Ok(0) => None,
192            Ok(n) if n <= self.action.len() => Some(n - 1),
193            _ => return Err(common::value_out_of_range_error()),
194        };
195        self.present_value = selected;
196        self.generation = next_generation();
197        let commands = index.map_or_else(Vec::new, |i| self.action[i].commands.clone());
198        if commands.is_empty() {
199            self.all_writes_successful = true;
200            return Ok(());
201        }
202        self.in_process = true;
203        self.all_writes_successful = false;
204        self.running = index;
205        self.pending_run = Some(CommandRun {
206            source: self.oid,
207            generation: self.generation,
208            plan: RunPlan::Actions(commands),
209            chain: Arc::from([]),
210        });
211        Ok(())
212    }
213}
214
215impl BACnetObject for CommandObject {
216    fn object_identifier(&self) -> ObjectIdentifier {
217        self.oid
218    }
219
220    fn object_name(&self) -> &str {
221        &self.name
222    }
223
224    fn read_property(
225        &self,
226        property: PropertyIdentifier,
227        array_index: Option<u32>,
228    ) -> Result<PropertyValue, Error> {
229        // Table 12-12 has no Out_Of_Service (#1064), and Clause 12.10 holds the
230        // OUT_OF_SERVICE flag FALSE.
231        if let Some(result) =
232            read_common_properties!(self, property, array_index, no_out_of_service)
233        {
234            return result;
235        }
236        match property {
237            p if p == PropertyIdentifier::OBJECT_TYPE => {
238                Ok(PropertyValue::Enumerated(ObjectType::COMMAND.to_raw()))
239            }
240            p if p == PropertyIdentifier::PRESENT_VALUE => {
241                Ok(PropertyValue::Unsigned(self.present_value))
242            }
243            p if p == PropertyIdentifier::IN_PROCESS => Ok(PropertyValue::Boolean(self.in_process)),
244            p if p == PropertyIdentifier::ALL_WRITES_SUCCESSFUL => {
245                Ok(PropertyValue::Boolean(self.all_writes_successful))
246            }
247            // BACnetARRAY[N] of BACnetActionList (Table 12-12): each element
248            // goes out framed in its own [0] pair, so one read alone carries
249            // the same octets the whole-array read concatenates.
250            p if p == PropertyIdentifier::ACTION => {
251                let lists = self
252                    .action
253                    .iter()
254                    .map(|list| {
255                        let mut encoded = BytesMut::new();
256                        encode_action_list(&mut encoded, list)?;
257                        Ok(PropertyValue::ApplicationData(encoded.to_vec()))
258                    })
259                    .collect::<Result<Vec<_>, Error>>()?;
260                common::read_array(lists, array_index)
261            }
262            p if p == PropertyIdentifier::ACTION_TEXT => match &self.action_text {
263                Some(texts) => common::read_array(
264                    texts
265                        .iter()
266                        .cloned()
267                        .map(PropertyValue::CharacterString)
268                        .collect(),
269                    array_index,
270                ),
271                None => Err(common::unknown_property_error()),
272            },
273            _ => Err(common::unknown_property_error()),
274        }
275    }
276
277    fn write_property(
278        &mut self,
279        property: PropertyIdentifier,
280        array_index: Option<u32>,
281        value: PropertyValue,
282        _priority: Option<u8>,
283    ) -> Result<(), Error> {
284        if let Some(result) = common::write_description(&mut self.description, property, &value) {
285            return result;
286        }
287        match property {
288            p if p == PropertyIdentifier::PRESENT_VALUE => match value {
289                PropertyValue::Unsigned(selected) => self.select(selected),
290                _ => Err(common::invalid_data_type_error()),
291            },
292            // Action and a present Action_Text are read-only from the network;
293            // unhandled_write_error answers both from the metadata.
294            _ => Err(crate::common::unhandled_write_error(
295                self.property_metadata().as_ref(),
296                property,
297                array_index,
298            )),
299        }
300    }
301
302    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
303        metadata::for_object(self)
304    }
305
306    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
307        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
308    }
309
310    fn supports_subscribe_cov_property(&self) -> bool {
311        // Table 13-1 has no Command row, so SubscribeCOV stays refused. A
312        // property subscription follows Table 13-1a, which lets a client watch
313        // In_Process and All_Writes_Successful through a run.
314        true
315    }
316
317    fn take_command_run_internal(&mut self) -> Option<CommandRun> {
318        self.pending_run.take()
319    }
320
321    fn command_generation_internal(&self) -> Option<u64> {
322        Some(self.generation)
323    }
324
325    fn record_command_write_internal(
326        &mut self,
327        generation: u64,
328        command: usize,
329        success: bool,
330    ) -> bool {
331        if generation != self.generation || !self.in_process {
332            return false;
333        }
334        let Some(list) = self.running.and_then(|index| self.action.get_mut(index)) else {
335            return false;
336        };
337        let Some(made) = list.commands.get_mut(command) else {
338            return false;
339        };
340        made.write_successful = success;
341        // A failed command that quits on failure ends the list: the commands
342        // after it are never made and read as unsuccessful (Clause 12.10.8).
343        if !success && made.quit_on_failure {
344            for skipped in &mut list.commands[command + 1..] {
345                skipped.write_successful = false;
346            }
347        }
348        true
349    }
350
351    fn complete_command_run_internal(
352        &mut self,
353        generation: u64,
354        outcome: Result<(), WriteFailure>,
355    ) -> bool {
356        if generation != self.generation || !self.in_process {
357            return false;
358        }
359        self.in_process = false;
360        // A failed write leaves Reliability alone (Clause 12.10.13).
361        self.all_writes_successful = outcome.is_ok();
362        self.running = None;
363        true
364    }
365}
366
367#[cfg(test)]
368mod tests;