feat(diagnostics): split error and shutdown messages

This commit is contained in:
Onur Özkan committed 2026-08-09 22:20:59 +03:00
1 parent 9646451645
commit afafd126d7
25 files changed
+608 -225

No files matched your search

+7 -5
View File
@@ -63,15 +63,17 @@ persistence, authenticated ingress, durable audit logging, disarmed
activation, and atomic consumer reconstruction remain separate reviewed work activation, and atomic consumer reconstruction remain separate reviewed work
and cannot be implied by a valid CBOR file or CRC. and cannot be implied by a valid CBOR file or CRC.
The MAVLink dialect root includes `kaptan/config.xml` and `kaptan/log.xml`. The MAVLink dialect root includes `kaptan/config.xml`, `kaptan/log.xml`,
`kaptan/error.xml`, and `kaptan/shutdown.xml`.
The build watches every fragment and generates one `message::kaptan` module; The build watches every fragment and generates one `message::kaptan` module;
fragments are never separately generated Rust modules. `kaptan-log` itself has fragments are never separately generated Rust modules. `kaptan-log` itself has
no MAVLink, platform, time, allocation, filesystem, or configuration no MAVLink, platform, time, allocation, filesystem, or configuration
dependency. It assigns bounded Critical and BestEffort queues for text logs. dependency. It assigns bounded Critical and BestEffort queues for text logs.
The platform drains a record only after TX admission; the diagnostic wire The platform drains a record only after TX admission. `KAPTAN_LOG` carries
contract deliberately contains only severity, flags, bounded text length, and severity plus one bounded text field; `KAPTAN_ERROR` carries flags plus bounded
text. Local saturation accounting remains inside the hub, facade, and producer path and additional-information fields. Local saturation accounting remains
mailboxes; heartbeat and safety telemetry remain separate from diagnostic text. inside the hub, facade, and producer mailboxes; heartbeat and safety telemetry
remain separate from diagnostics.
See [Configuration](configuration.md) for the deployment matrix and GCS upload See [Configuration](configuration.md) for the deployment matrix and GCS upload
contract, [Logs](logs.md) for the receiver contract, and [Errors](errors.md) contract, [Logs](logs.md) for the receiver contract, and [Errors](errors.md)
+16 -11
View File
@@ -11,10 +11,10 @@ enum SensorError {
``` ```
The derive implements `core::error::Error`, `Display`, and `ErrorReport`. It The derive implements `core::error::Error`, `Display`, and `ErrorReport`. It
creates a bounded human-facing `module::Type::Variant` message without creates a static `module::Type::Variant` error path without allocating,
allocating, publishing telemetry, exposing generic source values, or changing publishing telemetry, exposing generic source values, or changing safety
safety state. Renaming a module, type, or variant changes that text; it is not state. Renaming a module, type, or variant changes that path; it is not a
a stable machine protocol identity. stable machine protocol identity.
Normal-task code may use `log::error!("{error}")`; it is routed as a bounded Normal-task code may use `log::error!("{error}")`; it is routed as a bounded
`KAPTAN_ERROR`. ISR-adjacent flight paths may enqueue the derived static `KAPTAN_ERROR`. ISR-adjacent flight paths may enqueue the derived static
@@ -26,17 +26,22 @@ for safety state, actuator inhibit, or typed configuration results.
- Configuration writes receive authoritative structured failures through - Configuration writes receive authoritative structured failures through
`KAPTAN_CONFIG_RESULT`: operation, lifecycle state, field IDs, validation `KAPTAN_CONFIG_RESULT`: operation, lifecycle state, field IDs, validation
rule, and protocol error code. rule, and protocol error code.
- Runtime driver and link faults reach the F722 MAVLink worker as - Runtime driver and link faults reach the F722 MAVLink worker as a
`KAPTAN_ERROR` text, subject to bounded transport admission. The message `KAPTAN_ERROR` path, subject to bounded transport admission. The error
carries no per-error numeric catalog: a `KaptanError` derive supplies its payload is `{ flags, path_length, additional_information_length, path[96],
type/variant display identity. additional_information[64] }`; each length identifies the valid UTF-8
prefix and the optional information field may be empty. A `KaptanError`
derive supplies the type/variant path and normally leaves additional
information empty. `log::error!` uses the Rust log target as the path and
the formatted message as additional information.
- A rejected configuration operation may enqueue a companion `KAPTAN_ERROR` - A rejected configuration operation may enqueue a companion `KAPTAN_ERROR`
containing the high-level `ProtocolError` identity. It is intentionally not whose path is the high-level `ProtocolError` identity. It is intentionally
correlated and carries no field/rule detail; the matching not correlated and carries no field/rule detail; the matching
`KAPTAN_CONFIG_RESULT` remains the sole precise authority. `KAPTAN_CONFIG_RESULT` remains the sole precise authority.
- Control, navigation, safety, and simulator error types are derive-ready but - Control, navigation, safety, and simulator error types are derive-ready but
their live owners are not yet attached to a MAVLink worker. their live owners are not yet attached to a MAVLink worker.
- `KAPTAN_SHUTDOWN` represents only a future completed orderly, - `KAPTAN_SHUTDOWN`, defined in `definitions/kaptan/shutdown.xml`, represents
only a future completed orderly,
output-inhibited shutdown. It is not a shutdown request, panic, hard-fault, output-inhibited shutdown. It is not a shutdown request, panic, hard-fault,
reset, or retention-record protocol. F722 still uses `panic-halt`; it does reset, or retention-record protocol. F722 still uses `panic-halt`; it does
not claim a live panic frame or retained-record replay. not claim a live panic frame or retained-record replay.
+22 -15
View File
@@ -18,28 +18,35 @@ messages later.
## Receiver contract ## Receiver contract
`definitions/kaptan/log.xml` separates ordinary logs from errors: `definitions/kaptan/log.xml`, `definitions/kaptan/error.xml`, and
`definitions/kaptan/shutdown.xml` separate ordinary logs, errors, and
orderly-shutdown observations:
```text ```text
KAPTAN_LOG { level, flags, message_length, message[160] } KAPTAN_LOG { level, flags, message_length, message[160] }
KAPTAN_ERROR { flags, message_length, message[160] } KAPTAN_ERROR {
flags, path_length, additional_information_length,
path[96], additional_information[64]
}
KAPTAN_SHUTDOWN { reason } KAPTAN_SHUTDOWN { reason }
``` ```
A receiver generated from the same `kaptan.xml` revision validates A GCS decoder generated from the same `kaptan.xml` revision must validate
`message_length <= 160` and decodes exactly that UTF-8 prefix. `KAPTAN_LOG` `message_length <= 160` for `KAPTAN_LOG`, and `path_length <= 96` plus
displays its text at the MAVLink severity in `level`; `TRACE` is encoded as `additional_information_length <= 64` for `KAPTAN_ERROR`, before slicing the
`DEBUG` plus the `TRACE` flag. `KAPTAN_ERROR` is error-only and accepts only fixed arrays into UTF-8 prefixes. Generated MAVLink framing alone does not
`TRUNCATED` and `COALESCED`; a `TRACE` bit is never emitted there. Unknown enforce these semantic length fields. `KAPTAN_LOG` displays its text at the
future flag bits must be ignored. MAVLink severity in `level`; `TRACE` is encoded as `DEBUG` plus the `TRACE`
flag.
`KAPTAN_ERROR` is error-only and accepts only `TRUNCATED` and `COALESCED`; a
`TRACE` bit is never emitted there. Unknown future flag bits must be ignored.
The derived error display text is intentionally not a numeric error catalog; The derived error path is intentionally not a numeric error catalog; renaming
renaming its Rust module/type/variant changes the display identity. Error its Rust module/type/variant changes that identity. Error details requiring
details requiring machine action stay in their owning protocol, notably machine action stay in their owning protocol, notably `KAPTAN_CONFIG_RESULT`.
`KAPTAN_CONFIG_RESULT`. `KAPTAN_SHUTDOWN` has only its typed reason and is `KAPTAN_SHUTDOWN` has only its typed reason and is valid only after an orderly
valid only after an orderly output-inhibited completion. Delivery is bounded output-inhibited completion. Delivery is bounded and lossy under link pressure;
and lossy under link pressure; heartbeat and safety telemetry use separate heartbeat and safety telemetry use separate paths.
paths.
## Filtering ## Filtering
+3 -2
View File
@@ -19,8 +19,9 @@ replace pending best-effort traffic, but never the DMA-owned active buffer.
Completion retries preserve ordering and retain a transiently busy frame for a Completion retries preserve ordering and retain a transiently busy frame for a
later service turn. later service turn.
The custom Kaptan configuration and log definitions are aggregated by The custom Kaptan definitions are aggregated by `definitions/kaptan.xml` from
`definitions/kaptan.xml` from `kaptan/config.xml` and `kaptan/log.xml`. `kaptan/config.xml`, `kaptan/log.xml`, `kaptan/error.xml`, and
`kaptan/shutdown.xml`.
`KAPTAN_LOG` is an outbound human-readable four-field record. `KAPTAN_ERROR` `KAPTAN_LOG` is an outbound human-readable four-field record. `KAPTAN_ERROR`
is its separate bounded error channel, while `KAPTAN_SHUTDOWN` represents only is its separate bounded error channel, while `KAPTAN_SHUTDOWN` represents only
a completed orderly output-inhibited shutdown. Configuration messages are GCS a completed orderly output-inhibited shutdown. Configuration messages are GCS
+1 -1
View File
@@ -8,7 +8,7 @@ This page collects stable limits and points to the detailed contracts.
| SITL file | Private `[generation, raw-profile-blob]` envelope; not a MAVLink upload blob. | | SITL file | Private `[generation, raw-profile-blob]` envelope; not a MAVLink upload blob. |
| Config transaction | One owner-bound RAM transaction; typed writes or 192-byte idempotent CBOR chunks; commit is restart-required. | | Config transaction | One owner-bound RAM transaction; typed writes or 192-byte idempotent CBOR chunks; commit is restart-required. |
| Config discovery | Correlated `KAPTAN_CONFIG_INFO_REQUEST` / `KAPTAN_CONFIG_INFO`, then descriptor/value requests. | | Config discovery | Correlated `KAPTAN_CONFIG_INFO_REQUEST` / `KAPTAN_CONFIG_INFO`, then descriptor/value requests. |
| Diagnostics | `KAPTAN_LOG` (51020) for ordinary severity/text, `KAPTAN_ERROR` (51021) for bounded errors, and `KAPTAN_SHUTDOWN` (51022) for completed orderly output-inhibited shutdowns. | | Diagnostics | `KAPTAN_LOG` (51020) for ordinary severity/text; `KAPTAN_ERROR` (51021) for a bounded `{ path[96], additional_information[64] }` error; and `KAPTAN_SHUTDOWN` (51022) for completed orderly output-inhibited shutdowns. |
| F722 configuration | Immutable compiled board policy only; inbound MAVLink configuration remains disabled. | | F722 configuration | Immutable compiled board policy only; inbound MAVLink configuration remains disabled. |
Read [Configuration](configuration.md) for the field/consumer matrix and GCS Read [Configuration](configuration.md) for the field/consumer matrix and GCS
+2 -1
View File
@@ -180,9 +180,10 @@ mod tests {
let record = hub.peek().expect("rejection is retained"); let record = hub.peek().expect("rejection is retained");
assert_eq!(record.channel(), RecordChannel::Error); assert_eq!(record.channel(), RecordChannel::Error);
assert_eq!( assert_eq!(
record.message().as_str(), record.error_path().unwrap().as_str(),
"kaptan_config_mavlink::protocol::ProtocolError::BadState" "kaptan_config_mavlink::protocol::ProtocolError::BadState"
); );
assert!(record.additional_information().unwrap().is_empty());
let accepted = ConfigReply { let accepted = ConfigReply {
result: ResultKind::Accepted, result: ResultKind::Accepted,
+5 -5
View File
@@ -47,15 +47,15 @@ fn expand(input: DeriveInput) -> Result<proc_macro2::TokenStream, Error> {
}; };
quote! { quote! {
#pattern => { #pattern => {
static MESSAGE: ::kaptan_log::StaticErrorMessage = static ERROR: ::kaptan_log::StaticError =
::kaptan_log::StaticErrorMessage::new(concat!( ::kaptan_log::StaticError::new(concat!(
module_path!(), module_path!(),
"::", "::",
stringify!(#ident), stringify!(#ident),
"::", "::",
stringify!(#variant_ident), stringify!(#variant_ident),
)); ));
&MESSAGE &ERROR
} }
} }
}); });
@@ -70,7 +70,7 @@ fn expand(input: DeriveInput) -> Result<proc_macro2::TokenStream, Error> {
Ok(quote! { Ok(quote! {
impl #impl_generics ::kaptan_log::ErrorReport for #ident #type_generics #where_clause { impl #impl_generics ::kaptan_log::ErrorReport for #ident #type_generics #where_clause {
fn error_message(&self) -> &'static ::kaptan_log::StaticErrorMessage { fn static_error(&self) -> &'static ::kaptan_log::StaticError {
match self { match self {
#(#arms),* #(#arms),*
} }
@@ -83,7 +83,7 @@ fn expand(input: DeriveInput) -> Result<proc_macro2::TokenStream, Error> {
formatter: &mut ::core::fmt::Formatter<'_>, formatter: &mut ::core::fmt::Formatter<'_>,
) -> ::core::fmt::Result { ) -> ::core::fmt::Result {
formatter.write_str( formatter.write_str(
<Self as ::kaptan_log::ErrorReport>::error_message(self).as_str(), <Self as ::kaptan_log::ErrorReport>::static_error(self).path(),
) )
} }
} }
+23 -8
View File
@@ -2,20 +2,30 @@ extern crate self as kaptan_log;
use kaptan_log_derive::KaptanError; use kaptan_log_derive::KaptanError;
pub struct StaticErrorMessage(&'static str); pub struct StaticError {
path: &'static str,
additional_information: &'static str,
}
impl StaticErrorMessage { impl StaticError {
pub const fn new(text: &'static str) -> Self { pub const fn new(text: &'static str) -> Self {
Self(text) Self {
path: text,
additional_information: "",
}
} }
pub const fn as_str(&self) -> &'static str { pub const fn path(&self) -> &'static str {
self.0 self.path
}
pub const fn additional_information(&self) -> &'static str {
self.additional_information
} }
} }
pub trait ErrorReport { pub trait ErrorReport {
fn error_message(&self) -> &'static StaticErrorMessage; fn static_error(&self) -> &'static StaticError;
} }
#[derive(Debug, KaptanError)] #[derive(Debug, KaptanError)]
@@ -32,11 +42,11 @@ fn main() {
fn assert_error<T: core::error::Error>() {} fn assert_error<T: core::error::Error>() {}
assert_error::<Generic<'_, u8>>(); assert_error::<Generic<'_, u8>>();
assert_eq!( assert_eq!(
Generic::<u8>::Unit.error_message().as_str(), Generic::<u8>::Unit.static_error().path(),
concat!(module_path!(), "::Generic::Unit"), concat!(module_path!(), "::Generic::Unit"),
); );
assert_eq!( assert_eq!(
Generic::Tuple(1_u8, 2).error_message().as_str(), Generic::Tuple(1_u8, 2).static_error().path(),
concat!(module_path!(), "::Generic::Tuple"), concat!(module_path!(), "::Generic::Tuple"),
); );
let named: Generic<'_, u8> = Generic::Named { label: "test" }; let named: Generic<'_, u8> = Generic::Named { label: "test" };
@@ -44,4 +54,9 @@ fn main() {
named.to_string(), named.to_string(),
concat!(module_path!(), "::Generic::Named"), concat!(module_path!(), "::Generic::Named"),
); );
assert_eq!(
named.static_error().additional_information(),
"",
"derives emit identity only; callers own optional context",
);
} }
+130 -24
View File
@@ -9,7 +9,7 @@
#[cfg(test)] #[cfg(test)]
extern crate std; extern crate std;
use kaptan_log::{LogRecord, RecordChannel, ShutdownReason, ShutdownReport}; use kaptan_log::{LogRecord, LogText, RecordChannel, ShutdownReason, ShutdownReport};
use kaptan_mavlink::message::kaptan::{ use kaptan_mavlink::message::kaptan::{
KAPTAN_ERROR_DATA, KAPTAN_LOG_DATA, KAPTAN_SHUTDOWN_DATA, KaptanErrorFlags, KaptanLogFlags, KAPTAN_ERROR_DATA, KAPTAN_LOG_DATA, KAPTAN_SHUTDOWN_DATA, KaptanErrorFlags, KaptanLogFlags,
KaptanShutdownReason, KaptanShutdownReason,
@@ -17,6 +17,10 @@ use kaptan_mavlink::message::kaptan::{
/// Exact capacity of the fixed text field in [`KAPTAN_LOG_DATA`]. /// Exact capacity of the fixed text field in [`KAPTAN_LOG_DATA`].
pub const LOG_MESSAGE_BYTES: usize = 160; 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 /// One generated diagnostic payload. A platform-owned link remains the sole
/// MAVLink framing, admission, DMA, and retry owner. /// MAVLink framing, admission, DMA, and retry owner.
@@ -34,23 +38,61 @@ pub enum DiagnosticPayload {
/// MAVLink. `RecordChannel` is the only routing decision: ordinary records /// MAVLink. `RecordChannel` is the only routing decision: ordinary records
/// become `KAPTAN_LOG`, while error-or-higher records become `KAPTAN_ERROR`. /// become `KAPTAN_LOG`, while error-or-higher records become `KAPTAN_ERROR`.
pub fn encode_diagnostic(record: &LogRecord) -> DiagnosticPayload { pub fn encode_diagnostic(record: &LogRecord) -> DiagnosticPayload {
let message = record.message();
debug_assert!(usize::from(message.len()) <= LOG_MESSAGE_BYTES);
match record.channel() { match record.channel() {
RecordChannel::Log => DiagnosticPayload::Log(KAPTAN_LOG_DATA { RecordChannel::Log => {
level: record.severity() as u8, let message = record
flags: KaptanLogFlags::from_bits_retain(record.flags().bits()), .message()
message_length: message.len(), .expect("log channel must contain ordinary message text");
message: *message.bytes(), debug_assert!(usize::from(message.len()) <= LOG_MESSAGE_BYTES);
}), DiagnosticPayload::Log(KAPTAN_LOG_DATA {
RecordChannel::Error => DiagnosticPayload::Error(KAPTAN_ERROR_DATA { level: record.severity() as u8,
flags: KaptanErrorFlags::from_bits_retain(record.flags().error_bits()), flags: KaptanLogFlags::from_bits_retain(record.flags().bits()),
message_length: message.len(), message_length: message.len(),
message: *message.bytes(), 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 /// Encodes one typed shutdown event. The caller owns when a shutdown is safe
/// to report; this pure adapter cannot run a shutdown state machine. /// to report; this pure adapter cannot run a shutdown state machine.
pub const fn encode_shutdown(report: ShutdownReport) -> KAPTAN_SHUTDOWN_DATA { pub const fn encode_shutdown(report: ShutdownReport) -> KAPTAN_SHUTDOWN_DATA {
@@ -66,10 +108,13 @@ pub const fn encode_shutdown(report: ShutdownReport) -> KAPTAN_SHUTDOWN_DATA {
mod tests { mod tests {
use std::sync::Once; use std::sync::Once;
use super::{DiagnosticPayload, LOG_MESSAGE_BYTES, encode_diagnostic, encode_shutdown}; use super::{
DiagnosticPayload, ERROR_ADDITIONAL_INFORMATION_BYTES, ERROR_PATH_BYTES, LOG_MESSAGE_BYTES,
encode_diagnostic, encode_shutdown,
};
use kaptan_log::{ use kaptan_log::{
EventHub, FacadeIngress, GlobalLogger, LogFilter, LogFlags, LogRecord, LogText, Severity, ErrorAdditionalInformation, ErrorPath, EventHub, FacadeIngress, GlobalLogger, LogFilter,
ShutdownReason, ShutdownReport, LogFlags, LogRecord, LogText, Severity, ShutdownReason, ShutdownReport,
}; };
use kaptan_mavlink::{ use kaptan_mavlink::{
encoder::MavlinkEncoder, encoder::MavlinkEncoder,
@@ -123,8 +168,12 @@ mod tests {
panic!("log::error! must not produce KAPTAN_LOG"); panic!("log::error! must not produce KAPTAN_LOG");
}; };
assert!( assert!(
payload.message[..usize::from(payload.message_length)] payload.path[..usize::from(payload.path_length)]
.ends_with(b"critical error survives") .ends_with(b"kaptan_log_mavlink::tests")
);
assert_eq!(
&payload.additional_information[..usize::from(payload.additional_information_length)],
b"critical error survives"
); );
} }
@@ -153,19 +202,28 @@ mod tests {
} }
assert_eq!(data.message.len(), LOG_MESSAGE_BYTES); assert_eq!(data.message.len(), LOG_MESSAGE_BYTES);
let error = LogRecord::new( let error = LogRecord::error(
Severity::Error,
LogFlags::TRACE.with(LogFlags::COALESCED), LogFlags::TRACE.with(LogFlags::COALESCED),
LogText::from_message("driver::Error::Transfer"), ErrorPath::from_message("driver::Error::Transfer"),
ErrorAdditionalInformation::new(),
); );
let DiagnosticPayload::Error(error_data) = encode_diagnostic(&error) else { let DiagnosticPayload::Error(error_data) = encode_diagnostic(&error) else {
panic!("error record must use KAPTAN_ERROR"); panic!("error record must use KAPTAN_ERROR");
}; };
assert_eq!(error_data.flags.bits(), LogFlags::COALESCED.bits()); assert_eq!(error_data.flags.bits(), LogFlags::COALESCED.bits());
assert_eq!(error_data.message_length, 23); assert_eq!(error_data.path_length, 23);
assert_eq!(KAPTAN_ERROR_DATA::ENCODED_LEN, 162); 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); let error_frame = encoder.custom(&error_data);
assert_eq!(error_frame.message_id(), 51_021); assert_eq!(error_frame.message_id(), 51_021);
assert!(error_frame.raw_bytes().len() <= 175);
assert!(matches!( assert!(matches!(
MavMessage::parse( MavMessage::parse(
mavlink::MavlinkVersion::V2, mavlink::MavlinkVersion::V2,
@@ -176,6 +234,54 @@ mod tests {
)); ));
} }
#[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] #[test]
fn typed_shutdown_maps_to_its_sole_generated_message() { fn typed_shutdown_maps_to_its_sole_generated_message() {
let data = encode_shutdown(ShutdownReport::new(ShutdownReason::Safety)); let data = encode_shutdown(ShutdownReport::new(ShutdownReason::Safety));
+1 -1
View File
@@ -177,7 +177,7 @@ mod tests {
#[test] #[test]
fn validation_error_has_a_static_derived_message() { fn validation_error_has_a_static_derived_message() {
assert_eq!( assert_eq!(
CrashRecordError::BadChecksum.error_message().as_str(), CrashRecordError::BadChecksum.static_error().path(),
"kaptan_log::crash::CrashRecordError::BadChecksum" "kaptan_log::crash::CrashRecordError::BadChecksum"
); );
} }
+64 -8
View File
@@ -11,7 +11,10 @@ use core::{
use log::{Level, LevelFilter, Log, Metadata, Record, SetLoggerError}; use log::{Level, LevelFilter, Log, Metadata, Record, SetLoggerError};
use crate::{EventHub, LogFilter, LogFlags, LogText, PublishOutcome, Severity}; use crate::{
ErrorAdditionalInformation, ErrorPath, EventHub, LogFilter, LogFlags, LogText, PublishOutcome,
Severity,
};
/// Saturating, process-local accounting for the optional global facade. /// Saturating, process-local accounting for the optional global facade.
/// ///
@@ -80,7 +83,30 @@ impl FacadeCounters {
struct IngressRecord { struct IngressRecord {
severity: Severity, severity: Severity,
flags: LogFlags, flags: LogFlags,
text: LogText, body: IngressBody,
}
#[derive(Clone, Copy)]
enum IngressBody {
Log {
message: LogText,
},
Error {
path: ErrorPath,
additional_information: ErrorAdditionalInformation,
},
}
impl IngressBody {
const fn is_truncated(self) -> bool {
match self {
Self::Log { message } => message.is_truncated(),
Self::Error {
path,
additional_information,
} => path.is_truncated() || additional_information.is_truncated(),
}
}
} }
struct IngressQueue<const N: usize> { struct IngressQueue<const N: usize> {
@@ -206,7 +232,15 @@ impl<const N: usize> FacadeIngress<N> {
Some(None) => self.with_queue(IngressQueue::pop).flatten()?, Some(None) => self.with_queue(IngressQueue::pop).flatten()?,
None => return None, None => return None,
}; };
Some(hub.publish_text(record.severity, record.flags, record.text)) Some(match record.body {
IngressBody::Log { message } => {
hub.publish_text(record.severity, record.flags, message)
}
IngressBody::Error {
path,
additional_information,
} => hub.publish_error_text(record.flags, path, additional_information),
})
} }
fn with_critical<R>( fn with_critical<R>(
@@ -303,14 +337,30 @@ impl<const N: usize> Log for GlobalLogger<N> {
} else { } else {
LogFlags::NONE LogFlags::NONE
}; };
let text = LogText::from_arguments(format_args!("[{}] {}", record.target(), record.args())); let body = if severity <= Severity::Error {
if text.is_truncated() { IngressBody::Error {
path: ErrorPath::from_message(record.target()),
additional_information: ErrorAdditionalInformation::from_arguments(format_args!(
"{}",
record.args()
)),
}
} else {
IngressBody::Log {
message: LogText::from_arguments(format_args!(
"[{}] {}",
record.target(),
record.args()
)),
}
};
if body.is_truncated() {
FacadeCounters::increment(&self.ingress.counters.truncated); FacadeCounters::increment(&self.ingress.counters.truncated);
} }
self.ingress.push(IngressRecord { self.ingress.push(IngressRecord {
severity, severity,
flags, flags,
text, body,
}); });
} }
@@ -385,7 +435,11 @@ mod tests {
let mut hub = EventHub::<1, 1>::new(LogFilter::default()); let mut hub = EventHub::<1, 1>::new(LogFilter::default());
assert!(INGRESS.drain_one_into(&mut hub).is_some()); assert!(INGRESS.drain_one_into(&mut hub).is_some());
let pending = hub.peek().unwrap(); let pending = hub.peek().unwrap();
assert_eq!(pending.message().as_str(), "[test] link failed"); assert_eq!(pending.error_path().unwrap().as_str(), "test");
assert_eq!(
pending.additional_information().unwrap().as_str(),
"link failed"
);
assert_eq!(pending.severity(), Severity::Error); assert_eq!(pending.severity(), Severity::Error);
assert_eq!(pending.channel(), RecordChannel::Error); assert_eq!(pending.channel(), RecordChannel::Error);
} }
@@ -408,6 +462,7 @@ mod tests {
hub.peek() hub.peek()
.unwrap() .unwrap()
.message() .message()
.unwrap()
.as_str() .as_str()
.ends_with("armed configuration loaded") .ends_with("armed configuration loaded")
); );
@@ -505,7 +560,8 @@ mod tests {
assert!(INGRESS.drain_one_into(&mut hub).is_some()); assert!(INGRESS.drain_one_into(&mut hub).is_some());
let error = hub.peek().expect("error retained ahead of optional text"); let error = hub.peek().expect("error retained ahead of optional text");
assert_eq!(error.channel(), RecordChannel::Error); assert_eq!(error.channel(), RecordChannel::Error);
assert_eq!(error.message().as_str(), "[test] critical"); assert_eq!(error.error_path().unwrap().as_str(), "test");
assert_eq!(error.additional_information().unwrap().as_str(), "critical");
assert_eq!(INGRESS.health().full, 0); assert_eq!(INGRESS.health().full, 0);
} }
} }
+77 -16
View File
@@ -3,8 +3,8 @@
use core::fmt; use core::fmt;
use crate::{ use crate::{
DeliveryClass, ErrorReport, LogFilter, LogFlags, LogRecord, LogText, Severity, DeliveryClass, ErrorAdditionalInformation, ErrorPath, ErrorReport, LogFilter, LogFlags,
StaticErrorMessage, LogRecord, LogText, Severity, StaticError,
}; };
/// Outcome from one bounded diagnostic admission attempt. /// Outcome from one bounded diagnostic admission attempt.
@@ -164,7 +164,7 @@ impl<const CRITICAL: usize, const BEST_EFFORT: usize> EventHub<CRITICAL, BEST_EF
/// Retains one static derived error without formatting or exposing source /// Retains one static derived error without formatting or exposing source
/// values. The adapter maps it to `KAPTAN_ERROR`. /// values. The adapter maps it to `KAPTAN_ERROR`.
pub fn publish_error<E: ErrorReport>(&mut self, flags: LogFlags, error: &E) -> PublishOutcome { pub fn publish_error<E: ErrorReport>(&mut self, flags: LogFlags, error: &E) -> PublishOutcome {
self.publish_error_static(flags, error.error_message()) self.publish_error_static(flags, error.static_error())
} }
/// Retains one static mailbox error. This is the ISR-safe counterpart to /// Retains one static mailbox error. This is the ISR-safe counterpart to
@@ -172,15 +172,27 @@ impl<const CRITICAL: usize, const BEST_EFFORT: usize> EventHub<CRITICAL, BEST_EF
pub fn publish_error_static( pub fn publish_error_static(
&mut self, &mut self,
flags: LogFlags, flags: LogFlags,
message: &'static StaticErrorMessage, error: &'static StaticError,
) -> PublishOutcome { ) -> PublishOutcome {
self.publish_text( self.publish_error_text(
Severity::Error,
flags, flags,
LogText::from_message(message.text()), ErrorPath::from_message(error.path()),
ErrorAdditionalInformation::from_message(error.additional_information()),
) )
} }
/// Retains a preformatted error identity and optional detail. Both
/// components stay transport-neutral here; the MAVLink adapter enforces
/// its independent on-wire capacities.
pub fn publish_error_text(
&mut self,
flags: LogFlags,
path: ErrorPath,
additional_information: ErrorAdditionalInformation,
) -> PublishOutcome {
self.publish_record(LogRecord::error(flags, path, additional_information))
}
/// Retains one preformatted bounded text payload. Error-or-higher /// Retains one preformatted bounded text payload. Error-or-higher
/// severities are independently retained and sent as `KAPTAN_ERROR`. /// severities are independently retained and sent as `KAPTAN_ERROR`.
pub fn publish_text( pub fn publish_text(
@@ -189,16 +201,16 @@ impl<const CRITICAL: usize, const BEST_EFFORT: usize> EventHub<CRITICAL, BEST_EF
flags: LogFlags, flags: LogFlags,
text: LogText, text: LogText,
) -> PublishOutcome { ) -> PublishOutcome {
let delivery = if severity <= Severity::Error { let delivery = delivery_for(severity);
DeliveryClass::Critical
} else {
DeliveryClass::BestEffort
};
if delivery == DeliveryClass::BestEffort && !self.filter.allows(severity) { if delivery == DeliveryClass::BestEffort && !self.filter.allows(severity) {
HubHealth::increment(&mut self.health.filtered); HubHealth::increment(&mut self.health.filtered);
return PublishOutcome::Filtered; return PublishOutcome::Filtered;
} }
let record = LogRecord::new(severity, flags, text); self.publish_record(LogRecord::new(severity, flags, text))
}
fn publish_record(&mut self, record: LogRecord) -> PublishOutcome {
let delivery = record.delivery();
let outcome = match delivery { let outcome = match delivery {
DeliveryClass::Critical => self.critical.publish(record, false), DeliveryClass::Critical => self.critical.publish(record, false),
DeliveryClass::BestEffort => self.best_effort.publish(record, true), DeliveryClass::BestEffort => self.best_effort.publish(record, true),
@@ -250,11 +262,20 @@ impl<const CRITICAL: usize, const BEST_EFFORT: usize> EventHub<CRITICAL, BEST_EF
} }
} }
fn delivery_for(severity: Severity) -> DeliveryClass {
if severity <= Severity::Error {
DeliveryClass::Critical
} else {
DeliveryClass::BestEffort
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{EventHub, PublishOutcome, TransportAdmission}; use super::{EventHub, PublishOutcome, TransportAdmission};
use crate::{ use crate::{
DeliveryClass, LogFilter, LogFlags, LogText, RecordChannel, Severity, StaticErrorMessage, DeliveryClass, ErrorAdditionalInformation, ErrorPath, LogFilter, LogFlags, LogText,
RecordChannel, Severity, StaticError,
}; };
#[test] #[test]
@@ -277,13 +298,14 @@ mod tests {
PublishOutcome::Queued PublishOutcome::Queued
); );
let record = hub.peek().unwrap(); let record = hub.peek().unwrap();
assert_eq!(record.message().as_str(), "error"); assert_eq!(record.error_path().unwrap().as_str(), "error");
assert!(record.additional_information().unwrap().is_empty());
assert_eq!(record.channel(), RecordChannel::Error); assert_eq!(record.channel(), RecordChannel::Error);
} }
#[test] #[test]
fn static_messages_and_repeats_are_bounded() { fn static_messages_and_repeats_are_bounded() {
static FAULT: StaticErrorMessage = StaticErrorMessage::new("driver fault"); static FAULT: StaticError = StaticError::new("driver::Fault");
let mut hub = EventHub::<1, 1>::new(LogFilter::default()); let mut hub = EventHub::<1, 1>::new(LogFilter::default());
assert_eq!( assert_eq!(
hub.publish_error_static(LogFlags::NONE, &FAULT), hub.publish_error_static(LogFlags::NONE, &FAULT),
@@ -296,6 +318,45 @@ mod tests {
let record = hub.peek().unwrap(); let record = hub.peek().unwrap();
assert_eq!(record.occurrences, 2); assert_eq!(record.occurrences, 2);
assert!(record.flags().contains(LogFlags::COALESCED)); assert!(record.flags().contains(LogFlags::COALESCED));
assert_eq!(record.error_path().unwrap().as_str(), "driver::Fault");
assert!(record.additional_information().unwrap().is_empty());
}
#[test]
fn static_error_detail_needs_no_producer_formatting() {
static FAULT: StaticError =
StaticError::with_additional_information("driver::Dma::Fault", "stream 7");
let mut hub = EventHub::<1, 1>::new(LogFilter::default());
assert_eq!(
hub.publish_error_static(LogFlags::NONE, &FAULT),
PublishOutcome::Queued
);
let record = hub.peek().unwrap();
assert_eq!(record.error_path().unwrap().as_str(), "driver::Dma::Fault");
assert_eq!(
record.additional_information().unwrap().as_str(),
"stream 7"
);
}
#[test]
fn error_identity_and_detail_are_independent_record_components() {
let mut hub = EventHub::<1, 1>::new(LogFilter::default());
let _ = hub.publish_error_text(
LogFlags::NONE,
ErrorPath::from_message("driver::Icm42688::Transfer"),
ErrorAdditionalInformation::from_message("SPI DMA timeout"),
);
let record = hub.peek().unwrap();
assert_eq!(record.channel(), RecordChannel::Error);
assert_eq!(
record.error_path().unwrap().as_str(),
"driver::Icm42688::Transfer"
);
assert_eq!(
record.additional_information().unwrap().as_str(),
"SPI DMA timeout"
);
} }
#[test] #[test]
+2 -4
View File
@@ -29,11 +29,9 @@ pub use hub::{EventHub, HubHealth, PublishOutcome, TransportAdmission};
pub use kaptan_log_derive::KaptanError; pub use kaptan_log_derive::KaptanError;
pub use level::Severity; pub use level::Severity;
pub use mailbox::StaticErrorMailbox; pub use mailbox::StaticErrorMailbox;
pub use report::{ pub use report::{DeliveryClass, ErrorReport, LogFlags, LogRecord, RecordChannel, StaticError};
DeliveryClass, ErrorReport, LogFlags, LogRecord, RecordChannel, StaticErrorMessage,
};
pub use shutdown::{ShutdownReason, ShutdownReport}; pub use shutdown::{ShutdownReason, ShutdownReport};
pub use text::{LogText, MAXIMUM_LOG_TEXT_BYTES}; pub use text::{ErrorAdditionalInformation, ErrorPath, LogText, MAXIMUM_LOG_TEXT_BYTES};
#[cfg(feature = "facade")] #[cfg(feature = "facade")]
pub use facade::FacadeHealth; pub use facade::FacadeHealth;
+13 -13
View File
@@ -5,7 +5,7 @@ use core::{
sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering}, sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering},
}; };
use crate::StaticErrorMessage; use crate::StaticError;
/// One source-local, single-attempt static-fault mailbox. /// One source-local, single-attempt static-fault mailbox.
/// ///
@@ -13,7 +13,7 @@ use crate::StaticErrorMessage;
/// coalesced. They never loop, format, allocate, lock, or encode a frame. A /// coalesced. They never loop, format, allocate, lock, or encode a frame. A
/// worker drains the exact message pointer and flag later. /// worker drains the exact message pointer and flag later.
pub struct StaticErrorMailbox { pub struct StaticErrorMailbox {
message: AtomicPtr<StaticErrorMessage>, error: AtomicPtr<StaticError>,
coalesced: AtomicBool, coalesced: AtomicBool,
coalesced_total: AtomicU32, coalesced_total: AtomicU32,
} }
@@ -22,19 +22,19 @@ impl StaticErrorMailbox {
/// Creates an empty mailbox. /// Creates an empty mailbox.
pub const fn new() -> Self { pub const fn new() -> Self {
Self { Self {
message: AtomicPtr::new(ptr::null_mut()), error: AtomicPtr::new(ptr::null_mut()),
coalesced: AtomicBool::new(false), coalesced: AtomicBool::new(false),
coalesced_total: AtomicU32::new(0), coalesced_total: AtomicU32::new(0),
} }
} }
/// Records one static message with one bounded atomic attempt. /// Records one static message with one bounded atomic attempt.
pub fn record(&self, message: &'static StaticErrorMessage) { pub fn record(&self, error: &'static StaticError) {
if self if self
.message .error
.compare_exchange( .compare_exchange(
ptr::null_mut(), ptr::null_mut(),
core::ptr::from_ref(message).cast_mut(), core::ptr::from_ref(error).cast_mut(),
Ordering::AcqRel, Ordering::AcqRel,
Ordering::Acquire, Ordering::Acquire,
) )
@@ -46,13 +46,13 @@ impl StaticErrorMailbox {
} }
/// Removes the retained first message and its coalesced marker. /// Removes the retained first message and its coalesced marker.
pub fn take(&self) -> (Option<&'static StaticErrorMessage>, bool) { pub fn take(&self) -> (Option<&'static StaticError>, bool) {
let message = self.message.swap(ptr::null_mut(), Ordering::AcqRel); let error = self.error.swap(ptr::null_mut(), Ordering::AcqRel);
let coalesced = self.coalesced.swap(false, Ordering::AcqRel); let coalesced = self.coalesced.swap(false, Ordering::AcqRel);
// SAFETY: `record` stores only pointers to static messages and the // SAFETY: `record` stores only pointers to static messages and the
// atomic swap transfers the exact pointer to this one worker. // atomic swap transfers the exact pointer to this one worker.
let message = unsafe { message.as_ref() }; let error = unsafe { error.as_ref() };
(message, coalesced) (error, coalesced)
} }
/// Total later-fault coalesces since startup. This is local producer /// Total later-fault coalesces since startup. This is local producer
@@ -81,12 +81,12 @@ impl Default for StaticErrorMailbox {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::StaticErrorMailbox; use super::StaticErrorMailbox;
use crate::StaticErrorMessage; use crate::StaticError;
#[test] #[test]
fn source_mailbox_keeps_the_first_static_message_and_marks_later_faults() { fn source_mailbox_keeps_the_first_static_message_and_marks_later_faults() {
static FIRST: StaticErrorMessage = StaticErrorMessage::new("first"); static FIRST: StaticError = StaticError::new("first");
static SECOND: StaticErrorMessage = StaticErrorMessage::new("second"); static SECOND: StaticError = StaticError::new("second");
let mailbox = StaticErrorMailbox::new(); let mailbox = StaticErrorMailbox::new();
mailbox.record(&FIRST); mailbox.record(&FIRST);
mailbox.record(&SECOND); mailbox.record(&SECOND);
+139 -49
View File
@@ -1,37 +1,52 @@
//! Bounded human-readable diagnostic records. //! Bounded human-readable diagnostic records.
use crate::{LogText, Severity}; use crate::{ErrorAdditionalInformation, ErrorPath, LogText, Severity};
/// Static message generated for one error type/variant. /// Static identity and optional detail for one error.
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct StaticErrorMessage { pub struct StaticError {
text: &'static str, path: &'static str,
additional_information: &'static str,
} }
impl StaticErrorMessage { impl StaticError {
/// Creates one compile-time error message. /// Creates one compile-time error identity without additional detail.
pub const fn new(text: &'static str) -> Self { pub const fn new(path: &'static str) -> Self {
Self { text } Self {
path,
additional_information: "",
}
} }
/// Stable human-readable message text. /// Creates one compile-time error identity with static additional detail.
pub const fn text(self) -> &'static str { pub const fn with_additional_information(
self.text path: &'static str,
additional_information: &'static str,
) -> Self {
Self {
path,
additional_information,
}
} }
/// Alias used by the error derive's [`core::fmt::Display`] implementation. /// Stable human-readable error identity.
pub const fn as_str(self) -> &'static str { pub const fn path(self) -> &'static str {
self.text self.path
}
/// Optional static human-facing detail.
pub const fn additional_information(self) -> &'static str {
self.additional_information
} }
} }
/// Implemented by [`crate::KaptanError`] derives. /// Implemented by [`crate::KaptanError`] derives.
/// ///
/// This trait is deliberately pure: obtaining a message never allocates, /// This trait is deliberately pure: obtaining error identity never allocates,
/// publishes telemetry, changes safety state, or serializes generic sources. /// publishes telemetry, changes safety state, or serializes generic sources.
pub trait ErrorReport { pub trait ErrorReport {
/// Returns the static type/variant message for this error value. /// Returns static identity and optional detail for this error value.
fn error_message(&self) -> &'static StaticErrorMessage; fn static_error(&self) -> &'static StaticError;
} }
/// Bounded MAVLink log flags. /// Bounded MAVLink log flags.
@@ -81,6 +96,17 @@ pub enum RecordChannel {
Error, Error,
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum RecordBody {
Log {
message: LogText,
},
Error {
path: ErrorPath,
additional_information: ErrorAdditionalInformation,
},
}
/// Priority selected internally from a diagnostic level. /// Priority selected internally from a diagnostic level.
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DeliveryClass { pub enum DeliveryClass {
@@ -95,36 +121,58 @@ pub enum DeliveryClass {
pub struct LogRecord { pub struct LogRecord {
severity: Severity, severity: Severity,
flags: LogFlags, flags: LogFlags,
message: LogText, body: RecordBody,
channel: RecordChannel, /// Adjacent equivalent diagnostics retained as one record.
/// Adjacent identical messages retained as one record.
pub occurrences: u32, pub occurrences: u32,
delivery: DeliveryClass,
} }
impl LogRecord { impl LogRecord {
/// Creates one bounded record. Error-or-higher severities use /// Creates one bounded diagnostic record. Error-or-higher severities use
/// [`RecordChannel::Error`]; ordinary severity records use /// their supplied text as the error path with no additional information;
/// [`RecordChannel::Log`]. /// ordinary severity records use `KAPTAN_LOG` message text.
pub fn new(severity: Severity, mut flags: LogFlags, message: LogText) -> Self { pub fn new(severity: Severity, flags: LogFlags, message: LogText) -> Self {
if message.is_truncated() { if severity <= Severity::Error {
flags = flags.with(LogFlags::TRUNCATED); let flags = with_truncation(flags, message);
return Self::error_with_severity(
severity,
flags,
ErrorPath::from_message(message.as_str()),
ErrorAdditionalInformation::new(),
);
} }
Self { Self {
severity, severity,
flags, flags: with_truncation(flags, message),
message, body: RecordBody::Log { message },
channel: if severity <= Severity::Error { occurrences: 1,
RecordChannel::Error }
} else { }
RecordChannel::Log
/// Creates an error-channel record with independently bounded identity
/// and optional additional information.
pub fn error(
flags: LogFlags,
path: ErrorPath,
additional_information: ErrorAdditionalInformation,
) -> Self {
Self::error_with_severity(Severity::Error, flags, path, additional_information)
}
fn error_with_severity(
severity: Severity,
flags: LogFlags,
path: ErrorPath,
additional_information: ErrorAdditionalInformation,
) -> Self {
debug_assert!(severity <= Severity::Error);
Self {
severity,
flags: flags_for_error(flags, path, additional_information),
body: RecordBody::Error {
path,
additional_information,
}, },
occurrences: 1, occurrences: 1,
delivery: if severity <= Severity::Error {
DeliveryClass::Critical
} else {
DeliveryClass::BestEffort
},
} }
} }
@@ -138,29 +186,53 @@ impl LogRecord {
self.flags self.flags
} }
/// Fixed formatted text. /// Fixed text carried by an ordinary `KAPTAN_LOG` record.
pub const fn message(&self) -> &LogText { pub const fn message(&self) -> Option<&LogText> {
&self.message match &self.body {
RecordBody::Log { message } => Some(message),
RecordBody::Error { .. } => None,
}
}
/// Fixed identity path carried by a `KAPTAN_ERROR` record.
pub const fn error_path(&self) -> Option<&ErrorPath> {
match &self.body {
RecordBody::Log { .. } => None,
RecordBody::Error { path, .. } => Some(path),
}
}
/// Fixed optional information carried by a `KAPTAN_ERROR` record.
pub const fn additional_information(&self) -> Option<&ErrorAdditionalInformation> {
match &self.body {
RecordBody::Log { .. } => None,
RecordBody::Error {
additional_information,
..
} => Some(additional_information),
}
} }
/// Custom MAVLink diagnostic family selected for this record. /// Custom MAVLink diagnostic family selected for this record.
pub const fn channel(&self) -> RecordChannel { pub const fn channel(&self) -> RecordChannel {
self.channel match self.body {
RecordBody::Log { .. } => RecordChannel::Log,
RecordBody::Error { .. } => RecordChannel::Error,
}
} }
/// Internal admission priority. /// Internal admission priority.
pub const fn delivery(&self) -> DeliveryClass { pub const fn delivery(&self) -> DeliveryClass {
self.delivery match self.body {
RecordBody::Log { .. } => DeliveryClass::BestEffort,
RecordBody::Error { .. } => DeliveryClass::Critical,
}
} }
/// Tests whether two pending records can coalesce without hiding a new /// Tests whether two pending records can coalesce without hiding a new
/// message, severity, or delivery class. /// payload, severity, or delivery class.
pub fn is_equivalent_to(&self, other: &Self) -> bool { pub fn is_equivalent_to(&self, other: &Self) -> bool {
self.channel == other.channel self.severity == other.severity && self.flags == other.flags && self.body == other.body
&& self.severity == other.severity
&& self.flags == other.flags
&& self.message == other.message
&& self.delivery == other.delivery
} }
pub(crate) fn increment_occurrences(&mut self) { pub(crate) fn increment_occurrences(&mut self) {
@@ -168,3 +240,21 @@ impl LogRecord {
self.flags = self.flags.with(LogFlags::COALESCED); self.flags = self.flags.with(LogFlags::COALESCED);
} }
} }
fn with_truncation(mut flags: LogFlags, value: LogText) -> LogFlags {
if value.is_truncated() {
flags = flags.with(LogFlags::TRUNCATED);
}
flags
}
fn flags_for_error(
mut flags: LogFlags,
path: ErrorPath,
additional_information: ErrorAdditionalInformation,
) -> LogFlags {
if path.is_truncated() || additional_information.is_truncated() {
flags = flags.with(LogFlags::TRUNCATED);
}
flags
}
+11 -4
View File
@@ -1,11 +1,11 @@
//! Fixed-capacity UTF-8 log text. //! Fixed-capacity UTF-8 diagnostic text.
use core::fmt::{self, Write}; use core::fmt::{self, Write};
/// Maximum UTF-8 bytes carried by one Kaptan log record. /// Maximum bytes retained in one transport-neutral diagnostic text component.
pub const MAXIMUM_LOG_TEXT_BYTES: usize = 160; pub const MAXIMUM_LOG_TEXT_BYTES: usize = 160;
/// Bounded formatted text with an explicit truncation marker. /// Fixed UTF-8 diagnostic text with an explicit truncation marker.
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LogText { pub struct LogText {
bytes: [u8; MAXIMUM_LOG_TEXT_BYTES], bytes: [u8; MAXIMUM_LOG_TEXT_BYTES],
@@ -13,8 +13,15 @@ pub struct LogText {
truncated: bool, truncated: bool,
} }
/// Transport-neutral error identity path. The MAVLink adapter applies its
/// separate 96-byte wire bound.
pub type ErrorPath = LogText;
/// Transport-neutral optional error detail. The MAVLink adapter applies its
/// separate 64-byte wire bound.
pub type ErrorAdditionalInformation = LogText;
impl LogText { impl LogText {
/// Creates an empty bounded log message. /// Creates empty fixed text storage.
pub const fn new() -> Self { pub const fn new() -> Self {
Self { Self {
bytes: [0; MAXIMUM_LOG_TEXT_BYTES], bytes: [0; MAXIMUM_LOG_TEXT_BYTES],
+3 -1
View File
@@ -5,7 +5,9 @@ fn main() {
let dialect = manifest_directory.join("definitions/kaptan.xml"); let dialect = manifest_directory.join("definitions/kaptan.xml");
let config = manifest_directory.join("definitions/kaptan/config.xml"); let config = manifest_directory.join("definitions/kaptan/config.xml");
let log = manifest_directory.join("definitions/kaptan/log.xml"); let log = manifest_directory.join("definitions/kaptan/log.xml");
for definition in [&dialect, &config, &log] { let error = manifest_directory.join("definitions/kaptan/error.xml");
let shutdown = manifest_directory.join("definitions/kaptan/shutdown.xml");
for definition in [&dialect, &config, &log, &error, &shutdown] {
println!("cargo::rerun-if-changed={}", definition.display()); println!("cargo::rerun-if-changed={}", definition.display());
} }
println!("cargo::rustc-check-cfg=cfg(kaptan_has_messages)"); println!("cargo::rustc-check-cfg=cfg(kaptan_has_messages)");
@@ -4,4 +4,6 @@
fragments so their message catalogs can evolve without a monolith. --> fragments so their message catalogs can evolve without a monolith. -->
<include>kaptan/config.xml</include> <include>kaptan/config.xml</include>
<include>kaptan/log.xml</include> <include>kaptan/log.xml</include>
<include>kaptan/error.xml</include>
<include>kaptan/shutdown.xml</include>
</mavlink> </mavlink>
@@ -0,0 +1,26 @@
<?xml version="1.0"?>
<mavlink>
<version>3</version>
<dialect>0</dialect>
<!-- Kaptan-reserved development block. Register these IDs before release. -->
<enums>
<enum name="KAPTAN_ERROR_FLAGS" bitmask="true">
<entry value="2" name="KAPTAN_ERROR_FLAG_TRUNCATED">
<description>The path or additional-information UTF-8 prefix was truncated to its bounded on-wire field.</description>
</entry>
<entry value="4" name="KAPTAN_ERROR_FLAG_COALESCED">
<description>Error represents repeated adjacent observations with the same path and additional information.</description>
</entry>
</enum>
</enums>
<messages>
<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="path_length">Number of valid UTF-8 bytes in path; no more than 96.</field>
<field type="uint8_t" name="additional_information_length">Number of valid UTF-8 bytes in additional_information; no more than 64.</field>
<field type="uint8_t[96]" name="path">Bounded UTF-8 derived or explicit error path. Bytes after path_length are zero.</field>
<field type="uint8_t[64]" name="additional_information">Bounded UTF-8 optional error context. Bytes after additional_information_length are zero.</field>
</message>
</messages>
</mavlink>
@@ -15,22 +15,6 @@
<description>Message represents repeated adjacent observations.</description> <description>Message represents repeated adjacent observations.</description>
</entry> </entry>
</enum> </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> </enums>
<messages> <messages>
<message id="51020" name="KAPTAN_LOG"> <message id="51020" name="KAPTAN_LOG">
@@ -40,15 +24,5 @@
<field type="uint8_t" name="message_length">Number of valid UTF-8 bytes in message; at most 160.</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> <field type="uint8_t[160]" name="message">Bounded UTF-8 message bytes. Bytes after message_length are zero.</field>
</message> </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> </messages>
</mavlink> </mavlink>
@@ -0,0 +1,22 @@
<?xml version="1.0"?>
<mavlink>
<version>3</version>
<dialect>0</dialect>
<!-- Kaptan-reserved development block. Register these IDs before release. -->
<enums>
<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="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>
+16 -16
View File
@@ -18,8 +18,8 @@ use kaptan_drivers::{
mtf01::Mtf01Service, mtf01::Mtf01Service,
}; };
use kaptan_log::{ use kaptan_log::{
DeliveryClass, ErrorReport, EventHub, KaptanError, LogFilter, LogFlags, StaticErrorMailbox, DeliveryClass, ErrorReport, EventHub, KaptanError, LogFilter, LogFlags, StaticError,
StaticErrorMessage, TransportAdmission, StaticErrorMailbox, TransportAdmission,
}; };
use kaptan_log_mavlink::{DiagnosticPayload, encode_diagnostic}; use kaptan_log_mavlink::{DiagnosticPayload, encode_diagnostic};
use kaptan_spi::SpiCompletion; use kaptan_spi::SpiCompletion;
@@ -54,11 +54,11 @@ const KAPTAN_LOG_FRAME_MAXIMUM_BYTES: usize =
kaptan_mavlink::message::kaptan::KAPTAN_LOG_DATA::ENCODED_LEN kaptan_mavlink::message::kaptan::KAPTAN_LOG_DATA::ENCODED_LEN
+ MAVLINK_V2_UNSIGNED_OVERHEAD_BYTES; + MAVLINK_V2_UNSIGNED_OVERHEAD_BYTES;
// The static F722 telemetry profile is bounded at 62 scheduled frames/s. // The static F722 telemetry profile is bounded at 62 scheduled frames/s.
// It permits ten `KAPTAN_ERROR` and five `KAPTAN_LOG` frames/s. Their maxima // It permits ten `KAPTAN_ERROR` and five `KAPTAN_LOG` frames/s. Both maxima
// are respectively 162 and 163 payload bytes plus 12 unsigned MAVLink V2 // are 163 payload bytes plus 12 unsigned MAVLink V2 framing bytes, so
// framing bytes, so 62×48 + 10×174 + 5×175 = 5,591 bytes/s. MAVLink signing // 62×48 + 15×175 = 5,601 bytes/s. MAVLink signing is not enabled in this
// is not enabled in this telemetry-only image; any future signing profile // telemetry-only image; any future signing profile must re-budget its
// must re-budget its signature overhead before it can enable the endpoint. // signature overhead before it can enable the endpoint.
pub(crate) const WORST_CASE_OBSERVABILITY_BYTES_PER_SECOND: usize = SCHEDULED_FRAMES_PER_SECOND pub(crate) const WORST_CASE_OBSERVABILITY_BYTES_PER_SECOND: usize = SCHEDULED_FRAMES_PER_SECOND
* SCHEDULED_FRAME_MAXIMUM_BYTES * SCHEDULED_FRAME_MAXIMUM_BYTES
+ ERROR_RECORDS_PER_SECOND as usize * KAPTAN_ERROR_FRAME_MAXIMUM_BYTES + ERROR_RECORDS_PER_SECOND as usize * KAPTAN_ERROR_FRAME_MAXIMUM_BYTES
@@ -427,7 +427,7 @@ pub(crate) fn on_uart4_interrupt(mtf01: &mut Option<F722Mtf01>) {
} }
ReceiveInterrupt::Fault => { ReceiveInterrupt::Fault => {
mtf01.reset(); mtf01.reset();
record_mtf01_fault(F722TelemetryError::Mtf01Uart.error_message()); record_mtf01_fault(F722TelemetryError::Mtf01Uart.static_error());
} }
ReceiveInterrupt::NotPending => {} ReceiveInterrupt::NotPending => {}
} }
@@ -455,7 +455,7 @@ impl F722LidarState {
if self.startup_ticks == LIDAR_STARTUP_TICKS if self.startup_ticks == LIDAR_STARTUP_TICKS
&& let Err(error) = range.finish_startup() && let Err(error) = range.finish_startup()
{ {
record_lidar_fault(error.error_message()); record_lidar_fault(error.static_error());
} }
} else { } else {
publish_lidar_event(range.on_periodic_tick()); publish_lidar_event(range.on_periodic_tick());
@@ -542,13 +542,13 @@ impl F722MavlinkState {
while let Some(fault) = self.data_channel.take_transport_fault() { while let Some(fault) = self.data_channel.take_transport_fault() {
let message = match fault { let message = match fault {
kaptan_mavlink::transmitter::TransmitFaultKind::Memory => { kaptan_mavlink::transmitter::TransmitFaultKind::Memory => {
F722TelemetryError::TransmitMemory.error_message() F722TelemetryError::TransmitMemory.static_error()
} }
kaptan_mavlink::transmitter::TransmitFaultKind::Engine => { kaptan_mavlink::transmitter::TransmitFaultKind::Engine => {
F722TelemetryError::TransmitEngine.error_message() F722TelemetryError::TransmitEngine.static_error()
} }
kaptan_mavlink::transmitter::TransmitFaultKind::Terminal => { kaptan_mavlink::transmitter::TransmitFaultKind::Terminal => {
F722TelemetryError::TransmitTerminal.error_message() F722TelemetryError::TransmitTerminal.static_error()
} }
}; };
let _ = self.logs.publish_error_static(LogFlags::NONE, message); let _ = self.logs.publish_error_static(LogFlags::NONE, message);
@@ -634,19 +634,19 @@ pub fn record_mavlink_completion(completion: kaptan_dma::TxCompletion) {
fn publish_lidar_event<E: ErrorReport>(event: LidarLiteV3ServiceEvent<E>) { fn publish_lidar_event<E: ErrorReport>(event: LidarLiteV3ServiceEvent<E>) {
match event { match event {
LidarLiteV3ServiceEvent::Measurement(snapshot) => RANGES.publish(snapshot), LidarLiteV3ServiceEvent::Measurement(snapshot) => RANGES.publish(snapshot),
LidarLiteV3ServiceEvent::Fault(error) => record_lidar_fault(error.error_message()), LidarLiteV3ServiceEvent::Fault(error) => record_lidar_fault(error.static_error()),
LidarLiteV3ServiceEvent::Idle => {} LidarLiteV3ServiceEvent::Idle => {}
} }
} }
fn record_imu_fault(message: &'static StaticErrorMessage) { fn record_imu_fault(message: &'static StaticError) {
IMU_FAULTS.record(message); IMU_FAULTS.record(message);
} }
fn record_lidar_fault(message: &'static StaticErrorMessage) { fn record_lidar_fault(message: &'static StaticError) {
LIDAR_FAULTS.record(message); LIDAR_FAULTS.record(message);
} }
fn record_mtf01_fault(message: &'static StaticErrorMessage) { fn record_mtf01_fault(message: &'static StaticError) {
MTF01_FAULTS.record(message); MTF01_FAULTS.record(message);
} }
+6 -6
View File
@@ -9,7 +9,7 @@ use kaptan_drivers::{
Mpu6000Service, Mpu6000ServiceEvent, Mpu6000Service, Mpu6000ServiceEvent,
}, },
}; };
use kaptan_log::{ErrorReport, StaticErrorMessage}; use kaptan_log::{ErrorReport, StaticError};
use kaptan_sensor::imu::ImuSnapshot; use kaptan_sensor::imu::ImuSnapshot;
use kaptan_spi::SpiCompletion; use kaptan_spi::SpiCompletion;
use stm32f7::stm32f722; use stm32f7::stm32f722;
@@ -31,7 +31,7 @@ pub(crate) enum ImuEvent {
Idle, Idle,
Initialized, Initialized,
Sample(ImuSnapshot), Sample(ImuSnapshot),
Fault(&'static StaticErrorMessage), Fault(&'static StaticError),
} }
/// Opaque owner of the selected F722 IMU service. /// Opaque owner of the selected F722 IMU service.
@@ -96,11 +96,11 @@ impl F722Imu {
match &mut self.inner { match &mut self.inner {
F722ImuKind::Mpu6000(imu) => match imu.begin() { F722ImuKind::Mpu6000(imu) => match imu.begin() {
Ok(()) => ImuEvent::Idle, Ok(()) => ImuEvent::Idle,
Err(error) => ImuEvent::Fault(error.error_message()), Err(error) => ImuEvent::Fault(error.static_error()),
}, },
F722ImuKind::Icm42688(imu) => match imu.begin() { F722ImuKind::Icm42688(imu) => match imu.begin() {
Ok(()) => ImuEvent::Idle, Ok(()) => ImuEvent::Idle,
Err(error) => ImuEvent::Fault(error.error_message()), Err(error) => ImuEvent::Fault(error.static_error()),
}, },
} }
} }
@@ -134,7 +134,7 @@ fn mpu_event<E: ErrorReport>(event: Mpu6000ServiceEvent<E>) -> ImuEvent {
Mpu6000ServiceEvent::Idle => ImuEvent::Idle, Mpu6000ServiceEvent::Idle => ImuEvent::Idle,
Mpu6000ServiceEvent::Initialized => ImuEvent::Initialized, Mpu6000ServiceEvent::Initialized => ImuEvent::Initialized,
Mpu6000ServiceEvent::Sample(sample) => ImuEvent::Sample(sample), Mpu6000ServiceEvent::Sample(sample) => ImuEvent::Sample(sample),
Mpu6000ServiceEvent::Fault(error) => ImuEvent::Fault(error.error_message()), Mpu6000ServiceEvent::Fault(error) => ImuEvent::Fault(error.static_error()),
} }
} }
@@ -143,7 +143,7 @@ fn icm_event<E: ErrorReport>(event: Icm42688ServiceEvent<E>, mount: ImuMount) ->
Icm42688ServiceEvent::Idle => ImuEvent::Idle, Icm42688ServiceEvent::Idle => ImuEvent::Idle,
Icm42688ServiceEvent::Initialized => ImuEvent::Initialized, Icm42688ServiceEvent::Initialized => ImuEvent::Initialized,
Icm42688ServiceEvent::Sample(sample) => ImuEvent::Sample(align_mount(sample, mount)), Icm42688ServiceEvent::Sample(sample) => ImuEvent::Sample(align_mount(sample, mount)),
Icm42688ServiceEvent::Fault(error) => ImuEvent::Fault(error.error_message()), Icm42688ServiceEvent::Fault(error) => ImuEvent::Fault(error.static_error()),
} }
} }
@@ -4,7 +4,7 @@ use kaptan_board::{
BoardConfig, BoardProfile, DataChannelRates, ImuDevice, ImuMount, MavlinkEndpoint, MavlinkPort, BoardConfig, BoardProfile, DataChannelRates, ImuDevice, ImuMount, MavlinkEndpoint, MavlinkPort,
TelemetryProfile, VerticalSensingKind, TelemetryProfile, VerticalSensingKind,
}; };
use kaptan_log::{LogFlags, LogRecord, LogText, Severity}; use kaptan_log::{ErrorAdditionalInformation, ErrorPath, LogFlags, LogRecord, LogText, Severity};
use kaptan_log_mavlink::{DiagnosticPayload, encode_diagnostic}; use kaptan_log_mavlink::{DiagnosticPayload, encode_diagnostic};
use kaptan_mavlink::encoder::MavlinkEncoder; use kaptan_mavlink::encoder::MavlinkEncoder;
use platform_stm32f7::{ use platform_stm32f7::{
@@ -214,7 +214,11 @@ fn telemetry_has_no_inbound_command_authority() {
fn f722_link_budget_uses_both_maximum_generated_diagnostic_frames() { fn f722_link_budget_uses_both_maximum_generated_diagnostic_frames() {
let text = LogText::from_message(&"x".repeat(160)); let text = LogText::from_message(&"x".repeat(160));
let log_record = LogRecord::new(Severity::Info, LogFlags::NONE, text); let log_record = LogRecord::new(Severity::Info, LogFlags::NONE, text);
let error_record = LogRecord::new(Severity::Error, LogFlags::NONE, text); let error_record = LogRecord::error(
LogFlags::NONE,
ErrorPath::from_message(&"p".repeat(96)),
ErrorAdditionalInformation::from_message(&"i".repeat(64)),
);
let mut encoder = MavlinkEncoder::new(1, 1); let mut encoder = MavlinkEncoder::new(1, 1);
let DiagnosticPayload::Log(log_payload) = encode_diagnostic(&log_record) else { let DiagnosticPayload::Log(log_payload) = encode_diagnostic(&log_record) else {
panic!("info record must use KAPTAN_LOG"); panic!("info record must use KAPTAN_LOG");
@@ -230,17 +234,20 @@ fn f722_link_budget_uses_both_maximum_generated_diagnostic_frames() {
kaptan_mavlink::message::kaptan::KAPTAN_LOG_DATA::ENCODED_LEN, kaptan_mavlink::message::kaptan::KAPTAN_LOG_DATA::ENCODED_LEN,
163 163
); );
assert_eq!(error_frame.raw_bytes().len(), 174); assert_eq!(error_frame.raw_bytes().len(), 175);
assert_eq!( assert_eq!(
kaptan_mavlink::message::kaptan::KAPTAN_ERROR_DATA::ENCODED_LEN, kaptan_mavlink::message::kaptan::KAPTAN_ERROR_DATA::ENCODED_LEN,
162 163
); );
assert_eq!(error_payload.path_length, 96);
assert_eq!(error_payload.additional_information_length, 64);
assert_eq!(error_payload.flags.bits(), LogFlags::NONE.bits());
assert_eq!( assert_eq!(
62 * 48 + 10 * error_frame.raw_bytes().len() + 5 * log_frame.raw_bytes().len(), 62 * 48 + 10 * error_frame.raw_bytes().len() + 5 * log_frame.raw_bytes().len(),
5_591 5_601
); );
let board = platform_source("src/f722/board.rs"); let board = platform_source("src/f722/board.rs");
assert!(board.contains("KAPTAN_ERROR_FRAME_MAXIMUM_BYTES")); assert!(board.contains("KAPTAN_ERROR_FRAME_MAXIMUM_BYTES"));
assert!(board.contains("KAPTAN_LOG_FRAME_MAXIMUM_BYTES")); assert!(board.contains("KAPTAN_LOG_FRAME_MAXIMUM_BYTES"));
assert!(board.contains("5,591 bytes/s")); assert!(board.contains("5,601 bytes/s"));
} }
+4 -3
View File
@@ -345,11 +345,12 @@ fn rejected_config_result_enqueues_a_separate_high_level_error_payload() {
let Some(DiagnosticPayload::Error(payload)) = service.pending_diagnostic_payload() else { let Some(DiagnosticPayload::Error(payload)) = service.pending_diagnostic_payload() else {
panic!("rejected configuration result must enqueue KAPTAN_ERROR"); panic!("rejected configuration result must enqueue KAPTAN_ERROR");
}; };
let message = std::str::from_utf8(&payload.message[..usize::from(payload.message_length)]) let error_path = std::str::from_utf8(&payload.path[..usize::from(payload.path_length)])
.expect("derived error identity remains UTF-8"); .expect("derived error path remains UTF-8");
assert_eq!( assert_eq!(
message, error_path,
"kaptan_config_mavlink::protocol::ProtocolError::GenerationConflict" "kaptan_config_mavlink::protocol::ProtocolError::GenerationConflict"
); );
assert_eq!(payload.additional_information_length, 0);
let _ = fs::remove_file(&path); let _ = fs::remove_file(&path);
} }