feat(f722): add usb mavlink transport
This commit is contained in:
1 parent
7dc3ccb619
commit
604a7aba86
18 files changed
+1087
-208
No files matched your search
@@ -10,10 +10,10 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
kaptan-err-reportable = { path = "../kaptan-err-reportable" }
|
||||
kaptan-dma = { path = "../kaptan-dma" }
|
||||
kaptan-mavlink = { path = "../kaptan-mavlink", default-features = false, features = [
|
||||
"sensor-telemetry",
|
||||
] }
|
||||
kaptan-sensor = { path = "../kaptan-sensor" }
|
||||
kaptan-time = { path = "../kaptan-time" }
|
||||
kaptan-tx = { path = "../kaptan-tx" }
|
||||
mavlink-core = { version = "0.18", default-features = false }
|
||||
@@ -1,21 +1,15 @@
|
||||
//! Complete outbound MAVLink data flow.
|
||||
//! Outbound MAVLink frame production.
|
||||
//!
|
||||
//! This crate owns fixed-rate scheduling, mailbox consumption, vertical-data
|
||||
//! freshness, MAVLink encoding, and bounded queue classification. A platform
|
||||
//! supplies only mailbox references, vertical-data freshness, and a transport
|
||||
//! sink.
|
||||
//! freshness, MAVLink encoding, and frame classification. Platforms retain
|
||||
//! transport ownership and decide where each produced frame is admitted.
|
||||
|
||||
#![no_std]
|
||||
|
||||
use kaptan_dma::TxCompletion;
|
||||
use kaptan_err_reportable::KaptanError;
|
||||
use kaptan_mavlink::{
|
||||
MavlinkFrame,
|
||||
encoder::MavlinkEncoder,
|
||||
generated::kaptan::MavMessage,
|
||||
transmitter::{EnqueueResult, FrameClass},
|
||||
};
|
||||
use kaptan_mavlink::{MavlinkFrame, encoder::MavlinkEncoder, generated::kaptan::MavMessage};
|
||||
use kaptan_sensor::{optical_flow::OpticalFlowSnapshot, range::RangeSnapshot};
|
||||
use kaptan_tx::{FrameClass, PendingFrame};
|
||||
use mavlink_core::MessageData;
|
||||
|
||||
mod mailbox;
|
||||
@@ -39,19 +33,6 @@ pub struct DataChannelRates {
|
||||
pub heartbeat_hz: usize,
|
||||
}
|
||||
|
||||
/// Platform transport used by a data channel.
|
||||
///
|
||||
/// The channel decides exactly which encoded frame is admitted and with which
|
||||
/// delivery class. The platform retains only DMA endpoint ownership.
|
||||
pub trait FrameSink {
|
||||
/// Admits one fully encoded frame to the platform's bounded transport.
|
||||
fn send(&mut self, class: FrameClass, frame: MavlinkFrame) -> EnqueueResult;
|
||||
|
||||
/// Applies one terminal DMA interrupt and starts the next retained frame
|
||||
/// when possible.
|
||||
fn on_dma_complete(&mut self, completion: TxCompletion);
|
||||
}
|
||||
|
||||
/// Mailboxes read by one data-channel worker.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct DataSources<'a> {
|
||||
@@ -77,9 +58,8 @@ pub enum DataChannelError {
|
||||
ScheduleFull,
|
||||
}
|
||||
|
||||
/// Complete outbound data flow for one MAVLink endpoint.
|
||||
pub struct DataChannel<'a, Sink, const SERVICE_HZ: usize> {
|
||||
sink: Sink,
|
||||
/// Scheduled outbound MAVLink frame producer.
|
||||
pub struct DataChannel<'a, const SERVICE_HZ: usize> {
|
||||
encoder: MavlinkEncoder,
|
||||
schedule: Schedule<SERVICE_HZ>,
|
||||
sources: DataSources<'a>,
|
||||
@@ -93,13 +73,9 @@ pub struct DataChannel<'a, Sink, const SERVICE_HZ: usize> {
|
||||
optical_flow_age_ticks: u8,
|
||||
}
|
||||
|
||||
impl<'a, Sink, const SERVICE_HZ: usize> DataChannel<'a, Sink, SERVICE_HZ>
|
||||
where
|
||||
Sink: FrameSink,
|
||||
{
|
||||
impl<'a, const SERVICE_HZ: usize> DataChannel<'a, SERVICE_HZ> {
|
||||
/// Creates an outbound data flow from already-validated runtime inputs.
|
||||
pub fn new(
|
||||
sink: Sink,
|
||||
system_id: u8,
|
||||
component_id: u8,
|
||||
rates: DataChannelRates,
|
||||
@@ -108,7 +84,6 @@ where
|
||||
) -> Result<Self, DataChannelError> {
|
||||
let schedule = Schedule::from_rates(rates)?;
|
||||
Ok(Self {
|
||||
sink,
|
||||
encoder: MavlinkEncoder::new(system_id, component_id),
|
||||
schedule,
|
||||
sources,
|
||||
@@ -125,57 +100,53 @@ where
|
||||
|
||||
/// Runs all outbound work due for one platform service tick.
|
||||
///
|
||||
/// No schedule token escapes this method. It refreshes cached vertical
|
||||
/// observations, selects due data, encodes the MAVLink message, and admits
|
||||
/// it with the correct bounded-queue class.
|
||||
pub fn on_service_tick(&mut self) {
|
||||
/// At most one classified frame is returned for transport admission.
|
||||
pub fn on_service_tick(&mut self) -> Option<PendingFrame<MavlinkFrame>> {
|
||||
self.refresh_vertical_data();
|
||||
|
||||
match self.schedule.next_due() {
|
||||
Some(DueData::Imu) => {
|
||||
if let Some(sample) = self.sources.imu.take_newer(&mut self.observed_imu) {
|
||||
let _ = self
|
||||
.sink
|
||||
.send(FrameClass::BestEffort, self.encoder.imu_snapshot(&sample));
|
||||
}
|
||||
self.sources
|
||||
.imu
|
||||
.take_newer(&mut self.observed_imu)
|
||||
.map(|sample| PendingFrame {
|
||||
class: FrameClass::BestEffort,
|
||||
frame: self.encoder.imu_snapshot(&sample),
|
||||
})
|
||||
}
|
||||
Some(DueData::Distance) if self.range_age_ticks <= self.maximum_vertical_age_ticks => {
|
||||
if let Some(range) = self.last_range {
|
||||
let _ = self
|
||||
.sink
|
||||
.send(FrameClass::BestEffort, self.encoder.range_snapshot(&range));
|
||||
}
|
||||
self.last_range.map(|range| PendingFrame {
|
||||
class: FrameClass::BestEffort,
|
||||
frame: self.encoder.range_snapshot(&range),
|
||||
})
|
||||
}
|
||||
Some(DueData::OpticalFlow)
|
||||
if self.optical_flow_age_ticks <= self.maximum_vertical_age_ticks =>
|
||||
{
|
||||
if let Some(flow) = self.last_optical_flow {
|
||||
let _ = self.sink.send(
|
||||
FrameClass::BestEffort,
|
||||
self.encoder.optical_flow_snapshot(&flow),
|
||||
);
|
||||
}
|
||||
self.last_optical_flow.map(|flow| PendingFrame {
|
||||
class: FrameClass::BestEffort,
|
||||
frame: self.encoder.optical_flow_snapshot(&flow),
|
||||
})
|
||||
}
|
||||
Some(DueData::Heartbeat) => {
|
||||
let _ = self.sink.send(FrameClass::Vital, self.encoder.heartbeat());
|
||||
}
|
||||
Some(DueData::Distance | DueData::OpticalFlow) | None => {}
|
||||
Some(DueData::Heartbeat) => Some(PendingFrame {
|
||||
class: FrameClass::Vital,
|
||||
frame: self.encoder.heartbeat(),
|
||||
}),
|
||||
Some(DueData::Distance | DueData::OpticalFlow) | None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Completes the active DMA lifecycle for this channel's transport.
|
||||
pub fn on_dma_complete(&mut self, completion: TxCompletion) {
|
||||
self.sink.on_dma_complete(completion)
|
||||
}
|
||||
|
||||
/// Encodes an adapter-owned generated MAVLink data record through the
|
||||
/// channel's sole encoder and bounded transport sink.
|
||||
pub fn send_message<M: MessageData<Message = MavMessage>>(
|
||||
/// Encodes an adapter-owned generated MAVLink record through the channel's
|
||||
/// sole sequence-number owner.
|
||||
pub fn encode_message<M: MessageData<Message = MavMessage>>(
|
||||
&mut self,
|
||||
class: FrameClass,
|
||||
message: &M,
|
||||
) -> EnqueueResult {
|
||||
self.sink.send(class, self.encoder.encode(message))
|
||||
) -> PendingFrame<MavlinkFrame> {
|
||||
PendingFrame {
|
||||
class,
|
||||
frame: self.encoder.encode(message),
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_vertical_data(&mut self) {
|
||||
@@ -276,39 +247,15 @@ impl<const SERVICE_HZ: usize> Schedule<SERVICE_HZ> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
DataChannel, DataChannelRates, DataSources, FrameSink, LatestImuMailbox,
|
||||
LatestOpticalFlowMailbox, LatestRangeMailbox,
|
||||
};
|
||||
use kaptan_dma::TxCompletion;
|
||||
use kaptan_mavlink::{
|
||||
MavlinkFrame,
|
||||
transmitter::{EnqueueResult, FrameClass},
|
||||
DataChannel, DataChannelRates, DataSources, LatestImuMailbox, LatestOpticalFlowMailbox,
|
||||
LatestRangeMailbox,
|
||||
};
|
||||
use kaptan_mavlink::generated::kaptan::MavMessage;
|
||||
use kaptan_sensor::{
|
||||
imu::ImuSnapshot, optical_flow::OpticalFlowSnapshot, range::RangeSnapshot,
|
||||
};
|
||||
use kaptan_time::Timestamp;
|
||||
|
||||
struct Sink {
|
||||
best_effort: u8,
|
||||
critical: u8,
|
||||
completions: u8,
|
||||
}
|
||||
|
||||
impl FrameSink for Sink {
|
||||
fn send(&mut self, class: FrameClass, _: MavlinkFrame) -> EnqueueResult {
|
||||
match class {
|
||||
FrameClass::Vital => self.critical += 1,
|
||||
FrameClass::BestEffort => self.best_effort += 1,
|
||||
FrameClass::Critical => self.critical += 1,
|
||||
}
|
||||
EnqueueResult::Enqueued
|
||||
}
|
||||
|
||||
fn on_dma_complete(&mut self, _: TxCompletion) {
|
||||
self.completions += 1;
|
||||
}
|
||||
}
|
||||
use kaptan_tx::FrameClass;
|
||||
|
||||
const RATES: DataChannelRates = DataChannelRates {
|
||||
imu_hz: 20,
|
||||
@@ -318,16 +265,11 @@ mod tests {
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn owns_rate_selection_encoding_admission_and_completion() {
|
||||
fn owns_rate_selection_encoding_and_classification() {
|
||||
let imu = LatestImuMailbox::new();
|
||||
let range = LatestRangeMailbox::new();
|
||||
let flow = LatestOpticalFlowMailbox::new();
|
||||
let mut channel: DataChannel<'_, Sink, 100> = DataChannel::new(
|
||||
Sink {
|
||||
best_effort: 0,
|
||||
critical: 0,
|
||||
completions: 0,
|
||||
},
|
||||
let mut channel: DataChannel<'_, 100> = DataChannel::new(
|
||||
1,
|
||||
1,
|
||||
RATES,
|
||||
@@ -339,6 +281,8 @@ mod tests {
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let mut best_effort = 0;
|
||||
let mut vital = 0;
|
||||
|
||||
for tick in 0..100 {
|
||||
imu.publish(ImuSnapshot::new(
|
||||
@@ -354,12 +298,17 @@ mod tests {
|
||||
100,
|
||||
100,
|
||||
));
|
||||
channel.on_service_tick();
|
||||
if let Some(frame) = channel.on_service_tick() {
|
||||
assert!(frame.frame.has_valid_crc::<MavMessage>());
|
||||
assert_eq!(frame.frame.sequence(), best_effort + vital);
|
||||
match frame.class {
|
||||
FrameClass::Vital => vital += 1,
|
||||
FrameClass::BestEffort => best_effort += 1,
|
||||
FrameClass::Critical => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
channel.on_dma_complete(TxCompletion::Complete);
|
||||
let sink = channel.sink;
|
||||
assert_eq!(sink.best_effort, 60);
|
||||
assert_eq!(sink.critical, 2);
|
||||
assert_eq!(sink.completions, 1);
|
||||
assert_eq!(best_effort, 60);
|
||||
assert_eq!(vital, 2);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use crate::{MavlinkFrame, decode_message, generated::kaptan::MavMessage};
|
||||
/// An unsigned MAVLink 2 frame whose ID and CRC match the Kaptan dialect.
|
||||
///
|
||||
/// The payload is deliberately not decoded during byte admission. Consumers
|
||||
/// decode it once after taking the frame from the bounded receive queue.
|
||||
/// decode it once at protocol dispatch.
|
||||
#[repr(transparent)]
|
||||
pub struct IntegrityCheckedFrame(MavlinkFrame);
|
||||
|
||||
@@ -32,14 +32,20 @@ impl IntegrityCheckedFrame {
|
||||
}
|
||||
}
|
||||
|
||||
struct IntegrityFramer {
|
||||
/// Incremental, unbuffered MAVLink 2 framing with Kaptan ID and CRC validation.
|
||||
///
|
||||
/// Consumers that can pause byte admission can process each returned frame
|
||||
/// directly. Interrupt-driven byte streams can use [`IntegrityMavlinkReceiver`]
|
||||
/// to queue complete frames instead. Accepted frames remain unauthenticated.
|
||||
pub struct IntegrityMavlinkFramer {
|
||||
frame: MavlinkFrame,
|
||||
received: usize,
|
||||
expected: usize,
|
||||
}
|
||||
|
||||
impl IntegrityFramer {
|
||||
const fn new() -> Self {
|
||||
impl IntegrityMavlinkFramer {
|
||||
/// Creates an empty framer.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
frame: MavlinkFrame::new(),
|
||||
received: 0,
|
||||
@@ -47,7 +53,8 @@ impl IntegrityFramer {
|
||||
}
|
||||
}
|
||||
|
||||
fn receive_byte(&mut self, byte: u8) -> Option<IntegrityCheckedFrame> {
|
||||
/// Consumes one byte, returning a complete integrity-checked frame if ready.
|
||||
pub fn receive_byte(&mut self, byte: u8) -> Option<IntegrityCheckedFrame> {
|
||||
if self.received == 0 {
|
||||
if byte == 0xfd {
|
||||
self.frame.as_mut_slice()[0] = byte;
|
||||
@@ -81,20 +88,27 @@ impl IntegrityFramer {
|
||||
IntegrityCheckedFrame::try_new(frame)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
/// Discards a partial frame after a stream discontinuity.
|
||||
pub fn reset(&mut self) {
|
||||
self.frame = MavlinkFrame::new();
|
||||
self.received = 0;
|
||||
self.expected = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IntegrityMavlinkFramer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Bounded integrity-only MAVLink 2 receiver.
|
||||
///
|
||||
/// It rejects signed, incompatible, corrupt, and MAVLink 1 frames, but an
|
||||
/// accepted frame is still unauthenticated. The composition decides whether
|
||||
/// and how a received request may be admitted.
|
||||
pub struct IntegrityMavlinkReceiver<const N: usize> {
|
||||
framer: IntegrityFramer,
|
||||
framer: IntegrityMavlinkFramer,
|
||||
frames: [Option<IntegrityCheckedFrame>; N],
|
||||
front: usize,
|
||||
length: usize,
|
||||
@@ -105,7 +119,7 @@ impl<const N: usize> IntegrityMavlinkReceiver<N> {
|
||||
pub const fn new() -> Self {
|
||||
assert!(N != 0, "integrity receiver capacity must be nonzero");
|
||||
Self {
|
||||
framer: IntegrityFramer::new(),
|
||||
framer: IntegrityMavlinkFramer::new(),
|
||||
frames: [const { None }; N],
|
||||
front: 0,
|
||||
length: 0,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Bounded protocol-neutral transmit admission and ownership lifecycle.
|
||||
//!
|
||||
//! Protocol encoders own frame bytes. This crate owns only bounded priority
|
||||
//! admission and the transition from pending frame to active DMA ownership.
|
||||
//! admission and the transition from pending frame to active transport ownership.
|
||||
|
||||
#![no_std]
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub enum FrameClass {
|
||||
/// Heartbeat and current safety/health status. A pending vital frame may
|
||||
/// displace any pending non-vital frame; active DMA storage is untouched.
|
||||
/// displace any pending non-vital frame; active storage is untouched.
|
||||
Vital,
|
||||
/// Fault transitions and authoritative replies that best-effort samples
|
||||
/// cannot displace.
|
||||
@@ -147,12 +147,12 @@ impl<Frame: Copy, const N: usize> TransmitQueue<Frame, N> {
|
||||
Ok(StartOutcome::Started)
|
||||
}
|
||||
|
||||
/// Releases the active frame after a terminal DMA completion.
|
||||
/// Releases the active frame after the transport no longer borrows it.
|
||||
pub fn complete(&mut self) -> Option<PendingFrame<Frame>> {
|
||||
self.active.take()
|
||||
}
|
||||
|
||||
/// Releases the active frame after a terminal DMA fault.
|
||||
/// Releases the active frame after a terminal transport fault.
|
||||
pub fn fail(&mut self) -> Option<PendingFrame<Frame>> {
|
||||
self.active.take()
|
||||
}
|
||||
@@ -162,6 +162,14 @@ impl<Frame: Copy, const N: usize> TransmitQueue<Frame, N> {
|
||||
self.active.is_some()
|
||||
}
|
||||
|
||||
/// Borrows the frame currently owned by the active transport operation.
|
||||
///
|
||||
/// Packetized transports use this to retain one selected frame while
|
||||
/// advancing it across multiple bounded endpoint writes.
|
||||
pub const fn active(&self) -> Option<&PendingFrame<Frame>> {
|
||||
self.active.as_ref()
|
||||
}
|
||||
|
||||
fn take_next(&mut self) -> Option<PendingFrame<Frame>> {
|
||||
let vital = self
|
||||
.pending
|
||||
@@ -203,6 +211,7 @@ mod tests {
|
||||
Ok(StartOutcome::Started)
|
||||
);
|
||||
assert!(queue.is_active());
|
||||
assert_eq!(queue.active().map(|pending| pending.frame), Some(1));
|
||||
|
||||
assert_eq!(
|
||||
queue.enqueue(PendingFrame {
|
||||
@@ -212,6 +221,7 @@ mod tests {
|
||||
EnqueueResult::Enqueued
|
||||
);
|
||||
assert_eq!(queue.complete().unwrap().frame, 1);
|
||||
assert!(queue.active().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user