feat(f722): add usb mavlink transport

This commit is contained in:
Onur Özkan committed 2026-09-06 23:14:00 +03:00
1 parent 7dc3ccb619
commit 604a7aba86
18 files changed
+1087 -208

No files matched your search

+57 -108
View File
@@ -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);
}
}