feat(mavlink): adopt the kaptan-native dialect

This commit is contained in:
Onur Özkan committed 2026-08-24 00:56:06 +03:00
1 parent 1c90870634
commit 5f91465e66
87 files changed
+7733 -6850

No files matched your search

+2 -4
View File
@@ -12,12 +12,9 @@ workspace = true
kaptan-err-reportable = { path = "../../libs/kaptan-err-reportable" }
kaptan-control = { path = "../../libs/kaptan-control" }
kaptan-config = { path = "../../libs/kaptan-config" }
kaptan-config-mavlink = { path = "../../libs/kaptan-config-mavlink" }
kaptan-flight-config = { path = "../../libs/kaptan-flight-config" }
kaptan-flight-runtime = { path = "../../libs/kaptan-flight-runtime" }
kaptan-log = { path = "../../libs/kaptan-log" }
kaptan-diagnostics-mavlink = { path = "../../libs/kaptan-diagnostics-mavlink" }
kaptan-mavlink = { path = "../../libs/kaptan-mavlink" }
kaptan-math = { path = "../../libs/kaptan-math" }
kaptan-navigation = { path = "../../libs/kaptan-navigation", features = ["sitl"] }
kaptan-safety = { path = "../../libs/kaptan-safety" }
@@ -25,4 +22,5 @@ kaptan-time = { path = "../../libs/kaptan-time" }
minicbor = "0.25"
[dev-dependencies]
mavlink = { version = "0.18", default-features = false, features = ["embedded"] }
kaptan-diagnostics-mavlink = { path = "../../libs/kaptan-diagnostics-mavlink" }
kaptan-mavlink = { path = "../../libs/kaptan-mavlink", default-features = false }
+2 -1
View File
@@ -4,6 +4,7 @@
//! need a reviewed A/B journal and authority path; SITL uses a same-directory
//! file replacement to make startup and configuration tests reproducible.
#[cfg(test)]
mod logging;
mod runtime;
mod store;
@@ -11,7 +12,7 @@ mod store;
#[cfg(test)]
mod tests;
pub use runtime::{CAPABILITIES, SitlConfigRuntime, configuration_service};
pub(super) use runtime::{SitlConfigRuntime, configuration_service};
pub use store::{
FileConfigStore, FileStoreError, default_profile, load_profile, write_default_profile,
};
+14 -40
View File
@@ -1,49 +1,27 @@
//! Central SITL configuration and protocol composition.
//! Central SITL configuration composition.
use kaptan_config::ConfigManager;
use kaptan_config_mavlink::{ConfigCommand, ConfigProtocol, Principal, ServiceResponse, Target};
#[cfg(test)]
use kaptan_diagnostics_mavlink::encode_log;
use kaptan_flight_config::{CandidateProfile, VehicleCapabilities, VehicleProfile};
use kaptan_flight_runtime::FlightRuntime;
#[cfg(test)]
use kaptan_log::TransportAdmission;
use kaptan_mavlink::message::kaptan::KAPTAN_LOG_DATA;
#[cfg(test)]
use kaptan_mavlink::generated::kaptan::LOG_DATA;
use super::{FileConfigStore, logging};
use super::FileConfigStore;
#[cfg(test)]
use super::logging;
/// SITL's immutable compiled capability contract.
pub const CAPABILITIES: VehicleCapabilities = VehicleCapabilities::quad_x();
pub(super) const CAPABILITIES: VehicleCapabilities = VehicleCapabilities::quad_x();
/// SITL composition root retaining the one profile manager and one protocol
/// lease state machine. The MAVLink adapter never owns either resource.
/// SITL composition root retaining the one profile manager.
pub struct SitlConfigRuntime {
runtime: FlightRuntime<FileConfigStore>,
protocol: ConfigProtocol,
}
impl SitlConfigRuntime {
/// Dispatches one command for a caller-owned lease principal.
pub fn dispatch(&mut self, input: (Principal, ConfigCommand)) -> Vec<ServiceResponse> {
let (principal, command) = input;
let mut responses = Vec::new();
self.protocol
.dispatch_from_composition(
self.runtime.configuration_mut(),
principal,
command,
|response| {
responses.push(response);
Ok::<(), core::convert::Infallible>(())
},
)
.unwrap();
responses
}
/// Returns the central manager for SITL state inspection only.
pub const fn manager(&self) -> &ConfigManager<CandidateProfile, FileConfigStore> {
self.runtime.configuration()
}
/// Returns the immutable profile selected by the central manager at boot.
pub const fn active_profile(&self) -> VehicleProfile {
*self.runtime.configuration().active()
@@ -52,7 +30,8 @@ impl SitlConfigRuntime {
/// Drains one standard `log` facade record and exposes the generated
/// diagnostic payload for the host's sole MAVLink link owner. This
/// host-only harness accepts the bounded handoff synchronously.
pub fn pending_log_payload(&mut self) -> Option<KAPTAN_LOG_DATA> {
#[cfg(test)]
pub(super) fn pending_log_payload(&mut self) -> Option<LOG_DATA> {
let runtime = &mut self.runtime;
let _ = logging::drain_one(|record| runtime.publish_log_event(*record));
self.runtime
@@ -60,14 +39,9 @@ impl SitlConfigRuntime {
}
}
/// Builds the host-only central configuration composition used by forthcoming
/// SITL/GCS integration. Flight targets must supply authenticated authority
/// and a reviewed journal/activation coordinator before constructing one.
/// Builds the host-only central configuration composition.
pub fn configuration_service(store: FileConfigStore) -> Result<SitlConfigRuntime, String> {
let runtime = FlightRuntime::recover(CandidateProfile::factory(), CAPABILITIES, store)
.map_err(|error| format!("configuration recovery failed: {error:?}"))?;
Ok(SitlConfigRuntime {
runtime,
protocol: ConfigProtocol::new(Target::new(1, 1)),
})
Ok(SitlConfigRuntime { runtime })
}
+2 -2
View File
@@ -13,7 +13,7 @@ use kaptan_flight_config::{
decode_candidate, encode_candidate,
};
use super::{CAPABILITIES, configuration_service};
use super::{configuration_service, runtime::CAPABILITIES};
/// Host file-store error. It intentionally makes no flash power-loss claim.
#[derive(Debug, KaptanError)]
@@ -55,7 +55,7 @@ impl FileConfigStore {
impl ConfigStore<CandidateProfile> for FileConfigStore {
type Error = FileStoreError;
fn load(&mut self) -> Result<Option<CandidateProfile>, Self::Error> {
fn load(&mut self, _: &CandidateProfile) -> Result<Option<CandidateProfile>, Self::Error> {
let Some(path) = self.path.as_deref() else {
return Ok(None);
};
+24 -17
View File
@@ -3,11 +3,21 @@ use std::fs;
use kaptan_config::{ConfigModel, ConfigStore};
use kaptan_flight_config::{CandidateProfile, decode_candidate};
use kaptan_mavlink::{
MavlinkFrame,
encoder::MavlinkEncoder,
message::{Message, kaptan::MavMessage},
generated::kaptan::{LogLevel, MavMessage},
receiver::IntegrityMavlinkReceiver,
};
use super::{CAPABILITIES, FileConfigStore, configuration_service};
use super::{FileConfigStore, configuration_service, runtime::CAPABILITIES};
fn parse_test_message(frame: &MavlinkFrame) -> Option<MavMessage> {
let mut receiver = IntegrityMavlinkReceiver::<1>::new();
for byte in frame.raw_bytes() {
receiver.receive_byte(*byte);
}
receiver.take_received()?.decode()
}
#[test]
fn host_store_round_trips_a_complete_validated_snapshot() {
@@ -20,20 +30,21 @@ fn host_store_round_trips_a_complete_validated_snapshot() {
let _ = fs::remove_file(&path);
let _ = fs::remove_file(&temporary);
let factory = CandidateProfile::factory();
let mut store = FileConfigStore::new(&path);
assert!(store.load().unwrap().is_none());
store.persist(&CandidateProfile::factory()).unwrap();
assert!(store.load(&factory).unwrap().is_none());
store.persist(&factory).unwrap();
let snapshot = fs::read(&path).unwrap();
let decoded = decode_candidate(&snapshot, &CAPABILITIES).unwrap();
assert!(decoded.validate(&CAPABILITIES).is_ok());
let candidate = store.load().unwrap().expect("persisted candidate");
let candidate = store.load(&factory).unwrap().expect("persisted candidate");
assert!(candidate.validate(&CAPABILITIES).is_ok());
store.persist(&CandidateProfile::factory()).unwrap();
store.persist(&factory).unwrap();
let mut reopened = FileConfigStore::new(&path);
assert!(reopened.load().unwrap().is_some());
assert!(reopened.load(&factory).unwrap().is_some());
let mut fresh = FileConfigStore::new(&path);
fresh.persist(&CandidateProfile::factory()).unwrap();
fresh.persist(&factory).unwrap();
fs::remove_file(&path).unwrap();
}
@@ -51,21 +62,17 @@ fn standard_log_macro_reaches_the_generated_mavlink_log_payload() {
kaptan_log::info!("SITL configuration service ready");
let payload = service
.pending_log_payload()
.expect("info facade record must reach KAPTAN_LOG");
assert_eq!(payload.level, 6);
.expect("info facade record must reach LOG");
assert_eq!(payload.level, LogLevel::LOG_LEVEL_INFO);
let message = std::str::from_utf8(&payload.message[..usize::from(payload.message_length)])
.expect("facade text remains UTF-8");
assert_eq!(message, "SITL configuration service ready");
let mut encoder = MavlinkEncoder::new(1, 1);
let frame = encoder.custom(&payload);
let frame = encoder.encode(&payload);
assert_eq!(frame.message_id(), 51_020);
assert!(matches!(
MavMessage::parse(
mavlink::MavlinkVersion::V2,
frame.message_id(),
frame.payload(),
),
Ok(MavMessage::KAPTAN_LOG(data))
parse_test_message(&frame),
Some(MavMessage::LOG(data))
if data.level == payload.level
&& data.message_length == payload.message_length
&& data.message == payload.message