基于PyQt5和PyVISA的SCPI仪器控制工具

SimpleSCPI项目技术分享:基于PyQt5的SCPI仪器控制工具开发实战

前言

在自动化测试和仪器控制领域,SCPI(Standard Commands for Programmable Instruments)协议是一个广泛使用的标准。本文将分享我开发的一个开源项目——SimpleSCPI,这是一个基于PyQt5的图形化SCPI仪器控制工具。通过这个项目,您不仅可以获得一个实用的仪器控制工具,还能学习到PyQt5桌面应用开发的核心技术。

项目概述

🎯 项目特点

SimpleSCPI是一个功能完整的SCPI仪器控制软件,具有以下特点:

  • 多协议支持:支持TCP/IP、USB、串口等多种连接方式
  • 图形化界面:基于PyQt5的现代化深色主题界面
  • 命令管理:可视化的SCPI命令编辑、保存和批量执行
  • 实时监控:显示命令执行状态、响应时间和通信日志
  • 命令类型管理:支持Write和Query两种命令类型的可视化选择
  • 一键打包:使用PyInstaller打包成独立可执行文件

📸 界面预览

在这里插入图片描述
现代化的深色主题界面,支持实时I/O监控

技术架构

🏗️ 项目结构

SimpleSCPI/
├── src/
│   ├── main.py              # 程序入口
│   ├── core/                # 核心功能模块
│   │   ├── instrument.py    # 仪器控制类
│   │   ├── base.py         # 基础类
│   │   └── exceptions.py   # 异常处理
│   ├── ui/                 # 用户界面
│   │   ├── main_window.py   # 主窗口逻辑
│   │   └── MainUI.py       # UI定义
│   └── resources/          # 资源文件
├── SimpleSCPI.spec         # PyInstaller配置
├── requirements.txt        # 依赖包
└── environment.yml         # Conda环境配置

🔧 技术栈

  • GUI框架:PyQt5 - 跨平台GUI开发
  • 仪器通信:PyVISA - 标准仪器通信库
  • 界面主题:QDarkStyle - 现代化深色主题
  • 打包工具:PyInstaller - 生成独立可执行文件

核心技术实现

1. 仪器通信模块

仪器通信是整个项目的核心,我们使用PyVISA库来实现与SCPI仪器的通信:

class Instrument(BaseObject):
    """仪器控制类"""
    
    def __init__(self, visa_dll_path='c:/windows/system32/visa32.dll'):
        super().__init__()
        try:
            self.resource_manager = visa.ResourceManager(visa_dll_path)
        except Exception as e:
            raise ConnectionError(f"Failed to initialize VISA resource manager: {e}")
        
        self.instrument_ctrl = None
        self.instrument_id = None
        self.is_connected = False
        
    def open(self, resource_name, timeout=5000, termination=''):
        """打开仪器连接"""
        try:
            self.instrument_ctrl = self.resource_manager.open_resource(
                resource_name, 
                read_termination=termination
            )
            self.is_connected = True
            self.instrument_ctrl.timeout = timeout
            self.instrument_ctrl.clear()
            self.instrument_id = self.query("*IDN?")
            return True
        except Exception as e:
            raise ConnectionError(f"Failed to connect to {resource_name}: {e}")

    def write(self, command):
        """向仪器写入命令"""
        if not self.is_connected:
            raise CommunicationError("Instrument not connected")
        self.instrument_ctrl.write(command)

    def query(self, command):
        """查询命令并返回结果"""
        if not self.is_connected:
            raise CommunicationError("Instrument not connected")
        self.instrument_ctrl.clear()
        return self.instrument_ctrl.query(command).strip()

技术要点:

  • 使用工厂模式管理VISA资源
  • 完善的异常处理机制
  • 支持多种连接参数配置
  • 自动清除缓冲区避免数据污染

2. PyQt5界面设计

主界面采用分割窗口布局,实现了命令管理、实时监控和日志显示的分离:

class MainWindow(QMainWindow, Ui_MainWindow, BaseObject):
    """主窗口类"""
    
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        
        # 设置窗口标题包含版本信息
        self.setWindowTitle("SimpleSCPI v1.0.0")
        
        # 配置分割窗口比例
        self.splitter.setSizes([300, 200])
        self.splitter_2.setSizes([400, 180])
        
        # 初始化表格和控件
        self.TableWidgetInit()
        self.ToolBarSplit()
        
        # 设置仪器控制对象
        self.ins = Instrument()
        
    def TableWidgetInit(self):
        """初始化命令表格"""
        self.tableWidget_2.setColumnCount(5)
        self.tableWidget_2.setHorizontalHeaderLabels(
            ["Checked", "Command", "Comment", "Type", "Action"]
        )
        
        # 设置列宽和行高
        self.tableWidget_2.setColumnWidth(3, 80)   # Type列
        self.tableWidget_2.setColumnWidth(4, 150)  # Action列
        self.tableWidget_2.verticalHeader().setMinimumSectionSize(35)
        
        # 设置右键菜单
        self.tableWidget_2.setContextMenuPolicy(Qt.CustomContextMenu)
        self.tableWidget_2.customContextMenuRequested.connect(self.SelectMenu)

技术要点:

  • 使用QSplitter实现可调节的分割布局
  • QTableWidget实现可编辑的命令列表
  • 自定义右键菜单提供便捷操作
  • QComboBox实现命令类型选择

3. 命令类型管理系统

为了支持Write和Query两种不同的命令类型,我实现了一个可视化的类型管理系统:

def CreateTypeComboBox(self, row, current_type="write"):
    """创建类型选择下拉框"""
    combo = QComboBox()
    combo.addItems(["write", "query"])
    combo.setCurrentText(current_type)
    
    # 设置样式
    combo.setStyleSheet("""
        QComboBox {
            border: 1px solid #555;
            border-radius: 3px;
            padding: 2px 5px;
            background-color: #2b2b2b;
            color: white;
        }
        QComboBox:hover {
            border-color: #777;
            background-color: #3c3c3c;
        }
        QComboBox::drop-down {
            border: none;
            width: 20px;
        }
    """)
    
    # 连接信号
    combo.currentTextChanged.connect(lambda text: self.TypeComboChanged(row, text))
    
    return combo

def TypeComboChanged(self, row, new_type):
    """处理类型选择变化"""
    if row < len(self.cmdList):
        self.cmdList[row]["Type"] = new_type
        self.Log(f"命令 {row+1} 类型已更改为: {new_type}")

技术要点:

  • 动态创建QComboBox控件
  • 信号槽机制处理用户交互
  • 自定义样式适配深色主题
  • 实时更新数据模型

4. 缓冲区清除功能

为了解决仪器通信中的数据残留问题,实现了缓冲区清除功能:

def clear_buffer(self):
    """
    清除仪器接收缓冲区
    
    Raises:
        CommunicationError: 通信失败时抛出
    """
    if not self.is_connected or not self.instrument_ctrl:
        raise CommunicationError("Instrument not connected")
        
    try:
        self.instrument_ctrl.clear()
        if self.logger:
            self.logger.info("Instrument buffer cleared")
            
    except Exception as e:
        error_msg = f"Clear buffer failed: {e}"
        if self.logger:
            self.logger.error(error_msg)
        raise CommunicationError(error_msg)

def ClearBuffer(self):
    """清除缓冲区按钮处理"""
    try:
        self.ins.clear_buffer()
        self.Log("仪器缓冲区已清除")
    except Exception as e:
        self.Log(f"清除缓冲区失败: {e}")

技术要点:

  • 调用PyVISA的clear()方法清除缓冲区
  • 完善的异常处理和日志记录
  • 工具栏按钮提供便捷操作
  • 解决查询命令执行错误导致的通信问题

5. 配置文件管理

实现了JSON格式的配置文件保存和加载:

def SaveData(self):
    """保存命令配置"""
    config_data = {
        "commands": self.cmdList,
        "saved_time": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
        "version": "1.0",
        "description": "SimpleSCPI命令配置文件"
    }
    
    default_name = f"SCPI_Config_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
    filename, _ = QFileDialog.getSaveFileName(
        self, '保存命令配置', f'./{default_name}',
        'JSON Config File(*.json);;All Files(*.*)'
    )
    
    if filename:
        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(config_data, f, ensure_ascii=False, indent=2)
        self.Log(f"配置已保存到: {filename}")

def AutoSaveConfig(self):
    """自动保存配置"""
    try:
        config_data = {
            "commands": self.cmdList,
            "saved_time": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
            "auto_saved": True
        }
        
        with open("scpi_config_auto.json", 'w', encoding='utf-8') as f:
            json.dump(config_data, f, ensure_ascii=False, indent=2)
            
    except Exception as e:
        self.Log(f"自动保存失败: {e}")

技术要点:

  • JSON格式存储配置数据
  • 自动保存机制防止数据丢失
  • 文件对话框提供友好的用户体验
  • UTF-8编码支持中文注释

6. 多线程批量执行

实现了多线程的命令批量执行功能:

class TestThread(QThread):
    """测试线程类"""
    
    finished_signal = pyqtSignal(str)
    progress_signal = pyqtSignal(int, int)
    
    def __init__(self, cmd_list, instrument):
        super().__init__()
        self.cmd_list = cmd_list
        self.instrument = instrument
        self.is_running = True
        
    def run(self):
        """执行批量测试"""
        total_commands = len(self.cmd_list)
        
        for i, cmd_info in enumerate(self.cmd_list):
            if not self.is_running:
                break
                
            command = cmd_info["Command"]
            cmd_type = cmd_info["Type"]
            
            try:
                if cmd_type == "query":
                    result = self.instrument.query(command)
                elif cmd_type == "write":
                    self.instrument.write(command)
                    
                # 发送进度信号
                self.progress_signal.emit(i + 1, total_commands)
                
            except Exception as e:
                self.finished_signal.emit(f"Command failed: {e}")
                return
                
        self.finished_signal.emit("Batch execution completed")

技术要点:

  • QThread实现多线程处理
  • 信号槽机制更新UI状态
  • 异常处理确保线程安全
  • 进度反馈提升用户体验

PyInstaller打包配置

为了方便分发,我使用PyInstaller将程序打包成独立的可执行文件:

# SimpleSCPI.spec
a = Analysis(
    ['src/main.py'],
    pathex=[],
    binaries=[],
    datas=[
        ('src/resources', 'resources'),
    ],
    hiddenimports=[
        'PyQt5.QtCore',
        'PyQt5.QtGui', 
        'PyQt5.QtWidgets',
        'pyvisa',
        'qdarkstyle'
    ],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=None,
    noarchive=False,
)

pyz = PYZ(a.pure, a.zipped_data, cipher=None)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.zipfiles,
    a.datas,
    [],
    name='SimpleSCPI',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=False,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    icon='src/resources/icon.ico'
)

打包命令:

pyinstaller SimpleSCPI.spec

项目亮点与学习价值

🎯 PyQt5学习要点

  1. 界面布局管理:掌握QSplitter、QTableWidget等控件的使用
  2. 信号槽机制:理解Qt的事件处理模式
  3. 自定义控件:学会创建和管理自定义UI组件
  4. 多线程编程:掌握QThread在GUI应用中的应用
  5. 样式定制:学习QSS样式表的使用

🔧 实用技术技巧

  1. 模块化设计:清晰的项目结构和模块划分
  2. 异常处理:完善的错误处理和用户反馈机制
  3. 配置管理:JSON格式的配置文件存储方案
  4. 打包部署:PyInstaller的配置和使用技巧
  5. 跨平台兼容:处理不同操作系统的兼容性问题

📚 扩展学习方向

  1. 仪器通信协议:深入学习SCPI、VISA等标准
  2. 自动化测试:将工具集成到测试流程中
  3. 数据可视化:添加图表和数据分析功能
  4. 插件系统:设计可扩展的插件架构
  5. 云端集成:支持远程仪器控制和数据同步

安装使用

🚀 快速开始

  1. 克隆项目
git clone https://github.com/Alen2013/SimpleSCPI.git
cd SimpleSCPI
  1. 安装依赖
# 使用conda(推荐)
conda env create -f environment.yml
conda activate simplescpi

# 或使用pip
pip install -r requirements.txt
  1. 运行程序
cd src
python main.py

📦 直接下载

如果您不想配置开发环境,可以直接下载打包好的可执行文件:

🔧 基本使用

  1. 连接仪器:在工具栏输入仪器地址(如:TCPIP0::192.168.1.100::5001::SOCKET
  2. 添加命令:右键命令列表选择"Add Item"
  3. 执行命令:点击"Send"或"Query"按钮
  4. 查看结果:在右侧I/O面板查看通信记录

项目总结

SimpleSCPI项目展示了如何使用PyQt5开发一个功能完整的桌面应用程序。通过这个项目,您可以学习到:

  • PyQt5桌面应用开发的核心技术
  • 仪器通信编程的实践经验
  • 软件架构设计的最佳实践
  • 项目打包部署的完整流程

项目采用MIT开源协议,欢迎大家使用、学习和贡献代码。无论您是PyQt5初学者还是仪器控制开发者,这个项目都能为您提供有价值的参考。

开源地址

  • GitHub: https://github.com/Alen2013/SimpleSCPI

技术交流

如果您在使用过程中遇到问题,或者有好的建议,欢迎通过以下方式联系:

  • 提交GitHub Issues
  • 发送邮件至:mail_along@163.com
  • 关注我的优快云博客获取更多技术分享

本文介绍的SimpleSCPI项目是一个完全开源的实用工具,希望能够帮助到需要进行仪器控制开发的朋友们。如果觉得有用,请给项目点个Star支持一下!

#仪器控制 #自动化测试 #scpi

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值