1. 项目概述
1.1 项目背景
在嵌入式开发、工业控制和物联网项目中,串口通信是最基础也是最常用的通信方式。为提升开发效率和用户体验,本项目基于 Tauri 2 + Vue3 + TypeScript + Rust 打造现代化、跨平台的串口调试助手。
1.2 技术架构
- 前端:Vue 3 (Composition API) + TypeScript + Vite + CSS 变量
- 后端:Tauri 2 (Rust) + serialport crate
- 通信:Tauri invoke/event 机制实现前后端高效交互
2. 系统功能
2.1 串口管理
-
自动检测 USB 串口设备,支持热插拔,串口列表实时刷新
-
支持串口参数全配置:
- 波特率(110-921600)
- 数据位(5/6/7/8)
- 停止位(1/2)
- 校验位(无/奇/偶)
- 流控制(无/软件/硬件)
-
串口连接/断开,状态小圆圈指示(绿色/灰色)

2.2 数据通信
- 实时接收串口数据,支持最大1000字节/包,超时处理(10ms)
- 数据发送功能,支持添加换行符,任意文本发送
- 日志内容为纯文本,便于复制粘贴,无标签和颜色
- 支持日志清空、显示时间戳、自动滚动
- 支持日志保存为本地文件、加载本地日志文件(txt/json)

2.3 错误处理与反馈
- 串口占用、不存在、读取失败等均有友好提示
- 自动断开异常连接,防止死锁
- 前端弹窗/提示栏反馈错误信息

2.4 响应式现代UI
- 侧边栏参数设置、主区域日志/数据视图
- 状态指示器(小圆圈+文字)
- 响应式布局,适配不同分辨率

3. 技术实现细节
3.1 前后端交互
- 前端通过
@tauri-apps/api/core
的invoke
调用 Rust 后端命令(如 open_port、read_data、send_data、close_port、list_ports) - 后端通过 Tauri
emit
事件(如 ports-changed)通知前端串口列表变化,实现热插拔 - 数据流:前端定时轮询/事件驱动读取串口数据,写入日志区
3.2 串口事件与状态管理
- Rust 后端维护串口状态(连接、端口、参数等),并定时检测端口变化
- 端口变化通过事件推送到前端,前端自动刷新串口下拉列表
- 连接状态、错误状态通过响应式变量驱动 UI 实时更新
3.3 组件结构
SerialMonitor.vue
:主监控组件,包含参数设置、日志显示、状态指示、数据操作等SerialControl.vue
:参数选择与连接控制SerialData.vue
:数据区显示与操作DataVisualization.vue
:数据可视化(预留/可扩展)
3.4 性能与体验优化
- 日志区虚拟滚动,自动滚动到最新
- 数据处理采用异步/防抖,避免 UI 卡顿
- 组件解耦,便于维护和扩展
- 生产环境禁用开发者工具,提升安全性
3.5 异常与资源管理
- 串口断开/异常自动释放资源
- 日志保存/加载支持大文件处理
- 内存泄漏防护,组件卸载时清理定时器和事件
4. 关键代码与技术细节
4.1 Rust 后端串口管理(详细实现)
scss
// 串口状态结构体
struct SerialState {
port: Option<Box<dyn SerialPort>>,
}
// 打开串口
#[tauri::command]
fn open_port(
port_name: String,
baud_rate: u32,
data_bits: u8,
stop_bits: u8,
parity: String,
flow_control: String,
state: State<Arc<Mutex<SerialState>>>
) -> Result<(), String> {
// 检查端口、参数解析
// ...
let port = serialport::new(&port_name, baud_rate)
.timeout(Duration::from_millis(10))
.data_bits(parse_data_bits(data_bits)?)
.flow_control(parse_flow_control(&flow_control)?)
.parity(parse_parity(&parity)?)
.stop_bits(parse_stop_bits(stop_bits)?)
.open()
.map_err(|e| format!("打开串口失败: {}", e))?;
state.lock().unwrap().port = Some(port);
Ok(())
}
// 读取数据
#[tauri::command]
fn read_data(state: State<Arc<Mutex<SerialState>>>) -> Result<String, String> {
let mut state = state.lock().unwrap();
if let Some(port) = &mut state.port {
let mut buffer: Vec<u8> = vec![0; 1000];
match port.read(&mut buffer) {
Ok(t) => {
buffer.truncate(t);
Ok(String::from_utf8_lossy(&buffer).to_string())
}
Err(ref e) if e.kind() == io::ErrorKind::TimedOut => Ok(String::new()),
Err(e) => Err(format!("读取数据失败: {}", e)),
}
} else {
Err("串口未打开".to_string())
}
}
// 发送数据
#[tauri::command]
fn send_data(data: String, state: State<Arc<Mutex<SerialState>>>) -> Result<(), String> {
let mut state = state.lock().unwrap();
if let Some(port) = &mut state.port {
port.write_all(data.as_bytes()).map_err(|e| format!("发送数据失败: {}", e))?;
port.flush().map_err(|e| format!("刷新缓冲区失败: {}", e))?;
Ok(())
} else {
Err("串口未打开".to_string())
}
}
端口热插拔与事件推送
scss
fn start_port_monitor(app_handle: tauri::AppHandle, port_list_state: Arc<Mutex<PortListState>>) {
std::thread::spawn(move || {
loop {
if let Ok(current_ports) = list_ports() {
let mut state = port_list_state.lock().unwrap();
if check_ports_changed(&state.ports, ¤t_ports) {
state.ports = current_ports.clone();
if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.emit("ports-changed", current_ports);
}
}
}
std::thread::sleep(Duration::from_secs(1));
}
});
}
4.2 前端串口操作与数据流
javascript
import { invoke } from '@tauri-apps/api/core'
import { listen } from '@tauri-apps/api/event'
// 连接串口
await invoke('open_port', {
portName: selectedPort.value,
baudRate: baudRate.value,
dataBits: dataBits.value,
stopBits: stopBits.value,
parity: parity.value,
flowControl: flowControl.value
})
// 读取数据(定时轮询)
const startReading = () => {
readInterval = setInterval(async () => {
try {
const data = await invoke('read_data')
if (data) addLog(data, 'data')
} catch (error) {
addLog(`读取数据失败: ${error}`, 'error')
disconnectPort()
}
}, 100)
}
// 端口热插拔监听
onMounted(async () => {
await listPorts()
portsChangedUnlisten = await listen('ports-changed', (event) => {
availablePorts.value = event.payload
addLog('串口列表已更新', 'info')
})
})
4.3 日志保存与加载
typescript
// 保存日志为本地文件
const saveData = async () => {
const data = logs.value.map(log => ({
...log,
formattedTime: formatTimestamp(log.timestamp)
}))
const textData = data.map(log =>
`[${log.formattedTime}] ${log.type.toUpperCase()}: ${log.content}`
).join('\n')
const blob = new Blob([textData], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `serial-log-${new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-')}.txt`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
addLog('数据已下载', 'info')
}
// 加载日志文件
const loadData = async (event) => {
const file = event.target.files[0]
if (!file) return
const fileContent = await file.text()
if (file.name.endsWith('.json')) {
logs.value = JSON.parse(fileContent)
} else {
const lines = fileContent.split('\n').filter(line => line.trim())
logs.value = lines.map((line, index) => ({
content: line,
type: 'data',
timestamp: Date.now() + index
}))
}
addLog(`已加载数据文件: ${file.name}`, 'info')
}
4.4 组件结构与类型定义
SerialControl.vue(参数配置)
ruby
<select :value="selectedBaudRate" @change="handleBaudRateChange" :disabled="isConnected">
<option v-for="rate in baudRates" :key="rate" :value="rate">{{ rate }}</option>
</select>
SerialData.vue(数据区)
xml
<pre>{{ receivedData }}</pre>
<button @click="emit('clear-data')" :disabled="!receivedData">清除数据</button>
类型定义(TypeScript)
typescript
export interface SerialState {
isConnected: boolean;
isReading: boolean;
selectedPort: string;
selectedBaudRate: string;
receivedData: string;
statusMessage: string;
hasError: boolean;
}
4.5 数据可视化(可选扩展)
DataVisualization.vue(核心渲染逻辑)
javascript
const drawChart = () => {
// 计算数据范围、绘制坐标轴、网格线、数据线、数据点
// 支持自适应缩放和实时刷新
}
5. 未来扩展方向
- 多串口同时操作
- 协议解析插件
- 数据可视化与分析
- 跨平台打包与自动更新
如有需要可以联系我,我会无偿提供改该工具供大家使用。