feat(configurator): add web configuration console
This commit is contained in:
1 parent
ceb2134392
commit
b7631c0bc7
14 files changed
+4513
No files matched your search
@@ -0,0 +1,241 @@
|
||||
//! Bounded diagnostic-event history for the browser's live log view.
|
||||
|
||||
use std::{collections::VecDeque, time::SystemTime};
|
||||
|
||||
use serde::{Serialize, Serializer};
|
||||
|
||||
const MAX_LOG_ENTRIES: usize = 512;
|
||||
|
||||
/// Severity attached to one decoded diagnostic event.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum LogSeverity {
|
||||
Emergency,
|
||||
Alert,
|
||||
Critical,
|
||||
Error,
|
||||
Warning,
|
||||
Notice,
|
||||
Info,
|
||||
Debug,
|
||||
Trace,
|
||||
Unknown(u8),
|
||||
}
|
||||
|
||||
impl LogSeverity {
|
||||
/// Decodes the stable MAV_SEVERITY numeric representation without
|
||||
/// accepting values that the protocol has not defined.
|
||||
pub const fn from_wire(level: u8, trace: bool) -> Self {
|
||||
match level {
|
||||
0 => Self::Emergency,
|
||||
1 => Self::Alert,
|
||||
2 => Self::Critical,
|
||||
3 => Self::Error,
|
||||
4 => Self::Warning,
|
||||
5 => Self::Notice,
|
||||
6 => Self::Info,
|
||||
7 if trace => Self::Trace,
|
||||
7 => Self::Debug,
|
||||
value => Self::Unknown(value),
|
||||
}
|
||||
}
|
||||
|
||||
const fn known_label(self) -> Option<&'static str> {
|
||||
match self {
|
||||
Self::Emergency => Some("EMERGENCY"),
|
||||
Self::Alert => Some("ALERT"),
|
||||
Self::Critical => Some("CRITICAL"),
|
||||
Self::Error => Some("ERROR"),
|
||||
Self::Warning => Some("WARNING"),
|
||||
Self::Notice => Some("NOTICE"),
|
||||
Self::Info => Some("INFO"),
|
||||
Self::Debug => Some("DEBUG"),
|
||||
Self::Trace => Some("TRACE"),
|
||||
Self::Unknown(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for LogSeverity {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
if let Some(label) = self.known_label() {
|
||||
serializer.serialize_str(label)
|
||||
} else if let Self::Unknown(value) = self {
|
||||
serializer.serialize_str(&format!("UNKNOWN ({value})"))
|
||||
} else {
|
||||
unreachable!("every known severity has a static label")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One diagnostic event decoded from a CRC-valid Kaptan MAVLink frame.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct LogObservation {
|
||||
pub system_id: u8,
|
||||
pub component_id: u8,
|
||||
pub severity: LogSeverity,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
/// Read-only browser projection of the current link session's log history.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct LogSnapshot {
|
||||
pub session: u32,
|
||||
pub discarded: u64,
|
||||
pub entries: Vec<LogView>,
|
||||
}
|
||||
|
||||
/// One retained diagnostic event, ordered by `id` within a link session.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct LogView {
|
||||
pub id: u32,
|
||||
pub received_at_ms: u64,
|
||||
pub system_id: u8,
|
||||
pub component_id: u8,
|
||||
pub severity: LogSeverity,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LogMonitor {
|
||||
entries: VecDeque<LogView>,
|
||||
session: u32,
|
||||
next_id: u32,
|
||||
discarded: u64,
|
||||
}
|
||||
|
||||
impl LogMonitor {
|
||||
/// Retains every event in receive order up to the fixed host-side bound.
|
||||
pub fn observe(
|
||||
&mut self,
|
||||
observations: impl IntoIterator<Item = LogObservation>,
|
||||
now: SystemTime,
|
||||
) {
|
||||
let received_at_ms = now
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis()
|
||||
.min(u128::from(u64::MAX)) as u64;
|
||||
for observation in observations {
|
||||
if self.next_id == u32::MAX {
|
||||
self.clear();
|
||||
}
|
||||
if self.entries.len() == MAX_LOG_ENTRIES {
|
||||
self.entries.pop_front();
|
||||
self.discarded = self.discarded.saturating_add(1);
|
||||
}
|
||||
self.entries.push_back(LogView {
|
||||
id: self.next_id,
|
||||
received_at_ms,
|
||||
system_id: observation.system_id,
|
||||
component_id: observation.component_id,
|
||||
severity: observation.severity,
|
||||
text: observation.text,
|
||||
});
|
||||
self.next_id += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> LogSnapshot {
|
||||
LogSnapshot {
|
||||
session: self.session,
|
||||
discarded: self.discarded,
|
||||
entries: self.entries.iter().cloned().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts a fresh browser history for a new serial-link session.
|
||||
pub fn clear(&mut self) {
|
||||
self.entries.clear();
|
||||
self.session = self.session.wrapping_add(1);
|
||||
self.next_id = 0;
|
||||
self.discarded = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
|
||||
fn observation(index: usize) -> LogObservation {
|
||||
LogObservation {
|
||||
system_id: 1,
|
||||
component_id: 2,
|
||||
severity: LogSeverity::Info,
|
||||
text: format!("event {index}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retains_distinct_events_in_receive_order() {
|
||||
let mut monitor = LogMonitor::default();
|
||||
let now = SystemTime::UNIX_EPOCH + Duration::from_millis(1_234);
|
||||
monitor.observe([observation(0), observation(1)], now);
|
||||
|
||||
let snapshot = monitor.snapshot();
|
||||
assert_eq!(snapshot.entries.len(), 2);
|
||||
assert_eq!(snapshot.entries[0].id, 0);
|
||||
assert_eq!(snapshot.entries[0].received_at_ms, 1_234);
|
||||
assert_eq!(snapshot.entries[0].text, "event 0");
|
||||
assert_eq!(snapshot.entries[1].id, 1);
|
||||
assert_eq!(snapshot.entries[1].text, "event 1");
|
||||
assert_eq!(snapshot.discarded, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drops_only_the_oldest_entries_at_the_fixed_bound() {
|
||||
let mut monitor = LogMonitor::default();
|
||||
monitor.observe(
|
||||
(0..=MAX_LOG_ENTRIES).map(observation),
|
||||
SystemTime::UNIX_EPOCH,
|
||||
);
|
||||
|
||||
let snapshot = monitor.snapshot();
|
||||
assert_eq!(snapshot.entries.len(), MAX_LOG_ENTRIES);
|
||||
assert_eq!(snapshot.entries[0].text, "event 1");
|
||||
assert_eq!(snapshot.entries[MAX_LOG_ENTRIES - 1].text, "event 512");
|
||||
assert_eq!(snapshot.discarded, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_starts_a_new_link_session() {
|
||||
let mut monitor = LogMonitor::default();
|
||||
monitor.observe([observation(0)], SystemTime::UNIX_EPOCH);
|
||||
monitor.clear();
|
||||
|
||||
let snapshot = monitor.snapshot();
|
||||
assert_eq!(snapshot.session, 1);
|
||||
assert!(snapshot.entries.is_empty());
|
||||
assert_eq!(snapshot.discarded, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wire_severity_is_checked_and_unknown_values_remain_visible() {
|
||||
assert_eq!(LogSeverity::from_wire(3, false), LogSeverity::Error);
|
||||
assert_eq!(LogSeverity::from_wire(7, false), LogSeverity::Debug);
|
||||
assert_eq!(LogSeverity::from_wire(7, true), LogSeverity::Trace);
|
||||
assert_eq!(LogSeverity::from_wire(9, false), LogSeverity::Unknown(9));
|
||||
assert_eq!(
|
||||
serde_json::to_value(LogSeverity::Unknown(9)).unwrap(),
|
||||
"UNKNOWN (9)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identifier_exhaustion_starts_a_fresh_rendering_session() {
|
||||
let mut monitor = LogMonitor {
|
||||
next_id: u32::MAX,
|
||||
..LogMonitor::default()
|
||||
};
|
||||
monitor.observe([observation(0)], SystemTime::UNIX_EPOCH);
|
||||
|
||||
let snapshot = monitor.snapshot();
|
||||
assert_eq!(snapshot.session, 1);
|
||||
assert_eq!(snapshot.entries.len(), 1);
|
||||
assert_eq!(snapshot.entries[0].id, 0);
|
||||
assert_eq!(snapshot.entries[0].text, "event 0");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user