refactor: move firmware workspace under firmware

This commit is contained in:
Onur Özkan committed 2026-08-10 16:58:55 +03:00
1 parent 153b0bf262
commit f03fc119a4
222 files changed
+56 -13

No files matched your search

@@ -0,0 +1,248 @@
//! 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
}
@@ -0,0 +1,208 @@
//! Read-only protocol discovery and value-query responses.
use kaptan_config::{
ConfigManager, ConfigModel, ConfigState, ConfigValue, FieldDescriptor, FieldId, Generation,
ProfileStore,
};
use super::{import::SnapshotImporter, session::ConfigProtocol};
use crate::{ProtocolError, ValueSource};
/// Bounded descriptor page returned by a schema discovery request.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DescriptorPage {
/// 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],
}
/// One typed value read from an explicitly selected configuration snapshot.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ValueResponse {
/// Request being answered.
pub request_id: u32,
/// Transaction session for transaction values, otherwise zero.
pub session_id: u32,
/// Current transaction revision, otherwise zero.
pub revision: u32,
/// Requested stable field identity.
pub field: FieldId,
/// Declared scalar value.
pub value: ConfigValue,
}
/// Correlated protocol/schema discovery response.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
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.
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.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ServiceResponse {
/// Correlated protocol/schema discovery response.
Info(ConfigInfo),
/// Completion for a mutating operation or rejected query.
Result(crate::ConfigReply),
/// Bounded self-describing schema page.
Descriptors(DescriptorPage),
/// One typed scalar value.
Value(ValueResponse),
}
impl<I> ConfigProtocol<I> {
/// Returns static descriptor metadata by stable schema index.
pub fn descriptor<C, S>(
&self,
manager: &ConfigManager<C, S>,
index: u16,
) -> Option<&'static FieldDescriptor>
where
C: ConfigModel,
S: ProfileStore<C>,
{
manager.descriptor(index)
}
/// Reads one typed value without opening or changing a transaction.
pub fn value<C, S>(
&self,
manager: &ConfigManager<C, S>,
source: ValueSource,
field: FieldId,
) -> Option<ConfigValue>
where
C: ConfigModel,
S: ProfileStore<C>,
{
match source {
ValueSource::Active => manager.active_value(field),
ValueSource::Committed => manager.committed_value(field),
ValueSource::Transaction => manager.transaction_value(field),
}
}
pub(super) fn descriptors<C, S>(
&self,
manager: &ConfigManager<C, S>,
request_id: u32,
first: u16,
requested: u16,
) -> DescriptorPage
where
C: ConfigModel,
S: ProfileStore<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 {
request_id,
first,
len,
descriptors,
}
}
pub(super) fn info<C, S>(&self, manager: &ConfigManager<C, S>, request_id: u32) -> ConfigInfo
where
C: ConfigModel,
S: ProfileStore<C>,
I: SnapshotImporter<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,
request_id: u32,
session_id: u32,
field: FieldId,
source: ValueSource,
) -> ServiceResponse
where
C: ConfigModel,
S: ProfileStore<C>,
{
let (value, reply_session, revision) = match source {
ValueSource::Active => (manager.active_value(field), 0, 0),
ValueSource::Committed => (manager.committed_value(field), 0, 0),
ValueSource::Transaction => {
let Some(session) = self.session else {
return ServiceResponse::Result(self.rejected_for(
request_id,
session_id,
ProtocolError::InvalidSession,
));
};
if session.id != session_id || session.owner != owner {
return ServiceResponse::Result(self.rejected_for(
request_id,
session_id,
ProtocolError::InvalidSession,
));
}
(
manager.transaction_value(field),
session.id,
session.revision,
)
}
};
let Some(value) = value else {
return ServiceResponse::Result(self.rejected_for(
request_id,
session_id,
ProtocolError::UnknownField(field),
));
};
ServiceResponse::Value(ValueResponse {
request_id,
session_id: reply_session,
revision,
field,
value,
})
}
}
@@ -0,0 +1,272 @@
//! Bounded request-replay cache for idempotent client retries.
use kaptan_config::ConfigValue;
use super::session::ConfigProtocol;
use crate::{ConfigCommand, ConfigReply, ProtocolError};
pub(super) const REQUEST_CACHE_CAPACITY: usize = 8;
#[derive(Clone, Copy)]
pub(super) struct RememberedResult {
pub(super) owner: u64,
pub(super) command: ConfigCommand,
pub(super) reply: ConfigReply,
}
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));
}
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;
}
}
fn same_command(left: ConfigCommand, right: ConfigCommand) -> bool {
match (left, right) {
(
ConfigCommand::InfoRequest {
request_id: left_request,
target: left_target,
},
ConfigCommand::InfoRequest {
request_id: right_request,
target: right_target,
},
) => left_request == right_request && left_target == right_target,
(
ConfigCommand::DescriptorRequest {
request_id: left_request,
first: left_first,
count: left_count,
target: left_target,
},
ConfigCommand::DescriptorRequest {
request_id: right_request,
first: right_first,
count: right_count,
target: right_target,
},
) => {
left_request == right_request
&& left_first == right_first
&& left_count == right_count
&& left_target == right_target
}
(
ConfigCommand::ValueRequest {
request_id: left_request,
session_id: left_session,
field: left_field,
source: left_source,
target: left_target,
},
ConfigCommand::ValueRequest {
request_id: right_request,
session_id: right_session,
field: right_field,
source: right_source,
target: right_target,
},
) => {
left_request == right_request
&& left_session == right_session
&& left_field == right_field
&& left_source == right_source
&& left_target == right_target
}
(
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
}
(
ConfigCommand::Set {
request_id: left_request,
session_id: left_session,
expected_revision: left_revision,
field: left_field,
value: left_value,
target: left_target,
},
ConfigCommand::Set {
request_id: right_request,
session_id: right_session,
expected_revision: right_revision,
field: right_field,
value: right_value,
target: right_target,
},
) => {
left_request == right_request
&& left_session == right_session
&& left_revision == right_revision
&& left_field == right_field
&& same_value(left_value, right_value)
&& left_target == right_target
}
(
ConfigCommand::Validate {
request_id: left_request,
session_id: left_session,
expected_revision: left_revision,
target: left_target,
},
ConfigCommand::Validate {
request_id: right_request,
session_id: right_session,
expected_revision: right_revision,
target: right_target,
},
) => {
left_request == right_request
&& left_session == right_session
&& left_revision == right_revision
&& left_target == right_target
}
(
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
}
(
ConfigCommand::Abort {
request_id: left_request,
session_id: left_session,
target: left_target,
},
ConfigCommand::Abort {
request_id: right_request,
session_id: right_session,
target: right_target,
},
) => {
left_request == right_request
&& 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,
}
}
fn same_value(left: ConfigValue, right: ConfigValue) -> bool {
left.kind() == right.kind() && left.raw_bits() == right.raw_bits()
}
File diff suppressed because it is too large. Load diff