前文:电量计仪器测试应用是一种用于测试和校准电量计设备的软件工具。这类应用通常用于制造、研发和质量保证等领域,确保电量计设备的准确性和性能
- 功能列表
设备连接:通过IP连接电量计设备。实时数据监控:实时显示电量计测量的数据,如电压、电流、功率、能量等参数。数据记录与存储:记录测量数据,并且支持回显数据与记录删除自动化测试:支持设置自动化测试流程,根据预设的测试条件自动进行测试用户界面:友好的用户界面,易于操作和理解。定时测试:根据预设等待时间,测试时长进行自动化测试兼容性:支持Ni、Keysight等工具
- 示例图
- 技术方案
技术层:Tauri + RustUI层:Vue3 + ant-design-vue + echarts
内容相关
-
基础配置
主要是: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"
- 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的使用库,集成我们需要的方法
- 初始化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()
));
}
- 创建
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
}
}
- 为了初始化后获取当前的资源池,用来展示当前仪器的列表吗,用于选择,效果如图
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)
}
}
- 创建设置参数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(())
}
}
- 定义轮询获取数据与取消数据获取,这里使用了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 的电量计仪器测试应用工具,您可以参考以下文档和资源: