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
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user