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}