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