refactor(firmware): simplify configuration and diagnostics

This commit is contained in:
Onur Özkan committed 2026-08-23 00:40:36 +03:00
1 parent ba9c97ed3b
commit fdf82a5114
96 files changed
+1612 -2772

No files matched your search

+1
View File
@@ -11,6 +11,7 @@ workspace = true
[dependencies]
kaptan-config = { path = "../../libs/kaptan-config" }
kaptan-config-mavlink = { path = "../../libs/kaptan-config-mavlink" }
kaptan-log = { path = "../../libs/kaptan-log" }
kaptan-mavlink = { path = "../../libs/kaptan-mavlink", default-features = false, features = ["dialect-kaptan"] }
mavlink-core = { version = "0.18", default-features = false }
libc = "0.2"
+2
View File
@@ -209,6 +209,8 @@ mod tests {
assert!(INDEX.contains("id=\"packet-rows\""));
assert!(APP.contains("api('/api/logs')"));
assert!(APP.contains("newEntries.map(createLogEntry)"));
assert!(APP.contains("entry.truncated"));
assert!(APP.contains("truncated.textContent = 'TRUNCATED'"));
assert!(INDEX.contains("id=\"log-entries\""));
}
}
@@ -1540,15 +1540,15 @@ mod tests {
let descriptor = response(MavMessage::KAPTAN_CONFIG_DESCRIPTOR(
KAPTAN_CONFIG_DESCRIPTOR_DATA {
minimum_bits: 5,
maximum_bits: 6,
default_bits: 6,
minimum_bits: 3,
maximum_bits: 4,
default_bits: 3,
request_id: 2,
descriptor_index: 0,
field_id: 108,
enum_option_count: 2,
display_key: "future.logging.severity".into(),
unit: "MAV_SEVERITY".into(),
display_key: "future.logging.level_filter".into(),
unit: "log_level".into(),
value_kind: KaptanConfigValueKind::KAPTAN_CONFIG_VALUE_KIND_U32,
flags: DESCRIPTOR_HAS_MINIMUM | DESCRIPTOR_HAS_MAXIMUM,
},
@@ -1568,12 +1568,12 @@ mod tests {
let first_option = response(MavMessage::KAPTAN_CONFIG_ENUM_OPTION(
KAPTAN_CONFIG_ENUM_OPTION_DATA {
value: 5,
value: 3,
request_id: first.request_id,
descriptor_index: 0,
field_id: 108,
option_index: 0,
label: "Notice".into(),
label: "Info".into(),
},
));
let (event, second_request) = client.observe(&first_option, &mut sequence);
@@ -1591,12 +1591,12 @@ mod tests {
let second_option = response(MavMessage::KAPTAN_CONFIG_ENUM_OPTION(
KAPTAN_CONFIG_ENUM_OPTION_DATA {
value: 6,
value: 4,
request_id: second.request_id,
descriptor_index: 0,
field_id: 108,
option_index: 1,
label: "Info".into(),
label: "Debug".into(),
},
));
let (event, value_request) = client.observe(&second_option, &mut sequence);
@@ -1613,7 +1613,7 @@ mod tests {
_ => panic!("enum discovery must advance to VALUE_REQUEST"),
};
let value = response(MavMessage::KAPTAN_CONFIG_VALUE(KAPTAN_CONFIG_VALUE_DATA {
value_bits: 6,
value_bits: 3,
request_id: value_request_id,
session_id: 0,
revision: 0,
@@ -1626,9 +1626,9 @@ mod tests {
panic!("complete enum discovery must publish the configuration");
};
assert_eq!(configuration.fields[0].options.len(), 2);
assert_eq!(configuration.fields[0].options[0].label, "Notice");
assert_eq!(configuration.fields[0].options[0].value, Scalar::U32(5));
assert_eq!(configuration.fields[0].options[1].label, "Info");
assert_eq!(configuration.fields[0].options[0].label, "Info");
assert_eq!(configuration.fields[0].options[0].value, Scalar::U32(3));
assert_eq!(configuration.fields[0].options[1].label, "Debug");
}
#[test]
+74 -66
View File
@@ -2,71 +2,42 @@
use std::{collections::VecDeque, time::SystemTime};
use kaptan_log::Level;
use serde::{Serialize, Serializer};
const MAX_LOG_ENTRIES: usize = 512;
/// Severity attached to one decoded diagnostic event.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LogSeverity {
Emergency,
Alert,
Critical,
Error,
Warning,
Notice,
Info,
Debug,
Trace,
Unknown(u8),
fn serialize_level<S>(level: &Level, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(level.as_str())
}
impl LogSeverity {
/// Decodes the stable MAV_SEVERITY numeric representation without
/// accepting values that the protocol has not defined.
pub const fn from_wire(level: u8, trace: bool) -> Self {
match level {
0 => Self::Emergency,
1 => Self::Alert,
2 => Self::Critical,
3 => Self::Error,
4 => Self::Warning,
5 => Self::Notice,
6 => Self::Info,
7 if trace => Self::Trace,
7 => Self::Debug,
value => Self::Unknown(value),
}
/// Decodes the exact Kaptan log-level subset from its MAVLink representation.
///
/// Kaptan emits Error, Warn, Info, and Debug on the conventional MAVLink
/// numbers 3, 4, 6, and 7. Trace is Debug plus the dedicated trace flag. The
/// other MAVLink severity numbers are not Kaptan log levels.
pub const fn level_from_wire(level: u8, trace: bool) -> Option<Level> {
if trace && level != 7 {
return None;
}
const fn known_label(self) -> Option<&'static str> {
match self {
Self::Emergency => Some("EMERGENCY"),
Self::Alert => Some("ALERT"),
Self::Critical => Some("CRITICAL"),
Self::Error => Some("ERROR"),
Self::Warning => Some("WARNING"),
Self::Notice => Some("NOTICE"),
Self::Info => Some("INFO"),
Self::Debug => Some("DEBUG"),
Self::Trace => Some("TRACE"),
Self::Unknown(_) => None,
}
match level {
3 => Some(Level::Error),
4 => Some(Level::Warn),
6 => Some(Level::Info),
7 if trace => Some(Level::Trace),
7 => Some(Level::Debug),
_ => None,
}
}
impl Serialize for LogSeverity {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(label) = self.known_label() {
serializer.serialize_str(label)
} else if let Self::Unknown(value) = self {
serializer.serialize_str(&format!("UNKNOWN ({value})"))
} else {
unreachable!("every known severity has a static label")
}
/// Human-readable packet-view representation, including malformed values.
pub fn wire_level_label(level: u8, trace: bool) -> String {
match level_from_wire(level, trace) {
Some(level) => level.as_str().into(),
None => format!("UNKNOWN ({level})"),
}
}
@@ -75,7 +46,8 @@ impl Serialize for LogSeverity {
pub struct LogObservation {
pub system_id: u8,
pub component_id: u8,
pub severity: LogSeverity,
pub level: Level,
pub truncated: bool,
pub text: String,
}
@@ -94,7 +66,9 @@ pub struct LogView {
pub received_at_ms: u64,
pub system_id: u8,
pub component_id: u8,
pub severity: LogSeverity,
#[serde(serialize_with = "serialize_level")]
pub level: Level,
pub truncated: bool,
pub text: String,
}
@@ -131,7 +105,8 @@ impl LogMonitor {
received_at_ms,
system_id: observation.system_id,
component_id: observation.component_id,
severity: observation.severity,
level: observation.level,
truncated: observation.truncated,
text: observation.text,
});
self.next_id += 1;
@@ -164,7 +139,8 @@ mod tests {
LogObservation {
system_id: 1,
component_id: 2,
severity: LogSeverity::Info,
level: Level::Info,
truncated: false,
text: format!("event {index}"),
}
}
@@ -179,10 +155,15 @@ mod tests {
assert_eq!(snapshot.entries.len(), 2);
assert_eq!(snapshot.entries[0].id, 0);
assert_eq!(snapshot.entries[0].received_at_ms, 1_234);
assert!(!snapshot.entries[0].truncated);
assert_eq!(snapshot.entries[0].text, "event 0");
assert_eq!(snapshot.entries[1].id, 1);
assert_eq!(snapshot.entries[1].text, "event 1");
assert_eq!(snapshot.discarded, 0);
assert_eq!(
serde_json::to_value(&snapshot).unwrap()["entries"][0]["level"],
"INFO"
);
}
#[test]
@@ -213,14 +194,41 @@ mod tests {
}
#[test]
fn wire_severity_is_checked_and_unknown_values_remain_visible() {
assert_eq!(LogSeverity::from_wire(3, false), LogSeverity::Error);
assert_eq!(LogSeverity::from_wire(7, false), LogSeverity::Debug);
assert_eq!(LogSeverity::from_wire(7, true), LogSeverity::Trace);
assert_eq!(LogSeverity::from_wire(9, false), LogSeverity::Unknown(9));
fn wire_level_accepts_only_the_exact_kaptan_contract() {
for (wire, expected) in [
(0, None),
(1, None),
(2, None),
(3, Some(Level::Error)),
(4, Some(Level::Warn)),
(5, None),
(6, Some(Level::Info)),
(7, Some(Level::Debug)),
(8, None),
(u8::MAX, None),
] {
assert_eq!(level_from_wire(wire, false), expected, "wire {wire}");
}
assert_eq!(level_from_wire(7, true), Some(Level::Trace));
for invalid_trace_level in [0, 1, 2, 3, 4, 5, 6, 8, u8::MAX] {
assert_eq!(level_from_wire(invalid_trace_level, true), None);
}
assert_eq!(wire_level_label(2, false), "UNKNOWN (2)");
assert_eq!(wire_level_label(9, false), "UNKNOWN (9)");
}
#[test]
fn truncation_state_survives_the_browser_projection() {
let mut event = observation(0);
event.truncated = true;
let mut monitor = LogMonitor::default();
monitor.observe([event], SystemTime::UNIX_EPOCH);
let snapshot = monitor.snapshot();
assert!(snapshot.entries[0].truncated);
assert_eq!(
serde_json::to_value(LogSeverity::Unknown(9)).unwrap(),
"UNKNOWN (9)"
serde_json::to_value(&snapshot).unwrap()["entries"][0]["truncated"],
true
);
}
+144 -48
View File
@@ -15,10 +15,11 @@ use std::{
use crate::{
config_client::{ClientEvent, ConfigClient},
log_monitor::{LogObservation, LogSeverity},
log_monitor::{LogObservation, level_from_wire, wire_level_label},
packet_monitor::PacketObservation,
state::{SaveRequest, SharedState},
};
use kaptan_log::Level;
use kaptan_mavlink::{
MavlinkFrame,
message::{self, Message},
@@ -321,36 +322,34 @@ fn log_observation(
frame: &MavlinkFrame,
message: &message::kaptan::MavMessage,
) -> Option<LogObservation> {
let (severity, text) = match message {
let (level, truncated, text) = match message {
message::kaptan::MavMessage::KAPTAN_LOG(log) => {
let trace = log
.flags
.contains(message::kaptan::KaptanLogFlags::KAPTAN_LOG_FLAG_TRACE);
(
LogSeverity::from_wire(log.level, trace),
bounded_text(&log.message, log.message_length),
level_from_wire(log.level, trace)?,
log.flags
.contains(message::kaptan::KaptanLogFlags::KAPTAN_LOG_FLAG_TRUNCATED),
bounded_text(&log.message, log.message_length)?,
)
}
message::kaptan::MavMessage::KAPTAN_ERROR(error) => {
let path = bounded_text(&error.path, error.path_length);
let information = bounded_text(
&error.additional_information,
error.additional_information_length,
);
let text = match (path.is_empty(), information.is_empty()) {
(false, false) => format!("{path}: {information}"),
(false, true) => path,
(true, false) => information,
(true, true) => "Unspecified Kaptan error".into(),
let raised_at = zero_padded_text(&error.raised_at)?;
let text = if raised_at.is_empty() {
format!("Kaptan error {}", error.error_id)
} else {
format!("Kaptan error {} at {raised_at}", error.error_id)
};
(LogSeverity::from_wire(error.level, false), text)
(Level::Error, false, text)
}
_ => return None,
};
Some(LogObservation {
system_id: frame.system_id(),
component_id: frame.component_id(),
severity,
level,
truncated,
text,
})
}
@@ -358,23 +357,17 @@ fn log_observation(
fn kaptan_message_values(message: &message::kaptan::MavMessage) -> serde_json::Value {
match message {
message::kaptan::MavMessage::KAPTAN_ERROR(error) => serde_json::json!({
"level": LogSeverity::from_wire(error.level, false),
"flags": format!("{:?}", error.flags),
"flags_bits": error.flags.bits(),
"path": bounded_text(&error.path, error.path_length),
"additional_information": bounded_text(
&error.additional_information,
error.additional_information_length,
),
"error_id": error.error_id,
"raised_at": packet_zero_padded_text(&error.raised_at),
}),
message::kaptan::MavMessage::KAPTAN_LOG(log) => serde_json::json!({
"level": LogSeverity::from_wire(
"level": wire_level_label(
log.level,
log.flags.contains(message::kaptan::KaptanLogFlags::KAPTAN_LOG_FLAG_TRACE),
),
"flags": format!("{:?}", log.flags),
"flags_bits": log.flags.bits(),
"message": bounded_text(&log.message, log.message_length),
"message": packet_text(&log.message, log.message_length),
}),
_ => decoded_fields(message),
}
@@ -384,9 +377,22 @@ fn decoded_fields(message: &impl core::fmt::Debug) -> serde_json::Value {
format!("{message:#?}").into()
}
fn bounded_text<const N: usize>(bytes: &[u8; N], length: u8) -> String {
let end = usize::from(length).min(N);
String::from_utf8_lossy(&bytes[..end]).into_owned()
fn bounded_text<const N: usize>(bytes: &[u8; N], length: u8) -> Option<String> {
let text = core::str::from_utf8(bytes.get(..usize::from(length))?).ok()?;
Some(text.into())
}
fn packet_text<const N: usize>(bytes: &[u8; N], length: u8) -> String {
bounded_text(bytes, length).unwrap_or_else(|| format!("INVALID TEXT (length {length})"))
}
fn zero_padded_text<const N: usize>(bytes: &[u8; N]) -> Option<String> {
let length = bytes.iter().position(|byte| *byte == 0).unwrap_or(N);
Some(core::str::from_utf8(&bytes[..length]).ok()?.into())
}
fn packet_zero_padded_text<const N: usize>(bytes: &[u8; N]) -> String {
zero_padded_text(bytes).unwrap_or_else(|| "INVALID TEXT".into())
}
fn hex_bytes(bytes: &[u8]) -> String {
@@ -533,13 +539,10 @@ mod tests {
}
#[test]
fn packet_monitor_decodes_kaptan_error_text() {
let path_text = b"driver::Icm42688::Transfer";
let information_text = b"SPI timeout";
let mut path = [0_u8; 96];
let mut additional_information = [0_u8; 64];
path[..path_text.len()].copy_from_slice(path_text);
additional_information[..information_text.len()].copy_from_slice(information_text);
fn packet_and_history_decode_a_typed_kaptan_error() {
let raised_at_text = "platforms/stm32f7/src/f722/board.rs:572";
let mut raised_at = [0; 96];
raised_at[..raised_at_text.len()].copy_from_slice(raised_at_text.as_bytes());
let mut frame = MavlinkFrame::new();
frame.serialize_message_data(
MavHeader {
@@ -548,27 +551,28 @@ mod tests {
component_id: 7,
},
&message::kaptan::KAPTAN_ERROR_DATA {
level: 2,
flags: message::kaptan::KaptanErrorFlags::empty(),
path_length: path_text.len() as u8,
additional_information_length: information_text.len() as u8,
path,
additional_information,
error_id: 1234,
raised_at,
},
);
let (observation, log) = frame_observations(&frame);
assert_eq!(observation.name, "KAPTAN_ERROR");
assert_eq!(observation.values["level"], "CRITICAL");
assert_eq!(observation.values["path"], "driver::Icm42688::Transfer");
assert_eq!(observation.values["additional_information"], "SPI timeout");
assert!(observation.values.get("level").is_none());
assert_eq!(observation.values["error_id"], 1234);
assert_eq!(observation.values["raised_at"], raised_at_text);
assert!(observation.values.get("flags").is_none());
assert!(observation.values.get("flags_bits").is_none());
assert!(observation.values.get("path").is_none());
assert!(observation.values.get("additional_information").is_none());
assert_eq!(
log,
Some(LogObservation {
system_id: 42,
component_id: 7,
severity: LogSeverity::Critical,
text: "driver::Icm42688::Transfer: SPI timeout".into(),
level: kaptan_log::Level::Error,
truncated: false,
text: format!("Kaptan error 1234 at {raised_at_text}"),
})
);
}
@@ -601,12 +605,104 @@ mod tests {
Some(LogObservation {
system_id: 42,
component_id: 7,
severity: LogSeverity::Info,
level: kaptan_log::Level::Info,
truncated: false,
text: "estimator initialized".into(),
})
);
}
#[test]
fn error_level_runtime_log_remains_kaptan_log() {
let message_bytes = [b'x'; 160];
let message_text = "x".repeat(message_bytes.len());
let mut frame = MavlinkFrame::new();
frame.serialize_message_data(
MavHeader {
sequence: 1,
system_id: 42,
component_id: 7,
},
&message::kaptan::KAPTAN_LOG_DATA {
level: 3,
flags: message::kaptan::KaptanLogFlags::KAPTAN_LOG_FLAG_TRUNCATED,
message_length: message_bytes.len() as u8,
message: message_bytes,
},
);
let (packet, log) = frame_observations(&frame);
assert_eq!(packet.name, "KAPTAN_LOG");
assert_eq!(
log,
Some(LogObservation {
system_id: 42,
component_id: 7,
level: Level::Error,
truncated: true,
text: message_text,
})
);
}
#[test]
fn malformed_log_text_or_level_is_not_added_to_history() {
let mut invalid_utf8 = [0_u8; 160];
invalid_utf8[0] = 0xff;
for (level, flags, length, message) in [
(
6,
message::kaptan::KaptanLogFlags::KAPTAN_LOG_FLAG_TRACE,
0,
[0_u8; 160],
),
(
6,
message::kaptan::KaptanLogFlags::empty(),
161,
[0_u8; 160],
),
(6, message::kaptan::KaptanLogFlags::empty(), 1, invalid_utf8),
] {
let mut frame = MavlinkFrame::new();
frame.serialize_message_data(
MavHeader {
sequence: 1,
system_id: 42,
component_id: 7,
},
&message::kaptan::KAPTAN_LOG_DATA {
level,
flags,
message_length: length,
message,
},
);
let (_, log) = frame_observations(&frame);
assert_eq!(log, None);
}
}
#[test]
fn malformed_typed_error_location_is_not_added_to_history() {
let mut raised_at = [0_u8; 96];
raised_at[0] = 0xff;
let mut frame = MavlinkFrame::new();
frame.serialize_message_data(
MavHeader {
sequence: 1,
system_id: 42,
component_id: 7,
},
&message::kaptan::KAPTAN_ERROR_DATA {
error_id: 1,
raised_at,
},
);
let (_, log) = frame_observations(&frame);
assert_eq!(log, None);
}
#[test]
fn serial_baud_accepts_only_rates_with_an_exact_termios_mapping() {
for rate in [
+14 -7
View File
@@ -341,23 +341,30 @@ function logTime(receivedAtMs) {
function createLogEntry(entry) {
const item = document.createElement('li');
item.dataset.logId = String(entry.id);
const severityClass = entry.severity.split(/[^A-Z]+/, 1)[0].toLowerCase();
item.className = `log-entry log-${severityClass}`;
const levelClass = entry.level.split(/[^A-Z]+/, 1)[0].toLowerCase();
item.className = `log-entry log-${levelClass}`;
const time = document.createElement('time');
time.dateTime = new Date(entry.received_at_ms).toISOString();
time.textContent = logTime(entry.received_at_ms);
const severity = document.createElement('span');
severity.className = 'log-severity';
severity.textContent = entry.severity;
const level = document.createElement('span');
level.className = 'log-level';
level.textContent = entry.level;
const text = document.createElement('span');
text.className = 'log-text';
text.textContent = entry.text;
if (entry.truncated) {
const truncated = document.createElement('span');
truncated.className = 'log-truncated';
truncated.textContent = 'TRUNCATED';
text.append(truncated, document.createTextNode(entry.text));
} else {
text.textContent = entry.text;
}
item.title = `MAVLink source ${entry.system_id}:${entry.component_id}`;
item.append(time, severity, text);
item.append(time, level, text);
return item;
}
+11 -5
View File
@@ -223,13 +223,19 @@ th:nth-child(n + 3) { text-align: right; }
.log-entry:last-child { border-bottom: 0; }
.log-entry time { color: var(--muted); }
.log-severity { font-weight: 700; }
.log-level { font-weight: 700; }
.log-text { white-space: pre-wrap; overflow-wrap: anywhere; }
.log-emergency,
.log-alert,
.log-critical,
.log-truncated {
display: inline-block;
margin-right: .5rem;
padding: 0 .25rem;
border: 1px solid currentColor;
color: var(--warning);
font-size: .75em;
font-weight: 700;
}
.log-error { color: var(--error); }
.log-warning { color: var(--warning); }
.log-warn { color: var(--warning); }
.empty-log {
padding: .55rem;
+3 -3
View File
@@ -9,20 +9,20 @@ version.workspace = true
workspace = true
[dependencies]
kaptan-err-propagate = { path = "../../libs/kaptan-err-propagate" }
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", features = ["facade"] }
kaptan-log-mavlink = { path = "../../libs/kaptan-log-mavlink" }
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" }
kaptan-time = { path = "../../libs/kaptan-time" }
minicbor = "0.25"
log = { version = "0.4.33", default-features = false }
[dev-dependencies]
mavlink = { version = "0.18", default-features = false, features = ["embedded"] }
+5 -29
View File
@@ -1,32 +1,8 @@
//! Host-only standard-log facade composition.
//! Host-side access to the common bounded log ingress.
use std::sync::Once;
use kaptan_log::LogRecord;
use kaptan_log::{FacadeIngress, GlobalLogger, LogRecord};
const SITL_LOG_INGRESS_CAPACITY: usize = 16;
static SITL_LOG_INGRESS: FacadeIngress<SITL_LOG_INGRESS_CAPACITY> = FacadeIngress::new();
static SITL_LOGGER: GlobalLogger<SITL_LOG_INGRESS_CAPACITY> =
GlobalLogger::new(&SITL_LOG_INGRESS, sitl_forbidden_log_context);
static INSTALL_SITL_LOGGER: Once = Once::new();
const fn sitl_forbidden_log_context() -> bool {
false
}
/// Installs the host-only standard logging facade once for the SITL process.
///
/// SITL has no ISR, panic, or hard-fault context. Flight targets deliberately
/// do not enable this host-only feature.
pub fn install_log_facade() {
INSTALL_SITL_LOGGER.call_once(|| {
SITL_LOGGER
.install()
.expect("SITL owns the process-global log facade")
});
}
/// Removes one transport-neutral event for composition-level sink fanout.
pub(super) fn take() -> Option<LogRecord> {
SITL_LOG_INGRESS.take()
/// Visits one transport-neutral event for composition-level sink fanout.
pub(super) fn drain_one<R>(consume: impl FnOnce(&LogRecord) -> R) -> Option<R> {
kaptan_log::drain_one(consume)
}
-1
View File
@@ -11,7 +11,6 @@ mod store;
#[cfg(test)]
mod tests;
pub use logging::install_log_facade;
pub use runtime::{CAPABILITIES, SitlConfigRuntime, configuration_service};
pub use store::{
FileConfigStore, FileStoreError, default_profile, load_profile, write_default_profile,
+6 -7
View File
@@ -2,10 +2,11 @@
use kaptan_config::ConfigManager;
use kaptan_config_mavlink::{ConfigCommand, ConfigProtocol, Principal, ServiceResponse, Target};
use kaptan_diagnostics_mavlink::encode_log;
use kaptan_flight_config::{CandidateProfile, VehicleCapabilities, VehicleProfile};
use kaptan_flight_runtime::FlightRuntime;
use kaptan_log::TransportAdmission;
use kaptan_log_mavlink::{DiagnosticPayload, encode_diagnostic};
use kaptan_mavlink::message::kaptan::KAPTAN_LOG_DATA;
use super::{FileConfigStore, logging};
@@ -51,12 +52,11 @@ 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_diagnostic_payload(&mut self) -> Option<DiagnosticPayload> {
if let Some(record) = logging::take() {
self.runtime.publish_log_event(record);
}
pub fn pending_log_payload(&mut self) -> Option<KAPTAN_LOG_DATA> {
let runtime = &mut self.runtime;
let _ = logging::drain_one(|record| runtime.publish_log_event(*record));
self.runtime
.submit_next_mavlink(|record| (TransportAdmission::Accepted, encode_diagnostic(record)))
.submit_next_mavlink(|record| (TransportAdmission::Accepted, encode_log(record)))
}
}
@@ -64,7 +64,6 @@ impl SitlConfigRuntime {
/// SITL/GCS integration. Flight targets must supply authenticated authority
/// and a reviewed journal/activation coordinator before constructing one.
pub fn configuration_service(store: FileConfigStore) -> Result<SitlConfigRuntime, String> {
logging::install_log_facade();
let runtime = FlightRuntime::recover(CandidateProfile::factory(), CAPABILITIES, store)
.map_err(|error| format!("configuration recovery failed: {error:?}"))?;
Ok(SitlConfigRuntime {
+5 -1
View File
@@ -7,11 +7,11 @@ use std::{
};
use kaptan_config::ConfigStore;
use kaptan_err_propagate::KaptanError;
use kaptan_flight_config::{
CandidateProfile, CborEncodeError, CborError, MAXIMUM_CANDIDATE_BYTES, VehicleProfile,
decode_candidate, encode_candidate,
};
use kaptan_log::KaptanError;
use super::{CAPABILITIES, configuration_service};
@@ -19,12 +19,16 @@ use super::{CAPABILITIES, configuration_service};
#[derive(Debug, KaptanError)]
pub enum FileStoreError {
/// Host filesystem operation failed.
#[kaptan_error(id = 12101)]
Io(std::io::Error),
/// The bounded file record did not decode and validate.
#[kaptan_error(id = 12102)]
Cbor(CborError),
/// The fixed snapshot buffer could not encode the complete candidate.
#[kaptan_error(id = 12103)]
Encode,
/// Factory-only startup deliberately has no durable configuration path.
#[kaptan_error(id = 12104)]
FactoryOnly,
}
+6 -8
View File
@@ -2,7 +2,6 @@ use std::fs;
use kaptan_config::{ConfigModel, ConfigStore};
use kaptan_flight_config::{CandidateProfile, decode_candidate};
use kaptan_log_mavlink::DiagnosticPayload;
use kaptan_mavlink::{
encoder::MavlinkEncoder,
message::{Message, kaptan::MavMessage},
@@ -49,15 +48,14 @@ fn standard_log_macro_reaches_the_generated_mavlink_log_payload() {
let _ = fs::remove_file(&path);
let mut service = configuration_service(FileConfigStore::new(&path)).unwrap();
log::info!("SITL configuration service ready");
let Some(DiagnosticPayload::Log(payload)) = service.pending_diagnostic_payload() else {
panic!("info facade record must reach KAPTAN_LOG");
};
assert_eq!(payload.level, kaptan_log::Severity::Info.wire_value());
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);
let message = std::str::from_utf8(&payload.message[..usize::from(payload.message_length)])
.expect("facade text remains UTF-8");
assert!(message.contains("config::tests"));
assert!(message.ends_with("SITL configuration service ready"));
assert_eq!(message, "SITL configuration service ready");
let mut encoder = MavlinkEncoder::new(1, 1);
let frame = encoder.custom(&payload);
assert_eq!(frame.message_id(), 51_020);
+4 -1
View File
@@ -4,7 +4,7 @@
//! dialect. It is confined to one host network namespace and is versioned so
//! malformed simulator traffic never becomes estimator or motor authority.
use kaptan_log::KaptanError;
use kaptan_err_propagate::KaptanError;
use std::net::SocketAddr;
pub const SENSOR_PORT: u16 = 14_660;
@@ -42,8 +42,11 @@ pub const STATUS_OUTPUT_PERMITTED: u16 = 1 << 3;
pub const STATUS_ACTUATOR_TRANSPORT: u16 = 1 << 4;
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
pub enum PacketError {
#[kaptan_error(id = 12001)]
Length,
#[kaptan_error(id = 12002)]
Version,
#[kaptan_error(id = 12003)]
NonFinite,
}