use std::fmt;
use bacnet_objects::command::{CommandRun, RunPlan, WriteFailure};
use bacnet_types::constructed::BACnetActionCommand;
use bacnet_types::enums::{ErrorCode, RejectReason};
use bacnet_types::error::{Error, RemoteError};
use bacnet_types::primitives::ObjectIdentifier;
use tracing::debug;
use super::RunHost;
use crate::server::RemoteRequestError;
#[derive(Debug)]
pub(super) struct Failed {
pub(super) failure: WriteFailure,
pub(super) answer: Option<Error>,
pub(super) unanswered: bool,
}
pub(super) async fn write<H: RunHost>(
host: &H,
run: &CommandRun,
device: Option<ObjectIdentifier>,
command: &BACnetActionCommand,
) -> Result<(), Failed> {
let failed = match device {
None => {
let Err(error) = host.write(run, command).await else {
return Ok(());
};
log_failure(run, None, command, &error);
Failed {
failure: refused(&error),
answer: Some(error),
unanswered: false,
}
}
Some(device) => {
let Err(error) = host.write_remote(device, command).await else {
return Ok(());
};
log_failure(run, Some(device), command, &error);
match error {
RemoteRequestError::Refused(refusal) => {
let answer = Error::from(refusal);
Failed {
failure: refused(&answer),
answer: Some(answer),
unanswered: false,
}
}
other => Failed {
failure: unmade(other),
answer: None,
unanswered: silent(other),
},
}
}
};
Err(failed)
}
fn log_failure(
run: &CommandRun,
device: Option<ObjectIdentifier>,
command: &BACnetActionCommand,
error: &dyn fmt::Display,
) {
let target = command.object_identifier;
let property = command.property_identifier;
match (&run.plan, device) {
(RunPlan::Actions(_), None) => debug!(
command = %run.source, %target, ?property, %error, "Command write failed"
),
(RunPlan::Actions(_), Some(device)) => debug!(
command = %run.source, %device, %target, ?property, %error,
"Command write to another device failed"
),
(RunPlan::Channel(_), None) => debug!(
channel = %run.source, %target, ?property, %error, "Channel member write failed"
),
(RunPlan::Channel(_), Some(device)) => debug!(
channel = %run.source, %device, %target, ?property, %error,
"Channel member write to another device failed"
),
}
}
fn code(error: &Error) -> Option<ErrorCode> {
match error {
Error::Remote(RemoteError::Error { code, .. }) | Error::Structured { code, .. } => {
Some(*code)
}
_ => None,
}
}
pub(super) fn refuses_datatype(error: &Error) -> bool {
let invalid = ErrorCode::INVALID_DATA_TYPE;
code(error) == Some(invalid)
|| matches!(error, Error::Remote(RemoteError::Reject { reason })
if *reason == RejectReason::INVALID_PARAMETER_DATA_TYPE)
}
fn refused(error: &Error) -> WriteFailure {
const CONFIGURATION: [ErrorCode; 5] = [
ErrorCode::UNKNOWN_OBJECT,
ErrorCode::UNKNOWN_PROPERTY,
ErrorCode::INVALID_ARRAY_INDEX,
ErrorCode::PROPERTY_IS_NOT_AN_ARRAY,
ErrorCode::DATATYPE_NOT_SUPPORTED,
];
let configuration =
refuses_datatype(error) || code(error).is_some_and(|code| CONFIGURATION.contains(&code));
if configuration {
WriteFailure::Configuration
} else {
WriteFailure::Process
}
}
fn silent(error: RemoteRequestError) -> bool {
matches!(
error,
RemoteRequestError::Unanswered | RemoteRequestError::Undiscovered
)
}
fn unmade(error: RemoteRequestError) -> WriteFailure {
match error {
RemoteRequestError::Disabled
| RemoteRequestError::Unbound
| RemoteRequestError::Undiscovered
| RemoteRequestError::Unanswered => WriteFailure::Communication,
RemoteRequestError::Refused(refusal) => refused(&Error::from(refusal)),
RemoteRequestError::Unencodable
| RemoteRequestError::TooLong
| RemoteRequestError::NoInvokeId
| RemoteRequestError::Stopping
| RemoteRequestError::NoNetwork
| RemoteRequestError::Malformed => WriteFailure::Process,
}
}
#[cfg(test)]
mod tests {
use super::*;
use bacnet_types::enums::{AbortReason, ErrorClass};
use bacnet_types::error::RemoteError;
fn error(code: ErrorCode) -> Error {
RemoteError::Error {
class: ErrorClass::PROPERTY,
code,
}
.into()
}
#[test]
fn failures_sort_into_configuration_process_and_communication() {
for code in [
ErrorCode::UNKNOWN_OBJECT,
ErrorCode::UNKNOWN_PROPERTY,
ErrorCode::INVALID_ARRAY_INDEX,
ErrorCode::PROPERTY_IS_NOT_AN_ARRAY,
ErrorCode::INVALID_DATA_TYPE,
ErrorCode::DATATYPE_NOT_SUPPORTED,
] {
assert_eq!(
refused(&error(code)),
WriteFailure::Configuration,
"{code:?}"
);
}
let datatype = RemoteError::Reject {
reason: RejectReason::INVALID_PARAMETER_DATA_TYPE,
};
assert_eq!(refused(&datatype.into()), WriteFailure::Configuration);
for answer in [
error(ErrorCode::WRITE_ACCESS_DENIED),
error(ErrorCode::VALUE_OUT_OF_RANGE),
RemoteError::Reject {
reason: RejectReason::OTHER,
}
.into(),
RemoteError::Abort {
reason: AbortReason::OTHER,
}
.into(),
] {
assert_eq!(refused(&answer), WriteFailure::Process, "{answer:?}");
}
for silence in [
RemoteRequestError::Disabled,
RemoteRequestError::Unbound,
RemoteRequestError::Undiscovered,
RemoteRequestError::Unanswered,
] {
assert_eq!(unmade(silence), WriteFailure::Communication);
}
for local in [
RemoteRequestError::Unencodable,
RemoteRequestError::TooLong,
RemoteRequestError::NoInvokeId,
RemoteRequestError::Stopping,
RemoteRequestError::NoNetwork,
] {
assert_eq!(unmade(local), WriteFailure::Process);
}
}
#[test]
fn only_silence_to_the_write_or_its_who_is_marks_the_device_silent() {
assert!(silent(RemoteRequestError::Unanswered));
assert!(silent(RemoteRequestError::Undiscovered));
for unasked in [
RemoteRequestError::Disabled,
RemoteRequestError::Unbound,
RemoteRequestError::NoInvokeId,
RemoteRequestError::Stopping,
] {
assert!(!silent(unasked), "{unasked:?}");
}
let busy = RemoteError::Error {
class: ErrorClass::OBJECT,
code: ErrorCode::BUSY,
};
assert!(!silent(RemoteRequestError::Refused(busy)));
}
#[test]
fn only_an_invalid_datatype_error_or_reject_refuses_the_datatype() {
assert!(refuses_datatype(&error(ErrorCode::INVALID_DATA_TYPE)));
assert!(refuses_datatype(
&RemoteError::Reject {
reason: RejectReason::INVALID_PARAMETER_DATA_TYPE
}
.into()
));
assert!(!refuses_datatype(&error(ErrorCode::DATATYPE_NOT_SUPPORTED)));
assert!(!refuses_datatype(
&RemoteError::Reject {
reason: RejectReason::OTHER
}
.into()
));
}
}