feat(configurator): add web configuration console

This commit is contained in:
Onur Özkan committed 2026-08-22 18:40:43 +03:00
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");
}
}