//! MAVLink projection for Kaptan logs and reportable firmware errors. //! //! This adapter creates generated payload data only. A platform-owned link is //! solely responsible for MAVLink framing, bounded admission, DMA, and retry //! policy. #![no_std] #[cfg(test)] 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}; /// 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(), message: *record.message().bytes(), } } /// Encodes one reportable firmware error as its sole wire message. pub fn encode_error(record: &ErrorRecord) -> KAPTAN_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) { length -= 1; } let mut raised_at = [0; ERROR_RAISED_AT_BYTES]; raised_at[..length].copy_from_slice(&source[..length]); KAPTAN_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 { match level { Level::Error => 3, Level::Warn => 4, Level::Info => 6, Level::Debug | Level::Trace => 7, } } #[cfg(test)] mod tests { use super::{LOG_MESSAGE_BYTES, encode_error, encode_log}; use kaptan_err_reportable::KaptanError; use kaptan_log::{Level, LogFlags, LogRecord, LogText}; use kaptan_mavlink::{ MavlinkVersion, encoder::MavlinkEncoder, message::{ Message, MessageData, kaptan::{KAPTAN_ERROR_DATA, KAPTAN_LOG_DATA, KaptanLogFlags, MavMessage}, }, }; #[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)] enum TestError { #[kaptan_error(id = 65301)] Transfer, } #[kaptan_err_reportable::propagate] fn report_reportable_error() -> Result<(), TestError> { Err(TestError::Transfer) } #[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); } #[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)); } #[test] fn error_macro_remains_an_error_level_log() { 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.message[..usize::from(payload.message_length)], b"flight fault" ); } #[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 mut encoder = MavlinkEncoder::new(1, 1); let frame = encoder.custom(&data); assert!(frame.has_valid_crc::()); 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 )); while kaptan_err_reportable::take().is_some() {} assert_eq!(report_reportable_error(), Err(TestError::Transfer)); 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() .position(|byte| *byte == 0) .unwrap_or(error_data.raised_at.len()); assert_eq!( &error_data.raised_at[..raised_at_length], error.raised_at().as_bytes() ); assert!( error .raised_at() .contains("kaptan-diagnostics-mavlink/src/lib.rs:") ); let error_frame = encoder.custom(&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 )); } #[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 { 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]); } }