use bacnet_types::bip_port::{BbmdChanges, ForeignDeviceChanges};
use bacnet_types::enums::PropertyIdentifier as P;
use bacnet_types::error::Error;
use bacnet_types::primitives::PropertyValue;
use super::NetworkPortObject;
use super::bip_mode::{LiveMode, bdt_value, host_n_port_value};
use crate::common;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(super) struct Pending {
bbmd: BbmdChanges,
foreign: ForeignDeviceChanges,
}
impl Pending {
fn is_empty(&self) -> bool {
self.bbmd.is_empty() && self.foreign.is_empty()
}
}
impl NetworkPortObject {
pub(super) fn pending_value(&self, property: P) -> Option<Result<PropertyValue, Error>> {
let pending = &self.pending;
match property {
P::BBMD_BROADCAST_DISTRIBUTION_TABLE => pending
.bbmd
.broadcast_distribution_table
.as_deref()
.map(bdt_value),
P::BBMD_ACCEPT_FD_REGISTRATIONS => pending
.bbmd
.accept_foreign_device_registrations
.map(|accept| Ok(PropertyValue::Boolean(accept))),
P::BACNET_IP_NAT_TRAVERSAL => pending
.bbmd
.nat_traversal
.map(|nat| Ok(PropertyValue::Boolean(nat))),
P::BACNET_IP_GLOBAL_ADDRESS => {
pending.bbmd.global_address.as_ref().map(host_n_port_value)
}
P::FD_BBMD_ADDRESS => pending.foreign.bbmd.as_ref().map(host_n_port_value),
P::FD_SUBSCRIPTION_LIFETIME => pending
.foreign
.subscription_lifetime
.map(|ttl| Ok(PropertyValue::Unsigned(ttl.into()))),
_ => None,
}
}
pub(super) fn write_pending(
&mut self,
property: P,
array_index: Option<u32>,
value: &PropertyValue,
) -> Option<Result<(), Error>> {
let row = match &self.mode {
LiveMode::Bbmd(_) => {
matches!(
property,
P::BBMD_BROADCAST_DISTRIBUTION_TABLE | P::BBMD_ACCEPT_FD_REGISTRATIONS
) || (nat_row(property) && self.serves_nat())
}
LiveMode::Foreign(_) => {
matches!(property, P::FD_BBMD_ADDRESS | P::FD_SUBSCRIPTION_LIFETIME)
}
LiveMode::Normal => false,
};
if !row {
return None;
}
if !self.activates_changes() {
return Some(Err(common::write_access_denied_error()));
}
if array_index.is_some() {
return Some(Err(common::property_is_not_an_array_error()));
}
Some(match &self.mode {
LiveMode::Bbmd(tables) => {
let mut changes = BbmdChanges::default();
let decoded = match (property, value) {
(
P::BBMD_BROADCAST_DISTRIBUTION_TABLE,
PropertyValue::ApplicationData(octets),
) => bacnet_encoding::constructed::decode_bdt_entry_list(octets)
.map_err(|_| common::invalid_data_encoding_error())
.map(|table| changes.broadcast_distribution_table = Some(table)),
(P::BBMD_ACCEPT_FD_REGISTRATIONS, PropertyValue::Boolean(accept)) => {
changes.accept_foreign_device_registrations = Some(*accept);
Ok(())
}
(P::BACNET_IP_NAT_TRAVERSAL, PropertyValue::Boolean(nat)) => {
changes.nat_traversal = Some(*nat);
Ok(())
}
(P::BACNET_IP_GLOBAL_ADDRESS, PropertyValue::ApplicationData(octets)) => {
decode_host_n_port(octets)
.map(|global| changes.global_address = Some(global))
}
_ => Err(common::invalid_data_type_error()),
};
decoded
.and_then(|()| tables.check_value(&changes))
.map(|()| merge(&mut self.pending.bbmd, changes))
}
LiveMode::Foreign(registration) => {
let mut changes = self.pending.foreign.clone();
let decoded = match (property, value) {
(P::FD_BBMD_ADDRESS, PropertyValue::ApplicationData(octets)) => {
decode_host_n_port(octets).map(|bbmd| changes.bbmd = Some(bbmd))
}
(P::FD_SUBSCRIPTION_LIFETIME, PropertyValue::Unsigned(ttl)) => {
u16::try_from(*ttl)
.map(|ttl| changes.subscription_lifetime = Some(ttl))
.map_err(|_| common::value_out_of_range_error())
}
_ => Err(common::invalid_data_type_error()),
};
decoded
.and_then(|()| registration.check(&changes))
.map(|()| self.pending.foreign = changes)
}
LiveMode::Normal => Err(common::write_access_denied_error()),
})
}
pub fn check_pending_changes(&self) -> Result<(), Error> {
self.check_multicast()?;
if self.pending.is_empty() {
return Ok(());
}
match &self.mode {
LiveMode::Bbmd(tables) => tables.check(&self.pending.bbmd),
LiveMode::Foreign(registration) => registration.check(&self.pending.foreign),
LiveMode::Normal => Err(common::write_access_denied_error()),
}
}
pub(super) fn pending_group(&self) -> Option<[u8; 4]> {
self.pending.bbmd.multicast_group
}
pub(super) fn set_pending_group(&mut self, group: [u8; 4]) {
self.pending.bbmd.multicast_group = Some(group);
}
pub(super) fn changes_pending(&self) -> bool {
!self.pending.is_empty() || self.multicast.is_pending()
}
pub(super) fn activates_changes(&self) -> bool {
self.activation && self.mode.can_change()
}
pub fn activate_changes(&mut self) -> Result<bool, Error> {
if !self.changes_pending() {
return Ok(false);
}
self.check_pending_changes()?;
debug_assert!(
self.pending.is_empty() || !self.multicast.is_pending(),
"mode values and a multicast group pending together"
);
if !self.pending.is_empty() {
self.activate_mode_changes()?;
}
self.activate_multicast()?;
Ok(true)
}
fn activate_mode_changes(&mut self) -> Result<(), Error> {
match &self.mode {
LiveMode::Bbmd(tables) => tables.activate(&self.pending.bbmd)?,
LiveMode::Foreign(registration) => registration.activate(&self.pending.foreign)?,
LiveMode::Normal => return Err(common::write_access_denied_error()),
}
self.pending = Pending::default();
Ok(())
}
}
fn merge(pending: &mut BbmdChanges, written: BbmdChanges) {
if written.broadcast_distribution_table.is_some() {
pending.broadcast_distribution_table = written.broadcast_distribution_table;
}
if written.accept_foreign_device_registrations.is_some() {
pending.accept_foreign_device_registrations = written.accept_foreign_device_registrations;
}
if written.nat_traversal.is_some() {
pending.nat_traversal = written.nat_traversal;
}
if written.global_address.is_some() {
pending.global_address = written.global_address;
}
if written.multicast_group.is_some() {
pending.multicast_group = written.multicast_group;
}
}
fn decode_host_n_port(octets: &[u8]) -> Result<bacnet_types::constructed::BACnetHostNPort, Error> {
match bacnet_encoding::constructed::decode_host_n_port(octets, 0) {
Ok((address, end)) if end == octets.len() => Ok(address),
_ => Err(common::invalid_data_encoding_error()),
}
}
pub(super) fn writable_pending_row(property: P) -> bool {
matches!(
property,
P::BBMD_BROADCAST_DISTRIBUTION_TABLE
| P::BBMD_ACCEPT_FD_REGISTRATIONS
| P::FD_BBMD_ADDRESS
| P::FD_SUBSCRIPTION_LIFETIME
) || nat_row(property)
}
pub(super) fn nat_row(property: P) -> bool {
matches!(
property,
P::BACNET_IP_NAT_TRAVERSAL | P::BACNET_IP_GLOBAL_ADDRESS
)
}
impl NetworkPortObject {
pub(super) fn serves_nat(&self) -> bool {
self.ip
.as_ref()
.is_some_and(super::ip_config::IpConfig::is_ipv4)
&& self.mode.has_nat_rows()
}
}