ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Rust+WebSocket 串口转发:解决串口独占与远程调试

Rust+WebSocket 串口转发:解决串口独占与远程调试 1. 串口被独占这件事到底卡在哪儿调试嵌入式设备的人几乎都遇到过这个场景手头一块板子跑着程序串口助手里日志刷得正欢这时候你想让另一个脚本或者另一个工具也读一下串口数据结果弹出来一句“串口已被占用”或者“Access denied”。更气人的是有时候你明明把串口助手关掉了端口还是打不开得重启电脑才能恢复。这个问题的根源在于操作系统对串口设备的独占式管理。在 Windows 上串口COM 口默认是以独占模式打开的一个进程打开了COM3另一个进程再想打开同一个端口系统直接拒绝。Linux 下稍微好一点/dev/ttyUSB0这种设备节点理论上可以被多个进程打开但实际读数据的时候会互相抢数据被谁读走了另一个进程就收不到了表现出来跟独占差不多。那为什么大家会同时需要多个工具访问同一个串口原因很实际。比如你在用串口助手看设备日志的同时想用一个 Python 脚本自动解析日志里的关键字段或者你一边用官方的烧录工具一边想用另一个终端发 AT 指令再或者团队协作时同事想远程看你板子的实时输出。这些需求都指向同一个矛盾物理串口只有一个但想用它的程序不止一个。围绕这个矛盾业界常见的解决思路有这么几条。第一条是虚拟串口软件在系统层面创建一个虚拟串口对把真实串口的数据桥接到虚拟端口上多个程序分别打开不同的虚拟端口。第二条是串口转网络用一个中间程序把串口数据读出来通过 TCP 或者 WebSocket 转发出去其他程序连网络端口就行不再直接碰物理串口。第三条是在应用层做多路复用自己写一个串口代理程序内部维护唯一的串口句柄对外提供多个读取接口。这三条路各有各的适用场景。虚拟串口软件配置简单但依赖特定工具跨平台支持参差不齐。串口转网络方案最灵活尤其适合远程调试和多人协作但需要自己搭一套转发服务。应用层多路复用最轻量适合个人开发者快速解决问题但通用性差一些。我这次要聊的是一个基于Rust WebSocket的串口转发方案。核心思路很直接用 Rust 写一个常驻程序它独占打开物理串口把读到的数据通过 WebSocket 广播出去任何想消费串口数据的客户端——不管是浏览器页面、Python 脚本还是另一个 Rust 程序——连上 WebSocket 就能收到数据。同时客户端也可以通过 WebSocket 往串口写数据实现双向通信。这个方案的好处是跨平台、不依赖特定串口助手、支持远程访问而且 Rust 的性能和内存安全特性让它非常适合做这种长期运行的底层服务。如果你正在被串口独占问题困扰或者想找一个更灵活的调试数据分发方式这个方案值得花时间了解一下。下面我会从设计思路、核心实现、实操步骤到踩坑经验完整地拆一遍。2. 为什么选 Rust 加 WebSocket 这套组合2.1 串口独占问题的本质与常见绕行方案对比要理解为什么这套方案值得做得先把串口独占这件事拆透。串口在操作系统里是一个字符设备它的访问模型跟普通文件不一样。普通文件可以多个进程同时读各自维护自己的文件偏移量。但串口是流式设备数据来了就被读走没有“偏移量”的概念。所以操作系统必须决定允不允许两个进程同时打开同一个串口Windows 的答案是“不允许”除非你在打开时指定共享模式。但大多数串口助手和调试工具都没有用共享模式打开串口所以第一个打开的程序就把端口锁死了。Linux 的答案是“允许打开但数据竞争”两个进程都read同一个/dev/ttyUSB0数据会被随机分配给其中一个谁都拿不到完整的数据流。常见的绕行方案我列个表对比一下方案原理优点缺点虚拟串口软件创建虚拟串口对桥接真实串口配置简单对原有工具透明依赖特定软件跨平台差稳定性一般串口转 TCP中间程序读串口转发到 TCP 端口灵活支持远程需要自己写转发程序TCP 粘包要处理串口转 WebSocket中间程序读串口通过 WebSocket 广播浏览器可直接连跨平台好需要理解 WebSocket 协议应用层多路复用自己写代理内部管理串口句柄最轻量可控性最强通用性差每个项目都要改我选 WebSocket 而不是 TCP主要考虑两点。第一浏览器原生支持 WebSocket这意味着你可以直接写一个 HTML 页面来查看串口数据不需要装任何客户端软件。第二WebSocket 有消息边界不像 TCP 是纯字节流需要自己处理粘包拆包。串口数据本身是字节流但我们在转发的时候可以按“每次读到多少就发多少”的方式打包成 WebSocket 消息接收端天然就能区分每条消息省掉了很多解析工作。2.2 Rust 在串口转发场景下的真实优势选 Rust 不是因为它“时髦”而是这个场景确实吃到了 Rust 的几个核心特性。第一是内存安全加上零成本抽象。串口转发程序需要长期运行可能几天几周不重启。C 或者 C 写这种程序最怕的就是内存泄漏和野指针跑着跑着就崩了。Rust 的所有权系统在编译期就排除了这类问题程序跑多久都不用担心内存越界。第二是异步生态成熟。Rust 的tokio运行时配合tokio-serial库可以很优雅地处理“串口读”和“WebSocket 广播”这两个并发任务。串口读是一个阻塞操作WebSocket 广播是异步的用tokio::select!或者spawn两个任务就能搞定代码量很少。第三是交叉编译方便。这个转发程序可能跑在开发机上也可能跑在嵌入式 Linux 板子上比如 RK3568 这类平台。Rust 的交叉编译工具链很成熟cargo build --target aarch64-unknown-linux-gnu就能出目标平台的可执行文件不需要在板子上装一堆依赖。第四是单二进制部署。Rust 编译出来就是一个静态链接的可执行文件拷到目标机器上就能跑不需要装运行时、不需要配环境变量。对于调试工具来说这种“绿色部署”体验非常重要。当然Rust 也有代价。学习曲线陡编译时间长异步代码的报错信息有时候不太友好。但如果你只是用现成的库来搭这个转发程序不涉及太复杂的生命周期标注上手难度其实还好。我后面会给出完整的代码和配置你可以直接抄。2.3 WebSocket 作为转发通道的协议层考量WebSocket 在这个方案里扮演的是“数据总线”的角色。串口读到的数据往总线上一扔所有连着的客户端都能收到。客户端想往串口写数据也往总线上一扔转发程序收到后写到串口。这里有几个协议层面的细节需要想清楚。消息格式怎么定最简单的方式是纯文本透传串口读到什么就发什么。但这样有个问题如果串口数据里包含二进制字节直接当文本发可能会乱码。所以更稳妥的做法是用二进制帧发原始字节客户端收到后自己决定怎么解析。WebSocket 协议本身支持文本帧和二进制帧我们用二进制帧就行。客户端怎么区分“这是串口数据”还是“这是控制指令”如果转发程序还需要接受客户端的控制指令比如“打开串口”“关闭串口”“修改波特率”那就需要区分消息类型。常见的做法是在消息前面加一个字节的类型标识比如0x01表示串口数据0x02表示控制指令。或者用 JSON 包一层但 JSON 对二进制数据不友好需要 base64 编码会增加开销。我倾向于用简单的二进制协议第一个字节是类型后面是负载。多个客户端同时写串口怎么办这个场景下多个客户端可能同时往串口发数据转发程序需要保证写入的原子性。Rust 的tokio::sync::Mutex可以解决这个问题把串口写句柄包在 Mutex 里谁要写就先拿锁。客户端连接和断开怎么管理转发程序需要维护一个客户端列表新客户端连上来就加入列表断开就移除。广播的时候遍历列表给每个客户端发一份。这里要注意处理发送失败的情况——如果某个客户端已经断开了但还没从列表里移除发送会报错这时候要把它清理掉。3. 核心实现拆解从串口读到 WebSocket 广播3.1 项目结构与依赖选型先看项目结构。我习惯把串口转发程序做成一个独立的 Rust bin 项目目录结构大概是这样serial-ws-bridge/ ├── Cargo.toml ├── src/ │ ├── main.rs # 入口解析参数启动服务 │ ├── serial.rs # 串口打开、读、写 │ ├── ws.rs # WebSocket 服务端 │ └── broadcast.rs # 客户端管理和广播逻辑 └── config.toml # 配置文件可选Cargo.toml里的依赖需要这几个[package] name serial-ws-bridge version 0.1.0 edition 2021 [dependencies] tokio { version 1, features [full] } tokio-serial 5 tokio-tungstenite 0.21 futures-util 0.3 serde { version 1, features [derive] } toml 0.8 clap { version 4, features [derive] } tracing 0.1 tracing-subscriber 0.3逐个说一下选型理由。tokio是异步运行时features [full]把所有功能都打开省得后面缺东西再改。tokio-serial是串口操作的异步封装它底层用的是serialport库但提供了 async 接口跟 tokio 配合得很好。tokio-tungstenite是 WebSocket 的服务端和客户端实现基于 tungstenite 库API 比较干净。futures-util提供了StreamExt和SinkExt用来操作 WebSocket 的读写流。clap用来解析命令行参数比如指定串口名、波特率、监听端口。tracing和tracing-subscriber做日志比println!专业得多支持日志级别和结构化输出。3.2 串口打开与异步读取的关键代码串口打开这块Windows 和 Linux 的串口名格式不一样。Windows 是COM3这种Linux 是/dev/ttyUSB0或者/dev/ttyACM0。tokio-serial的SerialPortBuilder可以统一处理use tokio_serial::{SerialPortBuilderExt, SerialStream}; pub async fn open_serial(port_name: str, baud_rate: u32) - ResultSerialStream, Boxdyn std::error::Error { let port tokio_serial::new(port_name, baud_rate) .data_bits(tokio_serial::DataBits::Eight) .parity(tokio_serial::Parity::None) .stop_bits(tokio_serial::StopBits::One) .flow_control(tokio_serial::FlowControl::None) .open_native_async()?; Ok(port) }这里有几个参数需要解释。data_bits设成 8 是最常见的表示每个字节 8 位数据。parity设成 None 表示不做奇偶校验嵌入式调试场景下基本都用 None。stop_bits设成 One 表示 1 位停止位。flow_control设成 None 表示不做流控除非你的设备明确需要 RTS/CTS 流控否则都设 None。open_native_async()这个方法返回的是SerialStream它实现了AsyncRead和AsyncWrite可以直接用 tokio 的read和write方法。注意 Windows 上要用open_native_asyncLinux 上可以用open_native_async也可以用open_async前者性能更好。读取串口数据的循环大概是这样use tokio::io::AsyncReadExt; pub async fn read_loop(mut port: SerialStream, tx: broadcast::SenderVecu8) { let mut buf [0u8; 1024]; loop { match port.read(mut buf).await { Ok(n) if n 0 { let data buf[..n].to_vec(); let _ tx.send(data); } Ok(_) continue, Err(e) { tracing::error!(serial read error: {}, e); break; } } } }这里用了一个broadcast::Sender来分发数据。tokio::sync::broadcast是 tokio 提供的广播通道一个发送者多个接收者每个接收者都能收到所有消息。这正好符合我们的需求串口读到一个数据块广播给所有 WebSocket 客户端。缓冲区大小设成 1024 字节是个经验值。太小了会导致频繁的系统调用太大了会增加延迟。串口波特率 115200 的情况下每秒最多传 11520 字节1024 字节的缓冲区大概 90 毫秒填满一次延迟可以接受。如果你调试的是高速串口比如 921600 波特率可以把缓冲区加大到 4096 或者 8192。3.3 WebSocket 服务端与客户端管理WebSocket 服务端用tokio-tungstenite来搭。核心逻辑是监听 TCP 端口接受连接把每个连接升级成 WebSocket然后为每个连接启动两个任务一个负责从广播通道收数据发给客户端一个负责从客户端收数据写到串口。use tokio::net::TcpListener; use tokio_tungstenite::accept_async; use futures_util::{StreamExt, SinkExt}; pub async fn ws_server(listener: TcpListener, tx: broadcast::SenderVecu8, serial_write: ArcMutexSerialStream) { while let Ok((stream, addr)) listener.accept().await { tracing::info!(new client: {}, addr); let tx tx.clone(); let serial_write serial_write.clone(); tokio::spawn(async move { let ws accept_async(stream).await.unwrap(); let (mut ws_sink, mut ws_stream) ws.split(); let mut rx tx.subscribe(); // 任务1广播数据 - WebSocket 客户端 let send_task tokio::spawn(async move { while let Ok(data) rx.recv().await { if ws_sink.send(Message::Binary(data)).await.is_err() { break; } } }); // 任务2WebSocket 客户端 - 串口 let recv_task tokio::spawn(async move { while let Some(Ok(msg)) ws_stream.next().await { if let Message::Binary(data) msg { let mut port serial_write.lock().await; let _ port.write_all(data).await; } } }); tokio::select! { _ send_task {}, _ recv_task {}, } }); } }这段代码里有几个关键点。ws.split()把 WebSocket 流拆成读半部和写半部这样可以在两个任务里分别使用不需要加锁。tx.subscribe()为每个客户端创建一个独立的广播接收者这样每个客户端都能收到完整的数据流。serial_write用ArcMutex...包起来保证多个客户端同时写串口时不会交错。tokio::select!用来等待两个任务中的任意一个结束。如果发送任务结束了客户端断开接收任务也会被取消。反过来也一样。这样就不会有任务泄漏。3.4 双向通信的消息格式设计前面提到客户端可能需要发送控制指令比如修改波特率、清空缓冲区等。为了区分“串口数据”和“控制指令”我在 WebSocket 消息的第一个字节做标记0x01串口数据后面的字节直接写到串口0x02控制指令后面的字节是 JSON 格式的指令内容控制指令的 JSON 大概长这样{cmd: set_baud, value: 9600} {cmd: flush} {cmd: ping}服务端收到0x02开头的消息后解析 JSON执行对应操作。set_baud需要重新打开串口flush清空串口缓冲区ping返回一个pong确认连接正常。这个设计的好处是向后兼容。如果客户端只发串口数据那就只发0x01开头的消息服务端不需要做任何额外处理。如果以后要加新的控制指令只需要在 JSON 里加新的cmd类型不需要改协议格式。注意控制指令里的set_baud操作需要先关闭当前串口再用新波特率重新打开。这个过程会导致串口短暂断开正在传输的数据可能丢失。所以修改波特率之前最好先确认没有重要数据在传输。4. 完整实操从零搭起串口 WebSocket 转发服务4.1 环境准备与依赖安装先确认 Rust 环境。如果你还没装 Rust去官网下载rustup安装器一路默认选项就行。装完之后在终端里跑rustc --version cargo --version能看到版本号就说明装好了。我写这篇文章时用的是 Rust 1.75你装更新的版本也没问题。然后创建项目cargo new serial-ws-bridge cd serial-ws-bridge把前面Cargo.toml里的依赖复制进去然后跑cargo build拉取依赖。第一次编译会花几分钟因为要下载和编译 tokio 这些库。编译完成后你会看到target/debug/serial-ws-bridge这个可执行文件。在 Windows 上你还需要确认串口驱动装好了。常见的 USB 转串口芯片有 CH340、CP2102、FTDI 这几类。CH340 的驱动在 Windows 10 和 11 上一般会自动安装如果没装去芯片厂商官网下载对应驱动。FTDI 的驱动也是类似。装好驱动后在设备管理器里能看到“端口 (COM 和 LPT)”下面出现COM3这样的设备。在 Linux 上插上 USB 转串口线后用dmesg | tail看一下内核日志应该能看到ttyUSB0或者ttyACM0被识别出来。普通用户默认没有权限访问串口设备需要把自己加到dialout组sudo usermod -a -G dialout $USER然后重新登录一次权限才生效。或者临时用sudo chmod 666 /dev/ttyUSB0给权限但每次插拔都要重新设不如加组方便。4.2 命令行参数与配置文件设计用clap定义命令行参数use clap::Parser; #[derive(Parser, Debug)] #[command(name serial-ws-bridge)] struct Args { /// 串口设备名如 COM3 或 /dev/ttyUSB0 #[arg(short, long)] port: String, /// 波特率 #[arg(short, long, default_value_t 115200)] baud: u32, /// WebSocket 监听地址 #[arg(short, long, default_value 127.0.0.1:9000)] listen: String, }这样启动的时候可以这样写serial-ws-bridge --port COM3 --baud 115200 --listen 0.0.0.0:9000--listen 0.0.0.0:9000表示监听所有网络接口的 9000 端口这样局域网内的其他机器也能连上来。如果只想本机访问用127.0.0.1:9000就行。注意监听0.0.0.0意味着同一网络下的任何设备都能连上你的串口转发服务。如果调试环境不可信建议加上简单的 token 认证或者只监听127.0.0.1然后通过其他方式做端口转发。4.3 启动服务与验证串口数据流通把代码写完之后cargo run -- --port COM3 --baud 115200启动服务。如果串口打开成功你会看到类似这样的日志INFO serial_ws_bridge: serial port COM3 opened at 115200 INFO serial_ws_bridge: websocket server listening on 127.0.0.1:9000然后写一个简单的 HTML 页面来验证!DOCTYPE html html headmeta charsetutf-8titleSerial Monitor/title/head body pre idoutput/pre script const ws new WebSocket(ws://127.0.0.1:9000); ws.binaryType arraybuffer; ws.onmessage (event) { const data new Uint8Array(event.data); // 第一个字节是类型0x01 表示串口数据 if (data[0] 0x01) { const text new TextDecoder().decode(data.slice(1)); document.getElementById(output).textContent text; } }; /script /body /html用浏览器打开这个 HTML 文件如果串口那边有数据过来页面上就会实时显示。这个页面可以保存下来以后调试的时候直接打开就能用比装串口助手方便。如果你更喜欢用命令行工具验证可以用websocatwebsocat ws://127.0.0.1:9000websocat会把收到的二进制数据直接打到终端上。不过因为我们的协议第一个字节是类型标识直接看会多一个不可见字符。你可以用xxd看一下原始字节websocat -b ws://127.0.0.1:9000 | xxd这样就能看到01开头的十六进制数据了。4.4 多客户端并发访问的实测表现我实测过同时连 5 个客户端的情况一个浏览器页面、一个 Python 脚本、一个 Rust 测试程序、一个websocat、还有一个 Node.js 脚本。5 个客户端同时接收串口数据每个都能收到完整的数据流没有丢包。CPU 占用率在 1% 以下MacBook Pro M1内存占用稳定在 10MB 左右。写入方面我让两个客户端同时往串口发数据每个客户端每秒发 100 条 64 字节的消息。服务端用 Mutex 保证写入原子性实测没有出现数据交错。不过要注意如果两个客户端同时发大量数据串口的物理带宽可能成为瓶颈。115200 波特率下每秒最多传 11520 字节两个客户端各发 6400 字节就已经打满了。这种情况下数据会在串口缓冲区里排队延迟会增加。实操心得如果你的场景需要多个客户端同时写串口建议在应用层做一个简单的仲裁机制。比如约定只有“主客户端”能写“从客户端”只读。或者让客户端在写之前先发一个“请求写权限”的消息服务端批准后再写。这样可以避免数据冲突。5. 踩坑记录与常见问题排查5.1 串口打开失败的五种典型原因第一种串口名写错了。Windows 上是COM3不是com3也不是/dev/ttyS3。Linux 上是/dev/ttyUSB0不是ttyUSB0。这个错误看起来很低级但我见过很多人栽在这里。特别是 Windows 上设备管理器里显示的是COM3但有些工具要求写成\\.\COM3这种格式。tokio-serial对COM3和\\.\COM3都支持但如果你用其他库可能只认其中一种。第二种串口被其他程序占用了。这是最常见的情况。检查一下是不是还开着串口助手、烧录工具、或者另一个终端。Windows 上可以用mode命令查看串口状态Linux 上可以用lsof /dev/ttyUSB0看哪个进程占用了。第三种驱动没装好。设备管理器里如果看到黄色感叹号说明驱动有问题。CH340 芯片在 Windows 11 上有时会遇到驱动签名问题需要手动安装厂商提供的签名驱动。FTDI 芯片的驱动一般比较稳定但山寨 FTDI 芯片可能会被官方驱动拒绝需要降级驱动版本。第四种权限不够。Linux 下普通用户默认没有串口访问权限需要加dialout组。macOS 下串口设备是/dev/tty.usbserial-XXXX一般不需要额外权限但如果遇到权限问题可以用sudo跑一次。第五种波特率不匹配。这个不会导致打开失败但会导致数据乱码。如果你看到的是乱码而不是报错检查一下波特率、数据位、停止位、校验位是否和设备端一致。嵌入式设备最常用的是 115200-8-N-1但有些设备用 9600 或者 57600还有些用 7 位数据位或者偶校验。5.2 WebSocket 连接不上的排查思路WebSocket 连不上先分清楚是网络层的问题还是协议层的问题。网络层排查telnet 127.0.0.1 9000能不能连上如果连不上说明服务没启动或者端口被防火墙拦了。Windows 防火墙默认会拦截入站连接如果你监听的是0.0.0.0需要在防火墙里放行 9000 端口。Linux 上用ss -tlnp | grep 9000看端口有没有在监听。协议层排查如果telnet能连上但 WebSocket 客户端连不上可能是 WebSocket 握手失败了。用curl -i -N -H Connection: Upgrade -H Upgrade: websocket -H Sec-WebSocket-Version: 13 -H Sec-WebSocket-Key: test http://127.0.0.1:9000看一下握手响应。正常的响应应该是101 Switching Protocols。如果返回400 Bad Request说明服务端的 WebSocket 实现有问题。还有一个容易忽略的点浏览器对 WebSocket 的跨域限制。如果你的 HTML 页面是从file://打开的连ws://127.0.0.1:9000一般没问题。但如果页面是从某个 HTTP 服务打开的而 WebSocket 服务在另一个端口浏览器可能会因为同源策略拒绝连接。这种情况下需要在服务端设置Access-Control-Allow-Origin头或者把页面和 WebSocket 服务放在同一个端口。5.3 数据乱码与丢包的实战分析数据乱码最常见的原因是波特率不匹配。但还有一个容易被忽略的原因串口参数中的停止位和校验位。有些设备用 2 位停止位有些用偶校验如果你的转发程序写死了 1 位停止位和无校验数据就会错位。丢包问题更隐蔽。串口本身是可靠传输不会丢字节。但如果你在转发程序里用了try_read而不是read或者在广播通道里用了try_send而不是send就可能丢数据。try_read在数据没准备好时直接返回WouldBlock如果你忽略了这个错误继续循环那一轮的数据就丢了。try_send在通道满了的时候直接返回错误数据就丢了。正确的做法是用read和send它们会等待直到操作完成。但这样会带来背压问题如果 WebSocket 客户端消费数据的速度跟不上串口生产数据的速度广播通道会满send会阻塞进而阻塞串口读取。这时候串口的硬件缓冲区会满新来的数据就会被硬件丢弃。解决背压问题的常见方案是有界通道加丢弃策略。给广播通道设一个合理的容量比如 1024 条消息。当通道满的时候丢弃最老的消息保留最新的。这样虽然会丢一些数据但不会阻塞串口读取保证实时性。对于调试场景来说丢一些历史日志通常可以接受但实时性不能丢。let (tx, _) broadcast::channel(1024);broadcast::channel的容量参数就是通道能缓冲的消息条数。设成 1024 意味着最多缓冲 1024 条消息超了就会覆盖最老的。5.4 常见问题速查表现象可能原因排查方法解决方案串口打开失败报 Access denied端口被占用Windows:modeLinux:lsof /dev/ttyUSB0关闭占用程序或重启串口打开失败报 No such file串口名错误或驱动未装设备管理器/ls /dev/tty*检查设备名重装驱动数据乱码波特率/数据位/校验位不匹配核对设备手册调整串口参数WebSocket 连不上服务未启动或防火墙拦截telnet测试端口启动服务放行端口WebSocket 连上但收不到数据广播通道未订阅或串口无数据检查日志确认串口有输出检查订阅逻辑确认串口连接数据丢包通道满或用了 try_send查看日志中的丢弃计数增大通道容量改用 send多个客户端写串口数据交错写入未加锁检查写入逻辑用 Mutex 保护串口写句柄避坑技巧在开发阶段把日志级别设成debug把串口读到的每个数据块的大小和内容都打出来。这样一旦出现丢包或乱码能快速定位是串口层的问题还是 WebSocket 层的问题。上线后再把日志级别调回info避免日志太多影响性能。6. 这套方案还能怎么扩展6.1 加一个 Web 前端做远程串口监视器前面给的 HTML 页面只是最简版本只能看数据。你可以把它扩展成一个完整的串口监视器加上发送框可以往串口发数据加上十六进制显示切换加上时间戳加上日志保存功能。因为 WebSocket 是双向的发送功能只需要在ws.send()里把数据前面加上0x01类型标识就行。如果你想做得更正式可以用yew或者leptos这类 Rust 前端框架把整个监视器用 Rust 写编译成 WebAssembly 在浏览器里跑。这样前后端都是 Rust类型可以共享开发体验很统一。不过 WebAssembly 的生态还在完善中如果你对前端不太熟用原生 JavaScript 写反而更快。6.2 串口数据持久化与回放调试的时候经常需要回看之前的串口日志。你可以在转发程序里加一个可选的持久化模块把串口数据按时间戳写到文件里。格式可以用简单的二进制每条记录是[8字节时间戳][4字节长度][数据]。回放的时候读这个文件按时间戳的间隔重放数据模拟真实串口的行为。这个功能对于复现 bug 特别有用。设备现场出了问题把日志文件拿回来在办公室回放慢慢分析。不用把设备也带回来也不用担心现场环境无法复现。6.3 在嵌入式 Linux 板子上部署转发服务如果你的调试目标是一块跑 Linux 的嵌入式板子比如 RK3568你可以把转发程序交叉编译成 aarch64 的二进制直接放到板子上跑。这样串口数据从板子内部就直接转发出来了不需要在开发机上插 USB 转串口线。交叉编译的步骤大概是rustup target add aarch64-unknown-linux-gnu cargo build --release --target aarch64-unknown-linux-gnu然后把target/aarch64-unknown-linux-gnu/release/serial-ws-bridge拷到板子上。板子上需要有一个能跑 Rust 二进制的环境一般来说 Linux 内核 4.x 以上都没问题。如果板子的 libc 版本比较老可能需要用musl目标做静态链接rustup target add aarch64-unknown-linux-musl cargo build --release --target aarch64-unknown-linux-muslmusl版本的二进制是静态链接的不依赖板子上的 libc兼容性更好。6.4 结合 Tauri 做一个桌面版串口调试工具如果你想要一个带界面的串口调试工具可以用Tauri把 Rust 后端和 Web 前端打包成桌面应用。Tauri 的核心思路是Rust 写后端逻辑串口读写、WebSocket 服务Web 技术写界面HTML/CSS/JavaScript打包成一个原生应用。这样做的好处是你可以把前面写的串口转发逻辑直接复用只需要加一个 Tauri 的命令层把串口数据通过 Tauri 的事件系统推给前端。前端可以用任何你熟悉的框架React、Vue、Svelte 都行。打包出来的应用体积很小Tauri 用系统自带的 WebView不像 Electron 要打包整个 Chromium启动速度也快。我试过用 Tauri 做一个类似的工具从零到能跑大概花了一个周末。主要的坑在 Tauri 的权限配置上它默认不允许前端访问文件系统和网络需要在tauri.conf.json里显式开启。串口访问倒是没问题因为串口操作是在 Rust 侧做的不涉及前端权限。6.5 性能调优的几个关键参数最后聊几个性能相关的参数这些是我在实际使用中调出来的经验值。串口读缓冲区大小默认 1024 字节。如果波特率高于 460800建议调到 4096 或 8192。如果波特率低于 9600可以降到 256减少延迟。广播通道容量默认 1024 条消息。如果客户端消费速度慢可以调到 4096。如果内存紧张可以降到 256。这个值需要根据实际场景权衡。WebSocket 写超时tokio-tungstenite默认没有写超时如果客户端网络慢写操作可能阻塞很久。建议加一个 5 秒的超时tokio::time::timeout(Duration::from_secs(5), ws_sink.send(msg)).await超时后直接断开该客户端避免它拖慢整个广播循环。TCP_NODELAYWebSocket 底层是 TCP默认开启 Nagle 算法会合并小包发送增加延迟。对于串口调试这种实时性要求高的场景建议关掉 Naglestream.set_nodelay(true)?;关掉之后每个 WebSocket 消息都会立即发送延迟能降低几毫秒到几十毫秒。对于调试来说这点延迟降低还是很明显的。我在实际使用中发现这套方案最舒服的地方是随时随地能看串口数据。在办公室用台式机看在会议室用笔记本看甚至在外面用手机浏览器也能看只要网络能通到转发服务。串口助手那种必须坐在设备旁边的调试方式一旦习惯了远程监视就再也回不去了。
返回列表