use bacnet_encoding::constructed::{
decode_access_rule, decode_action_list, decode_authentication_factor,
decode_authentication_factor_format, decode_authentication_policy, decode_port_permission,
decode_prescale, decode_property_access_result, decode_read_access_specification, decode_scale,
decode_shed_level, decode_stage_limit_value,
};
use bacnet_encoding::tags;
use bacnet_services::rpm::{ReadAccessResult, ReadPropertyMultipleACK};
use bacnet_types::constructed::{
AccessResult, BACnetActionCommand, BACnetAuthenticationFactor, BACnetScale, BACnetShedLevel,
};
use bacnet_types::enums::{
AccessRuleLocationSpecifier, AccessRuleTimeRangeSpecifier, AuthenticationFactorType,
ErrorClass, ErrorCode, PropertyIdentifier,
};
use bacnet_types::error::Error;
use super::constructed::{object_reference, on_device, reference};
use super::primitive::hex;
use super::{StateTexts, ValueFormat};
use crate::label::{kebab, object_label};
type Rendered = Result<(String, usize), Error>;
fn error_text(class: ErrorClass, code: ErrorCode) -> String {
format!("error {}:{}", kebab(class), kebab(code))
}
fn property_text(property: PropertyIdentifier, index: Option<u32>) -> String {
match index {
Some(index) => format!("{}[{index}]", kebab(property)),
None => kebab(property),
}
}
fn action_command(format: &ValueFormat<'_>, command: &BACnetActionCommand) -> String {
let texts = StateTexts::default();
let target = format.for_object(command.object_identifier.object_type(), &texts);
let mut text = format!(
"{} {} = {}",
object_label(command.object_identifier),
property_text(command.property_identifier, command.property_array_index),
target.element(command.property_identifier, &command.property_value)
);
if let Some(priority) = command.priority {
text.push_str(&format!(" @{priority}"));
}
if let Some(device) = command.device_identifier {
text.push_str(&format!(" on device {}", device.instance_number()));
}
if let Some(delay) = command.post_delay {
text.push_str(&format!(", then wait {delay} s"));
}
if command.quit_on_failure {
text.push_str(", stop on failure");
}
text
}
pub(super) fn action_list(
format: &ValueFormat<'_>,
_: usize,
octets: &[u8],
offset: usize,
) -> Rendered {
decode_action_list(octets, offset).map(|(list, end)| {
let text = if list.commands.is_empty() {
"no writes".to_string()
} else {
let writes: Vec<String> = list
.commands
.iter()
.map(|command| action_command(format, command))
.collect();
format!("({})", writes.join("; "))
};
(text, end)
})
}
pub(super) fn read_access_specification(
_: &ValueFormat<'_>,
_: usize,
octets: &[u8],
offset: usize,
) -> Rendered {
decode_read_access_specification(octets, offset).map(|(spec, end)| {
let properties: Vec<String> = spec
.list_of_property_references
.iter()
.map(|r| property_text(r.property_identifier, r.property_array_index))
.collect();
let text = format!(
"{}: {}",
object_label(spec.object_identifier),
properties.join(", ")
);
(text, end)
})
}
pub(super) fn read_access_result(
format: &ValueFormat<'_>,
_: usize,
octets: &[u8],
offset: usize,
) -> Rendered {
let (result, end) = decode_read_access_result(octets, offset)?;
let texts = StateTexts::default();
let member = format.for_object(result.object_identifier.object_type(), &texts);
let values: Vec<String> = result
.list_of_results
.iter()
.map(|element| {
let value = match (&element.property_value, element.error) {
(Some(data), _) => member.value(element.property_identifier, data),
(None, Some((class, code))) => error_text(class, code),
(None, None) => "no value".into(),
};
format!(
"{} = {value}",
property_text(element.property_identifier, element.property_array_index)
)
})
.collect();
let text = format!(
"{}: {}",
object_label(result.object_identifier),
values.join(", ")
);
Ok((text, end))
}
fn decode_read_access_result(
octets: &[u8],
offset: usize,
) -> Result<(ReadAccessResult, usize), Error> {
let (tag, contents) = tags::decode_tag(octets, offset)?;
if !tag.is_context(0) || tag.is_opening || tag.is_closing {
return Err(Error::decoding(
offset,
"expected the object identifier [0]",
));
}
let after = contents
.checked_add(tag.length as usize)
.filter(|&after| after <= octets.len())
.ok_or_else(|| Error::decoding(contents, "object identifier cut short"))?;
let (tag, contents) = tags::decode_tag(octets, after)?;
if !tag.is_opening_tag(1) {
return Err(Error::decoding(after, "expected the results [1]"));
}
let (_, end) = tags::extract_context_value(octets, contents, 1)?;
let ack = ReadPropertyMultipleACK::decode(&octets[offset..end])?;
let [result] = <[ReadAccessResult; 1]>::try_from(ack.list_of_read_access_results)
.map_err(|_| Error::decoding(offset, "expected one result"))?;
Ok((result, end))
}
pub(super) fn property_access_result(
format: &ValueFormat<'_>,
_: usize,
octets: &[u8],
offset: usize,
) -> Rendered {
decode_property_access_result(octets, offset).map(|(result, end)| {
let r = &result.reference;
let named = on_device(
reference(
r.object_identifier,
r.property_identifier,
r.property_array_index,
),
r.device_identifier,
);
let value = match &result.access_result {
AccessResult::Value(value) => {
let texts = StateTexts::default();
format
.for_object(r.object_identifier.object_type(), &texts)
.element(PropertyIdentifier::from_raw(r.property_identifier), value)
}
AccessResult::Error { class, code } => error_text(*class, *code),
};
(format!("{named} = {value}"), end)
})
}
pub(super) fn stage_limit_value(
format: &ValueFormat<'_>,
_: usize,
octets: &[u8],
offset: usize,
) -> Rendered {
decode_stage_limit_value(octets, offset).map(|(stage, end)| {
let targets: String = stage
.values
.iter()
.map(|on| if *on { '1' } else { '0' })
.collect();
let text = format!(
"limit {}, deadband {}, targets {}",
format.real(stage.limit),
format.real(stage.deadband),
if targets.is_empty() { "none" } else { &targets }
);
(text, end)
})
}
pub(super) fn scale(format: &ValueFormat<'_>, _: usize, octets: &[u8], offset: usize) -> Rendered {
decode_scale(octets, offset).map(|(scale, end)| {
let text = match scale {
BACnetScale::FloatScale(factor) => format!("factor {}", format.real(factor)),
BACnetScale::IntegerScale(power) => format!("factor 10^{power}"),
};
(text, end)
})
}
pub(super) fn prescale(_: &ValueFormat<'_>, _: usize, octets: &[u8], offset: usize) -> Rendered {
decode_prescale(octets, offset).map(|(prescale, end)| {
let text = format!(
"multiplier {}, modulo divide {}",
prescale.multiplier, prescale.modulo_divide
);
(text, end)
})
}
pub(super) fn shed_level(
format: &ValueFormat<'_>,
_: usize,
octets: &[u8],
offset: usize,
) -> Rendered {
decode_shed_level(octets, offset).map(|(level, end)| {
let text = match level {
BACnetShedLevel::Percent(percent) => format!("{percent} %"),
BACnetShedLevel::Level(level) => format!("level {level}"),
BACnetShedLevel::Amount(kilowatts) => format!("reduce {} kW", format.real(kilowatts)),
};
(text, end)
})
}
pub(super) fn port_permission(
_: &ValueFormat<'_>,
_: usize,
octets: &[u8],
offset: usize,
) -> Rendered {
decode_port_permission(octets, offset).map(|(permission, end)| {
let state = if permission.enabled {
"enabled"
} else {
"disabled"
};
(format!("port {} {state}", permission.port_id), end)
})
}
pub(super) fn factor_text(factor: &BACnetAuthenticationFactor) -> String {
format!(
"{} class {} value 0x{}",
kebab(factor.format_type),
factor.format_class,
hex(&factor.value, "")
)
}
pub(super) fn authentication_factor(
_: &ValueFormat<'_>,
_: usize,
octets: &[u8],
offset: usize,
) -> Rendered {
decode_authentication_factor(octets, offset).map(|(factor, end)| (factor_text(&factor), end))
}
pub(super) fn authentication_factor_format(
_: &ValueFormat<'_>,
_: usize,
octets: &[u8],
offset: usize,
) -> Rendered {
decode_authentication_factor_format(octets, offset).map(|(format, end)| {
let mut text = kebab(format.format_type);
let vendor = format.vendor_id.unwrap_or(0);
let number = format.vendor_format.unwrap_or(0);
if format.format_type == AuthenticationFactorType::CUSTOM || vendor != 0 || number != 0 {
text.push_str(&format!(" (vendor {vendor} format {number})"));
}
(text, end)
})
}
pub(super) fn authentication_policy(
_: &ValueFormat<'_>,
_: usize,
octets: &[u8],
offset: usize,
) -> Rendered {
decode_authentication_policy(octets, offset).map(|(policy, end)| {
let mut parts: Vec<String> = policy
.policy
.iter()
.map(|entry| {
format!(
"{} step {}",
object_reference(&entry.credential_data_input),
entry.index
)
})
.collect();
if parts.is_empty() {
parts.push("no factors".into());
}
parts.push(if policy.order_enforced {
"in order".into()
} else {
"any order".into()
});
parts.push(match policy.timeout {
0 => "no timeout".into(),
seconds => format!("timeout {seconds} s"),
});
(parts.join(", "), end)
})
}
pub(super) fn access_rule(_: &ValueFormat<'_>, _: usize, octets: &[u8], offset: usize) -> Rendered {
decode_access_rule(octets, offset).map(|(rule, end)| {
let when = match (&rule.time_range, rule.time_range_specifier) {
(_, AccessRuleTimeRangeSpecifier::ALWAYS) => "always".to_string(),
(Some(r), _) => format!(
"when {}",
on_device(
reference(
r.object_identifier,
r.property_identifier,
r.property_array_index
),
r.device_identifier
)
),
(None, specifier) => format!("time range {}", kebab(specifier)),
};
let place = match (&rule.location, rule.location_specifier) {
(_, AccessRuleLocationSpecifier::ALL) => "every point".to_string(),
(Some(location), _) => object_reference(location),
(None, specifier) => format!("location {}", kebab(specifier)),
};
let state = if rule.enable { "enabled" } else { "disabled" };
(format!("{when} at {place}, {state}"), end)
})
}