use std::collections::VecDeque;
use std::time::{Duration, Instant};
use bacnet_transport::sc::{ScActiveHub, ScConnectionState};
use super::App;
use crate::tui::sc::credentials::{Credentials, Level};
use crate::tui::sc::doctor::{self, Diagnosis};
use crate::tui::sc::{
hub_name, vmac_text, CertSummary, Fault, LinkEvent, LinkSnapshot, ScFailure, ScIdentity,
ScReport,
};
pub(crate) const TIMELINE: usize = 100;
pub(crate) const TRANSITIONS: usize = 64;
pub(crate) const DUPLICATES: usize = 16;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum View {
#[default]
Status,
Doctor,
Credentials,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ScKey {
Cycle {
back: bool,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Severity {
Info,
Warn,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Entry {
pub(crate) at: Instant,
pub(crate) severity: Severity,
pub(crate) text: String,
pub(crate) repeats: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Transition {
pub(crate) at: Instant,
pub(crate) state: ScConnectionState,
pub(crate) hub: Option<ScActiveHub>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct LastFailure {
pub(crate) at: Instant,
pub(crate) hub: Option<ScActiveHub>,
pub(crate) failure: ScFailure,
pub(crate) diagnosis: Diagnosis,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Duplicate {
pub(crate) at: Instant,
pub(crate) hub: Option<ScActiveHub>,
pub(crate) reseeded: bool,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct ScState {
pub(crate) identity: Option<ScIdentity>,
pub(crate) link: Option<(LinkSnapshot, Instant)>,
pub(crate) transitions: VecDeque<Transition>,
pub(crate) attempt: Option<(ScActiveHub, u32, Duration)>,
pub(crate) chain: Vec<CertSummary>,
pub(crate) failure: Option<LastFailure>,
pub(crate) duplicates: VecDeque<Duplicate>,
pub(crate) timeline: VecDeque<Entry>,
pub(crate) missed: u64,
pub(crate) view: View,
pub(crate) credentials: Option<Result<Credentials, String>>,
}
pub(crate) fn describe(event: &LinkEvent) -> (Severity, String) {
match event {
LinkEvent::Connected { hub, hub_vmac } => (
Severity::Info,
format!(
"connected to the {} hub (hub VMAC {})",
hub_name(*hub),
vmac_text(hub_vmac)
),
),
LinkEvent::Disconnected { hub, fault } => (
Severity::Warn,
format!(
"lost the {} hub: {}",
hub_name(*hub),
doctor::diagnose(fault).summary
),
),
LinkEvent::ConnectFailed { hub, fault } => (
Severity::Warn,
format!(
"dial to the {} hub failed: {}",
hub_name(*hub),
doctor::diagnose(fault).summary
),
),
LinkEvent::Reconnecting {
hub,
attempt,
backoff,
} => (
Severity::Info,
format!(
"reconnect attempt {attempt} to the {} hub after {} ms",
hub_name(*hub),
backoff.as_millis()
),
),
LinkEvent::FailoverEngaged => (
Severity::Warn,
"primary hub unreachable: switching to the failover hub".into(),
),
LinkEvent::PrimaryRestored => (
Severity::Info,
"primary hub is back: leaving the failover hub".into(),
),
LinkEvent::PrimaryRestoreFailed { fault } => (
Severity::Warn,
format!(
"primary hub still unreachable: {}",
doctor::diagnose(fault).summary
),
),
LinkEvent::GaveUp { attempts } => (
Severity::Error,
format!("gave up reconnecting after {attempts} attempts"),
),
}
}
impl ScState {
fn note(&mut self, at: Instant, severity: Severity, text: String) {
if self.timeline.len() == TIMELINE {
self.timeline.pop_front();
}
self.timeline.push_back(Entry {
at,
severity,
text,
repeats: 1,
});
}
fn repeat(&mut self, at: Instant, text: &str) -> bool {
match self.timeline.back_mut() {
Some(last) if last.text == text => {
last.repeats = last.repeats.saturating_add(1);
last.at = at;
true
}
_ => false,
}
}
fn enter(&mut self, at: Instant, state: ScConnectionState, hub: Option<ScActiveHub>) {
if self
.transitions
.back()
.is_some_and(|last| (last.state, last.hub) == (state, hub))
{
return;
}
if self.transitions.len() == TRANSITIONS {
self.transitions.pop_front();
}
self.transitions.push_back(Transition { at, state, hub });
}
fn failed(&mut self, at: Instant, hub: Option<ScActiveHub>, failure: ScFailure) {
if failure.fault.is_duplicate_vmac() {
if self.duplicates.len() == DUPLICATES {
self.duplicates.pop_front();
}
let reseeded = matches!(failure.fault, Fault::Nak { reseeded: true, .. });
self.duplicates.push_back(Duplicate { at, hub, reseeded });
}
let diagnosis = doctor::diagnose(&failure.fault);
self.failure = Some(LastFailure {
at,
hub,
failure,
diagnosis,
});
}
fn patch(&mut self, at: Instant, state: ScConnectionState, hub: Option<ScActiveHub>) {
let (link, when) = self.link.get_or_insert((
LinkSnapshot {
state,
hub,
hub_vmac: None,
heartbeat_age: None,
heartbeat_rtt: None,
},
at,
));
link.state = state;
link.hub = hub.or(link.hub);
if state != ScConnectionState::Connected {
link.hub_vmac = None;
link.heartbeat_age = None;
}
*when = at;
}
fn event(&mut self, at: Instant, event: LinkEvent) {
let (severity, text) = describe(&event);
let repeated =
matches!(event, LinkEvent::PrimaryRestoreFailed { .. }) && self.repeat(at, &text);
if !repeated {
self.note(at, severity, text);
}
match event {
LinkEvent::Connected { hub, hub_vmac } => {
self.attempt = None;
self.enter(at, ScConnectionState::Connected, Some(hub));
self.patch(at, ScConnectionState::Connected, Some(hub));
if let Some((link, _)) = &mut self.link {
link.hub_vmac = Some(hub_vmac);
}
}
LinkEvent::Disconnected { hub, fault } => {
self.chain.clear();
self.enter(at, ScConnectionState::Disconnected, Some(hub));
self.patch(at, ScConnectionState::Disconnected, Some(hub));
if fault != Fault::LocalClose {
let detail = describe(&LinkEvent::Disconnected {
hub,
fault: fault.clone(),
})
.1;
let failure = ScFailure {
fault,
detail,
presented: Vec::new(),
};
self.failed(at, Some(hub), failure);
}
}
LinkEvent::ConnectFailed { hub, fault } => {
let detail = describe(&LinkEvent::ConnectFailed {
hub,
fault: fault.clone(),
})
.1;
let failure = ScFailure {
fault,
detail,
presented: Vec::new(),
};
self.failed(at, Some(hub), failure);
}
LinkEvent::Reconnecting {
hub,
attempt,
backoff,
} => {
self.attempt = Some((hub, attempt, backoff));
self.enter(at, ScConnectionState::Connecting, Some(hub));
self.patch(at, ScConnectionState::Connecting, Some(hub));
}
LinkEvent::FailoverEngaged => {
self.enter(
at,
ScConnectionState::Connecting,
Some(ScActiveHub::Failover),
);
self.patch(
at,
ScConnectionState::Connecting,
Some(ScActiveHub::Failover),
);
}
LinkEvent::PrimaryRestored | LinkEvent::PrimaryRestoreFailed { .. } => {}
LinkEvent::GaveUp { .. } => {
let hub = self.link.as_ref().and_then(|(link, _)| link.hub);
self.enter(at, ScConnectionState::Disconnected, hub);
self.patch(at, ScConnectionState::Disconnected, hub);
}
}
}
pub(crate) fn apply(&mut self, at: Instant, report: ScReport) {
match report {
ScReport::Dialing(identity) => {
self.identity = Some(identity);
self.enter(
at,
ScConnectionState::Connecting,
Some(ScActiveHub::Primary),
);
}
ScReport::Failed(failure) => {
self.note(
at,
Severity::Error,
format!(
"could not connect: {}",
doctor::diagnose(&failure.fault).summary
),
);
self.enter(at, ScConnectionState::Disconnected, None);
self.failed(at, None, *failure);
self.view = if self.lint_refused() {
View::Credentials
} else {
View::Doctor
};
}
ScReport::Credentials(credentials) => self.credentials = Some(*credentials),
ScReport::Status(snapshot) => {
self.enter(at, snapshot.state, snapshot.hub);
self.link = Some((snapshot, at));
}
ScReport::Event(event) => self.event(at, event),
ScReport::Gap { missed } => {
self.missed += missed;
self.note(
at,
Severity::Warn,
format!("... {missed} events missed; status resynced"),
);
}
ScReport::Chain(chain) => self.chain = chain,
}
}
pub(crate) fn lint_refused(&self) -> bool {
match &self.credentials {
Some(Ok(c)) => c.findings.iter().any(|(level, _)| *level == Level::Failed),
Some(Err(_)) => true,
None => false,
}
}
pub(crate) fn heartbeat_age(&self, now: Instant) -> Option<Duration> {
let (link, at) = self.link.as_ref()?;
Some(link.heartbeat_age? + now.saturating_duration_since(*at))
}
}
impl App {
pub(super) fn sc_report(&mut self, report: ScReport) {
let now = self.now;
self.sc.apply(now, report);
}
pub(super) fn sc_key(&mut self, key: ScKey) {
match key {
ScKey::Cycle { back } => {
const ORDER: [View; 3] = [View::Status, View::Doctor, View::Credentials];
let at = ORDER.iter().position(|v| *v == self.sc.view).unwrap_or(0);
let step = if back { ORDER.len() - 1 } else { 1 };
self.sc.view = ORDER[(at + step) % ORDER.len()];
}
}
}
}