//! 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 { 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 { match FieldId::try_new(value) { Some(value) => Ok(value), None => Err(DecodeError::InvalidFieldId), } } const fn value_source(value: KaptanConfigValueSource) -> Result { 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 { 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) -> 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); } }