前要
此文章属于代码没有进行优化(优化点还是很多,流水线代码更知道步骤点)
项目背景
在许多嵌入式设备或工业设备开发场景中,常常需要通过串口与设备进行通信,例如发送命令、读取传感器数据、控制设备工作状态。同时,一些设备(如相机、智能终端)会通过 MTP(Media Transfer Protocol) 接口提供文件访问功能,例如抓拍后的图像文件或设备的日志文件。
一个典型的需求是:
通过串口发送指令控制设备执行某个动作(例如拍照)。 通过 MTP 接口获取设备执行后的文件(例如拍摄完成的照片)。
传统方案多采用 C/C++,或者使用 Python 进行快速开发,但这些方案存在以下痛点:
- C/C++ :开发周期长、内存安全问题突出;
- Python:执行性能较弱、依赖解释器部署环境。
因此,需要一种 兼具性能与安全性 的解决方案来同时处理串口通信和 MTP 文件访问。
为什么选择 Rust
-
内存安全
Rust 拥有独特的所有权系统和借用检查机制,在无垃圾回收器的前提下保证内存安全,有效避免传统 C/C++ 项目中的野指针、内存泄漏等问题。
-
高性能
Rust 的执行性能接近 C/C++,适用于需要高吞吐和低延迟的硬件接口场景,比如串口高频数据通信和 MTP 文件传输。
-
并发友好
Rust 在语言层面提供安全的并发支持(如
Send
和Sync
trait 检查),结合tokio
等异步运行时,可以轻松实现同时进行串口通信和文件传输。 -
跨平台能力
Rust 支持 Windows、Linux、macOS,多平台开发体验一致,可以减少针对不同系统的移植成本。
-
生态支持
Rust 已有较为成熟的串口通信库(
serialport
、tokio-serial
)和 USB 通信库(rusb
),可以快速构建稳定可靠的解决方案
环境准备
这里只提供必备的依赖库
toml
tauri = {version = "1.6.0", features = ["api-all", "devtools"] }
tokio-serial = "5.4.5"
tokio = { version = "^1.0", features = ["full"] }
windows = { version = "0.56", features = [
"Win32_System_Com",
"Win32_System_Com_StructuredStorage",
"Win32_UI_Shell_PropertiesSystem",
"Win32_Devices_PortableDevices",
"Devices",
"Devices_Portable",
"Devices_Enumeration",
"Storage",
"Storage_Streams"
] }
Rust串口通信内容
使用tokio的serial 这个是基于tokio异步运行时封装的通信库,支持非阻塞I/O,持续监听串口、同时执行多任务
- 创建串口列表获取及串口绑定(因为串口板子是固定的,机器是不固定的,所以不同的机器可能存在不同的串口号,但是又不能一直去切换串口号进行绑定,所以通过VID 和 PID进行匹配,减少手动操作)
Rust
/// 串口设备信息结构体、用于返回串口列表信息
#[derive(Serialize, Deserialize, Debug)]
pub struct PortInfo {
/// 串口名称
pub port_name: String,
/// 供应商ID
pub vid: u16,
/// 产品ID
pub pid: u16,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct SerialManager {
/// 串口实例,使用Arc<Mutex>包装以支持多线程安全访问
/// Arc允许多个所有者共享同一数据,Mutex确保同一时间只有一个线程可以访问串口
pub serial: Arc<Mutex<SerialStream>>,
/// 用于控制后台任务停止的标志位
/// 使用AtomicBool确保在多线程环境下安全地修改状态
/// Arc包装允许在多个线程间共享这个停止标志
pub stop_flag: Arc<AtomicBool>,
}
impl Drop for SerialManager {
fn drop(&mut self) {
// 设置停止标志,让后台任务退出
self.stop_flag.store(true, Ordering::Relaxed);
// 给一点时间让后台任务完全退出
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
impl SerialManager {
/// 创建一个新的SerialManager实例 进行串口的绑定,并且处理绑定错误
/// - Result<Self, String>: 成功返回SerialManager实例,失败返回错误信息
pub async fn new(prot_name:&str)-> Result<Self, String> {
// 初始化重试计数器和最大重试次数
let mut retry_count = 0;
let max_retries = 3;
// 循环尝试打开串口
loop {
// 尝试以1.5M波特率打开串口
match tokio_serial::new(port_name, 1_500_000).open_native_async() {
// 串口打开成功
Ok(serial) => {
// 打印成功信息
println!("串口绑定成功: {}", port_name);
// 返回新的SerialManager实例
return Ok(Self {
// 使用Arc和Mutex包装串口实例以支持多线程访问
serial: Arc::new(Mutex::new(serial)),
// 初始化停止标志为false
stop_flag: Arc::new(AtomicBool::new(false)),
});
}
// 串口打开失败
Err(e) => {
// 增加重试计数
retry_count += 1;
// 检查是否达到最大重试次数
if retry_count >= max_retries {
// 达到最大重试次数,返回错误信息
return Err(format!("绑定串口失败 (重试{}次): {:?}", max_retries, e));
}
// 打印重试信息
println!("串口绑定失败,第{}次重试...", retry_count);
// 等待500毫秒后重试
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
}
}
}
}
/// 这里因为我使用的是串口板子,在将串口板子连接在设备上,这样能在使用的时候宝子VID 和 PID匹配时,直接能连接上设备,不用顾忌到他的串口号如COMM5 COMM8
/// 获取系统中所有可用的串口设备信息
///
/// # 返回值
/// 返回一个包含所有可用串口信息的向量,每个元素包含端口名称、VID和PID
///
/// # 实现细节
/// - 使用serialport库获取系统中所有可用的串口
/// - 遍历每个串口,提取其端口名称和USB信息(如果是USB设备)
/// - 对于非USB设备,VID和PID将被设置为0
pub async fn get_prots() -> Vec<PortInfo> {
let ports = serialport::available_ports().unwrap();
let mut port_list: Vec<PortInfo> = Vec::new();
for port in ports {
println!("{:?}", port.port_name); // 打印端口名
// 默认值 (非USB时)
let (vid, pid) = match port.port_type {
SerialPortType::UsbPort(info) => (info.vid, info.pid),
_ => {
println!("Not a USB port");
(0, 0)
}
};
port_list.push(PortInfo {
port_name: port.port_name,
vid,
pid,
});
}
port_list
}
}
- 串口通信,写入串口信息 和 获取串口信息内容;这里你也可以使用通道(
mpsc
)代替callback
rust
impl SerialManager {
...
....
pub fn loop_read_message<F>(&self, callback: F)
where
F: Fn(&str) + Send + 'static,
{
// 克隆串口实例的Arc引用计数器,用于在新线程中使用
let serial_clone = Arc::clone(&self.serial);
// 克隆停止标志的Arc引用计数器,用于控制线程的停止
let stop_flag = Arc::clone(&self.stop_flag);
// 创建新的异步任务
tokio::spawn(async move {
let mut buffer = [0u8; 2048];
let mut temp_buf = Vec::new();
loop {
// 检查停止标志
if stop_flag.load(Ordering::Relaxed) {
println!("串口读取任务收到停止信号,正在退出...");
break;
}
// 获取串口的互斥锁
let mut serial_guard = serial_clone.lock().await;match serial_guard.read(&mut buffer).await {
// 成功读取到数据且长度大于0
Ok(n) if n > 0 => {
// 将读取到的数据追加到临时缓冲区
temp_buf.extend_from_slice(&buffer[..n]);
// 循环处理缓冲区中的每一行数据
while let Some(pos) = temp_buf.iter().position(|&b| b == b'\n') {
// 提取一行数据
let line = temp_buf.drain(..=pos).collect::<Vec<u8>>();
// 将字节数据转换为ASCII字符串
let ascii_str: String = line
.iter()
.map(|&b| {
// 如果是可打印字符或空格则保留,否则替换为点
if b.is_ascii_graphic() || b == b' ' {
b as char
} else {
'.'
}
})
.collect();
callback(format!("output:{}", ascii_str).as_str())
}
}
// 没有读取到数据
Ok(_) => {
// 短暂休眠避免CPU占用过高
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
}
// 读取出错
Err(e) => {
eprintln!("串口读取错误: {:?}", e);
callback("Error:串口读取错误");
break;
}
}
// 释放串口锁
drop(serial_guard);
// 短暂休眠让出CPU时间片
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
})
}
pub async fn send(&self, data: &[u8]) {
let mut serial_guard = self.serial.lock().await;
println!("发送{:?}", data);
serial_guard.write_all(data).await.unwrap();
serial_guard.flush().await.unwrap(); // 关键: 异步 flush 确保数据发送完成
println!("发送完成");
}
}
/// 发送消息示例
pub async fn case_item(&mut self, case_item: &str) {
self.case_item = case_item.to_string();
let mut cmd = String::with_capacity(case_item.len() + 2);
info!("[TEST] 开始单项测试: {}", case_item);
cmd.push_str(case_item);
cmd.push_str("\r\n");
let cmd = cmd.as_bytes();
self.send(cmd).await;
}
/// 回调函数示例
let callback_emit: tauri::Window = emit_win.clone();
serial.loop_read_message(move |msg| {
// 这里进行自己的交互操作
// 比如进行tauri 的emit 发送至前端
let _ = callback_emit.emit("you_listen_name", msg);
});
MTP
1、 使用的是Windows 的API 进行文件的获取和文件流读取,基于Rust 将文件流创建或者写入新的访问文件
Rust
use windows::core::HSTRING;
use windows::Devices::Enumeration::DeviceInformation;
use windows::Devices::Portable::StorageDevice;
use windows::Storage::Streams::DataReader;
use windows::Storage::{FileIO, IStorageFile, NameCollisionOption, StorageFolder};
// 异步函数:获取 MTP 设备上的图片文件
pub async fn get_mtp_devices_image(img_name: &str) {
// 获取一个设备选择器字符串,用于枚举支持 MTP(便携式存储)的设备
// 这个选择器字符串会被传递给 DeviceInformation::FindAllAsync() 进行设备搜索
let devices = StorageDevice::GetDeviceSelector();
match devices{
Ok(devices)=>{
// 如果成功获得设备选择器(devices 是一个 HSTRING 字符串)
let storage = DeviceInformation::FindAllAsyncAqsFilter(&devices)
.unwrap()
.await
.map_err(|err|format!("get_mtp_devices {:?}",err)).unwrap();
// 收集所有设备ID,避免持有迭代器
let device_ids: Vec<_> = storage
.into_iter()
.filter_map(|device| device.Id().ok())
.collect();
// 处理每个设备ID
for device_id in device_ids {
// 这里其实就获取到了设备了,但是假如你要去获取某个文件,比如在SCRARD1/test/xx.img
let file_result = {
let root = StorageDevice::FromId(&device_id).unwrap();
let file_name = HSTRING::from("SCARD1");
root.GetFolderAsync(&file_name)
}; // root在这里被drop
/// 找test文件目录
let audio_file = match file_result {
Ok(file_async) => match file_async.await {
Ok(scard1_file) => {
let result = {
let file_name = HSTRING::from("test");
let file = scard1_file.GetFolderAsync(&file_name);
file
};
Ok(result)
}
Err(e) => {
println!("Failed to get SCARD1: {:?}", e);
Err(e)
}
},
Err(e) => {
println!("Failed to get file SCARD1: {:?}", e);
Err(e)
}
};
/// 找图片文件
let res = match audio_file.unwrap() {
Ok(file) => match file.await {
Ok(audio_file) => {
let result = {
let file_name = HSTRING::from(file_name);
let file = audio_file.GetFileAsync(&file_name);
file
};
Ok(result.unwrap())
}
Err(e) => {
println!("Failed to get audio: {:?}", e);
Err(e)
}
},
Err(e) => {
println!("Failed to get file audio: {:?}", e);
Err(e)
}
};
// 获取文件流
let img_file_buff = match res {
Ok(file) => match file.await {
Ok(img_file) => {
let buf = FileIO::ReadBufferAsync(&img_file);
Ok(buf)
}
Err(e) => {
println!("Failed to get file audio: {:?}", e);
Err(e)
}
},
Err(e) => {
println!("Failed to get file audio: {:?}", e);
Err(e)
}
};
// 将文件流写入可访问文件中
match img_file_buff {
Ok(buf) => match buf {
Ok(buffer) => {
let buf = buffer.await.unwrap();
let lens = buf.Length().unwrap();
let mut file_data = vec![0u8; lens as usize];
let data_reader = DataReader::FromBuffer(&buf).unwrap();
data_reader.ReadBytes(file_data.as_mut_slice()).unwrap();
// 保存到本地文件
let exe_path = std::env::current_exe().map_err(|e| e.to_string()).unwrap();
let exe_dir = exe_path.parent().ok_or("无法获取执行文件目录").unwrap();
let target_dir = exe_dir.join("resources").join("mtp").join("mic");
// 确保目录存在
std::fs::create_dir_all(&target_dir).map_err(|e| e.to_string()).unwrap();
// 写入文件
let target_file = target_dir.join("mic_5ch.wav");
std::fs::write(&target_file, &file_data).map_err(|e| e.to_string()).unwrap();
}
Err(e) => {
println!("Failed to get file audio: {:?}", e);
}
},
Err(e) => {
println!("Failed to get file audio: {:?}", e);
}
}
}
}
Err(e)=>{
println!("get_mtp_devices error {:?}", e);
}
}
}