基于Tauri + Visa 创建的电量计仪器测试应用工具

3,243 阅读13分钟

前文:电量计仪器测试应用是一种用于测试和校准电量计设备的软件工具。这类应用通常用于制造、研发和质量保证等领域,确保电量计设备的准确性和性能

  1. 功能列表
  • 设备连接:通过IP连接电量计设备。
  • 实时数据监控:实时显示电量计测量的数据,如电压、电流、功率、能量等参数。
  • 数据记录与存储:记录测量数据,并且支持回显数据与记录删除
  • 自动化测试:支持设置自动化测试流程,根据预设的测试条件自动进行测试
  • 用户界面:友好的用户界面,易于操作和理解。
  • 定时测试:根据预设等待时间,测试时长进行自动化测试
  • 兼容性:支持Ni、Keysight等工具
  1. 示例图

visa.gif

  1. 技术方案
  • 技术层:Tauri + Rust
  • UI层:Vue3 + ant-design-vue + echarts

内容相关

  1. 基础配置

    主要是:visa-rs,tokio,serde_json,lazy_static。

    Visa-rs是电量计仪器的基础工具,参考地址:crates.io/crates/visa… 但是他里面有个重点:This crate needs to link to an installed visa library, for example, NI-VISA.,这个库需要依赖于Visa的标准库,需要在使用时,你的电脑有存在这个环境,你可以去Ni-Visa或者德科里面去下载相关的应用,会自动配置相关的环境,参考地址:www.ni.com/zh-cn/suppo…

    tokio是Rust常用的异步工具库,是一个很成熟的库

    serde_json用于处理前后端交互或数据储存时,用于序列号与反序列号JSON数据,提供了简单的操作API

    lazy_static是常用的用于定义全局变量的库


tauri-build = { version = "1", features = [] }

[dependencies]
tauri = { version = "1", features = [ "api-all"] }
serde = { version = "1", features = ["derive"] }
tokio = { version = "1.38.0", features = ["full"] }
serde_json = "1"
lazy_static = "1.4.0"
visa-rs = "0.6.1"
rayon = "1.5.1"

log = "0.4.8"
simplelog = {version= "0.12.0",features= ["local-offset"] }
chrono = "0.4"
time = "0.3"

  1. Tauri config 的配置
{
  "build": {
    "beforeDevCommand": "npm run dev",
    "beforeBuildCommand": "npm run build",
    "devPath": "http://localhost:1421",
    "distDir": "../dist"
  },
  "package": {
    "productName": "电流测试工具",
    "version": "1.0.0"
  },
  "tauri": {
    "allowlist": {
      "all": true,
      "shell": {
        "all": false,
        "open": true
      },
      "window": {
        "all": true
      },
      "fs": {
        "all": true
      },
      "dialog": {
        "all": false,
        "open": true,
        "save": true
      }
    },
    "windows": [
      {
        "label": "main",
        "title": "电流测试工具",
        "width": 1400,
        "height": 900,
        "resizable": false,
        "skipTaskbar": false,
        "x": 0,
        "y": 0,
        "center": true,
        "decorations": false,
        "transparent": true
      }
    ],
    "security": {
      "csp": null
    },
    "bundle": {
      "active": true,
      "targets": "all",
      "identifier": "com.visa.dev",
      "resources":[
        "./log/"
      ],
      "icon": [
        "icons/32x32.png",
        "icons/128x128.png",
        "icons/128x128@2x.png",
        "icons/icon.icns",
        "icons/icon.ico"
      ],
      "windows": {
        "wix": {
          "language": "zh-CN"
        },
        "nsis": {
          "languages": [
            "SimpChinese"
          ]
        }
      }
    }
  }
}

主要代码

先去创建一个Visa的使用库,集成我们需要的方法

  1. 初始化Visa的实例需要创建资源管理器也就是DefaultRM,DefaultRM是一个visa_rs::AsResourceManager,是 visa-rs 库中的一个特性(trait),visa_rs::AsResourceManager 这个特性定义了与资源管理器(Resource Manager)相关的方法。这些方法允许你打开、关闭和管理与仪器的会话
   // 为了防止资源管理器释放后丢失连接,使用全局变量,使用Arc指针
   lazy_static! {
    static ref RESOURCE_DEFAULTRM: Arc<Mutex<DefaultRM>> = Arc::new(Mutex::new(
        DefaultRM::new()
            .map_err(|err| {
                log::error!("{:?}", err);
                return format!("{:?}", err);
            })
            .unwrap()
    ));
}
  1. 创建VisaInstrument的结构体,定义一个new方法,用于初始化连接设备,返回连接成功的Instrument 或者 失败信息
#[derive(Debug)]
pub struct VisaInstrument {
    instr: Instrument,
}
impl VisaInstrument {
    
    /// 创建一个新的Visa 仪器实例。
    ///
    /// 这个函数尝试根据提供的仪器IP地址初始化一个DEFAULTR资源管理器的实例。
    /// 它首先尝试获取资源管理器的锁,然后根据IP地址构造一个资源表达式,
    /// 接着查找和打开相应的资源,最后返回一个新的DEFAULTR实例。
    ///
    /// # 参数
    /// `instr_ip` - 仪器的IP地址,用于构造资源表达式。
    ///
    /// # 返回值
    /// 返回一个`Result`类型,其中包含初始化成功的DEFAULTR实例或错误信息。
    pub fn new(instr:String){
        // 日志记录初始化开始
        log::info!("初始化DEFAULTR 实例");
         let rm = RESOURCE_DEFAULTRM.lock().map_err(|e| format!("{:?}", e))?; 
        // 日志记录创建CString实例开始
        log::info!("创建CString 实例");
        // 构造资源表达式
        let ip_s = format!("TCPIP0::{}::inst0::INSTR", instr_ip);
        // 尝试将构造的字符串转换为CString,如果失败,则格式化错误并返回
        let expr = CString::new(ip_s)
            .map_err(|err| format!("没有找到相关资源:{:?}", err))?
            .into();
    
        
        // 在资源管理器中查找资源,如果失败,则格式化错误并返回
        let rsc = rm
            .find_res(&expr)
            .map_err(|op| format!("未找到相关的资源{:?}", op))?;

        // 使用找到的资源打开仪器,如果失败,则格式化错误并返回
        let instr = rm
            .open(&rsc, AccessMode::NO_LOCK, TIMEOUT_IMMEDIATE)
            .map_err(|op| format!("{:?}", op))?;

        // 返回初始化成功的DEFAULTR实例
        Ok(Self { instr })
    }
    
    /// 异步关闭所有设备。
    ///
    /// 此方法通过发送一条指令来关闭设备输出,然后清除指令缓冲区。
    /// 它返回一个结果,指示关闭操作是否成功。
    pub async fn close_all(&mut self) -> Result<(), String> {
        // 构造关闭输出的指令字符串
        let cmds = format!("OUTP OFF,(@1:3)\n");
        // 将指令字符串转换为字节序列,以备写入
        let cmd = cmds.as_bytes();
        // 获取指令写入器的引用
        let mut write_instr = &self.instr;
        // 尝试写入指令,并处理可能的错误
        let res = write_instr.write_all(cmd).map_err(|err| format!("{}", err));
        // 清除指令缓冲区,确保后续操作不会受到本次操作的影响
        let _ = self.instr.clear();
        // 返回操作结果
        res
    }
}
  1. 为了初始化后获取当前的资源池,用来展示当前仪器的列表吗,用于选择,效果如图

image.png

image.png

    impl VisaInstrument{
        ...
        ...
        
        /// 查找所有资源的函数
        /// 
        /// 返回结果为包含VisaString类型资源列表的Result对象,如果发生错误,则错误信息为String类型。
        pub fn find_all_resources() -> Result<Vec<VisaString>, String> {
            // 尝试获取RESOURCE_DEFAULTRM的锁,如果失败,则转换错误信息并返回
            let rm = RESOURCE_DEFAULTRM.lock().map_err(|e| format!("{:?}", e))?; 
            // 创建CString对象,用于后续的资源查找,如果创建失败,则转换错误信息并返回
            let expr = CString::new("?*INSTR").map_err(|err| format!("没有找到相关资源:{:?}", err))?; 
            // 初始化一个空的VisaString向量,用于存放查找结果
            let mut visa_list: Vec<VisaString> = Vec::new();
            // 使用rm对象和表达式查找资源列表
            let list = rm.find_res_list(&expr.into());
            // 根据查找结果进行处理
            match list {
                // 如果查找成功,遍历结果列表
                Ok(list) => {
                    for item in list {
                        // 对于每个查找结果项,根据结果进行处理
                        match item {
                            // 如果结果项成功,将其添加到visa_list中
                            Ok(vs) => {
                                visa_list.push(vs);
                            }
                            // 如果结果项失败,打印错误信息
                            Err(err) => {
                                println!("error:{:?}", err)
                            }
                        }
                    }
                }
                // 如果查找失败,返回错误信息
                Err(err) => {
                    return Err(format!("未找到相关的资源{:?}", err));
                }
            }
            // 打印查找结果
            println!("visa_list:{:?}",visa_list);
            // 返回包含查找结果的Ok对象
            Ok(visa_list)
        }
    }

  1. 创建设置参数API,这个地方是基于Keysight的命令格式。

#[derive(Serialize, Deserialize, Debug, Clone)]
struct ParamsItem {
    volt: f64,
    curr: f64,
    case: bool,
    switch: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct SetInstrParams {
    CH1: ParamsItem,
    CH2: ParamsItem,
    CH3: ParamsItem,
}

impl VisaInstarument{
    ...
    ...
     pub fn set_config(&mut self, item: String) -> Result<(), String> {
        let mut write_instr = &self.instr;

        // 解析item带来的配置信息
        let configs: SetInstrParams = serde_json::from_str(&item)
            .map_err(|err| {
                format!("{}", err)
            })
            .unwrap();
        for (index, config) in configs.into_iter().enumerate() {
            if config.case {
                let chn = (index + 1).to_string();
                let switch = config.switch;
                // 获取开关状态,并根据状态组装开/关命令
                let is_oepn = if switch { "ON" } else { "OFF" };
                let cmds = format!("OUTP {},(@{})\n", is_oepn, chn);
                let cmd = cmds.as_bytes();
                let _write = write_instr.write_all(cmd);

                // 设置通道选择
                let set_chn = format!("InST:NSEL {}\n", chn);
                let _is_set_chn = write_instr.write_all(set_chn.as_bytes()).map_err(|err| {
                    return format!("error:{:?}", err);
                })?;
                
                // 设置通道电压
                let volt = config.volt;
                let set_volt = format!("VOLT {}\n", volt);
                let _is_set_volt = write_instr.write_all(set_volt.as_bytes()).map_err(|err| {
                    return format!("error:{:?}", err);
                })?;
                
                // 设置通道电流
                let curr = config.curr;
                let set_curr = format!("CURR {}\n", curr);
                let _is_set_curr = write_instr.write_all(set_curr.as_bytes()).map_err(|err| {
                    return format!("error:{:?}", err);
                })?;

                log::info!("设置通道{},volt:{},curr:{}", index + 1, volt, curr);
            }
        }

        Ok(())
    }
    
}

    
  1. 定义轮询获取数据与取消数据获取,这里使用了stop_rx,是基于mpsc::channel 定义的通道消息,用来创建发送者与接受者,为了停止数据的获取。目前没有做重连机制,出现错误就直接抛出。

impl VisaInstarument{
     /// 异步读取当前数据。
    /// 
    /// 此函数负责定期从设备读取电流和电压数据,并将这些数据发送到TAURI窗口。
    /// 它会根据提供的通道列表循环读取每个通道的电流和电压值。
    /// 
    /// # 参数
    /// - `send_win`: Tauri窗口对象,用于向GUI发送事件和数据。
    /// - `stop_rx`: 接收停止信号的通道,用于在接收到停止信号时退出循环。
    /// - `item`: 包含通道列表的JSON字符串。
    /// - `split_time`: 读取数据之间的间隔时间(以毫秒为单位)的字符串 
     pub async fn read_current_data(
        &self,
        send_win: tauri::Window,
        mut stop_rx: Receiver<()>,
        item: String,
        split_time: String,
        ) {
            let chns: Vec<String> = serde_json::from_str(&item).unwrap();
            let rate = split_time.parse().unwrap();
            let interval = Duration::from_millis(rate);
            let _ = send_win.emit("start_read_case", "start");
            loop {
                tokio::select! {
                    _ = stop_rx.recv() => {
                        println!("Received stop signal, stopping loop.");
                        break;
                    },
                    _ = sleep(interval) => {

                        let chns =chns.clone();
                        let mut res = String::new();
                        for chn in chns.iter(){
                            let mut write_instr = &self.instr;
                            let command = format!("MEAS:CURR:DC? (@{})\n", chn);
                            let buf = command.as_bytes();
                            if let Err(_e) = write_instr.write_all(buf) {
                                let _ = send_win.emit("action_error","因未知原因掉线,请重新测试");
                                break;
                            }
                            let mut buf_reader = BufReader::new(write_instr);
                            let mut buf = String::new();
                            let _ = buf_reader.read_line(&mut buf);
                            let parsed: Result<f64, _> = buf.trim().parse();
                            let curr  =  match parsed {
                                Ok(val) => {
                                    let milli_val = val * 1000.0;
                                    milli_val
                                }
                                Err(_) => {
                                    let _ = send_win.emit("action_error","因未知原因掉线,请重新测试");
                                    break;
                                }
                            };


                            let command = format!("MEAS:VOLT:DC? (@{})\n", chn);
                            let buf = command.as_bytes();
                            if let Err(_e) = write_instr.write_all(buf) {
                                let _ = send_win.emit("action_error","因未知原因掉线,请重新测试");
                                break;
                            }
                            let mut buf_reader = BufReader::new(write_instr);
                            let mut buf = String::new();
                            let _ = buf_reader.read_line(&mut buf);
                            let parsed: Result<f64, _> = buf.trim().parse();
                            let volt = match parsed {
                                Ok(val) => {
                                    let milli_val = val *1.0;
                                    milli_val
                                }
                                Err(_) => {
                                    let _ = send_win.emit("action_error","因未知原因掉线,请重新测试");
                                    break;
                                }
                            };
                            let mut data = format!("{}:{:.3},{:.3}",chn,volt,curr);
                            if chn !="1" &&res != "" {
                                data = format!(";{}",data);
                            }
                            res+=&data;
                        }
                        let _ = send_win.emit("action_data", res);
                    }
                }
          }
    }
}

这里都是做的仪器的相关操作,包括参数设置,数据获取等功能。

数据储存与获取

创建一个use_case_data_file.rs文件, 其中包含临时文件创建,数据储存,数据获取,记录删除操作

使用到的库有 fs,io,path,serde


use std::{
    env,
    fs::{File, OpenOptions},
    io::{self, Read, Seek, SeekFrom, Write},
    path::PathBuf,
};

use serde::{Deserialize, Serialize};

// 定义数据结构
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Legend {
    data: Vec<String>,
    textStyle: TextStyle,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TextStyle {
    color: String,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Series {
    name: String,
    data: Vec<f64>,
    yAxisIndex: u32,
    tooltip: Tooltip,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Tooltip {}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Content {
    legend: Legend,
    series: Vec<Series>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TempData {
    name: String,
    content: Content,
}

#[derive(Debug, Clone)]
pub struct TempFile {
    path: PathBuf,
}

impl TempFile {
    pub fn new() -> Self {
        let mut temp_dir = env::temp_dir();
        temp_dir.push("visa_data_temp.json");
        TempFile { path: temp_dir }
    }

    pub fn initialize(&self) -> io::Result<()> {
        // 检查文件是否存在,如果不存在则创建并初始化
        if !self.path.exists() {
            let initial_data: Vec<TempData> = vec![]; // 初始化为一个空的 Data 向量
            let json_data = serde_json::to_string(&initial_data)?;
            let mut file = File::create(&self.path)?;
            file.write_all(json_data.as_bytes())?;
            file.flush()?;
        }
        Ok(())
    }

    pub fn write(&self, data: &TempData) -> io::Result<()> {
        // 读取现有内容
        let mut file = OpenOptions::new()
            .read(true)
            .write(true)
            .create(true)
            .open(&self.path)?;

        let mut contents = String::new();
        file.read_to_string(&mut contents)?;
        // 反序列化现有内容
        let mut data_vec: Vec<TempData> = if contents.trim().is_empty() {
            vec![]
        } else {
            serde_json::from_str(&contents)?
        };

        // 添加新的数据
        data_vec.push(data.clone());

        // 序列化并写回文件
        let json_data = serde_json::to_string(&data_vec)?;
        file.set_len(0)?; // 清空文件
        file.seek(SeekFrom::Start(0))?; // 将文件指针移动到文件开头
        file.write_all(json_data.as_bytes())?;
        file.flush()?;

        Ok(())
    }
    pub fn read(&self) -> io::Result<Vec<TempData>> {
        let mut file = File::open(&self.path)?;
        let mut contents = String::new();
        file.read_to_string(&mut contents)?;
        let data: Vec<TempData> = serde_json::from_str(&contents)?;
        Ok(data)
    }
    pub fn delete(&self, index: usize) -> io::Result<()> {
        // 读取现有内容
        let mut file = OpenOptions::new().read(true).write(true).open(&self.path)?;

        let mut contents = String::new();
        file.read_to_string(&mut contents)?;

        // 反序列化现有内容
        let mut data_vec: Vec<TempData> = if contents.trim().is_empty() {
            vec![]
        } else {
            serde_json::from_str(&contents)?
        };

        // 检查索引是否有效
        if index < data_vec.len() {
            // 删除指定索引的数据
            data_vec.remove(index);

            // 序列化并写回文件
            let json_data = serde_json::to_string(&data_vec)?;
            file.set_len(0)?; // 清空文件
            file.seek(SeekFrom::Start(0))?; // 将文件指针移动到文件开头
            file.write_all(json_data.as_bytes())?;
            file.flush()?;
        } else {
            println!("Invalid index: {}", index);
        }

        Ok(())
    }
}

前后端通信方式创建

这是部分参考代码,用于与前后端进行通信,使用的是Tauri 的listen 监听的方式,获取前端发送过来的信息,根据参数的不同而进行不同的逻辑。

#[derive(Serialize, Deserialize, Debug, Clone)]
struct ActionParams {
    name: String,
    item1: String,
    item2: String,
}

// 全局或高级作用域中维护最新的 Sender
struct Control {
    stop_tx: Option<mpsc::Sender<()>>,
}

impl Control {
    fn new() -> Self {
        Control { stop_tx: None }
    }
}

lazy_static! {
    static ref VISA_INSTRUMENT: Arc<RwLock<Option<VisaInstrument>>> = Arc::new(RwLock::new(None));
    static ref TEMP_DATA: Arc<Mutex<TempFile>> = Arc::new(Mutex::new(TempFile::new()));
}


#[tokio::main]
async fn main() {
    {
        let temp_file = TEMP_DATA.lock().await;
        let _ = temp_file.initialize();
    }
    tauri::Builder::default()
        .setup(|app|{
            let app_win = app.get_window('main').unwrap();
            let listen_win = app_win.clone();
            listen_win.listen("action_req", move |event| {
                let control = Arc::new(Mutex::new(Control::new()));
                let emit_win = app_win.clone();
                tokio::spawn(async move {
                     if let Some(data) = event.clone().payload() {
                          let params: ActionParams = serde_json::from_str(data).expect("No json"); 
                          let name = params.name.clone();
                          match name.as_str() {
                               "powerOn" => {
                                   /// 创建初始化的VisaInstrument 实例
                                   let instr_ip = params.item2.clone();
                                   let instr = VisaInstrument::new(instr_ip);
                                    match instr {
                                    Ok(instr) => {
                                        let mut visa_instrument = VISA_INSTRUMENT.write().await;
                                        *visa_instrument = Some(instr);
                                        let res = "ok:已经连接设备,请进行测试";
                                        let _emit = emit_win.emit("action_res", res);
                                    }
                                    Err(err) => {
                                        log::error!("error:{}", err);
                                        let res = format!("error:{}", err);
                                        let _emit = emit_win.emit("action_res", res);
                                    }
                                }
                                ...
                                ...
                                "start_read" => {
                                    let items = params.item1;
                                    let split_time = params.item2;
                                    let (stop_tx, stop_rx) = mpsc::channel::<()>(1);
                                    let mut ctrl = control.lock().await;
                                    ctrl.stop_tx = Some(stop_tx);
                                    drop(ctrl); // Drop lock before awaiting

                                    let send_win = emit_win.clone();
                                    let visa_instrument = VISA_INSTRUMENT.read().await;
                                    if let Some(ref instrs) = *visa_instrument {
                                        instrs
                                            .read_current_data(send_win, stop_rx, items, split_time)
                                            .await;
                                    }
                                }
                                ...
                               }
                          }
                            
                     } 
                })
            });
            Ok(())
        })
}

创建前端通信的代码。使用Tauri 的相关API,定义同步于异步的内容。


import { UnlistenFn, emit, listen } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api/tauri';

interface ActionParams {
    name: string,
    item1: any,
    item2: string
}

// 这个TypeScript代码片段定义了一些函数,用于与Tauri应用程序中的事件和API调用进行交互。

// ActionDataAsyncCallback函数是一个异步回调函数,用于执行某个操作并返回结果。它接受三个参数:name、item和item2。函数返回一个Promise对象,用于处理操作的成功或失败。内部逻辑是:

// 监听名为action_res的事件,当事件触发时,检查返回的数据中是否包含error,如果有则调用reject并返回错误信息;
// 如果没有错误,调用resolve并返回数据;
// 在监听之前,通过emit函数发送一个名为action_req的事件,并附带操作的参数。
// ACtionDataCallback函数用于执行某个操作并在操作完成后调用回调函数。它接受四个参数:name、item、item2和callback。函数内部逻辑是:

// 如果已经存在一个监听器unlisten,则先停止之前的监听;
// 监听名为action_data的事件,当事件触发时,调用传入的回调函数并传入事件的负载数据;
// 通过emit函数发送一个名为action_req的事件,并附带操作的参数。

export const ActionDataAsyncCallback = (name: string, item: any, item2: string) => {
    return new Promise(async (resolve, reject) => {
        try {
            let unlisten = await listen("action_res", e => {
                let data = e.payload as string;
                if (data.indexOf('error') != -1) {
                    reject(data)
                    return false;
                }
                unlisten()
                resolve(data)
            })
            let params: ActionParams = {
                name: name,
                item1: item,
                item2: item2
            }
            await emit("action_req", params)
        } catch (error) {
            reject(error)
        }
    })
}

let unlisten: UnlistenFn | null = null
export const ACtionDataCallback = async (name: string, item: string, item2: string, callback: (arg0: any) => void) => {
    if (unlisten) unlisten();
    unlisten = await listen("action_data", e => {
        callback(e.payload);
    })
    let params: ActionParams = {
        name: name,
        item1: item,
        item2: item2
    }
    await emit("action_req", params)
}

调用实例

    async function(){
       let res = await ActionDataAsynccallback('powerOn','','')
    }

调用时如下

2024-07-30 16:48:10.260 [INFO] params:ActionParams { name: "powerOn", item1: "", item2: "172.28.248.163" }

2024-07-30 16:48:13.280 [INFO] params:ActionParams { name: "set_config", item1: "{\"CH1\":{\"case\":false,\"volt\":1.1,\"curr\":2.1,\"switch\":true},\"CH2\":{\"case\":true,\"volt\":2,\"curr\":1.51,\"switch\":true},\"CH3\":{\"case\":false,\"volt\":2,\"curr\":1.5,\"switch\":true}}", item2: "" }

Git地址

github.com/zengyuhan50…

参考文档

为了更好地理解和实现基于 Tauri 和 Visa 的电量计仪器测试应用工具,您可以参考以下文档和资源:

1. Tauri 官方文档 Tauri 是一个用 Rust 编写的框架,它允许您使用 Web 技术来构建跨平台的桌面应用程序。Tauri 提供了详细的安装、配置和使用方法,适用于各种操作系统。 - Tauri Documentation
2. visa-rs 库 visa-rs 是一个用于与 Visa API 交互的 Rust 库。它提供了简单易用的接口,使您能够轻松集成 Visa 的支付解决方案。 - visa-rs GitHub 仓库
3. Rust 官方文档 Rust 是一种注重性能和安全性的系统编程语言。Rust 官方文档提供了全面的语言教程、标准库参考以及编程指南。 - Rust Documentation
4. Tokio 异步运行时 Tokio 是一个用于构建异步应用程序的 Rust 库。它提供了异步任务调度、网络 IO 和计时器等功能,使您能够编写高性能的异步代码。 - Tokio Documentation