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

@@ -10,30 +10,27 @@
extern crate std;
use kaptan_err_reportable::ErrorRecord;
use kaptan_log::{Level, LogRecord, MAXIMUM_LOG_TEXT_BYTES};
use kaptan_mavlink::message::kaptan::{KAPTAN_ERROR_DATA, KAPTAN_LOG_DATA, KaptanLogFlags};
use kaptan_log::{Level, LogRecord};
use kaptan_mavlink::generated::kaptan::{ERROR_DATA, LOG_DATA, LogFlags, LogLevel};
/// Exact capacity of the fixed log text field in [`KAPTAN_LOG_DATA`].
pub const LOG_MESSAGE_BYTES: usize = MAXIMUM_LOG_TEXT_BYTES;
const ERROR_RAISED_AT_BYTES: usize = 96;
/// Encodes one ordinary log record as its sole wire message.
pub fn encode_log(record: &LogRecord) -> KAPTAN_LOG_DATA {
let mut flags = KaptanLogFlags::from_bits_retain(record.flags().bits());
if record.level() == Level::Trace {
flags.insert(KaptanLogFlags::KAPTAN_LOG_FLAG_TRACE);
}
KAPTAN_LOG_DATA {
level: mavlink_severity(record.level()),
flags,
message_length: record.message().len(),
pub fn encode_log(record: &LogRecord) -> LOG_DATA {
LOG_DATA {
level: wire_level(record.level()),
flags: if record.message().is_truncated() {
LogFlags::LOG_FLAG_TRUNCATED
} else {
LogFlags::empty()
},
message_length: record.message().byte_length(),
message: *record.message().bytes(),
}
}
/// Encodes one reportable firmware error as its sole wire message.
pub fn encode_error(record: &ErrorRecord) -> KAPTAN_ERROR_DATA {
pub fn encode_error(record: &ErrorRecord) -> ERROR_DATA {
let source = record.raised_at().as_bytes();
let mut length = source.len().min(ERROR_RAISED_AT_BYTES);
while !record.raised_at().is_char_boundary(length) {
@@ -42,37 +39,44 @@ pub fn encode_error(record: &ErrorRecord) -> KAPTAN_ERROR_DATA {
let mut raised_at = [0; ERROR_RAISED_AT_BYTES];
raised_at[..length].copy_from_slice(&source[..length]);
KAPTAN_ERROR_DATA {
ERROR_DATA {
error_id: record.id(),
raised_at,
}
}
/// Projects the five conventional levels into MAVLink's wider severity
/// number space. This transport adapter is the only place that needs that
/// protocol-specific representation.
const fn mavlink_severity(level: Level) -> u8 {
/// Projects the five conventional levels into Kaptan's stable wire encoding.
const fn wire_level(level: Level) -> LogLevel {
match level {
Level::Error => 3,
Level::Warn => 4,
Level::Info => 6,
Level::Debug | Level::Trace => 7,
Level::Error => LogLevel::LOG_LEVEL_ERROR,
Level::Warn => LogLevel::LOG_LEVEL_WARN,
Level::Info => LogLevel::LOG_LEVEL_INFO,
Level::Debug => LogLevel::LOG_LEVEL_DEBUG,
Level::Trace => LogLevel::LOG_LEVEL_TRACE,
}
}
#[cfg(test)]
mod tests {
use super::{LOG_MESSAGE_BYTES, encode_error, encode_log};
use super::{encode_error, encode_log};
use kaptan_err_reportable::KaptanError;
use kaptan_log::{Level, LogFlags, LogRecord, LogText};
use kaptan_mavlink::{
MavlinkVersion,
MavlinkFrame,
encoder::MavlinkEncoder,
message::{
Message, MessageData,
kaptan::{KAPTAN_ERROR_DATA, KAPTAN_LOG_DATA, KaptanLogFlags, MavMessage},
},
generated::kaptan::{ERROR_DATA, LogFlags, LogLevel, MavMessage},
receiver::IntegrityMavlinkReceiver,
};
use std::sync::Mutex;
static LOG_TEST: Mutex<()> = Mutex::new(());
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()
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
enum TestError {
@@ -86,40 +90,35 @@ mod tests {
}
#[test]
fn ordinary_record_preserves_level_flags_and_bounded_text() {
let record = LogRecord::new(
Level::Info,
LogFlags::TRUNCATED,
LogText::from_message("imu unavailable"),
);
let data = encode_log(&record);
assert_eq!(data.level, 6);
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);
fn truncated_text_sets_its_wire_flag() {
let _guard = LOG_TEST.lock().unwrap();
while kaptan_log::drain_one(|_| ()).is_some() {}
kaptan_log::info!("{}", "x".repeat(161));
let data = kaptan_log::drain_one(encode_log).unwrap();
assert_eq!(data.level, LogLevel::LOG_LEVEL_INFO);
assert_eq!(usize::from(data.message_length), data.message.len());
assert_eq!(data.message, [b'x'; 160]);
assert_eq!(data.flags, LogFlags::LOG_FLAG_TRUNCATED);
}
#[test]
fn trace_level_owns_its_mavlink_projection() {
let record = LogRecord::new(
Level::Trace,
LogFlags::NONE,
LogText::from_message("scheduler tick"),
);
let data = encode_log(&record);
assert_eq!(data.level, 7);
assert!(data.flags.contains(KaptanLogFlags::KAPTAN_LOG_FLAG_TRACE));
fn trace_is_a_first_class_wire_level() {
let _guard = LOG_TEST.lock().unwrap();
while kaptan_log::drain_one(|_| ()).is_some() {}
kaptan_log::trace!("scheduler tick");
let data = kaptan_log::drain_one(encode_log).unwrap();
assert_eq!(data.level, LogLevel::LOG_LEVEL_TRACE);
assert!(data.flags.is_empty());
}
#[test]
fn error_macro_remains_an_error_level_log() {
let _guard = LOG_TEST.lock().unwrap();
while kaptan_log::drain_one(|_| ()).is_some() {}
kaptan_log::error!("flight fault");
let payload = kaptan_log::drain_one(encode_log).expect("error-level log retained");
assert_eq!(payload.level, 3);
assert_eq!(payload.level, LogLevel::LOG_LEVEL_ERROR);
assert_eq!(
&payload.message[..usize::from(payload.message_length)],
b"flight fault"
@@ -128,24 +127,17 @@ mod tests {
#[test]
fn generated_log_and_error_messages_round_trip() {
let record = LogRecord::new(
Level::Info,
LogFlags::NONE,
LogText::from_message(&"x".repeat(LOG_MESSAGE_BYTES)),
);
let data = encode_log(&record);
let _guard = LOG_TEST.lock().unwrap();
while kaptan_log::drain_one(|_| ()).is_some() {}
kaptan_log::info!("{}", "x".repeat(160));
let data = kaptan_log::drain_one(encode_log).unwrap();
let mut encoder = MavlinkEncoder::new(1, 1);
let frame = encoder.custom(&data);
assert!(frame.has_valid_crc::<MavMessage>());
let frame = encoder.encode(&data);
assert_eq!(frame.message_id(), 51_020);
assert_eq!(frame.raw_bytes().len(), 175);
assert!(matches!(
MavMessage::parse(
mavlink::MavlinkVersion::V2,
frame.message_id(),
frame.payload(),
),
Ok(MavMessage::KAPTAN_LOG(decoded)) if decoded == data
parse_test_message(&frame),
Some(MavMessage::LOG(decoded)) if decoded == data
));
while kaptan_err_reportable::take().is_some() {}
@@ -153,7 +145,6 @@ mod tests {
let error = kaptan_err_reportable::take().expect("reportable error retained");
let error_data = encode_error(&error);
assert_eq!(error_data.error_id, error.id());
assert_eq!(KAPTAN_ERROR_DATA::ENCODED_LEN, 98);
let raised_at_length = error_data
.raised_at
.iter()
@@ -168,35 +159,26 @@ mod tests {
.raised_at()
.contains("kaptan-diagnostics-mavlink/src/lib.rs:")
);
let error_frame = encoder.custom(&error_data);
let error_frame = encoder.encode(&error_data);
assert_eq!(error_frame.message_id(), 51_021);
assert!(error_frame.raw_bytes().len() <= 110);
assert!(matches!(
MavMessage::parse(
mavlink::MavlinkVersion::V2,
error_frame.message_id(),
error_frame.payload(),
),
Ok(MavMessage::KAPTAN_ERROR(decoded)) if decoded == error_data
parse_test_message(&error_frame),
Some(MavMessage::ERROR(decoded)) if decoded == error_data
));
}
#[test]
fn kaptan_error_wire_contract_is_stable() {
assert_eq!(KAPTAN_ERROR_DATA::ID, 51_021);
assert_eq!(KAPTAN_ERROR_DATA::EXTRA_CRC, 46);
assert_eq!(KAPTAN_ERROR_DATA::ENCODED_LEN, 98);
let data = KAPTAN_ERROR_DATA {
let data = ERROR_DATA {
error_id: 1234,
raised_at: [b'A'; 96],
};
assert_eq!(data.raised_at.len(), 96);
let mut payload = [0_u8; KAPTAN_ERROR_DATA::ENCODED_LEN];
let length = data.ser(MavlinkVersion::V2, &mut payload);
assert_eq!(length, KAPTAN_ERROR_DATA::ENCODED_LEN);
assert_eq!(&payload[..2], &[210, 4]);
assert_eq!(&payload[2..], &[b'A'; 96]);
let mut encoder = MavlinkEncoder::new(1, 1);
let frame = encoder.encode(&data);
assert_eq!(&frame.payload()[..2], &[210, 4]);
assert_eq!(&frame.payload()[2..], &[b'A'; 96]);
}
}