使用Rust 串口通信 及 MTP文件获取

前要

此文章属于代码没有进行优化(优化点还是很多,流水线代码更知道步骤点)

项目背景

在许多嵌入式设备或工业设备开发场景中,常常需要通过串口与设备进行通信,例如发送命令、读取传感器数据、控制设备工作状态。同时,一些设备(如相机、智能终端)会通过 MTP(Media Transfer Protocol) 接口提供文件访问功能,例如抓拍后的图像文件或设备的日志文件。

一个典型的需求是:

通过串口发送指令控制设备执行某个动作(例如拍照)。 通过 MTP 接口获取设备执行后的文件(例如拍摄完成的照片)。

传统方案多采用 C/C++,或者使用 Python 进行快速开发,但这些方案存在以下痛点:

  • C/C++ :开发周期长、内存安全问题突出;
  • Python:执行性能较弱、依赖解释器部署环境。

因此,需要一种 兼具性能与安全性 的解决方案来同时处理串口通信和 MTP 文件访问。

为什么选择 Rust

  1. 内存安全

    Rust 拥有独特的所有权系统和借用检查机制,在无垃圾回收器的前提下保证内存安全,有效避免传统 C/C++ 项目中的野指针、内存泄漏等问题。

  2. 高性能

    Rust 的执行性能接近 C/C++,适用于需要高吞吐和低延迟的硬件接口场景,比如串口高频数据通信和 MTP 文件传输。

  3. 并发友好

    Rust 在语言层面提供安全的并发支持(如 SendSync trait 检查),结合 tokio 等异步运行时,可以轻松实现同时进行串口通信和文件传输。

  4. 跨平台能力

    Rust 支持 Windows、Linux、macOS,多平台开发体验一致,可以减少针对不同系统的移植成本。

  5. 生态支持

    Rust 已有较为成熟的串口通信库(serialporttokio-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,持续监听串口、同时执行多任务

  1. 创建串口列表获取及串口绑定(因为串口板子是固定的,机器是不固定的,所以不同的机器可能存在不同的串口号,但是又不能一直去切换串口号进行绑定,所以通过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
    }
}
  1. 串口通信,写入串口信息 和 获取串口信息内容;这里你也可以使用通道(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);
        }
    }
}
相关推荐
无名客028 分钟前
npm run dev 启动项目 报Error: listen EACCES: permission denied 0.0.0.0:80 解决方法
前端·javascript·vue.js
零点七九30 分钟前
vue npm install卡住没反应
前端·vue.js·npm
墨菲安全34 分钟前
NPM组件 @0xme5war/apicli 等窃取主机敏感信息
前端·npm·node.js·主机信息窃取·npm恶意包·npm投毒
Komorebi_999935 分钟前
vue create 项目名 和 npm init vue@latest 创建vue项目的不同
前端·vue.js·npm
好好研究4 小时前
使用JavaScript实现轮播图的自动切换和左右箭头切换效果
开发语言·前端·javascript·css·html
程序视点8 小时前
IObit Uninstaller Pro专业卸载,免激活版本,卸载清理注册表,彻底告别软件残留
前端·windows·后端
前端程序媛-Tian8 小时前
【dropdown组件填坑指南】—怎么实现下拉框的位置计算
前端·javascript·vue
嘉琪0018 小时前
实现视频实时马赛克
linux·前端·javascript
烛阴8 小时前
Smoothstep
前端·webgl
若梦plus9 小时前
Eslint中微内核&插件化思想的应用
前端·eslint