diff --git a/docs/src/architecture.md b/docs/src/architecture.md index ee90050..ebfb7a1 100644 --- a/docs/src/architecture.md +++ b/docs/src/architecture.md @@ -63,15 +63,17 @@ persistence, authenticated ingress, durable audit logging, disarmed activation, and atomic consumer reconstruction remain separate reviewed work 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; fragments are never separately generated Rust modules. `kaptan-log` itself has no MAVLink, platform, time, allocation, filesystem, or configuration dependency. It assigns bounded Critical and BestEffort queues for text logs. -The platform drains a record only after TX admission; the diagnostic wire -contract deliberately contains only severity, flags, bounded text length, and -text. Local saturation accounting remains inside the hub, facade, and producer -mailboxes; heartbeat and safety telemetry remain separate from diagnostic text. +The platform drains a record only after TX admission. `KAPTAN_LOG` carries +severity plus one bounded text field; `KAPTAN_ERROR` carries flags plus bounded +path and additional-information fields. Local saturation accounting remains +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 contract, [Logs](logs.md) for the receiver contract, and [Errors](errors.md) diff --git a/docs/src/errors.md b/docs/src/errors.md index 05376f8..b89f72d 100644 --- a/docs/src/errors.md +++ b/docs/src/errors.md @@ -11,10 +11,10 @@ enum SensorError { ``` The derive implements `core::error::Error`, `Display`, and `ErrorReport`. It -creates a bounded human-facing `module::Type::Variant` message without -allocating, publishing telemetry, exposing generic source values, or changing -safety state. Renaming a module, type, or variant changes that text; it is not -a stable machine protocol identity. +creates a static `module::Type::Variant` error path without allocating, +publishing telemetry, exposing generic source values, or changing safety +state. Renaming a module, type, or variant changes that path; it is not a +stable machine protocol identity. 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 @@ -26,17 +26,22 @@ for safety state, actuator inhibit, or typed configuration results. - Configuration writes receive authoritative structured failures through `KAPTAN_CONFIG_RESULT`: operation, lifecycle state, field IDs, validation rule, and protocol error code. -- Runtime driver and link faults reach the F722 MAVLink worker as - `KAPTAN_ERROR` text, subject to bounded transport admission. The message - carries no per-error numeric catalog: a `KaptanError` derive supplies its - type/variant display identity. +- Runtime driver and link faults reach the F722 MAVLink worker as a + `KAPTAN_ERROR` path, subject to bounded transport admission. The error + payload is `{ flags, path_length, additional_information_length, path[96], + 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` - containing the high-level `ProtocolError` identity. It is intentionally not - correlated and carries no field/rule detail; the matching + whose path is the high-level `ProtocolError` identity. It is intentionally + not correlated and carries no field/rule detail; the matching `KAPTAN_CONFIG_RESULT` remains the sole precise authority. - Control, navigation, safety, and simulator error types are derive-ready but 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, reset, or retention-record protocol. F722 still uses `panic-halt`; it does not claim a live panic frame or retained-record replay. diff --git a/docs/src/logs.md b/docs/src/logs.md index 8d6d2e1..95ff01a 100644 --- a/docs/src/logs.md +++ b/docs/src/logs.md @@ -18,28 +18,35 @@ messages later. ## 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 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 } ``` -A receiver generated from the same `kaptan.xml` revision validates -`message_length <= 160` and decodes exactly that UTF-8 prefix. `KAPTAN_LOG` -displays its text at the 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. +A GCS decoder generated from the same `kaptan.xml` revision must validate +`message_length <= 160` for `KAPTAN_LOG`, and `path_length <= 96` plus +`additional_information_length <= 64` for `KAPTAN_ERROR`, before slicing the +fixed arrays into UTF-8 prefixes. Generated MAVLink framing alone does not +enforce these semantic length fields. `KAPTAN_LOG` displays its text at the +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; -renaming its Rust module/type/variant changes the display identity. Error -details requiring machine action stay in their owning protocol, notably -`KAPTAN_CONFIG_RESULT`. `KAPTAN_SHUTDOWN` has only its typed reason and is -valid only after an orderly output-inhibited completion. Delivery is bounded -and lossy under link pressure; heartbeat and safety telemetry use separate -paths. +The derived error path is intentionally not a numeric error catalog; renaming +its Rust module/type/variant changes that identity. Error details requiring +machine action stay in their owning protocol, notably `KAPTAN_CONFIG_RESULT`. +`KAPTAN_SHUTDOWN` has only its typed reason and is valid only after an orderly +output-inhibited completion. Delivery is bounded and lossy under link pressure; +heartbeat and safety telemetry use separate paths. ## Filtering diff --git a/docs/src/mavlink.md b/docs/src/mavlink.md index 4c07057..4023913 100644 --- a/docs/src/mavlink.md +++ b/docs/src/mavlink.md @@ -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 later service turn. -The custom Kaptan configuration and log definitions are aggregated by -`definitions/kaptan.xml` from `kaptan/config.xml` and `kaptan/log.xml`. +The custom Kaptan definitions are aggregated by `definitions/kaptan.xml` from +`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` is its separate bounded error channel, while `KAPTAN_SHUTDOWN` represents only a completed orderly output-inhibited shutdown. Configuration messages are GCS diff --git a/docs/src/reference.md b/docs/src/reference.md index 043edff..554580b 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -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. | | 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. | -| 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. | Read [Configuration](configuration.md) for the field/consumer matrix and GCS diff --git a/libs/kaptan-config-mavlink/src/protocol.rs b/libs/kaptan-config-mavlink/src/protocol.rs index 77fe003..99d3158 100644 --- a/libs/kaptan-config-mavlink/src/protocol.rs +++ b/libs/kaptan-config-mavlink/src/protocol.rs @@ -180,9 +180,10 @@ mod tests { let record = hub.peek().expect("rejection is retained"); assert_eq!(record.channel(), RecordChannel::Error); assert_eq!( - record.message().as_str(), + record.error_path().unwrap().as_str(), "kaptan_config_mavlink::protocol::ProtocolError::BadState" ); + assert!(record.additional_information().unwrap().is_empty()); let accepted = ConfigReply { result: ResultKind::Accepted, diff --git a/libs/kaptan-log-derive/src/lib.rs b/libs/kaptan-log-derive/src/lib.rs index d2f4b92..d94e695 100644 --- a/libs/kaptan-log-derive/src/lib.rs +++ b/libs/kaptan-log-derive/src/lib.rs @@ -47,15 +47,15 @@ fn expand(input: DeriveInput) -> Result { }; quote! { #pattern => { - static MESSAGE: ::kaptan_log::StaticErrorMessage = - ::kaptan_log::StaticErrorMessage::new(concat!( + static ERROR: ::kaptan_log::StaticError = + ::kaptan_log::StaticError::new(concat!( module_path!(), "::", stringify!(#ident), "::", stringify!(#variant_ident), )); - &MESSAGE + &ERROR } } }); @@ -70,7 +70,7 @@ fn expand(input: DeriveInput) -> Result { Ok(quote! { 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 { #(#arms),* } @@ -83,7 +83,7 @@ fn expand(input: DeriveInput) -> Result { formatter: &mut ::core::fmt::Formatter<'_>, ) -> ::core::fmt::Result { formatter.write_str( - ::error_message(self).as_str(), + ::static_error(self).path(), ) } } diff --git a/libs/kaptan-log-derive/tests/ui/enum.rs b/libs/kaptan-log-derive/tests/ui/enum.rs index 6d88687..96b21d5 100644 --- a/libs/kaptan-log-derive/tests/ui/enum.rs +++ b/libs/kaptan-log-derive/tests/ui/enum.rs @@ -2,20 +2,30 @@ extern crate self as kaptan_log; 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 { - Self(text) + Self { + path: text, + additional_information: "", + } } - pub const fn as_str(&self) -> &'static str { - self.0 + pub const fn path(&self) -> &'static str { + self.path + } + + pub const fn additional_information(&self) -> &'static str { + self.additional_information } } pub trait ErrorReport { - fn error_message(&self) -> &'static StaticErrorMessage; + fn static_error(&self) -> &'static StaticError; } #[derive(Debug, KaptanError)] @@ -32,11 +42,11 @@ fn main() { fn assert_error() {} assert_error::>(); assert_eq!( - Generic::::Unit.error_message().as_str(), + Generic::::Unit.static_error().path(), concat!(module_path!(), "::Generic::Unit"), ); assert_eq!( - Generic::Tuple(1_u8, 2).error_message().as_str(), + Generic::Tuple(1_u8, 2).static_error().path(), concat!(module_path!(), "::Generic::Tuple"), ); let named: Generic<'_, u8> = Generic::Named { label: "test" }; @@ -44,4 +54,9 @@ fn main() { named.to_string(), concat!(module_path!(), "::Generic::Named"), ); + assert_eq!( + named.static_error().additional_information(), + "", + "derives emit identity only; callers own optional context", + ); } diff --git a/libs/kaptan-log-mavlink/src/lib.rs b/libs/kaptan-log-mavlink/src/lib.rs index e17c643..0ef82ba 100644 --- a/libs/kaptan-log-mavlink/src/lib.rs +++ b/libs/kaptan-log-mavlink/src/lib.rs @@ -9,7 +9,7 @@ #[cfg(test)] extern crate std; -use kaptan_log::{LogRecord, RecordChannel, ShutdownReason, ShutdownReport}; +use kaptan_log::{LogRecord, LogText, RecordChannel, ShutdownReason, ShutdownReport}; use kaptan_mavlink::message::kaptan::{ KAPTAN_ERROR_DATA, KAPTAN_LOG_DATA, KAPTAN_SHUTDOWN_DATA, KaptanErrorFlags, KaptanLogFlags, KaptanShutdownReason, @@ -17,6 +17,10 @@ use kaptan_mavlink::message::kaptan::{ /// Exact capacity of the fixed text field in [`KAPTAN_LOG_DATA`]. 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 /// MAVLink framing, admission, DMA, and retry owner. @@ -34,23 +38,61 @@ pub enum DiagnosticPayload { /// MAVLink. `RecordChannel` is the only routing decision: ordinary records /// become `KAPTAN_LOG`, while error-or-higher records become `KAPTAN_ERROR`. pub fn encode_diagnostic(record: &LogRecord) -> DiagnosticPayload { - let message = record.message(); - debug_assert!(usize::from(message.len()) <= LOG_MESSAGE_BYTES); match record.channel() { - RecordChannel::Log => DiagnosticPayload::Log(KAPTAN_LOG_DATA { - level: record.severity() as u8, - flags: KaptanLogFlags::from_bits_retain(record.flags().bits()), - message_length: message.len(), - message: *message.bytes(), - }), - RecordChannel::Error => DiagnosticPayload::Error(KAPTAN_ERROR_DATA { - flags: KaptanErrorFlags::from_bits_retain(record.flags().error_bits()), - message_length: message.len(), - message: *message.bytes(), - }), + RecordChannel::Log => { + let message = record + .message() + .expect("log channel must contain ordinary message text"); + debug_assert!(usize::from(message.len()) <= LOG_MESSAGE_BYTES); + DiagnosticPayload::Log(KAPTAN_LOG_DATA { + level: record.severity() as u8, + flags: KaptanLogFlags::from_bits_retain(record.flags().bits()), + message_length: message.len(), + 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::(path); + let (additional_information, additional_information_length, information_clipped) = + project_text::(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(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 /// to report; this pure adapter cannot run a shutdown state machine. 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 { 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::{ - EventHub, FacadeIngress, GlobalLogger, LogFilter, LogFlags, LogRecord, LogText, Severity, - ShutdownReason, ShutdownReport, + ErrorAdditionalInformation, ErrorPath, EventHub, FacadeIngress, GlobalLogger, LogFilter, + LogFlags, LogRecord, LogText, Severity, ShutdownReason, ShutdownReport, }; use kaptan_mavlink::{ encoder::MavlinkEncoder, @@ -123,8 +168,12 @@ mod tests { panic!("log::error! must not produce KAPTAN_LOG"); }; assert!( - payload.message[..usize::from(payload.message_length)] - .ends_with(b"critical error survives") + payload.path[..usize::from(payload.path_length)] + .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); - let error = LogRecord::new( - Severity::Error, + let error = LogRecord::error( 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 { panic!("error record must use KAPTAN_ERROR"); }; assert_eq!(error_data.flags.bits(), LogFlags::COALESCED.bits()); - assert_eq!(error_data.message_length, 23); - assert_eq!(KAPTAN_ERROR_DATA::ENCODED_LEN, 162); + assert_eq!(error_data.path_length, 23); + 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); assert_eq!(error_frame.message_id(), 51_021); + assert!(error_frame.raw_bytes().len() <= 175); assert!(matches!( MavMessage::parse( 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] fn typed_shutdown_maps_to_its_sole_generated_message() { let data = encode_shutdown(ShutdownReport::new(ShutdownReason::Safety)); diff --git a/libs/kaptan-log/src/crash.rs b/libs/kaptan-log/src/crash.rs index 2df8db8..17da920 100644 --- a/libs/kaptan-log/src/crash.rs +++ b/libs/kaptan-log/src/crash.rs @@ -177,7 +177,7 @@ mod tests { #[test] fn validation_error_has_a_static_derived_message() { assert_eq!( - CrashRecordError::BadChecksum.error_message().as_str(), + CrashRecordError::BadChecksum.static_error().path(), "kaptan_log::crash::CrashRecordError::BadChecksum" ); } diff --git a/libs/kaptan-log/src/facade.rs b/libs/kaptan-log/src/facade.rs index cb3bbff..ff43f6c 100644 --- a/libs/kaptan-log/src/facade.rs +++ b/libs/kaptan-log/src/facade.rs @@ -11,7 +11,10 @@ use core::{ 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. /// @@ -80,7 +83,30 @@ impl FacadeCounters { struct IngressRecord { severity: Severity, 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 { @@ -206,7 +232,15 @@ impl FacadeIngress { Some(None) => self.with_queue(IngressQueue::pop).flatten()?, 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( @@ -303,14 +337,30 @@ impl Log for GlobalLogger { } else { LogFlags::NONE }; - let text = LogText::from_arguments(format_args!("[{}] {}", record.target(), record.args())); - if text.is_truncated() { + let body = if severity <= Severity::Error { + 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); } self.ingress.push(IngressRecord { severity, flags, - text, + body, }); } @@ -385,7 +435,11 @@ mod tests { let mut hub = EventHub::<1, 1>::new(LogFilter::default()); assert!(INGRESS.drain_one_into(&mut hub).is_some()); 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.channel(), RecordChannel::Error); } @@ -408,6 +462,7 @@ mod tests { hub.peek() .unwrap() .message() + .unwrap() .as_str() .ends_with("armed configuration loaded") ); @@ -505,7 +560,8 @@ mod tests { assert!(INGRESS.drain_one_into(&mut hub).is_some()); let error = hub.peek().expect("error retained ahead of optional text"); 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); } } diff --git a/libs/kaptan-log/src/hub.rs b/libs/kaptan-log/src/hub.rs index 9b8c171..cb2516b 100644 --- a/libs/kaptan-log/src/hub.rs +++ b/libs/kaptan-log/src/hub.rs @@ -3,8 +3,8 @@ use core::fmt; use crate::{ - DeliveryClass, ErrorReport, LogFilter, LogFlags, LogRecord, LogText, Severity, - StaticErrorMessage, + DeliveryClass, ErrorAdditionalInformation, ErrorPath, ErrorReport, LogFilter, LogFlags, + LogRecord, LogText, Severity, StaticError, }; /// Outcome from one bounded diagnostic admission attempt. @@ -164,7 +164,7 @@ impl EventHub(&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 @@ -172,15 +172,27 @@ impl EventHub PublishOutcome { - self.publish_text( - Severity::Error, + self.publish_error_text( 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 /// severities are independently retained and sent as `KAPTAN_ERROR`. pub fn publish_text( @@ -189,16 +201,16 @@ impl EventHub PublishOutcome { - let delivery = if severity <= Severity::Error { - DeliveryClass::Critical - } else { - DeliveryClass::BestEffort - }; + let delivery = delivery_for(severity); if delivery == DeliveryClass::BestEffort && !self.filter.allows(severity) { HubHealth::increment(&mut self.health.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 { DeliveryClass::Critical => self.critical.publish(record, false), DeliveryClass::BestEffort => self.best_effort.publish(record, true), @@ -250,11 +262,20 @@ impl EventHub DeliveryClass { + if severity <= Severity::Error { + DeliveryClass::Critical + } else { + DeliveryClass::BestEffort + } +} + #[cfg(test)] mod tests { use super::{EventHub, PublishOutcome, TransportAdmission}; use crate::{ - DeliveryClass, LogFilter, LogFlags, LogText, RecordChannel, Severity, StaticErrorMessage, + DeliveryClass, ErrorAdditionalInformation, ErrorPath, LogFilter, LogFlags, LogText, + RecordChannel, Severity, StaticError, }; #[test] @@ -277,13 +298,14 @@ mod tests { PublishOutcome::Queued ); 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); } #[test] 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()); assert_eq!( hub.publish_error_static(LogFlags::NONE, &FAULT), @@ -296,6 +318,45 @@ mod tests { let record = hub.peek().unwrap(); assert_eq!(record.occurrences, 2); 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] diff --git a/libs/kaptan-log/src/lib.rs b/libs/kaptan-log/src/lib.rs index 023837e..0fc7414 100644 --- a/libs/kaptan-log/src/lib.rs +++ b/libs/kaptan-log/src/lib.rs @@ -29,11 +29,9 @@ pub use hub::{EventHub, HubHealth, PublishOutcome, TransportAdmission}; pub use kaptan_log_derive::KaptanError; pub use level::Severity; pub use mailbox::StaticErrorMailbox; -pub use report::{ - DeliveryClass, ErrorReport, LogFlags, LogRecord, RecordChannel, StaticErrorMessage, -}; +pub use report::{DeliveryClass, ErrorReport, LogFlags, LogRecord, RecordChannel, StaticError}; 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")] pub use facade::FacadeHealth; diff --git a/libs/kaptan-log/src/mailbox.rs b/libs/kaptan-log/src/mailbox.rs index aeb4d15..c1f04a7 100644 --- a/libs/kaptan-log/src/mailbox.rs +++ b/libs/kaptan-log/src/mailbox.rs @@ -5,7 +5,7 @@ use core::{ sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering}, }; -use crate::StaticErrorMessage; +use crate::StaticError; /// 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 /// worker drains the exact message pointer and flag later. pub struct StaticErrorMailbox { - message: AtomicPtr, + error: AtomicPtr, coalesced: AtomicBool, coalesced_total: AtomicU32, } @@ -22,19 +22,19 @@ impl StaticErrorMailbox { /// Creates an empty mailbox. pub const fn new() -> Self { Self { - message: AtomicPtr::new(ptr::null_mut()), + error: AtomicPtr::new(ptr::null_mut()), coalesced: AtomicBool::new(false), coalesced_total: AtomicU32::new(0), } } /// 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 - .message + .error .compare_exchange( ptr::null_mut(), - core::ptr::from_ref(message).cast_mut(), + core::ptr::from_ref(error).cast_mut(), Ordering::AcqRel, Ordering::Acquire, ) @@ -46,13 +46,13 @@ impl StaticErrorMailbox { } /// Removes the retained first message and its coalesced marker. - pub fn take(&self) -> (Option<&'static StaticErrorMessage>, bool) { - let message = self.message.swap(ptr::null_mut(), Ordering::AcqRel); + pub fn take(&self) -> (Option<&'static StaticError>, bool) { + let error = self.error.swap(ptr::null_mut(), Ordering::AcqRel); let coalesced = self.coalesced.swap(false, Ordering::AcqRel); // SAFETY: `record` stores only pointers to static messages and the // atomic swap transfers the exact pointer to this one worker. - let message = unsafe { message.as_ref() }; - (message, coalesced) + let error = unsafe { error.as_ref() }; + (error, coalesced) } /// Total later-fault coalesces since startup. This is local producer @@ -81,12 +81,12 @@ impl Default for StaticErrorMailbox { #[cfg(test)] mod tests { use super::StaticErrorMailbox; - use crate::StaticErrorMessage; + use crate::StaticError; #[test] fn source_mailbox_keeps_the_first_static_message_and_marks_later_faults() { - static FIRST: StaticErrorMessage = StaticErrorMessage::new("first"); - static SECOND: StaticErrorMessage = StaticErrorMessage::new("second"); + static FIRST: StaticError = StaticError::new("first"); + static SECOND: StaticError = StaticError::new("second"); let mailbox = StaticErrorMailbox::new(); mailbox.record(&FIRST); mailbox.record(&SECOND); diff --git a/libs/kaptan-log/src/report.rs b/libs/kaptan-log/src/report.rs index bc20a86..c37aa9d 100644 --- a/libs/kaptan-log/src/report.rs +++ b/libs/kaptan-log/src/report.rs @@ -1,37 +1,52 @@ //! 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)] -pub struct StaticErrorMessage { - text: &'static str, +pub struct StaticError { + path: &'static str, + additional_information: &'static str, } -impl StaticErrorMessage { - /// Creates one compile-time error message. - pub const fn new(text: &'static str) -> Self { - Self { text } +impl StaticError { + /// Creates one compile-time error identity without additional detail. + pub const fn new(path: &'static str) -> Self { + Self { + path, + additional_information: "", + } } - /// Stable human-readable message text. - pub const fn text(self) -> &'static str { - self.text + /// Creates one compile-time error identity with static additional detail. + pub const fn with_additional_information( + path: &'static str, + additional_information: &'static str, + ) -> Self { + Self { + path, + additional_information, + } } - /// Alias used by the error derive's [`core::fmt::Display`] implementation. - pub const fn as_str(self) -> &'static str { - self.text + /// Stable human-readable error identity. + pub const fn path(self) -> &'static str { + self.path + } + + /// Optional static human-facing detail. + pub const fn additional_information(self) -> &'static str { + self.additional_information } } /// 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. pub trait ErrorReport { - /// Returns the static type/variant message for this error value. - fn error_message(&self) -> &'static StaticErrorMessage; + /// Returns static identity and optional detail for this error value. + fn static_error(&self) -> &'static StaticError; } /// Bounded MAVLink log flags. @@ -81,6 +96,17 @@ pub enum RecordChannel { 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. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum DeliveryClass { @@ -95,36 +121,58 @@ pub enum DeliveryClass { pub struct LogRecord { severity: Severity, flags: LogFlags, - message: LogText, - channel: RecordChannel, - /// Adjacent identical messages retained as one record. + body: RecordBody, + /// Adjacent equivalent diagnostics retained as one record. pub occurrences: u32, - delivery: DeliveryClass, } impl LogRecord { - /// Creates one bounded record. Error-or-higher severities use - /// [`RecordChannel::Error`]; ordinary severity records use - /// [`RecordChannel::Log`]. - pub fn new(severity: Severity, mut flags: LogFlags, message: LogText) -> Self { - if message.is_truncated() { - flags = flags.with(LogFlags::TRUNCATED); + /// Creates one bounded diagnostic record. Error-or-higher severities use + /// their supplied text as the error path with no additional information; + /// ordinary severity records use `KAPTAN_LOG` message text. + pub fn new(severity: Severity, flags: LogFlags, message: LogText) -> Self { + if severity <= Severity::Error { + let flags = with_truncation(flags, message); + return Self::error_with_severity( + severity, + flags, + ErrorPath::from_message(message.as_str()), + ErrorAdditionalInformation::new(), + ); } Self { severity, - flags, - message, - channel: if severity <= Severity::Error { - RecordChannel::Error - } else { - RecordChannel::Log + flags: with_truncation(flags, message), + body: RecordBody::Log { message }, + occurrences: 1, + } + } + + /// 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, - delivery: if severity <= Severity::Error { - DeliveryClass::Critical - } else { - DeliveryClass::BestEffort - }, } } @@ -138,29 +186,53 @@ impl LogRecord { self.flags } - /// Fixed formatted text. - pub const fn message(&self) -> &LogText { - &self.message + /// Fixed text carried by an ordinary `KAPTAN_LOG` record. + pub const fn message(&self) -> Option<&LogText> { + 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. pub const fn channel(&self) -> RecordChannel { - self.channel + match self.body { + RecordBody::Log { .. } => RecordChannel::Log, + RecordBody::Error { .. } => RecordChannel::Error, + } } /// Internal admission priority. 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 - /// message, severity, or delivery class. + /// payload, severity, or delivery class. pub fn is_equivalent_to(&self, other: &Self) -> bool { - self.channel == other.channel - && self.severity == other.severity - && self.flags == other.flags - && self.message == other.message - && self.delivery == other.delivery + self.severity == other.severity && self.flags == other.flags && self.body == other.body } pub(crate) fn increment_occurrences(&mut self) { @@ -168,3 +240,21 @@ impl LogRecord { 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 +} diff --git a/libs/kaptan-log/src/text.rs b/libs/kaptan-log/src/text.rs index 8df9305..ee5c4ee 100644 --- a/libs/kaptan-log/src/text.rs +++ b/libs/kaptan-log/src/text.rs @@ -1,11 +1,11 @@ -//! Fixed-capacity UTF-8 log text. +//! Fixed-capacity UTF-8 diagnostic text. 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; -/// Bounded formatted text with an explicit truncation marker. +/// Fixed UTF-8 diagnostic text with an explicit truncation marker. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct LogText { bytes: [u8; MAXIMUM_LOG_TEXT_BYTES], @@ -13,8 +13,15 @@ pub struct LogText { 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 { - /// Creates an empty bounded log message. + /// Creates empty fixed text storage. pub const fn new() -> Self { Self { bytes: [0; MAXIMUM_LOG_TEXT_BYTES], diff --git a/libs/kaptan-mavlink/build.rs b/libs/kaptan-mavlink/build.rs index d6c314c..8031f84 100644 --- a/libs/kaptan-mavlink/build.rs +++ b/libs/kaptan-mavlink/build.rs @@ -5,7 +5,9 @@ fn main() { let dialect = manifest_directory.join("definitions/kaptan.xml"); let config = manifest_directory.join("definitions/kaptan/config.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::rustc-check-cfg=cfg(kaptan_has_messages)"); diff --git a/libs/kaptan-mavlink/definitions/kaptan.xml b/libs/kaptan-mavlink/definitions/kaptan.xml index 71e12bf..54b3f24 100644 --- a/libs/kaptan-mavlink/definitions/kaptan.xml +++ b/libs/kaptan-mavlink/definitions/kaptan.xml @@ -4,4 +4,6 @@ fragments so their message catalogs can evolve without a monolith. --> kaptan/config.xml kaptan/log.xml + kaptan/error.xml + kaptan/shutdown.xml diff --git a/libs/kaptan-mavlink/definitions/kaptan/error.xml b/libs/kaptan-mavlink/definitions/kaptan/error.xml new file mode 100644 index 0000000..c7e4c71 --- /dev/null +++ b/libs/kaptan-mavlink/definitions/kaptan/error.xml @@ -0,0 +1,26 @@ + + + 3 + 0 + + + + + The path or additional-information UTF-8 prefix was truncated to its bounded on-wire field. + + + Error represents repeated adjacent observations with the same path and additional information. + + + + + + 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. + Known bits are TRUNCATED=2 and COALESCED=4. Receivers ignore unknown future bits. + Number of valid UTF-8 bytes in path; no more than 96. + Number of valid UTF-8 bytes in additional_information; no more than 64. + Bounded UTF-8 derived or explicit error path. Bytes after path_length are zero. + Bounded UTF-8 optional error context. Bytes after additional_information_length are zero. + + + diff --git a/libs/kaptan-mavlink/definitions/kaptan/log.xml b/libs/kaptan-mavlink/definitions/kaptan/log.xml index 063c7e6..70da5d9 100644 --- a/libs/kaptan-mavlink/definitions/kaptan/log.xml +++ b/libs/kaptan-mavlink/definitions/kaptan/log.xml @@ -15,22 +15,6 @@ Message represents repeated adjacent observations. - - - Error display text was truncated to the bounded on-wire buffer. - - - Error represents repeated adjacent observations. - - - - - An operator initiated the completed controlled firmware shutdown. - - - The safety owner initiated the completed controlled firmware shutdown. - - @@ -40,15 +24,5 @@ Number of valid UTF-8 bytes in message; at most 160. Bounded UTF-8 message bytes. Bytes after message_length are zero. - - 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. - Known bits are TRUNCATED=2 and COALESCED=4. Receivers ignore unknown future bits. - Number of valid UTF-8 bytes in message; at most 160. - Bounded UTF-8 error display identity. Bytes after message_length are zero. - - - 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. - Typed shutdown reason. - diff --git a/libs/kaptan-mavlink/definitions/kaptan/shutdown.xml b/libs/kaptan-mavlink/definitions/kaptan/shutdown.xml new file mode 100644 index 0000000..a269f90 --- /dev/null +++ b/libs/kaptan-mavlink/definitions/kaptan/shutdown.xml @@ -0,0 +1,22 @@ + + + 3 + 0 + + + + + An operator initiated the completed controlled firmware shutdown. + + + The safety owner initiated the completed controlled firmware shutdown. + + + + + + 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. + Typed shutdown reason. + + + diff --git a/platforms/stm32f7/src/f722/board.rs b/platforms/stm32f7/src/f722/board.rs index bcb2291..259dbcc 100644 --- a/platforms/stm32f7/src/f722/board.rs +++ b/platforms/stm32f7/src/f722/board.rs @@ -18,8 +18,8 @@ use kaptan_drivers::{ mtf01::Mtf01Service, }; use kaptan_log::{ - DeliveryClass, ErrorReport, EventHub, KaptanError, LogFilter, LogFlags, StaticErrorMailbox, - StaticErrorMessage, TransportAdmission, + DeliveryClass, ErrorReport, EventHub, KaptanError, LogFilter, LogFlags, StaticError, + StaticErrorMailbox, TransportAdmission, }; use kaptan_log_mavlink::{DiagnosticPayload, encode_diagnostic}; use kaptan_spi::SpiCompletion; @@ -54,11 +54,11 @@ const KAPTAN_LOG_FRAME_MAXIMUM_BYTES: usize = kaptan_mavlink::message::kaptan::KAPTAN_LOG_DATA::ENCODED_LEN + MAVLINK_V2_UNSIGNED_OVERHEAD_BYTES; // 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 -// are respectively 162 and 163 payload bytes plus 12 unsigned MAVLink V2 -// framing bytes, so 62×48 + 10×174 + 5×175 = 5,591 bytes/s. MAVLink signing -// is not enabled in this telemetry-only image; any future signing profile -// must re-budget its signature overhead before it can enable the endpoint. +// It permits ten `KAPTAN_ERROR` and five `KAPTAN_LOG` frames/s. Both maxima +// are 163 payload bytes plus 12 unsigned MAVLink V2 framing bytes, so +// 62×48 + 15×175 = 5,601 bytes/s. MAVLink signing is not enabled in this +// telemetry-only image; any future signing profile must re-budget its +// signature overhead before it can enable the endpoint. pub(crate) const WORST_CASE_OBSERVABILITY_BYTES_PER_SECOND: usize = SCHEDULED_FRAMES_PER_SECOND * SCHEDULED_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) { } ReceiveInterrupt::Fault => { mtf01.reset(); - record_mtf01_fault(F722TelemetryError::Mtf01Uart.error_message()); + record_mtf01_fault(F722TelemetryError::Mtf01Uart.static_error()); } ReceiveInterrupt::NotPending => {} } @@ -455,7 +455,7 @@ impl F722LidarState { if self.startup_ticks == LIDAR_STARTUP_TICKS && let Err(error) = range.finish_startup() { - record_lidar_fault(error.error_message()); + record_lidar_fault(error.static_error()); } } else { publish_lidar_event(range.on_periodic_tick()); @@ -542,13 +542,13 @@ impl F722MavlinkState { while let Some(fault) = self.data_channel.take_transport_fault() { let message = match fault { kaptan_mavlink::transmitter::TransmitFaultKind::Memory => { - F722TelemetryError::TransmitMemory.error_message() + F722TelemetryError::TransmitMemory.static_error() } kaptan_mavlink::transmitter::TransmitFaultKind::Engine => { - F722TelemetryError::TransmitEngine.error_message() + F722TelemetryError::TransmitEngine.static_error() } kaptan_mavlink::transmitter::TransmitFaultKind::Terminal => { - F722TelemetryError::TransmitTerminal.error_message() + F722TelemetryError::TransmitTerminal.static_error() } }; 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(event: LidarLiteV3ServiceEvent) { match event { 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 => {} } } -fn record_imu_fault(message: &'static StaticErrorMessage) { +fn record_imu_fault(message: &'static StaticError) { IMU_FAULTS.record(message); } -fn record_lidar_fault(message: &'static StaticErrorMessage) { +fn record_lidar_fault(message: &'static StaticError) { LIDAR_FAULTS.record(message); } -fn record_mtf01_fault(message: &'static StaticErrorMessage) { +fn record_mtf01_fault(message: &'static StaticError) { MTF01_FAULTS.record(message); } diff --git a/platforms/stm32f7/src/f722/sensors.rs b/platforms/stm32f7/src/f722/sensors.rs index f85adcc..a971735 100644 --- a/platforms/stm32f7/src/f722/sensors.rs +++ b/platforms/stm32f7/src/f722/sensors.rs @@ -9,7 +9,7 @@ use kaptan_drivers::{ Mpu6000Service, Mpu6000ServiceEvent, }, }; -use kaptan_log::{ErrorReport, StaticErrorMessage}; +use kaptan_log::{ErrorReport, StaticError}; use kaptan_sensor::imu::ImuSnapshot; use kaptan_spi::SpiCompletion; use stm32f7::stm32f722; @@ -31,7 +31,7 @@ pub(crate) enum ImuEvent { Idle, Initialized, Sample(ImuSnapshot), - Fault(&'static StaticErrorMessage), + Fault(&'static StaticError), } /// Opaque owner of the selected F722 IMU service. @@ -96,11 +96,11 @@ impl F722Imu { match &mut self.inner { F722ImuKind::Mpu6000(imu) => match imu.begin() { Ok(()) => ImuEvent::Idle, - Err(error) => ImuEvent::Fault(error.error_message()), + Err(error) => ImuEvent::Fault(error.static_error()), }, F722ImuKind::Icm42688(imu) => match imu.begin() { Ok(()) => ImuEvent::Idle, - Err(error) => ImuEvent::Fault(error.error_message()), + Err(error) => ImuEvent::Fault(error.static_error()), }, } } @@ -134,7 +134,7 @@ fn mpu_event(event: Mpu6000ServiceEvent) -> ImuEvent { Mpu6000ServiceEvent::Idle => ImuEvent::Idle, Mpu6000ServiceEvent::Initialized => ImuEvent::Initialized, 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(event: Icm42688ServiceEvent, mount: ImuMount) -> Icm42688ServiceEvent::Idle => ImuEvent::Idle, Icm42688ServiceEvent::Initialized => ImuEvent::Initialized, 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()), } } diff --git a/platforms/stm32f7/tests/platform_safety_boundary.rs b/platforms/stm32f7/tests/platform_safety_boundary.rs index 9e553ee..111300a 100644 --- a/platforms/stm32f7/tests/platform_safety_boundary.rs +++ b/platforms/stm32f7/tests/platform_safety_boundary.rs @@ -4,7 +4,7 @@ use kaptan_board::{ BoardConfig, BoardProfile, DataChannelRates, ImuDevice, ImuMount, MavlinkEndpoint, MavlinkPort, 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_mavlink::encoder::MavlinkEncoder; use platform_stm32f7::{ @@ -214,7 +214,11 @@ fn telemetry_has_no_inbound_command_authority() { fn f722_link_budget_uses_both_maximum_generated_diagnostic_frames() { let text = LogText::from_message(&"x".repeat(160)); 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 DiagnosticPayload::Log(log_payload) = encode_diagnostic(&log_record) else { 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, 163 ); - assert_eq!(error_frame.raw_bytes().len(), 174); + assert_eq!(error_frame.raw_bytes().len(), 175); assert_eq!( 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!( 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"); assert!(board.contains("KAPTAN_ERROR_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")); } diff --git a/tools/sitl/src/config/tests.rs b/tools/sitl/src/config/tests.rs index 52fcbd9..bae4f22 100644 --- a/tools/sitl/src/config/tests.rs +++ b/tools/sitl/src/config/tests.rs @@ -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 { panic!("rejected configuration result must enqueue KAPTAN_ERROR"); }; - let message = std::str::from_utf8(&payload.message[..usize::from(payload.message_length)]) - .expect("derived error identity remains UTF-8"); + let error_path = std::str::from_utf8(&payload.path[..usize::from(payload.path_length)]) + .expect("derived error path remains UTF-8"); assert_eq!( - message, + error_path, "kaptan_config_mavlink::protocol::ProtocolError::GenerationConflict" ); + assert_eq!(payload.additional_information_length, 0); let _ = fs::remove_file(&path); }