use std::collections::HashMap;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Instant;
use bacnet_encoding::npdu::NpduAddress;
use bacnet_types::enums::{ErrorClass, ErrorCode, PropertyIdentifier};
use bacnet_types::error::Error;
use bacnet_types::primitives::ObjectIdentifier;
use bacnet_types::MacAddr;
mod identity;
pub use identity::*;
pub(crate) mod active;
mod admission;
pub use admission::MultipleRefusal;
mod confirmed;
pub(crate) use confirmed::{BeginRefusal, CovRevisits};
mod sample;
pub use sample::CovSample;
mod observation;
mod observation_order;
pub use observation::CovObservation;
use observation_order::ObservationOwner;
pub(crate) use observation_order::PreparedCovCompletion;
pub(crate) mod flags;
mod lifetime;
pub(crate) mod multiple_reads;
pub(crate) mod prepare;
pub(crate) mod reported;
pub(crate) mod timed;
mod timed_capture;
pub(crate) use timed_capture::TimedWriteCapture;
pub(crate) mod value_source;
pub use lifetime::CovTimeRemaining;
mod policy;
pub use policy::*;
#[cfg(test)]
mod identity_tests;
#[cfg(test)]
mod policy_tests;
#[cfg(test)]
mod tests;
const DEFAULT_TIMED_APDU_LENGTH: usize = 1476;
#[derive(Debug, Clone)]
pub struct CovSubscription {
pub subscriber_mac: MacAddr,
pub subscriber_network: Option<NpduAddress>,
pub subscriber_process_identifier: u32,
pub monitored_object_identifier: ObjectIdentifier,
pub issue_confirmed_notifications: bool,
pub expires_at: Option<Instant>,
pub last_notified_observation: Option<CovObservation>,
pub monitored_property: Option<PropertyIdentifier>,
pub monitored_property_array_index: Option<u32>,
pub cov_increment: Option<f32>,
pub notification_kind: CovNotificationKind,
pub timestamped: bool,
}
impl CovSubscription {
pub fn recipient(&self) -> CovRecipient {
CovRecipient::from_endpoint(&self.subscriber_mac, self.subscriber_network.as_ref())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CovNotificationKind {
Single,
Multiple,
}
#[derive(Debug)]
pub struct CovSubscriptionTable {
subs: HashMap<CovSubscriptionKey, CovSubscriptionSnapshot>,
generation: u64,
owner: Arc<ObservationOwner>,
peer_counts: HashMap<CovRecipient, usize>,
peer_indefinite_counts: HashMap<CovRecipient, usize>,
policy: CovPolicy,
counters: Arc<AtomicCovCounters>,
in_flight: Arc<CovInFlightTracker>,
revisits: Arc<CovRevisits>,
dispatch_turn: usize,
timed: timed::TimedStore,
#[cfg(test)]
live_samples: std::sync::atomic::AtomicUsize,
}
impl Default for CovSubscriptionTable {
fn default() -> Self {
Self::new()
}
}
impl CovSubscriptionTable {
pub fn new() -> Self {
Self::with_policy(CovPolicy::default(), Arc::new(AtomicCovCounters::default()))
}
pub fn with_policy(policy: CovPolicy, counters: Arc<AtomicCovCounters>) -> Self {
let timed = timed::TimedStore::new(DEFAULT_TIMED_APDU_LENGTH, Arc::clone(&counters));
Self {
subs: HashMap::new(),
generation: 0,
owner: Arc::new(ObservationOwner::default()),
peer_counts: HashMap::new(),
peer_indefinite_counts: HashMap::new(),
policy: policy.sanitized(),
counters,
in_flight: Arc::new(CovInFlightTracker::default()),
revisits: Arc::default(),
dispatch_turn: 0,
timed,
#[cfg(test)]
live_samples: std::sync::atomic::AtomicUsize::new(0),
}
}
pub fn with_max_apdu_length(mut self, max_apdu_length: usize) -> Self {
self.timed = timed::TimedStore::new(max_apdu_length, Arc::clone(&self.counters));
self
}
pub(crate) fn timed(&self) -> &timed::TimedStore {
&self.timed
}
pub(crate) fn next_dispatch_turn(&mut self) -> usize {
let turn = self.dispatch_turn;
self.dispatch_turn = self.dispatch_turn.wrapping_add(1);
turn
}
pub fn policy(&self) -> &CovPolicy {
&self.policy
}
pub fn counters(&self) -> &Arc<AtomicCovCounters> {
&self.counters
}
pub fn in_flight_tracker(&self) -> &Arc<CovInFlightTracker> {
&self.in_flight
}
pub fn peer_subscription_count(&self, peer: &CovRecipient) -> usize {
self.peer_counts.get(peer).copied().unwrap_or(0)
}
pub fn peer_indefinite_count(&self, peer: &CovRecipient) -> usize {
self.peer_indefinite_counts.get(peer).copied().unwrap_or(0)
}
pub fn get_subscription(&self, key: &CovSubscriptionKey) -> Option<&CovSubscriptionSnapshot> {
self.subs.get(key)
}
pub(crate) fn multiple_context_references<'a>(
&'a self,
context: &'a MultipleContextKey,
) -> impl Iterator<Item = &'a CovSubscriptionSnapshot> + 'a {
self.subs
.iter()
.filter(move |(key, _)| key.multiple_context() == Some(context))
.map(|(_, sub)| sub)
}
pub fn contains(&self, key: &CovSubscriptionKey) -> bool {
self.subs.contains_key(key)
}
fn remove_internal(&mut self, key: &CovSubscriptionKey, was_cancelled: bool) -> bool {
if let Some(sub) = self.subs.remove(key) {
self.timed.lock().remove(key);
self.revisits.forget(key);
let peer = sub.recipient();
if let Some(count) = self.peer_counts.get_mut(&peer) {
*count = count.saturating_sub(1);
if *count == 0 {
self.peer_counts.remove(&peer);
}
}
if sub.expires_at.is_none() {
if let Some(count) = self.peer_indefinite_counts.get_mut(&peer) {
*count = count.saturating_sub(1);
if *count == 0 {
self.peer_indefinite_counts.remove(&peer);
}
}
}
if was_cancelled {
self.counters
.subscriptions_cancelled
.fetch_add(1, Ordering::Relaxed);
}
self.counters
.subscriptions_active
.store(self.subs.len() as u64, Ordering::Relaxed);
true
} else {
false
}
}
pub fn unsubscribe(&mut self, key: &CovSubscriptionKey) -> bool {
self.remove_internal(key, true)
}
pub fn unsubscribe_cov_multiple_context(&mut self, context: &MultipleContextKey) {
let keys: Vec<_> = self
.subs
.keys()
.filter(|key| key.multiple_context() == Some(context))
.cloned()
.collect();
for key in keys {
self.remove_internal(&key, true);
}
}
pub fn remove_for_object(&mut self, oid: ObjectIdentifier) {
let to_remove: Vec<_> = self
.subs
.iter()
.filter(|(k, _)| k.object() == oid)
.map(|(k, _)| k.clone())
.collect();
for key in to_remove {
self.remove_internal(&key, false);
}
}
pub fn remove_peer_subscriptions(
&mut self,
mac: &[u8],
network: Option<&NpduAddress>,
) -> usize {
let target = SubscriberEndpoint::new(mac, network);
let to_remove: Vec<_> = self
.subs
.iter()
.filter(|(_, entry)| entry.endpoint() == target)
.map(|(k, _)| k.clone())
.collect();
let count = to_remove.len();
for key in to_remove {
self.remove_internal(&key, true);
}
count
}
pub fn purge_expired(&mut self) -> usize {
let now = Instant::now();
let mut purged_count = 0;
let mut to_remove = Vec::new();
for (k, sub) in &self.subs {
if sub.expires_at.is_some_and(|exp| exp <= now) {
to_remove.push(k.clone());
}
}
for key in to_remove {
purged_count += usize::from(self.remove_internal(&key, false));
}
if purged_count > 0 {
self.counters
.subscriptions_purged
.fetch_add(purged_count as u64, Ordering::Relaxed);
self.counters
.subscriptions_active
.store(self.subs.len() as u64, Ordering::Relaxed);
}
purged_count
}
#[cfg(test)]
pub(crate) fn expire_all_for_test(&mut self) {
let now = Instant::now();
for entry in self.subs.values_mut() {
entry.subscription.expires_at = Some(now);
}
}
pub fn subscriptions_for(&mut self, oid: &ObjectIdentifier) -> Vec<&CovSubscriptionSnapshot> {
self.purge_expired();
self.subs
.values()
.filter(|sub| sub.monitored_object_identifier == *oid)
.collect()
}
pub fn is_current(&self, snapshot: &CovSubscriptionSnapshot) -> bool {
self.remaining_lifetime(snapshot, Instant::now())
.and_then(CovTimeRemaining::wire_seconds)
.is_some()
}
pub fn should_notify(
sub: &CovSubscription,
current_value: Option<&CovSample>,
cov_increment: Option<f64>,
) -> bool {
let Some(current) = current_value else {
return true;
};
current.reports(
sub.last_notified_observation
.as_ref()
.map(|observation| observation.sample()),
cov_increment,
true,
)
}
pub fn len(&self) -> usize {
self.subs.len()
}
pub fn is_empty(&self) -> bool {
self.subs.is_empty()
}
}