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

+83 -54
View File
@@ -1,45 +1,48 @@
//! Central, transport-neutral flight-runtime composition.
//!
//! This crate owns the one configuration manager and one bounded log hub for
//! an image. MAVLink adapters receive mutable borrows for bounded dispatch or
//! draining, but neither can own configuration lifecycle or link state.
//! This crate owns the one configuration manager and the bounded queue for the
//! image's MAVLink diagnostic sink. MAVLink adapters receive mutable borrows
//! for bounded dispatch or draining, but cannot own configuration lifecycle or
//! link state.
#![no_std]
use kaptan_config::{ConfigManager, ProfileStore, StartupError};
use kaptan_config::{ConfigManager, ConfigStore, StartupError};
use kaptan_flight_config::{CandidateProfile, VehicleCapabilities};
use kaptan_log::{EventHub, LogFlags, LogRecord, LogText, PublishOutcome, Severity};
use kaptan_log::{LogRecord, LogSinkQueue};
/// Default bounded allocation for central runtime text diagnostics. Error
/// records are partitioned from optional detail.
pub type RuntimeLogHub = EventHub<8, 16>;
/// Default bounded allocation for the MAVLink diagnostic sink. Error records
/// are partitioned from optional detail.
pub type RuntimeMavlinkLogQueue = LogSinkQueue<8, 16>;
/// One boot-active vehicle runtime. It deliberately has no MAVLink dependency.
pub struct FlightRuntime<S> {
configuration: ConfigManager<CandidateProfile, S>,
logs: RuntimeLogHub,
mavlink_logs: RuntimeMavlinkLogQueue,
}
impl<S> FlightRuntime<S>
where
S: ProfileStore<CandidateProfile>,
S: ConfigStore<CandidateProfile>,
{
/// Recovers the central manager, validates its boot-active snapshot, and
/// copies the restart-only observability policy into the bounded hub.
/// copies the restart-only MAVLink sink policy into its bounded queue.
pub fn recover(
factory: CandidateProfile,
capabilities: VehicleCapabilities,
store: S,
) -> Result<Self, StartupError<S::Error>> {
let configuration =
ConfigManager::recover(CandidateProfile::default(), capabilities, store)?;
let maximum_log_level = configuration.active().observability().maximum_log_level();
let logs = RuntimeLogHub::new(kaptan_log::LogFilter::new(
Severity::try_from_wire(maximum_log_level as u8)
.expect("validated flight profile has a MAVLink log severity"),
));
let configuration = ConfigManager::recover(factory, capabilities, store)?;
let logging = configuration.active().mavlink_logging();
let filter = if logging.enabled() {
kaptan_log::LogFilter::up_to(logging.maximum_severity())
} else {
kaptan_log::LogFilter::disabled()
};
let mavlink_logs = RuntimeMavlinkLogQueue::new(filter);
Ok(Self {
configuration,
logs,
mavlink_logs,
})
}
@@ -48,82 +51,108 @@ where
&self.configuration
}
/// Borrows the manager and hub together for a state-only protocol adapter.
/// This makes adapter ownership inversion unrepresentable in composition.
pub fn configuration_and_logs_mut(
&mut self,
) -> (&mut ConfigManager<CandidateProfile, S>, &mut RuntimeLogHub) {
(&mut self.configuration, &mut self.logs)
/// Mutably borrows the sole configuration manager for protocol dispatch.
pub fn configuration_mut(&mut self) -> &mut ConfigManager<CandidateProfile, S> {
&mut self.configuration
}
/// Publishes one preformatted bounded task record through the sole hub.
pub fn log(&mut self, severity: Severity, flags: LogFlags, message: LogText) -> PublishOutcome {
self.logs.publish_text(severity, flags, message)
/// Composition-owned fan-out boundary for one preformatted event.
/// A future blackbox adds its independent sink admission here.
pub fn publish_log_event(&mut self, record: LogRecord) {
let _ = self.mavlink_logs.publish_record(record);
}
/// Peeks the highest-priority pending text log for the sole telemetry link.
pub fn pending_report(&self) -> Option<LogRecord> {
self.logs.peek()
pub fn pending_mavlink_report(&self) -> Option<LogRecord> {
self.mavlink_logs.peek()
}
/// Submits the next record and removes it only after transport admission.
pub fn submit_next<R>(
pub fn submit_next_mavlink<R>(
&mut self,
submit: impl FnOnce(&LogRecord) -> (kaptan_log::TransportAdmission, R),
) -> Option<R> {
self.logs.submit_next(submit)
self.mavlink_logs.submit_next(submit)
}
/// Immutable boot-active log policy copied from the validated profile.
pub const fn log_filter(&self) -> kaptan_log::LogFilter {
self.logs.filter()
/// Immutable boot-active MAVLink log policy copied from the validated profile.
pub const fn mavlink_log_filter(&self) -> kaptan_log::LogFilter {
self.mavlink_logs.filter()
}
}
#[cfg(test)]
mod tests {
use kaptan_config::{Generation, ProfileStore};
use kaptan_log::{LogFlags, LogText, Severity};
use kaptan_config::{ConfigModel, ConfigStore, ConfigValue};
use kaptan_log::{LogFlags, LogRecord, LogText, Severity};
use super::FlightRuntime;
struct Store;
impl ProfileStore<kaptan_flight_config::CandidateProfile> for Store {
impl ConfigStore<kaptan_flight_config::CandidateProfile> for Store {
type Error = ();
fn load(
&mut self,
) -> Result<Option<(Generation, kaptan_flight_config::CandidateProfile)>, Self::Error>
{
fn load(&mut self) -> Result<Option<kaptan_flight_config::CandidateProfile>, Self::Error> {
Ok(None)
}
fn persist(
&mut self,
_: &kaptan_flight_config::CandidateProfile,
) -> Result<Generation, Self::Error> {
Ok(Generation::new(1))
) -> Result<(), Self::Error> {
Ok(())
}
}
#[test]
fn central_runtime_uses_the_active_profile_filter_and_owns_one_hub() {
let mut runtime =
FlightRuntime::recover(kaptan_flight_config::VehicleCapabilities::quad_x(), Store)
.expect("factory profile validates");
fn runtime_uses_the_active_mavlink_policy_and_owns_one_sink_queue() {
let mut runtime = FlightRuntime::recover(
kaptan_flight_config::CandidateProfile::factory(),
kaptan_flight_config::VehicleCapabilities::quad_x(),
Store,
)
.expect("factory configuration validates");
assert_eq!(
runtime
.configuration()
.active()
.observability()
.maximum_log_level(),
runtime.log_filter().maximum_severity() as u32
.mavlink_logging()
.maximum_severity(),
runtime
.mavlink_log_filter()
.maximum_severity()
.expect("factory enables MAVLink logs")
);
let _ = runtime.log(
runtime.publish_log_event(LogRecord::new(
Severity::Info,
LogFlags::NONE,
LogText::from_message("runtime ready"),
);
assert!(runtime.pending_report().is_some());
));
assert!(runtime.pending_mavlink_report().is_some());
}
#[test]
fn disabled_mavlink_sink_filters_every_severity() {
let mut factory = kaptan_flight_config::CandidateProfile::factory();
let enabled = kaptan_flight_config::CandidateProfile::descriptors()
.find(|descriptor| descriptor.display_key == "mavlink.logging.enabled")
.expect("MAVLink logging enable descriptor");
factory
.set_field(enabled.id, ConfigValue::Bool(false))
.unwrap();
let mut runtime = FlightRuntime::recover(
factory,
kaptan_flight_config::VehicleCapabilities::quad_x(),
Store,
)
.expect("disabled MAVLink sink is valid");
assert_eq!(runtime.mavlink_log_filter().maximum_severity(), None);
runtime.publish_log_event(LogRecord::new(
Severity::Error,
LogFlags::NONE,
LogText::from_message("not routed to MAVLink"),
));
assert!(runtime.pending_mavlink_report().is_none());
}
}