refactor: enforce layered crates and Rust audit gates

This commit is contained in:
Horldsence
2026-09-24 13:00:03 +08:00
parent af151525d1
commit 8a582fc102
67 changed files with 3918 additions and 581 deletions
+244
View File
@@ -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)));
}
}