refactor(firmware): simplify configuration and diagnostics
This commit is contained in:
1 parent
ba9c97ed3b
commit
fdf82a5114
96 files changed
+1612
-2772
No files matched your search
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"] }
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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,
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user