refactor: move firmware workspace under firmware
This commit is contained in:
1 parent
153b0bf262
commit
f03fc119a4
222 files changed
+56
-13
No files matched your search
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user