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
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
//! Command object (type 7) per ASHRAE 135-2020 Clause 12.10.
//!
//! Writing Present_Value selects one of the Action lists, each a typed
//! [`BACnetActionList`], and starts it. The object owns the bookkeeping:
//! the range check, the busy interlock while a list runs, In_Process,
//! All_Writes_Successful and each command's write-successful flag. The writes
//! themselves are the bundled server's: it takes the queued [`CommandRun`]
//! under the guard that committed the Present_Value write and makes each
//! write once that guard is released.
use bacnet_encoding::constructed::encode_action_list;
use bacnet_types::constructed::{BACnetActionCommand, BACnetActionList};
use bacnet_types::enums::{ErrorClass, ErrorCode, ObjectType, PropertyIdentifier, Reliability};
use bacnet_types::error::Error;
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
use bytes::BytesMut;
use std::borrow::Cow;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use crate::common::{self, read_common_properties};
use crate::traits::BACnetObject;
mod metadata;
/// The writes a Present_Value write queued, for the server to make: a
/// Command object's selected list, or a Channel object's value for each of
/// its members.
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq)]
pub struct CommandRun {
/// The commanding object; its generation guards every report back.
pub source: ObjectIdentifier,
/// The generation the write started.
pub generation: u64,
/// What the run writes.
pub plan: RunPlan,
/// The objects whose runs led to this one, outermost first. The object
/// leaves it empty; the server fills it in when one run's write starts
/// another, so a run that would start its own object again is caught.
pub chain: Arc<[ObjectIdentifier]>,
}
/// The next run generation. One counter serves every Command and Channel in
/// the process, so a run left over from an object that was removed never
/// matches the object that took its place.
pub(crate) fn next_generation() -> u64 {
static NEXT: AtomicU64 = AtomicU64::new(1);
NEXT.fetch_add(1, Ordering::Relaxed)
}
/// What a [`CommandRun`] writes.
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq)]
pub enum RunPlan {
/// A Command object's selected list: its commands, in the order they're
/// made (Clause 12.10).
Actions(Vec<BACnetActionCommand>),
/// A Channel object's value and the members it goes to (Clause 12.53).
Channel(crate::channel::ChannelDistribution),
}
/// Why a write a [`CommandRun`] made, or meant to make, failed. A Channel
/// reports the first failure of a run in its Reliability (Clause 12.53.9); a
/// Command keeps only whether each write succeeded.
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteFailure {
/// The target can't take the value as configured: the Channel can't
/// coerce the value to the member's datatype, or the target says the
/// object or property doesn't exist, or that it takes no value of this
/// datatype.
Configuration,
/// The target turned the write down for another reason, or the run
/// couldn't make it: it ended first, or the request couldn't be built or
/// sent from here.
Process,
/// The target's device couldn't be reached: no binding for it, initiation
/// disabled by DeviceCommunicationControl, or no answer to any attempt.
Communication,
}
impl WriteFailure {
/// The Reliability a Channel whose run failed this way reports.
pub fn reliability(self) -> Reliability {
match self {
Self::Configuration => Reliability::CONFIGURATION_ERROR,
Self::Process => Reliability::PROCESS_ERROR,
Self::Communication => Reliability::COMMUNICATION_FAILURE,
}
}
}
/// BACnet Command object: runs the action list Present_Value selects.
pub struct CommandObject {
oid: ObjectIdentifier,
name: String,
description: String,
present_value: u64,
in_process: bool,
all_writes_successful: bool,
action: Vec<BACnetActionList>,
action_text: Option<Vec<String>>,
status_flags: StatusFlags,
reliability: Reliability,
/// A fresh [`next_generation`] at every Present_Value write and Action
/// change.
generation: u64,
/// The Action element (zero-based) whose commands are being made.
running: Option<usize>,
pending_run: Option<CommandRun>,
}
impl CommandObject {
/// Create a new Command object with default values.
pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
let oid = ObjectIdentifier::new(ObjectType::COMMAND, instance)?;
Ok(Self {
oid,
name: name.into(),
description: String::new(),
present_value: 0,
in_process: false,
all_writes_successful: true,
action: Vec::new(),
action_text: None,
status_flags: StatusFlags::empty(),
reliability: Reliability::NO_FAULT_DETECTED,
generation: next_generation(),
running: None,
pending_run: None,
})
}
/// Replace the Action array: element N is the list Present_Value N
/// selects (Clause 12.10.8).
///
/// Refused with PROPERTY / VALUE_OUT_OF_RANGE, leaving Action as it was,
/// when a command's device identifier isn't a Device object (#1308) or a
/// command can't be encoded: a priority outside 1..=16, or a value with no
/// encoding. A present Action_Text follows the new size (Clause 12.10.9),
/// keeping its leading texts and adding empty ones. A run in progress is
/// abandoned: its remaining writes aren't made and In_Process returns to
/// FALSE.
pub fn set_action(&mut self, action: Vec<BACnetActionList>) -> Result<(), Error> {
let mut scratch = BytesMut::new();
for list in &action {
for command in &list.commands {
crate::device_reference::check_device_member(command.device_identifier)?;
}
encode_action_list(&mut scratch, list)
.map_err(|_| common::value_out_of_range_error())?;
scratch.clear();
}
if let Some(texts) = &mut self.action_text {
texts.resize(action.len(), String::new());
}
self.action = action;
self.generation = next_generation();
self.in_process = false;
self.running = None;
self.pending_run = None;
Ok(())
}
/// Serve Action_Text, one description per Action list (Clause 12.10.9).
///
/// Refused with PROPERTY / VALUE_OUT_OF_RANGE, leaving the property as it
/// was, unless there's exactly one text per Action element, so set Action
/// first. The property is read-only on the network.
pub fn set_action_text(&mut self, texts: Vec<String>) -> Result<(), Error> {
if texts.len() != self.action.len() {
return Err(common::value_out_of_range_error());
}
self.action_text = Some(texts);
Ok(())
}
/// Start the list `selected` names; zero or an empty list completes at
/// once (Clauses 12.10.5 to 12.10.8).
fn select(&mut self, selected: u64) -> Result<(), Error> {
// Refusing a write while a list runs also stops a Command whose list
// writes its own Present_Value from starting over (Clause 12.10).
if self.in_process {
return Err(common::protocol_error(ErrorClass::OBJECT, ErrorCode::BUSY));
}
let index = match usize::try_from(selected) {
Ok(0) => None,
Ok(n) if n <= self.action.len() => Some(n - 1),
_ => return Err(common::value_out_of_range_error()),
};
self.present_value = selected;
self.generation = next_generation();
let commands = index.map_or_else(Vec::new, |i| self.action[i].commands.clone());
if commands.is_empty() {
self.all_writes_successful = true;
return Ok(());
}
self.in_process = true;
self.all_writes_successful = false;
self.running = index;
self.pending_run = Some(CommandRun {
source: self.oid,
generation: self.generation,
plan: RunPlan::Actions(commands),
chain: Arc::from([]),
});
Ok(())
}
}
impl BACnetObject for CommandObject {
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> {
// Table 12-12 has no Out_Of_Service (#1064), and Clause 12.10 holds the
// OUT_OF_SERVICE flag FALSE.
if let Some(result) =
read_common_properties!(self, property, array_index, no_out_of_service)
{
return result;
}
match property {
p if p == PropertyIdentifier::OBJECT_TYPE => {
Ok(PropertyValue::Enumerated(ObjectType::COMMAND.to_raw()))
}
p if p == PropertyIdentifier::PRESENT_VALUE => {
Ok(PropertyValue::Unsigned(self.present_value))
}
p if p == PropertyIdentifier::IN_PROCESS => Ok(PropertyValue::Boolean(self.in_process)),
p if p == PropertyIdentifier::ALL_WRITES_SUCCESSFUL => {
Ok(PropertyValue::Boolean(self.all_writes_successful))
}
// BACnetARRAY[N] of BACnetActionList (Table 12-12): each element
// goes out framed in its own [0] pair, so one read alone carries
// the same octets the whole-array read concatenates.
p if p == PropertyIdentifier::ACTION => {
let lists = self
.action
.iter()
.map(|list| {
let mut encoded = BytesMut::new();
encode_action_list(&mut encoded, list)?;
Ok(PropertyValue::ApplicationData(encoded.to_vec()))
})
.collect::<Result<Vec<_>, Error>>()?;
common::read_array(lists, array_index)
}
p if p == PropertyIdentifier::ACTION_TEXT => match &self.action_text {
Some(texts) => common::read_array(
texts
.iter()
.cloned()
.map(PropertyValue::CharacterString)
.collect(),
array_index,
),
None => Err(common::unknown_property_error()),
},
_ => 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::PRESENT_VALUE => match value {
PropertyValue::Unsigned(selected) => self.select(selected),
_ => Err(common::invalid_data_type_error()),
},
// Action and a present Action_Text are read-only from the network;
// unhandled_write_error answers both from the metadata.
_ => 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_object(self)
}
fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
}
fn supports_subscribe_cov_property(&self) -> bool {
// Table 13-1 has no Command row, so SubscribeCOV stays refused. A
// property subscription follows Table 13-1a, which lets a client watch
// In_Process and All_Writes_Successful through a run.
true
}
fn take_command_run_internal(&mut self) -> Option<CommandRun> {
self.pending_run.take()
}
fn command_generation_internal(&self) -> Option<u64> {
Some(self.generation)
}
fn record_command_write_internal(
&mut self,
generation: u64,
command: usize,
success: bool,
) -> bool {
if generation != self.generation || !self.in_process {
return false;
}
let Some(list) = self.running.and_then(|index| self.action.get_mut(index)) else {
return false;
};
let Some(made) = list.commands.get_mut(command) else {
return false;
};
made.write_successful = success;
// A failed command that quits on failure ends the list: the commands
// after it are never made and read as unsuccessful (Clause 12.10.8).
if !success && made.quit_on_failure {
for skipped in &mut list.commands[command + 1..] {
skipped.write_successful = false;
}
}
true
}
fn complete_command_run_internal(
&mut self,
generation: u64,
outcome: Result<(), WriteFailure>,
) -> bool {
if generation != self.generation || !self.in_process {
return false;
}
self.in_process = false;
// A failed write leaves Reliability alone (Clause 12.10.13).
self.all_writes_successful = outcome.is_ok();
self.running = None;
true
}
}
#[cfg(test)]
mod tests;