use std::time::Duration;
use bacnet_transport::mstp::{LatenessHistogram, MstpDiagnosticsSnapshot, HOST_LATE_DEADLINE};
use super::{App, Link, Overlay, FLASH_FOR};
use crate::mstp::MstpArgs;
use crate::tui::message::Command;
use crate::tui::mstp::{MstpReport, Sample};
use crate::tui::profile::service::Request;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MstpKey {
Clear,
ToggleWatch,
Cycle {
back: bool,
},
Join,
Pick {
back: bool,
},
Accept,
Back,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct MstpState {
pub(crate) station: Option<MstpArgs>,
pub(crate) thread: Option<String>,
pub(crate) shown: Option<Sample>,
pub(crate) latest: Option<Sample>,
pub(crate) baseline: Option<MstpDiagnosticsSnapshot>,
pub(crate) paused: bool,
}
impl MstpState {
pub(crate) fn report(&mut self, report: MstpReport) {
match report {
MstpReport::Joined(station, thread) => {
*self = Self {
station: Some(station),
thread: Some(thread),
..Self::default()
};
}
MstpReport::Sample(sample) => {
self.latest = Some(*sample);
if !self.paused {
self.shown = Some(*sample);
}
}
}
}
pub(crate) fn key(&mut self, key: MstpKey) {
match key {
MstpKey::Clear => self.baseline = self.latest.map(|sample| sample.counters),
MstpKey::ToggleWatch => {
self.paused = !self.paused;
if !self.paused {
self.shown = self.latest;
}
}
_ => {}
}
}
pub(crate) fn counters(&self) -> Option<Vec<(String, u64)>> {
let now = self.shown?.counters;
let base = self.baseline.unwrap_or_default();
Some(
rows(&now)
.into_iter()
.zip(rows(&base))
.map(|((label, value), (_, before))| {
(label.to_string(), value.saturating_sub(before))
})
.chain([(late_label(), late_timers(&now, &base))])
.collect(),
)
}
pub(crate) fn lateness(&self) -> Option<Lateness> {
let now = self.shown?.counters.mac.deadline_lateness;
let mut buckets = now.buckets;
let all_time: Duration = now.max.into();
let base = self.baseline.map(|base| base.mac.deadline_lateness);
if let Some(base) = base {
for (count, before) in buckets.iter_mut().zip(base.buckets) {
*count = count.saturating_sub(before);
}
}
let max = match (buckets.iter().rposition(|&count| count > 0), base) {
(None, _) => None,
(Some(_), None) => Some(LatenessMax::Exact(all_time)),
(Some(_), Some(base)) if now.max > base.max => Some(LatenessMax::Exact(all_time)),
(Some(top), Some(_)) => Some(match LatenessHistogram::EDGES.get(top) {
Some(&edge) if edge <= all_time => LatenessMax::Below(edge),
_ => LatenessMax::AtMost(all_time),
}),
};
Some(Lateness { buckets, max })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Lateness {
pub(crate) buckets: [u64; 6],
pub(crate) max: Option<LatenessMax>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LatenessMax {
Exact(Duration),
AtMost(Duration),
Below(Duration),
}
pub(crate) const LATENESS_BANDS: [&str; 6] =
["<1 ms", "1-2 ms", "2-5 ms", "5-10 ms", "10-25 ms", "≥25 ms"];
pub(crate) fn late_label() -> String {
format!("Late timers (≥{} ms)", HOST_LATE_DEADLINE.as_millis())
}
fn late_timers(now: &MstpDiagnosticsSnapshot, base: &MstpDiagnosticsSnapshot) -> u64 {
now.mac
.late_deadlines
.saturating_sub(base.mac.late_deadlines)
}
fn rows(c: &MstpDiagnosticsSnapshot) -> [(&'static str, u64); 15] {
let m = &c.mac;
[
("Frames received", m.frames_received),
("Frames sent", m.frames_transmitted),
("NPDUs delivered", m.npdus_delivered),
("Tokens passed", m.token_rotations),
("Token retries", m.token_retries),
("Tokens regenerated", m.token_regenerations),
("Invalid frames", m.parser_errors),
("Frame aborts", m.frame_aborts),
("Reply timeouts", c.wait_for_reply_timeouts),
("Replies postponed", c.reply_postponed_tx),
("Late replies", m.late_replies),
("Own-MAC frames", m.own_address_frames),
("TX queue full", c.outbound_queue_full),
("Serial read errors", c.serial_read_errors),
("Serial write errors", c.serial_write_errors),
]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum JoinKey {
Up,
Down,
Confirm,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum JoinOrigin {
Flags,
Profile(Box<Command>),
Monitor {
note: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct JoinDialog {
pub(crate) settings: MstpArgs,
pub(crate) ports: Vec<String>,
pub(crate) selected: usize,
pub(crate) origin: JoinOrigin,
}
impl JoinDialog {
pub(crate) fn for_flags(settings: MstpArgs, listed: Vec<String>) -> Self {
let mut ports = vec![settings.port.clone()];
ports.extend(listed);
let ports = crate::mstp::dedup_resolved(ports, crate::mstp::resolve);
Self {
settings,
ports,
selected: 0,
origin: JoinOrigin::Flags,
}
}
pub(crate) fn for_profile(settings: MstpArgs, then: Command) -> Self {
Self {
ports: vec![settings.port.clone()],
settings,
selected: 0,
origin: JoinOrigin::Profile(Box::new(then)),
}
}
pub(crate) fn for_monitor(settings: MstpArgs, note: String) -> Self {
Self {
ports: vec![settings.port.clone()],
settings,
selected: 0,
origin: JoinOrigin::Monitor { note },
}
}
pub(crate) fn port(&self) -> &str {
&self.ports[self.selected]
}
pub(crate) fn key(&mut self, key: JoinKey) -> Option<Command> {
let last = self.ports.len().saturating_sub(1);
match key {
JoinKey::Up => self.selected = self.selected.saturating_sub(1),
JoinKey::Down => self.selected = (self.selected + 1).min(last),
JoinKey::Confirm => {
return Some(match &self.origin {
JoinOrigin::Profile(then) => (**then).clone(),
JoinOrigin::Flags | JoinOrigin::Monitor { .. } => Command::JoinTrunk {
settings: MstpArgs {
port: self.port().to_string(),
..self.settings.clone()
},
},
});
}
}
None
}
}
impl App {
pub(super) fn hold_for_join(&mut self, command: Command) -> Option<Command> {
let Command::Profile(Request::Connect { .. }) = &command else {
return Some(command);
};
let settings = self
.connection
.as_ref()
.and_then(|connection| connection.resolved().ok())
.and_then(|profile| profile.mstp.map(|mstp| mstp.args()));
match settings {
Some(settings) if !self.safety.passive => {
self.overlay = Overlay::Join(Box::new(JoinDialog::for_profile(settings, command)));
None
}
_ => Some(command),
}
}
pub(super) fn join_key(&mut self, key: JoinKey, out: &mut Vec<Command>) {
let Overlay::Join(dialog) = &mut self.overlay else {
return;
};
let Some(command) = dialog.key(key) else {
return;
};
let text = format!(
"Joining the MS/TP trunk on {} as station {}.",
dialog.port(),
dialog.settings.mac
);
if let (JoinOrigin::Monitor { .. }, Command::JoinTrunk { settings }) =
(&dialog.origin, &command)
{
let settings = settings.clone();
if !self.confirm_monitor_join(&settings) {
return;
}
}
tracing::info!("{text}");
self.overlay = Overlay::None;
if let Command::Profile(Request::Connect { profile, .. }) = &command {
let kind = profile.transport.kind();
(self.transport, self.style) = (kind.label(), kind.style());
}
self.link = Link::Connecting;
self.flash(text, FLASH_FOR);
out.push(command);
}
pub(super) fn decline_join(&mut self) {
let Overlay::Join(dialog) = &self.overlay else {
return;
};
if let JoinOrigin::Profile(_) = dialog.origin {
if let Some(connection) = &mut self.connection {
connection.pending = false;
}
self.overlay = Overlay::Connection;
} else if let JoinOrigin::Monitor { .. } = dialog.origin {
self.overlay = Overlay::None;
} else if self.link == Link::Picking {
self.quit = true;
} else {
self.overlay = Overlay::None;
}
}
}