refactor(firmware): centralize configuration and diagnostics

This commit is contained in:
Onur Özkan committed 2026-08-22 18:37:18 +03:00
1 parent 7048ed89d0
commit 387fdaad23
140 files changed
+6891 -4541

No files matched your search

+60 -79
View File
@@ -6,15 +6,12 @@
use core::{
cell::UnsafeCell,
sync::atomic::{AtomicBool, AtomicU8, Ordering},
sync::atomic::{AtomicBool, Ordering},
};
use log::{Level, LevelFilter, Log, Metadata, Record, SetLoggerError};
use crate::{
ErrorAdditionalInformation, ErrorPath, EventHub, LogFilter, LogFlags, LogText, PublishOutcome,
Severity,
};
use crate::{ErrorAdditionalInformation, ErrorPath, LogFlags, LogRecord, LogText, Severity};
/// Saturating, process-local accounting for the optional global facade.
///
@@ -24,8 +21,6 @@ use crate::{
pub struct FacadeHealth {
/// Calls rejected before formatting because their context is forbidden.
pub forbidden_context: u32,
/// Optional records excluded by the active verbosity cap.
pub filtered: u32,
/// Calls that lost the one-attempt ingress ownership race.
pub contended: u32,
/// Calls dropped because their bounded ingress lane was full.
@@ -36,7 +31,6 @@ pub struct FacadeHealth {
struct FacadeCounters {
forbidden_context: core::sync::atomic::AtomicU32,
filtered: core::sync::atomic::AtomicU32,
contended: core::sync::atomic::AtomicU32,
full: core::sync::atomic::AtomicU32,
truncated: core::sync::atomic::AtomicU32,
@@ -46,7 +40,6 @@ impl FacadeCounters {
const fn new() -> Self {
Self {
forbidden_context: core::sync::atomic::AtomicU32::new(0),
filtered: core::sync::atomic::AtomicU32::new(0),
contended: core::sync::atomic::AtomicU32::new(0),
full: core::sync::atomic::AtomicU32::new(0),
truncated: core::sync::atomic::AtomicU32::new(0),
@@ -71,7 +64,6 @@ impl FacadeCounters {
fn snapshot(&self) -> FacadeHealth {
FacadeHealth {
forbidden_context: self.forbidden_context.load(Ordering::Relaxed),
filtered: self.filtered.load(Ordering::Relaxed),
contended: self.contended.load(Ordering::Relaxed),
full: self.full.load(Ordering::Relaxed),
truncated: self.truncated.load(Ordering::Relaxed),
@@ -146,8 +138,8 @@ impl<const N: usize> IngressQueue<N> {
}
}
/// Fixed global-facade ingress. The normal task/runtime owner drains it into
/// an [`EventHub`] after startup.
/// Fixed global-facade ingress. The normal task/runtime owner takes each
/// transport-neutral record and fans it out to independently filtered sinks.
pub struct FacadeIngress<const N: usize> {
/// One dedicated critical slot keeps error records independent from the
/// bounded ordinary FIFO. It is deliberately one slot: producers never
@@ -157,7 +149,6 @@ pub struct FacadeIngress<const N: usize> {
critical_busy: AtomicBool,
queue: UnsafeCell<IngressQueue<N>>,
queue_busy: AtomicBool,
maximum_severity: AtomicU8,
counters: FacadeCounters,
}
@@ -167,29 +158,17 @@ pub struct FacadeIngress<const N: usize> {
unsafe impl<const N: usize> Sync for FacadeIngress<N> {}
impl<const N: usize> FacadeIngress<N> {
/// Creates empty fixed ingress with the boot-active verbosity cap.
pub const fn new(filter: LogFilter) -> Self {
/// Creates an empty fixed ingress.
pub const fn new() -> Self {
Self {
critical: UnsafeCell::new(None),
critical_busy: AtomicBool::new(false),
queue: UnsafeCell::new(IngressQueue::new()),
queue_busy: AtomicBool::new(false),
maximum_severity: AtomicU8::new(filter.maximum_severity() as u8),
counters: FacadeCounters::new(),
}
}
/// Updates the runtime-selected maximum non-error verbosity.
pub fn set_filter(&self, filter: LogFilter) {
self.maximum_severity
.store(filter.maximum_severity() as u8, Ordering::Release);
}
fn allows(&self, severity: Severity) -> bool {
severity <= Severity::Error
|| (severity as u8) <= self.maximum_severity.load(Ordering::Acquire)
}
fn push(&self, record: IngressRecord) {
if record.severity <= Severity::Error {
self.push_critical(record);
@@ -222,24 +201,23 @@ impl<const N: usize> FacadeIngress<N> {
self.counters.snapshot()
}
/// Moves at most one preformatted facade record into the central hub.
pub fn drain_one_into<const CRITICAL: usize, const BEST_EFFORT: usize>(
&self,
hub: &mut EventHub<CRITICAL, BEST_EFFORT>,
) -> Option<PublishOutcome> {
/// Takes at most one preformatted event for composition-level fan-out.
///
/// The ingress deliberately applies no sink policy. A caller may submit
/// the returned copy to MAVLink, blackbox, or any later sink without one
/// sink's verbosity selection suppressing another's event.
pub fn take(&self) -> Option<LogRecord> {
let record = match self.with_critical(Option::take) {
Some(Some(record)) => record,
Some(None) => self.with_queue(IngressQueue::pop).flatten()?,
None => return None,
};
Some(match record.body {
IngressBody::Log { message } => {
hub.publish_text(record.severity, record.flags, message)
}
IngressBody::Log { message } => LogRecord::new(record.severity, record.flags, message),
IngressBody::Error {
path,
additional_information,
} => hub.publish_error_text(record.flags, path, additional_information),
} => LogRecord::error(record.flags, path, additional_information),
})
}
@@ -278,6 +256,12 @@ impl<const N: usize> FacadeIngress<N> {
}
}
impl<const N: usize> Default for FacadeIngress<N> {
fn default() -> Self {
Self::new()
}
}
/// Installed standard logger. The context probe must return true for
/// ISR/panic/hard-fault/shutdown contexts; those calls are rejected before
/// [`Record::args`] is formatted.
@@ -296,7 +280,8 @@ impl<const N: usize> GlobalLogger<N> {
}
/// Installs this logger once for the process/image and enables all Rust
/// levels compiled into the image. Kaptan's own filter applies afterward.
/// levels compiled into the image. Each sink applies its policy after it
/// receives a record from the ingress.
pub fn install(&'static self) -> Result<(), SetLoggerError> {
log::set_logger(self)?;
log::set_max_level(LevelFilter::Trace);
@@ -305,18 +290,11 @@ impl<const N: usize> GlobalLogger<N> {
}
impl<const N: usize> Log for GlobalLogger<N> {
fn enabled(&self, metadata: &Metadata<'_>) -> bool {
fn enabled(&self, _: &Metadata<'_>) -> bool {
if (self.forbidden_context)() {
FacadeCounters::increment(&self.ingress.counters.forbidden_context);
return false;
}
let Some(severity) = severity(metadata.level()) else {
return false;
};
if !self.ingress.allows(severity) {
FacadeCounters::increment(&self.ingress.counters.filtered);
return false;
}
true
}
@@ -325,13 +303,7 @@ impl<const N: usize> Log for GlobalLogger<N> {
FacadeCounters::increment(&self.ingress.counters.forbidden_context);
return;
}
let Some(severity) = severity(record.level()) else {
return;
};
if !self.ingress.allows(severity) {
FacadeCounters::increment(&self.ingress.counters.filtered);
return;
}
let severity = severity(record.level());
let flags = if record.level() == Level::Trace {
LogFlags::TRACE
} else {
@@ -367,12 +339,12 @@ impl<const N: usize> Log for GlobalLogger<N> {
fn flush(&self) {}
}
const fn severity(level: Level) -> Option<Severity> {
const fn severity(level: Level) -> Severity {
match level {
Level::Error => Some(Severity::Error),
Level::Warn => Some(Severity::Warning),
Level::Info => Some(Severity::Info),
Level::Debug | Level::Trace => Some(Severity::Debug),
Level::Error => Severity::Error,
Level::Warn => Severity::Warning,
Level::Info => Severity::Info,
Level::Debug | Level::Trace => Severity::Debug,
}
}
@@ -382,10 +354,10 @@ mod tests {
use std::sync::Once;
use super::{FacadeIngress, GlobalLogger};
use crate::{EventHub, LogFilter, RecordChannel, Severity};
use crate::{RecordChannel, Severity};
use log::{Level, Log, Metadata, Record};
static GLOBAL_INGRESS: FacadeIngress<2> = FacadeIngress::new(LogFilter::new(Severity::Debug));
static GLOBAL_INGRESS: FacadeIngress<2> = FacadeIngress::new();
static GLOBAL_LOGGER: GlobalLogger<2> = GlobalLogger::new(&GLOBAL_INGRESS, never_forbidden);
static INSTALL_GLOBAL: Once = Once::new();
@@ -399,7 +371,7 @@ mod tests {
#[test]
fn rejects_forbidden_context_before_formatting() {
static INGRESS: FacadeIngress<2> = FacadeIngress::new(LogFilter::new(Severity::Debug));
static INGRESS: FacadeIngress<2> = FacadeIngress::new();
let logger = GlobalLogger::new(&INGRESS, always_forbidden);
let metadata = Metadata::builder()
.level(Level::Error)
@@ -408,19 +380,31 @@ mod tests {
assert!(!logger.enabled(&metadata));
assert_eq!(INGRESS.health().forbidden_context, 1);
static FILTERED: FacadeIngress<2> = FacadeIngress::new(LogFilter::new(Severity::Info));
let filtered_logger = GlobalLogger::new(&FILTERED, never_forbidden);
static UNFILTERED: FacadeIngress<2> = FacadeIngress::new();
let unfiltered_logger = GlobalLogger::new(&UNFILTERED, never_forbidden);
let debug = Metadata::builder()
.level(Level::Debug)
.target("test")
.build();
assert!(!filtered_logger.enabled(&debug));
assert_eq!(FILTERED.health().filtered, 1);
assert!(unfiltered_logger.enabled(&debug));
unfiltered_logger.log(
&Record::builder()
.metadata(debug)
.args(format_args!("sink decides whether to keep this"))
.build(),
);
assert_eq!(
UNFILTERED
.take()
.expect("debug event reaches the unfiltered ingress")
.severity(),
Severity::Debug
);
}
#[test]
fn formats_once_then_drains_into_priority_hub() {
static INGRESS: FacadeIngress<2> = FacadeIngress::new(LogFilter::new(Severity::Debug));
fn formats_once_then_exposes_a_transport_neutral_record() {
static INGRESS: FacadeIngress<2> = FacadeIngress::new();
let logger = GlobalLogger::new(&INGRESS, never_forbidden);
let record = Record::builder()
.metadata(
@@ -432,9 +416,7 @@ mod tests {
.args(format_args!("link failed"))
.build();
logger.log(&record);
let mut hub = EventHub::<1, 1>::new(LogFilter::default());
assert!(INGRESS.drain_one_into(&mut hub).is_some());
let pending = hub.peek().unwrap();
let pending = INGRESS.take().expect("facade event retained");
assert_eq!(pending.error_path().unwrap().as_str(), "test");
assert_eq!(
pending.additional_information().unwrap().as_str(),
@@ -456,11 +438,10 @@ mod tests {
fn standard_log_macros_enqueue_bounded_targeted_text() {
INSTALL_GLOBAL.call_once(|| GLOBAL_LOGGER.install().expect("one global test logger"));
log::info!("armed configuration loaded");
let mut hub = EventHub::<1, 2>::new(LogFilter::default());
assert!(GLOBAL_INGRESS.drain_one_into(&mut hub).is_some());
assert!(
hub.peek()
.unwrap()
GLOBAL_INGRESS
.take()
.expect("macro event retained")
.message()
.unwrap()
.as_str()
@@ -470,7 +451,7 @@ mod tests {
#[test]
fn forbidden_context_skips_argument_formatting() {
static INGRESS: FacadeIngress<2> = FacadeIngress::new(LogFilter::new(Severity::Debug));
static INGRESS: FacadeIngress<2> = FacadeIngress::new();
let logger = GlobalLogger::new(&INGRESS, always_forbidden);
logger.log(
&Record::builder()
@@ -488,7 +469,7 @@ mod tests {
#[test]
fn bounded_ingress_exposes_local_drop_and_truncation_accounting() {
static INGRESS: FacadeIngress<1> = FacadeIngress::new(LogFilter::new(Severity::Info));
static INGRESS: FacadeIngress<1> = FacadeIngress::new();
let logger = GlobalLogger::new(&INGRESS, never_forbidden);
logger.log(
&Record::builder()
@@ -531,7 +512,7 @@ mod tests {
#[test]
fn full_optional_ingress_retains_an_error_for_the_error_channel() {
static INGRESS: FacadeIngress<1> = FacadeIngress::new(LogFilter::new(Severity::Info));
static INGRESS: FacadeIngress<1> = FacadeIngress::new();
let logger = GlobalLogger::new(&INGRESS, never_forbidden);
logger.log(
&Record::builder()
@@ -556,9 +537,9 @@ mod tests {
.build(),
);
let mut hub = EventHub::<1, 1>::new(LogFilter::default());
assert!(INGRESS.drain_one_into(&mut hub).is_some());
let error = hub.peek().expect("error retained ahead of optional text");
let error = INGRESS
.take()
.expect("error retained ahead of optional text");
assert_eq!(error.channel(), RecordChannel::Error);
assert_eq!(error.error_path().unwrap().as_str(), "test");
assert_eq!(error.additional_information().unwrap().as_str(), "critical");