refactor: move firmware workspace under firmware

This commit is contained in:
Onur Özkan committed 2026-08-10 16:58:55 +03:00
1 parent 153b0bf262
commit f03fc119a4
222 files changed
+56 -13

No files matched your search

+433
View File
@@ -0,0 +1,433 @@
//! Bounded priority handling for fixed human-readable diagnostic records.
use core::fmt;
use crate::{
DeliveryClass, ErrorAdditionalInformation, ErrorPath, ErrorReport, LogFilter, LogFlags,
LogRecord, LogText, Severity, StaticError,
};
/// Outcome from one bounded diagnostic admission attempt.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PublishOutcome {
/// The record was retained.
Queued,
/// An adjacent equivalent record absorbed this occurrence.
Coalesced,
/// An ordinary log record was excluded by the configured verbosity cap.
Filtered,
/// A best-effort record was replaced by newer optional information.
ReplacedOldest,
/// The critical lane kept its earlier record.
RejectedFull,
}
/// Result returned by the sole telemetry owner after attempting to admit a
/// record to its lower transport.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TransportAdmission {
/// The lower transport retained the record, so it may leave the hub.
Accepted,
/// The lower transport did not accept it yet; retain it for retry.
Retry,
/// The lower transport will not accept optional detail. This can only
/// discard a best-effort record; critical records remain queued.
DropBestEffort,
}
/// Saturating, process-local accounting for bounded diagnostic admission.
///
/// These counters intentionally are not part of `KAPTAN_LOG` or
/// `KAPTAN_ERROR`: neither wire message is a metrics protocol.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct HubHealth {
/// Optional records rejected by the active verbosity cap.
pub filtered: u32,
/// Adjacent records folded into an existing entry.
pub coalesced: u32,
/// Optional entries replaced because their bounded lane was full.
pub best_effort_replaced: u32,
/// Error entries rejected because their bounded critical lane was full.
pub critical_rejected: u32,
/// Optional entries the lower transport refused permanently.
pub best_effort_transport_dropped: u32,
}
impl HubHealth {
fn increment(value: &mut u32) {
*value = value.saturating_add(1);
}
}
struct Lane<const N: usize> {
entries: [Option<LogRecord>; N],
head: usize,
len: usize,
}
impl<const N: usize> Lane<N> {
const fn new() -> Self {
assert!(N > 0, "diagnostic lanes must be nonempty");
Self {
entries: [None; N],
head: 0,
len: 0,
}
}
fn publish(&mut self, record: LogRecord, replace_oldest: bool) -> PublishOutcome {
if self.len != 0 {
let tail = (self.head + self.len - 1) % N;
if let Some(previous) = self.entries[tail].as_mut()
&& previous.is_equivalent_to(&record)
{
previous.increment_occurrences();
return PublishOutcome::Coalesced;
}
}
if self.len == N {
if replace_oldest {
self.entries[self.head] = Some(record);
self.head = (self.head + 1) % N;
return PublishOutcome::ReplacedOldest;
}
return PublishOutcome::RejectedFull;
}
let index = (self.head + self.len) % N;
self.entries[index] = Some(record);
self.len += 1;
PublishOutcome::Queued
}
fn peek(&self) -> Option<LogRecord> {
self.entries.get(self.head).copied().flatten()
}
fn pop(&mut self) -> Option<LogRecord> {
if self.len == 0 {
return None;
}
let record = self.entries[self.head].take();
self.head = (self.head + 1) % N;
self.len -= 1;
record
}
}
/// One bounded priority hub. It owns no global logger, MAVLink state, or DMA.
pub struct EventHub<const CRITICAL: usize, const BEST_EFFORT: usize> {
filter: LogFilter,
critical: Lane<CRITICAL>,
best_effort: Lane<BEST_EFFORT>,
health: HubHealth,
}
impl<const CRITICAL: usize, const BEST_EFFORT: usize> EventHub<CRITICAL, BEST_EFFORT> {
/// Creates one hub with its immutable boot-active verbosity cap.
pub const fn new(filter: LogFilter) -> Self {
Self {
filter,
critical: Lane::new(),
best_effort: Lane::new(),
health: HubHealth {
filtered: 0,
coalesced: 0,
best_effort_replaced: 0,
critical_rejected: 0,
best_effort_transport_dropped: 0,
},
}
}
/// Immutable best-effort verbosity policy.
pub const fn filter(&self) -> LogFilter {
self.filter
}
/// Process-local bounded-admission accounting.
pub const fn health(&self) -> HubHealth {
self.health
}
/// Formats and retains one ordinary task/worker diagnostic. Error-level
/// severities are emitted through `KAPTAN_ERROR`; lower severities use
/// `KAPTAN_LOG`.
pub fn publish_arguments(
&mut self,
severity: Severity,
flags: LogFlags,
arguments: fmt::Arguments<'_>,
) -> PublishOutcome {
self.publish_text(severity, flags, LogText::from_arguments(arguments))
}
/// Retains one static derived error without formatting or exposing source
/// values. The adapter maps it to `KAPTAN_ERROR`.
pub fn publish_error<E: ErrorReport>(&mut self, flags: LogFlags, error: &E) -> PublishOutcome {
self.publish_error_static(flags, error.static_error())
}
/// Retains one static mailbox error. This is the ISR-safe counterpart to
/// [`Self::publish_error`].
pub fn publish_error_static(
&mut self,
flags: LogFlags,
error: &'static StaticError,
) -> PublishOutcome {
self.publish_error_text(
flags,
ErrorPath::from_message(error.path()),
ErrorAdditionalInformation::from_message(error.additional_information()),
)
}
/// Retains a preformatted error identity and optional detail. Both
/// components stay transport-neutral here; the MAVLink adapter enforces
/// its independent on-wire capacities.
pub fn publish_error_text(
&mut self,
flags: LogFlags,
path: ErrorPath,
additional_information: ErrorAdditionalInformation,
) -> PublishOutcome {
self.publish_record(LogRecord::error(flags, path, additional_information))
}
/// Retains one preformatted bounded text payload. Error-or-higher
/// severities are independently retained and sent as `KAPTAN_ERROR`.
pub fn publish_text(
&mut self,
severity: Severity,
flags: LogFlags,
text: LogText,
) -> PublishOutcome {
let delivery = delivery_for(severity);
if delivery == DeliveryClass::BestEffort && !self.filter.allows(severity) {
HubHealth::increment(&mut self.health.filtered);
return PublishOutcome::Filtered;
}
self.publish_record(LogRecord::new(severity, flags, text))
}
fn publish_record(&mut self, record: LogRecord) -> PublishOutcome {
let delivery = record.delivery();
let outcome = match delivery {
DeliveryClass::Critical => self.critical.publish(record, false),
DeliveryClass::BestEffort => self.best_effort.publish(record, true),
};
match outcome {
PublishOutcome::Coalesced => HubHealth::increment(&mut self.health.coalesced),
PublishOutcome::ReplacedOldest => {
HubHealth::increment(&mut self.health.best_effort_replaced);
}
PublishOutcome::RejectedFull => {
HubHealth::increment(&mut self.health.critical_rejected);
}
PublishOutcome::Queued | PublishOutcome::Filtered => {}
}
outcome
}
/// Highest-priority record without relinquishing it.
pub fn peek(&self) -> Option<LogRecord> {
self.critical.peek().or_else(|| self.best_effort.peek())
}
/// Submits the current record through the sole telemetry owner. The hub
/// removes it only after `Accepted`, and only permits `DropBestEffort` for
/// optional detail. This keeps encoder/admission retry semantics local.
pub fn submit_next<R>(
&mut self,
submit: impl FnOnce(&LogRecord) -> (TransportAdmission, R),
) -> Option<R> {
let record = self.peek()?;
let (admission, result) = submit(&record);
match admission {
TransportAdmission::Accepted => {
let _ = self.pop();
}
TransportAdmission::DropBestEffort
if record.delivery() == DeliveryClass::BestEffort =>
{
let _ = self.best_effort.pop();
HubHealth::increment(&mut self.health.best_effort_transport_dropped);
}
TransportAdmission::Retry | TransportAdmission::DropBestEffort => {}
}
Some(result)
}
fn pop(&mut self) -> Option<LogRecord> {
self.critical.pop().or_else(|| self.best_effort.pop())
}
}
fn delivery_for(severity: Severity) -> DeliveryClass {
if severity <= Severity::Error {
DeliveryClass::Critical
} else {
DeliveryClass::BestEffort
}
}
#[cfg(test)]
mod tests {
use super::{EventHub, PublishOutcome, TransportAdmission};
use crate::{
DeliveryClass, ErrorAdditionalInformation, ErrorPath, LogFilter, LogFlags, LogText,
RecordChannel, Severity, StaticError,
};
#[test]
fn errors_bypass_filter_but_debug_does_not() {
let mut hub = EventHub::<1, 1>::new(LogFilter::new(Severity::Info));
assert_eq!(
hub.publish_text(
Severity::Debug,
LogFlags::NONE,
LogText::from_message("debug")
),
PublishOutcome::Filtered
);
assert_eq!(
hub.publish_text(
Severity::Error,
LogFlags::NONE,
LogText::from_message("error")
),
PublishOutcome::Queued
);
let record = hub.peek().unwrap();
assert_eq!(record.error_path().unwrap().as_str(), "error");
assert!(record.additional_information().unwrap().is_empty());
assert_eq!(record.channel(), RecordChannel::Error);
}
#[test]
fn static_messages_and_repeats_are_bounded() {
static FAULT: StaticError = StaticError::new("driver::Fault");
let mut hub = EventHub::<1, 1>::new(LogFilter::default());
assert_eq!(
hub.publish_error_static(LogFlags::NONE, &FAULT),
PublishOutcome::Queued
);
assert_eq!(
hub.publish_error_static(LogFlags::NONE, &FAULT),
PublishOutcome::Coalesced
);
let record = hub.peek().unwrap();
assert_eq!(record.occurrences, 2);
assert!(record.flags().contains(LogFlags::COALESCED));
assert_eq!(record.error_path().unwrap().as_str(), "driver::Fault");
assert!(record.additional_information().unwrap().is_empty());
}
#[test]
fn static_error_detail_needs_no_producer_formatting() {
static FAULT: StaticError =
StaticError::with_additional_information("driver::Dma::Fault", "stream 7");
let mut hub = EventHub::<1, 1>::new(LogFilter::default());
assert_eq!(
hub.publish_error_static(LogFlags::NONE, &FAULT),
PublishOutcome::Queued
);
let record = hub.peek().unwrap();
assert_eq!(record.error_path().unwrap().as_str(), "driver::Dma::Fault");
assert_eq!(
record.additional_information().unwrap().as_str(),
"stream 7"
);
}
#[test]
fn error_identity_and_detail_are_independent_record_components() {
let mut hub = EventHub::<1, 1>::new(LogFilter::default());
let _ = hub.publish_error_text(
LogFlags::NONE,
ErrorPath::from_message("driver::Icm42688::Transfer"),
ErrorAdditionalInformation::from_message("SPI DMA timeout"),
);
let record = hub.peek().unwrap();
assert_eq!(record.channel(), RecordChannel::Error);
assert_eq!(
record.error_path().unwrap().as_str(),
"driver::Icm42688::Transfer"
);
assert_eq!(
record.additional_information().unwrap().as_str(),
"SPI DMA timeout"
);
}
#[test]
fn retry_retains_critical_and_only_best_effort_can_be_discarded() {
let mut hub = EventHub::<1, 1>::new(LogFilter::default());
let _ = hub.publish_text(
Severity::Error,
LogFlags::NONE,
LogText::from_message("fault"),
);
assert_eq!(
hub.submit_next(|_| (TransportAdmission::DropBestEffort, ())),
Some(())
);
assert_eq!(hub.peek().unwrap().delivery(), DeliveryClass::Critical);
assert_eq!(
hub.submit_next(|_| (TransportAdmission::Accepted, ())),
Some(())
);
assert!(hub.peek().is_none());
let _ = hub.publish_text(
Severity::Info,
LogFlags::NONE,
LogText::from_message("detail"),
);
assert_eq!(
hub.submit_next(|_| (TransportAdmission::DropBestEffort, ())),
Some(())
);
assert!(hub.peek().is_none());
assert_eq!(hub.health().best_effort_transport_dropped, 1);
}
#[test]
fn admission_losses_are_retained_as_local_saturating_accounting() {
let mut hub = EventHub::<1, 1>::new(LogFilter::new(Severity::Info));
let _ = hub.publish_text(
Severity::Debug,
LogFlags::NONE,
LogText::from_message("filtered"),
);
let _ = hub.publish_text(
Severity::Info,
LogFlags::NONE,
LogText::from_message("first optional"),
);
let _ = hub.publish_text(
Severity::Info,
LogFlags::NONE,
LogText::from_message("newer optional"),
);
let _ = hub.publish_text(
Severity::Error,
LogFlags::NONE,
LogText::from_message("first error"),
);
let _ = hub.publish_text(
Severity::Error,
LogFlags::NONE,
LogText::from_message("second error"),
);
assert_eq!(
hub.health(),
super::HubHealth {
filtered: 1,
coalesced: 0,
best_effort_replaced: 1,
critical_rejected: 1,
best_effort_transport_dropped: 0,
}
);
}
}