workspace: replace runtime with F722 platform
This commit is contained in:
1 parent
82b0f41237
commit
c9ca2f4257
7 files changed
+50
-576
No files matched your search
Generated
+46
-35
@@ -139,6 +139,21 @@ dependencies = [
|
||||
name = "kaptan-actuator"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "kaptan-board"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "kaptan-data-channel"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"kaptan-board",
|
||||
"kaptan-dma",
|
||||
"kaptan-mavlink",
|
||||
"kaptan-sensor",
|
||||
"kaptan-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kaptan-dma"
|
||||
version = "0.1.0"
|
||||
@@ -153,6 +168,14 @@ dependencies = [
|
||||
"kaptan-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kaptan-geprc-f722-v2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"kaptan-board",
|
||||
"platform-stm32f7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kaptan-i2c"
|
||||
version = "0.1.0"
|
||||
@@ -161,18 +184,8 @@ version = "0.1.0"
|
||||
name = "kaptan-kakute-f7-mini-v3"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cortex-m",
|
||||
"cortex-m-rt",
|
||||
"kaptan-actuator",
|
||||
"kaptan-dma",
|
||||
"kaptan-drivers",
|
||||
"kaptan-mavlink",
|
||||
"kaptan-runtime",
|
||||
"kaptan-spi",
|
||||
"kaptan-stm32f7",
|
||||
"panic-halt",
|
||||
"rtic",
|
||||
"stm32f7",
|
||||
"kaptan-board",
|
||||
"platform-stm32f7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -188,17 +201,6 @@ dependencies = [
|
||||
"mavlink-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kaptan-runtime"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"kaptan-drivers",
|
||||
"kaptan-i2c",
|
||||
"kaptan-sensor",
|
||||
"kaptan-spi",
|
||||
"kaptan-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kaptan-sensor"
|
||||
version = "0.1.0"
|
||||
@@ -210,18 +212,6 @@ dependencies = [
|
||||
name = "kaptan-spi"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "kaptan-stm32f7"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cortex-m",
|
||||
"kaptan-dma",
|
||||
"kaptan-i2c",
|
||||
"kaptan-spi",
|
||||
"kaptan-time",
|
||||
"stm32f7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kaptan-time"
|
||||
version = "0.1.0"
|
||||
@@ -316,6 +306,27 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a513e167849a384b7f9b746e517604398518590a9142f4846a32e3c2a4de7b11"
|
||||
|
||||
[[package]]
|
||||
name = "platform-stm32f7"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cortex-m",
|
||||
"cortex-m-rt",
|
||||
"kaptan-actuator",
|
||||
"kaptan-board",
|
||||
"kaptan-data-channel",
|
||||
"kaptan-dma",
|
||||
"kaptan-drivers",
|
||||
"kaptan-i2c",
|
||||
"kaptan-mavlink",
|
||||
"kaptan-sensor",
|
||||
"kaptan-spi",
|
||||
"kaptan-time",
|
||||
"panic-halt",
|
||||
"rtic",
|
||||
"stm32f7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.14.0"
|
||||
|
||||
+4
-2
@@ -1,17 +1,19 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"boards/geprc-f722-v2",
|
||||
"boards/kakute-f7-mini-v3",
|
||||
"libs/kaptan-actuator",
|
||||
"libs/kaptan-board",
|
||||
"libs/kaptan-data-channel",
|
||||
"libs/kaptan-dma",
|
||||
"libs/kaptan-drivers",
|
||||
"libs/kaptan-i2c",
|
||||
"libs/kaptan-mavlink",
|
||||
"libs/kaptan-runtime",
|
||||
"libs/kaptan-sensor",
|
||||
"libs/kaptan-spi",
|
||||
"libs/kaptan-stm32f7",
|
||||
"libs/kaptan-time",
|
||||
"libs/kaptan-tx",
|
||||
"platforms/stm32f7",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "kaptan-runtime"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
kaptan-drivers = { path = "../kaptan-drivers" }
|
||||
kaptan-i2c = { path = "../kaptan-i2c" }
|
||||
kaptan-sensor = { path = "../kaptan-sensor" }
|
||||
kaptan-spi = { path = "../kaptan-spi" }
|
||||
kaptan-time = { path = "../kaptan-time" }
|
||||
@@ -1,148 +0,0 @@
|
||||
//! Single-owner MPU acquisition service.
|
||||
|
||||
use kaptan_drivers::mpu6000::{Error as Mpu6000Error, Mpu6000, Mpu6000Event};
|
||||
use kaptan_sensor::imu::ImuSnapshot;
|
||||
use kaptan_spi::{SpiCompletion, SpiDevice};
|
||||
use kaptan_time::MonotonicClock;
|
||||
|
||||
/// One observable result from the IMU worker.
|
||||
#[derive(Debug)]
|
||||
pub enum ImuEvent<BusError> {
|
||||
/// No accepted sample or terminal fault was produced.
|
||||
Idle,
|
||||
/// MPU configuration completed and the DRDY route may be enabled.
|
||||
Initialized,
|
||||
/// A coherent IMU observation is ready for telemetry or control.
|
||||
Sample(ImuSnapshot),
|
||||
/// The selected sensor path entered a terminal fault state.
|
||||
Fault(Mpu6000Error<BusError>),
|
||||
}
|
||||
|
||||
/// Owns MPU state, its clock, and sample acceptance.
|
||||
///
|
||||
/// Exactly one worker invokes this service. Hardware ISRs only report deferred
|
||||
/// DRDY, timer, and SPI-DMA terminal events to that worker.
|
||||
pub struct ImuService<Clock, Bus> {
|
||||
clock: Clock,
|
||||
imu: Mpu6000<Bus>,
|
||||
}
|
||||
|
||||
impl<Clock, Bus> ImuService<Clock, Bus> {
|
||||
/// Creates an inactive acquisition service.
|
||||
pub const fn new(clock: Clock, imu: Mpu6000<Bus>) -> Self {
|
||||
Self { clock, imu }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Clock, Bus> ImuService<Clock, Bus>
|
||||
where
|
||||
Clock: MonotonicClock,
|
||||
Bus: SpiDevice,
|
||||
{
|
||||
/// Submits the asynchronous sensor reset transaction.
|
||||
pub fn begin(&mut self) -> Result<(), Mpu6000Error<Bus::StartError>> {
|
||||
self.imu.begin_initialization()
|
||||
}
|
||||
|
||||
/// Advances the reset-settling state on a board periodic event.
|
||||
pub fn on_periodic_tick(&mut self) -> ImuEvent<Bus::StartError> {
|
||||
if self.imu.is_ready() {
|
||||
return match self.imu.request_sample(self.clock.now()) {
|
||||
Ok(()) | Err(Mpu6000Error::NotReady) => ImuEvent::Idle,
|
||||
Err(error) => ImuEvent::Fault(error),
|
||||
};
|
||||
}
|
||||
|
||||
let _ = self.clock.now();
|
||||
match self.imu.advance_initialization() {
|
||||
Ok(()) => ImuEvent::Idle,
|
||||
Err(error) => ImuEvent::Fault(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles one coalesced data-ready event.
|
||||
pub fn on_data_ready(&mut self) -> ImuEvent<Bus::StartError> {
|
||||
if !self.imu.is_ready() {
|
||||
return ImuEvent::Idle;
|
||||
}
|
||||
|
||||
match self.imu.request_sample(self.clock.now()) {
|
||||
Ok(()) | Err(Mpu6000Error::NotReady) => ImuEvent::Idle,
|
||||
Err(error) => ImuEvent::Fault(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// Advances the MPU transaction state from a DMA terminal result.
|
||||
pub fn on_transfer_complete(&mut self, completion: SpiCompletion) -> ImuEvent<Bus::StartError> {
|
||||
match self.imu.on_transfer_complete(completion) {
|
||||
Mpu6000Event::NotPending | Mpu6000Event::Initializing => ImuEvent::Idle,
|
||||
Mpu6000Event::Initialized => ImuEvent::Initialized,
|
||||
Mpu6000Event::Sample(sample) => ImuEvent::Sample(sample),
|
||||
Mpu6000Event::Fault(error) => ImuEvent::Fault(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ImuEvent, ImuService};
|
||||
use kaptan_drivers::mpu6000::{Mpu6000, Mpu6000Config};
|
||||
use kaptan_spi::{SpiCompletion, SpiDevice};
|
||||
use kaptan_time::{MonotonicClock, Timestamp};
|
||||
|
||||
struct Clock;
|
||||
|
||||
impl MonotonicClock for Clock {
|
||||
fn now(&mut self) -> Timestamp {
|
||||
Timestamp::from_microseconds(42)
|
||||
}
|
||||
}
|
||||
|
||||
struct Bus {
|
||||
response: [u8; 15],
|
||||
}
|
||||
|
||||
impl SpiDevice for Bus {
|
||||
type StartError = ();
|
||||
|
||||
fn start_transfer(&mut self, _: &[u8], receive: &mut [u8]) -> Result<(), Self::StartError> {
|
||||
receive.copy_from_slice(&self.response[..receive.len()]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn complete_transfer(&mut self, _: SpiCompletion) {}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_a_sample_only_after_initialization() {
|
||||
let mut response = [0; 15];
|
||||
response[1] = 0x68;
|
||||
let mut service = ImuService::new(
|
||||
Clock,
|
||||
Mpu6000::new(Bus { response }, Mpu6000Config::default()),
|
||||
);
|
||||
|
||||
service.begin().unwrap();
|
||||
assert!(matches!(service.on_data_ready(), ImuEvent::Idle));
|
||||
assert!(matches!(
|
||||
service.on_transfer_complete(SpiCompletion::Complete),
|
||||
ImuEvent::Idle
|
||||
));
|
||||
assert!(matches!(service.on_periodic_tick(), ImuEvent::Idle));
|
||||
for _ in 0..9 {
|
||||
assert!(matches!(
|
||||
service.on_transfer_complete(SpiCompletion::Complete),
|
||||
ImuEvent::Idle
|
||||
));
|
||||
}
|
||||
assert!(matches!(
|
||||
service.on_transfer_complete(SpiCompletion::Complete),
|
||||
ImuEvent::Initialized
|
||||
));
|
||||
assert!(matches!(service.on_periodic_tick(), ImuEvent::Idle));
|
||||
assert!(matches!(
|
||||
service.on_transfer_complete(SpiCompletion::Complete),
|
||||
ImuEvent::Sample(_)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
//! RTIC- and PAC-independent flight services.
|
||||
//!
|
||||
//! Acquisition services and board-owned output workers have separate owners.
|
||||
//! Boards connect hardware interrupts to their dedicated workers through
|
||||
//! bounded deferred events; they must not place unrelated work behind one
|
||||
//! priority-ceiling resource.
|
||||
|
||||
#![no_std]
|
||||
|
||||
/// Single-owner IMU acquisition service.
|
||||
pub mod imu;
|
||||
/// Lock-free worker handoff primitives.
|
||||
pub mod mailbox;
|
||||
/// Single-owner LIDAR-Lite range acquisition service.
|
||||
pub mod range;
|
||||
|
||||
pub use imu::{ImuEvent, ImuService};
|
||||
pub use mailbox::LatestImuMailbox;
|
||||
pub use mailbox::LatestRangeMailbox;
|
||||
pub use range::{RangeEvent, RangeService};
|
||||
@@ -1,189 +0,0 @@
|
||||
//! Lock-free single-producer, single-consumer latest-value mailboxes.
|
||||
|
||||
use core::{
|
||||
marker::PhantomData,
|
||||
sync::atomic::{AtomicU32, Ordering},
|
||||
};
|
||||
|
||||
use kaptan_sensor::{imu::ImuSnapshot, range::RangeSnapshot};
|
||||
use kaptan_time::Timestamp;
|
||||
|
||||
/// Fixed-width snapshot representation for [`LatestMailbox`].
|
||||
///
|
||||
/// The representation consists only of atomic words. Implementations must
|
||||
/// encode and decode every field without loss so an unread older observation
|
||||
/// can be safely replaced by a newer one.
|
||||
pub trait MailboxValue<const WORDS: usize>: Copy {
|
||||
/// Converts a snapshot into its atomic-word representation.
|
||||
fn into_words(self) -> [u32; WORDS];
|
||||
|
||||
/// Reconstructs a snapshot from its atomic-word representation.
|
||||
fn from_words(words: [u32; WORDS]) -> Self;
|
||||
}
|
||||
|
||||
/// Lock-free latest-value SPSC mailbox.
|
||||
///
|
||||
/// Publishing a newer value intentionally replaces an unread older value: the
|
||||
/// output path is best-effort and must never delay acquisition. The sequence
|
||||
/// counter is a seqlock; odd values indicate a writer and even values identify
|
||||
/// a stable snapshot. Exactly one producer and one consumer may use an
|
||||
/// instance concurrently. Sequence operations are sequentially consistent so
|
||||
/// a reader cannot validate an old even sequence around concurrently written
|
||||
/// atomic payload words; the payload words themselves need only be relaxed.
|
||||
pub struct LatestMailbox<Value, const WORDS: usize> {
|
||||
sequence: AtomicU32,
|
||||
words: [AtomicU32; WORDS],
|
||||
value: PhantomData<Value>,
|
||||
}
|
||||
|
||||
impl<Value: MailboxValue<WORDS>, const WORDS: usize> LatestMailbox<Value, WORDS> {
|
||||
/// Creates an empty mailbox.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
sequence: AtomicU32::new(0),
|
||||
words: [const { AtomicU32::new(0) }; WORDS],
|
||||
value: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Publishes a snapshot, replacing any unread older snapshot.
|
||||
pub fn publish(&self, value: Value) {
|
||||
let sequence = self.sequence.fetch_add(1, Ordering::SeqCst);
|
||||
debug_assert_eq!(sequence & 1, 0, "SPSC producer must not reenter");
|
||||
|
||||
for (destination, word) in self.words.iter().zip(value.into_words()) {
|
||||
destination.store(word, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
self.sequence
|
||||
.store(sequence.wrapping_add(2), Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Returns the newest stable snapshot different from `observed_sequence`.
|
||||
pub fn take_newer(&self, observed_sequence: &mut u32) -> Option<Value> {
|
||||
let before = self.sequence.load(Ordering::SeqCst);
|
||||
if before == *observed_sequence || before & 1 != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut words = [0; WORDS];
|
||||
for (destination, source) in words.iter_mut().zip(&self.words) {
|
||||
*destination = source.load(Ordering::Relaxed);
|
||||
}
|
||||
|
||||
if self.sequence.load(Ordering::SeqCst) != before {
|
||||
return None;
|
||||
}
|
||||
|
||||
*observed_sequence = before;
|
||||
Some(Value::from_words(words))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Value: MailboxValue<WORDS>, const WORDS: usize> Default for LatestMailbox<Value, WORDS> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MailboxValue<9> for ImuSnapshot {
|
||||
fn into_words(self) -> [u32; 9] {
|
||||
let timestamp = self.timestamp.as_microseconds();
|
||||
[
|
||||
timestamp as u32,
|
||||
(timestamp >> 32) as u32,
|
||||
self.acceleration_milligravity[0] as u16 as u32,
|
||||
self.acceleration_milligravity[1] as u16 as u32,
|
||||
self.acceleration_milligravity[2] as u16 as u32,
|
||||
self.angular_rate_milliradians_per_second[0] as u16 as u32,
|
||||
self.angular_rate_milliradians_per_second[1] as u16 as u32,
|
||||
self.angular_rate_milliradians_per_second[2] as u16 as u32,
|
||||
self.temperature_centidegrees_celsius as u16 as u32,
|
||||
]
|
||||
}
|
||||
|
||||
fn from_words(words: [u32; 9]) -> Self {
|
||||
ImuSnapshot::new(
|
||||
timestamp_from_words(words[0], words[1]),
|
||||
[
|
||||
words[2] as u16 as i16,
|
||||
words[3] as u16 as i16,
|
||||
words[4] as u16 as i16,
|
||||
],
|
||||
[
|
||||
words[5] as u16 as i16,
|
||||
words[6] as u16 as i16,
|
||||
words[7] as u16 as i16,
|
||||
],
|
||||
words[8] as u16 as i16,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl MailboxValue<3> for RangeSnapshot {
|
||||
fn into_words(self) -> [u32; 3] {
|
||||
let timestamp = self.timestamp.as_microseconds();
|
||||
[
|
||||
timestamp as u32,
|
||||
(timestamp >> 32) as u32,
|
||||
self.distance_millimetres,
|
||||
]
|
||||
}
|
||||
|
||||
fn from_words(words: [u32; 3]) -> Self {
|
||||
RangeSnapshot::new(timestamp_from_words(words[0], words[1]), words[2])
|
||||
}
|
||||
}
|
||||
|
||||
/// Latest-value mailbox for IMU telemetry snapshots.
|
||||
pub type LatestImuMailbox = LatestMailbox<ImuSnapshot, 9>;
|
||||
/// Latest-value mailbox for range telemetry snapshots.
|
||||
pub type LatestRangeMailbox = LatestMailbox<RangeSnapshot, 3>;
|
||||
|
||||
const fn timestamp_from_words(low: u32, high: u32) -> Timestamp {
|
||||
Timestamp::from_microseconds((low as u64) | ((high as u64) << 32))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{LatestImuMailbox, LatestRangeMailbox};
|
||||
use kaptan_sensor::{imu::ImuSnapshot, range::RangeSnapshot};
|
||||
use kaptan_time::Timestamp;
|
||||
|
||||
#[test]
|
||||
fn publishes_only_the_newest_unread_imu_sample() {
|
||||
let mailbox = LatestImuMailbox::new();
|
||||
let mut observed = 0;
|
||||
mailbox.publish(ImuSnapshot::new(
|
||||
Timestamp::from_microseconds(1),
|
||||
[1; 3],
|
||||
[2; 3],
|
||||
3,
|
||||
));
|
||||
mailbox.publish(ImuSnapshot::new(
|
||||
Timestamp::from_microseconds(2),
|
||||
[4; 3],
|
||||
[5; 3],
|
||||
6,
|
||||
));
|
||||
|
||||
let sample = mailbox.take_newer(&mut observed).unwrap();
|
||||
assert_eq!(sample.timestamp, Timestamp::from_microseconds(2));
|
||||
assert_eq!(sample.acceleration_milligravity, [4; 3]);
|
||||
assert!(mailbox.take_newer(&mut observed).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn publishes_only_the_newest_unread_range_sample() {
|
||||
let mailbox = LatestRangeMailbox::new();
|
||||
let mut observed = 0;
|
||||
mailbox.publish(RangeSnapshot::new(Timestamp::from_microseconds(1), 100));
|
||||
mailbox.publish(RangeSnapshot::new(Timestamp::from_microseconds(2), 200));
|
||||
|
||||
assert_eq!(
|
||||
mailbox.take_newer(&mut observed),
|
||||
Some(RangeSnapshot::new(Timestamp::from_microseconds(2), 200))
|
||||
);
|
||||
assert!(mailbox.take_newer(&mut observed).is_none());
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
//! Single-owner LIDAR-Lite range acquisition service.
|
||||
|
||||
use kaptan_drivers::lidar_lite_v3::{
|
||||
AcquisitionMode, Error as LidarError, LidarLiteV3, LidarLiteV3Event, StartupError,
|
||||
};
|
||||
use kaptan_i2c::{I2cBus, I2cCompletion};
|
||||
use kaptan_sensor::range::RangeSnapshot;
|
||||
use kaptan_time::MonotonicClock;
|
||||
|
||||
/// One observable result from the range worker.
|
||||
#[derive(Debug)]
|
||||
pub enum RangeEvent<BusError> {
|
||||
/// No observation or terminal fault was produced.
|
||||
Idle,
|
||||
/// One coherent range observation is ready for publication.
|
||||
Measurement(RangeSnapshot),
|
||||
/// The selected sensor path entered a terminal fault state.
|
||||
Fault(LidarError<BusError>),
|
||||
}
|
||||
|
||||
/// Owns LIDAR-Lite state, its clock, and measurement acceptance.
|
||||
pub struct RangeService<Clock, Bus> {
|
||||
clock: Clock,
|
||||
lidar: LidarLiteV3<Bus>,
|
||||
mode: AcquisitionMode,
|
||||
}
|
||||
|
||||
impl<Clock, Bus> RangeService<Clock, Bus> {
|
||||
/// Creates an inactive range acquisition service.
|
||||
pub const fn new(clock: Clock, lidar: LidarLiteV3<Bus>, mode: AcquisitionMode) -> Self {
|
||||
Self { clock, lidar, mode }
|
||||
}
|
||||
|
||||
/// Completes the LIDAR-Lite startup interval selected by the board.
|
||||
pub fn finish_startup(&mut self) -> Result<(), StartupError> {
|
||||
self.lidar.finish_startup()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Clock: MonotonicClock, Bus: I2cBus> RangeService<Clock, Bus> {
|
||||
/// Starts a new measurement, or polls one already in progress.
|
||||
pub fn on_periodic_tick(&mut self) -> RangeEvent<Bus::StartError> {
|
||||
let now = self.clock.now();
|
||||
let result = if self.lidar.is_ready() {
|
||||
self.lidar.request_measurement(now, self.mode)
|
||||
} else {
|
||||
self.lidar.poll_measurement(now)
|
||||
};
|
||||
match result {
|
||||
Ok(()) | Err(LidarError::NotReady) => RangeEvent::Idle,
|
||||
Err(error) => RangeEvent::Fault(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// Advances the LIDAR-Lite transaction state from an I²C terminal event.
|
||||
pub fn on_transaction_complete(
|
||||
&mut self,
|
||||
completion: I2cCompletion,
|
||||
) -> RangeEvent<Bus::StartError> {
|
||||
match self.lidar.on_transaction_complete(completion) {
|
||||
LidarLiteV3Event::Measurement(sample) => RangeEvent::Measurement(sample),
|
||||
LidarLiteV3Event::Fault(error) => RangeEvent::Fault(error),
|
||||
_ => RangeEvent::Idle,
|
||||
}
|
||||
}
|
||||
|
||||
/// Acknowledges a board-controller interrupt and advances the acquisition.
|
||||
///
|
||||
/// The closure keeps the controller-specific interrupt handling at the
|
||||
/// board boundary while preserving this service as the sole owner of the
|
||||
/// LIDAR state machine.
|
||||
pub fn on_controller_interrupt<F>(&mut self, acknowledge: F) -> RangeEvent<Bus::StartError>
|
||||
where
|
||||
F: FnOnce(&mut Bus) -> I2cCompletion,
|
||||
{
|
||||
let completion = self.lidar.acknowledge_with(acknowledge);
|
||||
self.on_transaction_complete(completion)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{RangeEvent, RangeService};
|
||||
use kaptan_drivers::lidar_lite_v3::LidarLiteV3;
|
||||
use kaptan_i2c::{I2cBus, I2cCompletion};
|
||||
use kaptan_time::{MonotonicClock, Timestamp};
|
||||
|
||||
struct Clock;
|
||||
|
||||
impl MonotonicClock for Clock {
|
||||
fn now(&mut self) -> Timestamp {
|
||||
Timestamp::from_microseconds(123)
|
||||
}
|
||||
}
|
||||
|
||||
struct Bus {
|
||||
response: [u8; 2],
|
||||
}
|
||||
|
||||
impl I2cBus for Bus {
|
||||
type StartError = ();
|
||||
|
||||
fn start_write(&mut self, _: u8, _: &[u8]) -> Result<(), Self::StartError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start_read(&mut self, _: u8, _: usize) -> Result<(), Self::StartError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn complete_transaction(&mut self, completion: I2cCompletion, read: Option<&mut [u8]>) {
|
||||
if completion == I2cCompletion::Complete
|
||||
&& let Some(read) = read
|
||||
{
|
||||
read.copy_from_slice(&self.response[..read.len()]);
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel_transaction(&mut self) {}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn produces_a_measurement_after_stop_separated_i2c_transactions() {
|
||||
let mut service = RangeService::new(
|
||||
Clock,
|
||||
LidarLiteV3::new(Bus { response: [0; 2] }),
|
||||
kaptan_drivers::lidar_lite_v3::AcquisitionMode::WithReceiverBiasCorrection,
|
||||
);
|
||||
service.finish_startup().unwrap();
|
||||
|
||||
assert!(matches!(service.on_periodic_tick(), RangeEvent::Idle));
|
||||
assert!(matches!(
|
||||
service.on_transaction_complete(I2cCompletion::Complete),
|
||||
RangeEvent::Idle
|
||||
));
|
||||
assert!(matches!(service.on_periodic_tick(), RangeEvent::Idle));
|
||||
assert!(matches!(
|
||||
service.on_controller_interrupt(|bus| {
|
||||
bus.response = [1 << 5, 0];
|
||||
I2cCompletion::Complete
|
||||
}),
|
||||
RangeEvent::Idle
|
||||
));
|
||||
assert!(matches!(service.on_periodic_tick(), RangeEvent::Idle));
|
||||
assert!(matches!(
|
||||
service.on_controller_interrupt(|_| I2cCompletion::Complete),
|
||||
RangeEvent::Idle
|
||||
));
|
||||
assert!(matches!(service.on_periodic_tick(), RangeEvent::Idle));
|
||||
assert!(matches!(
|
||||
service.on_controller_interrupt(|bus| {
|
||||
bus.response = [0x01, 0x23];
|
||||
I2cCompletion::Complete
|
||||
}),
|
||||
RangeEvent::Idle
|
||||
));
|
||||
assert!(matches!(service.on_periodic_tick(), RangeEvent::Idle));
|
||||
assert!(matches!(
|
||||
service.on_controller_interrupt(|_| I2cCompletion::Complete),
|
||||
RangeEvent::Measurement(sample)
|
||||
if sample.timestamp == Timestamp::from_microseconds(123)
|
||||
&& sample.distance_millimetres == 2_910
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user