//! 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. #![no_std] use kaptan_config::{ConfigManager, ProfileStore, StartupError}; use kaptan_flight_config::{CandidateProfile, VehicleCapabilities}; use kaptan_log::{EventHub, LogFlags, LogRecord, LogText, PublishOutcome, Severity}; /// Default bounded allocation for central runtime text diagnostics. Error /// records are partitioned from optional detail. pub type RuntimeLogHub = EventHub<8, 16>; /// One boot-active vehicle runtime. It deliberately has no MAVLink dependency. pub struct FlightRuntime { configuration: ConfigManager, logs: RuntimeLogHub, } impl FlightRuntime where S: ProfileStore, { /// Recovers the central manager, validates its boot-active snapshot, and /// copies the restart-only observability policy into the bounded hub. pub fn recover( capabilities: VehicleCapabilities, store: S, ) -> Result> { 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"), )); Ok(Self { configuration, logs, }) } /// The sole central configuration manager. pub const fn configuration(&self) -> &ConfigManager { &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, &mut RuntimeLogHub) { (&mut self.configuration, &mut self.logs) } /// 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) } /// Peeks the highest-priority pending text log for the sole telemetry link. pub fn pending_report(&self) -> Option { self.logs.peek() } /// Submits the next record and removes it only after transport admission. pub fn submit_next( &mut self, submit: impl FnOnce(&LogRecord) -> (kaptan_log::TransportAdmission, R), ) -> Option { self.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() } } #[cfg(test)] mod tests { use kaptan_config::{Generation, ProfileStore}; use kaptan_log::{LogFlags, LogText, Severity}; use super::FlightRuntime; struct Store; impl ProfileStore for Store { type Error = (); fn load( &mut self, ) -> Result, Self::Error> { Ok(None) } fn persist( &mut self, _: &kaptan_flight_config::CandidateProfile, ) -> Result { Ok(Generation::new(1)) } } #[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"); assert_eq!( runtime .configuration() .active() .observability() .maximum_log_level(), runtime.log_filter().maximum_severity() as u32 ); let _ = runtime.log( Severity::Info, LogFlags::NONE, LogText::from_message("runtime ready"), ); assert!(runtime.pending_report().is_some()); } }