feat(log): add bounded diagnostic transport
This commit is contained in:
1 parent
632bf9f01b
commit
0a75860841
46 files changed
+2758
-60
No files matched your search
@@ -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" }
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user