feat(log): add bounded diagnostic transport

This commit is contained in:
Onur Özkan committed 2026-08-09 22:04:06 +03:00
1 parent 632bf9f01b
commit 0a75860841
46 files changed
+2758 -60

No files matched your search

+4
View File
@@ -17,11 +17,15 @@ sensor-telemetry = ["dep:kaptan-sensor"]
dialect-kaptan = []
[dependencies]
kaptan-log = { path = "../kaptan-log" }
kaptan-dma = { path = "../kaptan-dma" }
kaptan-sensor = { path = "../kaptan-sensor", optional = true }
kaptan-tx = { path = "../kaptan-tx" }
mavlink = { version = "0.18", default-features = false, features = ["embedded", "dialect-common"] }
mavlink-core = { version = "0.18", default-features = false, features = ["embedded"] }
bitflags = "2"
num-derive = "0.4"
num-traits = { version = "0.2", default-features = false }
[dev-dependencies]
kaptan-time = { path = "../kaptan-time" }
+16 -10
View File
@@ -3,17 +3,23 @@ use std::path::PathBuf;
fn main() {
let manifest_directory = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let dialect = manifest_directory.join("definitions/kaptan.xml");
let output_directory = std::env::var("OUT_DIR").unwrap();
let dialect_source = std::fs::read_to_string(&dialect).unwrap();
let generated = mavlink_bindgen::generate(
mavlink_bindgen::XmlDefinitions::Files(vec![dialect]),
output_directory,
)
.unwrap();
mavlink_bindgen::emit_cargo_build_messages(&generated);
let log = manifest_directory.join("definitions/kaptan/log.xml");
for definition in [&dialect, &log] {
println!("cargo::rerun-if-changed={}", definition.display());
}
println!("cargo::rustc-check-cfg=cfg(kaptan_has_messages)");
if dialect_source.contains("<message ") {
// The upstream generator conditionally emits derives for these optional
// ecosystem crates. Kaptan deliberately does not expose those incomplete
// features, so tell rustc the generated inactive cfgs are intentional.
println!("cargo::rustc-check-cfg=cfg(feature, values(\"serde\", \"arbitrary\", \"ts-rs\"))");
if std::env::var_os("CARGO_FEATURE_DIALECT_KAPTAN").is_some() {
let output_directory = std::env::var("OUT_DIR").unwrap();
let generated = mavlink_bindgen::generate(
mavlink_bindgen::XmlDefinitions::Files(vec![dialect]),
output_directory,
)
.unwrap();
mavlink_bindgen::emit_cargo_build_messages(&generated);
println!("cargo::rustc-cfg=kaptan_has_messages");
}
}
+3 -6
View File
@@ -1,9 +1,6 @@
<?xml version="1.0"?>
<mavlink>
<version>3</version>
<dialect>0</dialect>
<!-- Special enums and messages for Kaptan. -->
<enums/>
<messages/>
<!-- The root is the only bindgen input. Feature domains remain independent
fragments so their message catalogs can evolve without a monolith. -->
<include>kaptan/log.xml</include>
</mavlink>
@@ -0,0 +1,54 @@
<?xml version="1.0"?>
<mavlink>
<version>3</version>
<dialect>0</dialect>
<!-- Kaptan-reserved development block. Register these IDs before release. -->
<enums>
<enum name="KAPTAN_LOG_FLAGS" bitmask="true">
<entry value="1" name="KAPTAN_LOG_FLAG_TRACE">
<description>Record originated at Rust trace level and uses MAVLink DEBUG severity.</description>
</entry>
<entry value="2" name="KAPTAN_LOG_FLAG_TRUNCATED">
<description>Message text was truncated to the bounded on-wire buffer.</description>
</entry>
<entry value="4" name="KAPTAN_LOG_FLAG_COALESCED">
<description>Message represents repeated adjacent observations.</description>
</entry>
</enum>
<enum name="KAPTAN_ERROR_FLAGS" bitmask="true">
<entry value="2" name="KAPTAN_ERROR_FLAG_TRUNCATED">
<description>Error display text was truncated to the bounded on-wire buffer.</description>
</entry>
<entry value="4" name="KAPTAN_ERROR_FLAG_COALESCED">
<description>Error represents repeated adjacent observations.</description>
</entry>
</enum>
<enum name="KAPTAN_SHUTDOWN_REASON">
<entry value="1" name="KAPTAN_SHUTDOWN_REASON_OPERATOR">
<description>An operator initiated the completed controlled firmware shutdown.</description>
</entry>
<entry value="2" name="KAPTAN_SHUTDOWN_REASON_SAFETY">
<description>The safety owner initiated the completed controlled firmware shutdown.</description>
</entry>
</enum>
</enums>
<messages>
<message id="51020" name="KAPTAN_LOG">
<description>Bounded human-readable runtime log. The receiver needs only level, flags, and the valid UTF-8 text prefix. MAVLink framing, bounded admission, DMA, retries, and local loss accounting remain platform-owned.</description>
<field type="uint8_t" name="level">MAV_SEVERITY-compatible level. Rust TRACE uses DEBUG plus the TRACE flag.</field>
<field type="uint8_t" name="flags" enum="KAPTAN_LOG_FLAGS">Known bits are TRACE=1, TRUNCATED=2, and COALESCED=4. Receivers ignore unknown future bits.</field>
<field type="uint8_t" name="message_length">Number of valid UTF-8 bytes in message; at most 160.</field>
<field type="uint8_t[160]" name="message">Bounded UTF-8 message bytes. Bytes after message_length are zero.</field>
</message>
<message id="51021" name="KAPTAN_ERROR">
<description>Bounded derived or explicit runtime error. This is distinct from ordinary KAPTAN_LOG records. The display identity is generated from the Rust error type and variant when a KaptanError-derived value is available; structured configuration failures remain authoritative in KAPTAN_CONFIG_RESULT.</description>
<field type="uint8_t" name="flags" enum="KAPTAN_ERROR_FLAGS">Known bits are TRUNCATED=2 and COALESCED=4. Receivers ignore unknown future bits.</field>
<field type="uint8_t" name="message_length">Number of valid UTF-8 bytes in message; at most 160.</field>
<field type="uint8_t[160]" name="message">Bounded UTF-8 error display identity. Bytes after message_length are zero.</field>
</message>
<message id="51022" name="KAPTAN_SHUTDOWN">
<description>Typed completed orderly firmware shutdown observation. It is emitted only after a platform coordinator has inhibited outputs. It is not a log record, a shutdown request, or a panic/hard-fault/reset report.</description>
<field type="uint8_t" name="reason" enum="KAPTAN_SHUTDOWN_REASON">Typed shutdown reason.</field>
</message>
</messages>
</mavlink>
+7
View File
@@ -56,6 +56,13 @@ impl MavlinkEncoder {
raw
}
/// Encodes one generated MAVLink data record through this endpoint's sole
/// header-sequence owner. Runtime-log/config adapters return data records
/// and must use this method rather than create a competing encoder.
pub fn custom<M: MessageData>(&mut self, message: &M) -> MavlinkFrame {
self.serialize(message)
}
/// Encodes a board-I/O heartbeat with its current arming state.
pub fn heartbeat(&mut self, system_status: message::MavState, armed: bool) -> MavlinkFrame {
let heartbeat = message::HEARTBEAT_DATA {
+2
View File
@@ -18,6 +18,8 @@ pub type MavlinkFrame = message::MAVLinkV2MessageRaw;
pub mod message {
pub use mavlink::MAVLinkV2MessageRaw;
pub use mavlink::MavHeader;
pub use mavlink::Message;
pub use mavlink::MessageData;
pub use mavlink::dialects::common::*;
#[cfg(kaptan_has_messages)]
+17 -1
View File
@@ -75,7 +75,11 @@ impl<const N: usize> MavlinkReceiver<N> {
let frame = core::mem::replace(&mut self.frame, MavlinkFrame::new());
self.received = 0;
self.expected = 0;
if !frame.has_valid_crc::<message::MavMessage>() {
// The production receiver still drops every inbound frame until
// signing/replay verification exists. Validate either known
// dialect here so a future verifier receives no CRC-corrupt
// custom configuration message.
if !has_valid_known_crc(&frame) {
return false;
}
}
@@ -110,6 +114,18 @@ impl<const N: usize> MavlinkReceiver<N> {
}
}
fn has_valid_known_crc(frame: &MavlinkFrame) -> bool {
let common_valid = frame.has_valid_crc::<message::MavMessage>();
#[cfg(feature = "dialect-kaptan")]
{
common_valid || frame.has_valid_crc::<message::kaptan::MavMessage>()
}
#[cfg(not(feature = "dialect-kaptan"))]
{
common_valid
}
}
#[cfg(test)]
mod tests {
use super::MavlinkReceiver;
+52 -2
View File
@@ -1,6 +1,7 @@
//! MAVLink frame encoding and UART transmission API.
use kaptan_dma::{DmaTxBufferFactory, DmaTxEngine, TxCompletion};
use kaptan_log::KaptanError;
use kaptan_tx::{PendingFrame, TransmitQueue};
use crate::MavlinkFrame;
@@ -9,7 +10,7 @@ use crate::MavlinkFrame;
pub use kaptan_tx::{EnqueueResult, FrameClass};
/// Failure while preparing or starting a MAVLink frame transfer.
#[derive(Debug)]
#[derive(Debug, KaptanError)]
pub enum MavlinkTransmitFault<MemoryError, EngineError> {
/// DMA-visible frame preparation failed.
Memory(MemoryError),
@@ -17,6 +18,40 @@ pub enum MavlinkTransmitFault<MemoryError, EngineError> {
Engine(EngineError),
}
/// Transport-neutral class of a terminal link fault.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TransmitFaultKind {
/// DMA-visible frame preparation failed.
Memory,
/// The UART DMA engine rejected a transfer start.
Engine,
/// An active DMA transfer ended with a terminal fault or halt.
Terminal,
}
impl<MemoryError, EngineError> MavlinkTransmitFault<MemoryError, EngineError> {
/// Returns the bounded category a platform may convert to a static log
/// message without exposing a generic HAL error value on the wire.
pub const fn kind(&self) -> TransmitFaultKind {
match self {
Self::Memory(_) => TransmitFaultKind::Memory,
Self::Engine(_) => TransmitFaultKind::Engine,
}
}
}
/// Combined bounded-admission and DMA-start outcome.
///
/// The two outcomes stay distinct so a platform can account for a failed DMA
/// start without falsely treating a queue-full source frame as retained.
#[must_use]
pub struct AdmissionStart<MemoryError, EngineError> {
/// Queue admission for the source frame.
pub admission: EnqueueResult,
/// One bounded attempt to start pending DMA work.
pub start: Result<(), MavlinkTransmitFault<MemoryError, EngineError>>,
}
/// Owns MAVLink frame admission and UART DMA lifecycle.
///
/// Exactly one worker invokes this type. It accepts local encoded frames and
@@ -54,8 +89,23 @@ where
class: FrameClass,
frame: MavlinkFrame,
) -> Result<EnqueueResult, MavlinkTransmitFault<Memory::Error, Engine::StartError>> {
let outcome = self.admit_and_start(class, frame);
outcome.start.map(|()| outcome.admission)
}
/// Admits one frame and attempts one bounded DMA start while preserving
/// both outcomes. Platform policy uses this to account for a failed DMA
/// start without mistaking an admission rejection for a retained frame.
/// A start failure retains the selected frame in the queue reservation for
/// a later service-turn retry.
pub fn admit_and_start(
&mut self,
class: FrameClass,
frame: MavlinkFrame,
) -> AdmissionStart<Memory::Error, Engine::StartError> {
let admission = self.enqueue_frame(class, frame);
self.start_pending().map(|()| admission)
let start = self.start_pending();
AdmissionStart { admission, start }
}
/// Admits a frame without attempting to start the DMA engine.