refactor: move firmware workspace under firmware
This commit is contained in:
1 parent
153b0bf262
commit
f03fc119a4
222 files changed
+56
-13
No files matched your search
@@ -0,0 +1,356 @@
|
||||
use std::fs;
|
||||
|
||||
use kaptan_config::{ConfigModel, Generation, ProfileStore};
|
||||
use kaptan_config_mavlink::{
|
||||
Authorized, ConfigCommand, ResultKind, ServiceResponse, SnapshotImporter,
|
||||
};
|
||||
use kaptan_flight_config::{
|
||||
CandidateProfile, CborError, MAXIMUM_CANDIDATE_BYTES, SCHEMA_ID as PROFILE_SCHEMA_ID,
|
||||
decode_candidate, encode_candidate,
|
||||
};
|
||||
use kaptan_log_mavlink::DiagnosticPayload;
|
||||
use kaptan_mavlink::{
|
||||
encoder::MavlinkEncoder,
|
||||
message::{Message, kaptan::MavMessage},
|
||||
};
|
||||
|
||||
use super::{CAPABILITIES, FileProfileStore, SitlSnapshotImporter, configuration_service};
|
||||
|
||||
// Independently authored current-format CBOR map. It is intentionally not
|
||||
// produced by `encode_candidate`: this is the GCS/upload compatibility vector
|
||||
// for schema KAPTANPROFILE-01, with a non-default 1.7 kg mass.
|
||||
const EXTERNAL_PROFILE_CBOR: &[u8] = &[
|
||||
0xb8, 0x1a, // definite map, 26 entries
|
||||
0x00, 0x01, // format key/value
|
||||
0x19, 0xff, 0xff, 0x50, b'K', b'A', b'P', b'T', b'A', b'N', b'P', b'R', b'O', b'F', b'I', b'L',
|
||||
b'E', b'-', b'0', b'1', // schema key/value
|
||||
0x01, 0x1a, 0x00, 0x01, 0x86, 0xa0, // prearm dwell: 100000
|
||||
0x02, 0x19, 0xc3, 0x50, // IMU interval: 50000
|
||||
0x03, 0x19, 0xc3, 0x50, // IMU freshness: 50000
|
||||
0x04, 0x1a, 0x00, 0x02, 0x49, 0xf0, // local freshness: 150000
|
||||
0x05, 0x1a, 0x00, 0x07, 0xa1, 0x20, // link freshness: 500000
|
||||
0x06, 0xfa, 0x3f, 0x80, 0x00, 0x00, 0x07, 0xfa, 0x3f, 0x80, 0x00, 0x00, 0x08, 0xfa, 0x3f, 0x80,
|
||||
0x00, 0x00, 0x09, 0xfa, 0x3f, 0x80, 0x00, 0x00, 0x0a, 0xfa, 0x3f, 0x80, 0x00, 0x00, 0x0b, 0xfa,
|
||||
0x3f, 0x80, 0x00, 0x00, 0x0c, 0xfa, 0x3f, 0x80, 0x00, 0x00, 0x0d, 0xfa, 0x3f, 0xd9, 0x99,
|
||||
0x9a, // mass: 1.7
|
||||
0x0e, 0xfa, 0x41, 0xa0, 0x00, 0x00, // collective thrust: 20
|
||||
0x0f, 0xfa, 0x3f, 0x80, 0x00, 0x00, 0x10, 0xfa, 0x3f, 0x80, 0x00, 0x00, 0x11, 0xfa, 0x3f, 0x80,
|
||||
0x00, 0x00, 0x12, 0xfa, 0x00, 0x00, 0x00, 0x00, 0x13, 0xfa, 0x3f, 0x80, 0x00, 0x00, 0x14, 0xfa,
|
||||
0x3f, 0x80, 0x00, 0x00, 0x15, 0xfa, 0x00, 0x00, 0x00, 0x00, 0x16, 0xfa, 0x3f, 0x80, 0x00, 0x00,
|
||||
0x17, 0xfa, 0x3f, 0x80, 0x00, 0x00, 0x18, 0x18, 0x06, // maximum log level: INFO
|
||||
];
|
||||
|
||||
fn result(response: ServiceResponse) -> kaptan_config_mavlink::ConfigReply {
|
||||
match response {
|
||||
ServiceResponse::Result(reply) => reply,
|
||||
ServiceResponse::Info(_) | ServiceResponse::Descriptors(_) | ServiceResponse::Value(_) => {
|
||||
panic!("expected a mutating configuration result")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_store_round_trips_a_complete_validated_snapshot() {
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"kaptan-config-store-{}-{}.cbor",
|
||||
std::process::id(),
|
||||
std::thread::current().name().unwrap_or("test")
|
||||
));
|
||||
let temporary = path.with_extension("cbor.tmp");
|
||||
let _ = fs::remove_file(&path);
|
||||
let _ = fs::remove_file(&temporary);
|
||||
|
||||
let mut store = FileProfileStore::new(&path);
|
||||
assert!(store.load().unwrap().is_none());
|
||||
assert_eq!(
|
||||
store.persist(&CandidateProfile::default()).unwrap(),
|
||||
Generation::new(1)
|
||||
);
|
||||
let envelope = fs::read(&path).unwrap();
|
||||
assert!(matches!(
|
||||
decode_candidate(&envelope, &CAPABILITIES),
|
||||
Err(CborError::Malformed)
|
||||
));
|
||||
let (_, candidate) = store.load().unwrap().expect("persisted candidate");
|
||||
assert!(candidate.validate(&CAPABILITIES).is_ok());
|
||||
|
||||
assert_eq!(
|
||||
store.persist(&CandidateProfile::default()).unwrap(),
|
||||
Generation::new(2)
|
||||
);
|
||||
let mut reopened = FileProfileStore::new(&path);
|
||||
assert_eq!(
|
||||
reopened.load().unwrap().map(|(generation, _)| generation),
|
||||
Some(Generation::new(2))
|
||||
);
|
||||
let mut fresh = FileProfileStore::new(&path);
|
||||
assert_eq!(
|
||||
fresh.persist(&CandidateProfile::default()).unwrap(),
|
||||
Generation::new(3)
|
||||
);
|
||||
|
||||
fs::remove_file(&path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sitl_snapshot_importer_uses_the_real_schema_and_capabilities() {
|
||||
let candidate = CandidateProfile::default();
|
||||
let mut writer = minicbor::encode::write::Cursor::new([0_u8; MAXIMUM_CANDIDATE_BYTES]);
|
||||
encode_candidate(candidate, &CAPABILITIES, &mut writer).unwrap();
|
||||
let length = writer.position();
|
||||
let bytes = writer.into_inner();
|
||||
let imported = SitlSnapshotImporter
|
||||
.decode(&bytes[..length])
|
||||
.expect("valid current candidate import");
|
||||
assert!(imported.validate(&CAPABILITIES).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sitl_service_imports_a_real_cbor_candidate_only_into_its_transaction() {
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"kaptan-config-import-{}-{}.cbor",
|
||||
std::process::id(),
|
||||
std::thread::current().name().unwrap_or("test")
|
||||
));
|
||||
let _ = fs::remove_file(&path);
|
||||
let mut service = configuration_service(FileProfileStore::new(&path)).unwrap();
|
||||
let begin = result(service.dispatch(Authorized::host(
|
||||
9,
|
||||
ConfigCommand::Begin {
|
||||
request_id: 1,
|
||||
expected_generation: 0,
|
||||
target: kaptan_config_mavlink::Target::new(1, 1),
|
||||
},
|
||||
)));
|
||||
let mut writer = minicbor::encode::write::Cursor::new([0_u8; MAXIMUM_CANDIDATE_BYTES]);
|
||||
encode_candidate(CandidateProfile::default(), &CAPABILITIES, &mut writer).unwrap();
|
||||
let length = writer.position();
|
||||
assert!(length <= 192);
|
||||
let bytes = writer.into_inner();
|
||||
let import_begin = result(service.dispatch(Authorized::host(
|
||||
9,
|
||||
ConfigCommand::ImportBegin {
|
||||
request_id: 2,
|
||||
session_id: begin.session_id,
|
||||
expected_revision: 0,
|
||||
length: length as u16,
|
||||
schema_id: PROFILE_SCHEMA_ID,
|
||||
crc32c: crc32c(&bytes[..length]),
|
||||
target: kaptan_config_mavlink::Target::new(1, 1),
|
||||
},
|
||||
)));
|
||||
assert_eq!(import_begin.result, ResultKind::Accepted);
|
||||
let mut chunk = [0; 192];
|
||||
chunk[..length].copy_from_slice(&bytes[..length]);
|
||||
assert_eq!(
|
||||
result(service.dispatch(Authorized::host(
|
||||
9,
|
||||
ConfigCommand::ImportChunk {
|
||||
request_id: 3,
|
||||
session_id: begin.session_id,
|
||||
transfer_id: import_begin.transfer_id,
|
||||
offset: 0,
|
||||
length: length as u8,
|
||||
data: chunk,
|
||||
target: kaptan_config_mavlink::Target::new(1, 1),
|
||||
},
|
||||
)))
|
||||
.result,
|
||||
ResultKind::Accepted
|
||||
);
|
||||
assert_eq!(
|
||||
result(service.dispatch(Authorized::host(
|
||||
9,
|
||||
ConfigCommand::ImportFinish {
|
||||
request_id: 4,
|
||||
session_id: begin.session_id,
|
||||
transfer_id: import_begin.transfer_id,
|
||||
expected_revision: 0,
|
||||
target: kaptan_config_mavlink::Target::new(1, 1),
|
||||
},
|
||||
)))
|
||||
.result,
|
||||
ResultKind::Accepted
|
||||
);
|
||||
assert!(
|
||||
service
|
||||
.manager()
|
||||
.transaction_value(kaptan_config::FieldId::new(1))
|
||||
.is_some()
|
||||
);
|
||||
assert!(
|
||||
service
|
||||
.manager()
|
||||
.active_value(kaptan_config::FieldId::new(1))
|
||||
.is_some()
|
||||
);
|
||||
let _ = fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_gcs_vector_imports_commits_and_reopens_as_the_boot_profile() {
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"kaptan-config-gcs-vector-{}-{}.cbor",
|
||||
std::process::id(),
|
||||
std::thread::current().name().unwrap_or("test")
|
||||
));
|
||||
let _ = fs::remove_file(&path);
|
||||
let mut service = configuration_service(FileProfileStore::new(&path)).unwrap();
|
||||
let target = kaptan_config_mavlink::Target::new(1, 1);
|
||||
let begin = result(service.dispatch(Authorized::host(
|
||||
9,
|
||||
ConfigCommand::Begin {
|
||||
request_id: 1,
|
||||
expected_generation: 0,
|
||||
target,
|
||||
},
|
||||
)));
|
||||
let import = result(service.dispatch(Authorized::host(
|
||||
9,
|
||||
ConfigCommand::ImportBegin {
|
||||
request_id: 2,
|
||||
session_id: begin.session_id,
|
||||
expected_revision: 0,
|
||||
length: EXTERNAL_PROFILE_CBOR.len() as u16,
|
||||
schema_id: PROFILE_SCHEMA_ID,
|
||||
crc32c: crc32c(EXTERNAL_PROFILE_CBOR),
|
||||
target,
|
||||
},
|
||||
)));
|
||||
let mut data = [0_u8; 192];
|
||||
data[..EXTERNAL_PROFILE_CBOR.len()].copy_from_slice(EXTERNAL_PROFILE_CBOR);
|
||||
assert_eq!(
|
||||
result(service.dispatch(Authorized::host(
|
||||
9,
|
||||
ConfigCommand::ImportChunk {
|
||||
request_id: 3,
|
||||
session_id: begin.session_id,
|
||||
transfer_id: import.transfer_id,
|
||||
offset: 0,
|
||||
length: EXTERNAL_PROFILE_CBOR.len() as u8,
|
||||
data,
|
||||
target,
|
||||
},
|
||||
)))
|
||||
.result,
|
||||
ResultKind::Accepted
|
||||
);
|
||||
assert_eq!(
|
||||
result(service.dispatch(Authorized::host(
|
||||
9,
|
||||
ConfigCommand::ImportFinish {
|
||||
request_id: 4,
|
||||
session_id: begin.session_id,
|
||||
transfer_id: import.transfer_id,
|
||||
expected_revision: 0,
|
||||
target,
|
||||
},
|
||||
)))
|
||||
.revision,
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
result(service.dispatch(Authorized::host(
|
||||
9,
|
||||
ConfigCommand::Commit {
|
||||
request_id: 5,
|
||||
session_id: begin.session_id,
|
||||
expected_generation: 0,
|
||||
expected_revision: 1,
|
||||
target,
|
||||
},
|
||||
)))
|
||||
.result,
|
||||
ResultKind::CommittedRestartRequired
|
||||
);
|
||||
drop(service);
|
||||
|
||||
let reopened = configuration_service(FileProfileStore::new(&path)).unwrap();
|
||||
assert_eq!(reopened.active_profile().thrust().mass_kg(), 1.7);
|
||||
let _ = fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_log_macro_reaches_the_generated_mavlink_log_payload() {
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"kaptan-sitl-log-{}-{}.cbor",
|
||||
std::process::id(),
|
||||
std::thread::current().name().unwrap_or("test")
|
||||
));
|
||||
let _ = fs::remove_file(&path);
|
||||
let mut service = configuration_service(FileProfileStore::new(&path)).unwrap();
|
||||
|
||||
log::info!("SITL configuration service ready");
|
||||
let Some(DiagnosticPayload::Log(payload)) = service.pending_diagnostic_payload() else {
|
||||
panic!("info facade record must reach KAPTAN_LOG");
|
||||
};
|
||||
assert_eq!(payload.level, kaptan_log::Severity::Info as u8);
|
||||
let message = std::str::from_utf8(&payload.message[..usize::from(payload.message_length)])
|
||||
.expect("facade text remains UTF-8");
|
||||
assert!(message.contains("config::tests"));
|
||||
assert!(message.ends_with("SITL configuration service ready"));
|
||||
let mut encoder = MavlinkEncoder::new(1, 1);
|
||||
let frame = encoder.custom(&payload);
|
||||
assert_eq!(frame.message_id(), 51_020);
|
||||
assert!(matches!(
|
||||
MavMessage::parse(
|
||||
mavlink::MavlinkVersion::V2,
|
||||
frame.message_id(),
|
||||
frame.payload(),
|
||||
),
|
||||
Ok(MavMessage::KAPTAN_LOG(data))
|
||||
if data.level == payload.level
|
||||
&& data.message_length == payload.message_length
|
||||
&& data.message == payload.message
|
||||
));
|
||||
let _ = fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejected_config_result_enqueues_a_separate_high_level_error_payload() {
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"kaptan-sitl-config-error-{}-{}.cbor",
|
||||
std::process::id(),
|
||||
std::thread::current().name().unwrap_or("test")
|
||||
));
|
||||
let _ = fs::remove_file(&path);
|
||||
let mut service = configuration_service(FileProfileStore::new(&path)).unwrap();
|
||||
let response = service.dispatch(Authorized::host(
|
||||
9,
|
||||
ConfigCommand::Begin {
|
||||
request_id: 1,
|
||||
expected_generation: 1,
|
||||
target: kaptan_config_mavlink::Target::new(1, 1),
|
||||
},
|
||||
));
|
||||
let reply = result(response);
|
||||
assert_eq!(reply.result, ResultKind::Rejected);
|
||||
service.after_config_result(response);
|
||||
|
||||
let Some(DiagnosticPayload::Error(payload)) = service.pending_diagnostic_payload() else {
|
||||
panic!("rejected configuration result must enqueue KAPTAN_ERROR");
|
||||
};
|
||||
let error_path = std::str::from_utf8(&payload.path[..usize::from(payload.path_length)])
|
||||
.expect("derived error path remains UTF-8");
|
||||
assert_eq!(
|
||||
error_path,
|
||||
"kaptan_config_mavlink::protocol::ProtocolError::GenerationConflict"
|
||||
);
|
||||
assert_eq!(payload.additional_information_length, 0);
|
||||
let _ = fs::remove_file(&path);
|
||||
}
|
||||
Reference in new issue
Block a user