Files
kaptan/firmware/libs/kaptan-config-mavlink/src/wire.rs
T

502 lines
20 KiB
Rust

//! Generated Kaptan-dialect message translation.
use kaptan_config::{
ApplyPolicy, ConfigState, ConfigValue, FieldDescriptor, FieldId, Generation, 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,
KaptanConfigResult, KaptanConfigState, KaptanConfigValueKind, KaptanConfigValueSource,
MavMessage,
};
use crate::{
ConfigCommand, ConfigReply, Operation, ProtocolError, ResultKind, Target, ValueSource,
service::{ConfigInfo, ValueResponse},
};
/// Rejects a generated message that is not a Kaptan configuration command.
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
pub enum DecodeError {
/// The message belongs to a response, telemetry, or another service.
UnsupportedMessage,
/// A wire field identity used a reserved sentinel.
InvalidFieldId,
/// The message used an unknown value-source discriminator.
InvalidValueSource,
/// A boolean setter used a representation other than zero or one.
InvalidBoolean,
/// A configuration request used a broadcast/zero target endpoint.
InvalidTarget,
}
/// 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.
pub fn decode_command(message: &MavMessage) -> Result<ConfigCommand, DecodeError> {
if matches!(message.target_system_id(), Some(0))
|| matches!(message.target_component_id(), Some(0))
{
return Err(DecodeError::InvalidTarget);
}
match message {
MavMessage::KAPTAN_CONFIG_INFO_REQUEST(data) => Ok(ConfigCommand::InfoRequest {
request_id: data.request_id,
target: Target::new(data.target_system, data.target_component),
}),
MavMessage::KAPTAN_CONFIG_DESCRIPTOR_REQUEST(data) => {
Ok(ConfigCommand::DescriptorRequest {
request_id: data.request_id,
first: data.first,
count: data.count,
target: Target::new(data.target_system, data.target_component),
})
}
MavMessage::KAPTAN_CONFIG_VALUE_REQUEST(data) => Ok(ConfigCommand::ValueRequest {
request_id: data.request_id,
session_id: data.session_id,
field: field_id(data.field_id)?,
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 {
request_id: data.request_id,
session_id: data.session_id,
expected_revision: data.expected_revision,
field: field_id(data.field_id)?,
value: ConfigValue::F32(data.value),
target: Target::new(data.target_system, data.target_component),
}),
MavMessage::KAPTAN_CONFIG_SET_BOOL(data) => Ok(ConfigCommand::Set {
request_id: data.request_id,
session_id: data.session_id,
expected_revision: data.expected_revision,
field: field_id(data.field_id)?,
value: ConfigValue::Bool(boolean(data.value)?),
target: Target::new(data.target_system, data.target_component),
}),
MavMessage::KAPTAN_CONFIG_SET_I32(data) => Ok(ConfigCommand::Set {
request_id: data.request_id,
session_id: data.session_id,
expected_revision: data.expected_revision,
field: field_id(data.field_id)?,
value: ConfigValue::I32(data.value),
target: Target::new(data.target_system, data.target_component),
}),
MavMessage::KAPTAN_CONFIG_SET_U32(data) => Ok(ConfigCommand::Set {
request_id: data.request_id,
session_id: data.session_id,
expected_revision: data.expected_revision,
field: field_id(data.field_id)?,
value: ConfigValue::U32(data.value),
target: Target::new(data.target_system, data.target_component),
}),
MavMessage::KAPTAN_CONFIG_SET_U64(data) => Ok(ConfigCommand::Set {
request_id: data.request_id,
session_id: data.session_id,
expected_revision: data.expected_revision,
field: field_id(data.field_id)?,
value: ConfigValue::U64(data.value),
target: Target::new(data.target_system, data.target_component),
}),
MavMessage::KAPTAN_CONFIG_SET_F64(data) => Ok(ConfigCommand::Set {
request_id: data.request_id,
session_id: data.session_id,
expected_revision: data.expected_revision,
field: field_id(data.field_id)?,
value: ConfigValue::F64(data.value),
target: Target::new(data.target_system, data.target_component),
}),
MavMessage::KAPTAN_CONFIG_VALIDATE(data) => Ok(ConfigCommand::Validate {
request_id: data.request_id,
session_id: data.session_id,
expected_revision: data.expected_revision,
target: Target::new(data.target_system, data.target_component),
}),
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),
}),
MavMessage::KAPTAN_CONFIG_ABORT(data) => Ok(ConfigCommand::Abort {
request_id: data.request_id,
session_id: data.session_id,
target: Target::new(data.target_system, data.target_component),
}),
_ => Err(DecodeError::UnsupportedMessage),
}
}
const fn field_id(value: u16) -> Result<FieldId, DecodeError> {
match FieldId::try_new(value) {
Some(value) => Ok(value),
None => Err(DecodeError::InvalidFieldId),
}
}
const fn value_source(value: KaptanConfigValueSource) -> Result<ValueSource, DecodeError> {
match value {
KaptanConfigValueSource::KAPTAN_CONFIG_VALUE_SOURCE_ACTIVE => Ok(ValueSource::Active),
KaptanConfigValueSource::KAPTAN_CONFIG_VALUE_SOURCE_COMMITTED => Ok(ValueSource::Committed),
KaptanConfigValueSource::KAPTAN_CONFIG_VALUE_SOURCE_TRANSACTION => {
Ok(ValueSource::Transaction)
}
}
}
const fn boolean(value: u8) -> Result<bool, DecodeError> {
match value {
0 => Ok(false),
1 => Ok(true),
_ => Err(DecodeError::InvalidBoolean),
}
}
/// 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(),
validation_rule: reply.validation_rule(),
operation: operation(reply.operation),
result: result(reply.result),
error: error(reply.error),
config_state: state(reply.state),
}
}
/// 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,
config_state: state(info.state),
capability_flags: info.capability_flags,
}
}
/// Encodes self-describing metadata for one schema field.
pub fn encode_descriptor(
request_id: u32,
descriptor_index: u16,
descriptor: FieldDescriptor,
) -> 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(),
request_id,
descriptor_index,
field_id: descriptor.id.get(),
display_key: descriptor.display_key.into(),
unit: descriptor.unit.into(),
value_kind: value_kind(descriptor.kind),
apply_policy: policy(descriptor.apply_policy),
flags: 0,
}
}
/// Encodes a typed scalar read response for the Kaptan control station.
pub fn encode_value(value: ValueResponse) -> KAPTAN_CONFIG_VALUE_DATA {
KAPTAN_CONFIG_VALUE_DATA {
value_bits: value.value.raw_bits(),
request_id: value.request_id,
session_id: value.session_id,
revision: value.revision,
field_id: value.field.get(),
value_kind: value_kind(value.value.kind()),
}
}
const fn value_kind(kind: ValueKind) -> KaptanConfigValueKind {
match kind {
ValueKind::Bool => KaptanConfigValueKind::KAPTAN_CONFIG_VALUE_KIND_BOOL,
ValueKind::I32 => KaptanConfigValueKind::KAPTAN_CONFIG_VALUE_KIND_I32,
ValueKind::U32 => KaptanConfigValueKind::KAPTAN_CONFIG_VALUE_KIND_U32,
ValueKind::U64 => KaptanConfigValueKind::KAPTAN_CONFIG_VALUE_KIND_U64,
ValueKind::F32 => KaptanConfigValueKind::KAPTAN_CONFIG_VALUE_KIND_F32,
ValueKind::F64 => KaptanConfigValueKind::KAPTAN_CONFIG_VALUE_KIND_F64,
}
}
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,
Operation::Begin => KaptanConfigOperation::KAPTAN_CONFIG_OPERATION_BEGIN,
Operation::Set => KaptanConfigOperation::KAPTAN_CONFIG_OPERATION_SET,
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,
}
}
const fn result(result: ResultKind) -> KaptanConfigResult {
match result {
ResultKind::Accepted => KaptanConfigResult::KAPTAN_CONFIG_RESULT_ACCEPTED,
ResultKind::CommittedRestartRequired => {
KaptanConfigResult::KAPTAN_CONFIG_RESULT_COMMITTED_RESTART_REQUIRED
}
ResultKind::Rejected => KaptanConfigResult::KAPTAN_CONFIG_RESULT_REJECTED,
}
}
const fn state(state: ConfigState) -> KaptanConfigState {
match state {
ConfigState::Active => KaptanConfigState::KAPTAN_CONFIG_STATE_ACTIVE,
ConfigState::Editing => KaptanConfigState::KAPTAN_CONFIG_STATE_EDITING,
ConfigState::RestartRequired => KaptanConfigState::KAPTAN_CONFIG_STATE_RESTART_REQUIRED,
}
}
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
}
Some(ProtocolError::RequestConflict) => {
KaptanConfigError::KAPTAN_CONFIG_ERROR_REQUEST_CONFLICT
}
Some(ProtocolError::StaleRevision) => KaptanConfigError::KAPTAN_CONFIG_ERROR_STALE_REVISION,
Some(ProtocolError::BadState) => KaptanConfigError::KAPTAN_CONFIG_ERROR_BAD_STATE,
Some(ProtocolError::UnknownField(_)) => {
KaptanConfigError::KAPTAN_CONFIG_ERROR_UNKNOWN_FIELD
}
Some(ProtocolError::TypeMismatch(_)) => {
KaptanConfigError::KAPTAN_CONFIG_ERROR_TYPE_MISMATCH
}
Some(ProtocolError::Violation(violation)) => match violation.rule() {
ValidationRule::NonFinite => KaptanConfigError::KAPTAN_CONFIG_ERROR_NON_FINITE,
ValidationRule::BelowMinimum | ValidationRule::Zero => {
KaptanConfigError::KAPTAN_CONFIG_ERROR_BELOW_MINIMUM
}
ValidationRule::AboveMaximum => KaptanConfigError::KAPTAN_CONFIG_ERROR_ABOVE_MAXIMUM,
ValidationRule::Inconsistent => KaptanConfigError::KAPTAN_CONFIG_ERROR_CROSS_FIELD,
ValidationRule::UnsupportedCapability => {
KaptanConfigError::KAPTAN_CONFIG_ERROR_UNSUPPORTED_CAPABILITY
}
ValidationRule::UnsupportedFormat => {
KaptanConfigError::KAPTAN_CONFIG_ERROR_UNSUPPORTED_FORMAT
}
},
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_mavlink::message::kaptan::{
KAPTAN_CONFIG_INFO_REQUEST_DATA, KAPTAN_CONFIG_SET_F32_DATA, MavMessage,
};
use super::*;
use crate::CAPABILITY_CBOR_IMPORT;
#[test]
fn generated_set_message_preserves_typed_value_and_identifiers() {
let message = MavMessage::KAPTAN_CONFIG_SET_F32(KAPTAN_CONFIG_SET_F32_DATA {
request_id: 7,
session_id: 8,
expected_revision: 9,
field_id: 10,
value: 1.5,
target_system: 1,
target_component: 2,
});
assert_eq!(
decode_command(&message),
Ok(ConfigCommand::Set {
request_id: 7,
session_id: 8,
expected_revision: 9,
field: FieldId::new(10),
value: ConfigValue::F32(1.5),
target: Target::new(1, 2),
})
);
}
#[test]
fn generated_info_request_is_correlated_and_exactly_targeted() {
let message = MavMessage::KAPTAN_CONFIG_INFO_REQUEST(KAPTAN_CONFIG_INFO_REQUEST_DATA {
request_id: 31,
target_system: 1,
target_component: 2,
});
assert_eq!(
decode_command(&message),
Ok(ConfigCommand::InfoRequest {
request_id: 31,
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 {
request_id: 7,
session_id: 8,
expected_revision: 9,
field_id: 10,
value: 1.5,
target_system: 0,
target_component: 2,
});
let zero_component = MavMessage::KAPTAN_CONFIG_SET_F32(KAPTAN_CONFIG_SET_F32_DATA {
request_id: 7,
session_id: 8,
expected_revision: 9,
field_id: 10,
value: 1.5,
target_system: 1,
target_component: 0,
});
assert_eq!(
decode_command(&zero_system),
Err(DecodeError::InvalidTarget)
);
assert_eq!(
decode_command(&zero_component),
Err(DecodeError::InvalidTarget)
);
}
#[test]
fn result_encodes_exact_validation_identity() {
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(
FieldId::new(11),
ValidationRule::NonFinite,
))),
state: ConfigState::Editing,
};
let encoded = encode_result(reply);
assert_eq!(encoded.primary_field, 11);
assert_eq!(encoded.validation_rule, ValidationRule::NonFinite.code());
assert_eq!(
encoded.error,
KaptanConfigError::KAPTAN_CONFIG_ERROR_NON_FINITE
);
}
#[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),
state: ConfigState::Active,
};
assert_eq!(
encode_result(reply).error,
KaptanConfigError::KAPTAN_CONFIG_ERROR_IMPORT_UNAVAILABLE
);
}
#[test]
fn info_preserves_service_capabilities() {
let info = encode_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);
}
}