pub(crate) mod audit;
pub(crate) mod ledger;
pub(crate) mod lock;
pub(crate) mod plan;
use bacnet_types::enums::{ErrorClass, ErrorCode, PropertyIdentifier};
use bacnet_types::error::Error;
use bacnet_types::primitives::ObjectIdentifier;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use self::ledger::Entry;
use super::profile::Safety;
use crate::core::class::Class;
pub(crate) const DEFAULT_MINUTES: u32 = 15;
pub(crate) const MAX_MINUTES: u32 = 480;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Level {
Passive,
ReadOnly,
Armed {
dangerous: bool,
},
Expired,
}
impl Level {
pub(crate) fn permits(self, class: Class) -> bool {
match (self, class) {
(Self::Passive, _) => false,
(_, Class::Read) => true,
(Self::Armed { .. }, Class::Write) => true,
(Self::Armed { dangerous }, Class::DeviceManagement) => dangerous,
(Self::ReadOnly | Self::Expired, _) => false,
}
}
pub(crate) fn audit_name(self) -> &'static str {
match self {
Self::Passive => "passive",
Self::ReadOnly => "read-only",
Self::Armed { dangerous: false } => "write-session",
Self::Armed { dangerous: true } => "write-session+dangerous",
Self::Expired => "write-session (expired)",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum Tier {
T1,
T2,
T3,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Gate {
Allowed(Tier),
Blocked(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Stored {
#[serde(serialize_with = "to_hex", deserialize_with = "from_hex")]
pub(crate) bytes: Vec<u8>,
pub(crate) text: String,
}
pub(crate) const NULL: [u8; 1] = [0x00];
impl Stored {
pub(crate) fn null() -> Self {
Self {
bytes: NULL.to_vec(),
text: "NULL".into(),
}
}
}
fn to_hex<S: Serializer>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&bytes.iter().map(|b| format!("{b:02x}")).collect::<String>())
}
fn from_hex<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
let text = String::deserialize(deserializer)?;
if !text.len().is_multiple_of(2) || !text.is_ascii() {
return Err(serde::de::Error::custom("odd or non-ASCII hex"));
}
(0..text.len())
.step_by(2)
.map(|i| u8::from_str_radix(&text[i..i + 2], 16).map_err(serde::de::Error::custom))
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct WriteSpec {
pub(crate) device: u32,
pub(crate) object: ObjectIdentifier,
pub(crate) property: PropertyIdentifier,
pub(crate) index: Option<u32>,
pub(crate) value: Option<String>,
pub(crate) priority: Option<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Occupancy {
NotCommandable,
Empty,
Ours,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Effect {
Changes {
from: String,
to: String,
},
Unchanged {
held_by: Option<u8>,
},
NotPresentValue,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Planned {
pub(crate) spec: WriteSpec,
pub(crate) priority: Option<u8>,
pub(crate) value: Stored,
pub(crate) commandable: bool,
pub(crate) before: Option<Stored>,
pub(crate) occupancy: Occupancy,
pub(crate) effect: Effect,
pub(crate) out_of_service: Option<bool>,
pub(crate) notes: Vec<String>,
pub(crate) gate: Gate,
}
impl Planned {
pub(crate) fn label(&self) -> String {
label(
self.spec.device,
self.spec.object,
self.spec.property,
self.spec.index,
self.priority,
)
}
}
pub(crate) fn label(
device: u32,
object: ObjectIdentifier,
property: PropertyIdentifier,
index: Option<u32>,
priority: Option<u8>,
) -> String {
let object = super::app::objects::object_label(object);
let property = super::app::objects::kebab(property);
let index = index.map(|i| format!("[{i}]")).unwrap_or_default();
let priority = priority.map(|p| format!(" @{p}")).unwrap_or_default();
format!("device {device} {object} {property}{index}{priority}")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Preview {
pub(crate) device: u32,
pub(crate) items: Vec<Planned>,
pub(crate) batch: bool,
pub(crate) gate: Gate,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Outcome {
Ack,
Denied,
Error {
class: u32,
code: u32,
},
Reject(u8),
Abort(u8),
Timeout,
NotSent(String),
Failed(String),
}
impl Outcome {
pub(crate) fn of(result: &Result<(), Error>) -> Self {
let denied = |class: ErrorClass, code: ErrorCode| {
class == ErrorClass::SERVICES && code == ErrorCode::SERVICE_REQUEST_DENIED
};
match result {
Ok(()) => Self::Ack,
Err(Error::Protocol { class, code } | Error::Structured { class, code, .. }) => {
if denied(*class, *code) {
Self::Denied
} else {
Self::Error {
class: u32::from(class.to_raw()),
code: u32::from(code.to_raw()),
}
}
}
Err(Error::Reject { reason }) => Self::Reject(reason.to_raw()),
Err(Error::Abort { reason })
if *reason == bacnet_types::enums::AbortReason::TSM_TIMEOUT =>
{
Self::Timeout
}
Err(Error::Abort { reason }) => Self::Abort(reason.to_raw()),
Err(Error::Timeout(_)) => Self::Timeout,
Err(other) => Self::Failed(other.to_string()),
}
}
pub(crate) fn applied(&self) -> Option<bool> {
match self {
Self::Ack => Some(true),
Self::Denied | Self::Error { .. } | Self::Reject(_) | Self::Abort(_) => Some(false),
Self::NotSent(_) => Some(false),
Self::Timeout | Self::Failed(_) => None,
}
}
pub(crate) fn describe(&self) -> String {
match self {
Self::Ack => "acknowledged".into(),
Self::Denied => "DENIED by the device's write policy (SERVICE_REQUEST_DENIED)".into(),
Self::Error { class, code } => format!(
"error {} / {}",
ErrorClass::from_raw(*class as u16),
ErrorCode::from_raw(*code as u16)
),
Self::Reject(reason) => format!("rejected, reason {reason}"),
Self::Abort(reason) => format!("aborted, reason {reason}"),
Self::Timeout => "no answer: it may or may not have been applied".into(),
Self::NotSent(why) => format!("not sent: {why}"),
Self::Failed(why) => format!("failed: {why}"),
}
}
pub(crate) fn json(&self) -> serde_json::Value {
use serde_json::json;
match self {
Self::Ack => json!({ "result": "ack" }),
Self::Denied => json!({ "result": "error", "class": "SERVICES",
"code": "SERVICE_REQUEST_DENIED", "denied": true }),
Self::Error { class, code } => json!({ "result": "error",
"class": ErrorClass::from_raw(*class as u16).to_string(),
"code": ErrorCode::from_raw(*code as u16).to_string() }),
Self::Reject(reason) => json!({ "result": "reject", "reason": reason }),
Self::Abort(reason) => json!({ "result": "abort", "reason": reason }),
Self::Timeout => json!({ "result": "timeout" }),
Self::NotSent(why) => json!({ "result": "not-sent", "why": why }),
Self::Failed(why) => json!({ "result": "failed", "why": why }),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum SafetyCommand {
Arm {
reason: String,
minutes: u32,
},
Disarm {
expired: bool,
},
Declined {
preview: Box<Preview>,
},
Prepare {
req: u64,
specs: Vec<WriteSpec>,
batch: bool,
},
Execute {
req: u64,
preview: Box<Preview>,
},
Revert {
req: u64,
},
Keep,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum SafetyEvent {
Ready {
policy: Safety,
workspace: bool,
run: String,
ledger: Vec<Entry>,
},
Armed {
minutes: u32,
},
Disarmed,
Refused {
req: Option<u64>,
reason: String,
},
Preview {
req: u64,
preview: Box<Preview>,
},
Done {
req: u64,
outcomes: Vec<(String, Outcome)>,
},
Ledger(Vec<Entry>),
Reverted {
req: u64,
failed: usize,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn levels_permit_the_shared_classes() {
use Class::{DeviceManagement, Read, Write};
let table = [
(Level::Passive, [false, false, false]),
(Level::ReadOnly, [true, false, false]),
(Level::Expired, [true, false, false]),
(Level::Armed { dangerous: false }, [true, true, false]),
(Level::Armed { dangerous: true }, [true, true, true]),
];
for (level, permits) in table {
let got = [Read, Write, DeviceManagement].map(|class| level.permits(class));
assert_eq!(got, permits, "{level:?}");
}
}
}