refactor: move firmware workspace under firmware
This commit is contained in:
1 parent
153b0bf262
commit
f03fc119a4
222 files changed
+56
-13
No files matched your search
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "kaptan-log-mavlink"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
kaptan-log = { path = "../kaptan-log" }
|
||||
kaptan-mavlink = { path = "../kaptan-mavlink", default-features = false, features = ["dialect-kaptan"] }
|
||||
|
||||
[dev-dependencies]
|
||||
kaptan-log = { path = "../kaptan-log", features = ["facade"] }
|
||||
mavlink = { version = "0.18", default-features = false, features = ["embedded"] }
|
||||
@@ -0,0 +1,301 @@
|
||||
//! MAVLink projection for bounded Kaptan diagnostics.
|
||||
//!
|
||||
//! This adapter creates generated payload data only. A platform-owned link is
|
||||
//! solely responsible for MAVLink header sequence, bounded admission, DMA, and
|
||||
//! retry policy.
|
||||
|
||||
#![no_std]
|
||||
|
||||
#[cfg(test)]
|
||||
extern crate std;
|
||||
|
||||
use kaptan_log::{LogRecord, LogText, RecordChannel, ShutdownReason, ShutdownReport};
|
||||
use kaptan_mavlink::message::kaptan::{
|
||||
KAPTAN_ERROR_DATA, KAPTAN_LOG_DATA, KAPTAN_SHUTDOWN_DATA, KaptanErrorFlags, KaptanLogFlags,
|
||||
KaptanShutdownReason,
|
||||
};
|
||||
|
||||
/// Exact capacity of the fixed text field in [`KAPTAN_LOG_DATA`].
|
||||
pub const LOG_MESSAGE_BYTES: usize = 160;
|
||||
/// Exact capacity of the fixed error identity path field.
|
||||
pub const ERROR_PATH_BYTES: usize = 96;
|
||||
/// Exact capacity of the fixed error additional-information field.
|
||||
pub const ERROR_ADDITIONAL_INFORMATION_BYTES: usize = 64;
|
||||
|
||||
/// One generated diagnostic payload. A platform-owned link remains the sole
|
||||
/// MAVLink framing, admission, DMA, and retry owner.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum DiagnosticPayload {
|
||||
/// Ordinary severity-and-text record.
|
||||
Log(KAPTAN_LOG_DATA),
|
||||
/// Error-only bounded display identity.
|
||||
Error(KAPTAN_ERROR_DATA),
|
||||
}
|
||||
|
||||
/// Encodes one bounded diagnostic record.
|
||||
///
|
||||
/// The adapter produces payload data only; it neither frames nor transmits
|
||||
/// MAVLink. `RecordChannel` is the only routing decision: ordinary records
|
||||
/// become `KAPTAN_LOG`, while error-or-higher records become `KAPTAN_ERROR`.
|
||||
pub fn encode_diagnostic(record: &LogRecord) -> DiagnosticPayload {
|
||||
match record.channel() {
|
||||
RecordChannel::Log => {
|
||||
let message = record
|
||||
.message()
|
||||
.expect("log channel must contain ordinary message text");
|
||||
debug_assert!(usize::from(message.len()) <= LOG_MESSAGE_BYTES);
|
||||
DiagnosticPayload::Log(KAPTAN_LOG_DATA {
|
||||
level: record.severity() as u8,
|
||||
flags: KaptanLogFlags::from_bits_retain(record.flags().bits()),
|
||||
message_length: message.len(),
|
||||
message: *message.bytes(),
|
||||
})
|
||||
}
|
||||
RecordChannel::Error => {
|
||||
let path = record
|
||||
.error_path()
|
||||
.expect("error channel must contain an identity path");
|
||||
let additional_information = record
|
||||
.additional_information()
|
||||
.expect("error channel must contain additional-information storage");
|
||||
let (path, path_length, path_clipped) = project_text::<ERROR_PATH_BYTES>(path);
|
||||
let (additional_information, additional_information_length, information_clipped) =
|
||||
project_text::<ERROR_ADDITIONAL_INFORMATION_BYTES>(additional_information);
|
||||
let mut flags = record.flags().error_bits();
|
||||
if path_clipped || information_clipped {
|
||||
flags |= KaptanErrorFlags::KAPTAN_ERROR_FLAG_TRUNCATED.bits();
|
||||
}
|
||||
DiagnosticPayload::Error(KAPTAN_ERROR_DATA {
|
||||
flags: KaptanErrorFlags::from_bits_retain(flags),
|
||||
path_length,
|
||||
additional_information_length,
|
||||
path,
|
||||
additional_information,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn project_text<const N: usize>(text: &LogText) -> ([u8; N], u8, bool) {
|
||||
let source = text.as_str();
|
||||
let retained = source
|
||||
.char_indices()
|
||||
.take_while(|(index, character)| index + character.len_utf8() <= N)
|
||||
.map(|(index, character)| index + character.len_utf8())
|
||||
.last()
|
||||
.unwrap_or(0);
|
||||
let mut bytes = [0; N];
|
||||
bytes[..retained].copy_from_slice(&source.as_bytes()[..retained]);
|
||||
(
|
||||
bytes,
|
||||
retained as u8,
|
||||
text.is_truncated() || retained != source.len(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Encodes one typed shutdown event. The caller owns when a shutdown is safe
|
||||
/// to report; this pure adapter cannot run a shutdown state machine.
|
||||
pub const fn encode_shutdown(report: ShutdownReport) -> KAPTAN_SHUTDOWN_DATA {
|
||||
KAPTAN_SHUTDOWN_DATA {
|
||||
reason: match report.reason() {
|
||||
ShutdownReason::Operator => KaptanShutdownReason::KAPTAN_SHUTDOWN_REASON_OPERATOR,
|
||||
ShutdownReason::Safety => KaptanShutdownReason::KAPTAN_SHUTDOWN_REASON_SAFETY,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Once;
|
||||
|
||||
use super::{
|
||||
DiagnosticPayload, ERROR_ADDITIONAL_INFORMATION_BYTES, ERROR_PATH_BYTES, LOG_MESSAGE_BYTES,
|
||||
encode_diagnostic, encode_shutdown,
|
||||
};
|
||||
use kaptan_log::{
|
||||
ErrorAdditionalInformation, ErrorPath, EventHub, FacadeIngress, GlobalLogger, LogFilter,
|
||||
LogFlags, LogRecord, LogText, Severity, ShutdownReason, ShutdownReport,
|
||||
};
|
||||
use kaptan_mavlink::{
|
||||
encoder::MavlinkEncoder,
|
||||
message::{
|
||||
Message,
|
||||
kaptan::{KAPTAN_ERROR_DATA, KAPTAN_LOG_DATA, KAPTAN_SHUTDOWN_DATA, MavMessage},
|
||||
},
|
||||
};
|
||||
|
||||
static ERROR_INGRESS: FacadeIngress<1> = FacadeIngress::new(LogFilter::new(Severity::Info));
|
||||
static ERROR_LOGGER: GlobalLogger<1> = GlobalLogger::new(&ERROR_INGRESS, never_forbidden);
|
||||
static INSTALL_ERROR_LOGGER: Once = Once::new();
|
||||
|
||||
const fn never_forbidden() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_record_preserves_level_flags_and_bounded_text() {
|
||||
let record = LogRecord::new(
|
||||
Severity::Info,
|
||||
LogFlags::TRUNCATED,
|
||||
LogText::from_message("imu unavailable"),
|
||||
);
|
||||
let DiagnosticPayload::Log(data) = encode_diagnostic(&record) else {
|
||||
panic!("info record must use KAPTAN_LOG");
|
||||
};
|
||||
assert_eq!(data.level, Severity::Info as u8);
|
||||
assert_eq!(data.message_length, 15);
|
||||
assert_eq!(&data.message[..15], b"imu unavailable");
|
||||
assert!(data.message[15..].iter().all(|byte| *byte == 0));
|
||||
assert_eq!(data.flags.bits(), LogFlags::TRUNCATED.bits());
|
||||
assert_eq!(KAPTAN_LOG_DATA::ENCODED_LEN, 163);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_error_macro_reaches_error_channel_after_optional_ingress_is_full() {
|
||||
INSTALL_ERROR_LOGGER.call_once(|| {
|
||||
ERROR_LOGGER
|
||||
.install()
|
||||
.expect("the test owns the process-global log facade");
|
||||
});
|
||||
|
||||
kaptan_log::log::info!("optional ingress is occupied");
|
||||
kaptan_log::log::error!("critical error survives");
|
||||
|
||||
let mut hub = EventHub::<1, 1>::new(LogFilter::default());
|
||||
assert!(ERROR_INGRESS.drain_one_into(&mut hub).is_some());
|
||||
let record = hub.peek().expect("critical macro record retained");
|
||||
let DiagnosticPayload::Error(payload) = encode_diagnostic(&record) else {
|
||||
panic!("log::error! must not produce KAPTAN_LOG");
|
||||
};
|
||||
assert!(
|
||||
payload.path[..usize::from(payload.path_length)]
|
||||
.ends_with(b"kaptan_log_mavlink::tests")
|
||||
);
|
||||
assert_eq!(
|
||||
&payload.additional_information[..usize::from(payload.additional_information_length)],
|
||||
b"critical error survives"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_log_and_error_messages_round_trip_through_the_sole_encoder() {
|
||||
let record = LogRecord::new(
|
||||
Severity::Info,
|
||||
LogFlags::NONE,
|
||||
LogText::from_message(&"x".repeat(LOG_MESSAGE_BYTES)),
|
||||
);
|
||||
let DiagnosticPayload::Log(data) = encode_diagnostic(&record) else {
|
||||
panic!("info record must use KAPTAN_LOG");
|
||||
};
|
||||
let mut encoder = MavlinkEncoder::new(1, 1);
|
||||
let frame = encoder.custom(&data);
|
||||
assert!(frame.has_valid_crc::<MavMessage>());
|
||||
assert_eq!(frame.message_id(), 51_020);
|
||||
assert_eq!(frame.raw_bytes().len(), 175);
|
||||
match MavMessage::parse(
|
||||
mavlink::MavlinkVersion::V2,
|
||||
frame.message_id(),
|
||||
frame.payload(),
|
||||
) {
|
||||
Ok(MavMessage::KAPTAN_LOG(decoded)) => assert_eq!(decoded, data),
|
||||
other => panic!("unexpected decoded KAPTAN_LOG payload: {other:?}"),
|
||||
}
|
||||
assert_eq!(data.message.len(), LOG_MESSAGE_BYTES);
|
||||
|
||||
let error = LogRecord::error(
|
||||
LogFlags::TRACE.with(LogFlags::COALESCED),
|
||||
ErrorPath::from_message("driver::Error::Transfer"),
|
||||
ErrorAdditionalInformation::new(),
|
||||
);
|
||||
let DiagnosticPayload::Error(error_data) = encode_diagnostic(&error) else {
|
||||
panic!("error record must use KAPTAN_ERROR");
|
||||
};
|
||||
assert_eq!(error_data.flags.bits(), LogFlags::COALESCED.bits());
|
||||
assert_eq!(error_data.path_length, 23);
|
||||
assert_eq!(error_data.additional_information_length, 0);
|
||||
assert_eq!(&error_data.path[..23], b"driver::Error::Transfer");
|
||||
assert!(
|
||||
error_data
|
||||
.additional_information
|
||||
.iter()
|
||||
.all(|byte| *byte == 0)
|
||||
);
|
||||
assert_eq!(KAPTAN_ERROR_DATA::ENCODED_LEN, 163);
|
||||
let error_frame = encoder.custom(&error_data);
|
||||
assert_eq!(error_frame.message_id(), 51_021);
|
||||
assert!(error_frame.raw_bytes().len() <= 175);
|
||||
assert!(matches!(
|
||||
MavMessage::parse(
|
||||
mavlink::MavlinkVersion::V2,
|
||||
error_frame.message_id(),
|
||||
error_frame.payload(),
|
||||
),
|
||||
Ok(MavMessage::KAPTAN_ERROR(decoded)) if decoded == error_data
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_wire_projection_clips_each_component_independently() {
|
||||
let error = LogRecord::error(
|
||||
LogFlags::NONE,
|
||||
ErrorPath::from_message(&"p".repeat(LOG_MESSAGE_BYTES)),
|
||||
ErrorAdditionalInformation::from_message(&"i".repeat(LOG_MESSAGE_BYTES)),
|
||||
);
|
||||
let DiagnosticPayload::Error(payload) = encode_diagnostic(&error) else {
|
||||
panic!("error record must use KAPTAN_ERROR");
|
||||
};
|
||||
assert_eq!(usize::from(payload.path_length), ERROR_PATH_BYTES);
|
||||
assert_eq!(
|
||||
usize::from(payload.additional_information_length),
|
||||
ERROR_ADDITIONAL_INFORMATION_BYTES
|
||||
);
|
||||
assert_eq!(payload.flags.bits(), LogFlags::TRUNCATED.bits());
|
||||
assert_eq!(&payload.path, &[b'p'; ERROR_PATH_BYTES]);
|
||||
assert_eq!(
|
||||
&payload.additional_information,
|
||||
&[b'i'; ERROR_ADDITIONAL_INFORMATION_BYTES]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_path_remains_decodable_when_only_additional_information_clips() {
|
||||
let error = LogRecord::error(
|
||||
LogFlags::NONE,
|
||||
ErrorPath::from_message("kaptan::driver::Transfer"),
|
||||
ErrorAdditionalInformation::from_message(&"é".repeat(80)),
|
||||
);
|
||||
let DiagnosticPayload::Error(payload) = encode_diagnostic(&error) else {
|
||||
panic!("error record must use KAPTAN_ERROR");
|
||||
};
|
||||
assert_eq!(
|
||||
core::str::from_utf8(&payload.path[..usize::from(payload.path_length)]),
|
||||
Ok("kaptan::driver::Transfer")
|
||||
);
|
||||
assert_eq!(usize::from(payload.additional_information_length), 64);
|
||||
assert!(
|
||||
core::str::from_utf8(
|
||||
&payload.additional_information
|
||||
[..usize::from(payload.additional_information_length)]
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(payload.flags.bits() & LogFlags::TRUNCATED.bits() != 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_shutdown_maps_to_its_sole_generated_message() {
|
||||
let data = encode_shutdown(ShutdownReport::new(ShutdownReason::Safety));
|
||||
assert_eq!(KAPTAN_SHUTDOWN_DATA::ENCODED_LEN, 1);
|
||||
let mut encoder = MavlinkEncoder::new(1, 1);
|
||||
let frame = encoder.custom(&data);
|
||||
assert_eq!(frame.message_id(), 51_022);
|
||||
assert!(matches!(
|
||||
MavMessage::parse(
|
||||
mavlink::MavlinkVersion::V2,
|
||||
frame.message_id(),
|
||||
frame.payload(),
|
||||
),
|
||||
Ok(MavMessage::KAPTAN_SHUTDOWN(decoded)) if decoded == data
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user