refactor(firmware): centralize configuration and diagnostics

This commit is contained in:
Onur Özkan committed 2026-08-22 18:37:18 +03:00
1 parent 7048ed89d0
commit 387fdaad23
140 files changed
+6891 -4541

No files matched your search

@@ -8,11 +8,6 @@ version.workspace = true
[lints]
workspace = true
[features]
# Explicitly opt-in host/SITL test authority. Production cannot construct an
# authorization token from a CRC-valid raw MAVLink frame.
host-authority = []
[dependencies]
kaptan-config = { path = "../kaptan-config" }
kaptan-log = { path = "../kaptan-log" }
@@ -1,4 +1,4 @@
//! Authorized, transport-neutral configuration commands.
//! Transport-neutral configuration commands.
use kaptan_config::{ConfigValue, FieldId};
@@ -11,6 +11,30 @@ pub struct Target {
component: u8,
}
/// Exact MAVLink endpoint that owns a configuration transaction lease.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Principal(u16);
impl Principal {
/// Creates a principal from one exact source system/component pair.
pub const fn new(system: u8, component: u8) -> Self {
assert!(
system != 0 && component != 0,
"configuration principal must be exact"
);
Self(u16::from_be_bytes([system, component]))
}
/// Parses an exact source principal from an untrusted transport.
pub const fn try_new(system: u8, component: u8) -> Option<Self> {
if system == 0 || component == 0 {
None
} else {
Some(Self(u16::from_be_bytes([system, component])))
}
}
}
/// Selects the configuration snapshot read by a value request.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ValueSource {
@@ -45,21 +69,30 @@ impl Target {
/// One command accepted by the Kaptan configuration microservice.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ConfigCommand {
/// Reads protocol/schema metadata before any descriptor or upload request.
/// Reads protocol/schema metadata before descriptor and value requests.
InfoRequest {
/// Client correlation identity.
request_id: u32,
/// Exact addressed endpoint.
target: Target,
},
/// Enumerates a bounded page of schema descriptors.
/// Reads one schema descriptor by stable index.
DescriptorRequest {
/// Client correlation identity.
request_id: u32,
/// First stable descriptor index.
first: u16,
/// Requested number of descriptors, clamped to the protocol bound.
count: u16,
/// Stable descriptor index.
index: u16,
/// Exact addressed endpoint.
target: Target,
},
/// Reads one named choice for one descriptor.
EnumOptionRequest {
/// Client correlation identity.
request_id: u32,
/// Stable index of the owning descriptor.
descriptor_index: u16,
/// Stable enum-option index.
option_index: u8,
/// Exact addressed endpoint.
target: Target,
},
@@ -76,59 +109,10 @@ pub enum ConfigCommand {
/// Exact addressed endpoint.
target: Target,
},
/// Starts one bounded CBOR snapshot import into the caller's transaction.
ImportBegin {
/// Client correlation identity.
request_id: u32,
/// Lease-bound transaction identity.
session_id: u32,
/// Optimistic transaction revision.
expected_revision: u32,
/// Complete CBOR byte length.
length: u16,
/// Exact schema identifier advertised by `KAPTAN_CONFIG_INFO`.
schema_id: [u8; 16],
/// CRC-32C over exactly `length` CBOR bytes.
crc32c: u32,
/// Exact addressed endpoint.
target: Target,
},
/// Supplies one fixed-bounded CBOR import chunk.
ImportChunk {
/// Client correlation identity.
request_id: u32,
/// Lease-bound transaction identity.
session_id: u32,
/// Server-issued ID returned by the matching import-begin result.
transfer_id: u32,
/// Byte offset into the complete import.
offset: u16,
/// Used bytes of `data`.
length: u8,
/// Fixed MAVLink payload data.
data: [u8; 192],
/// Exact addressed endpoint.
target: Target,
},
/// Checks the completed import and replaces only the RAM transaction.
ImportFinish {
/// Client correlation identity.
request_id: u32,
/// Lease-bound transaction identity.
session_id: u32,
/// Server-issued ID returned by the matching import-begin result.
transfer_id: u32,
/// Optimistic transaction revision.
expected_revision: u32,
/// Exact addressed endpoint.
target: Target,
},
/// Starts an edit transaction from the expected committed generation.
/// Starts an edit transaction from the latest committed configuration.
Begin {
/// Client correlation identity.
request_id: u32,
/// Optimistic concurrency generation.
expected_generation: u64,
/// Exact addressed endpoint.
target: Target,
},
@@ -164,8 +148,6 @@ pub enum ConfigCommand {
request_id: u32,
/// Lease-bound transaction identity.
session_id: u32,
/// Optimistic committed generation.
expected_generation: u64,
/// Optimistic transaction revision.
expected_revision: u32,
/// Exact addressed endpoint.
@@ -188,10 +170,8 @@ impl ConfigCommand {
match self {
Self::InfoRequest { request_id, .. }
| Self::DescriptorRequest { request_id, .. }
| Self::EnumOptionRequest { request_id, .. }
| Self::ValueRequest { request_id, .. }
| Self::ImportBegin { request_id, .. }
| Self::ImportChunk { request_id, .. }
| Self::ImportFinish { request_id, .. }
| Self::Begin { request_id, .. }
| Self::Set { request_id, .. }
| Self::Validate { request_id, .. }
@@ -205,10 +185,8 @@ impl ConfigCommand {
match self {
Self::InfoRequest { target, .. }
| Self::DescriptorRequest { target, .. }
| Self::EnumOptionRequest { target, .. }
| Self::ValueRequest { target, .. }
| Self::ImportBegin { target, .. }
| Self::ImportChunk { target, .. }
| Self::ImportFinish { target, .. }
| Self::Begin { target, .. }
| Self::Set { target, .. }
| Self::Validate { target, .. }
@@ -218,54 +196,25 @@ impl ConfigCommand {
}
}
/// A capability token produced only by the future signing/replay authority.
///
/// The token carries a stable principal identity so the service can bind a
/// transaction lease to one authenticated source. It is intentionally opaque:
/// an unverified raw MAVLink frame cannot create one.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct VerifiedAuthority {
principal: u64,
_private: (),
}
impl VerifiedAuthority {
/// Returns the authenticated principal identity for ownership checks.
pub const fn principal(self) -> u64 {
self.principal
}
#[cfg(any(test, feature = "host-authority"))]
/// Creates an explicit host/SITL authority token.
///
/// This exists only for host integration and protocol tests. Flight code
/// must receive the equivalent token from the reviewed signing/replay path.
pub const fn host(principal: u64) -> Self {
Self {
principal,
_private: (),
}
}
}
/// A command whose authority was verified before it reached this adapter.
/// Test command paired with its lease principal.
#[cfg(test)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Authorized<T> {
authority: VerifiedAuthority,
principal: Principal,
command: T,
}
#[cfg(test)]
impl<T> Authorized<T> {
#[cfg(any(test, feature = "host-authority"))]
/// Wraps a host/SITL command in an explicit test-only authority token.
pub const fn host(principal: u64, command: T) -> Self {
/// Pairs a test command with component `principal` in test system 1.
pub const fn host(principal: u8, command: T) -> Self {
Self {
authority: VerifiedAuthority::host(principal),
principal: Principal::new(1, principal),
command,
}
}
pub(crate) fn into_parts(self) -> (VerifiedAuthority, T) {
(self.authority, self.command)
pub(crate) fn into_parts(self) -> (Principal, T) {
(self.principal, self.command)
}
}
+19 -4
View File
@@ -2,19 +2,34 @@
//!
//! This crate translates generated `kaptan.xml` records to the transport-
//! neutral configuration lifecycle. It does not parse raw frames, authenticate
//! peers, deserialize CBOR, access storage directly, or mutate active flight
//! peers, access storage directly, or mutate active flight
//! consumers.
#![no_std]
#[cfg(test)]
extern crate std;
mod command;
mod protocol;
pub mod service;
pub mod wire;
pub use command::{Authorized, ConfigCommand, Target, ValueSource, VerifiedAuthority};
#[cfg(test)]
pub use command::Authorized;
pub use command::{ConfigCommand, Principal, Target, ValueSource};
pub use protocol::{ConfigReply, Operation, ProtocolError, ResultKind};
pub use service::{
CAPABILITY_CBOR_IMPORT, ConfigInfo, ConfigProtocol, DescriptorPage, NoSnapshotImporter,
ServiceResponse, SnapshotImporter, ValueResponse,
ConfigInfo, ConfigProtocol, DescriptorResponse, EnumOptionResponse, ServiceResponse,
ValueResponse,
};
pub use wire::{ResponseVisitor, visit_response};
/// Wire protocol implemented by the descriptor/value configuration service.
pub const PROTOCOL_MAJOR: u8 = 2;
pub const PROTOCOL_MINOR: u8 = 0;
/// `KAPTAN_CONFIG_DESCRIPTOR.flags` bit: `minimum_bits` is present.
pub const DESCRIPTOR_HAS_MINIMUM: u8 = 1 << 0;
/// `KAPTAN_CONFIG_DESCRIPTOR.flags` bit: `maximum_bits` is present.
pub const DESCRIPTOR_HAS_MAXIMUM: u8 = 1 << 1;
@@ -1,7 +1,8 @@
//! Stable typed outcomes for the Kaptan configuration microservice.
use kaptan_config::{ConfigState, ConfigViolation, FieldId};
use kaptan_log::{EventHub, KaptanError, LogFlags, PublishOutcome};
use kaptan_log::KaptanError;
/// Operation reported in an authoritative result record.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Operation {
@@ -17,8 +18,6 @@ pub enum Operation {
Commit,
/// Transaction abort.
Abort,
/// CBOR snapshot import into the isolated transaction.
Import,
}
/// Stable result classification.
@@ -38,11 +37,7 @@ pub enum ResultKind {
pub enum ProtocolError {
/// The request target did not match this component.
BadTarget,
/// Authorization was absent or insufficient.
AuthorizationDenied,
/// The optimistic durable generation did not match.
GenerationConflict,
/// The single transaction slot is occupied.
/// The configuration lease or single transaction slot is occupied.
SessionBusy,
/// The session is unknown, expired, or belongs to a different principal.
InvalidSession,
@@ -54,22 +49,14 @@ pub enum ProtocolError {
BadState,
/// The field is not present in this schema.
UnknownField(FieldId),
/// A query requested an empty or out-of-range descriptor span.
InvalidQuery,
/// The typed setter does not match the field descriptor.
TypeMismatch(FieldId),
/// A local or complete validation issue.
Violation(ConfigViolation),
/// Persistence failed after a complete validation pass.
StorageFailure,
/// A CBOR snapshot could not be decoded into this schema.
CborMalformed,
/// A CBOR snapshot exceeded the fixed import budget.
CborTooLarge,
/// The complete CBOR snapshot did not match the supplied CRC-32C.
BlobCrc,
/// The uploaded CBOR blob was prepared for a different schema.
SchemaMismatch,
/// This deployment does not expose bounded CBOR transaction import.
ImportUnavailable,
/// The adapter reached an impossible internal state.
Internal,
}
@@ -81,12 +68,8 @@ pub struct ConfigReply {
pub request_id: u32,
/// The current session, or zero when none exists.
pub session_id: u32,
/// Server-assigned import transfer identity, or zero when not importing.
pub transfer_id: u32,
/// The transaction revision, or zero when no transaction exists.
pub revision: u32,
/// Latest committed generation, or zero for the factory profile.
pub generation: u64,
/// Completed operation.
pub operation: Operation,
/// High-level result.
@@ -130,66 +113,4 @@ impl ConfigReply {
_ => 0,
}
}
/// Enqueues a critical diagnostic companion for a rejected structured
/// result. The `KAPTAN_CONFIG_RESULT` remains the correlated,
/// field/rule-precise authority; this sends only the derive-generated
/// high-level `ProtocolError` display identity through `KAPTAN_ERROR`.
///
/// The helper deliberately carries no request/session/generation fields
/// into the error channel. A composition owner may call it after it has
/// emitted or retained the authoritative result.
pub fn publish_rejection<const CRITICAL: usize, const BEST_EFFORT: usize>(
self,
hub: &mut EventHub<CRITICAL, BEST_EFFORT>,
) -> Option<PublishOutcome> {
if self.result != ResultKind::Rejected {
return None;
}
self.error
.as_ref()
.map(|error| hub.publish_error(LogFlags::NONE, error))
}
}
#[cfg(test)]
mod tests {
use kaptan_log::{EventHub, LogFilter, PublishOutcome, RecordChannel};
use super::{ConfigReply, Operation, ProtocolError, ResultKind};
use kaptan_config::ConfigState;
#[test]
fn rejected_result_enqueues_only_its_high_level_error_identity() {
let reply = ConfigReply {
request_id: 42,
session_id: 7,
transfer_id: 0,
revision: 3,
generation: 9,
operation: Operation::Set,
result: ResultKind::Rejected,
error: Some(ProtocolError::BadState),
state: ConfigState::Editing,
};
let mut hub = EventHub::<1, 1>::new(LogFilter::default());
assert_eq!(
reply.publish_rejection(&mut hub),
Some(PublishOutcome::Queued)
);
let record = hub.peek().expect("rejection is retained");
assert_eq!(record.channel(), RecordChannel::Error);
assert_eq!(
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,
error: None,
..reply
};
assert_eq!(accepted.publish_rejection(&mut hub), None);
}
}
@@ -1,14 +1,14 @@
//! Transport-neutral façade for the custom configuration service.
//!
//! Public callers see one small protocol API here. Its bounded query,
//! session, replay, and CBOR-import mechanics stay in private sibling modules
//! session and replay mechanics stay in private sibling modules
//! so no one concern becomes a second service owner.
mod import;
mod query;
mod replay;
mod session;
pub use import::{CAPABILITY_CBOR_IMPORT, NoSnapshotImporter, SnapshotImporter};
pub use query::{ConfigInfo, DescriptorPage, ServiceResponse, ValueResponse};
pub use query::{
ConfigInfo, DescriptorResponse, EnumOptionResponse, ServiceResponse, ValueResponse,
};
pub use session::ConfigProtocol;
@@ -1,248 +0,0 @@
//! Bounded CBOR snapshot transfer state.
use kaptan_config::{CommitError, ConfigManager, ConfigModel, ProfileStore};
use super::session::{ConfigProtocol, Session};
use crate::{ConfigCommand, ConfigReply, Operation, ProtocolError, ResultKind};
/// Maximum complete CBOR snapshot accepted by the protocol.
pub(super) const IMPORT_MAXIMUM_BYTES: usize = 4096;
/// Payload bytes carried by each aligned import chunk.
pub(super) const IMPORT_CHUNK_BYTES: usize = 192;
pub(super) const IMPORT_CHUNK_COUNT: usize = IMPORT_MAXIMUM_BYTES.div_ceil(IMPORT_CHUNK_BYTES);
/// `KAPTAN_CONFIG_INFO.capability_flags`: bounded CBOR transaction import.
pub const CAPABILITY_CBOR_IMPORT: u8 = 1;
/// Decodes a complete bounded CBOR snapshot into this service's candidate.
///
/// Vehicle-specific implementations live above this transport adapter so the
/// generic MAVLink layer never owns a profile schema or board capabilities.
pub trait SnapshotImporter<C> {
/// Whether this concrete deployment exposes the bounded import path.
const IMPORT_SUPPORTED: bool;
/// Current format carried inside the deployment's raw CBOR profile blob.
const PROFILE_FORMAT: u16;
/// Converts one complete CBOR payload to a candidate. The service performs
/// the subsequent complete-context validation before replacing the RAM
/// transaction.
fn decode(&self, bytes: &[u8]) -> Result<C, ProtocolError>;
}
/// Default importer used when a deployment deliberately does not expose CBOR
/// import yet. It keeps the service type usable for descriptor-only tests.
#[derive(Clone, Copy, Debug, Default)]
pub struct NoSnapshotImporter;
impl<C> SnapshotImporter<C> for NoSnapshotImporter {
const IMPORT_SUPPORTED: bool = false;
const PROFILE_FORMAT: u16 = 0;
fn decode(&self, _: &[u8]) -> Result<C, ProtocolError> {
Err(ProtocolError::CborMalformed)
}
}
#[derive(Clone, Copy)]
pub(super) struct ImportBuffer {
owner: u64,
session_id: u32,
transfer_id: u32,
revision: u32,
length: u16,
crc32c: u32,
received: [bool; IMPORT_CHUNK_COUNT],
data: [u8; IMPORT_MAXIMUM_BYTES],
}
impl<I> ConfigProtocol<I> {
pub(super) fn begin_import<C>(
&mut self,
owner: u64,
command: ConfigCommand,
session: Session,
) -> ConfigReply
where
C: ConfigModel,
I: SnapshotImporter<C>,
{
let ConfigCommand::ImportBegin {
request_id,
length,
schema_id,
crc32c,
..
} = command
else {
return self.rejected(command, ProtocolError::Internal);
};
if !I::IMPORT_SUPPORTED {
return self.rejected(command, ProtocolError::ImportUnavailable);
}
if schema_id != C::SCHEMA_ID {
return self.rejected(command, ProtocolError::SchemaMismatch);
}
if length == 0 || usize::from(length) > IMPORT_MAXIMUM_BYTES {
return self.rejected(command, ProtocolError::CborTooLarge);
}
let Some(transfer_id) = self.allocate_transfer_id() else {
return self.rejected(command, ProtocolError::Internal);
};
self.import = Some(ImportBuffer {
owner,
session_id: session.id,
transfer_id,
revision: session.revision,
length,
crc32c,
received: [false; IMPORT_CHUNK_COUNT],
data: [0; IMPORT_MAXIMUM_BYTES],
});
let mut reply = self.reply(
request_id,
session.id,
session.revision,
Operation::Import,
ResultKind::Accepted,
None,
);
reply.transfer_id = transfer_id;
reply
}
pub(super) fn import_chunk(&mut self, command: ConfigCommand, session: Session) -> ConfigReply {
let ConfigCommand::ImportChunk {
request_id,
transfer_id,
offset,
length,
data,
..
} = command
else {
return self.rejected(command, ProtocolError::Internal);
};
let offset = usize::from(offset);
let length = usize::from(length);
let Some(import) = self.import.as_mut() else {
return self.rejected(command, ProtocolError::BadState);
};
if import.owner != session.owner
|| import.session_id != session.id
|| import.transfer_id != transfer_id
{
return self.rejected(command, ProtocolError::InvalidSession);
}
if import.revision != session.revision {
return self.rejected(command, ProtocolError::StaleRevision);
}
let total = usize::from(import.length);
let Some(end) = offset.checked_add(length) else {
return self.rejected(command, ProtocolError::CborMalformed);
};
if length == 0
|| offset % IMPORT_CHUNK_BYTES != 0
|| end > total
|| (length != IMPORT_CHUNK_BYTES && end != total)
{
return self.rejected(command, ProtocolError::CborMalformed);
}
let chunk = offset / IMPORT_CHUNK_BYTES;
if import.received[chunk] {
if import.data[offset..end] != data[..length] {
return self.rejected(command, ProtocolError::RequestConflict);
}
} else {
import.data[offset..end].copy_from_slice(&data[..length]);
import.received[chunk] = true;
}
self.reply(
request_id,
session.id,
session.revision,
Operation::Import,
ResultKind::Accepted,
None,
)
}
pub(super) fn finish_import<C, S>(
&mut self,
manager: &mut ConfigManager<C, S>,
command: ConfigCommand,
session: Session,
request_id: u32,
transfer_id: u32,
) -> ConfigReply
where
C: ConfigModel,
S: ProfileStore<C>,
I: SnapshotImporter<C>,
{
let Some(import) = self.import else {
return self.rejected(command, ProtocolError::BadState);
};
if import.owner != session.owner
|| import.session_id != session.id
|| import.transfer_id != transfer_id
{
return self.rejected(command, ProtocolError::InvalidSession);
}
if import.revision != session.revision {
return self.rejected(command, ProtocolError::StaleRevision);
}
let total = usize::from(import.length);
let chunks = total.div_ceil(IMPORT_CHUNK_BYTES);
if import.received[..chunks].iter().any(|received| !received) {
return self.rejected(command, ProtocolError::CborMalformed);
}
if crc32c(&import.data[..total]) != import.crc32c {
return self.rejected(command, ProtocolError::BlobCrc);
}
let candidate = match self.importer.decode(&import.data[..total]) {
Ok(candidate) => candidate,
Err(error) => return self.rejected(command, error),
};
let Some(next_revision) = session.revision.checked_add(1) else {
return self.rejected(command, ProtocolError::Internal);
};
match manager.replace_transaction(candidate) {
Ok(()) => {
let Some(session) = self.session.as_mut() else {
return self.rejected(command, ProtocolError::Internal);
};
session.revision = next_revision;
let (session_id, revision) = (session.id, session.revision);
self.import = None;
self.reply(
request_id,
session_id,
revision,
Operation::Import,
ResultKind::Accepted,
None,
)
}
Err(CommitError::NoTransaction) => self.rejected(command, ProtocolError::BadState),
Err(CommitError::InvalidCandidate(violation)) => {
self.rejected(command, ProtocolError::Violation(violation))
}
Err(CommitError::Store(_)) => self.rejected(command, ProtocolError::Internal),
}
}
}
pub(super) fn crc32c(bytes: &[u8]) -> u32 {
let mut crc = !0_u32;
for byte in bytes {
crc ^= u32::from(*byte);
for _ in 0..8 {
crc = if crc & 1 == 0 {
crc >> 1
} else {
(crc >> 1) ^ 0x82f6_3b78
};
}
}
!crc
}
@@ -1,24 +1,39 @@
//! Read-only protocol discovery and value-query responses.
use kaptan_config::{
ConfigManager, ConfigModel, ConfigState, ConfigValue, FieldDescriptor, FieldId, Generation,
ProfileStore,
ConfigManager, ConfigModel, ConfigState, ConfigStore, ConfigValue, EnumOption, FieldDescriptor,
FieldId,
};
use super::{import::SnapshotImporter, session::ConfigProtocol};
use crate::{ProtocolError, ValueSource};
use super::session::ConfigProtocol;
use crate::{Principal, ProtocolError, ValueSource};
/// Bounded descriptor page returned by a schema discovery request.
/// One descriptor returned by a schema discovery request.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DescriptorPage {
pub struct DescriptorResponse {
/// Request being answered.
pub request_id: u32,
/// First requested descriptor index.
pub first: u16,
/// Number of descriptors present in `descriptors`.
pub len: u8,
/// Fixed-capacity response page; `None` slots are unused.
pub descriptors: [Option<&'static FieldDescriptor>; 4],
/// Stable index of this descriptor in the advertised schema.
pub index: u16,
/// Self-describing field metadata.
pub descriptor: FieldDescriptor,
/// Factory value selected by the concrete board/image composition.
pub default: ConfigValue,
}
/// One named enum choice associated with a descriptor response.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct EnumOptionResponse {
/// Request being answered.
pub request_id: u32,
/// Stable schema index of the owning descriptor.
pub descriptor_index: u16,
/// Stable field identity of the owning descriptor.
pub field: FieldId,
/// Stable zero-based order within the descriptor's choices.
pub option_index: u8,
/// Self-describing named choice.
pub option: EnumOption,
}
/// One typed value read from an explicitly selected configuration snapshot.
@@ -41,20 +56,12 @@ pub struct ValueResponse {
pub struct ConfigInfo {
/// Request being answered.
pub request_id: u32,
/// Boot-active generation, or `None` for the factory profile.
pub active_generation: Option<Generation>,
/// Last committed generation, or `None` for the factory profile.
pub committed_generation: Option<Generation>,
/// Exact schema identifier expected by profile import.
/// Stable schema identity for this target-described field registry.
pub schema_id: [u8; 16],
/// Version carried inside the one raw CBOR profile blob.
pub profile_format: u16,
/// Number of editable fields in the registry.
pub field_count: u16,
/// Current configuration lifecycle state.
pub state: ConfigState,
/// Deployment capabilities such as bounded CBOR import.
pub capability_flags: u8,
}
/// Exact response produced by the custom configuration service.
@@ -64,13 +71,15 @@ pub enum ServiceResponse {
Info(ConfigInfo),
/// Completion for a mutating operation or rejected query.
Result(crate::ConfigReply),
/// Bounded self-describing schema page.
Descriptors(DescriptorPage),
/// One self-describing schema field.
Descriptor(DescriptorResponse),
/// One named choice for its explicitly identified enum descriptor.
EnumOption(EnumOptionResponse),
/// One typed scalar value.
Value(ValueResponse),
}
impl<I> ConfigProtocol<I> {
impl ConfigProtocol {
/// Returns static descriptor metadata by stable schema index.
pub fn descriptor<C, S>(
&self,
@@ -79,7 +88,7 @@ impl<I> ConfigProtocol<I> {
) -> Option<&'static FieldDescriptor>
where
C: ConfigModel,
S: ProfileStore<C>,
S: ConfigStore<C>,
{
manager.descriptor(index)
}
@@ -93,7 +102,7 @@ impl<I> ConfigProtocol<I> {
) -> Option<ConfigValue>
where
C: ConfigModel,
S: ProfileStore<C>,
S: ConfigStore<C>,
{
match source {
ValueSource::Active => manager.active_value(field),
@@ -102,60 +111,65 @@ impl<I> ConfigProtocol<I> {
}
}
pub(super) fn descriptors<C, S>(
pub(super) fn descriptor_response<C, S>(
&self,
manager: &ConfigManager<C, S>,
request_id: u32,
first: u16,
requested: u16,
) -> DescriptorPage
index: u16,
) -> Option<DescriptorResponse>
where
C: ConfigModel,
S: ProfileStore<C>,
S: ConfigStore<C>,
{
let mut descriptors = [None; 4];
let count = requested.min(descriptors.len() as u16);
let mut len = 0;
for offset in 0..count {
let Some(index) = first.checked_add(offset) else {
break;
};
let Some(descriptor) = manager.descriptor(index) else {
break;
};
descriptors[usize::from(len)] = Some(descriptor);
len += 1;
}
DescriptorPage {
let descriptor = *manager.descriptor(index)?;
let default = manager.factory_value(descriptor.id)?;
Some(DescriptorResponse {
request_id,
first,
len,
descriptors,
}
index,
descriptor,
default,
})
}
pub(super) fn enum_option_response<C, S>(
&self,
manager: &ConfigManager<C, S>,
request_id: u32,
descriptor_index: u16,
option_index: u8,
) -> Option<EnumOptionResponse>
where
C: ConfigModel,
S: ConfigStore<C>,
{
let descriptor = manager.descriptor(descriptor_index)?;
let option = *descriptor.enum_options.get(usize::from(option_index))?;
Some(EnumOptionResponse {
request_id,
descriptor_index,
field: descriptor.id,
option_index,
option,
})
}
pub(super) fn info<C, S>(&self, manager: &ConfigManager<C, S>, request_id: u32) -> ConfigInfo
where
C: ConfigModel,
S: ProfileStore<C>,
I: SnapshotImporter<C>,
S: ConfigStore<C>,
{
ConfigInfo {
request_id,
active_generation: manager.active_generation(),
committed_generation: manager.committed_generation(),
schema_id: C::SCHEMA_ID,
profile_format: I::PROFILE_FORMAT,
field_count: manager.field_count(),
state: self.state,
capability_flags: self.capability_flags_for::<C>(),
}
}
pub(super) fn value_response<C, S>(
&self,
manager: &ConfigManager<C, S>,
owner: u64,
owner: Principal,
request_id: u32,
session_id: u32,
field: FieldId,
@@ -163,7 +177,7 @@ impl<I> ConfigProtocol<I> {
) -> ServiceResponse
where
C: ConfigModel,
S: ProfileStore<C>,
S: ConfigStore<C>,
{
let (value, reply_session, revision) = match source {
ValueSource::Active => (manager.active_value(field), 0, 0),
@@ -176,7 +190,7 @@ impl<I> ConfigProtocol<I> {
ProtocolError::InvalidSession,
));
};
if session.id != session_id || session.owner != owner {
if session.id != session_id || self.principal != Some(owner) {
return ServiceResponse::Result(self.rejected_for(
request_id,
session_id,
@@ -3,34 +3,42 @@
use kaptan_config::ConfigValue;
use super::session::ConfigProtocol;
use crate::{ConfigCommand, ConfigReply, ProtocolError};
pub(super) const REQUEST_CACHE_CAPACITY: usize = 8;
use crate::{ConfigCommand, ConfigReply, Operation, Principal, ProtocolError};
#[derive(Clone, Copy)]
pub(super) struct RememberedResult {
pub(super) owner: u64,
pub(super) command: ConfigCommand,
pub(super) reply: ConfigReply,
pub(super) owner: Principal,
}
impl<I> ConfigProtocol<I> {
pub(super) fn cached(&self, owner: u64, command: ConfigCommand) -> Option<ConfigReply> {
for cached in self.remembered.iter().flatten() {
if cached.owner != owner || cached.command.request_id() != command.request_id() {
continue;
}
if same_command(cached.command, command) {
return Some(cached.reply);
}
return Some(self.rejected(command, ProtocolError::RequestConflict));
impl ConfigProtocol {
pub(super) fn cached(&self, owner: Principal, command: ConfigCommand) -> Option<ConfigReply> {
let cached = self.remembered?;
if cached.owner != owner || cached.command.request_id() != command.request_id() {
return None;
}
if same_command(cached.command, command) {
Some(cached.reply)
} else {
Some(self.rejected(command, ProtocolError::RequestConflict))
}
None
}
pub(super) fn remember(&mut self, result: RememberedResult) {
self.remembered[self.next_remembered] = Some(result);
self.next_remembered = (self.next_remembered + 1) % REQUEST_CACHE_CAPACITY;
pub(super) fn remember(
&mut self,
owner: Principal,
command: ConfigCommand,
reply: ConfigReply,
) {
if reply.operation == Operation::Query {
return;
}
self.remembered = Some(RememberedResult {
command,
reply,
owner,
});
}
}
@@ -49,20 +57,36 @@ fn same_command(left: ConfigCommand, right: ConfigCommand) -> bool {
(
ConfigCommand::DescriptorRequest {
request_id: left_request,
first: left_first,
count: left_count,
index: left_index,
target: left_target,
},
ConfigCommand::DescriptorRequest {
request_id: right_request,
first: right_first,
count: right_count,
index: right_index,
target: right_target,
},
) => {
left_request == right_request
&& left_first == right_first
&& left_count == right_count
&& left_index == right_index
&& left_target == right_target
}
(
ConfigCommand::EnumOptionRequest {
request_id: left_request,
descriptor_index: left_descriptor,
option_index: left_option,
target: left_target,
},
ConfigCommand::EnumOptionRequest {
request_id: right_request,
descriptor_index: right_descriptor,
option_index: right_option,
target: right_target,
},
) => {
left_request == right_request
&& left_descriptor == right_descriptor
&& left_option == right_option
&& left_target == right_target
}
(
@@ -90,19 +114,13 @@ fn same_command(left: ConfigCommand, right: ConfigCommand) -> bool {
(
ConfigCommand::Begin {
request_id: left_request,
expected_generation: left_generation,
target: left_target,
},
ConfigCommand::Begin {
request_id: right_request,
expected_generation: right_generation,
target: right_target,
},
) => {
left_request == right_request
&& left_generation == right_generation
&& left_target == right_target
}
) => left_request == right_request && left_target == right_target,
(
ConfigCommand::Set {
request_id: left_request,
@@ -151,21 +169,18 @@ fn same_command(left: ConfigCommand, right: ConfigCommand) -> bool {
ConfigCommand::Commit {
request_id: left_request,
session_id: left_session,
expected_generation: left_generation,
expected_revision: left_revision,
target: left_target,
},
ConfigCommand::Commit {
request_id: right_request,
session_id: right_session,
expected_generation: right_generation,
expected_revision: right_revision,
target: right_target,
},
) => {
left_request == right_request
&& left_session == right_session
&& left_generation == right_generation
&& left_revision == right_revision
&& left_target == right_target
}
@@ -185,84 +200,6 @@ fn same_command(left: ConfigCommand, right: ConfigCommand) -> bool {
&& left_session == right_session
&& left_target == right_target
}
(
ConfigCommand::ImportBegin {
request_id: left_request,
session_id: left_session,
expected_revision: left_revision,
length: left_length,
schema_id: left_schema,
crc32c: left_crc,
target: left_target,
},
ConfigCommand::ImportBegin {
request_id: right_request,
session_id: right_session,
expected_revision: right_revision,
length: right_length,
schema_id: right_schema,
crc32c: right_crc,
target: right_target,
},
) => {
left_request == right_request
&& left_session == right_session
&& left_revision == right_revision
&& left_length == right_length
&& left_schema == right_schema
&& left_crc == right_crc
&& left_target == right_target
}
(
ConfigCommand::ImportChunk {
request_id: left_request,
session_id: left_session,
transfer_id: left_transfer,
offset: left_offset,
length: left_length,
data: left_data,
target: left_target,
},
ConfigCommand::ImportChunk {
request_id: right_request,
session_id: right_session,
transfer_id: right_transfer,
offset: right_offset,
length: right_length,
data: right_data,
target: right_target,
},
) => {
left_request == right_request
&& left_session == right_session
&& left_transfer == right_transfer
&& left_offset == right_offset
&& left_length == right_length
&& left_data == right_data
&& left_target == right_target
}
(
ConfigCommand::ImportFinish {
request_id: left_request,
session_id: left_session,
transfer_id: left_transfer,
expected_revision: left_revision,
target: left_target,
},
ConfigCommand::ImportFinish {
request_id: right_request,
session_id: right_session,
transfer_id: right_transfer,
expected_revision: right_revision,
target: right_target,
},
) => {
left_request == right_request
&& left_session == right_session
&& left_transfer == right_transfer
&& left_revision == right_revision
&& left_target == right_target
}
_ => false,
}
}
File diff suppressed because it is too large. Load diff
+277 -104
View File
@@ -1,23 +1,68 @@
//! Generated Kaptan-dialect message translation.
use kaptan_config::{
ApplyPolicy, ConfigState, ConfigValue, FieldDescriptor, FieldId, Generation, ValidationRule,
ValueKind,
ConfigState, ConfigValue, FieldDescriptor, FieldId, ValidationRule, ValueKind,
};
use kaptan_log::KaptanError;
use kaptan_mavlink::message::Message;
use kaptan_mavlink::message::kaptan::{
KAPTAN_CONFIG_DESCRIPTOR_DATA, KAPTAN_CONFIG_INFO_DATA, KAPTAN_CONFIG_RESULT_DATA,
KAPTAN_CONFIG_VALUE_DATA, KaptanConfigError, KaptanConfigOperation, KaptanConfigPolicy,
KAPTAN_CONFIG_DESCRIPTOR_DATA, KAPTAN_CONFIG_ENUM_OPTION_DATA, KAPTAN_CONFIG_INFO_DATA,
KAPTAN_CONFIG_RESULT_DATA, KAPTAN_CONFIG_VALUE_DATA, KaptanConfigError, KaptanConfigOperation,
KaptanConfigResult, KaptanConfigState, KaptanConfigValueKind, KaptanConfigValueSource,
MavMessage,
};
use crate::{
ConfigCommand, ConfigReply, Operation, ProtocolError, ResultKind, Target, ValueSource,
service::{ConfigInfo, ValueResponse},
ConfigCommand, ConfigReply, DESCRIPTOR_HAS_MAXIMUM, DESCRIPTOR_HAS_MINIMUM, Operation,
PROTOCOL_MAJOR, PROTOCOL_MINOR, ProtocolError, ResultKind, Target, ValueSource,
service::{ConfigInfo, EnumOptionResponse, ServiceResponse, ValueResponse},
};
/// Transport-owned consumer of generated configuration response payloads.
///
/// Implementors retain ownership of MAVLink framing, sender sequence, queue
/// classification, and physical transmission.
pub trait ResponseVisitor {
/// Transport-specific admission or serialization failure.
type Error;
/// Visits one protocol/schema information response.
fn info(&mut self, response: &KAPTAN_CONFIG_INFO_DATA) -> Result<(), Self::Error>;
/// Visits one authoritative operation result.
fn result(&mut self, response: &KAPTAN_CONFIG_RESULT_DATA) -> Result<(), Self::Error>;
/// Visits one field descriptor.
fn descriptor(&mut self, response: &KAPTAN_CONFIG_DESCRIPTOR_DATA) -> Result<(), Self::Error>;
/// Visits one named choice for an enum-backed field descriptor.
fn enum_option(&mut self, response: &KAPTAN_CONFIG_ENUM_OPTION_DATA)
-> Result<(), Self::Error>;
/// Visits one typed configuration value.
fn value(&mut self, response: &KAPTAN_CONFIG_VALUE_DATA) -> Result<(), Self::Error>;
}
/// Projects one transport-neutral service response into generated MAVLink
/// payloads without allocating or assuming a board, schema, or field set.
pub fn visit_response<V>(response: ServiceResponse, visitor: &mut V) -> Result<(), V::Error>
where
V: ResponseVisitor,
{
match response {
ServiceResponse::Info(info) => visitor.info(&encode_info(info)),
ServiceResponse::Result(result) => visitor.result(&encode_result(result)),
ServiceResponse::Descriptor(response) => visitor.descriptor(&encode_descriptor(
response.request_id,
response.index,
response.descriptor,
response.default,
)),
ServiceResponse::EnumOption(response) => visitor.enum_option(&encode_enum_option(response)),
ServiceResponse::Value(value) => visitor.value(&encode_value(value)),
}
}
/// Rejects a generated message that is not a Kaptan configuration command.
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
pub enum DecodeError {
@@ -35,8 +80,9 @@ pub enum DecodeError {
/// Converts one generated custom request into a transport-neutral command.
///
/// This only decodes a validated MAVLink payload. An authenticated ingress must
/// still wrap the result in [`crate::Authorized`] before dispatch.
/// This only decodes a validated MAVLink payload. CRC validity is not peer
/// authentication; the platform composition must separately decide whether
/// the ingress may dispatch the command and bind its transaction principal.
pub fn decode_command(message: &MavMessage) -> Result<ConfigCommand, DecodeError> {
if matches!(message.target_system_id(), Some(0))
|| matches!(message.target_component_id(), Some(0))
@@ -51,8 +97,15 @@ pub fn decode_command(message: &MavMessage) -> Result<ConfigCommand, DecodeError
MavMessage::KAPTAN_CONFIG_DESCRIPTOR_REQUEST(data) => {
Ok(ConfigCommand::DescriptorRequest {
request_id: data.request_id,
first: data.first,
count: data.count,
index: data.descriptor_index,
target: Target::new(data.target_system, data.target_component),
})
}
MavMessage::KAPTAN_CONFIG_ENUM_OPTION_REQUEST(data) => {
Ok(ConfigCommand::EnumOptionRequest {
request_id: data.request_id,
descriptor_index: data.descriptor_index,
option_index: data.option_index,
target: Target::new(data.target_system, data.target_component),
})
}
@@ -63,34 +116,8 @@ pub fn decode_command(message: &MavMessage) -> Result<ConfigCommand, DecodeError
source: value_source(data.source)?,
target: Target::new(data.target_system, data.target_component),
}),
MavMessage::KAPTAN_CONFIG_IMPORT_BEGIN(data) => Ok(ConfigCommand::ImportBegin {
request_id: data.request_id,
session_id: data.session_id,
expected_revision: data.expected_revision,
length: data.length,
schema_id: data.schema_id,
crc32c: data.crc32c,
target: Target::new(data.target_system, data.target_component),
}),
MavMessage::KAPTAN_CONFIG_IMPORT_CHUNK(data) => Ok(ConfigCommand::ImportChunk {
request_id: data.request_id,
session_id: data.session_id,
transfer_id: data.transfer_id,
offset: data.offset,
length: data.length,
data: data.data,
target: Target::new(data.target_system, data.target_component),
}),
MavMessage::KAPTAN_CONFIG_IMPORT_FINISH(data) => Ok(ConfigCommand::ImportFinish {
request_id: data.request_id,
session_id: data.session_id,
transfer_id: data.transfer_id,
expected_revision: data.expected_revision,
target: Target::new(data.target_system, data.target_component),
}),
MavMessage::KAPTAN_CONFIG_BEGIN(data) => Ok(ConfigCommand::Begin {
request_id: data.request_id,
expected_generation: data.expected_generation,
target: Target::new(data.target_system, data.target_component),
}),
MavMessage::KAPTAN_CONFIG_SET_F32(data) => Ok(ConfigCommand::Set {
@@ -150,7 +177,6 @@ pub fn decode_command(message: &MavMessage) -> Result<ConfigCommand, DecodeError
MavMessage::KAPTAN_CONFIG_COMMIT(data) => Ok(ConfigCommand::Commit {
request_id: data.request_id,
session_id: data.session_id,
expected_generation: data.expected_generation,
expected_revision: data.expected_revision,
target: Target::new(data.target_system, data.target_component),
}),
@@ -191,10 +217,8 @@ const fn boolean(value: u8) -> Result<bool, DecodeError> {
/// Encodes one authoritative service completion.
pub fn encode_result(reply: ConfigReply) -> KAPTAN_CONFIG_RESULT_DATA {
KAPTAN_CONFIG_RESULT_DATA {
generation: reply.generation,
request_id: reply.request_id,
session_id: reply.session_id,
transfer_id: reply.transfer_id,
revision: reply.revision,
primary_field: reply.primary_field(),
related_field: reply.related_field(),
@@ -209,16 +233,12 @@ pub fn encode_result(reply: ConfigReply) -> KAPTAN_CONFIG_RESULT_DATA {
/// Encodes configuration protocol/schema status for a Kaptan control station.
pub fn encode_info(info: ConfigInfo) -> KAPTAN_CONFIG_INFO_DATA {
KAPTAN_CONFIG_INFO_DATA {
active_generation: info.active_generation.map_or(0, Generation::get),
committed_generation: info.committed_generation.map_or(0, Generation::get),
request_id: info.request_id,
schema_id: info.schema_id,
profile_format: info.profile_format,
field_count: info.field_count,
protocol_major: 1,
protocol_minor: 0,
protocol_major: PROTOCOL_MAJOR,
protocol_minor: PROTOCOL_MINOR,
config_state: state(info.state),
capability_flags: info.capability_flags,
}
}
@@ -227,19 +247,34 @@ pub fn encode_descriptor(
request_id: u32,
descriptor_index: u16,
descriptor: FieldDescriptor,
default: ConfigValue,
) -> KAPTAN_CONFIG_DESCRIPTOR_DATA {
KAPTAN_CONFIG_DESCRIPTOR_DATA {
minimum_bits: descriptor.minimum.map_or(0, ConfigValue::raw_bits),
maximum_bits: descriptor.maximum.map_or(0, ConfigValue::raw_bits),
default_bits: descriptor.default.raw_bits(),
default_bits: default.raw_bits(),
request_id,
descriptor_index,
field_id: descriptor.id.get(),
enum_option_count: u8::try_from(descriptor.enum_options.len())
.expect("validated enum option count fits u8"),
display_key: descriptor.display_key.into(),
unit: descriptor.unit.into(),
value_kind: value_kind(descriptor.kind),
apply_policy: policy(descriptor.apply_policy),
flags: 0,
flags: descriptor.minimum.map_or(0, |_| DESCRIPTOR_HAS_MINIMUM)
| descriptor.maximum.map_or(0, |_| DESCRIPTOR_HAS_MAXIMUM),
}
}
/// Encodes one generic named enum choice.
pub fn encode_enum_option(response: EnumOptionResponse) -> KAPTAN_CONFIG_ENUM_OPTION_DATA {
KAPTAN_CONFIG_ENUM_OPTION_DATA {
value: response.option.0,
request_id: response.request_id,
descriptor_index: response.descriptor_index,
field_id: response.field.get(),
option_index: response.option_index,
label: response.option.1.into(),
}
}
@@ -266,14 +301,6 @@ const fn value_kind(kind: ValueKind) -> KaptanConfigValueKind {
}
}
const fn policy(policy: ApplyPolicy) -> KaptanConfigPolicy {
match policy {
ApplyPolicy::Live => KaptanConfigPolicy::KAPTAN_CONFIG_POLICY_LIVE,
ApplyPolicy::DisarmedReset => KaptanConfigPolicy::KAPTAN_CONFIG_POLICY_DISARMED_RESET,
ApplyPolicy::RestartRequired => KaptanConfigPolicy::KAPTAN_CONFIG_POLICY_RESTART_REQUIRED,
}
}
const fn operation(operation: Operation) -> KaptanConfigOperation {
match operation {
Operation::Query => KaptanConfigOperation::KAPTAN_CONFIG_OPERATION_QUERY,
@@ -282,7 +309,6 @@ const fn operation(operation: Operation) -> KaptanConfigOperation {
Operation::Validate => KaptanConfigOperation::KAPTAN_CONFIG_OPERATION_VALIDATE,
Operation::Commit => KaptanConfigOperation::KAPTAN_CONFIG_OPERATION_COMMIT,
Operation::Abort => KaptanConfigOperation::KAPTAN_CONFIG_OPERATION_ABORT,
Operation::Import => KaptanConfigOperation::KAPTAN_CONFIG_OPERATION_IMPORT,
}
}
@@ -308,12 +334,6 @@ const fn error(error: Option<ProtocolError>) -> KaptanConfigError {
match error {
None => KaptanConfigError::KAPTAN_CONFIG_ERROR_NONE,
Some(ProtocolError::BadTarget) => KaptanConfigError::KAPTAN_CONFIG_ERROR_BAD_TARGET,
Some(ProtocolError::AuthorizationDenied) => {
KaptanConfigError::KAPTAN_CONFIG_ERROR_AUTHORIZATION_DENIED
}
Some(ProtocolError::GenerationConflict) => {
KaptanConfigError::KAPTAN_CONFIG_ERROR_GENERATION_CONFLICT
}
Some(ProtocolError::SessionBusy) => KaptanConfigError::KAPTAN_CONFIG_ERROR_SESSION_BUSY,
Some(ProtocolError::InvalidSession) => {
KaptanConfigError::KAPTAN_CONFIG_ERROR_INVALID_SESSION
@@ -326,6 +346,7 @@ const fn error(error: Option<ProtocolError>) -> KaptanConfigError {
Some(ProtocolError::UnknownField(_)) => {
KaptanConfigError::KAPTAN_CONFIG_ERROR_UNKNOWN_FIELD
}
Some(ProtocolError::InvalidQuery) => KaptanConfigError::KAPTAN_CONFIG_ERROR_INVALID_QUERY,
Some(ProtocolError::TypeMismatch(_)) => {
KaptanConfigError::KAPTAN_CONFIG_ERROR_TYPE_MISMATCH
}
@@ -339,36 +360,88 @@ const fn error(error: Option<ProtocolError>) -> KaptanConfigError {
ValidationRule::UnsupportedCapability => {
KaptanConfigError::KAPTAN_CONFIG_ERROR_UNSUPPORTED_CAPABILITY
}
ValidationRule::UnsupportedFormat => {
KaptanConfigError::KAPTAN_CONFIG_ERROR_UNSUPPORTED_FORMAT
ValidationRule::UnsupportedFormat => KaptanConfigError::KAPTAN_CONFIG_ERROR_INTERNAL,
ValidationRule::InvalidEnumValue => {
KaptanConfigError::KAPTAN_CONFIG_ERROR_INVALID_ENUM_VALUE
}
},
Some(ProtocolError::StorageFailure) => {
KaptanConfigError::KAPTAN_CONFIG_ERROR_STORAGE_FAILURE
}
Some(ProtocolError::CborMalformed) => KaptanConfigError::KAPTAN_CONFIG_ERROR_CBOR_MALFORMED,
Some(ProtocolError::CborTooLarge) => KaptanConfigError::KAPTAN_CONFIG_ERROR_CBOR_TOO_LARGE,
Some(ProtocolError::BlobCrc) => KaptanConfigError::KAPTAN_CONFIG_ERROR_BLOB_CRC,
Some(ProtocolError::SchemaMismatch) => {
KaptanConfigError::KAPTAN_CONFIG_ERROR_SCHEMA_MISMATCH
}
Some(ProtocolError::ImportUnavailable) => {
KaptanConfigError::KAPTAN_CONFIG_ERROR_IMPORT_UNAVAILABLE
}
Some(ProtocolError::Internal) => KaptanConfigError::KAPTAN_CONFIG_ERROR_INTERNAL,
}
}
#[cfg(test)]
mod tests {
use kaptan_config::{ConfigState, ConfigViolation, ValidationRule};
use kaptan_config::{
ConfigState, ConfigValue, ConfigViolation, FieldDescriptor, ValidationRule, ValueKind,
};
use kaptan_mavlink::message::kaptan::{
KAPTAN_CONFIG_INFO_REQUEST_DATA, KAPTAN_CONFIG_SET_F32_DATA, MavMessage,
KAPTAN_CONFIG_ENUM_OPTION_REQUEST_DATA, KAPTAN_CONFIG_INFO_REQUEST_DATA,
KAPTAN_CONFIG_SET_F32_DATA, MavMessage,
};
use super::*;
use crate::CAPABILITY_CBOR_IMPORT;
#[derive(Debug, PartialEq)]
enum Emitted {
Info(KAPTAN_CONFIG_INFO_DATA),
Result(KAPTAN_CONFIG_RESULT_DATA),
Descriptor(KAPTAN_CONFIG_DESCRIPTOR_DATA),
EnumOption(KAPTAN_CONFIG_ENUM_OPTION_DATA),
Value(KAPTAN_CONFIG_VALUE_DATA),
}
#[derive(Default)]
struct RecordingVisitor {
emitted: std::vec::Vec<Emitted>,
}
impl ResponseVisitor for RecordingVisitor {
type Error = core::convert::Infallible;
fn info(&mut self, response: &KAPTAN_CONFIG_INFO_DATA) -> Result<(), Self::Error> {
self.emitted.push(Emitted::Info(response.clone()));
Ok(())
}
fn result(&mut self, response: &KAPTAN_CONFIG_RESULT_DATA) -> Result<(), Self::Error> {
self.emitted.push(Emitted::Result(response.clone()));
Ok(())
}
fn descriptor(
&mut self,
response: &KAPTAN_CONFIG_DESCRIPTOR_DATA,
) -> Result<(), Self::Error> {
self.emitted.push(Emitted::Descriptor(response.clone()));
Ok(())
}
fn enum_option(
&mut self,
response: &KAPTAN_CONFIG_ENUM_OPTION_DATA,
) -> Result<(), Self::Error> {
self.emitted.push(Emitted::EnumOption(response.clone()));
Ok(())
}
fn value(&mut self, response: &KAPTAN_CONFIG_VALUE_DATA) -> Result<(), Self::Error> {
self.emitted.push(Emitted::Value(response.clone()));
Ok(())
}
}
const FIRST_DESCRIPTOR: FieldDescriptor = FieldDescriptor {
id: FieldId::new(17),
display_key: "future.sensor.rate_hz",
kind: ValueKind::U32,
minimum: Some(ConfigValue::U32(1)),
maximum: Some(ConfigValue::U32(200)),
enum_options: &[],
unit: "Hz",
};
#[test]
fn generated_set_message_preserves_typed_value_and_identifiers() {
let message = MavMessage::KAPTAN_CONFIG_SET_F32(KAPTAN_CONFIG_SET_F32_DATA {
@@ -409,6 +482,27 @@ mod tests {
);
}
#[test]
fn generated_enum_option_request_preserves_the_explicit_index() {
let message =
MavMessage::KAPTAN_CONFIG_ENUM_OPTION_REQUEST(KAPTAN_CONFIG_ENUM_OPTION_REQUEST_DATA {
request_id: 41,
descriptor_index: 3,
option_index: 6,
target_system: 1,
target_component: 2,
});
assert_eq!(
decode_command(&message),
Ok(ConfigCommand::EnumOptionRequest {
request_id: 41,
descriptor_index: 3,
option_index: 6,
target: Target::new(1, 2),
})
);
}
#[test]
fn generated_config_requests_reject_broadcast_targets() {
let zero_system = MavMessage::KAPTAN_CONFIG_SET_F32(KAPTAN_CONFIG_SET_F32_DATA {
@@ -444,9 +538,7 @@ mod tests {
let reply = ConfigReply {
request_id: 1,
session_id: 2,
transfer_id: 0,
revision: 3,
generation: 4,
operation: Operation::Set,
result: ResultKind::Rejected,
error: Some(ProtocolError::Violation(ConfigViolation::field(
@@ -465,37 +557,118 @@ mod tests {
}
#[test]
fn unavailable_import_has_its_own_structured_error_code() {
let reply = ConfigReply {
request_id: 1,
session_id: 0,
transfer_id: 0,
revision: 0,
generation: 0,
operation: Operation::Import,
result: ResultKind::Rejected,
error: Some(ProtocolError::ImportUnavailable),
fn info_preserves_schema_and_protocol_identity() {
let info = encode_info(ConfigInfo {
request_id: 7,
schema_id: [0; 16],
field_count: 23,
state: ConfigState::Active,
};
});
assert_eq!(info.request_id, 7);
assert_eq!(info.schema_id, [0; 16]);
assert_eq!(
encode_result(reply).error,
KaptanConfigError::KAPTAN_CONFIG_ERROR_IMPORT_UNAVAILABLE
(info.protocol_major, info.protocol_minor),
(PROTOCOL_MAJOR, PROTOCOL_MINOR)
);
}
#[test]
fn info_preserves_service_capabilities() {
let info = encode_info(ConfigInfo {
fn descriptor_flags_identify_present_bounds() {
let descriptor = FieldDescriptor {
id: FieldId::new(9),
display_key: "test.value",
kind: ValueKind::U32,
minimum: Some(ConfigValue::U32(1)),
maximum: Some(ConfigValue::U32(10)),
enum_options: &[],
unit: "count",
};
assert_eq!(
encode_descriptor(1, 0, descriptor, ConfigValue::U32(2)).flags,
DESCRIPTOR_HAS_MINIMUM | DESCRIPTOR_HAS_MAXIMUM
);
assert_eq!(
encode_descriptor(1, 0, descriptor, ConfigValue::U32(2)).enum_option_count,
0
);
}
#[test]
fn response_visitor_projects_every_singleton_response_variant() {
let info = ConfigInfo {
request_id: 7,
active_generation: None,
committed_generation: None,
schema_id: [0; 16],
profile_format: 1,
field_count: 23,
state: ConfigState::Active,
capability_flags: CAPABILITY_CBOR_IMPORT,
});
assert_eq!(info.request_id, 7);
assert_eq!(info.capability_flags, CAPABILITY_CBOR_IMPORT);
schema_id: *b"UNKNOWN-SCHEMA01",
field_count: 2,
state: ConfigState::RestartRequired,
};
let result = ConfigReply {
request_id: 8,
session_id: 9,
revision: 2,
operation: Operation::Commit,
result: ResultKind::CommittedRestartRequired,
error: None,
state: ConfigState::RestartRequired,
};
let value = ValueResponse {
request_id: 10,
session_id: 0,
revision: 0,
field: FieldId::new(401),
value: ConfigValue::Bool(true),
};
let enum_option = crate::EnumOptionResponse {
request_id: 9,
descriptor_index: 12,
field: FieldId::new(17),
option_index: 1,
option: (3, "Error"),
};
let mut visitor = RecordingVisitor::default();
assert_eq!(
visit_response(ServiceResponse::Info(info), &mut visitor),
Ok(())
);
assert_eq!(
visit_response(ServiceResponse::Result(result), &mut visitor),
Ok(())
);
assert_eq!(
visit_response(
ServiceResponse::Descriptor(crate::DescriptorResponse {
request_id: 9,
index: 12,
descriptor: FIRST_DESCRIPTOR,
default: ConfigValue::U32(20),
}),
&mut visitor,
),
Ok(())
);
assert_eq!(
visit_response(ServiceResponse::EnumOption(enum_option), &mut visitor),
Ok(())
);
assert_eq!(
visit_response(ServiceResponse::Value(value), &mut visitor),
Ok(())
);
assert_eq!(
visitor.emitted,
std::vec![
Emitted::Info(encode_info(info)),
Emitted::Result(encode_result(result)),
Emitted::Descriptor(encode_descriptor(
9,
12,
FIRST_DESCRIPTOR,
ConfigValue::U32(20),
)),
Emitted::EnumOption(encode_enum_option(enum_option)),
Emitted::Value(encode_value(value)),
]
);
}
}