Compare commits
No commits in common. "code-example" and "main" have entirely different histories.
code-examp
...
main
54
FSW.py
Normal file
54
FSW.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# python script created by FSW: 24:11:2020 04:53:44
|
||||||
|
import visa
|
||||||
|
def write_command(instrument, command) :
|
||||||
|
instrument.write(command)
|
||||||
|
return process_system_error(instrument)
|
||||||
|
def write_query(instrument, command) :
|
||||||
|
buffer = instrument.query(command)
|
||||||
|
bSuccess = process_system_error(instrument)
|
||||||
|
return bSuccess, buffer
|
||||||
|
def process_system_error(instrument) :
|
||||||
|
bSuccess = True
|
||||||
|
EsrErrorMask = 0x3C
|
||||||
|
if ((get_esr(instrument) & EsrErrorMask) != 0) :
|
||||||
|
print(instrument.query(":SYST:ERR?"))
|
||||||
|
instrument.write("*CLS")
|
||||||
|
bSuccess = False
|
||||||
|
return bSuccess
|
||||||
|
def get_esr(instrument) :
|
||||||
|
esr = instrument.query("*ESR?")
|
||||||
|
return int(esr)
|
||||||
|
VisaResourceManager = visa.ResourceManager()
|
||||||
|
#set center freQ
|
||||||
|
fre = '200e6'
|
||||||
|
descr = '_harm_dis_meas'
|
||||||
|
#location on FSW
|
||||||
|
tar_loc = 'd:\\murmur\\'
|
||||||
|
# connect to analyzer
|
||||||
|
Analyzer = VisaResourceManager.open_resource("TCPIP::192.168.1.43::inst0::INSTR")
|
||||||
|
print(Analyzer.query("*IDN?"))
|
||||||
|
success = write_command( Analyzer, "*CLS" )
|
||||||
|
success = write_command( Analyzer, ":SYST:DISP:UPD ON" )
|
||||||
|
success = write_command( Analyzer, ":INIT:CONT OFF" )
|
||||||
|
#success = write_command( Analyzer, ":DISP:WIND:SUBW:TRAC:Y:SCAL:RLEV 10" )
|
||||||
|
success = write_command( Analyzer, ":SENS:FREQ:CENT " + fre )
|
||||||
|
success = write_command( Analyzer, ":CALC:MARK:FUNC:HARM:STAT ON" )
|
||||||
|
success = write_command( Analyzer, ":INIT:CONT ON" )
|
||||||
|
success = write_command( Analyzer, ":FORM:DEXP:DSEP POIN" )
|
||||||
|
success = write_command( Analyzer, ":FORM:DEXP:FORM CSV" )
|
||||||
|
success = write_command( Analyzer, ":FORM:DEXP:HEAD OFF" )
|
||||||
|
success = write_command( Analyzer, ":FORM:DEXP:TRAC ALL" )
|
||||||
|
#success = write_command( Analyzer, ":MMEM:STOR1:TRAC 1,'d:\\" + fre + descr +".CSV'" )
|
||||||
|
#
|
||||||
|
success = write_command( Analyzer, ":MMEM:STOR1:TRAC 1,'" + tar_loc + fre + descr +".CSV'" )
|
||||||
|
success, data = write_query(Analyzer, f"MMEM:DATA? '{tar_loc}screenshot.png'")
|
||||||
|
with open("local_screenshot.png", "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
success = write_command( Analyzer, ":SENS:FREQ:MODE SWE" )
|
||||||
|
# back to local mode
|
||||||
|
success = write_command(Analyzer, "@LOC")
|
||||||
|
if success:
|
||||||
|
print("Done.\r\nFile's location is {}{}{}.CSV.\r\n".format(tar_loc,fre,descr))
|
||||||
|
# cleanup
|
||||||
|
Analyzer.close()
|
||||||
|
VisaResourceManager.close()
|
74
README.md
Normal file
74
README.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# 功能
|
||||||
|
采集板具备深度计、加速度计和陀螺仪,可采集相关数据并以`*.BIN`格式存储,可通过工具格式化输出为`*.CSV`,支持Excel或文本编辑器查看、编辑。
|
||||||
|
|
||||||
|
# 采集状态检查
|
||||||
|
|
||||||
|
调试口有调试串口(TTL/115200-8-1-N),具备查看采集状态的功能。
|
||||||
|
|
||||||
|
采集状态有误时会不断重启,正常状态则会不断刷新采集的数据。
|
||||||
|
|
||||||
|
如遇采集状态有误,则需要给水密桶`断电、稍等、重新上电`尝试恢复。
|
||||||
|
|
||||||
|
# 采集数据格式
|
||||||
|
原始采集的数据文件后缀为`.BIN`,以结构体`info_t`小端序存储。
|
||||||
|
```c
|
||||||
|
// MPU传感器数据结构
|
||||||
|
typedef struct {
|
||||||
|
int16_t ax; // 加速度计 X轴
|
||||||
|
int16_t ay; // 加速度计 Y轴
|
||||||
|
int16_t az; // 加速度计 Z轴
|
||||||
|
int16_t gx; // 陀螺仪 X轴
|
||||||
|
int16_t gy; // 陀螺仪 Y轴
|
||||||
|
int16_t gz; // 陀螺仪 Z轴
|
||||||
|
} sensor_data_t;
|
||||||
|
|
||||||
|
// 完整的数据记录结构
|
||||||
|
#pragma pack(1)
|
||||||
|
typedef struct {
|
||||||
|
uint8_t y; // 年(相对值,需要加上2000)
|
||||||
|
uint8_t month; // 月
|
||||||
|
uint8_t d; // 日
|
||||||
|
uint8_t h; // 时
|
||||||
|
uint8_t m; // 分
|
||||||
|
uint8_t s; // 秒
|
||||||
|
uint16_t ms; // 毫秒
|
||||||
|
int16_t deepth; // 深度
|
||||||
|
sensor_data_t data; // MPU传感器数据
|
||||||
|
} info_t;
|
||||||
|
```
|
||||||
|
# 导出采集数据
|
||||||
|
|
||||||
|
调试口连有一A口USB接头,插入电脑后**稍作等待**,系统会多出一个盘符,双击打开可浏览采集的数据文件,其以日期为文件名,形如`20241112.BIN`。
|
||||||
|
|
||||||
|
选中采集文件后**用鼠标拖入**本地磁盘某位置即可。
|
||||||
|
|
||||||
|
# 解析采集数据
|
||||||
|
|
||||||
|
`WIN+R`输入cmd后按回车键运行命令行,在命令行中输入`decode.exe`回车,会提示具体的使用方法,如:
|
||||||
|
|
||||||
|
```
|
||||||
|
用法:
|
||||||
|
decode.exe -f <文件名> [-o <输出文件名>]
|
||||||
|
选项:
|
||||||
|
-f <文件名> 指定输入文件
|
||||||
|
-o <文件名> 指定输出文件(可选,默认输出到与输入同名的.csv文件)
|
||||||
|
-h 显示帮助信息
|
||||||
|
|
||||||
|
示例:
|
||||||
|
decode.exe -f 20241112.BIN
|
||||||
|
decode.exe -f 20241112.BIN -o output.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
运行结果如:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
D:\Users\murmur\Desktop\test>decode.exe -f 20241112.BIN
|
||||||
|
系统类型: Windows
|
||||||
|
系统字节序: 小端序
|
||||||
|
共解析 46980 条记录
|
||||||
|
```
|
||||||
|
|
||||||
|
# 异常处理
|
||||||
|
|
||||||
|
可通过转换后文件内的日期数据判断解析是否正确,如不正确请重新导出。
|
||||||
|
|
315
inputbox_with_tips.html
Normal file
315
inputbox_with_tips.html
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>输入框自动全选和放大提示</title>
|
||||||
|
<style>
|
||||||
|
/* 基本样式 */
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 200px;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: 'Consolas';
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮样式 */
|
||||||
|
button {
|
||||||
|
padding: 10px 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 提示数字样式 */
|
||||||
|
.number-tip {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: 'Consolas';
|
||||||
|
color: #333;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-tip.hide {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 颜色样式 */
|
||||||
|
.color-0 {
|
||||||
|
color: #153fea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-1 {
|
||||||
|
color: #15f00e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-2 {
|
||||||
|
color: #6963688a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 错误状态样式 */
|
||||||
|
.number-tip.error {
|
||||||
|
background: rgba(255, 200, 200, 0.9);
|
||||||
|
border-color: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
/* display: block; */
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="number1">数字 1:</label>
|
||||||
|
<input type="text" id="number1" placeholder="请输入数字 1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="number2">数字 2:</label>
|
||||||
|
<input type="text" id="number2" placeholder="请输入数字 2">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="number3">数字 3:</label>
|
||||||
|
<input type="text" id="number3" placeholder="请输入数字 3">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="focusNumber1">激活数字 1 输入框</button>
|
||||||
|
<button id="focusNumber2">激活数字 2 输入框</button>
|
||||||
|
<button id="focusNumber3">激活数字 3 输入框</button>
|
||||||
|
|
||||||
|
<div id="numberTip" class="number-tip hide"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 获取所有的输入框和提示元素
|
||||||
|
const inputBoxes = document.querySelectorAll('input[type="text"]');
|
||||||
|
const numberTip = document.getElementById('numberTip');
|
||||||
|
|
||||||
|
// 按钮点击事件
|
||||||
|
document.getElementById('focusNumber1').addEventListener('click', () => {
|
||||||
|
document.getElementById('number1').focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('focusNumber2').addEventListener('click', () => {
|
||||||
|
document.getElementById('number2').focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('focusNumber3').addEventListener('click', () => {
|
||||||
|
document.getElementById('number3').focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加输入范围配置
|
||||||
|
const inputRanges = {
|
||||||
|
0: { min: 0, max: 100 }, // 第一个输入框:0-100
|
||||||
|
1: { min: 100, max: 1000 }, // 第二个输入框:100-1000
|
||||||
|
2: { min: 1000, max: 10000 } // 第三个输入框:1000-10000
|
||||||
|
};
|
||||||
|
|
||||||
|
// 为所有输入框添加事件监听
|
||||||
|
inputBoxes.forEach((inputBox, index) => {
|
||||||
|
// 输入框获取焦点时
|
||||||
|
inputBox.addEventListener('focus', (event) => {
|
||||||
|
event.target.select();
|
||||||
|
const value = event.target.value;
|
||||||
|
if (value !== '') {
|
||||||
|
const rawValue = value.replace(/\s/g, '');
|
||||||
|
showNumberTip(formatNumberWithColor(rawValue), inputBox, index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
inputBox.addEventListener('input', (event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
if (value !== '') {
|
||||||
|
const rawValue = value.replace(/\s/g, '');
|
||||||
|
const numValue = parseFloat(rawValue);
|
||||||
|
const range = inputRanges[index];
|
||||||
|
|
||||||
|
// 只检查最大值,允许临时小于最小值
|
||||||
|
if (!isNaN(numValue) && numValue <= range.max) {
|
||||||
|
// 保存有效值
|
||||||
|
event.target.value = formatNumber(rawValue);
|
||||||
|
event.target.dataset.lastValidValue = event.target.value;
|
||||||
|
// 显示放大提示
|
||||||
|
showNumberTip(formatNumberWithColor(rawValue), inputBox, index, false);
|
||||||
|
} else if (!isNaN(numValue) && numValue > range.max) {
|
||||||
|
// 如果超出最大值,显示错误提示
|
||||||
|
showNumberTip(
|
||||||
|
`超出最大值 ${range.max}`,
|
||||||
|
inputBox,
|
||||||
|
index,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
// 还原到上一个有效值或清空
|
||||||
|
if (event.target.dataset.lastValidValue) {
|
||||||
|
event.target.value = event.target.dataset.lastValidValue;
|
||||||
|
} else {
|
||||||
|
event.target.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 输入框失去焦点时
|
||||||
|
inputBox.addEventListener('blur', (event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
let finalValue = value;
|
||||||
|
|
||||||
|
// 处理小数点
|
||||||
|
if (value.includes('.')) {
|
||||||
|
const [integerPart, decimalPart] = value.split('.');
|
||||||
|
if (!decimalPart || decimalPart.replace(/[^\d]/g, '').length === 0) {
|
||||||
|
finalValue = integerPart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查最小值
|
||||||
|
if (finalValue !== '') {
|
||||||
|
const numValue = parseFloat(finalValue.replace(/\s/g, ''));
|
||||||
|
const range = inputRanges[index];
|
||||||
|
|
||||||
|
if (!isNaN(numValue)) {
|
||||||
|
if (numValue < range.min) {
|
||||||
|
// 如果小于最小值,显示错误提示
|
||||||
|
showNumberTip(
|
||||||
|
`不能小于最小值 ${range.min}`,
|
||||||
|
inputBox,
|
||||||
|
index,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
// 设置为最小值
|
||||||
|
event.target.value = range.min;
|
||||||
|
} else if (numValue <= range.max) {
|
||||||
|
// 值在有效范围内
|
||||||
|
event.target.value = finalValue;
|
||||||
|
// 隐藏提示
|
||||||
|
numberTip.classList.add('hide');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 修改数字格式化函数,移除输入框的颜色格式化
|
||||||
|
function formatNumber(value) {
|
||||||
|
// 分割整数部分和小数部分
|
||||||
|
const [integerPart, decimalPart] = value.split('.');
|
||||||
|
|
||||||
|
// 格式化整数部分,只添加空格,不添加颜色
|
||||||
|
const formattedInteger = integerPart
|
||||||
|
.replace(/[^\d]/g, '')
|
||||||
|
.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
||||||
|
|
||||||
|
// 格式化小数部分,只添加空格,不添加颜色
|
||||||
|
if (decimalPart !== undefined) {
|
||||||
|
const formattedDecimal = decimalPart
|
||||||
|
.replace(/[^\d]/g, '')
|
||||||
|
.replace(/(\d{3})/g, '$1 ')
|
||||||
|
.trim();
|
||||||
|
return `${formattedInteger}.${formattedDecimal}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedInteger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改彩色格式化函数
|
||||||
|
function formatNumberWithColor(value) {
|
||||||
|
if (!value.includes('.')) {
|
||||||
|
// 如果没有小数点,从右向左添加颜色
|
||||||
|
return value
|
||||||
|
.replace(/[^\d]/g, '')
|
||||||
|
.replace(/\B(?=(\d{3})+(?!\d))/g, ' ')
|
||||||
|
.split(' ')
|
||||||
|
.map((group, index, array) => `<span class="color-${(array.length - 1 - index) % 3}">${group}</span>`)
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有小数点的情况
|
||||||
|
const [integerPart, decimalPart] = value.split('.');
|
||||||
|
|
||||||
|
// 整数部分:从右向左添加颜色
|
||||||
|
const coloredInteger = integerPart
|
||||||
|
.replace(/[^\d]/g, '')
|
||||||
|
.replace(/\B(?=(\d{3})+(?!\d))/g, ' ')
|
||||||
|
.split(' ')
|
||||||
|
.map((group, index, array) => `<span class="color-${(array.length - 1 - index) % 3}">${group}</span>`)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
// 小数部分:从左向右添加颜色
|
||||||
|
const coloredDecimal = decimalPart
|
||||||
|
.replace(/[^\d]/g, '')
|
||||||
|
.replace(/(\d{3})/g, '$1 ')
|
||||||
|
.trim()
|
||||||
|
.split(' ')
|
||||||
|
.map((group, index) => `<span class="color-${index % 3}">${group}</span>`)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
return `${coloredInteger}.${coloredDecimal}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加一个变量来存储定时器
|
||||||
|
let hideTimer;
|
||||||
|
|
||||||
|
// 更新显示提示的函数
|
||||||
|
function showNumberTip(value, inputBox, index, isError = false) {
|
||||||
|
if (hideTimer) {
|
||||||
|
clearTimeout(hideTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
numberTip.innerHTML = value;
|
||||||
|
numberTip.classList.toggle('error', isError);
|
||||||
|
|
||||||
|
const rect = inputBox.getBoundingClientRect();
|
||||||
|
const tipRect = numberTip.getBoundingClientRect();
|
||||||
|
|
||||||
|
let left = rect.left;
|
||||||
|
let top = rect.bottom;
|
||||||
|
if (top + tipRect.height > window.innerHeight) {
|
||||||
|
top = rect.top - tipRect.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
numberTip.style.left = `${left}px`;
|
||||||
|
numberTip.style.top = `${top}px`;
|
||||||
|
numberTip.classList.remove('hide');
|
||||||
|
|
||||||
|
hideTimer = setTimeout(() => {
|
||||||
|
numberTip.classList.add('hide');
|
||||||
|
}, 6000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
176
instrument_utils.py
Normal file
176
instrument_utils.py
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import visa
|
||||||
|
from typing import Optional, Tuple, Union
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
class InstrumentInterface:
|
||||||
|
def __init__(self, resource_string: str):
|
||||||
|
"""
|
||||||
|
初始化仪器接口
|
||||||
|
Args:
|
||||||
|
resource_string: VISA资源字符串,例如 "TCPIP::192.168.1.43::inst0::INSTR"
|
||||||
|
"""
|
||||||
|
self.rm = visa.ResourceManager()
|
||||||
|
self.instrument = self.rm.open_resource(resource_string)
|
||||||
|
self.instrument.timeout = 30000 # 30秒超时
|
||||||
|
|
||||||
|
# 查询仪器信息以确定系统类型
|
||||||
|
success, idn = self.query("*IDN?")
|
||||||
|
if success:
|
||||||
|
self.instrument_info = idn
|
||||||
|
# 根据IDN确定仪器类型和路径
|
||||||
|
if "R&S" in idn:
|
||||||
|
self.temp_path = "/tmp/" # R&S通常基于Linux
|
||||||
|
elif "Keysight" in idn or "Agilent" in idn:
|
||||||
|
self.temp_path = "C:/temp/" # Keysight通常基于Windows
|
||||||
|
else:
|
||||||
|
# 默认使用/tmp/,因为大多数仪器基于Linux
|
||||||
|
self.temp_path = "/tmp/"
|
||||||
|
else:
|
||||||
|
raise ConnectionError("Failed to identify instrument")
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def write(self, command: str) -> bool:
|
||||||
|
"""发送SCPI命令"""
|
||||||
|
try:
|
||||||
|
self.instrument.write(command)
|
||||||
|
return self._check_error()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error writing command {command}: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def query(self, command: str) -> Tuple[bool, str]:
|
||||||
|
"""查询SCPI命令"""
|
||||||
|
try:
|
||||||
|
response = self.instrument.query(command)
|
||||||
|
success = self._check_error()
|
||||||
|
return success, response.strip()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error querying {command}: {str(e)}")
|
||||||
|
return False, ""
|
||||||
|
|
||||||
|
def _check_error(self) -> bool:
|
||||||
|
"""检查仪器错误"""
|
||||||
|
try:
|
||||||
|
errors = self.instrument.query("SYST:ERR?")
|
||||||
|
if "No error" in errors:
|
||||||
|
return True
|
||||||
|
print(f"Instrument error: {errors}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error checking system errors: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _transfer_file_to_local(self,
|
||||||
|
instrument_path: str,
|
||||||
|
local_path: str,
|
||||||
|
delete_source: bool = True) -> bool:
|
||||||
|
"""
|
||||||
|
从仪器传输文件到本地
|
||||||
|
Args:
|
||||||
|
instrument_path: 仪器上的文件路径
|
||||||
|
local_path: 本地保存路径
|
||||||
|
delete_source: 传输后是否删除仪器上的源文件
|
||||||
|
Returns:
|
||||||
|
bool: 操作是否成功
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 确保本地目录存在
|
||||||
|
os.makedirs(os.path.dirname(os.path.abspath(local_path)), exist_ok=True)
|
||||||
|
|
||||||
|
# 读取仪器文件数据
|
||||||
|
success, data = self.query(f"MMEM:DATA? '{instrument_path}'")
|
||||||
|
if not success:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 保存到本地
|
||||||
|
with open(local_path, 'wb') as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
# 清理仪器上的源文件(如果需要)
|
||||||
|
if delete_source:
|
||||||
|
self.write(f"MMEM:DEL '{instrument_path}'")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error transferring file: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def screenshot_to_file(self,
|
||||||
|
local_path: str,
|
||||||
|
format: str = "PNG") -> bool:
|
||||||
|
"""
|
||||||
|
保存仪器截图到本地文件
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 生成临时文件名
|
||||||
|
temp_filename = f"screenshot_{datetime.now().strftime('%Y%m%d_%H%M%S')}.{format.lower()}"
|
||||||
|
temp_filepath = os.path.join(self.temp_path, temp_filename).replace('\\', '/')
|
||||||
|
|
||||||
|
# 配置并执行截图
|
||||||
|
self.write(f":HCOP:DEV:LANG {format}")
|
||||||
|
self.write(f":MMEM:NAME '{temp_filepath}'")
|
||||||
|
self.write(":HCOP:IMM") # 执行截图
|
||||||
|
|
||||||
|
# 传输文件到本地
|
||||||
|
return self._transfer_file_to_local(temp_filepath, local_path)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving screenshot: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def data_to_file(self,
|
||||||
|
local_path: str,
|
||||||
|
trace_number: int = 1,
|
||||||
|
format: str = "CSV") -> bool:
|
||||||
|
"""
|
||||||
|
保存轨迹数据到本地文件
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 生成临时文件名
|
||||||
|
temp_filename = f"trace_{datetime.now().strftime('%Y%m%d_%H%M%S')}.{format.lower()}"
|
||||||
|
temp_filepath = os.path.join(self.temp_path, temp_filename).replace('\\', '/')
|
||||||
|
|
||||||
|
# 配置数据导出格式
|
||||||
|
self.write(":FORM:DEXP:DSEP POIN")
|
||||||
|
self.write(f":FORM:DEXP:FORM {format}")
|
||||||
|
self.write(":FORM:DEXP:HEAD OFF")
|
||||||
|
self.write(":FORM:DEXP:TRAC ALL")
|
||||||
|
|
||||||
|
# 保存到仪器临时文件
|
||||||
|
self.write(f":MMEM:STOR{trace_number}:TRAC {trace_number},'{temp_filepath}'")
|
||||||
|
|
||||||
|
# 传输文件到本地
|
||||||
|
return self._transfer_file_to_local(temp_filepath, local_path)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving trace data: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def file_to_local(self,
|
||||||
|
instrument_path: str,
|
||||||
|
local_path: str,
|
||||||
|
delete_source: bool = False) -> bool:
|
||||||
|
"""
|
||||||
|
从仪器复制任意文件到本地
|
||||||
|
Args:
|
||||||
|
instrument_path: 仪器上的文件路径
|
||||||
|
local_path: 本地保存路径
|
||||||
|
delete_source: 是否删除仪器上的源文件
|
||||||
|
"""
|
||||||
|
return self._transfer_file_to_local(instrument_path, local_path, delete_source)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""关闭连接"""
|
||||||
|
try:
|
||||||
|
self.instrument.close()
|
||||||
|
self.rm.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error closing connection: {str(e)}")
|
153
visa-scpi-example/File.cpp
Normal file
153
visa-scpi-example/File.cpp
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
// C++ script created by FSW: 04:12:2024 14:23:36
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <iostream>
|
||||||
|
#include <visa.h>
|
||||||
|
|
||||||
|
// forward declarations
|
||||||
|
static bool write_command(const ViSession& handle, const std::string& command);
|
||||||
|
static bool write_query(const ViSession& handle, const std::string& query, ViPBuf result_buffer, const ViUInt32 buffer_size, ViPUInt32 read_count = nullptr);
|
||||||
|
static bool process_system_error(const ViSession& handle);
|
||||||
|
static unsigned char get_esr(const ViSession& handle);
|
||||||
|
|
||||||
|
|
||||||
|
static ViSession g_analyzer;
|
||||||
|
|
||||||
|
// assign your own variables
|
||||||
|
static ViByte g_result[1000000];
|
||||||
|
|
||||||
|
|
||||||
|
static bool write_command( const ViSession& handle, const std::string& command )
|
||||||
|
{
|
||||||
|
ViStatus status = viWrite(handle, (ViBuf)command.c_str(), command.length(), VI_NULL);
|
||||||
|
|
||||||
|
// return true if successful
|
||||||
|
return ((status >= VI_SUCCESS) && process_system_error(handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool write_query( const ViSession& handle, const std::string& query, ViPBuf result_buffer, const ViUInt32 buffer_size, ViPUInt32 read_count )
|
||||||
|
{
|
||||||
|
ViStatus status = viWrite(handle, (ViBuf)query.c_str(), query.length(), VI_NULL);
|
||||||
|
if (status >= VI_SUCCESS)
|
||||||
|
{
|
||||||
|
status = viRead(handle, result_buffer, buffer_size, read_count);
|
||||||
|
}
|
||||||
|
// return true if successful
|
||||||
|
return ((status >= VI_SUCCESS) && process_system_error(handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool process_system_error(const ViSession& handle)
|
||||||
|
{
|
||||||
|
bool success = true;
|
||||||
|
const unsigned char EsrErrorMask = 0x3C;
|
||||||
|
|
||||||
|
if ((get_esr(handle) & EsrErrorMask) != 0)
|
||||||
|
{
|
||||||
|
char err_buf[256];
|
||||||
|
ViUInt32 readCount;
|
||||||
|
viWrite(handle, (ViBuf)":SYST:ERR?", 10, VI_NULL);
|
||||||
|
viRead(handle, (ViPBuf)err_buf, sizeof(err_buf), &readCount);
|
||||||
|
err_buf[readCount] = '\0';
|
||||||
|
std::cout << err_buf << std::endl;
|
||||||
|
viWrite(handle, (ViBuf)"*CLS", 4, VI_NULL);
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
static unsigned char get_esr(const ViSession& handle)
|
||||||
|
{
|
||||||
|
char esr_buf[16];
|
||||||
|
ViUInt32 readCount;
|
||||||
|
viWrite(handle, (ViBuf)"*ESR?", 5, VI_NULL);
|
||||||
|
viRead(handle, (ViPBuf)esr_buf, sizeof(esr_buf), &readCount);
|
||||||
|
esr_buf[readCount] = '\0';
|
||||||
|
return (static_cast<unsigned char>(std::stoi(std::string(esr_buf))));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int main ()
|
||||||
|
{
|
||||||
|
ViSession defaultRM;
|
||||||
|
bool success = true;
|
||||||
|
|
||||||
|
// create a VISA session and return a handle to it
|
||||||
|
viOpenDefaultRM(&defaultRM);
|
||||||
|
// create a VISA session to the connected device and return a handle to it
|
||||||
|
viOpen(defaultRM, (ViRsrc)"TCPIP::169.254.177.77::inst0::INSTR", VI_NULL, VI_NULL, &g_analyzer);
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, "*RST" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, "*CLS" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":SYST:DISP:UPD ON" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":INIT:CONT OFF" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":SENS:FREQ:STAR 1000000000" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":SENS:FREQ:STOP 2000000000" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":DISP:WIND:TRAC:Y:SCAL:RLEV -30" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":DISP:WIND:TRAC:Y:SCAL:RLEV 30" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":SENS:BAND:RES:AUTO OFF" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":SENS:BAND:RES:AUTO ON" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":SENS:BAND:VID:AUTO OFF" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":SENS:BAND:VID:AUTO ON" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":SENS:FREQ:MODE SWE" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":DISP:WIND1:SIZE LARG" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":CALC1:MARK1:FUNC:STR:STAT ON" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":CALC1:MARK1:FUNC:STR:STAT OFF" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":CALC:MARK:FUNC:POW:SEL CN" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":SENS:POW:ACH:BAND:CHAN 300000" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":SENS:POW:ACH:PRES CN" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":CALC:MARK1:STAT ON" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":CALC:MARK1:X 1200000000" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":CALC:MARK:FUNC:POW:SEL CN" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":SENS:POW:ACH:PRES CN" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":SENS:POW:ACH:PRES CN" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":CALC:MARK:FUNC:POW:SEL CN0" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":CALC:MARK:FUNC:POW:SEL CN" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":SENS:FREQ:SPAN 1000000" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":SENS:FREQ:SPAN 1000000000" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":CALC:MARK1:STAT ON" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":CALC:MARK1:X 1200000000" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":CALC:DELT2:STAT ON" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":CALC:DELT2:X 1600000000" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":INIT:SPUR" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":MMEM:NAME 'C:\R_S\Instr\user\FSW_ScreenShot_2024-12-04_14-22-12.PNG'" );
|
||||||
|
|
||||||
|
success = write_command( g_analyzer, ":HCOP:IMM1" );
|
||||||
|
|
||||||
|
viClose(g_analyzer);
|
||||||
|
viClose(defaultRM);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
35
visa-scpi-example/File.inp
Normal file
35
visa-scpi-example/File.inp
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
*RST
|
||||||
|
*CLS
|
||||||
|
:SYST:DISP:UPD ON
|
||||||
|
:INIT:CONT OFF
|
||||||
|
:SENS:FREQ:STAR 1000000000
|
||||||
|
:SENS:FREQ:STOP 2000000000
|
||||||
|
:DISP:WIND:TRAC:Y:SCAL:RLEV 0
|
||||||
|
:DISP:WIND:TRAC:Y:SCAL:RLEV 0
|
||||||
|
:SENS:BAND:RES:AUTO OFF
|
||||||
|
:SENS:BAND:RES:AUTO ON
|
||||||
|
:SENS:BAND:VID:AUTO OFF
|
||||||
|
:SENS:BAND:VID:AUTO ON
|
||||||
|
:SENS:FREQ:MODE SWE
|
||||||
|
:DISP:WIND1:SIZE LARG
|
||||||
|
:CALC1:MARK1:FUNC:STR:STAT ON
|
||||||
|
:CALC1:MARK1:FUNC:STR:STAT OFF
|
||||||
|
:CALC:MARK:FUNC:POW:SEL CN
|
||||||
|
:SENS:POW:ACH:BAND:CHAN 300000
|
||||||
|
:SENS:POW:ACH:PRES CN
|
||||||
|
:CALC:MARK1:STAT ON
|
||||||
|
:CALC:MARK1:X 1200000000
|
||||||
|
:CALC:MARK:FUNC:POW:SEL CN
|
||||||
|
:SENS:POW:ACH:PRES CN
|
||||||
|
:SENS:POW:ACH:PRES CN
|
||||||
|
:CALC:MARK:FUNC:POW:SEL CN0
|
||||||
|
:CALC:MARK:FUNC:POW:SEL CN
|
||||||
|
:SENS:FREQ:SPAN 1000000
|
||||||
|
:SENS:FREQ:SPAN 1000000000
|
||||||
|
:CALC:MARK1:STAT ON
|
||||||
|
:CALC:MARK1:X 1200000000
|
||||||
|
:CALC:DELT2:STAT ON
|
||||||
|
:CALC:DELT2:X 1600000000
|
||||||
|
:INIT:SPUR
|
||||||
|
:MMEM:NAME 'C:\R_S\Instr\user\FSW_ScreenShot_2024-12-04_14-22-12.PNG'
|
||||||
|
:HCOP:IMM1
|
109
visa-scpi-example/File.py
Normal file
109
visa-scpi-example/File.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
# python script created by FSW: 04:12:2024 14:23:54
|
||||||
|
|
||||||
|
import pyvisa as visa
|
||||||
|
|
||||||
|
def write_command(instrument, command) :
|
||||||
|
instrument.write(command)
|
||||||
|
return process_system_error(instrument)
|
||||||
|
|
||||||
|
def write_query(instrument, command) :
|
||||||
|
buffer = instrument.query(command)
|
||||||
|
bSuccess = process_system_error(instrument)
|
||||||
|
return bSuccess, buffer
|
||||||
|
|
||||||
|
def process_system_error(instrument) :
|
||||||
|
bSuccess = True
|
||||||
|
EsrErrorMask = 0x3C
|
||||||
|
if ((get_esr(instrument) & EsrErrorMask) != 0) :
|
||||||
|
print(instrument.query(":SYST:ERR?"))
|
||||||
|
instrument.write("*CLS")
|
||||||
|
bSuccess = False
|
||||||
|
return bSuccess
|
||||||
|
|
||||||
|
def get_esr(instrument) :
|
||||||
|
esr = instrument.query("*ESR?")
|
||||||
|
return int(esr)
|
||||||
|
|
||||||
|
|
||||||
|
VisaResourceManager = visa.ResourceManager()
|
||||||
|
|
||||||
|
# connect to analyzer
|
||||||
|
Analyzer = VisaResourceManager.open_resource("TCPIP::169.254.177.77::inst0::INSTR")
|
||||||
|
|
||||||
|
success = write_command( Analyzer, "*RST" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, "*CLS" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":SYST:DISP:UPD ON" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":INIT:CONT OFF" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":SENS:FREQ:STAR 1000000000" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":SENS:FREQ:STOP 2000000000" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":DISP:WIND:TRAC:Y:SCAL:RLEV -30" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":DISP:WIND:TRAC:Y:SCAL:RLEV 30" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":SENS:BAND:RES:AUTO OFF" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":SENS:BAND:RES:AUTO ON" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":SENS:BAND:VID:AUTO OFF" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":SENS:BAND:VID:AUTO ON" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":SENS:FREQ:MODE SWE" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":DISP:WIND1:SIZE LARG" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":CALC1:MARK1:FUNC:STR:STAT ON" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":CALC1:MARK1:FUNC:STR:STAT OFF" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":CALC:MARK:FUNC:POW:SEL CN" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":SENS:POW:ACH:BAND:CHAN 300000" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":SENS:POW:ACH:PRES CN" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":CALC:MARK1:STAT ON" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":CALC:MARK1:X 1200000000" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":CALC:MARK:FUNC:POW:SEL CN" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":SENS:POW:ACH:PRES CN" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":SENS:POW:ACH:PRES CN" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":CALC:MARK:FUNC:POW:SEL CN0" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":CALC:MARK:FUNC:POW:SEL CN" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":SENS:FREQ:SPAN 1000000" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":SENS:FREQ:SPAN 1000000000" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":CALC:MARK1:STAT ON" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":CALC:MARK1:X 1200000000" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":CALC:DELT2:STAT ON" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":CALC:DELT2:X 1600000000" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":INIT:SPUR" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":MMEM:NAME 'C:\R_S\Instr\user\FSW_ScreenShot_2024-12-04_14-22-12.PNG'" )
|
||||||
|
|
||||||
|
success = write_command( Analyzer, ":HCOP:IMM1" )
|
||||||
|
|
||||||
|
|
||||||
|
# back to local mode
|
||||||
|
success = write_command(Analyzer, "@LOC")
|
||||||
|
|
||||||
|
# cleanup
|
||||||
|
Analyzer.close()
|
||||||
|
VisaResourceManager.close()
|
9
visa-scpi-example/commands.json
Normal file
9
visa-scpi-example/commands.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
":SENS:FREQ:STAR": "1000000000",
|
||||||
|
":SENS:FREQ:STOP": "2000000000",
|
||||||
|
":DISP:WIND:TRAC:Y:SCAL:RLEV": "0",
|
||||||
|
":SENS:POW:ACH:BAND:CHAN": "300000",
|
||||||
|
":CALC:MARK1:X": "1200000000",
|
||||||
|
":SENS:FREQ:SPAN": "1000000000",
|
||||||
|
":CALC:DELT2:X": "1600000000"
|
||||||
|
}
|
9
visa-scpi-example/comments.json
Normal file
9
visa-scpi-example/comments.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
":SENS:FREQ:STAR": "\u8d77\u59cb\u9891\u7387",
|
||||||
|
":SENS:FREQ:STOP": " \u7ec8\u6b62\u9891\u7387",
|
||||||
|
":DISP:WIND:TRAC:Y:SCAL:RLEV": " \u53c2\u8003\u7535\u5e73",
|
||||||
|
":SENS:POW:ACH:BAND:CHAN": " \u4fe1\u9053\u5e26\u5bbd",
|
||||||
|
":CALC:MARK1:X": " MARK\u70b91",
|
||||||
|
":SENS:FREQ:SPAN": " \u9891\u7387\u533a\u95f4\uff08\u5bbd\u5ea6\uff09",
|
||||||
|
":CALC:DELT2:X": " MARK\u70b91\u504f\u79fb"
|
||||||
|
}
|
271
visa-scpi-example/index.html
Normal file
271
visa-scpi-example/index.html
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>SCPI命令参数编辑器</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.command-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.command-row {
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: inherit;
|
||||||
|
/* margin: 10px 0; */
|
||||||
|
padding: 10px ;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 30%;
|
||||||
|
flex-direction: column;
|
||||||
|
/* width: calc(30% - 20px); */
|
||||||
|
|
||||||
|
}
|
||||||
|
.command-row::before {
|
||||||
|
content: attr(data-index);
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 25%;
|
||||||
|
transform: translate(-50%,-50%);
|
||||||
|
font-size: 40px;
|
||||||
|
color: rgba(0, 0, 0, 0.1);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.comment-input {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 5px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
.command-label {
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
font-family: monospace;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.value-input {
|
||||||
|
width: auto;
|
||||||
|
padding: 5px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.button-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
#update-btn {
|
||||||
|
width: 50%;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#update-btn:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
#reset-btn {
|
||||||
|
width: 50%;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background-color: #f44336;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#reset-btn:hover {
|
||||||
|
background-color: #e53935;
|
||||||
|
}
|
||||||
|
.highlight {
|
||||||
|
background-color: yellow; /* 高亮更新后的值 */
|
||||||
|
}
|
||||||
|
/* 添加 toast 样式 */
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background-color: #333;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.success {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error {
|
||||||
|
background-color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-row.active {
|
||||||
|
background-color: rgba(173, 216, 230, 0.7) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>SCPI命令参数编辑器</h1>
|
||||||
|
<div class="command-container" id="commands-container"></div>
|
||||||
|
<div class="button-container">
|
||||||
|
<button id="reset-btn">复位</button>
|
||||||
|
<button id="update-btn">更新</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let commandsData = [];
|
||||||
|
|
||||||
|
async function loadCommands() {
|
||||||
|
const container = document.getElementById('commands-container');
|
||||||
|
container.innerHTML = '';
|
||||||
|
const response = await fetch('commands.json');
|
||||||
|
commandsData = await response.json();
|
||||||
|
const commentsResponse = await fetch('comments.json');
|
||||||
|
const comments = await commentsResponse.json();
|
||||||
|
|
||||||
|
|
||||||
|
commandsData.forEach(({ command, value }, index) => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'command-row';
|
||||||
|
row.setAttribute('data-index', index + 1);
|
||||||
|
|
||||||
|
const commentDisplay = document.createElement('span');
|
||||||
|
commentDisplay.className = 'comment-input';
|
||||||
|
commentDisplay.textContent = comments[command] || '';
|
||||||
|
commentDisplay.ondblclick = () => {
|
||||||
|
const commentInput = document.createElement('input');
|
||||||
|
commentInput.type = 'text';
|
||||||
|
commentInput.value = commentDisplay.textContent;
|
||||||
|
commentInput.onblur = async () => {
|
||||||
|
comments[command] = commentInput.value;
|
||||||
|
commentDisplay.textContent = commentInput.value;
|
||||||
|
row.replaceChild(commentDisplay, commentInput);
|
||||||
|
await fetch('/save_comments', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(comments)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
row.replaceChild(commentInput, commentDisplay);
|
||||||
|
commentInput.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const label = document.createElement('span');
|
||||||
|
label.className = 'command-label';
|
||||||
|
label.textContent = command;
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.className = 'value-input';
|
||||||
|
// 仅支持输入数字
|
||||||
|
input.type = 'number';
|
||||||
|
input.value = value;
|
||||||
|
input.onfocus = () => {
|
||||||
|
row.classList.add('active');
|
||||||
|
};
|
||||||
|
input.onblur = () => {
|
||||||
|
row.classList.remove('active');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 与原值比较,变化则高亮
|
||||||
|
input.oninput = () => {
|
||||||
|
if (input.value !== value) {
|
||||||
|
input.classList.add('highlight');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
input.classList.remove('highlight');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
row.appendChild(commentDisplay);
|
||||||
|
row.appendChild(label);
|
||||||
|
row.appendChild(input);
|
||||||
|
container.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(message, type = 'success') {
|
||||||
|
// 移除现有的 toast
|
||||||
|
const existingToast = document.querySelector('.toast');
|
||||||
|
if (existingToast) {
|
||||||
|
existingToast.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `toast ${type}`;
|
||||||
|
toast.textContent = message;
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
// 触发重排以显示动画
|
||||||
|
setTimeout(() => toast.classList.add('show'), 10);
|
||||||
|
|
||||||
|
// 3秒后自动消失
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('show');
|
||||||
|
setTimeout(() => toast.remove(), 300);
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('update-btn').onclick = async () => {
|
||||||
|
const updates = commandsData.map(({ command }, index) => ({
|
||||||
|
command: command,
|
||||||
|
value: document.querySelectorAll('.value-input')[index].value
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 根据回应判断是否更新成功
|
||||||
|
const response = await fetch('/update_command', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(updates)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success) {
|
||||||
|
showToast('更新成功!');
|
||||||
|
loadCommands(); // 重新加载命令
|
||||||
|
} else {
|
||||||
|
showToast('更新失败!', 'error');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showToast('网络错误,无法更新!', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('reset-btn').onclick = () => {
|
||||||
|
loadCommands(); // 重新加载命令以复位
|
||||||
|
};
|
||||||
|
|
||||||
|
window.onload = loadCommands;
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
58
visa-scpi-example/parse_inp.py
Normal file
58
visa-scpi-example/parse_inp.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import re
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
def parse_inp_file(file_path):
|
||||||
|
commands_dict = []
|
||||||
|
comments_dict = {}
|
||||||
|
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
parts = line.strip().split(' ')
|
||||||
|
if len(parts) == 2: # 只处理有一个参数的指令
|
||||||
|
command, value = parts
|
||||||
|
# 检查参数是否为数字(包括科学计数法)
|
||||||
|
if re.match(r'^-?\d+\.?\d*([eE][-+]?\d+)?$', value):
|
||||||
|
commands_dict.append((command, value)) # 记录指令和参数的元组
|
||||||
|
|
||||||
|
# 读取注释文件
|
||||||
|
comments_file_path = './comments.json'
|
||||||
|
if not os.path.exists(comments_file_path):
|
||||||
|
with open(comments_file_path, 'w') as f:
|
||||||
|
comments_dict = {key: " " for key, _ in commands_dict}
|
||||||
|
json.dump(comments_dict, f)
|
||||||
|
else:
|
||||||
|
with open(comments_file_path, 'r') as f:
|
||||||
|
comments_dict = json.load(f)
|
||||||
|
|
||||||
|
# 保存为JSON文件
|
||||||
|
with open('./commands.json', 'w') as f:
|
||||||
|
json.dump([{ "command": cmd, "value": val } for cmd, val in commands_dict], f, indent=2) # 保存所有指令
|
||||||
|
|
||||||
|
return commands_dict, comments_dict
|
||||||
|
|
||||||
|
def save_comments(comments_dict):
|
||||||
|
with open('./comments.json', 'w') as f:
|
||||||
|
json.dump(comments_dict, f, indent=2)
|
||||||
|
|
||||||
|
def update_inp_file(file_path, command, new_values):
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
updated_lines = []
|
||||||
|
new_values_set = set(new_values) # 使用集合以避免重复值
|
||||||
|
for line in lines:
|
||||||
|
parts = line.strip().split(' ')
|
||||||
|
if len(parts) == 2 and parts[0] == command:
|
||||||
|
# 如果当前行的指令在新值中,则更新为新值
|
||||||
|
if parts[1] in new_values_set:
|
||||||
|
updated_lines.append(f"{command} {parts[1]}\n")
|
||||||
|
else:
|
||||||
|
updated_lines.append(line) # 保留原行
|
||||||
|
else:
|
||||||
|
updated_lines.append(line)
|
||||||
|
|
||||||
|
with open(file_path, 'w') as f:
|
||||||
|
f.writelines(updated_lines)
|
70
visa-scpi-example/server.py
Normal file
70
visa-scpi-example/server.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
from flask import Flask, request, jsonify, send_from_directory
|
||||||
|
from parse_inp import parse_inp_file, update_inp_file, save_comments
|
||||||
|
import os
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# 确保在启动时解析inp文件
|
||||||
|
INP_FILE_PATH = os.path.abspath('File.inp')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
commands, comments = parse_inp_file(INP_FILE_PATH)
|
||||||
|
return send_from_directory(os.path.abspath('./'), 'index.html')
|
||||||
|
|
||||||
|
@app.route('/commands.json')
|
||||||
|
def get_commands():
|
||||||
|
return send_from_directory(os.path.abspath('./'), 'commands.json')
|
||||||
|
|
||||||
|
@app.route('/update_command', methods=['POST'])
|
||||||
|
def update_command():
|
||||||
|
data = request.json
|
||||||
|
try:
|
||||||
|
# 读取当前的指令和参数
|
||||||
|
with open(INP_FILE_PATH, 'r') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
# 创建一个列表来存储更新后的指令
|
||||||
|
updated_lines = []
|
||||||
|
|
||||||
|
# 遍历现有的指令行
|
||||||
|
for line in lines:
|
||||||
|
parts = line.strip().split(' ')
|
||||||
|
if len(parts) == 2: # 只处理有一个参数的指令
|
||||||
|
command, value = parts
|
||||||
|
# 检查是否需要更新该指令
|
||||||
|
for item in data:
|
||||||
|
if item['command'] == command:
|
||||||
|
# 如果指令匹配,使用新的值
|
||||||
|
updated_lines.append(f"{command} {item['value']}\n")
|
||||||
|
# 匹配后应删除data中对应的指令及参数
|
||||||
|
data.remove(item)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# 如果没有匹配,保留原行
|
||||||
|
updated_lines.append(line)
|
||||||
|
|
||||||
|
# 将更新后的指令写回文件
|
||||||
|
with open(INP_FILE_PATH, 'w') as f:
|
||||||
|
f.writelines(updated_lines)
|
||||||
|
|
||||||
|
# 更新后重新解析文件
|
||||||
|
parse_inp_file(INP_FILE_PATH)
|
||||||
|
return jsonify({'success': True})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/save_comments', methods=['POST'])
|
||||||
|
def save_comments_route():
|
||||||
|
comments = request.json
|
||||||
|
save_comments(comments)
|
||||||
|
return jsonify({'success': True})
|
||||||
|
|
||||||
|
@app.route('/comments.json')
|
||||||
|
def get_comments():
|
||||||
|
return send_from_directory(os.path.abspath('./'), 'comments.json')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# 启动时先解析一次文件
|
||||||
|
parse_inp_file(INP_FILE_PATH)
|
||||||
|
app.run(debug=True)
|
Loading…
Reference in New Issue
Block a user