refactor: enforce layered crates and Rust audit gates
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "dr16"
|
||||
description = "DJI DR16 D-BUS serial receiver driver"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
dr16-protocol = { workspace = true }
|
||||
serial = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["test-util"] }
|
||||
|
||||
[package.metadata.sentry-chassis]
|
||||
layer = "device/remote"
|
||||
@@ -0,0 +1,244 @@
|
||||
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
|
||||
|
||||
//! DJI DR16 D-BUS 串口接收驱动。
|
||||
//!
|
||||
//! DR16 使用非标准 100000 baud、8 data bits、even parity、1 stop bit。阻塞串口
|
||||
//! I/O 运行在专用线程上,异步侧只消费**最新**样本,避免遥控数据形成积压队列。
|
||||
|
||||
use std::io::{self, Read};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use dr16_protocol::{Dr16Sample, FrameDecoder};
|
||||
use serial::prelude::*;
|
||||
use tokio::sync::watch;
|
||||
use tracing::{debug, error};
|
||||
|
||||
/// D-BUS UART 波特率。
|
||||
pub const BAUD_RATE: usize = 100_000;
|
||||
|
||||
/// 串口读取参数。
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Dr16Options {
|
||||
/// 阻塞串口读超时;也用于让读取线程定期观察接收端关闭。
|
||||
pub read_timeout: Duration,
|
||||
}
|
||||
|
||||
impl Default for Dr16Options {
|
||||
fn default() -> Self {
|
||||
Self { read_timeout: Duration::from_millis(20) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 带接收时间的 DR16 样本。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct StampedSample {
|
||||
/// 解码后的遥控输入。
|
||||
pub sample: Dr16Sample,
|
||||
/// 样本到达用户态读取循环的时刻。
|
||||
pub received_at: Instant,
|
||||
}
|
||||
|
||||
/// 最新样本发送端。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SampleSender {
|
||||
tx: watch::Sender<Option<StampedSample>>,
|
||||
}
|
||||
|
||||
impl SampleSender {
|
||||
/// 发布一个新样本并返回带时间戳的副本。
|
||||
#[must_use]
|
||||
pub fn send(&self, sample: Dr16Sample) -> StampedSample {
|
||||
let stamped = StampedSample { sample, received_at: Instant::now() };
|
||||
self.tx.send_replace(Some(stamped));
|
||||
stamped
|
||||
}
|
||||
|
||||
/// 当前是否有异步消费者。
|
||||
#[must_use]
|
||||
pub fn is_closed(&self) -> bool {
|
||||
self.tx.is_closed()
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建一对最新样本发送端和接收流。
|
||||
#[must_use]
|
||||
pub fn sample_channel() -> (SampleSender, SampleStream) {
|
||||
let (tx, rx) = watch::channel(None);
|
||||
(SampleSender { tx }, SampleStream { rx })
|
||||
}
|
||||
|
||||
/// 最新 DR16 样本流。
|
||||
///
|
||||
/// 内部使用 `watch`:慢消费者看到的是最新遥控状态,不会执行延迟的旧摇杆位置。
|
||||
#[derive(Debug)]
|
||||
pub struct SampleStream {
|
||||
rx: watch::Receiver<Option<StampedSample>>,
|
||||
}
|
||||
|
||||
impl SampleStream {
|
||||
/// 等待下一个新样本;读取线程退出且没有样本时返回 `None`。
|
||||
pub async fn recv(&mut self) -> Option<StampedSample> {
|
||||
loop {
|
||||
self.rx.changed().await.ok()?;
|
||||
if let Some(sample) = *self.rx.borrow_and_update() {
|
||||
return Some(sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 读取当前最新样本;尚未收到帧时返回 `None`。
|
||||
#[must_use]
|
||||
pub fn latest(&self) -> Option<StampedSample> {
|
||||
*self.rx.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
/// 打开 DR16 串口并开始接收。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 设备不存在、权限不足、串口不支持 100000 baud 或配置 8E1 失败。
|
||||
pub fn open(device: &str) -> io::Result<SampleStream> {
|
||||
open_with(device, Dr16Options::default())
|
||||
}
|
||||
|
||||
/// 使用指定读取参数打开 DR16 串口。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 同 [`open`]。
|
||||
pub fn open_with(device: &str, options: Dr16Options) -> io::Result<SampleStream> {
|
||||
let mut port = serial::open(device)?;
|
||||
port.reconfigure(&|settings| {
|
||||
settings.set_baud_rate(serial::BaudOther(BAUD_RATE))?;
|
||||
settings.set_char_size(serial::Bits8);
|
||||
settings.set_parity(serial::ParityEven);
|
||||
settings.set_stop_bits(serial::Stop1);
|
||||
settings.set_flow_control(serial::FlowNone);
|
||||
Ok(())
|
||||
})?;
|
||||
port.set_timeout(options.read_timeout)?;
|
||||
debug!(device, baud = BAUD_RATE, "DR16 serial opened");
|
||||
Ok(spawn_reader(port))
|
||||
}
|
||||
|
||||
/// 在专用线程上泵送任意字节 reader,返回最新样本流。
|
||||
#[must_use]
|
||||
pub fn spawn_reader<R>(reader: R) -> SampleStream
|
||||
where
|
||||
R: Read + Send + 'static,
|
||||
{
|
||||
let (tx, stream) = sample_channel();
|
||||
let thread = thread::Builder::new().name("dr16-reader".into()).spawn(move || {
|
||||
pump_reader(reader, &tx);
|
||||
});
|
||||
if let Err(error) = thread {
|
||||
error!(%error, "failed to start DR16 reader thread");
|
||||
}
|
||||
stream
|
||||
}
|
||||
|
||||
fn pump_reader<R: Read>(mut reader: R, tx: &SampleSender) {
|
||||
let mut decoder = FrameDecoder::new();
|
||||
let mut buffer = [0u8; 64];
|
||||
loop {
|
||||
match reader.read(&mut buffer) {
|
||||
Ok(0) => {
|
||||
debug!("DR16 reader reached end of input");
|
||||
return;
|
||||
}
|
||||
Ok(count) => {
|
||||
for sample in decoder.push(&buffer[..count]) {
|
||||
let _ = tx.send(sample);
|
||||
}
|
||||
}
|
||||
Err(error) if is_transient(&error) => {
|
||||
// 读超时内没有任何字节,已经超过正常 D-BUS 帧间隔;清掉半帧,避免错位。
|
||||
decoder.clear();
|
||||
if tx.is_closed() {
|
||||
debug!("DR16 sample consumer closed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
error!(%error, "DR16 serial read failed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if tx.is_closed() {
|
||||
debug!("DR16 sample consumer closed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_transient(error: &io::Error) -> bool {
|
||||
matches!(
|
||||
error.kind(),
|
||||
io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::cmp;
|
||||
|
||||
use dr16_protocol::{FRAME_LEN, SwitchPosition};
|
||||
|
||||
use super::*;
|
||||
|
||||
struct ChunkReader {
|
||||
chunks: Vec<Vec<u8>>,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl Read for ChunkReader {
|
||||
fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
|
||||
let Some(chunk) = self.chunks.get(self.index) else {
|
||||
return Ok(0);
|
||||
};
|
||||
self.index += 1;
|
||||
let count = cmp::min(output.len(), chunk.len());
|
||||
output[..count].copy_from_slice(&chunk[..count]);
|
||||
Ok(count)
|
||||
}
|
||||
}
|
||||
|
||||
fn frame() -> [u8; FRAME_LEN] {
|
||||
let mut frame = [0u8; FRAME_LEN];
|
||||
frame[1] = 0x04;
|
||||
frame[2] = 0x20;
|
||||
frame[4] = 0x01;
|
||||
frame[5] = 0x78;
|
||||
frame
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn generic_reader_publishes_decoded_latest_sample() {
|
||||
let raw = frame();
|
||||
let reader = ChunkReader {
|
||||
chunks: vec![raw[..5].to_vec(), raw[5..13].to_vec(), raw[13..].to_vec()],
|
||||
index: 0,
|
||||
};
|
||||
let mut stream = spawn_reader(reader);
|
||||
let stamped = stream.recv().await.unwrap();
|
||||
assert_eq!(stamped.sample.s1, SwitchPosition::Up);
|
||||
assert_eq!(stamped.sample.s2, SwitchPosition::Mid);
|
||||
assert_eq!(stream.latest(), Some(stamped));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn closed_reader_reports_end_of_stream() {
|
||||
let reader = ChunkReader { chunks: Vec::new(), index: 0 };
|
||||
let mut stream = spawn_reader(reader);
|
||||
assert_eq!(stream.recv().await, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transient_read_errors_are_not_fatal() {
|
||||
assert!(is_transient(&io::Error::from(io::ErrorKind::TimedOut)));
|
||||
assert!(is_transient(&io::Error::from(io::ErrorKind::WouldBlock)));
|
||||
assert!(!is_transient(&io::Error::from(io::ErrorKind::BrokenPipe)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user