feat(diagnostics): split error and shutdown messages
This commit is contained in:
1 parent
9646451645
commit
afafd126d7
25 files changed
+604
-221
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -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,21 +38,59 @@ 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 => {
|
||||||
|
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,
|
level: record.severity() as u8,
|
||||||
flags: KaptanLogFlags::from_bits_retain(record.flags().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 => DiagnosticPayload::Error(KAPTAN_ERROR_DATA {
|
|
||||||
flags: KaptanErrorFlags::from_bits_retain(record.flags().error_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::<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
|
||||||
@@ -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));
|
||||||
|
|||||||
@@ -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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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]
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
@@ -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],
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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"));
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user