Skip to main content

bacnet_ops/
class.rs

1//! What each command does to a device: read, write or device management
2//! (#972). One table serves the one-shot commands and the TUI's safety
3//! levels (#964), so a command can't be guarded in one front end and not in
4//! another.
5//!
6//! - **Read**: discovery, reads, finite COV subscriptions and
7//!   foreign-device registration, plus the commands that never reach a
8//!   device (`mstp-ports`, `capture`, `sc-check`, `help`), and `tui` and
9//!   `mcp`, which guard their own writes. A
10//!   subscription and a registration do leave an entry on the remote side,
11//!   but only to deliver what the client watches, and it lapses on its own:
12//!   `subscribe` cancels its subscription when it stops, and a registration
13//!   runs out with its TTL. The TUI makes both from its read-only level.
14//! - **Write**: anything that changes a value, an object, a file, an
15//!   alarm's acknowledgment, the device's clock or a BBMD table, and a
16//!   subscription that never expires ([`of_subscribe`]).
17//! - **Device management**: communication control and reinitialize, which
18//!   change whether and how the device runs at all.
19
20use serde::Serialize;
21
22/// What a command does to a device.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
24#[serde(rename_all = "kebab-case")]
25pub enum Class {
26    /// Observes only.
27    Read,
28    /// Changes a value, an object, a file or a table.
29    Write,
30    /// Changes whether or how the device runs.
31    DeviceManagement,
32}
33
34impl Class {
35    /// The name in messages and JSON.
36    pub fn name(self) -> &'static str {
37        match self {
38            Self::Read => "read",
39            Self::Write => "write",
40            Self::DeviceManagement => "device-management",
41        }
42    }
43}
44
45use Class::{DeviceManagement, Read, Write};
46
47/// Every subcommand, aliases included, with its class. A test in
48/// `bacnet-cli` checks this against the clap definitions in both
49/// directions.
50pub const COMMANDS: &[(&str, Class)] = &[
51    // Discovery and reads.
52    ("discover", Read),
53    ("whois", Read),
54    ("find", Read),
55    ("whohas", Read),
56    ("read", Read),
57    ("rp", Read),
58    ("readm", Read),
59    ("rpm", Read),
60    ("read-range", Read),
61    ("rr", Read),
62    ("alarms", Read),
63    ("alarm-summary", Read),
64    ("enrollment-summary", Read),
65    ("file-read", Read),
66    ("bdt", Read),
67    ("fdt", Read),
68    ("whois-router", Read),
69    ("subscribe", Read),
70    ("cov", Read),
71    ("register", Read),
72    // Nothing reaches a device, or a front end whose own writes are
73    // guarded one by one.
74    ("mstp-ports", Read),
75    ("capture", Read),
76    ("sc-check", Read),
77    ("tui", Read),
78    ("mcp", Read),
79    ("help", Read),
80    // Writes.
81    ("write", Write),
82    ("wp", Write),
83    ("writem", Write),
84    ("wpm", Write),
85    ("create-object", Write),
86    ("delete-object", Write),
87    ("file-write", Write),
88    ("ack-alarm", Write),
89    ("ack", Write),
90    ("time-sync", Write),
91    ("ts", Write),
92    // Delete-Foreign-Device-Table-Entry: the CLI names its own address,
93    // but the BBMD removes whichever entry has it.
94    ("unregister", Write),
95    // Device management.
96    ("control", DeviceManagement),
97    ("dcc", DeviceManagement),
98    ("reinit", DeviceManagement),
99];
100
101/// The class of `subscribe` (`cov`) asking for `lifetime` seconds. A
102/// finite subscription is renewed while the CLI watches and cancelled when
103/// it stops, so it is a read; one of 0 never expires (Clause 13.14.1.1.4)
104/// and outlives the CLI, so it is a write.
105pub fn of_subscribe(lifetime: u32) -> Class {
106    if lifetime == 0 { Write } else { Read }
107}
108
109/// The class of a subcommand, by name or alias;
110/// `None` for a word that is neither.
111pub fn of(command: &str) -> Option<Class> {
112    COMMANDS
113        .iter()
114        .find(|(name, _)| *name == command)
115        .map(|&(_, class)| class)
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    /// The classes themselves, pinned: a change here is a safety change.
123    #[test]
124    fn the_mutating_commands_are_pinned() {
125        let mutating: Vec<(&str, &str)> = COMMANDS
126            .iter()
127            .filter(|(_, class)| *class != Read)
128            .map(|&(name, class)| (name, class.name()))
129            .collect();
130        assert_eq!(
131            mutating,
132            [
133                ("write", "write"),
134                ("wp", "write"),
135                ("writem", "write"),
136                ("wpm", "write"),
137                ("create-object", "write"),
138                ("delete-object", "write"),
139                ("file-write", "write"),
140                ("ack-alarm", "write"),
141                ("ack", "write"),
142                ("time-sync", "write"),
143                ("ts", "write"),
144                ("unregister", "write"),
145                ("control", "device-management"),
146                ("dcc", "device-management"),
147                ("reinit", "device-management"),
148            ]
149        );
150        assert_eq!(of("subscribe"), Some(Read));
151        assert_eq!(of_subscribe(300), Read);
152        assert_eq!(of_subscribe(1), Read);
153        assert_eq!(of_subscribe(0), Write);
154        assert_eq!(of("register"), Some(Read));
155        assert_eq!(of("mcp"), Some(Read));
156        assert_eq!(of("no-such-command"), None);
157    }
158}