Rename project from xserial to pipeview

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-12 22:17:40 +08:00
parent fd680858f6
commit 8caf7d6d62
64 changed files with 314 additions and 314 deletions

View File

@@ -0,0 +1,556 @@
use async_trait::async_trait;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio_serial::{DataBits, FlowControl, Parity, SerialPortBuilderExt, SerialStream, StopBits};
use serialport::SerialPort;
use tracing::{debug, info, warn};
use super::{Transport, TransportType};
use crate::error::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum SerialDataBits {
Five,
Six,
Seven,
Eight,
}
impl From<SerialDataBits> for DataBits {
fn from(value: SerialDataBits) -> Self {
match value {
SerialDataBits::Five => DataBits::Five,
SerialDataBits::Six => DataBits::Six,
SerialDataBits::Seven => DataBits::Seven,
SerialDataBits::Eight => DataBits::Eight,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum SerialParity {
None,
Odd,
Even,
}
impl From<SerialParity> for Parity {
fn from(value: SerialParity) -> Self {
match value {
SerialParity::None => Parity::None,
SerialParity::Odd => Parity::Odd,
SerialParity::Even => Parity::Even,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum SerialStopBits {
One,
Two,
}
impl From<SerialStopBits> for StopBits {
fn from(value: SerialStopBits) -> Self {
match value {
SerialStopBits::One => StopBits::One,
SerialStopBits::Two => StopBits::Two,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum SerialFlowControl {
None,
Software,
Hardware,
}
impl From<SerialFlowControl> for FlowControl {
fn from(value: SerialFlowControl) -> Self {
match value {
SerialFlowControl::None => FlowControl::None,
SerialFlowControl::Software => FlowControl::Software,
SerialFlowControl::Hardware => FlowControl::Hardware,
}
}
}
#[derive(Debug)]
pub struct SerialTransport {
port: Option<SerialStream>,
port_name: String,
baud_rate: u32,
data_bits: SerialDataBits,
parity: SerialParity,
stop_bits: SerialStopBits,
flow_control: SerialFlowControl,
dtr: bool,
rts: bool,
}
impl SerialTransport {
#[allow(clippy::too_many_arguments)]
pub fn new(
port_name: String,
baud_rate: u32,
data_bits: SerialDataBits,
parity: SerialParity,
stop_bits: SerialStopBits,
flow_control: SerialFlowControl,
dtr: bool,
rts: bool,
) -> Self {
Self {
port: None,
port_name,
baud_rate,
data_bits,
parity,
stop_bits,
flow_control,
dtr,
rts,
}
}
pub fn list_ports() -> Vec<serialport::SerialPortInfo> {
match serialport::available_ports() {
Ok(ports) => ports,
Err(e) => {
warn!("Failed to enumerate serial ports: {}", e);
vec![]
}
}
}
pub fn port_name(&self) -> &str {
&self.port_name
}
pub fn baud_rate(&self) -> u32 {
self.baud_rate
}
pub fn data_bits(&self) -> SerialDataBits {
self.data_bits
}
pub fn parity(&self) -> SerialParity {
self.parity
}
pub fn stop_bits(&self) -> SerialStopBits {
self.stop_bits
}
pub fn flow_control(&self) -> SerialFlowControl {
self.flow_control
}
pub fn set_dtr(&mut self, state: bool) -> Result<()> {
match &mut self.port {
Some(port) => {
port.write_data_terminal_ready(state)?;
self.dtr = state;
debug!("DTR set to {} on {}", state, self.port_name);
Ok(())
}
None => Err(crate::error::Error::ConnectionFailed(format!(
"port {} not open",
self.port_name
))),
}
}
pub fn set_rts(&mut self, state: bool) -> Result<()> {
match &mut self.port {
Some(port) => {
port.write_request_to_send(state)?;
self.rts = state;
debug!("RTS set to {} on {}", state, self.port_name);
Ok(())
}
None => Err(crate::error::Error::ConnectionFailed(format!(
"port {} not open",
self.port_name
))),
}
}
}
impl AsyncRead for SerialTransport {
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match &mut self.get_mut().port {
Some(port) => std::pin::Pin::new(port).poll_read(cx, buf),
None => std::task::Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"Serial port not open",
))),
}
}
}
impl AsyncWrite for SerialTransport {
fn poll_write(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
match &mut self.get_mut().port {
Some(port) => std::pin::Pin::new(port).poll_write(cx, buf),
None => std::task::Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"Serial port not open",
))),
}
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match &mut self.get_mut().port {
Some(port) => std::pin::Pin::new(port).poll_flush(cx),
None => std::task::Poll::Ready(Ok(())),
}
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match &mut self.get_mut().port {
Some(port) => std::pin::Pin::new(port).poll_shutdown(cx),
None => std::task::Poll::Ready(Ok(())),
}
}
}
#[async_trait]
impl Transport for SerialTransport {
fn name(&self) -> &str {
&self.port_name
}
fn transport_type(&self) -> TransportType {
TransportType::Serial
}
fn is_connected(&self) -> bool {
self.port.is_some()
}
async fn connect(&mut self) -> Result<()> {
if self.is_connected() {
return Ok(());
}
info!(
"Opening serial port {} at {} baud ({:?}, {:?}, {:?}, {:?})",
self.port_name,
self.baud_rate,
self.data_bits,
self.parity,
self.stop_bits,
self.flow_control
);
let mut port = tokio_serial::new(&self.port_name, self.baud_rate)
.data_bits(self.data_bits.into())
.parity(self.parity.into())
.stop_bits(self.stop_bits.into())
.flow_control(self.flow_control.into())
.open_native_async()
.map_err(|e| {
crate::error::Error::ConnectionFailed(format!(
"Failed to open {}: {}",
self.port_name, e
))
})?;
// Linux kernel toggles DTR/RTS on every open() — restore configured
// state immediately afterward. Many wireless serial modules (HC-12,
// HC-15, HC-05, Bluetooth/UART bridges) need DTR asserted to stay in
// transparent data mode and not fall into AT-command / reset state.
if let Err(e) = port.write_data_terminal_ready(self.dtr) {
warn!("Failed to set DTR({}) on {}: {}", self.dtr, self.port_name, e);
}
if let Err(e) = port.write_request_to_send(self.rts) {
warn!("Failed to set RTS({}) on {}: {}", self.rts, self.port_name, e);
}
debug!("Serial port {} opened successfully", self.port_name);
self.port = Some(port);
Ok(())
}
async fn disconnect(&mut self) -> Result<()> {
if let Some(port) = self.port.take() {
debug!("Closing serial port {}", self.port_name);
drop(port);
debug!("Serial port {} closed", self.port_name);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::ErrorKind;
use std::pin::Pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
use tokio::io::ReadBuf;
fn noop_waker() -> Waker {
unsafe fn raw_clone(_: *const ()) -> RawWaker {
RawWaker::new(std::ptr::null(), &RAW_VTABLE)
}
unsafe fn raw_wake(_: *const ()) {}
unsafe fn raw_wake_by_ref(_: *const ()) {}
unsafe fn raw_drop(_: *const ()) {}
static RAW_VTABLE: RawWakerVTable =
RawWakerVTable::new(raw_clone, raw_wake, raw_wake_by_ref, raw_drop);
unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &RAW_VTABLE)) }
}
// ── Constructor and accessor tests ──
#[test]
fn test_new() {
let transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
assert_eq!(transport.port_name(), "COM1");
assert_eq!(transport.baud_rate(), 115200);
}
#[test]
fn test_new_different_params() {
let transport = SerialTransport::new(
"COM3".into(),
9600,
SerialDataBits::Seven,
SerialParity::Even,
SerialStopBits::Two,
SerialFlowControl::Hardware,
true,
true,
);
assert_eq!(transport.port_name(), "COM3");
assert_eq!(transport.baud_rate(), 9600);
assert_eq!(transport.data_bits(), SerialDataBits::Seven);
assert_eq!(transport.parity(), SerialParity::Even);
assert_eq!(transport.stop_bits(), SerialStopBits::Two);
assert_eq!(transport.flow_control(), SerialFlowControl::Hardware);
}
#[test]
fn test_name() {
let transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
assert_eq!(transport.name(), "COM1");
}
#[test]
fn test_transport_type() {
let transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
assert_eq!(transport.transport_type(), TransportType::Serial);
}
#[test]
fn test_is_connected_false_by_default() {
let transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
assert!(!transport.is_connected());
}
// ── list_ports tests ──
#[test]
fn test_list_ports_does_not_panic() {
let _ = SerialTransport::list_ports();
}
#[test]
fn test_list_ports_returns_vec() {
let ports: Vec<serialport::SerialPortInfo> = SerialTransport::list_ports();
let _ = ports;
}
// ── AsyncRead tests (not connected) ──
#[tokio::test]
async fn test_poll_read_not_connected() {
let mut transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
let pinned = Pin::new(&mut transport);
let mut buf_data = [0u8; 16];
let mut buf = ReadBuf::new(&mut buf_data);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_read(&mut cx, &mut buf) {
Poll::Ready(Err(e)) => assert_eq!(e.kind(), ErrorKind::NotConnected),
other => panic!("expected Poll::Ready(Err(NotConnected)), got {:?}", other),
}
}
// ── AsyncWrite tests (not connected) ──
#[tokio::test]
async fn test_poll_write_not_connected() {
let mut transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
let pinned = Pin::new(&mut transport);
let data = b"hello";
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_write(&mut cx, data) {
Poll::Ready(Err(e)) => assert_eq!(e.kind(), ErrorKind::NotConnected),
other => panic!("expected Poll::Ready(Err(NotConnected)), got {:?}", other),
}
}
#[tokio::test]
async fn test_poll_flush_not_connected() {
let mut transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
let pinned = Pin::new(&mut transport);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_flush(&mut cx) {
Poll::Ready(Ok(())) => {}
other => panic!("expected Poll::Ready(Ok(())), got {:?}", other),
}
}
#[tokio::test]
async fn test_poll_shutdown_not_connected() {
let mut transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
let pinned = Pin::new(&mut transport);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_shutdown(&mut cx) {
Poll::Ready(Ok(())) => {}
other => panic!("expected Poll::Ready(Ok(())), got {:?}", other),
}
}
// ── Transport trait method tests ──
#[test]
fn test_transport_name() {
let transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
assert_eq!(transport.name(), "COM1");
}
#[test]
fn test_transport_transport_type() {
let transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
assert_eq!(transport.transport_type(), TransportType::Serial);
}
#[test]
fn test_transport_is_connected() {
let transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
assert!(!transport.is_connected());
}
}