加载中...

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

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

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

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

New Image

  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是常用的用于定义全局变量的库

cargo.toml
代码解读
复制代码
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 的配置
tauri.config.json
代码解读
复制代码
{ "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)相关的方法。这些方法允许你打开、关闭和管理与仪器的会话
Rust
代码解读
复制代码
// 为了防止资源管理器释放后丢失连接,使用全局变量,使用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 或者 失败信息
Rust
代码解读
复制代码
#[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. 为了初始化后获取当前的资源池,用来展示当前仪器的列表吗,用于选择,效果如图

New Image

New Image

Rust
代码解读
复制代码
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的命令格式。
Rust
代码解读
复制代码
#[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 定义的通道消息,用来创建发送者与接受者,为了停止数据的获取。目前没有做重连机制,出现错误就直接抛出。
Rust
代码解读
复制代码
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

Rust
代码解读
复制代码
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 监听的方式,获取前端发送过来的信息,根据参数的不同而进行不同的逻辑。

Rust
代码解读
复制代码
#[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,定义同步于异步的内容。

js
代码解读
复制代码
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) }

调用实例

Rust
代码解读
复制代码
async function(){ let res = await ActionDataAsynccallback(''powerOn'','''','''') }

调用时如下

swift
代码解读
复制代码
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