use std::collections::HashSet;
use bacnet_services::cov_multiple::{
COVNotificationItem, COVNotificationMultipleRequest, COVNotificationValue,
};
use bacnet_types::primitives::ObjectIdentifier;
use bytes::BytesMut;
use tracing::warn;
use super::cov_clock::cov_multiple_datetime;
use super::multiple_items::{Coordinate, History, Latest, Retained, Stamp, build_items};
use crate::cov::CovSubscriptionKey;
use crate::cov::CovSubscriptionSnapshot;
use crate::cov::multiple_reads::MultipleReads;
use crate::cov::timed::{
ITEM_FRAMING, TimedChange, TimedClaim, ValueFit, request_header, value_len,
};
pub(super) struct Envelope {
pub(super) subscriber_process_identifier: u32,
pub(super) initiating_device_identifier: ObjectIdentifier,
pub(super) time_remaining: u32,
}
impl Envelope {
fn notification(
&self,
timestamp: Option<bacnet_objects::clock::ClockFrame>,
items: Vec<COVNotificationItem>,
) -> COVNotificationMultipleRequest {
COVNotificationMultipleRequest {
subscriber_process_identifier: self.subscriber_process_identifier,
initiating_device_identifier: self.initiating_device_identifier,
time_remaining: self.time_remaining,
timestamp: timestamp.map(cov_multiple_datetime),
list_of_cov_notifications: items,
}
}
}
pub(super) struct Part {
pub(super) notification: COVNotificationMultipleRequest,
pub(super) claim: TimedClaim,
pub(super) finishes: HashSet<CovSubscriptionKey>,
}
pub(super) fn request_limit(local: u32, subscriber: Option<u16>, confirmed: bool) -> usize {
let local = usize::try_from(local).unwrap_or(usize::MAX);
let apdu = subscriber.map_or(local, |subscriber| local.min(usize::from(subscriber)));
apdu.saturating_sub(request_header(confirmed))
}
pub(super) fn untimed_octets(retained: &Retained<'_>) -> usize {
let mut objects = HashSet::new();
retained
.iter()
.filter(|(sub, _)| !sub.timestamped)
.map(|(sub, values)| {
let framing = if objects.insert(sub.monitored_object_identifier) {
ITEM_FRAMING
} else {
0
};
framing + values.iter().map(value_len).sum::<usize>()
})
.sum()
}
pub(super) struct ReportContent<'a> {
pub(super) envelope: &'a Envelope,
pub(super) retained: &'a Retained<'a>,
pub(super) reads: &'a MultipleReads,
pub(super) untimed: &'a HashSet<Coordinate>,
pub(super) stamp: &'a Stamp<'a>,
}
impl ReportContent<'_> {
fn history(&self, changes: &History<'_>) -> COVNotificationMultipleRequest {
let timestamp = changes.last().map(|(_, change)| change.frame());
let (items, _) = build_items(
changes,
None,
&Latest::new(),
self.reads,
self.untimed,
self.stamp,
);
self.envelope.notification(timestamp, items)
}
fn last(&self, changes: &History<'_>) -> COVNotificationMultipleRequest {
self.current(changes, self.retained)
}
fn current(
&self,
changes: &History<'_>,
retained: &Retained<'_>,
) -> COVNotificationMultipleRequest {
let latest: Latest<'_> = changes.iter().copied().collect();
let history: Vec<_> = changes
.iter()
.copied()
.filter(|(key, change)| latest[key].seq() != change.seq())
.collect();
let (items, stamped) = build_items(
&history,
Some(retained),
&latest,
self.reads,
self.untimed,
self.stamp,
);
let newest = changes
.last()
.map(|(_, change)| (change.seq(), change.frame()))
.into_iter()
.chain(stamped)
.max_by_key(|(seq, _)| *seq)
.map(|(_, frame)| frame);
self.envelope.notification(newest, items)
}
}
fn encoded_len(notification: &COVNotificationMultipleRequest) -> Option<usize> {
let mut encoded = BytesMut::new();
notification.encode(&mut encoded).ok()?;
Some(encoded.len())
}
fn fits(notification: &COVNotificationMultipleRequest, limit: usize) -> bool {
encoded_len(notification).is_some_and(|len| len <= limit)
}
pub(super) fn split(content: &ReportContent<'_>, mut claim: TimedClaim, limit: usize) -> Vec<Part> {
let plan = {
let changes = claim.in_order();
let whole = content.last(&changes);
if encoded_len(&whole).is_none_or(|len| len <= limit) {
return finish(content, Vec::new(), whole, claim);
}
let moved = smallest(changes.len(), |count| {
let last = content.last(&changes[count..]);
last.list_of_cov_notifications.is_empty() || fits(&last, limit)
});
let mut plan = Vec::new();
let mut start = 0;
while start < moved {
let alone = |end: usize| fits(&content.history(&changes[start..end]), limit);
let end = largest(start + 1, moved, alone);
plan.push((end - start, end == start + 1 && !alone(end)));
start = end;
}
plan
};
let mut parts = Vec::new();
let fit = |key: &CovSubscriptionKey, value: &TimedChange| {
let notification = content.history(&[(key, value)]);
if notification.list_of_cov_notifications.is_empty() {
ValueFit::Empty
} else if fits(¬ification, limit) {
ValueFit::Fits
} else {
ValueFit::TooLarge
}
};
for (count, too_large) in plan {
let part = claim.split_oldest(count);
if !too_large {
parts.push((content.history(&part.in_order()), part));
continue;
}
for value in part.split_values(fit) {
parts.push((content.history(&value.in_order()), value));
}
}
let last = content.last(&claim.in_order());
if encoded_len(&last).is_some_and(|len| len > limit) {
let mut parts = history_parts(parts);
parts.extend(untimed_parts(content, claim, limit));
finish_timed(&mut parts);
return parts;
}
finish(content, parts, last, claim)
}
fn finish(
content: &ReportContent<'_>,
parts: Vec<(COVNotificationMultipleRequest, TimedClaim)>,
last: COVNotificationMultipleRequest,
claim: TimedClaim,
) -> Vec<Part> {
let mut parts = history_parts(parts);
if !last.list_of_cov_notifications.is_empty() {
let untimed = content
.retained
.iter()
.filter(|(sub, _)| !sub.timestamped)
.map(|(sub, _)| sub.key().clone());
parts.push(Part {
notification: last,
claim,
finishes: untimed.collect(),
});
}
finish_timed(&mut parts);
parts
}
fn history_parts(parts: Vec<(COVNotificationMultipleRequest, TimedClaim)>) -> Vec<Part> {
parts
.into_iter()
.map(|(notification, claim)| Part {
notification,
claim,
finishes: HashSet::new(),
})
.collect()
}
fn finish_timed(parts: &mut [Part]) {
let mut later = HashSet::new();
for part in parts.iter_mut().rev() {
for (key, _) in part.claim.last_changes() {
if later.insert(key.clone()) {
part.finishes.insert(key.clone());
}
}
}
}
type Untimed<'a> = (&'a CovSubscriptionSnapshot, &'a [COVNotificationValue]);
fn untimed_parts(content: &ReportContent<'_>, mut claim: TimedClaim, limit: usize) -> Vec<Part> {
let mut objects: Vec<Vec<Untimed<'_>>> = Vec::new();
for &(sub, values) in content.retained.iter().filter(|(sub, _)| !sub.timestamped) {
let oid = sub.monitored_object_identifier;
match objects
.iter_mut()
.find(|refs| refs[0].0.monitored_object_identifier == oid)
{
Some(refs) => refs.push((sub, values)),
None => objects.push(vec![(sub, values)]),
}
}
let alone = |refs: &[Untimed<'_>]| fits(&content.current(&[], refs), limit);
let mut groups: Vec<Vec<Untimed<'_>>> = Vec::new();
let mut start = 0;
while start < objects.len() {
let run = |end: usize| alone(&objects[start..end].concat());
let end = largest(start + 1, objects.len(), run);
if end > start + 1 || run(end) {
groups.push(objects[start..end].concat());
start = end;
continue;
}
let refs = &objects[start];
let mut first = 0;
while first < refs.len() {
let end = largest(first + 1, refs.len(), |end| alone(&refs[first..end]));
if end > first + 1 || alone(&refs[first..end]) {
groups.push(refs[first..end].to_vec());
} else {
warn!(
object = ?refs[first].0.monitored_object_identifier,
property = ?refs[first].0.monitored_property,
limit,
"Untimestamped COV-multiple values exceed the notification size on \
their own; left out"
);
}
first = end;
}
start += 1;
}
let parts = groups
.into_iter()
.map(|group| {
let keys: HashSet<_> = group.iter().map(|(sub, _)| sub.key().clone()).collect();
Part {
notification: content.current(&[], &group),
claim: claim.split_untimed(&keys),
finishes: keys,
}
})
.collect();
claim.forgo_untimed();
parts
}
fn smallest(max: usize, fits: impl Fn(usize) -> bool) -> usize {
let (mut failing, mut fitting) = (0, max);
if !fits(max) {
return max;
}
while fitting - failing > 1 {
let mid = failing + (fitting - failing) / 2;
if fits(mid) {
fitting = mid;
} else {
failing = mid;
}
}
fitting
}
fn largest(first: usize, max: usize, fits: impl Fn(usize) -> bool) -> usize {
if !fits(first) {
return first;
}
let (mut fitting, mut step) = (first, 1);
let mut failing = loop {
let next = fitting + step;
if next > max {
break max + 1;
}
if !fits(next) {
break next;
}
fitting = next;
step *= 2;
};
while failing - fitting > 1 {
let mid = fitting + (failing - fitting) / 2;
if fits(mid) {
fitting = mid;
} else {
failing = mid;
}
}
fitting
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bisection_finds_the_flip_point() {
for flip in 1..=9 {
assert_eq!(smallest(9, |count| count >= flip), flip);
assert_eq!(largest(1, 9, |end| end <= flip), flip);
}
assert_eq!(smallest(9, |_| false), 9, "never fits: move everything");
assert_eq!(largest(3, 9, |_| false), 3, "at least one change");
assert_eq!(largest(3, 9, |_| true), 9);
}
#[test]
fn the_limit_is_the_smaller_maximum_less_the_header() {
assert_eq!(request_limit(1476, None, false), 1474);
assert_eq!(request_limit(1476, Some(206), false), 204);
assert_eq!(request_limit(480, Some(1476), true), 476);
}
}