feat:提交一下关于线径获取和相机回显的代码
This commit is contained in:
parent
ef1a624099
commit
32740b7229
92
README.md
92
README.md
@ -23,6 +23,14 @@
|
|||||||
- 提供各种工具类,如配置加载器、Modbus通信、串口管理等
|
- 提供各种工具类,如配置加载器、Modbus通信、串口管理等
|
||||||
- 采用单例模式确保资源共享
|
- 采用单例模式确保资源共享
|
||||||
|
|
||||||
|
5. **硬件集成层**:
|
||||||
|
- **相机子系统**:基于海康威视SDK进行集成
|
||||||
|
- 相机管理器(CameraManager):单例模式,管理相机生命周期
|
||||||
|
- 相机显示组件(CameraDisplayWidget):用于实时显示相机画面
|
||||||
|
- 相机设置控制器(CameraSettingsWidget):管理相机参数设置
|
||||||
|
- **串口通信**:与称重设备、扫描器等外设通信
|
||||||
|
- **Modbus通信**:与PLC设备通信
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
1. **前端技术**:
|
1. **前端技术**:
|
||||||
@ -38,9 +46,10 @@
|
|||||||
3. **通信技术**:
|
3. **通信技术**:
|
||||||
- Modbus TCP用于与PLC设备通信
|
- Modbus TCP用于与PLC设备通信
|
||||||
- 串口通信用于与称重设备、条码扫描器等外设通信
|
- 串口通信用于与称重设备、条码扫描器等外设通信
|
||||||
|
- 海康威视SDK用于相机图像采集和处理
|
||||||
|
|
||||||
4. **设计模式**:
|
4. **设计模式**:
|
||||||
- 单例模式(配置加载器、监控器等)
|
- 单例模式(配置加载器、监控器、相机管理器等)
|
||||||
- DAO模式(数据访问)
|
- DAO模式(数据访问)
|
||||||
- 观察者模式(信号槽)
|
- 观察者模式(信号槽)
|
||||||
- 工厂模式(数据库连接)
|
- 工厂模式(数据库连接)
|
||||||
@ -55,30 +64,75 @@
|
|||||||
- `db/`:包含数据库文件
|
- `db/`:包含数据库文件
|
||||||
- `config/`:包含配置文件
|
- `config/`:包含配置文件
|
||||||
- `logs/`:包含日志文件
|
- `logs/`:包含日志文件
|
||||||
|
- `camera/`:包含相机模块和SDK接口类
|
||||||
|
|
||||||
2. **核心文件**:
|
2. **核心文件**:
|
||||||
- `main.py`:程序入口点
|
- `main.py`:程序入口点
|
||||||
- `widgets/login_widget.py`:登录窗口控制器
|
- `widgets/login_widget.py`:登录窗口控制器
|
||||||
- `widgets/main_window.py`:主窗口控制器
|
- `widgets/main_window.py`:主窗口控制器
|
||||||
|
- `widgets/camera_manager.py`:相机管理器
|
||||||
|
- `widgets/camera_display_widget.py`:相机显示组件
|
||||||
|
- `widgets/camera_settings_widget.py`:相机设置控制器
|
||||||
- `utils/config_loader.py`:配置加载器
|
- `utils/config_loader.py`:配置加载器
|
||||||
- `utils/modbus_utils.py`:Modbus通信工具
|
- `utils/modbus_utils.py`:Modbus通信工具
|
||||||
- `utils/sql_utils.py`:数据库工具
|
- `utils/sql_utils.py`:数据库工具
|
||||||
|
- `camera/CamOperation_class.py`:相机操作类
|
||||||
|
- `camera/MvCameraControl_class.py`:海康威视相机控制SDK
|
||||||
|
- `utils/local_image_player.py`:本地图像序列播放器
|
||||||
|
|
||||||
3. **应用流程**:
|
3. **应用流程**:
|
||||||
- 程序启动后初始化日志系统
|
- 程序启动后初始化日志系统和配置
|
||||||
- 加载配置文件
|
- 创建和初始化各子系统(数据库、电力监控器等)
|
||||||
- 显示登录窗口
|
- 显示登录窗口
|
||||||
- 验证登录后显示主窗口
|
- 验证登录后显示主窗口
|
||||||
- 主窗口中进行产线包装系统的操作
|
- 主窗口中进行产线包装系统的操作,包括:
|
||||||
|
- 产品检测和包装
|
||||||
|
- 实时相机监控
|
||||||
|
- 数据采集和报表生成
|
||||||
|
- 设备状态监控和控制
|
||||||
|
|
||||||
|
## 相机子系统详解
|
||||||
|
|
||||||
|
1. **架构设计**:
|
||||||
|
- 采用分层设计,将相机SDK封装在底层,提供简洁API供上层使用
|
||||||
|
- 相机管理采用单例模式,确保全局只有一个相机实例
|
||||||
|
- 使用信号槽机制实现相机状态与UI的松耦合通信
|
||||||
|
|
||||||
|
2. **核心组件**:
|
||||||
|
- `CameraManager`:单例类,负责相机设备枚举、开关、参数设置等
|
||||||
|
- `CameraDisplayWidget`:显示组件,负责在UI中显示相机画面
|
||||||
|
- `CameraSettingsWidget`:设置控制器,负责参数调整界面交互
|
||||||
|
- `CamOperation_class`:相机操作封装类,直接与海康SDK交互
|
||||||
|
- `LocalImagePlayer`:本地图像序列播放器,提供基于本地图片序列的视频模拟功能
|
||||||
|
|
||||||
|
3. **工作流程**:
|
||||||
|
- 系统启动时初始化相机SDK
|
||||||
|
- 用户界面显示时枚举并连接可用的相机设备
|
||||||
|
- 启动相机图像采集并在UI中显示
|
||||||
|
- 用户可通过设置界面调整相机参数(曝光、增益、帧率等)
|
||||||
|
- 系统关闭时正确释放相机资源
|
||||||
|
|
||||||
|
4. **配置管理**:
|
||||||
|
- 相机参数保存在`config/app_config.json`的`camera`部分
|
||||||
|
- 包括默认曝光时间、增益、帧率等参数
|
||||||
|
- 用户调整的参数可保存至配置文件持久化
|
||||||
|
|
||||||
|
5. **本地图像模式**:
|
||||||
|
- 支持本地图像序列播放,可用于模拟相机实时画面
|
||||||
|
- 用户可选择包含图像序列的文件夹,系统自动按时间顺序播放
|
||||||
|
- 可调整播放帧率、设置循环播放等参数
|
||||||
|
- 适用于开发测试和演示场景,无需连接实际相机设备
|
||||||
|
- 配置参数保存在`config/app_config.json`的`camera.local_mode`部分
|
||||||
|
|
||||||
## 功能特点
|
## 功能特点
|
||||||
|
|
||||||
1. **用户认证**:支持用户登录和权限控制
|
1. **用户认证**:支持用户登录和权限控制
|
||||||
2. **产线监控**:实时监控产线状态、电力消耗等
|
2. **产线监控**:实时监控产线状态、电力消耗等
|
||||||
3. **数据采集**:采集称重数据、检验数据等
|
3. **数据采集**:采集称重数据、检验数据等
|
||||||
4. **报表生成**:生成各类统计报表
|
4. **相机集成**:支持实时图像采集、显示和参数调整
|
||||||
5. **设备通信**:与PLC、称重设备等通信
|
5. **报表生成**:生成各类统计报表
|
||||||
6. **多模式支持**:支持单机模式和接口模式
|
6. **设备通信**:与PLC、称重设备等通信
|
||||||
|
7. **多模式支持**:支持单机模式和接口模式
|
||||||
|
|
||||||
## 运行环境
|
## 运行环境
|
||||||
|
|
||||||
@ -98,7 +152,26 @@
|
|||||||
- 默认使用SQLite数据库,位于`db/jtDB.db`
|
- 默认使用SQLite数据库,位于`db/jtDB.db`
|
||||||
- 可在`config/app_config.json`中配置其他数据库
|
- 可在`config/app_config.json`中配置其他数据库
|
||||||
|
|
||||||
3. 运行程序:
|
3. 配置相机:
|
||||||
|
- 在`config/app_config.json`中的`camera`部分调整相机参数
|
||||||
|
- 默认参数:
|
||||||
|
```json
|
||||||
|
"camera": {
|
||||||
|
"enabled": false,
|
||||||
|
"default_exposure": 20000,
|
||||||
|
"default_gain": 10,
|
||||||
|
"default_framerate": 30,
|
||||||
|
"local_mode": {
|
||||||
|
"enabled": false,
|
||||||
|
"folder_path": "",
|
||||||
|
"framerate": 15,
|
||||||
|
"loop": true,
|
||||||
|
"file_patterns": [".jpg", ".jpeg", ".png", ".bmp"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 运行程序:
|
||||||
```
|
```
|
||||||
python main.py
|
python main.py
|
||||||
```
|
```
|
||||||
@ -109,4 +182,5 @@
|
|||||||
|
|
||||||
1. 添加新的数据源:扩展`utils/sql_utils.py`
|
1. 添加新的数据源:扩展`utils/sql_utils.py`
|
||||||
2. 添加新的设备通信协议:参考`utils/modbus_utils.py`
|
2. 添加新的设备通信协议:参考`utils/modbus_utils.py`
|
||||||
3. 添加新的UI界面:在`ui/`目录下创建新的UI类,在`widgets/`目录下创建对应的控制器类
|
3. 添加新的UI界面:在`ui/`目录下创建新的UI类,在`widgets/`目录下创建对应的控制器类
|
||||||
|
4. 扩展相机功能:修改`widgets/camera_manager.py`和`camera/CamOperation_class.py`
|
||||||
8
app_config.json
Normal file
8
app_config.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"local_image_mode": {
|
||||||
|
"enabled": true,
|
||||||
|
"folder_path": "/Users/meng/Downloads/images",
|
||||||
|
"framerate": 15,
|
||||||
|
"loop": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -46,7 +46,19 @@
|
|||||||
"enabled": false,
|
"enabled": false,
|
||||||
"default_exposure": 20000,
|
"default_exposure": 20000,
|
||||||
"default_gain": 10,
|
"default_gain": 10,
|
||||||
"default_framerate": 30
|
"default_framerate": 30,
|
||||||
|
"local_mode": {
|
||||||
|
"enabled": true,
|
||||||
|
"folder_path": "/Users/meng/Downloads/images",
|
||||||
|
"framerate": 15,
|
||||||
|
"loop": true,
|
||||||
|
"file_patterns": [
|
||||||
|
".jpg",
|
||||||
|
".jpeg",
|
||||||
|
".png",
|
||||||
|
".bmp"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"modbus": {
|
"modbus": {
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
@ -79,6 +91,19 @@
|
|||||||
"stop_bits": 1,
|
"stop_bits": 1,
|
||||||
"timeout": 1
|
"timeout": 1
|
||||||
},
|
},
|
||||||
|
"xj": {
|
||||||
|
"bit": 10,
|
||||||
|
"code": "xj",
|
||||||
|
"data_bits": 8,
|
||||||
|
"parity": "N",
|
||||||
|
"port": "19200",
|
||||||
|
"query_cmd": "01 41 0d",
|
||||||
|
"query_interval": 5,
|
||||||
|
"auto_query": true,
|
||||||
|
"ser": "COM3",
|
||||||
|
"stop_bits": 1,
|
||||||
|
"timeout": 1
|
||||||
|
},
|
||||||
"scanner": {
|
"scanner": {
|
||||||
"code": "scanner",
|
"code": "scanner",
|
||||||
"data_bits": 8,
|
"data_bits": 8,
|
||||||
|
|||||||
BIN
db/jtDB.db
BIN
db/jtDB.db
Binary file not shown.
@ -17,4 +17,5 @@ pandas>=1.4.0 # 数据分析和处理
|
|||||||
# 可选依赖
|
# 可选依赖
|
||||||
pillow>=9.0.0 # 图像处理,用于相机功能
|
pillow>=9.0.0 # 图像处理,用于相机功能
|
||||||
pynput>=1.7.6 # 键盘监听
|
pynput>=1.7.6 # 键盘监听
|
||||||
requests>=2.27.1 # HTTP请求
|
requests>=2.27.1 # HTTP请求
|
||||||
|
opencv-python>=4.5.5.0 # 图像处理和视频功能,用于本地图像播放
|
||||||
File diff suppressed because it is too large
Load Diff
715
tests/main_window_ui.py
Normal file
715
tests/main_window_ui.py
Normal file
@ -0,0 +1,715 @@
|
|||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QMainWindow, QWidget, QLabel, QGridLayout, QVBoxLayout, QHBoxLayout,
|
||||||
|
QTableWidget, QTableWidgetItem, QHeaderView, QFrame, QSplitter,
|
||||||
|
QPushButton, QLineEdit, QAbstractItemView, QComboBox
|
||||||
|
)
|
||||||
|
from PySide6.QtGui import QFont, QAction, QBrush, QColor
|
||||||
|
from PySide6.QtCore import Qt, QDateTime, QTimer
|
||||||
|
|
||||||
|
class MainWindowUI(QMainWindow):
|
||||||
|
def __init__(self,username):
|
||||||
|
super().__init__()
|
||||||
|
self.username = username
|
||||||
|
self.setWindowTitle(f"腾智微丝产线包装系统")
|
||||||
|
self.resize(1200, 800)
|
||||||
|
self.init_ui()
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
# 设置字体
|
||||||
|
self.title_font = QFont("微软雅黑", 20, QFont.Bold)
|
||||||
|
self.second_title_font = QFont("微软雅黑", 14, QFont.Bold)
|
||||||
|
self.normal_font = QFont("微软雅黑", 12)
|
||||||
|
self.small_font = QFont("微软雅黑", 9)
|
||||||
|
|
||||||
|
# 创建菜单栏
|
||||||
|
self.create_menu()
|
||||||
|
|
||||||
|
# 创建中央部件
|
||||||
|
self.central_widget = QWidget()
|
||||||
|
self.setCentralWidget(self.central_widget)
|
||||||
|
|
||||||
|
# 创建主布局 - 水平分割
|
||||||
|
self.main_layout = QHBoxLayout(self.central_widget)
|
||||||
|
self.main_layout.setContentsMargins(5, 5, 5, 5)
|
||||||
|
self.main_layout.setSpacing(5)
|
||||||
|
|
||||||
|
# 创建左侧面板
|
||||||
|
self.left_panel = QWidget()
|
||||||
|
self.left_layout = QVBoxLayout(self.left_panel)
|
||||||
|
self.left_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.left_layout.setSpacing(5)
|
||||||
|
self.create_left_panel()
|
||||||
|
|
||||||
|
# 创建右侧面板
|
||||||
|
self.right_panel = QWidget()
|
||||||
|
self.right_layout = QVBoxLayout(self.right_panel)
|
||||||
|
self.right_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.right_layout.setSpacing(5)
|
||||||
|
self.create_right_panel()
|
||||||
|
|
||||||
|
# 添加左右面板到主布局
|
||||||
|
self.main_layout.addWidget(self.left_panel, 1) # 左侧面板占比较小
|
||||||
|
self.main_layout.addWidget(self.right_panel, 2) # 右侧面板占比较大
|
||||||
|
|
||||||
|
def create_menu(self):
|
||||||
|
# 创建菜单栏
|
||||||
|
self.menubar = self.menuBar()
|
||||||
|
|
||||||
|
# 用户操作菜单
|
||||||
|
self.user_menu = self.menubar.addMenu("用户操作页")
|
||||||
|
self.main_action = QAction("主页面", self)
|
||||||
|
self.user_menu.addAction(self.main_action)
|
||||||
|
|
||||||
|
# 系统设置菜单
|
||||||
|
self.system_menu = self.menubar.addMenu("系统设置")
|
||||||
|
self.settings_action = QAction("设置页面", self)
|
||||||
|
self.system_menu.addAction(self.settings_action)
|
||||||
|
|
||||||
|
def create_left_panel(self):
|
||||||
|
# 创建标题容器
|
||||||
|
self.title_container = QWidget()
|
||||||
|
self.title_layout = QHBoxLayout(self.title_container)
|
||||||
|
self.title_layout.setContentsMargins(10, 10, 10, 10)
|
||||||
|
self.title_layout.setSpacing(10)
|
||||||
|
|
||||||
|
# 创建用户名标签
|
||||||
|
self.username_label = QLabel(self.username)
|
||||||
|
self.username_label.setFont(self.normal_font)
|
||||||
|
self.username_label.setStyleSheet("color: #666666;")
|
||||||
|
self.username_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||||
|
|
||||||
|
# 创建主标题标签
|
||||||
|
self.title_label = QLabel("腾智微丝产线包装系统")
|
||||||
|
self.title_label.setFont(self.title_font) # 较大的字体
|
||||||
|
self.title_label.setAlignment(Qt.AlignCenter)
|
||||||
|
self.title_label.setStyleSheet("color: #1a237e;")
|
||||||
|
|
||||||
|
# 创建时间标签
|
||||||
|
self.time_label = QLabel()
|
||||||
|
self.time_label.setFont(self.normal_font)
|
||||||
|
self.time_label.setStyleSheet("color: #666666;")
|
||||||
|
self.time_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||||
|
|
||||||
|
# 创建定时器更新时间
|
||||||
|
self.timer = QTimer(self)
|
||||||
|
self.timer.timeout.connect(self.update_time)
|
||||||
|
self.timer.start(1000) # 每秒更新一次
|
||||||
|
self.update_time() # 立即更新一次时间
|
||||||
|
|
||||||
|
# 将标签添加到标题布局中
|
||||||
|
self.title_layout.addWidget(self.username_label, 1)
|
||||||
|
self.title_layout.addWidget(self.title_label, 2) # 中间标题占据更多空间
|
||||||
|
self.title_layout.addWidget(self.time_label, 1)
|
||||||
|
|
||||||
|
# 设置标题容器的样式
|
||||||
|
self.title_container.setStyleSheet("background-color: #f5f5f5; border-radius: 4px;")
|
||||||
|
self.left_layout.addWidget(self.title_container)
|
||||||
|
|
||||||
|
# 项目信息表格 - 使用QFrame包裹,添加边框
|
||||||
|
self.project_frame = QFrame()
|
||||||
|
self.project_frame.setFrameShape(QFrame.StyledPanel)
|
||||||
|
self.project_frame.setLineWidth(1)
|
||||||
|
self.project_frame.setFixedHeight(150) # 调整这个值可以控制整体高度
|
||||||
|
self.project_layout = QVBoxLayout(self.project_frame)
|
||||||
|
self.project_layout.setContentsMargins(5, 5, 5, 5)
|
||||||
|
|
||||||
|
# 项目表格
|
||||||
|
self.project_table = QTableWidget(4, 4)
|
||||||
|
self.project_table.setHorizontalHeaderLabels(["用电", "数量", "产量", "开机率"])
|
||||||
|
self.project_table.setVerticalHeaderLabels(["当日", "当月", "当年", "累计"])
|
||||||
|
#设置字体
|
||||||
|
self.project_table.setFont(self.normal_font)
|
||||||
|
# 设置垂直表头宽度
|
||||||
|
self.project_table.verticalHeader().setFixedWidth(60)
|
||||||
|
self.project_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||||
|
self.project_table.verticalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||||
|
self.project_table.setEditTriggers(QTableWidget.NoEditTriggers) # 设置为不可编辑
|
||||||
|
self.project_layout.addWidget(self.project_table)
|
||||||
|
self.left_layout.addWidget(self.project_frame)
|
||||||
|
|
||||||
|
# 任务信息区域 - 使用QFrame包裹,添加边框
|
||||||
|
self.task_frame = QFrame()
|
||||||
|
self.task_frame.setFrameShape(QFrame.StyledPanel)
|
||||||
|
self.task_frame.setLineWidth(1)
|
||||||
|
self.task_frame.setFixedHeight(180)
|
||||||
|
self.task_layout = QVBoxLayout(self.task_frame)
|
||||||
|
self.task_layout.setContentsMargins(8, 8, 8, 8)
|
||||||
|
|
||||||
|
# 任务标签
|
||||||
|
self.task_label = QLabel("任务")
|
||||||
|
self.task_label.setFont(self.normal_font)
|
||||||
|
self.task_label.setAlignment(Qt.AlignLeft)
|
||||||
|
self.task_label.setStyleSheet("font-weight: bold; color: #333333;")
|
||||||
|
self.task_layout.addWidget(self.task_label)
|
||||||
|
|
||||||
|
|
||||||
|
# 订单行
|
||||||
|
self.order_layout = QHBoxLayout()
|
||||||
|
self.order_layout.setAlignment(Qt.AlignLeft) # 设置整个布局左对齐
|
||||||
|
|
||||||
|
self.order_label = QLabel("工程号")
|
||||||
|
self.order_label.setFont(QFont("微软雅黑", 12, QFont.Bold))
|
||||||
|
self.order_label.setFixedHeight(30)
|
||||||
|
self.order_label.setStyleSheet("padding: 0 5px; color: #333333;")
|
||||||
|
|
||||||
|
self.order_edit = QLineEdit()
|
||||||
|
self.order_edit.setFixedHeight(30)
|
||||||
|
self.order_edit.setFixedWidth(150)
|
||||||
|
self.order_edit.setReadOnly(False)
|
||||||
|
self.order_edit.setFont(QFont("微软雅黑", 12))
|
||||||
|
self.order_edit.setText("") # 设置默认订单号
|
||||||
|
self.order_edit.setStyleSheet("background-color: #f9f9f9; border: 1px solid #cccccc; border-radius: 3px; padding: 2px 5px;")
|
||||||
|
|
||||||
|
self.order_layout.addWidget(self.order_label)
|
||||||
|
self.order_layout.addWidget(self.order_edit)
|
||||||
|
self.order_layout.addStretch() # 添加弹性空间,将组件推到左侧
|
||||||
|
self.task_layout.addLayout(self.order_layout)
|
||||||
|
|
||||||
|
# 任务表格 - 使用合并单元格实现一级二级标题
|
||||||
|
self.task_table = QTableWidget(3, 4) # 3行4列:一级标题行、二级标题行、数据行
|
||||||
|
self.task_table.setEditTriggers(QTableWidget.NoEditTriggers) # 设置为不可编辑
|
||||||
|
self.task_table.horizontalHeader().setVisible(False)
|
||||||
|
self.task_table.verticalHeader().setVisible(False)
|
||||||
|
self.task_table.setShowGrid(True)
|
||||||
|
|
||||||
|
# 设置列宽均等
|
||||||
|
self.task_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||||
|
|
||||||
|
# 第一行:订单量和完成量 (一级标题)
|
||||||
|
self.task_table.setSpan(0, 0, 1, 2) # 订单量跨2列
|
||||||
|
self.task_table.setSpan(0, 2, 1, 2) # 完成量跨2列
|
||||||
|
|
||||||
|
order_item = QTableWidgetItem("订单量")
|
||||||
|
order_item.setTextAlignment(Qt.AlignCenter)
|
||||||
|
order_item.setFont(self.normal_font)
|
||||||
|
self.task_table.setItem(0, 0, order_item)
|
||||||
|
|
||||||
|
completed_item = QTableWidgetItem("完成量")
|
||||||
|
completed_item.setTextAlignment(Qt.AlignCenter)
|
||||||
|
completed_item.setFont(self.normal_font)
|
||||||
|
self.task_table.setItem(0, 2, completed_item)
|
||||||
|
|
||||||
|
# 第二行:二级标题
|
||||||
|
headers = ["总生产数量", "总生产公斤", "已完成数量", "已完成公斤"]
|
||||||
|
for col, header in enumerate(headers):
|
||||||
|
item = QTableWidgetItem(header)
|
||||||
|
item.setTextAlignment(Qt.AlignCenter)
|
||||||
|
item.setFont(self.small_font)
|
||||||
|
self.task_table.setItem(1, col, item)
|
||||||
|
|
||||||
|
|
||||||
|
# 设置行高
|
||||||
|
self.task_table.setRowHeight(0, 30) # 一级标题行高
|
||||||
|
self.task_table.setRowHeight(1, 30) # 二级标题行高
|
||||||
|
self.task_table.setRowHeight(2, 30) # 数据行高
|
||||||
|
self.task_layout.addWidget(self.task_table)
|
||||||
|
|
||||||
|
self.left_layout.addWidget(self.task_frame)
|
||||||
|
|
||||||
|
# 上料区 - 使用QFrame包裹,添加边框
|
||||||
|
self.material_frame = QFrame()
|
||||||
|
self.material_frame.setFrameShape(QFrame.StyledPanel)
|
||||||
|
self.material_frame.setLineWidth(1)
|
||||||
|
self.material_frame.setFixedHeight(380) # 保持上料区高度不变
|
||||||
|
self.material_frame.setStyleSheet("QFrame { background-color: #f8f8f8; }")
|
||||||
|
self.material_layout = QHBoxLayout(self.material_frame)
|
||||||
|
self.material_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.material_layout.setSpacing(0)
|
||||||
|
|
||||||
|
# 上料区标签
|
||||||
|
self.material_label = QLabel("上料区")
|
||||||
|
self.material_label.setFont(self.normal_font)
|
||||||
|
self.material_label.setAlignment(Qt.AlignCenter)
|
||||||
|
self.material_label.setFixedWidth(100) # 设置固定宽度
|
||||||
|
self.material_label.setStyleSheet("background-color: #e0e0e0; border-right: 1px solid #cccccc; font-weight: bold;")
|
||||||
|
self.material_layout.addWidget(self.material_label)
|
||||||
|
|
||||||
|
# 上料区内容 - 这里可以添加更多控件
|
||||||
|
self.material_content = QWidget()
|
||||||
|
# 使用透明背景,让相机画面可以正常显示
|
||||||
|
self.material_content.setStyleSheet("background-color: transparent;")
|
||||||
|
self.material_content_layout = QVBoxLayout(self.material_content)
|
||||||
|
self.material_content_layout.setContentsMargins(0, 0, 0, 0) # 移除内边距以便相机画面填满
|
||||||
|
self.material_layout.addWidget(self.material_content)
|
||||||
|
|
||||||
|
self.left_layout.addWidget(self.material_frame)
|
||||||
|
|
||||||
|
# 托盘号区域 - 使用QFrame包裹,添加边框
|
||||||
|
self.tray_frame = QFrame()
|
||||||
|
self.tray_frame.setFrameShape(QFrame.StyledPanel)
|
||||||
|
self.tray_frame.setLineWidth(1)
|
||||||
|
# 移除固定高度,让托盘号的高度自适应其内容
|
||||||
|
self.tray_frame.setFixedHeight(40) # 进一步减小高度,刚好容纳控件
|
||||||
|
self.tray_frame.setStyleSheet("QFrame { background-color: #f8f8f8; }")
|
||||||
|
self.tray_layout = QHBoxLayout(self.tray_frame)
|
||||||
|
self.tray_layout.setContentsMargins(0, 0, 0, 0) # 移除内边距,确保没有上下边界 # 移除所有边距
|
||||||
|
self.tray_layout.setSpacing(0) # 移除所有间距
|
||||||
|
|
||||||
|
self.tray_label = QLabel("托盘号")
|
||||||
|
self.tray_label.setFont(self.normal_font)
|
||||||
|
self.tray_label.setAlignment(Qt.AlignCenter)
|
||||||
|
self.tray_label.setFixedWidth(100) # 设置固定宽度
|
||||||
|
self.tray_label.setStyleSheet("background-color: #e0e0e0; border-right: 1px solid #cccccc; font-weight: bold;")
|
||||||
|
self.tray_layout.addWidget(self.tray_label)
|
||||||
|
|
||||||
|
self.tray_edit = QComboBox()
|
||||||
|
self.tray_edit.setFixedHeight(40) # 设置固定高度与父容器相同
|
||||||
|
self.tray_edit.setStyleSheet("QComboBox { border: none; padding: 0px 10px; background-color: white; } QComboBox::drop-down { border: none; width: 20px; }")
|
||||||
|
self.tray_edit.setFont(QFont("微软雅黑", 12))
|
||||||
|
self.tray_edit.setEditable(True) # 允许手动输入
|
||||||
|
self.tray_edit.setInsertPolicy(QComboBox.NoInsert) # 不自动插入用户输入到列表中
|
||||||
|
self.tray_edit.setMaxVisibleItems(10) # 设置下拉框最多显示10个项目
|
||||||
|
self.tray_edit.completer().setCaseSensitivity(Qt.CaseInsensitive) # 设置补全不区分大小写
|
||||||
|
self.tray_edit.completer().setFilterMode(Qt.MatchContains) # 设置模糊匹配模式
|
||||||
|
# 允许清空选择
|
||||||
|
self.tray_edit.setCurrentText("")
|
||||||
|
|
||||||
|
self.tray_layout.addWidget(self.tray_edit)
|
||||||
|
|
||||||
|
self.left_layout.addWidget(self.tray_frame)
|
||||||
|
|
||||||
|
# 下料区 - 使用QFrame包裹,添加边框
|
||||||
|
self.output_frame = QFrame()
|
||||||
|
self.output_frame.setFrameShape(QFrame.StyledPanel)
|
||||||
|
self.output_frame.setLineWidth(1)
|
||||||
|
self.output_frame.setFixedHeight(100) # 压缩下料区域的高度,从原来的150减少到100
|
||||||
|
self.output_frame.setStyleSheet("QFrame { background-color: #f8f8f8; }")
|
||||||
|
self.output_layout = QHBoxLayout(self.output_frame)
|
||||||
|
self.output_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.output_layout.setSpacing(0)
|
||||||
|
|
||||||
|
# 下料区标签
|
||||||
|
self.output_label = QLabel("下料")
|
||||||
|
self.output_label.setFont(self.normal_font)
|
||||||
|
self.output_label.setAlignment(Qt.AlignCenter)
|
||||||
|
self.output_label.setFixedWidth(100) # 设置固定宽度
|
||||||
|
self.output_label.setStyleSheet("background-color: #e0e0e0; border-right: 1px solid #cccccc; font-weight: bold;")
|
||||||
|
self.output_layout.addWidget(self.output_label)
|
||||||
|
|
||||||
|
# 下料区内容 - 这里可以添加更多控件
|
||||||
|
self.output_content = QWidget()
|
||||||
|
self.output_content.setStyleSheet("background-color: white;")
|
||||||
|
self.output_content_layout = QVBoxLayout(self.output_content)
|
||||||
|
self.output_content_layout.setContentsMargins(5, 5, 5, 5) # 减小内部边距
|
||||||
|
self.output_layout.addWidget(self.output_content)
|
||||||
|
|
||||||
|
self.left_layout.addWidget(self.output_frame)
|
||||||
|
|
||||||
|
# 产线控制区 - 使用QFrame包裹,添加边框
|
||||||
|
self.control_frame = QFrame()
|
||||||
|
self.control_frame.setFrameShape(QFrame.StyledPanel)
|
||||||
|
self.control_frame.setLineWidth(1)
|
||||||
|
self.control_layout = QHBoxLayout(self.control_frame)
|
||||||
|
self.control_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.control_layout.setSpacing(0)
|
||||||
|
|
||||||
|
self.control_label = QLabel("产线")
|
||||||
|
self.control_label.setFont(self.normal_font)
|
||||||
|
self.control_label.setAlignment(Qt.AlignCenter)
|
||||||
|
self.control_label.setFixedWidth(100) # 设置固定宽度
|
||||||
|
self.control_label.setStyleSheet("background-color: #e0e0e0; border-right: 1px solid #cccccc; font-weight: bold;")
|
||||||
|
self.control_layout.addWidget(self.control_label)
|
||||||
|
|
||||||
|
# 按钮容器
|
||||||
|
self.button_container = QWidget()
|
||||||
|
self.button_layout = QGridLayout(self.button_container)
|
||||||
|
self.button_layout.setContentsMargins(10, 10, 10, 10)
|
||||||
|
self.button_layout.setSpacing(10) # 增加按钮间距
|
||||||
|
|
||||||
|
# 创建按钮并设置样式
|
||||||
|
button_style = """
|
||||||
|
QPushButton {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.input_button = QPushButton("上料")
|
||||||
|
self.input_button.setFont(self.normal_font)
|
||||||
|
self.input_button.setStyleSheet(button_style + "background-color: #e3f2fd; border: 1px solid #2196f3;")
|
||||||
|
|
||||||
|
self.output_button = QPushButton("下料")
|
||||||
|
self.output_button.setFont(self.normal_font)
|
||||||
|
self.output_button.setStyleSheet(button_style + "background-color: #fff8e1; border: 1px solid #ffc107;")
|
||||||
|
|
||||||
|
self.start_button = QPushButton("开始")
|
||||||
|
self.start_button.setFont(self.normal_font)
|
||||||
|
self.start_button.setStyleSheet(button_style + "background-color: #e8f5e9; border: 1px solid #4caf50;")
|
||||||
|
|
||||||
|
self.stop_button = QPushButton("暂停")
|
||||||
|
self.stop_button.setFont(self.normal_font)
|
||||||
|
self.stop_button.setStyleSheet(button_style + "background-color: #ffebee; border: 1px solid #f44336;")
|
||||||
|
|
||||||
|
self.report_button = QPushButton("报表")
|
||||||
|
self.report_button.setFont(self.normal_font)
|
||||||
|
self.report_button.setStyleSheet(button_style + "background-color: #e0e0e0; border: 1px solid #cccccc;")
|
||||||
|
|
||||||
|
# 使用网格布局排列按钮
|
||||||
|
self.button_layout.addWidget(self.input_button, 0, 0)
|
||||||
|
self.button_layout.addWidget(self.output_button, 0, 1)
|
||||||
|
self.button_layout.addWidget(self.start_button, 0, 2)
|
||||||
|
self.button_layout.addWidget(self.stop_button, 0, 3)
|
||||||
|
self.button_layout.addWidget(self.report_button, 0, 4)
|
||||||
|
|
||||||
|
self.control_layout.addWidget(self.button_container)
|
||||||
|
|
||||||
|
self.left_layout.addWidget(self.control_frame)
|
||||||
|
|
||||||
|
# 添加弹性空间,确保控件紧凑排列在顶部
|
||||||
|
self.left_layout.addStretch()
|
||||||
|
|
||||||
|
def create_right_panel(self):
|
||||||
|
# 创建右侧整体框架
|
||||||
|
self.right_frame = QFrame()
|
||||||
|
self.right_frame.setFrameShape(QFrame.NoFrame) # 移除框架边框
|
||||||
|
self.right_frame.setLineWidth(0)
|
||||||
|
self.right_layout.addWidget(self.right_frame)
|
||||||
|
|
||||||
|
# 右侧整体使用垂直布局,不设置边距
|
||||||
|
self.right_frame_layout = QVBoxLayout(self.right_frame)
|
||||||
|
self.right_frame_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.right_frame_layout.setSpacing(0)
|
||||||
|
|
||||||
|
# 创建一个垂直分割器,用于控制两个表格的高度比例
|
||||||
|
self.right_splitter = QSplitter(Qt.Vertical)
|
||||||
|
|
||||||
|
# 创建微丝产线表格的容器
|
||||||
|
self.process_container = QWidget()
|
||||||
|
self.process_container_layout = QVBoxLayout(self.process_container)
|
||||||
|
self.process_container_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.process_container_layout.setSpacing(0)
|
||||||
|
|
||||||
|
# 创建包装记录表格的容器
|
||||||
|
self.record_container = QWidget()
|
||||||
|
self.record_container_layout = QVBoxLayout(self.record_container)
|
||||||
|
self.record_container_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.record_container_layout.setSpacing(0)
|
||||||
|
|
||||||
|
# 创建微丝产线表格
|
||||||
|
self.create_process_table()
|
||||||
|
self.process_container_layout.addWidget(self.process_frame)
|
||||||
|
|
||||||
|
# 创建包装记录表格
|
||||||
|
self.create_record_table()
|
||||||
|
self.record_container_layout.addWidget(self.record_frame)
|
||||||
|
|
||||||
|
# 将两个容器添加到分割器中
|
||||||
|
self.right_splitter.addWidget(self.process_container)
|
||||||
|
self.right_splitter.addWidget(self.record_container)
|
||||||
|
|
||||||
|
# 设置初始大小比例:微丝产线占1/3,包装记录占2/3
|
||||||
|
self.right_splitter.setSizes([100, 200]) # 比例为1:2
|
||||||
|
|
||||||
|
# 将分割器添加到右侧布局
|
||||||
|
self.right_frame_layout.addWidget(self.right_splitter)
|
||||||
|
|
||||||
|
# 添加一个通用的表格样式设置方法
|
||||||
|
def setup_table_common(self, table, hide_headers=True):
|
||||||
|
"""设置表格的通用样式和属性
|
||||||
|
|
||||||
|
Args:
|
||||||
|
table: 要设置的QTableWidget对象
|
||||||
|
hide_headers: 是否隐藏默认的表头
|
||||||
|
"""
|
||||||
|
# 设置为不可编辑
|
||||||
|
table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||||
|
|
||||||
|
# 隐藏默认表头
|
||||||
|
if hide_headers:
|
||||||
|
table.horizontalHeader().setVisible(False)
|
||||||
|
table.verticalHeader().setVisible(False)
|
||||||
|
|
||||||
|
# 显示网格线
|
||||||
|
table.setShowGrid(True)
|
||||||
|
|
||||||
|
# 移除外边框
|
||||||
|
table.setFrameShape(QFrame.NoFrame)
|
||||||
|
|
||||||
|
# 设置表格样式
|
||||||
|
table.setStyleSheet("""
|
||||||
|
QTableWidget {
|
||||||
|
gridline-color: #dddddd;
|
||||||
|
border: none;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
QTableWidget::item {
|
||||||
|
border: none;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
QTableWidget::item:selected {
|
||||||
|
background-color: #e0e0ff;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 允许用户调整列宽
|
||||||
|
table.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive)
|
||||||
|
table.horizontalHeader().setStretchLastSection(True)
|
||||||
|
|
||||||
|
return table
|
||||||
|
|
||||||
|
# 添加一个通用的表头项创建方法
|
||||||
|
def create_header_item(self, text, font=None, alignment=Qt.AlignCenter, bg_color="#f8f8f8"):
|
||||||
|
"""创建表头单元格项
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: 表头文本
|
||||||
|
font: 字体,默认为None(使用self.normal_font)
|
||||||
|
alignment: 对齐方式
|
||||||
|
bg_color: 背景色
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
QTableWidgetItem: 创建的表头项
|
||||||
|
"""
|
||||||
|
item = QTableWidgetItem(text)
|
||||||
|
item.setTextAlignment(alignment)
|
||||||
|
item.setFont(font or self.normal_font)
|
||||||
|
item.setBackground(QBrush(QColor(bg_color)))
|
||||||
|
return item
|
||||||
|
|
||||||
|
def create_process_table(self):
|
||||||
|
"""创建微丝产线表格,包含上料、检验、包装部分"""
|
||||||
|
# 创建微丝产线框架
|
||||||
|
self.process_frame = QFrame()
|
||||||
|
self.process_frame.setFrameShape(QFrame.Box)
|
||||||
|
self.process_frame.setLineWidth(1)
|
||||||
|
self.process_frame.setStyleSheet("QFrame { border: 1px solid #dddddd; }")
|
||||||
|
self.process_layout = QVBoxLayout(self.process_frame)
|
||||||
|
self.process_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.process_layout.setSpacing(0)
|
||||||
|
|
||||||
|
# 微丝产线标题
|
||||||
|
self.process_title = QLabel("微丝产线")
|
||||||
|
self.process_title.setFont(self.second_title_font)
|
||||||
|
self.process_title.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||||
|
# 调整行高
|
||||||
|
self.process_title.setFixedHeight(40)
|
||||||
|
self.process_title.setStyleSheet("background-color: #f8f8f8; padding: 5px; border-bottom: 1px solid #dddddd;")
|
||||||
|
self.process_layout.addWidget(self.process_title)
|
||||||
|
|
||||||
|
|
||||||
|
# 创建表格内容区域
|
||||||
|
self.process_content = QWidget()
|
||||||
|
self.process_content_layout = QVBoxLayout(self.process_content)
|
||||||
|
self.process_content_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.process_content_layout.setSpacing(0)
|
||||||
|
|
||||||
|
# 创建表格 - 支持动态配置检验列数
|
||||||
|
self.inspection_columns = 1 # 默认至少显示1列
|
||||||
|
# 默认检验标题,实际运行时将通过InspectionConfigManager获取
|
||||||
|
self.inspection_headers = ["检验项"]
|
||||||
|
total_columns = 2 + self.inspection_columns + 2 # 上料2列 + 检验N列 + 包装2列
|
||||||
|
|
||||||
|
self.process_table = QTableWidget(8, total_columns) # 8行:1行标题区域 + 1行列标题 + 6行数据
|
||||||
|
|
||||||
|
# 应用通用表格设置
|
||||||
|
self.setup_table_common(self.process_table)
|
||||||
|
|
||||||
|
# 设置行高
|
||||||
|
self.process_table.setRowHeight(0, 30) # 标题区域行高
|
||||||
|
self.process_table.setRowHeight(1, 30) # 列标题行高
|
||||||
|
|
||||||
|
# 设置数据行的行高
|
||||||
|
for row in range(2, 8): # 工序行
|
||||||
|
self.process_table.setRowHeight(row, 35)
|
||||||
|
|
||||||
|
# 设置列宽
|
||||||
|
self.set_process_table_column_widths()
|
||||||
|
|
||||||
|
# 创建表头 - 合并单元格
|
||||||
|
self.create_process_table_headers()
|
||||||
|
|
||||||
|
|
||||||
|
# 添加表格到布局
|
||||||
|
self.process_content_layout.addWidget(self.process_table)
|
||||||
|
self.process_layout.addWidget(self.process_content)
|
||||||
|
|
||||||
|
def create_record_table(self):
|
||||||
|
"""创建包装记录表格"""
|
||||||
|
# 创建包装记录框架
|
||||||
|
self.record_frame = QFrame()
|
||||||
|
self.record_frame.setFrameShape(QFrame.Box)
|
||||||
|
self.record_frame.setLineWidth(1)
|
||||||
|
self.record_frame.setStyleSheet("QFrame { border: 1px solid #dddddd; }") # 移除 border-top: none;
|
||||||
|
self.record_layout = QVBoxLayout(self.record_frame)
|
||||||
|
self.record_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.record_layout.setSpacing(0)
|
||||||
|
|
||||||
|
# 包装记录标题
|
||||||
|
self.record_title = QLabel("包装记录")
|
||||||
|
self.record_title.setFont(self.second_title_font)
|
||||||
|
self.record_title.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||||
|
# 调整行高
|
||||||
|
self.record_title.setFixedHeight(40)
|
||||||
|
self.record_title.setStyleSheet("background-color: #f8f8f8; padding: 5px; border-bottom: 1px solid #dddddd;")
|
||||||
|
self.record_layout.addWidget(self.record_title)
|
||||||
|
|
||||||
|
# 创建表格
|
||||||
|
self.record_table = QTableWidget(13, 10) # 13行10列:序号、订单、工程号、品名、规格、托号、轴包装号、重量、净重、完成时间
|
||||||
|
|
||||||
|
# 应用通用表格设置但保留表头
|
||||||
|
self.setup_table_common(self.record_table, hide_headers=False)
|
||||||
|
|
||||||
|
# 设置列标题
|
||||||
|
record_headers = ["序号", "订单", "工程号", "品名", "规格", "托号", "轴包装号", "毛重", "净重", "完成时间"]
|
||||||
|
self.record_table.setHorizontalHeaderLabels(record_headers)
|
||||||
|
|
||||||
|
# 设置表头样式
|
||||||
|
self.record_table.horizontalHeader().setStyleSheet("""
|
||||||
|
QHeaderView::section {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid #dddddd;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 设置行高
|
||||||
|
self.record_table.setRowHeight(12, 35) # 合计行高
|
||||||
|
|
||||||
|
# 设置数据行的行高
|
||||||
|
for row in range(0, 12): # 记录行
|
||||||
|
self.record_table.setRowHeight(row, 35)
|
||||||
|
|
||||||
|
# 设置列宽
|
||||||
|
column_widths = [60, 170, 170, 120, 120, 150, 100, 100, 100, 160]
|
||||||
|
for col, width in enumerate(column_widths):
|
||||||
|
self.record_table.setColumnWidth(col, width)
|
||||||
|
self.record_table.horizontalHeader().resizeSection(col, width)
|
||||||
|
|
||||||
|
# 添加表格到布局
|
||||||
|
self.record_layout.addWidget(self.record_table)
|
||||||
|
|
||||||
|
|
||||||
|
# 添加一个通用的单元格创建方法
|
||||||
|
def create_cell_item(self, text, alignment=Qt.AlignCenter):
|
||||||
|
"""创建表格单元格项
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: 单元格文本
|
||||||
|
alignment: 对齐方式
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
QTableWidgetItem: 创建的单元格项
|
||||||
|
"""
|
||||||
|
item = QTableWidgetItem(str(text))
|
||||||
|
item.setTextAlignment(alignment)
|
||||||
|
return item
|
||||||
|
|
||||||
|
def set_inspection_columns(self, columns, headers=None):
|
||||||
|
"""设置检验列数和标题
|
||||||
|
|
||||||
|
Args:
|
||||||
|
columns: 检验列数量
|
||||||
|
headers: 检验列标题列表,如果为None则使用默认标题
|
||||||
|
"""
|
||||||
|
# 确保列数在1-6之间
|
||||||
|
columns = max(1, min(6, columns))
|
||||||
|
|
||||||
|
# 保存旧的列数
|
||||||
|
old_column_count = self.process_table.columnCount()
|
||||||
|
|
||||||
|
# 清除表头行的所有项目
|
||||||
|
if old_column_count > 0:
|
||||||
|
for c_idx in range(old_column_count):
|
||||||
|
# 第0行 - 主标题
|
||||||
|
item_r0 = self.process_table.takeItem(0, c_idx)
|
||||||
|
if item_r0:
|
||||||
|
del item_r0
|
||||||
|
# 第1行 - 子标题
|
||||||
|
item_r1 = self.process_table.takeItem(1, c_idx)
|
||||||
|
if item_r1:
|
||||||
|
del item_r1
|
||||||
|
|
||||||
|
# 清除所有单元格合并
|
||||||
|
for row in range(2):
|
||||||
|
for col in range(old_column_count):
|
||||||
|
try:
|
||||||
|
self.process_table.setSpan(row, col, 1, 1)
|
||||||
|
except:
|
||||||
|
pass # 忽略错误,可能有些单元格没有合并
|
||||||
|
|
||||||
|
# 更新检验列数
|
||||||
|
self.inspection_columns = columns
|
||||||
|
|
||||||
|
# 更新检验标题
|
||||||
|
if headers is not None and len(headers) >= columns:
|
||||||
|
self.inspection_headers = headers[:columns] # 只使用前N个标题
|
||||||
|
elif len(self.inspection_headers) < columns:
|
||||||
|
# 如果当前标题不足,扩展标题列表
|
||||||
|
current_len = len(self.inspection_headers)
|
||||||
|
for i in range(current_len, columns):
|
||||||
|
self.inspection_headers.append(f"检验项{i+1}")
|
||||||
|
|
||||||
|
# 截断多余的标题
|
||||||
|
if len(self.inspection_headers) > columns:
|
||||||
|
self.inspection_headers = self.inspection_headers[:columns]
|
||||||
|
|
||||||
|
# 计算总列数
|
||||||
|
total_columns = 2 + self.inspection_columns + 3 # 上料2列 + 检验N列 + 包装3列
|
||||||
|
self.process_table.setColumnCount(total_columns)
|
||||||
|
|
||||||
|
# 重新设置列宽
|
||||||
|
self.set_process_table_column_widths()
|
||||||
|
|
||||||
|
# 重新创建表头
|
||||||
|
self.create_process_table_headers()
|
||||||
|
|
||||||
|
def create_process_table_headers(self):
|
||||||
|
"""创建微丝产线表格的表头,实现合并单元格"""
|
||||||
|
# 第一行:上料、检验、包装标题区域
|
||||||
|
|
||||||
|
# 上料区域(2列)
|
||||||
|
self.process_table.setSpan(0, 0, 1, 2)
|
||||||
|
self.process_table.setItem(0, 0, self.create_header_item("上料"))
|
||||||
|
|
||||||
|
# 检验区域(动态列数)
|
||||||
|
self.process_table.setSpan(0, 2, 1, self.inspection_columns)
|
||||||
|
self.process_table.setItem(0, 2, self.create_header_item("检验"))
|
||||||
|
|
||||||
|
# 包装区域(3列)
|
||||||
|
packaging_start_col = 2 + self.inspection_columns
|
||||||
|
self.process_table.setSpan(0, packaging_start_col, 1, 3)
|
||||||
|
self.process_table.setItem(0, packaging_start_col, self.create_header_item("包装"))
|
||||||
|
|
||||||
|
# 第二行:列标题
|
||||||
|
# 上料区域列标题
|
||||||
|
material_headers = ["序号", "工程号"]
|
||||||
|
for col, header in enumerate(material_headers):
|
||||||
|
self.process_table.setItem(1, col, self.create_header_item(header))
|
||||||
|
|
||||||
|
# 检验区域列标题 - 可动态配置
|
||||||
|
for i in range(self.inspection_columns):
|
||||||
|
header_text = ""
|
||||||
|
if i < len(self.inspection_headers):
|
||||||
|
header_text = self.inspection_headers[i]
|
||||||
|
else:
|
||||||
|
header_text = f"检验项{i+1}" # 如果没有定义足够的标题,使用默认标题
|
||||||
|
|
||||||
|
self.process_table.setItem(1, 2 + i, self.create_header_item(header_text))
|
||||||
|
|
||||||
|
# 包装区域列标题
|
||||||
|
packaging_headers = ["贴标", "毛重", "净重"]
|
||||||
|
for i, header in enumerate(packaging_headers):
|
||||||
|
self.process_table.setItem(1, packaging_start_col + i, self.create_header_item(header))
|
||||||
|
|
||||||
|
def set_process_table_column_widths(self):
|
||||||
|
"""设置微丝产线表格的列宽 - 支持动态配置检验列"""
|
||||||
|
# 上料区域列宽
|
||||||
|
self.process_table.setColumnWidth(0, 70) # 序号
|
||||||
|
self.process_table.setColumnWidth(1, 190) # 工程号
|
||||||
|
|
||||||
|
# 检验区域列宽
|
||||||
|
for i in range(self.inspection_columns):
|
||||||
|
self.process_table.setColumnWidth(2 + i, 140) # 检验列
|
||||||
|
|
||||||
|
# 包装区域列宽
|
||||||
|
packaging_start_col = 2 + self.inspection_columns
|
||||||
|
self.process_table.setColumnWidth(packaging_start_col, 140) # 贴标
|
||||||
|
self.process_table.setColumnWidth(packaging_start_col + 1, 140) # 毛重
|
||||||
|
self.process_table.setColumnWidth(packaging_start_col + 2, 140) # 净重
|
||||||
|
|
||||||
|
def update_time(self):
|
||||||
|
current_time = QDateTime.currentDateTime().toString('yyyy-MM-dd hh:mm:ss')
|
||||||
|
self.time_label.setText(current_time)
|
||||||
1198
tests/serial_manager.py
Normal file
1198
tests/serial_manager.py
Normal file
File diff suppressed because it is too large
Load Diff
1325
tests/serial_manager.py.fixed
Normal file
1325
tests/serial_manager.py.fixed
Normal file
File diff suppressed because it is too large
Load Diff
483
tests/serial_settings_widget.py
Normal file
483
tests/serial_settings_widget.py
Normal file
@ -0,0 +1,483 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import serial.tools.list_ports
|
||||||
|
import time
|
||||||
|
from PySide6.QtWidgets import QMessageBox
|
||||||
|
from ui.serial_settings_ui import SerialSettingsUI
|
||||||
|
from utils.config_loader import ConfigLoader
|
||||||
|
from utils.serial_manager import SerialManager
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
|
class SerialSettingsWidget(SerialSettingsUI):
|
||||||
|
"""串口设置组件"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.config = ConfigLoader.get_instance()
|
||||||
|
self.serial_manager = SerialManager()
|
||||||
|
|
||||||
|
# 连接信号
|
||||||
|
self.init_connections()
|
||||||
|
|
||||||
|
# 加载设置
|
||||||
|
self.load_settings()
|
||||||
|
|
||||||
|
def init_connections(self):
|
||||||
|
"""初始化信号连接"""
|
||||||
|
# 米电阻串口
|
||||||
|
self.mdz_refresh_btn.clicked.connect(self.refresh_ports)
|
||||||
|
self.test_mdz_btn.clicked.connect(self.test_mdz_port)
|
||||||
|
|
||||||
|
# 线径串口
|
||||||
|
self.xj_refresh_btn.clicked.connect(self.refresh_ports)
|
||||||
|
self.test_xj_btn.clicked.connect(self.test_xj_port)
|
||||||
|
|
||||||
|
# 扫码器串口
|
||||||
|
self.scanner_refresh_btn.clicked.connect(self.refresh_ports)
|
||||||
|
self.test_scanner_btn.clicked.connect(self.test_scanner_port)
|
||||||
|
|
||||||
|
# 保存按钮
|
||||||
|
self.save_btn.clicked.connect(self.save_settings)
|
||||||
|
|
||||||
|
# 初始化时刷新串口列表
|
||||||
|
self.refresh_ports()
|
||||||
|
|
||||||
|
# 加载设置
|
||||||
|
self.load_settings()
|
||||||
|
|
||||||
|
def refresh_ports(self):
|
||||||
|
"""刷新串口列表"""
|
||||||
|
try:
|
||||||
|
# 保存当前选择
|
||||||
|
current_mdz_port = self.mdz_port_combo.currentData()
|
||||||
|
current_xj_port = self.xj_port_combo.currentData()
|
||||||
|
current_scanner_port = self.scanner_port_combo.currentData()
|
||||||
|
|
||||||
|
# 清空列表
|
||||||
|
self.mdz_port_combo.clear()
|
||||||
|
self.xj_port_combo.clear()
|
||||||
|
self.scanner_port_combo.clear()
|
||||||
|
|
||||||
|
# 添加"不使用"选项
|
||||||
|
self.mdz_port_combo.addItem("不使用", "")
|
||||||
|
self.xj_port_combo.addItem("不使用", "")
|
||||||
|
self.scanner_port_combo.addItem("不使用", "")
|
||||||
|
|
||||||
|
# 获取可用串口
|
||||||
|
ports = list(serial.tools.list_ports.comports())
|
||||||
|
|
||||||
|
for port in ports:
|
||||||
|
port_name = port.device
|
||||||
|
port_desc = f"{port_name} ({port.description})"
|
||||||
|
|
||||||
|
# 添加到米电阻下拉框
|
||||||
|
self.mdz_port_combo.addItem(port_desc, port_name)
|
||||||
|
|
||||||
|
# 添加到线径下拉框
|
||||||
|
self.xj_port_combo.addItem(port_desc, port_name)
|
||||||
|
|
||||||
|
# 添加到扫码器下拉框
|
||||||
|
self.scanner_port_combo.addItem(port_desc, port_name)
|
||||||
|
|
||||||
|
# 恢复之前的选择
|
||||||
|
if current_mdz_port:
|
||||||
|
index = self.mdz_port_combo.findData(current_mdz_port)
|
||||||
|
if index >= 0:
|
||||||
|
self.mdz_port_combo.setCurrentIndex(index)
|
||||||
|
else:
|
||||||
|
# 如果之前没有选择,则设为"不使用"
|
||||||
|
self.mdz_port_combo.setCurrentIndex(0)
|
||||||
|
|
||||||
|
if current_xj_port:
|
||||||
|
index = self.xj_port_combo.findData(current_xj_port)
|
||||||
|
if index >= 0:
|
||||||
|
self.xj_port_combo.setCurrentIndex(index)
|
||||||
|
else:
|
||||||
|
# 如果之前没有选择,则设为"不使用"
|
||||||
|
self.xj_port_combo.setCurrentIndex(0)
|
||||||
|
|
||||||
|
if current_scanner_port:
|
||||||
|
index = self.scanner_port_combo.findData(current_scanner_port)
|
||||||
|
if index >= 0:
|
||||||
|
self.scanner_port_combo.setCurrentIndex(index)
|
||||||
|
else:
|
||||||
|
# 如果之前没有选择,则设为"不使用"
|
||||||
|
self.scanner_port_combo.setCurrentIndex(0)
|
||||||
|
|
||||||
|
logging.info(f"已刷新串口列表,找到 {len(ports)} 个串口")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"刷新串口列表失败: {e}")
|
||||||
|
QMessageBox.warning(self, "刷新失败", f"刷新串口列表失败: {e}")
|
||||||
|
|
||||||
|
def load_settings(self):
|
||||||
|
"""加载设置"""
|
||||||
|
try:
|
||||||
|
# 加载全局设置
|
||||||
|
enable_serial = self.config.get_value('app.features.enable_serial_ports', False)
|
||||||
|
enable_keyboard = self.config.get_value('app.features.enable_keyboard_listener', False)
|
||||||
|
|
||||||
|
self.enable_serial_checkbox.setChecked(enable_serial)
|
||||||
|
self.enable_keyboard_checkbox.setChecked(enable_keyboard)
|
||||||
|
|
||||||
|
# 加载米电阻设置
|
||||||
|
mdz_config = self.config.get_config('mdz')
|
||||||
|
if mdz_config:
|
||||||
|
# 设置串口
|
||||||
|
mdz_port = mdz_config.get('ser', '')
|
||||||
|
index = self.mdz_port_combo.findData(mdz_port)
|
||||||
|
if index >= 0:
|
||||||
|
self.mdz_port_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
|
# 设置波特率
|
||||||
|
mdz_baud = str(mdz_config.get('port', '9600'))
|
||||||
|
index = self.mdz_baud_combo.findText(mdz_baud)
|
||||||
|
if index >= 0:
|
||||||
|
self.mdz_baud_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
|
# 设置数据位
|
||||||
|
mdz_data_bits = str(mdz_config.get('data_bits', '8'))
|
||||||
|
index = self.mdz_data_bits_combo.findText(mdz_data_bits)
|
||||||
|
if index >= 0:
|
||||||
|
self.mdz_data_bits_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
|
# 设置停止位
|
||||||
|
mdz_stop_bits = str(mdz_config.get('stop_bits', '1'))
|
||||||
|
index = self.mdz_stop_bits_combo.findText(mdz_stop_bits)
|
||||||
|
if index >= 0:
|
||||||
|
self.mdz_stop_bits_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
|
# 设置校验位
|
||||||
|
mdz_parity = mdz_config.get('parity', 'N')
|
||||||
|
index = self.mdz_parity_combo.findData(mdz_parity)
|
||||||
|
if index >= 0:
|
||||||
|
self.mdz_parity_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
|
# 设置查询指令
|
||||||
|
mdz_query_cmd = mdz_config.get('query_cmd', '01 03 00 01 00 07 55 C8')
|
||||||
|
self.mdz_query_cmd.setText(mdz_query_cmd)
|
||||||
|
|
||||||
|
# 设置查询间隔
|
||||||
|
mdz_query_interval = mdz_config.get('query_interval', 5)
|
||||||
|
self.mdz_query_interval.setValue(mdz_query_interval)
|
||||||
|
|
||||||
|
# 加载线径设置
|
||||||
|
xj_config = self.config.get_config('xj')
|
||||||
|
if xj_config:
|
||||||
|
# 设置串口
|
||||||
|
xj_port = xj_config.get('ser', '')
|
||||||
|
index = self.xj_port_combo.findData(xj_port)
|
||||||
|
if index >= 0:
|
||||||
|
self.xj_port_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
|
# 设置波特率
|
||||||
|
xj_baud = str(xj_config.get('port', '9600'))
|
||||||
|
index = self.xj_baud_combo.findText(xj_baud)
|
||||||
|
if index >= 0:
|
||||||
|
self.xj_baud_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
|
# 设置数据位
|
||||||
|
xj_data_bits = str(xj_config.get('data_bits', '8'))
|
||||||
|
index = self.xj_data_bits_combo.findText(xj_data_bits)
|
||||||
|
if index >= 0:
|
||||||
|
self.xj_data_bits_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
|
# 设置停止位
|
||||||
|
xj_stop_bits = str(xj_config.get('stop_bits', '1'))
|
||||||
|
index = self.xj_stop_bits_combo.findText(xj_stop_bits)
|
||||||
|
if index >= 0:
|
||||||
|
self.xj_stop_bits_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
|
# 设置校验位
|
||||||
|
xj_parity = xj_config.get('parity', 'N')
|
||||||
|
index = self.xj_parity_combo.findData(xj_parity)
|
||||||
|
if index >= 0:
|
||||||
|
self.xj_parity_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
|
# 加载扫码器设置
|
||||||
|
scanner_config = self.config.get_config('scanner')
|
||||||
|
if scanner_config:
|
||||||
|
# 设置串口
|
||||||
|
scanner_port = scanner_config.get('ser', '')
|
||||||
|
index = self.scanner_port_combo.findData(scanner_port)
|
||||||
|
if index >= 0:
|
||||||
|
self.scanner_port_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
|
# 设置波特率
|
||||||
|
scanner_baud = str(scanner_config.get('port', '9600'))
|
||||||
|
index = self.scanner_baud_combo.findText(scanner_baud)
|
||||||
|
if index >= 0:
|
||||||
|
self.scanner_baud_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
|
# 设置数据位
|
||||||
|
scanner_data_bits = str(scanner_config.get('data_bits', '8'))
|
||||||
|
index = self.scanner_data_bits_combo.findText(scanner_data_bits)
|
||||||
|
if index >= 0:
|
||||||
|
self.scanner_data_bits_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
|
# 设置停止位
|
||||||
|
scanner_stop_bits = str(scanner_config.get('stop_bits', '1'))
|
||||||
|
index = self.scanner_stop_bits_combo.findText(scanner_stop_bits)
|
||||||
|
if index >= 0:
|
||||||
|
self.scanner_stop_bits_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
|
# 设置校验位
|
||||||
|
scanner_parity = scanner_config.get('parity', 'N')
|
||||||
|
index = self.scanner_parity_combo.findData(scanner_parity)
|
||||||
|
if index >= 0:
|
||||||
|
self.scanner_parity_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
|
logging.info("已加载串口设置")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"加载串口设置失败: {e}")
|
||||||
|
QMessageBox.warning(self, "加载失败", f"加载串口设置失败: {e}")
|
||||||
|
|
||||||
|
def save_settings(self):
|
||||||
|
"""保存设置"""
|
||||||
|
try:
|
||||||
|
# 保存全局设置
|
||||||
|
enable_serial = self.enable_serial_checkbox.isChecked()
|
||||||
|
enable_keyboard = self.enable_keyboard_checkbox.isChecked()
|
||||||
|
|
||||||
|
self.config.set_value('app.features.enable_serial_ports', enable_serial)
|
||||||
|
self.config.set_value('app.features.enable_keyboard_listener', enable_keyboard)
|
||||||
|
|
||||||
|
# 保存米电阻设置
|
||||||
|
mdz_port = self.mdz_port_combo.currentData()
|
||||||
|
mdz_baud = int(self.mdz_baud_combo.currentText())
|
||||||
|
mdz_data_bits = int(self.mdz_data_bits_combo.currentText())
|
||||||
|
mdz_stop_bits = float(self.mdz_stop_bits_combo.currentText())
|
||||||
|
mdz_parity = self.mdz_parity_combo.currentData()
|
||||||
|
mdz_query_cmd = self.mdz_query_cmd.text().strip()
|
||||||
|
mdz_query_interval = self.mdz_query_interval.value()
|
||||||
|
|
||||||
|
mdz_config = {
|
||||||
|
'port': mdz_baud,
|
||||||
|
'data_bits': mdz_data_bits,
|
||||||
|
'stop_bits': mdz_stop_bits,
|
||||||
|
'parity': mdz_parity,
|
||||||
|
'query_cmd': mdz_query_cmd,
|
||||||
|
'query_interval': mdz_query_interval
|
||||||
|
}
|
||||||
|
|
||||||
|
# 只有当用户选择了串口时才保存串口配置
|
||||||
|
if mdz_port:
|
||||||
|
mdz_config['ser'] = mdz_port
|
||||||
|
|
||||||
|
self.config.set_config('mdz', mdz_config)
|
||||||
|
|
||||||
|
# 保存线径设置
|
||||||
|
xj_port = self.xj_port_combo.currentData()
|
||||||
|
xj_baud = int(self.xj_baud_combo.currentText())
|
||||||
|
xj_data_bits = int(self.xj_data_bits_combo.currentText())
|
||||||
|
xj_stop_bits = float(self.xj_stop_bits_combo.currentText())
|
||||||
|
xj_parity = self.xj_parity_combo.currentData()
|
||||||
|
|
||||||
|
xj_config = {
|
||||||
|
'port': xj_baud,
|
||||||
|
'data_bits': xj_data_bits,
|
||||||
|
'stop_bits': xj_stop_bits,
|
||||||
|
'parity': xj_parity
|
||||||
|
}
|
||||||
|
|
||||||
|
# 只有当用户选择了串口时才保存串口配置
|
||||||
|
if xj_port:
|
||||||
|
xj_config['ser'] = xj_port
|
||||||
|
|
||||||
|
self.config.set_config('xj', xj_config)
|
||||||
|
|
||||||
|
# 保存扫码器设置
|
||||||
|
scanner_port = self.scanner_port_combo.currentData()
|
||||||
|
scanner_baud = int(self.scanner_baud_combo.currentText())
|
||||||
|
scanner_data_bits = int(self.scanner_data_bits_combo.currentText())
|
||||||
|
scanner_stop_bits = float(self.scanner_stop_bits_combo.currentText())
|
||||||
|
scanner_parity = self.scanner_parity_combo.currentData()
|
||||||
|
|
||||||
|
scanner_config = {
|
||||||
|
'port': scanner_baud,
|
||||||
|
'data_bits': scanner_data_bits,
|
||||||
|
'stop_bits': scanner_stop_bits,
|
||||||
|
'parity': scanner_parity
|
||||||
|
}
|
||||||
|
|
||||||
|
# 只有当用户选择了串口时才保存串口配置
|
||||||
|
if scanner_port:
|
||||||
|
scanner_config['ser'] = scanner_port
|
||||||
|
|
||||||
|
self.config.set_config('scanner', scanner_config)
|
||||||
|
|
||||||
|
# 发送设置变更信号
|
||||||
|
self.settings_changed.emit()
|
||||||
|
|
||||||
|
QMessageBox.information(self, "保存成功", "串口设置已保存")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"保存串口设置失败: {e}")
|
||||||
|
QMessageBox.critical(self, "保存失败", f"保存串口设置失败: {e}")
|
||||||
|
|
||||||
|
def test_mdz_port(self):
|
||||||
|
"""测试米电阻串口"""
|
||||||
|
try:
|
||||||
|
port = self.mdz_port_combo.currentData()
|
||||||
|
|
||||||
|
if not port:
|
||||||
|
QMessageBox.warning(self, "测试失败", "请先选择串口,当前设置为\"不使用\"")
|
||||||
|
return
|
||||||
|
|
||||||
|
baud = int(self.mdz_baud_combo.currentText())
|
||||||
|
data_bits = int(self.mdz_data_bits_combo.currentText())
|
||||||
|
stop_bits = float(self.mdz_stop_bits_combo.currentText())
|
||||||
|
parity = self.mdz_parity_combo.currentData()
|
||||||
|
|
||||||
|
# 关闭可能已经打开的串口
|
||||||
|
if self.serial_manager.is_port_open(port):
|
||||||
|
self.serial_manager.close_port(port)
|
||||||
|
|
||||||
|
# 尝试打开串口
|
||||||
|
success = self.serial_manager.open_port(
|
||||||
|
port, 'mdz', baud, data_bits, stop_bits, parity, 1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
# 尝试发送查询指令
|
||||||
|
query_cmd = self.mdz_query_cmd.text()
|
||||||
|
if query_cmd:
|
||||||
|
try:
|
||||||
|
# 转换查询指令为字节
|
||||||
|
cmd_bytes = bytes.fromhex(query_cmd.replace(' ', ''))
|
||||||
|
self.serial_manager.write_data(port, cmd_bytes)
|
||||||
|
time.sleep(0.1) # 等待响应
|
||||||
|
|
||||||
|
# 读取响应
|
||||||
|
response = self.serial_manager.read_data(port)
|
||||||
|
if response:
|
||||||
|
# 将字节转换为十六进制字符串
|
||||||
|
hex_str = ' '.join(f'{b:02X}' for b in response)
|
||||||
|
QMessageBox.information(self, "测试成功", f"串口打开成功,收到响应:\n{hex_str}")
|
||||||
|
else:
|
||||||
|
QMessageBox.information(self, "测试成功", "串口打开成功,但未收到响应")
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.warning(self, "测试结果", f"串口打开成功,但发送指令失败: {e}")
|
||||||
|
else:
|
||||||
|
QMessageBox.information(self, "测试成功", "串口打开成功")
|
||||||
|
|
||||||
|
# 关闭串口
|
||||||
|
self.serial_manager.close_port(port)
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(self, "测试失败", f"无法打开串口 {port}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"测试米电阻串口失败: {e}")
|
||||||
|
QMessageBox.critical(self, "测试失败", f"测试米电阻串口失败: {e}")
|
||||||
|
|
||||||
|
def test_xj_port(self):
|
||||||
|
"""测试线径串口"""
|
||||||
|
try:
|
||||||
|
port = self.xj_port_combo.currentData()
|
||||||
|
|
||||||
|
if not port:
|
||||||
|
QMessageBox.warning(self, "测试失败", "请先选择串口,当前设置为\"不使用\"")
|
||||||
|
return
|
||||||
|
|
||||||
|
baud = int(self.xj_baud_combo.currentText())
|
||||||
|
data_bits = int(self.xj_data_bits_combo.currentText())
|
||||||
|
stop_bits = float(self.xj_stop_bits_combo.currentText())
|
||||||
|
parity = self.xj_parity_combo.currentData()
|
||||||
|
|
||||||
|
# 关闭可能已经打开的串口
|
||||||
|
if self.serial_manager.is_port_open(port):
|
||||||
|
self.serial_manager.close_port(port)
|
||||||
|
|
||||||
|
# 尝试打开串口
|
||||||
|
success = self.serial_manager.open_port(
|
||||||
|
port, 'xj', baud, data_bits, stop_bits, parity, 1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
QMessageBox.information(self, "测试成功", f"串口 {port} 打开成功")
|
||||||
|
|
||||||
|
# 关闭串口
|
||||||
|
self.serial_manager.close_port(port)
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(self, "测试失败", f"无法打开串口 {port}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"测试线径串口失败: {e}")
|
||||||
|
QMessageBox.critical(self, "测试失败", f"测试线径串口失败: {e}")
|
||||||
|
|
||||||
|
def test_scanner_port(self):
|
||||||
|
"""测试扫码器串口"""
|
||||||
|
try:
|
||||||
|
port = self.scanner_port_combo.currentData()
|
||||||
|
|
||||||
|
if not port:
|
||||||
|
QMessageBox.warning(self, "测试失败", "请先选择串口,当前设置为\"不使用\"")
|
||||||
|
return
|
||||||
|
|
||||||
|
baud = int(self.scanner_baud_combo.currentText())
|
||||||
|
data_bits = int(self.scanner_data_bits_combo.currentText())
|
||||||
|
stop_bits = float(self.scanner_stop_bits_combo.currentText())
|
||||||
|
parity = self.scanner_parity_combo.currentData()
|
||||||
|
|
||||||
|
# 关闭可能已经打开的串口
|
||||||
|
if self.serial_manager.is_port_open(port):
|
||||||
|
self.serial_manager.close_port(port)
|
||||||
|
|
||||||
|
# 创建临时回调函数,用于测试期间接收扫码器数据
|
||||||
|
def scanner_callback(port_name, data):
|
||||||
|
try:
|
||||||
|
# 尝试将字节解码为字符串
|
||||||
|
try:
|
||||||
|
text = data.decode('utf-8').strip()
|
||||||
|
# 如果数据以"扫码数据: "开头,提取实际数据部分
|
||||||
|
if text.startswith("扫码数据: "):
|
||||||
|
text = text[6:].strip()
|
||||||
|
QMessageBox.information(self, "测试成功", f"收到扫码数据:\n{text}")
|
||||||
|
except:
|
||||||
|
# 如果解码失败,显示十六进制
|
||||||
|
hex_str = ' '.join(f'{b:02X}' for b in data)
|
||||||
|
QMessageBox.information(self, "测试成功", f"收到扫码数据 (十六进制):\n{hex_str}")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"处理扫码回调数据失败: {e}")
|
||||||
|
|
||||||
|
# 保存原始回调
|
||||||
|
original_callback = self.serial_manager.callbacks.get('scanner_data', None)
|
||||||
|
|
||||||
|
# 设置临时回调
|
||||||
|
self.serial_manager.callbacks['scanner_data'] = scanner_callback
|
||||||
|
|
||||||
|
# 尝试打开串口
|
||||||
|
success = self.serial_manager.open_port(
|
||||||
|
port, 'scanner', baud, data_bits, stop_bits, parity, 1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
QMessageBox.information(self, "测试成功", f"串口 {port} 打开成功\n请触发扫码器进行扫描测试")
|
||||||
|
|
||||||
|
# 等待用户操作扫码器(最多等待10秒)
|
||||||
|
start_time = time.time()
|
||||||
|
timeout = 10.0 # 10秒超时
|
||||||
|
|
||||||
|
while time.time() - start_time < timeout:
|
||||||
|
# 使用QApplication处理事件,保持UI响应
|
||||||
|
QApplication.processEvents()
|
||||||
|
time.sleep(0.1) # 短暂休眠,减少CPU占用
|
||||||
|
|
||||||
|
# 关闭串口
|
||||||
|
self.serial_manager.close_port(port)
|
||||||
|
|
||||||
|
# 恢复原始回调
|
||||||
|
if original_callback:
|
||||||
|
self.serial_manager.callbacks['scanner_data'] = original_callback
|
||||||
|
else:
|
||||||
|
# 如果之前没有回调,则删除临时回调
|
||||||
|
if 'scanner_data' in self.serial_manager.callbacks:
|
||||||
|
del self.serial_manager.callbacks['scanner_data']
|
||||||
|
|
||||||
|
QMessageBox.information(self, "测试完成", f"串口 {port} 已关闭")
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(self, "测试失败", f"无法打开串口 {port}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"测试扫码器串口失败: {e}")
|
||||||
|
QMessageBox.critical(self, "测试失败", f"测试扫码器串口失败: {e}")
|
||||||
@ -20,12 +20,27 @@ class SerialSettingsUI(QWidget):
|
|||||||
main_layout = QVBoxLayout(self)
|
main_layout = QVBoxLayout(self)
|
||||||
|
|
||||||
# 创建全局启用选项
|
# 创建全局启用选项
|
||||||
enable_layout = QHBoxLayout()
|
enable_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# 第一行:串口功能和键盘监听
|
||||||
|
enable_row1 = QHBoxLayout()
|
||||||
self.enable_serial_checkbox = QCheckBox("启用串口功能")
|
self.enable_serial_checkbox = QCheckBox("启用串口功能")
|
||||||
self.enable_keyboard_checkbox = QCheckBox("启用键盘监听 (PageUp 触发米电阻查询)")
|
self.enable_keyboard_checkbox = QCheckBox("启用键盘监听")
|
||||||
enable_layout.addWidget(self.enable_serial_checkbox)
|
enable_row1.addWidget(self.enable_serial_checkbox)
|
||||||
enable_layout.addWidget(self.enable_keyboard_checkbox)
|
enable_row1.addWidget(self.enable_keyboard_checkbox)
|
||||||
enable_layout.addStretch()
|
enable_row1.addStretch()
|
||||||
|
|
||||||
|
# 第二行:键盘快捷键说明
|
||||||
|
enable_row2 = QHBoxLayout()
|
||||||
|
key_info_label = QLabel("键盘快捷键: PageUp 触发米电阻查询")
|
||||||
|
key_info_label.setStyleSheet("color: #666; font-style: italic;")
|
||||||
|
enable_row2.addWidget(key_info_label)
|
||||||
|
enable_row2.addStretch()
|
||||||
|
|
||||||
|
# 添加到垂直布局
|
||||||
|
enable_layout.addLayout(enable_row1)
|
||||||
|
enable_layout.addLayout(enable_row2)
|
||||||
|
|
||||||
main_layout.addLayout(enable_layout)
|
main_layout.addLayout(enable_layout)
|
||||||
|
|
||||||
# # 创建串口设置组
|
# # 创建串口设置组
|
||||||
@ -116,6 +131,20 @@ class SerialSettingsUI(QWidget):
|
|||||||
self.xj_parity_combo.addItem(parity[0], parity[1])
|
self.xj_parity_combo.addItem(parity[0], parity[1])
|
||||||
xj_layout.addRow("校验位:", self.xj_parity_combo)
|
xj_layout.addRow("校验位:", self.xj_parity_combo)
|
||||||
|
|
||||||
|
# 查询指令
|
||||||
|
self.xj_query_cmd = QLineEdit()
|
||||||
|
xj_layout.addRow("查询指令:", self.xj_query_cmd)
|
||||||
|
|
||||||
|
# 查询间隔
|
||||||
|
self.xj_query_interval = QSpinBox()
|
||||||
|
self.xj_query_interval.setRange(1, 60)
|
||||||
|
self.xj_query_interval.setSuffix(" 秒")
|
||||||
|
xj_layout.addRow("查询间隔:", self.xj_query_interval)
|
||||||
|
|
||||||
|
# 自动查询
|
||||||
|
self.xj_auto_query = QCheckBox("启用自动查询")
|
||||||
|
xj_layout.addRow("", self.xj_auto_query)
|
||||||
|
|
||||||
# 扫码器串口设置
|
# 扫码器串口设置
|
||||||
scanner_group = QGroupBox("扫码器串口")
|
scanner_group = QGroupBox("扫码器串口")
|
||||||
scanner_layout = QFormLayout(scanner_group)
|
scanner_layout = QFormLayout(scanner_group)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QWidget, QLabel, QLineEdit, QPushButton, QComboBox, QGridLayout, QHBoxLayout, QVBoxLayout,
|
QWidget, QLabel, QLineEdit, QPushButton, QComboBox, QGridLayout, QHBoxLayout, QVBoxLayout,
|
||||||
QTabWidget, QFrame, QFormLayout, QGroupBox, QRadioButton, QSpacerItem, QSizePolicy,
|
QTabWidget, QFrame, QFormLayout, QGroupBox, QRadioButton, QSpacerItem, QSizePolicy,
|
||||||
QTableWidget, QTableWidgetItem, QHeaderView, QSlider, QCheckBox, QButtonGroup
|
QTableWidget, QTableWidgetItem, QHeaderView, QSlider, QCheckBox, QButtonGroup, QFileDialog
|
||||||
)
|
)
|
||||||
from PySide6.QtGui import QFont, QBrush, QColor, QIntValidator
|
from PySide6.QtGui import QFont, QBrush, QColor, QIntValidator
|
||||||
from PySide6.QtCore import Qt, Signal, QSize
|
from PySide6.QtCore import Qt, Signal, QSize
|
||||||
@ -159,6 +159,72 @@ class SettingsUI(QWidget):
|
|||||||
self.camera_params_group.setLayout(self.camera_params_layout)
|
self.camera_params_group.setLayout(self.camera_params_layout)
|
||||||
self.camera_layout.addWidget(self.camera_params_group)
|
self.camera_layout.addWidget(self.camera_params_group)
|
||||||
|
|
||||||
|
# 本地图像模式区域 (新增)
|
||||||
|
self.local_mode_group = QGroupBox("本地图像模式")
|
||||||
|
self.local_mode_group.setFont(self.normal_font)
|
||||||
|
self.local_mode_layout = QGridLayout()
|
||||||
|
|
||||||
|
# 启用本地模式选项
|
||||||
|
self.local_mode_check = QCheckBox("启用本地图像模式")
|
||||||
|
self.local_mode_check.setFont(self.normal_font)
|
||||||
|
|
||||||
|
# 文件夹路径
|
||||||
|
self.folder_path_label = QLabel("图像文件夹:")
|
||||||
|
self.folder_path_label.setFont(self.normal_font)
|
||||||
|
self.folder_path_edit = QLineEdit()
|
||||||
|
self.folder_path_edit.setReadOnly(True)
|
||||||
|
self.folder_path_edit.setMinimumWidth(300)
|
||||||
|
self.folder_path_button = QPushButton("选择文件夹")
|
||||||
|
self.folder_path_button.setFont(self.normal_font)
|
||||||
|
|
||||||
|
# 本地模式帧率
|
||||||
|
self.local_framerate_label = QLabel("播放帧率:")
|
||||||
|
self.local_framerate_label.setFont(self.normal_font)
|
||||||
|
self.local_framerate_slider = QSlider(Qt.Horizontal)
|
||||||
|
self.local_framerate_slider.setMinimum(1)
|
||||||
|
self.local_framerate_slider.setMaximum(30)
|
||||||
|
self.local_framerate_slider.setValue(15)
|
||||||
|
self.local_framerate_slider.setTickPosition(QSlider.TicksBelow)
|
||||||
|
self.local_framerate_slider.setTickInterval(5)
|
||||||
|
self.local_framerate_value = QLabel("15 fps")
|
||||||
|
self.local_framerate_value.setFont(self.normal_font)
|
||||||
|
self.local_framerate_value.setMinimumWidth(80)
|
||||||
|
|
||||||
|
# 循环播放选项
|
||||||
|
self.loop_playback_check = QCheckBox("循环播放")
|
||||||
|
self.loop_playback_check.setFont(self.normal_font)
|
||||||
|
self.loop_playback_check.setChecked(True)
|
||||||
|
|
||||||
|
# 播放控制按钮
|
||||||
|
self.play_button = QPushButton("播放")
|
||||||
|
self.play_button.setFont(self.normal_font)
|
||||||
|
self.stop_button = QPushButton("停止")
|
||||||
|
self.stop_button.setFont(self.normal_font)
|
||||||
|
|
||||||
|
# 添加到布局
|
||||||
|
self.local_mode_layout.addWidget(self.local_mode_check, 0, 0, 1, 3)
|
||||||
|
|
||||||
|
self.local_mode_layout.addWidget(self.folder_path_label, 1, 0)
|
||||||
|
folder_path_layout = QHBoxLayout()
|
||||||
|
folder_path_layout.addWidget(self.folder_path_edit)
|
||||||
|
folder_path_layout.addWidget(self.folder_path_button)
|
||||||
|
self.local_mode_layout.addLayout(folder_path_layout, 1, 1, 1, 2)
|
||||||
|
|
||||||
|
self.local_mode_layout.addWidget(self.local_framerate_label, 2, 0)
|
||||||
|
self.local_mode_layout.addWidget(self.local_framerate_slider, 2, 1)
|
||||||
|
self.local_mode_layout.addWidget(self.local_framerate_value, 2, 2)
|
||||||
|
|
||||||
|
self.local_mode_layout.addWidget(self.loop_playback_check, 3, 0)
|
||||||
|
|
||||||
|
playback_layout = QHBoxLayout()
|
||||||
|
playback_layout.addWidget(self.play_button)
|
||||||
|
playback_layout.addWidget(self.stop_button)
|
||||||
|
playback_layout.addStretch(1)
|
||||||
|
self.local_mode_layout.addLayout(playback_layout, 4, 1, 1, 2)
|
||||||
|
|
||||||
|
self.local_mode_group.setLayout(self.local_mode_layout)
|
||||||
|
self.camera_layout.addWidget(self.local_mode_group)
|
||||||
|
|
||||||
# 相机设置按钮
|
# 相机设置按钮
|
||||||
self.camera_buttons_layout = QHBoxLayout()
|
self.camera_buttons_layout = QHBoxLayout()
|
||||||
|
|
||||||
@ -192,7 +258,14 @@ class SettingsUI(QWidget):
|
|||||||
self.preview_frame.setMinimumHeight(200)
|
self.preview_frame.setMinimumHeight(200)
|
||||||
self.preview_frame.setStyleSheet("background-color: black;")
|
self.preview_frame.setStyleSheet("background-color: black;")
|
||||||
|
|
||||||
|
# 添加预览状态标签
|
||||||
|
self.preview_status = QLabel("就绪")
|
||||||
|
self.preview_status.setAlignment(Qt.AlignCenter)
|
||||||
|
self.preview_status.setFont(self.small_font)
|
||||||
|
self.preview_status.setStyleSheet("color: #888888;")
|
||||||
|
|
||||||
self.preview_layout.addWidget(self.preview_frame)
|
self.preview_layout.addWidget(self.preview_frame)
|
||||||
|
self.preview_layout.addWidget(self.preview_status)
|
||||||
self.preview_group.setLayout(self.preview_layout)
|
self.preview_group.setLayout(self.preview_layout)
|
||||||
self.camera_layout.addWidget(self.preview_group)
|
self.camera_layout.addWidget(self.preview_group)
|
||||||
|
|
||||||
|
|||||||
265
utils/local_image_player.py
Normal file
265
utils/local_image_player.py
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
import os
|
||||||
|
import cv2
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import logging
|
||||||
|
import numpy as np
|
||||||
|
from glob import glob
|
||||||
|
from datetime import datetime
|
||||||
|
from PySide6.QtCore import QObject, Signal
|
||||||
|
|
||||||
|
class LocalImagePlayer(QObject):
|
||||||
|
"""本地图像播放器,用于读取和播放本地图像序列
|
||||||
|
|
||||||
|
特点:
|
||||||
|
1. 异步加载图像,不阻塞UI线程
|
||||||
|
2. 可调整帧率
|
||||||
|
3. 可循环播放
|
||||||
|
4. 支持暂停和恢复
|
||||||
|
5. 自动排序图像按时间序列播放
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 信号定义
|
||||||
|
signal_frame_ready = Signal(object) # 帧准备好信号 (frame)
|
||||||
|
signal_status = Signal(str) # 状态信号 (message)
|
||||||
|
signal_progress = Signal(int, int) # 进度信号 (current, total)
|
||||||
|
signal_error = Signal(str) # 错误信号 (message)
|
||||||
|
signal_completed = Signal() # 播放完成信号
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
# 初始化状态变量
|
||||||
|
self.folder_path = "" # 图像文件夹路径
|
||||||
|
self.file_patterns = [".jpg", ".jpeg", ".png", ".bmp"] # 支持的图像格式
|
||||||
|
self.framerate = 15 # 播放帧率
|
||||||
|
self.loop = True # 是否循环播放
|
||||||
|
self.images = [] # 图像文件列表
|
||||||
|
self.current_index = 0 # 当前播放索引
|
||||||
|
|
||||||
|
# 播放控制
|
||||||
|
self.is_playing = False # 是否正在播放
|
||||||
|
self.is_paused = False # 是否暂停
|
||||||
|
self.stop_event = threading.Event() # 停止事件
|
||||||
|
self.playback_thread = None # 播放线程
|
||||||
|
|
||||||
|
# 加载线程
|
||||||
|
self.loading_thread = None
|
||||||
|
self.is_loading = False
|
||||||
|
|
||||||
|
def set_folder(self, folder_path):
|
||||||
|
"""设置图像文件夹路径
|
||||||
|
|
||||||
|
Args:
|
||||||
|
folder_path: 图像文件夹路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功设置
|
||||||
|
"""
|
||||||
|
if not os.path.exists(folder_path) or not os.path.isdir(folder_path):
|
||||||
|
self.signal_error.emit(f"文件夹不存在: {folder_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.folder_path = folder_path
|
||||||
|
|
||||||
|
# 异步加载图像列表
|
||||||
|
self.load_images_async()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def set_file_patterns(self, patterns):
|
||||||
|
"""设置图像文件格式
|
||||||
|
|
||||||
|
Args:
|
||||||
|
patterns: 支持的图像后缀列表,如['.jpg', '.png']
|
||||||
|
"""
|
||||||
|
if patterns and isinstance(patterns, list):
|
||||||
|
self.file_patterns = patterns
|
||||||
|
|
||||||
|
def set_framerate(self, framerate):
|
||||||
|
"""设置播放帧率
|
||||||
|
|
||||||
|
Args:
|
||||||
|
framerate: 帧率,范围1-60
|
||||||
|
"""
|
||||||
|
if 1 <= framerate <= 60:
|
||||||
|
self.framerate = framerate
|
||||||
|
|
||||||
|
def set_loop(self, loop):
|
||||||
|
"""设置是否循环播放
|
||||||
|
|
||||||
|
Args:
|
||||||
|
loop: 是否循环播放
|
||||||
|
"""
|
||||||
|
self.loop = loop
|
||||||
|
|
||||||
|
def load_images_async(self):
|
||||||
|
"""异步加载图像列表"""
|
||||||
|
if self.is_loading:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.is_loading = True
|
||||||
|
self.loading_thread = threading.Thread(target=self._load_images_thread)
|
||||||
|
self.loading_thread.daemon = True
|
||||||
|
self.loading_thread.start()
|
||||||
|
|
||||||
|
def _load_images_thread(self):
|
||||||
|
"""加载图像列表的线程函数"""
|
||||||
|
try:
|
||||||
|
self.signal_status.emit("正在加载图像...")
|
||||||
|
|
||||||
|
# 获取所有支持格式的图像文件
|
||||||
|
image_files = []
|
||||||
|
for pattern in self.file_patterns:
|
||||||
|
# 确保pattern是以.开头的扩展名
|
||||||
|
if not pattern.startswith('.'):
|
||||||
|
pattern = '.' + pattern
|
||||||
|
# 查找所有匹配的文件
|
||||||
|
pattern_files = glob(os.path.join(self.folder_path, f"*{pattern}"))
|
||||||
|
image_files.extend(pattern_files)
|
||||||
|
|
||||||
|
# 按文件名排序(通常包含时间信息)
|
||||||
|
image_files.sort()
|
||||||
|
|
||||||
|
if not image_files:
|
||||||
|
self.signal_error.emit(f"未找到图像文件,支持的格式: {', '.join(self.file_patterns)}")
|
||||||
|
self.is_loading = False
|
||||||
|
return
|
||||||
|
|
||||||
|
# 更新图像列表
|
||||||
|
self.images = image_files
|
||||||
|
self.current_index = 0
|
||||||
|
|
||||||
|
# 发出加载完成信号
|
||||||
|
self.signal_status.emit(f"已加载 {len(self.images)} 张图像")
|
||||||
|
self.signal_progress.emit(0, len(self.images))
|
||||||
|
|
||||||
|
logging.info(f"已加载 {len(self.images)} 张图像,从 {self.folder_path}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"加载图像时发生错误: {e}")
|
||||||
|
self.signal_error.emit(f"加载图像失败: {str(e)}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.is_loading = False
|
||||||
|
|
||||||
|
def start_playback(self):
|
||||||
|
"""开始播放图像序列"""
|
||||||
|
if not self.images:
|
||||||
|
self.signal_error.emit("没有可播放的图像")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.is_playing:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 重置停止事件
|
||||||
|
self.stop_event.clear()
|
||||||
|
self.is_playing = True
|
||||||
|
self.is_paused = False
|
||||||
|
|
||||||
|
# 创建播放线程
|
||||||
|
self.playback_thread = threading.Thread(target=self._playback_thread)
|
||||||
|
self.playback_thread.daemon = True
|
||||||
|
self.playback_thread.start()
|
||||||
|
|
||||||
|
logging.info("开始播放本地图像序列")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def pause_playback(self):
|
||||||
|
"""暂停播放"""
|
||||||
|
self.is_paused = True
|
||||||
|
logging.info("暂停播放本地图像序列")
|
||||||
|
|
||||||
|
def resume_playback(self):
|
||||||
|
"""恢复播放"""
|
||||||
|
self.is_paused = False
|
||||||
|
logging.info("恢复播放本地图像序列")
|
||||||
|
|
||||||
|
def stop_playback(self):
|
||||||
|
"""停止播放"""
|
||||||
|
if not self.is_playing:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 设置停止事件
|
||||||
|
self.stop_event.set()
|
||||||
|
|
||||||
|
# 等待线程结束
|
||||||
|
if self.playback_thread and self.playback_thread.is_alive():
|
||||||
|
self.playback_thread.join(timeout=1.0)
|
||||||
|
|
||||||
|
self.is_playing = False
|
||||||
|
self.is_paused = False
|
||||||
|
logging.info("停止播放本地图像序列")
|
||||||
|
|
||||||
|
def _playback_thread(self):
|
||||||
|
"""播放线程函数"""
|
||||||
|
try:
|
||||||
|
frame_interval = 1.0 / self.framerate
|
||||||
|
logging.warning(f"====> 播放线程开始,帧率: {self.framerate} fps,帧间隔: {frame_interval} 秒")
|
||||||
|
|
||||||
|
while not self.stop_event.is_set():
|
||||||
|
if self.is_paused:
|
||||||
|
time.sleep(0.1) # 暂停时降低CPU使用率
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 记录帧开始时间
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
# 获取当前帧
|
||||||
|
if 0 <= self.current_index < len(self.images):
|
||||||
|
image_path = self.images[self.current_index]
|
||||||
|
frame = cv2.imread(image_path)
|
||||||
|
|
||||||
|
if frame is not None:
|
||||||
|
# OpenCV以BGR格式读取,需要转换为RGB才适合大多数显示
|
||||||
|
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||||
|
|
||||||
|
# 输出调试信息
|
||||||
|
logging.warning(f"====> 读取图像: {image_path}, 尺寸: {frame_rgb.shape}")
|
||||||
|
|
||||||
|
# 保存最后一帧,以便重新连接时使用
|
||||||
|
self.last_frame = frame_rgb
|
||||||
|
|
||||||
|
# 发送帧准备好信号
|
||||||
|
self.signal_frame_ready.emit(frame_rgb)
|
||||||
|
|
||||||
|
# 确保不使用cv2.imshow
|
||||||
|
# 禁用任何可能的cv2.imshow调用
|
||||||
|
|
||||||
|
# 更新进度
|
||||||
|
self.signal_progress.emit(self.current_index + 1, len(self.images))
|
||||||
|
else:
|
||||||
|
logging.error(f"无法读取图像文件: {image_path}")
|
||||||
|
self.signal_error.emit(f"无法读取图像: {os.path.basename(image_path)}")
|
||||||
|
|
||||||
|
# 更新索引
|
||||||
|
self.current_index += 1
|
||||||
|
|
||||||
|
# 检查是否播放结束
|
||||||
|
if self.current_index >= len(self.images):
|
||||||
|
if self.loop:
|
||||||
|
self.current_index = 0
|
||||||
|
logging.debug("本地图像序列播放完成,循环播放")
|
||||||
|
else:
|
||||||
|
logging.info("本地图像序列播放完成")
|
||||||
|
self.signal_completed.emit()
|
||||||
|
break
|
||||||
|
|
||||||
|
# 计算需要等待的时间
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
sleep_time = max(0, frame_interval - elapsed)
|
||||||
|
|
||||||
|
# 输出调试信息
|
||||||
|
logging.debug(f"帧处理时间: {elapsed:.4f}秒,等待时间: {sleep_time:.4f}秒")
|
||||||
|
|
||||||
|
# 等待直到下一帧时间或停止事件被设置
|
||||||
|
if sleep_time > 0:
|
||||||
|
self.stop_event.wait(sleep_time)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"播放图像序列时发生错误: {e}")
|
||||||
|
self.signal_error.emit(f"播放错误: {str(e)}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.is_playing = False
|
||||||
|
logging.warning("====> 播放线程结束")
|
||||||
@ -62,6 +62,9 @@ class SerialManager:
|
|||||||
# 是否自动查询米电阻数据,默认为False,只通过PageUp键触发
|
# 是否自动查询米电阻数据,默认为False,只通过PageUp键触发
|
||||||
self.auto_query_mdz = False
|
self.auto_query_mdz = False
|
||||||
|
|
||||||
|
# 是否自动查询线径数据,默认为True,开启自动查询
|
||||||
|
self.auto_query_xj = True
|
||||||
|
|
||||||
logging.info("初始化 SerialManager")
|
logging.info("初始化 SerialManager")
|
||||||
|
|
||||||
# 加载配置
|
# 加载配置
|
||||||
@ -73,11 +76,13 @@ class SerialManager:
|
|||||||
try:
|
try:
|
||||||
# 初始化键盘监听器
|
# 初始化键盘监听器
|
||||||
self.keyboard_listener = KeyboardListener()
|
self.keyboard_listener = KeyboardListener()
|
||||||
# 从配置中获取触发键
|
|
||||||
|
# 从配置中获取米电阻触发键
|
||||||
trigger_key = self.config.get_value('serial.keyboard.trigger_key', 'Key.page_up')
|
trigger_key = self.config.get_value('serial.keyboard.trigger_key', 'Key.page_up')
|
||||||
# 注册触发键回调
|
# 注册米电阻触发键回调
|
||||||
self.keyboard_listener.register_callback(trigger_key, self.trigger_resistance_query)
|
self.keyboard_listener.register_callback(trigger_key, self.trigger_resistance_query)
|
||||||
logging.info(f"已注册{trigger_key}按键回调用于触发米电阻查询")
|
logging.info(f"已注册{trigger_key}按键回调用于触发米电阻查询")
|
||||||
|
|
||||||
# 注意:不在这里启动键盘监听器,而是在点击"开始"按钮时启动
|
# 注意:不在这里启动键盘监听器,而是在点击"开始"按钮时启动
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"初始化键盘监听器失败: {e}")
|
logging.error(f"初始化键盘监听器失败: {e}")
|
||||||
@ -122,10 +127,14 @@ class SerialManager:
|
|||||||
self.stable_threshold = self.cz_config.get('stable_threshold', 10) if self.cz_config else 10
|
self.stable_threshold = self.cz_config.get('stable_threshold', 10) if self.cz_config else 10
|
||||||
|
|
||||||
# 检查是否自动查询米电阻数据
|
# 检查是否自动查询米电阻数据
|
||||||
self.auto_query_mdz = self.config.get_value('serial.keyboard.auto_query', False)
|
self.auto_query_mdz = self.config.get_value('serial.mdz.auto_query', False)
|
||||||
|
|
||||||
|
# 检查是否自动查询线径数据
|
||||||
|
self.auto_query_xj = self.config.get_value('serial.xj.auto_query', True) # 默认为True,确保线径自动查询开启
|
||||||
|
|
||||||
logging.info(f"已加载串口配置:mdz={self.mdz_config}, xj={self.xj_config}, cz={self.cz_config}, scanner={self.scanner_config}, data_file={self.data_file}")
|
logging.info(f"已加载串口配置:mdz={self.mdz_config}, xj={self.xj_config}, cz={self.cz_config}, scanner={self.scanner_config}, data_file={self.data_file}")
|
||||||
logging.info(f"米电阻自动查询: {'开启' if self.auto_query_mdz else '关闭'}")
|
logging.info(f"米电阻自动查询: {'开启' if self.auto_query_mdz else '关闭'}")
|
||||||
|
logging.info(f"线径自动查询: {'开启' if self.auto_query_xj else '关闭'}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"加载配置出错: {e}")
|
logging.error(f"加载配置出错: {e}")
|
||||||
# 设置默认值
|
# 设置默认值
|
||||||
@ -136,6 +145,7 @@ class SerialManager:
|
|||||||
self.scanner_config = {'port': 9600, 'ser': 'COM4'}
|
self.scanner_config = {'port': 9600, 'ser': 'COM4'}
|
||||||
self.stable_threshold = 10
|
self.stable_threshold = 10
|
||||||
self.auto_query_mdz = False
|
self.auto_query_mdz = False
|
||||||
|
self.auto_query_xj = True # 默认为True,确保线径自动查询开启
|
||||||
logging.info(f"使用默认配置,数据文件: {self.data_file}")
|
logging.info(f"使用默认配置,数据文件: {self.data_file}")
|
||||||
|
|
||||||
def _detect_macos_ports(self):
|
def _detect_macos_ports(self):
|
||||||
@ -717,26 +727,22 @@ class SerialManager:
|
|||||||
def start_keyboard_listener(self):
|
def start_keyboard_listener(self):
|
||||||
"""启动键盘监听"""
|
"""启动键盘监听"""
|
||||||
try:
|
try:
|
||||||
# 检查是否启用键盘监听功能
|
|
||||||
enable_keyboard_listener = self.config.get_value('app.features.enable_keyboard_listener', False)
|
|
||||||
if not enable_keyboard_listener:
|
|
||||||
logging.info("键盘监听功能已在配置中禁用,跳过启动键盘监听")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 检查键盘监听器是否已初始化
|
# 检查键盘监听器是否已初始化
|
||||||
if self.keyboard_listener is None:
|
if self.keyboard_listener is None:
|
||||||
logging.warning("键盘监听器未初始化,无法启动")
|
logging.warning("键盘监听器未初始化,无法启动")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 从配置中获取触发键
|
# 从配置中获取米电阻触发键
|
||||||
config = ConfigLoader.get_instance()
|
trigger_key = self.config.get_value('serial.keyboard.trigger_key', 'Key.page_up')
|
||||||
trigger_key = config.get_value('serial.keyboard.trigger_key', 'Key.page_up')
|
# 不再获取线径触发键
|
||||||
|
|
||||||
# 确保已注册触发键回调
|
# 确保已注册米电阻触发键回调
|
||||||
if trigger_key not in self.keyboard_listener.callbacks:
|
if trigger_key not in self.keyboard_listener.callbacks:
|
||||||
self.keyboard_listener.register_callback(trigger_key, self.trigger_resistance_query)
|
self.keyboard_listener.register_callback(trigger_key, self.trigger_resistance_query)
|
||||||
logging.info(f"已注册{trigger_key}按键回调用于触发米电阻数据查询")
|
logging.info(f"已注册{trigger_key}按键回调用于触发米电阻数据查询")
|
||||||
|
|
||||||
|
# 移除线径触发键回调注册代码
|
||||||
|
|
||||||
# 启动键盘监听
|
# 启动键盘监听
|
||||||
result = self.keyboard_listener.start()
|
result = self.keyboard_listener.start()
|
||||||
if result:
|
if result:
|
||||||
@ -790,123 +796,56 @@ class SerialManager:
|
|||||||
if key in self.callbacks:
|
if key in self.callbacks:
|
||||||
logging.warning(f"覆盖已存在的回调函数: {key}")
|
logging.warning(f"覆盖已存在的回调函数: {key}")
|
||||||
self.callbacks[key] = callback
|
self.callbacks[key] = callback
|
||||||
logging.info(f"已注册回调函数: {key}")
|
logging.info(f"已注册回调函数: {key}, 回调对象类型: {callback.__self__.__class__.__name__}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"注册回调失败: {e}")
|
logging.error(f"注册回调失败: {e}")
|
||||||
|
|
||||||
def trigger_resistance_query(self):
|
def trigger_resistance_query(self):
|
||||||
"""触发米电阻数据查询,如果串口未打开,则尝试临时打开并查询"""
|
"""触发米电阻查询"""
|
||||||
# 直接打印到控制台,确保可见
|
|
||||||
print("\n[米电阻查询] PageUp键被按下,正在触发米电阻数据查询...\n")
|
|
||||||
|
|
||||||
# 检查是否启用串口功能
|
|
||||||
enable_serial_ports = self.config.get_value('app.features.enable_serial_ports', False)
|
|
||||||
if not enable_serial_ports:
|
|
||||||
logging.info("串口功能已在配置中禁用,跳过米电阻数据查询")
|
|
||||||
print("\n[米电阻查询] 串口功能已禁用,无法查询\n")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 检查是否启用键盘监听功能 - 如果这个按键是通过键盘触发的,应该尊重键盘监听器配置
|
|
||||||
enable_keyboard_listener = self.config.get_value('app.features.enable_keyboard_listener', False)
|
|
||||||
if not enable_keyboard_listener:
|
|
||||||
logging.info("键盘监听功能已在配置中禁用,但收到了Page Up触发,检查是否为其他来源的调用")
|
|
||||||
print("\n[米电阻查询] 键盘监听功能已禁用,但收到了触发\n")
|
|
||||||
# 这里我们仍然继续执行,因为该方法可能由其他非键盘源调用
|
|
||||||
|
|
||||||
# 从配置中获取触发键
|
|
||||||
config = ConfigLoader.get_instance()
|
|
||||||
trigger_key = config.get_value('serial.keyboard.trigger_key', 'Key.page_up')
|
|
||||||
logging.info(f"[SerialManager] {trigger_key}键按下,正在触发米电阻数据查询...")
|
|
||||||
print(f"\n[米电阻查询] {trigger_key}键按下,正在处理...\n")
|
|
||||||
|
|
||||||
if not self.mdz_config:
|
|
||||||
logging.error("[SerialManager] 米电阻配置 (mdz_config) 未加载,无法查询。")
|
|
||||||
print("\n[米电阻查询] 配置未加载,无法查询\n")
|
|
||||||
return
|
|
||||||
|
|
||||||
mdz_port_name = self.mdz_config.get('ser')
|
|
||||||
query_cmd_hex = self.mdz_config.get('query_cmd', '01030001000755C8')
|
|
||||||
baud_rate = self.mdz_config.get('port', 9600)
|
|
||||||
timeout = self.mdz_config.get('timeout', 1.0)
|
|
||||||
data_bits = self.mdz_config.get('data_bits', 8)
|
|
||||||
stop_bits = self.mdz_config.get('stop_bits', 1)
|
|
||||||
parity_char = self.mdz_config.get('parity', 'N')
|
|
||||||
parity = parity_char # Use the character directly ('N', 'E', or 'O')
|
|
||||||
logging.info(f"[SerialManager] 使用校验位设置: {parity}")
|
|
||||||
print(f"\n[米电阻查询] 串口配置: {mdz_port_name}, {baud_rate}, {data_bits}, {stop_bits}, {parity}\n")
|
|
||||||
|
|
||||||
temp_ser = None
|
|
||||||
try:
|
try:
|
||||||
byte_data = bytes.fromhex(query_cmd_hex.replace(' ', ''))
|
# 检查米电阻配置是否存在
|
||||||
logging.info(f"[SerialManager] 准备发送米电阻查询指令: {byte_data.hex(' ').upper()} 到端口 {mdz_port_name}")
|
if not self.mdz_config:
|
||||||
print(f"\n[米电阻查询] 准备发送查询指令: {byte_data.hex(' ').upper()}\n")
|
logging.warning("未找到米电阻配置,无法触发查询")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查米电阻串口是否已打开
|
||||||
|
ser_name = self.mdz_config.get('ser')
|
||||||
|
if not ser_name or not self.is_port_open(ser_name):
|
||||||
|
logging.warning(f"米电阻串口 {ser_name} 未打开,无法触发查询")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 从配置获取查询命令,如果没有则使用默认命令
|
||||||
|
query_cmd = self.mdz_config.get('query_cmd', '01 03 00 01 00 07 55 C8')
|
||||||
|
|
||||||
# 检查 SerialManager 是否已管理此端口且已打开
|
# 转换为字节数据
|
||||||
if self.is_port_open(mdz_port_name):
|
try:
|
||||||
logging.info(f"[SerialManager] 米电阻串口 {mdz_port_name} 已由 SerialManager 管理并打开,直接发送指令。")
|
byte_data = bytes.fromhex(query_cmd.replace(' ', ''))
|
||||||
print(f"\n[米电阻查询] 串口 {mdz_port_name} 已打开,直接发送指令\n")
|
except ValueError as e:
|
||||||
if self.write_data(mdz_port_name, byte_data):
|
logging.error(f"米电阻查询命令格式错误: {query_cmd}, 错误: {e}")
|
||||||
logging.info(f"[SerialManager] 指令已发送到 {mdz_port_name} (通过已打开的串口)。响应将由读取线程处理。")
|
return False
|
||||||
print("\n[米电阻查询] 指令发送成功,等待响应\n")
|
|
||||||
# 当串口已打开时,指令发送后,响应会由 _read_resistance_thread 捕获并处理。
|
# 发送查询命令
|
||||||
# _read_resistance_thread 内部的 _process_mdz_response 会负责更新 self.data,
|
result = self.write_data(ser_name, byte_data)
|
||||||
# 调用 _write_data_to_file 和 _notify_callbacks。
|
if result:
|
||||||
# 因此,这里不需要再显式地 sleep 后调用 _write_data_to_file 和 _notify_callbacks,
|
logging.info(f"已向米电阻串口 {ser_name} 发送查询命令: {query_cmd}")
|
||||||
# 以避免数据竞争或重复通知。
|
|
||||||
return # 指令已发送,等待线程处理
|
# 特殊情况:如果触发了查询,但串口没有响应,尝试模拟一个合理的响应
|
||||||
else:
|
# 这里我们先等待一段时间,让设备有机会响应
|
||||||
logging.warning(f"[SerialManager] 向已打开的串口 {mdz_port_name} 发送指令失败。将尝试临时打开。")
|
time.sleep(0.5)
|
||||||
print("\n[米电阻查询] 指令发送失败,尝试临时打开串口\n")
|
|
||||||
|
# 如果没有数据可读,或者读取的数据不包含有效的米电阻值
|
||||||
# 如果串口未被 SerialManager 管理或发送失败,则尝试临时打开
|
if self.serial_ports[ser_name].in_waiting == 0:
|
||||||
logging.info(f"[SerialManager] 米电阻串口 {mdz_port_name} 未打开或发送失败。尝试临时打开并查询...")
|
logging.warning("米电阻串口未返回数据,检查设备连接")
|
||||||
print(f"\n[米电阻查询] 串口 {mdz_port_name} 未打开,尝试临时打开\n")
|
|
||||||
temp_ser = serial.Serial(
|
# 在这里我们不再自动提供模拟数据,而是由用户决定是否重试
|
||||||
port=mdz_port_name,
|
return True
|
||||||
baudrate=baud_rate,
|
|
||||||
bytesize=data_bits,
|
|
||||||
stopbits=stop_bits,
|
|
||||||
parity=parity,
|
|
||||||
timeout=timeout
|
|
||||||
)
|
|
||||||
if not temp_ser.is_open:
|
|
||||||
temp_ser.open()
|
|
||||||
|
|
||||||
temp_ser.write(byte_data)
|
|
||||||
logging.info(f"[SerialManager] 指令已通过临时串口发送到 {mdz_port_name}。等待响应...")
|
|
||||||
print("\n[米电阻查询] 指令已通过临时串口发送,等待响应\n")
|
|
||||||
time.sleep(0.1) # 等待设备响应
|
|
||||||
response_bytes = b''
|
|
||||||
if temp_ser.in_waiting > 0:
|
|
||||||
response_bytes = temp_ser.read(temp_ser.in_waiting)
|
|
||||||
|
|
||||||
if response_bytes:
|
|
||||||
logging.info(f"[SerialManager] 收到来自 {mdz_port_name} (临时串口) 的响应: {response_bytes.hex(' ').upper()}")
|
|
||||||
print(f"\n[米电阻查询] 收到响应: {response_bytes.hex(' ').upper()}\n")
|
|
||||||
|
|
||||||
# 将响应交给标准的处理函数
|
|
||||||
parse_success = self._process_mdz_response(mdz_port_name, response_bytes)
|
|
||||||
if not parse_success:
|
|
||||||
logging.warning(f"[SerialManager] _process_mdz_response未能成功处理来自临时串口{mdz_port_name}的响应。将依赖其内部的mock/old data逻辑。")
|
|
||||||
print("\n[米电阻查询] 响应解析失败\n")
|
|
||||||
# _process_mdz_response 内部在失败时会处理 mock/old data 及文件写入和通知,这里无需额外操作。
|
|
||||||
else:
|
else:
|
||||||
logging.warning(f"[SerialManager] 未收到来自 {mdz_port_name} (临时串口) 的响应。")
|
logging.error(f"向米电阻串口 {ser_name} 发送查询命令失败")
|
||||||
print("\n[米电阻查询] 未收到响应\n")
|
return False
|
||||||
except serial.SerialException as se:
|
|
||||||
logging.error(f"[SerialManager] 临时打开或操作串口 {mdz_port_name} 失败: {se}")
|
|
||||||
print(f"\n[米电阻查询] 串口操作失败: {se}\n")
|
|
||||||
except ValueError as ve:
|
|
||||||
logging.error(f"[SerialManager] 指令转换错误或响应解析错误 (临时查询): {ve}")
|
|
||||||
print(f"\n[米电阻查询] 指令转换或响应解析错误: {ve}\n")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"[SerialManager] 触发米电阻查询时发生未知错误 (临时查询): {e}", exc_info=True)
|
logging.error(f"触发米电阻查询时出错: {e}")
|
||||||
print(f"\n[米电阻查询] 未知错误: {e}\n")
|
return False
|
||||||
finally:
|
|
||||||
if temp_ser and temp_ser.is_open:
|
|
||||||
temp_ser.close()
|
|
||||||
logging.info("[SerialManager] 米电阻数据查询流程结束。")
|
|
||||||
print("\n[米电阻查询] 查询流程结束\n")
|
|
||||||
|
|
||||||
def _notify_callbacks(self, port_name, value):
|
def _notify_callbacks(self, port_name, value):
|
||||||
"""通知所有相关回调函数"""
|
"""通知所有相关回调函数"""
|
||||||
@ -1045,20 +984,41 @@ class SerialManager:
|
|||||||
if os_type == "Darwin" and (
|
if os_type == "Darwin" and (
|
||||||
(self.mdz_config and 'ser' in self.mdz_config and self.mdz_config['ser'] and self.mdz_config['ser'].startswith('COM')) or
|
(self.mdz_config and 'ser' in self.mdz_config and self.mdz_config['ser'] and self.mdz_config['ser'].startswith('COM')) or
|
||||||
(self.cz_config and 'ser' in self.cz_config and self.cz_config['ser'] and self.cz_config['ser'].startswith('COM')) or
|
(self.cz_config and 'ser' in self.cz_config and self.cz_config['ser'] and self.cz_config['ser'].startswith('COM')) or
|
||||||
|
(self.xj_config and 'ser' in self.xj_config and self.xj_config['ser'] and self.xj_config['ser'].startswith('COM')) or
|
||||||
(self.scanner_config and 'ser' in self.scanner_config and self.scanner_config['ser'] and self.scanner_config['ser'].startswith('COM'))
|
(self.scanner_config and 'ser' in self.scanner_config and self.scanner_config['ser'] and self.scanner_config['ser'].startswith('COM'))
|
||||||
):
|
):
|
||||||
logging.warning("检测到在macOS系统上配置了Windows格式的COM端口,这些端口将无法正常打开")
|
logging.warning("检测到在macOS系统上配置了Windows格式的COM端口,这些端口将无法正常打开")
|
||||||
logging.warning("macOS上的串口通常是/dev/tty.*或/dev/cu.*格式")
|
logging.warning("macOS上的串口通常是/dev/tty.*或/dev/cu.*格式")
|
||||||
# 继续尝试打开,但不影响程序流程
|
# 继续尝试打开,但不影响程序流程
|
||||||
|
|
||||||
# 尝试打开线径串口
|
# 尝试打开称重串口
|
||||||
if self.cz_config and 'ser' in self.cz_config and self.cz_config['ser'] and self.cz_config['ser'].strip():
|
if self.cz_config and 'ser' in self.cz_config and self.cz_config['ser'] and self.cz_config['ser'].strip():
|
||||||
port_name = self.cz_config['ser']
|
port_name = self.cz_config['ser']
|
||||||
baud_rate = self.cz_config.get('port', 2400)
|
baud_rate = self.cz_config.get('port', 9600)
|
||||||
|
|
||||||
if not self.is_port_open(port_name):
|
if not self.is_port_open(port_name):
|
||||||
try:
|
try:
|
||||||
if self.open_port(port_name, 'cz', baud_rate):
|
if self.open_port(port_name, 'cz', baud_rate):
|
||||||
|
logging.info(f"自动打开称重串口 {port_name} 成功")
|
||||||
|
else:
|
||||||
|
logging.error(f"自动打开称重串口 {port_name} 失败")
|
||||||
|
success = False
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"自动打开称重串口 {port_name} 时发生异常: {e}")
|
||||||
|
success = False
|
||||||
|
else:
|
||||||
|
logging.info(f"称重串口 {port_name} 已经打开,无需重新打开")
|
||||||
|
else:
|
||||||
|
logging.warning("称重串口未配置或设置为不使用,跳过自动打开")
|
||||||
|
|
||||||
|
# 尝试打开线径串口
|
||||||
|
if self.xj_config and 'ser' in self.xj_config and self.xj_config['ser'] and self.xj_config['ser'].strip():
|
||||||
|
port_name = self.xj_config['ser']
|
||||||
|
baud_rate = self.xj_config.get('port', 19200)
|
||||||
|
|
||||||
|
if not self.is_port_open(port_name):
|
||||||
|
try:
|
||||||
|
if self.open_port(port_name, 'xj', baud_rate):
|
||||||
logging.info(f"自动打开线径串口 {port_name} 成功")
|
logging.info(f"自动打开线径串口 {port_name} 成功")
|
||||||
else:
|
else:
|
||||||
logging.error(f"自动打开线径串口 {port_name} 失败")
|
logging.error(f"自动打开线径串口 {port_name} 失败")
|
||||||
@ -1143,16 +1103,38 @@ class SerialManager:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logging.info(f"[{port_name}] 线径线程启动")
|
logging.info(f"[{port_name}] 线径线程启动")
|
||||||
|
logging.info(f"线径自动查询已开启,将持续发送查询指令获取数据")
|
||||||
|
|
||||||
while self.running_flags.get(port_name, False):
|
while self.running_flags.get(port_name, False):
|
||||||
if not self.is_port_open(port_name):
|
if not self.is_port_open(port_name):
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 检查是否有数据可读
|
try:
|
||||||
if self.serial_ports[port_name].in_waiting > 0:
|
# 从配置获取查询命令,如果没有则使用默认命令
|
||||||
response = self.serial_ports[port_name].read(self.serial_ports[port_name].in_waiting)
|
query_cmd = self.xj_config.get('query_cmd', '01 41 0d')
|
||||||
self._process_diameter_response(port_name, response)
|
# 发送查询指令
|
||||||
|
byte_data = bytes.fromhex(query_cmd.replace(' ', ''))
|
||||||
|
self.serial_ports[port_name].write(byte_data)
|
||||||
|
|
||||||
|
# 等待响应
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
if self.serial_ports[port_name].in_waiting > 0:
|
||||||
|
response = self.serial_ports[port_name].read(self.serial_ports[port_name].in_waiting)
|
||||||
|
self._process_diameter_response(port_name, response)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"线径数据处理异常: {e}")
|
||||||
|
|
||||||
|
# 查询间隔,从配置中获取或使用默认值
|
||||||
|
query_interval = self.xj_config.get('query_interval', 5) if self.xj_config else 5
|
||||||
|
wait_cycles = int(query_interval * 10) # 转换为0.1秒的周期数
|
||||||
|
|
||||||
|
# 每隔query_interval秒查询一次
|
||||||
|
for i in range(wait_cycles):
|
||||||
|
if not self.running_flags.get(port_name, False):
|
||||||
|
break
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -1182,7 +1164,22 @@ class SerialManager:
|
|||||||
# 更新数据
|
# 更新数据
|
||||||
self.data['xj'] = xj_value
|
self.data['xj'] = xj_value
|
||||||
self._write_data_to_file()
|
self._write_data_to_file()
|
||||||
|
|
||||||
|
# 构建MainWindow.on_diameter_data_received期望的格式
|
||||||
|
callback_data_str = f"线径数据: {xj_value}"
|
||||||
|
if 'xj_data' in self.callbacks:
|
||||||
|
try:
|
||||||
|
# 与米电阻类似,传递实际的串口名称
|
||||||
|
logging.info(f"线径回调开始调用,回调对象: {self.callbacks['xj_data'].__self__.__class__.__name__}, 数据: {xj_value}")
|
||||||
|
self.callbacks['xj_data'](port_name, callback_data_str.encode('utf-8'))
|
||||||
|
logging.info(f"通知 'xj_data' 回调成功. 值: {xj_value}, 串口: {port_name}")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"调用 'xj_data' 回调失败: {e}")
|
||||||
|
else:
|
||||||
|
# 如果未注册回调,仍然使用通用方法通知
|
||||||
|
logging.warning(f"未找到xj_data回调,使用通用_notify_callbacks方法")
|
||||||
self._notify_callbacks('xj_data', {"type": "xj", "value": self.data['xj'], "source": f"serial ({port_name})"})
|
self._notify_callbacks('xj_data', {"type": "xj", "value": self.data['xj'], "source": f"serial ({port_name})"})
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logging.warning(f"线径数据字符串 '{number_str}' 无法转换为浮点数")
|
logging.warning(f"线径数据字符串 '{number_str}' 无法转换为浮点数")
|
||||||
|
|||||||
@ -1,17 +1,18 @@
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
import numpy as np
|
||||||
from ctypes import *
|
from ctypes import *
|
||||||
|
|
||||||
# 确定使用哪个UI框架
|
# 确定使用哪个UI框架
|
||||||
try:
|
try:
|
||||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QFrame, QSizePolicy
|
from PySide6.QtWidgets import QWidget, QVBoxLayout, QFrame, QSizePolicy
|
||||||
from PySide6.QtCore import Qt, Signal, QSize
|
from PySide6.QtCore import Qt, Signal, QSize, QTimer
|
||||||
from PySide6.QtGui import QPalette, QColor
|
from PySide6.QtGui import QPalette, QColor, QImage, QPixmap
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QFrame, QSizePolicy
|
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QFrame, QSizePolicy
|
||||||
from PyQt5.QtCore import Qt, pyqtSignal as Signal, QSize
|
from PyQt5.QtCore import Qt, pyqtSignal as Signal, QSize
|
||||||
from PyQt5.QtGui import QPalette, QColor
|
from PyQt5.QtGui import QPalette, QColor, QImage, QPixmap
|
||||||
|
|
||||||
# 添加相机模块路径
|
# 添加相机模块路径
|
||||||
sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), "camera"))
|
sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), "camera"))
|
||||||
@ -52,6 +53,11 @@ class CameraDisplayWidget(QWidget):
|
|||||||
|
|
||||||
# 设置最小尺寸
|
# 设置最小尺寸
|
||||||
self.setMinimumSize(QSize(320, 240))
|
self.setMinimumSize(QSize(320, 240))
|
||||||
|
|
||||||
|
# 添加本地图像处理功能
|
||||||
|
self._current_pixmap = None
|
||||||
|
self._is_local_frame_connected = False
|
||||||
|
self._connect_local_frame_signal()
|
||||||
|
|
||||||
def init_ui(self):
|
def init_ui(self):
|
||||||
"""初始化UI - 只包含相机显示框架"""
|
"""初始化UI - 只包含相机显示框架"""
|
||||||
@ -75,6 +81,152 @@ class CameraDisplayWidget(QWidget):
|
|||||||
# 设置布局
|
# 设置布局
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
def _connect_local_frame_signal(self):
|
||||||
|
"""连接本地图像帧信号"""
|
||||||
|
if not self._is_local_frame_connected:
|
||||||
|
try:
|
||||||
|
# 尝试不同的方式获取LocalImagePlayer实例
|
||||||
|
|
||||||
|
# 方法1:从主窗口的SettingsWindow查找
|
||||||
|
from widgets.settings_window import SettingsWindow
|
||||||
|
for window in self.window().findChildren(SettingsWindow):
|
||||||
|
if hasattr(window, 'camera_settings'):
|
||||||
|
if hasattr(window.camera_settings, 'local_player'):
|
||||||
|
local_player = window.camera_settings.local_player
|
||||||
|
# 连接本地图像帧信号
|
||||||
|
if hasattr(local_player, 'signal_frame_ready'):
|
||||||
|
local_player.signal_frame_ready.connect(self.update_local_frame)
|
||||||
|
self._is_local_frame_connected = True
|
||||||
|
logging.info("成功连接本地图像帧信号 (方法1)")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 方法2:直接从相机管理器获取实例
|
||||||
|
from widgets.camera_settings_widget import CameraSettingsWidget
|
||||||
|
from widgets.main_window import MainWindow
|
||||||
|
|
||||||
|
for window in self.window().findChildren(MainWindow):
|
||||||
|
# 如果已初始化了设置窗口
|
||||||
|
if hasattr(window, 'settings_window'):
|
||||||
|
if hasattr(window.settings_window, 'camera_settings'):
|
||||||
|
camera_settings = window.settings_window.camera_settings
|
||||||
|
if hasattr(camera_settings, 'local_player'):
|
||||||
|
local_player = camera_settings.local_player
|
||||||
|
if hasattr(local_player, 'signal_frame_ready'):
|
||||||
|
local_player.signal_frame_ready.connect(self.update_local_frame)
|
||||||
|
self._is_local_frame_connected = True
|
||||||
|
logging.info("成功连接本地图像帧信号 (方法2)")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 如果以上方法都失败了,启动重试机制
|
||||||
|
from PySide6.QtCore import QTimer
|
||||||
|
self._retry_timer = QTimer(self)
|
||||||
|
self._retry_timer.timeout.connect(self._retry_connect_signal)
|
||||||
|
self._retry_timer.start(1000) # 1秒后重试
|
||||||
|
logging.info("未找到本地图像播放器,将在1秒后重试")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"连接本地图像帧信号失败: {e}")
|
||||||
|
|
||||||
|
def _retry_connect_signal(self):
|
||||||
|
"""重试连接本地图像帧信号"""
|
||||||
|
if self._is_local_frame_connected:
|
||||||
|
if hasattr(self, '_retry_timer'):
|
||||||
|
self._retry_timer.stop()
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 重新尝试连接
|
||||||
|
self._connect_local_frame_signal()
|
||||||
|
|
||||||
|
# 如果仍然未连接成功,停止重试计时器
|
||||||
|
if not self._is_local_frame_connected and hasattr(self, '_retry_counter'):
|
||||||
|
self._retry_counter = getattr(self, '_retry_counter', 0) + 1
|
||||||
|
if self._retry_counter >= 5: # 最多重试5次
|
||||||
|
logging.warning("连接本地图像帧信号重试5次后仍失败,停止重试")
|
||||||
|
self._retry_timer.stop()
|
||||||
|
else:
|
||||||
|
logging.info(f"第{self._retry_counter}次重试连接本地图像帧信号")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"重试连接本地图像帧信号失败: {e}")
|
||||||
|
|
||||||
|
def update_local_frame(self, frame):
|
||||||
|
"""处理并显示本地图像帧
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: 本地图像帧数据(OpenCV RGB图像)
|
||||||
|
"""
|
||||||
|
# 添加明显的调试日志,确认该方法被调用
|
||||||
|
logging.warning(f"====> CameraDisplayWidget.update_local_frame被调用,帧尺寸: {frame.shape if frame is not None else 'None'}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if frame is None:
|
||||||
|
logging.warning("接收到的本地图像帧为空")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 转换OpenCV的BGR图像为Qt的QImage
|
||||||
|
height, width, channel = frame.shape
|
||||||
|
bytes_per_line = channel * width
|
||||||
|
|
||||||
|
# 创建QImage (注意:OpenCV使用RGB格式,QImage使用RGB格式)
|
||||||
|
q_img = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888)
|
||||||
|
|
||||||
|
# 创建QPixmap并设置到标签
|
||||||
|
pixmap = QPixmap.fromImage(q_img)
|
||||||
|
|
||||||
|
# 保存当前图像
|
||||||
|
self._current_pixmap = pixmap
|
||||||
|
|
||||||
|
# 立即绘制
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
# 强制重绘
|
||||||
|
self.repaint()
|
||||||
|
|
||||||
|
# 记录日志
|
||||||
|
logging.warning(f"====> 成功更新本地图像帧: 图像大小={width}x{height},已调用update()和repaint()触发重绘")
|
||||||
|
|
||||||
|
# 确保相机区域可见
|
||||||
|
self.setVisible(True)
|
||||||
|
self.raise_()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"处理本地图像帧时出错: {e}")
|
||||||
|
|
||||||
|
def paintEvent(self, event):
|
||||||
|
"""重写绘制事件,用于显示图像"""
|
||||||
|
# 调用父类的paintEvent,确保原有功能完整
|
||||||
|
super().paintEvent(event)
|
||||||
|
|
||||||
|
# 添加调试日志,检查是否有图像需要绘制
|
||||||
|
if self._current_pixmap:
|
||||||
|
logging.warning(f"====> paintEvent: 绘制图像,尺寸={self._current_pixmap.width()}x{self._current_pixmap.height()}")
|
||||||
|
|
||||||
|
# 确保我们直接在这个窗口上绘制,不创建新组件
|
||||||
|
from PySide6.QtGui import QPainter
|
||||||
|
painter = QPainter(self)
|
||||||
|
|
||||||
|
# 按比例缩放图像,保持图像比例
|
||||||
|
scaled_pixmap = self._current_pixmap.scaled(
|
||||||
|
self.size(), # 使用整个控件尺寸
|
||||||
|
Qt.KeepAspectRatio,
|
||||||
|
Qt.SmoothTransformation
|
||||||
|
)
|
||||||
|
|
||||||
|
# 计算居中位置
|
||||||
|
x = (self.width() - scaled_pixmap.width()) // 2
|
||||||
|
y = (self.height() - scaled_pixmap.height()) // 2
|
||||||
|
|
||||||
|
# 绘制图像
|
||||||
|
painter.drawPixmap(x, y, scaled_pixmap)
|
||||||
|
|
||||||
|
# 记录绘制完成
|
||||||
|
logging.warning(f"====> 图像绘制完成: 控件尺寸={self.width()}x{self.height()}, 图像绘制位置=({x},{y}), 缩放后尺寸={scaled_pixmap.width()}x{scaled_pixmap.height()}")
|
||||||
|
|
||||||
|
painter.end()
|
||||||
|
else:
|
||||||
|
logging.debug("paintEvent: 没有图像需要绘制")
|
||||||
|
|
||||||
def start_display(self):
|
def start_display(self):
|
||||||
"""开始显示相机图像"""
|
"""开始显示相机图像"""
|
||||||
if not self.camera_manager.isOpen:
|
if not self.camera_manager.isOpen:
|
||||||
@ -139,8 +291,11 @@ class CameraDisplayWidget(QWidget):
|
|||||||
# 记录大小变化
|
# 记录大小变化
|
||||||
logging.debug(f"相机显示区域大小变化为: {self.width()}x{self.height()}")
|
logging.debug(f"相机显示区域大小变化为: {self.width()}x{self.height()}")
|
||||||
|
|
||||||
|
# 重绘当前图像
|
||||||
|
self.update()
|
||||||
|
|
||||||
# 当尺寸变化超过一定阈值时,重新调整相机显示
|
# 当尺寸变化超过一定阈值时,重新调整相机显示
|
||||||
if self.camera_manager.isGrabbing:
|
if self.camera_manager.isGrabbing and not self.camera_manager.local_mode:
|
||||||
# 停止当前显示
|
# 停止当前显示
|
||||||
self.stop_display()
|
self.stop_display()
|
||||||
# 使用新尺寸重新开始显示
|
# 使用新尺寸重新开始显示
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import logging
|
|||||||
import json
|
import json
|
||||||
import ctypes
|
import ctypes
|
||||||
from ctypes import *
|
from ctypes import *
|
||||||
|
import numpy as np
|
||||||
|
import cv2
|
||||||
|
|
||||||
# 添加相机模块路径
|
# 添加相机模块路径
|
||||||
sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), "camera"))
|
sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), "camera"))
|
||||||
@ -36,23 +38,74 @@ class CameraManager:
|
|||||||
else:
|
else:
|
||||||
CameraManager._instance = self
|
CameraManager._instance = self
|
||||||
|
|
||||||
# 初始化变量
|
# 初始化属性
|
||||||
self.deviceList = None
|
|
||||||
self.cam = MvCamera()
|
|
||||||
self.nSelCamIndex = -1
|
|
||||||
self.obj_cam_operation = None
|
|
||||||
self.isOpen = False
|
self.isOpen = False
|
||||||
self.isGrabbing = False
|
self.isGrabbing = False
|
||||||
|
self.last_device_index = -1
|
||||||
|
self.device_list = []
|
||||||
|
self.current_window_id = 0
|
||||||
|
|
||||||
# 初始化SDK (只在第一次时初始化)
|
# 本地图像模式相关
|
||||||
if not CameraManager._initialized:
|
self.local_mode = False
|
||||||
MvCamera.MV_CC_Initialize()
|
self.last_frame = None
|
||||||
CameraManager._initialized = True
|
self.has_real_camera = False # 是否有真实相机连接
|
||||||
logging.info("相机SDK已初始化")
|
|
||||||
|
# 初始化SDK
|
||||||
|
self.obj_cam_operation = CameraOperation()
|
||||||
|
|
||||||
|
# 将单例标记为已初始化
|
||||||
|
CameraManager._initialized = True
|
||||||
|
|
||||||
|
# 初始化日志
|
||||||
|
logging.info("相机管理器初始化")
|
||||||
|
|
||||||
def enum_devices(self):
|
def enum_devices(self):
|
||||||
"""枚举相机设备,完全参考BasicDemo.py的enum_devices实现"""
|
"""枚举相机设备列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: 设备信息列表
|
||||||
|
"""
|
||||||
|
# 如果当前处于本地图像模式,返回一个虚拟设备
|
||||||
|
if self.local_mode:
|
||||||
|
# 返回虚拟设备列表
|
||||||
|
virtual_device = {
|
||||||
|
"vendor_name": "本地图像模式",
|
||||||
|
"model_name": "虚拟相机",
|
||||||
|
"serial_number": "LOCAL_IMG",
|
||||||
|
"device_version": "1.0",
|
||||||
|
"spec_version": "1.0",
|
||||||
|
"user_defined_name": "本地图像播放器",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
self.device_list = [virtual_device]
|
||||||
|
return self.device_list
|
||||||
|
|
||||||
|
# 清空设备列表
|
||||||
|
self.device_list = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# 枚举设备
|
||||||
|
ret = self.obj_cam_operation.Enumrate_Devices()
|
||||||
|
if ret != 0:
|
||||||
|
error_msg = f"枚举相机设备失败! 错误码: 0x{ret:x}"
|
||||||
|
logging.error(error_msg)
|
||||||
|
# 标记没有真实相机
|
||||||
|
self.has_real_camera = False
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 获取设备数量
|
||||||
|
device_num = self.obj_cam_operation.device_num
|
||||||
|
|
||||||
|
if device_num <= 0:
|
||||||
|
logging.warning("未发现相机设备")
|
||||||
|
# 标记没有真实相机
|
||||||
|
self.has_real_camera = False
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 标记存在真实相机
|
||||||
|
self.has_real_camera = True
|
||||||
|
|
||||||
|
# 解析并构建设备信息
|
||||||
# 确保Hikvision SDK已正确加载
|
# 确保Hikvision SDK已正确加载
|
||||||
from camera.MvCameraControl_class import MvCamCtrldll
|
from camera.MvCameraControl_class import MvCamCtrldll
|
||||||
if MvCamCtrldll is None:
|
if MvCamCtrldll is None:
|
||||||
@ -182,15 +235,17 @@ class CameraManager:
|
|||||||
|
|
||||||
# 添加详细日志
|
# 添加详细日志
|
||||||
logging.debug(f"枚举到的设备数量: {len(devices_info)}")
|
logging.debug(f"枚举到的设备数量: {len(devices_info)}")
|
||||||
|
self.device_list = devices_info
|
||||||
return devices_info
|
return devices_info
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"枚举设备时发生异常: {str(e)}"
|
error_msg = f"枚举相机设备时发生异常: {str(e)}"
|
||||||
logging.error(error_msg)
|
logging.error(error_msg)
|
||||||
return None
|
self.has_real_camera = False
|
||||||
|
return []
|
||||||
|
|
||||||
def open_device(self, device_index):
|
def open_device(self, device_index):
|
||||||
"""打开相机设备,参考BasicDemo.py的open_device实现
|
"""打开相机设备
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
device_index: 设备索引
|
device_index: 设备索引
|
||||||
@ -198,27 +253,32 @@ class CameraManager:
|
|||||||
Returns:
|
Returns:
|
||||||
bool: 是否成功打开设备
|
bool: 是否成功打开设备
|
||||||
"""
|
"""
|
||||||
# 检查是否已经打开
|
# 如果当前处于本地图像模式,且设备索引是虚拟设备
|
||||||
if self.isOpen:
|
if self.local_mode and device_index == 0:
|
||||||
logging.warning("相机已经打开!")
|
# 模拟打开设备成功
|
||||||
|
self.isOpen = True
|
||||||
|
self.last_device_index = device_index
|
||||||
|
logging.info("打开本地图像模式虚拟设备")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 真实相机模式处理
|
||||||
|
# 检查设备索引是否有效
|
||||||
|
if device_index < 0 or device_index >= len(self.device_list):
|
||||||
|
logging.error(f"无效的设备索引: {device_index}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 确保有效的设备索引
|
# 如果之前已经打开设备,先关闭
|
||||||
if device_index < 0 or self.deviceList is None or device_index >= self.deviceList.nDeviceNum:
|
if self.isOpen:
|
||||||
error_msg = f"无效的设备索引: {device_index}, 设备列表: {self.deviceList is not None}"
|
self.close_device()
|
||||||
if self.deviceList:
|
|
||||||
error_msg += f", 设备数量: {self.deviceList.nDeviceNum}"
|
|
||||||
logging.error(error_msg)
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logging.info(f"开始打开相机,设备索引: {device_index}")
|
logging.info(f"开始打开相机,设备索引: {device_index}")
|
||||||
|
|
||||||
# 设置当前选中的相机索引
|
# 设置当前选中的相机索引
|
||||||
self.nSelCamIndex = device_index
|
self.last_device_index = device_index
|
||||||
|
|
||||||
# 创建相机操作对象
|
# 创建相机操作对象
|
||||||
self.obj_cam_operation = CameraOperation(self.cam, self.deviceList, self.nSelCamIndex)
|
self.obj_cam_operation = CameraOperation(self.obj_cam_operation.cam, self.deviceList, self.last_device_index)
|
||||||
ret = self.obj_cam_operation.Open_device()
|
ret = self.obj_cam_operation.Open_device()
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
error_msg = f"打开相机失败! 错误码: 0x{ret:x}"
|
error_msg = f"打开相机失败! 错误码: 0x{ret:x}"
|
||||||
@ -260,6 +320,16 @@ class CameraManager:
|
|||||||
Returns:
|
Returns:
|
||||||
bool: 是否成功关闭设备
|
bool: 是否成功关闭设备
|
||||||
"""
|
"""
|
||||||
|
# 如果处于本地图像模式且使用的是虚拟设备
|
||||||
|
if self.local_mode and not self.has_real_camera:
|
||||||
|
# 模拟关闭设备成功
|
||||||
|
if self.isGrabbing:
|
||||||
|
self.stop_grabbing()
|
||||||
|
self.isOpen = False
|
||||||
|
logging.info("关闭本地图像模式虚拟设备")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 真实相机模式处理
|
||||||
if not self.isOpen:
|
if not self.isOpen:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -287,22 +357,28 @@ class CameraManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def start_grabbing(self, window_id):
|
def start_grabbing(self, window_id):
|
||||||
"""开始取图
|
"""开始图像采集
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
window_id: 显示窗口句柄
|
window_id: 显示窗口句柄
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: 是否成功开始取图
|
bool: 是否成功开始采集
|
||||||
"""
|
"""
|
||||||
|
# 保存窗口ID
|
||||||
|
self.current_window_id = window_id
|
||||||
|
|
||||||
|
# 如果处于本地图像模式,模拟开始采集
|
||||||
|
if self.local_mode:
|
||||||
|
self.isGrabbing = True
|
||||||
|
logging.info("开始本地图像模式采集, 窗口ID: " + str(window_id))
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 检查设备是否已打开
|
||||||
if not self.isOpen:
|
if not self.isOpen:
|
||||||
logging.error("相机未打开,无法开始取图")
|
logging.error("相机未打开,无法开始取图")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.isGrabbing:
|
|
||||||
logging.warning("相机已经在取图")
|
|
||||||
return True
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ret = self.obj_cam_operation.Start_grabbing(window_id)
|
ret = self.obj_cam_operation.Start_grabbing(window_id)
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
@ -326,6 +402,12 @@ class CameraManager:
|
|||||||
Returns:
|
Returns:
|
||||||
bool: 是否成功停止取图
|
bool: 是否成功停止取图
|
||||||
"""
|
"""
|
||||||
|
# 如果处于本地图像模式,模拟停止采集
|
||||||
|
if self.local_mode:
|
||||||
|
self.isGrabbing = False
|
||||||
|
logging.info("停止本地图像模式采集")
|
||||||
|
return True
|
||||||
|
|
||||||
if not self.isOpen:
|
if not self.isOpen:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -412,6 +494,64 @@ class CameraManager:
|
|||||||
logging.error(error_msg)
|
logging.error(error_msg)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def set_local_mode(self, enabled):
|
||||||
|
"""设置是否启用本地图像模式
|
||||||
|
|
||||||
|
Args:
|
||||||
|
enabled: 是否启用本地模式
|
||||||
|
"""
|
||||||
|
# 如果当前正在采集,先停止
|
||||||
|
if self.isGrabbing:
|
||||||
|
self.stop_grabbing()
|
||||||
|
|
||||||
|
# 如果当前相机已打开且切换到本地模式,先关闭相机
|
||||||
|
if self.isOpen and enabled and not self.local_mode:
|
||||||
|
self.close_device()
|
||||||
|
|
||||||
|
# 设置模式
|
||||||
|
self.local_mode = enabled
|
||||||
|
|
||||||
|
# 重新枚举设备(会返回真实设备或虚拟设备)
|
||||||
|
self.enum_devices()
|
||||||
|
|
||||||
|
logging.info(f"本地图像模式已{'启用' if enabled else '禁用'}")
|
||||||
|
|
||||||
|
# 如果切换到本地模式,自动"打开"虚拟设备
|
||||||
|
if enabled and not self.isOpen:
|
||||||
|
self.open_device(0) # 虚拟设备索引为0
|
||||||
|
|
||||||
|
def handle_local_frame(self, frame, window_id=0):
|
||||||
|
"""处理并存储本地图像帧(不使用OpenCV窗口显示)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: 本地图像帧数据(OpenCV RGB图像)
|
||||||
|
window_id: 窗口句柄ID,仅用于兼容旧代码,实际不再使用
|
||||||
|
"""
|
||||||
|
if not self.local_mode:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 记录窗口ID (仅用于日志和兼容)
|
||||||
|
self.current_window_id = window_id
|
||||||
|
|
||||||
|
# 保存最后一帧,这个帧可以供其他组件使用
|
||||||
|
self.last_frame = frame
|
||||||
|
|
||||||
|
# 设置为正在抓取状态,与实际相机行为保持一致
|
||||||
|
self.isGrabbing = True
|
||||||
|
|
||||||
|
# 输出日志帮助调试
|
||||||
|
logging.warning(f"====> 相机管理器存储本地图像帧,尺寸: {frame.shape if frame is not None else 'None'}")
|
||||||
|
|
||||||
|
# 我们仅存储帧数据,不进行显示操作
|
||||||
|
# 显示操作由CameraDisplayWidget完成
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"处理本地图像帧失败: {e}")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def save_params_to_config(self, exposure, gain, frame_rate):
|
def save_params_to_config(self, exposure, gain, frame_rate):
|
||||||
"""保存相机参数到配置文件
|
"""保存相机参数到配置文件
|
||||||
|
|
||||||
|
|||||||
@ -8,11 +8,11 @@ from camera.CameraParams_const import *
|
|||||||
sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), "camera"))
|
sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), "camera"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PySide6.QtWidgets import QMessageBox
|
from PySide6.QtWidgets import QMessageBox, QFileDialog
|
||||||
from PySide6.QtCore import Qt, Signal
|
from PySide6.QtCore import Qt, Signal
|
||||||
USE_PYSIDE6 = True
|
USE_PYSIDE6 = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox, QFileDialog
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtCore import pyqtSignal as Signal
|
from PyQt5.QtCore import pyqtSignal as Signal
|
||||||
USE_PYSIDE6 = False
|
USE_PYSIDE6 = False
|
||||||
@ -27,8 +27,13 @@ from ui.settings_ui import SettingsUI
|
|||||||
# 导入相机管理器
|
# 导入相机管理器
|
||||||
from widgets.camera_manager import CameraManager
|
from widgets.camera_manager import CameraManager
|
||||||
|
|
||||||
|
# 导入本地图像播放器
|
||||||
|
from utils.local_image_player import LocalImagePlayer
|
||||||
|
# 导入配置加载器
|
||||||
|
from utils.config_loader import ConfigLoader
|
||||||
|
|
||||||
from PySide6.QtCore import QObject
|
from PySide6.QtCore import QObject
|
||||||
|
import json
|
||||||
|
|
||||||
class CameraSettingsWidget(QObject):
|
class CameraSettingsWidget(QObject):
|
||||||
"""相机设置控制器,管理相机设置并提供与主窗口相机显示部分的通信
|
"""相机设置控制器,管理相机设置并提供与主窗口相机显示部分的通信
|
||||||
@ -39,6 +44,7 @@ class CameraSettingsWidget(QObject):
|
|||||||
signal_camera_params_changed = Signal(float, float, float) # 相机参数变化信号 (曝光, 增益, 帧率)
|
signal_camera_params_changed = Signal(float, float, float) # 相机参数变化信号 (曝光, 增益, 帧率)
|
||||||
signal_camera_error = Signal(str) # 相机错误信号
|
signal_camera_error = Signal(str) # 相机错误信号
|
||||||
settings_changed = Signal() # 设置变更信号,与 SettingsWindow 兼容
|
settings_changed = Signal() # 设置变更信号,与 SettingsWindow 兼容
|
||||||
|
signal_local_mode_changed = Signal(bool) # 本地图像模式变更信号 (是否启用)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
"""初始化相机设置控制器
|
"""初始化相机设置控制器
|
||||||
@ -55,6 +61,24 @@ class CameraSettingsWidget(QObject):
|
|||||||
# 获取相机管理器实例
|
# 获取相机管理器实例
|
||||||
self.camera_manager = CameraManager.get_instance()
|
self.camera_manager = CameraManager.get_instance()
|
||||||
|
|
||||||
|
# 加载配置
|
||||||
|
try:
|
||||||
|
self.config_loader = ConfigLoader()
|
||||||
|
self.camera_config = self.config_loader.get_config("camera")
|
||||||
|
# 使用 .get() 来安全地访问 'display'
|
||||||
|
self.display_config = self.camera_config.get("display", {})
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"加载相机配置文件失败: {e}")
|
||||||
|
self.camera_config = {}
|
||||||
|
self.display_config = {}
|
||||||
|
|
||||||
|
# 创建本地图像播放器
|
||||||
|
self.local_player = LocalImagePlayer()
|
||||||
|
|
||||||
|
# 本地图像模式状态
|
||||||
|
self.local_mode_enabled = False
|
||||||
|
self.is_playing = False
|
||||||
|
|
||||||
# 初始化日志记录
|
# 初始化日志记录
|
||||||
logging.debug("CameraSettingsWidget初始化开始")
|
logging.debug("CameraSettingsWidget初始化开始")
|
||||||
|
|
||||||
@ -78,6 +102,17 @@ class CameraSettingsWidget(QObject):
|
|||||||
self.set_params_button = getattr(parent, 'set_params_button', None)
|
self.set_params_button = getattr(parent, 'set_params_button', None)
|
||||||
self.save_camera_button = getattr(parent, 'save_camera_button', None)
|
self.save_camera_button = getattr(parent, 'save_camera_button', None)
|
||||||
self.preview_frame = getattr(parent, 'preview_frame', None)
|
self.preview_frame = getattr(parent, 'preview_frame', None)
|
||||||
|
self.preview_status = getattr(parent, 'preview_status', None)
|
||||||
|
|
||||||
|
# 本地图像模式相关控件
|
||||||
|
self.local_mode_check = getattr(parent, 'local_mode_check', None)
|
||||||
|
self.folder_path_edit = getattr(parent, 'folder_path_edit', None)
|
||||||
|
self.folder_path_button = getattr(parent, 'folder_path_button', None)
|
||||||
|
self.local_framerate_slider = getattr(parent, 'local_framerate_slider', None)
|
||||||
|
self.local_framerate_value = getattr(parent, 'local_framerate_value', None)
|
||||||
|
self.loop_playback_check = getattr(parent, 'loop_playback_check', None)
|
||||||
|
self.play_button = getattr(parent, 'play_button', None)
|
||||||
|
self.stop_button = getattr(parent, 'stop_button', None)
|
||||||
|
|
||||||
# 检查是否成功获取到了所有必要的UI控件
|
# 检查是否成功获取到了所有必要的UI控件
|
||||||
if self.camera_combo is None:
|
if self.camera_combo is None:
|
||||||
@ -109,7 +144,19 @@ class CameraSettingsWidget(QObject):
|
|||||||
self.set_params_button = None
|
self.set_params_button = None
|
||||||
self.save_camera_button = None
|
self.save_camera_button = None
|
||||||
self.preview_frame = None
|
self.preview_frame = None
|
||||||
|
self.preview_status = None
|
||||||
|
self.local_mode_check = None
|
||||||
|
self.folder_path_edit = None
|
||||||
|
self.folder_path_button = None
|
||||||
|
self.local_framerate_slider = None
|
||||||
|
self.local_framerate_value = None
|
||||||
|
self.loop_playback_check = None
|
||||||
|
self.play_button = None
|
||||||
|
self.stop_button = None
|
||||||
|
|
||||||
|
# 连接本地图像播放器信号
|
||||||
|
self.connect_local_player_signals()
|
||||||
|
|
||||||
# 连接信号和槽
|
# 连接信号和槽
|
||||||
self.connect_signals()
|
self.connect_signals()
|
||||||
|
|
||||||
@ -122,9 +169,24 @@ class CameraSettingsWidget(QObject):
|
|||||||
self.gain_min = 0.0
|
self.gain_min = 0.0
|
||||||
self.gain_max = 15.0
|
self.gain_max = 15.0
|
||||||
|
|
||||||
|
# 从配置加载本地模式设置
|
||||||
|
self.load_local_mode_settings()
|
||||||
|
|
||||||
|
# 更新本地模式UI
|
||||||
|
self.update_local_mode_ui()
|
||||||
|
|
||||||
# 枚举设备
|
# 枚举设备
|
||||||
self.refresh_devices()
|
self.refresh_devices()
|
||||||
|
|
||||||
|
def connect_local_player_signals(self):
|
||||||
|
"""连接本地图像播放器的信号"""
|
||||||
|
if self.local_player:
|
||||||
|
self.local_player.signal_status.connect(self.handle_local_player_status)
|
||||||
|
self.local_player.signal_error.connect(self.handle_local_player_error)
|
||||||
|
self.local_player.signal_progress.connect(self.handle_local_player_progress)
|
||||||
|
self.local_player.signal_frame_ready.connect(self.handle_local_player_frame)
|
||||||
|
self.local_player.signal_completed.connect(self.handle_local_player_completed)
|
||||||
|
|
||||||
def connect_signals(self):
|
def connect_signals(self):
|
||||||
"""连接信号和槽"""
|
"""连接信号和槽"""
|
||||||
# 设备选择和刷新
|
# 设备选择和刷新
|
||||||
@ -145,6 +207,25 @@ class CameraSettingsWidget(QObject):
|
|||||||
self.get_params_button.clicked.connect(self.get_camera_params)
|
self.get_params_button.clicked.connect(self.get_camera_params)
|
||||||
self.set_params_button.clicked.connect(self.set_camera_params)
|
self.set_params_button.clicked.connect(self.set_camera_params)
|
||||||
self.save_camera_button.clicked.connect(self.save_camera_params)
|
self.save_camera_button.clicked.connect(self.save_camera_params)
|
||||||
|
|
||||||
|
# 本地图像模式相关按钮和控件
|
||||||
|
if self.local_mode_check:
|
||||||
|
self.local_mode_check.stateChanged.connect(self.toggle_local_mode)
|
||||||
|
|
||||||
|
if self.folder_path_button:
|
||||||
|
self.folder_path_button.clicked.connect(self.choose_image_folder)
|
||||||
|
|
||||||
|
if self.local_framerate_slider:
|
||||||
|
self.local_framerate_slider.valueChanged.connect(self.update_local_framerate_value)
|
||||||
|
|
||||||
|
if self.loop_playback_check:
|
||||||
|
self.loop_playback_check.stateChanged.connect(self.update_loop_playback)
|
||||||
|
|
||||||
|
if self.play_button:
|
||||||
|
self.play_button.clicked.connect(self.play_local_images)
|
||||||
|
|
||||||
|
if self.stop_button:
|
||||||
|
self.stop_button.clicked.connect(self.stop_local_images)
|
||||||
|
|
||||||
def refresh_devices(self):
|
def refresh_devices(self):
|
||||||
"""刷新设备列表"""
|
"""刷新设备列表"""
|
||||||
@ -195,70 +276,25 @@ class CameraSettingsWidget(QObject):
|
|||||||
devList = []
|
devList = []
|
||||||
if devices_info:
|
if devices_info:
|
||||||
for device in devices_info:
|
for device in devices_info:
|
||||||
devList.append(device["display"])
|
# 修复:安全获取 display 键,如果不存在则使用默认值
|
||||||
|
if isinstance(device, dict):
|
||||||
|
display_name = device.get("display", f"设备 {len(devList)}")
|
||||||
|
devList.append(display_name)
|
||||||
|
else:
|
||||||
|
# 如果整个设备对象不是字典,则添加一个默认名称
|
||||||
|
devList.append(f"设备 {len(devList)}")
|
||||||
logging.info(f"【设备刷新】找到 {len(devList)} 个设备: {devList}")
|
logging.info(f"【设备刷新】找到 {len(devList)} 个设备: {devList}")
|
||||||
else:
|
else:
|
||||||
devList.append("未发现相机设备")
|
devList = ["未发现相机设备"]
|
||||||
logging.info(f"【设备刷新】将显示默认值 '未发现相机设备'")
|
|
||||||
|
|
||||||
# 3. 更新UI上的下拉列表
|
|
||||||
try:
|
|
||||||
if hasattr(self, 'camera_combo') and self.camera_combo is not None:
|
|
||||||
logging.info(f"【设备刷新】开始更新下拉列表,当前状态: 项目数={self.camera_combo.count()}, 是否可见={self.camera_combo.isVisible()}")
|
|
||||||
|
|
||||||
self.camera_combo.blockSignals(True)
|
|
||||||
self.camera_combo.clear()
|
|
||||||
|
|
||||||
# 确保项目数清零
|
|
||||||
if self.camera_combo.count() > 0:
|
|
||||||
logging.warning(f"【设备刷新】clear()后项目数仍为 {self.camera_combo.count()}")
|
|
||||||
|
|
||||||
# 直接添加项目 - 单个添加,避免批量添加可能的问题
|
|
||||||
for item in devList:
|
|
||||||
self.camera_combo.addItem(item)
|
|
||||||
logging.debug(f"【设备刷新】已添加项目: {item}")
|
|
||||||
|
|
||||||
# 确保设置当前项目
|
|
||||||
if self.camera_combo.count() > 0:
|
|
||||||
self.camera_combo.setCurrentIndex(0)
|
|
||||||
logging.info(f"【设备刷新】已设置当前索引为0,显示文本: {self.camera_combo.currentText()}")
|
|
||||||
else:
|
|
||||||
logging.error("【设备刷新】未能添加任何项目到下拉列表")
|
|
||||||
|
|
||||||
self.camera_combo.blockSignals(False)
|
|
||||||
|
|
||||||
# 强制更新UI
|
|
||||||
self.camera_combo.update()
|
|
||||||
self.camera_combo.repaint()
|
|
||||||
|
|
||||||
# 确保ComboBox有足够的尺寸显示内容
|
|
||||||
self.camera_combo.adjustSize()
|
|
||||||
|
|
||||||
logging.info(f"【设备刷新】下拉列表更新完成。当前项目数: {self.camera_combo.count()}, 当前文本: {self.camera_combo.currentText()}")
|
|
||||||
else:
|
|
||||||
logging.error("【设备刷新】无法更新下拉列表,camera_combo不存在")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"【设备刷新】更新下拉列表时发生错误: {e}")
|
|
||||||
|
|
||||||
# 4. 更新其他控件的状态
|
|
||||||
try:
|
|
||||||
self.update_controls()
|
|
||||||
logging.info("【设备刷新】控件状态已更新。")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"【设备刷新】更新控件状态时发生错误: {e}")
|
|
||||||
|
|
||||||
# 5. 如果下拉列表仍然为空,尝试最后一次强制添加
|
# 3. 清空并更新下拉列表
|
||||||
try:
|
self.camera_combo.clear()
|
||||||
if hasattr(self, 'camera_combo') and self.camera_combo is not None:
|
for dev in devList:
|
||||||
if self.camera_combo.count() == 0:
|
self.camera_combo.addItem(dev)
|
||||||
logging.warning("【设备刷新】下拉列表仍然为空,尝试强制添加项目")
|
|
||||||
self.camera_combo.addItem("未发现相机设备(强制添加)")
|
# 4. 更新UI状态
|
||||||
self.camera_combo.setCurrentIndex(0)
|
self.update_controls()
|
||||||
self.camera_combo.update()
|
|
||||||
self.camera_combo.repaint()
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"【设备刷新】强制添加项目时发生错误: {e}")
|
|
||||||
|
|
||||||
def get_selected_device_index(self):
|
def get_selected_device_index(self):
|
||||||
"""获取当前选中的设备索引,参考BasicDemo.py的TxtWrapBy实现"""
|
"""获取当前选中的设备索引,参考BasicDemo.py的TxtWrapBy实现"""
|
||||||
try:
|
try:
|
||||||
@ -351,7 +387,6 @@ class CameraSettingsWidget(QObject):
|
|||||||
self.signal_camera_connection.emit(True, "")
|
self.signal_camera_connection.emit(True, "")
|
||||||
|
|
||||||
# 更新配置
|
# 更新配置
|
||||||
from utils.config_loader import ConfigLoader
|
|
||||||
config_loader = ConfigLoader.get_instance()
|
config_loader = ConfigLoader.get_instance()
|
||||||
config_loader.set_value('camera.enabled', True)
|
config_loader.set_value('camera.enabled', True)
|
||||||
config_loader.save_config()
|
config_loader.save_config()
|
||||||
@ -386,7 +421,6 @@ class CameraSettingsWidget(QObject):
|
|||||||
self.signal_camera_connection.emit(False, "")
|
self.signal_camera_connection.emit(False, "")
|
||||||
|
|
||||||
# 更新配置
|
# 更新配置
|
||||||
from utils.config_loader import ConfigLoader
|
|
||||||
config_loader = ConfigLoader.get_instance()
|
config_loader = ConfigLoader.get_instance()
|
||||||
config_loader.set_value('camera.enabled', False)
|
config_loader.set_value('camera.enabled', False)
|
||||||
config_loader.save_config()
|
config_loader.save_config()
|
||||||
@ -538,3 +572,495 @@ class CameraSettingsWidget(QObject):
|
|||||||
if self.camera_manager.isOpen:
|
if self.camera_manager.isOpen:
|
||||||
self.camera_manager.close_device()
|
self.camera_manager.close_device()
|
||||||
super().closeEvent(event)
|
super().closeEvent(event)
|
||||||
|
|
||||||
|
def load_local_mode_settings(self):
|
||||||
|
"""加载本地图像模式的配置"""
|
||||||
|
try:
|
||||||
|
# 使用 .get() 防止因缺少 local_mode 键而崩溃
|
||||||
|
config = self.camera_config.get("local_mode", {})
|
||||||
|
self.local_mode_enabled = config.get("enabled", False)
|
||||||
|
self.local_player.framerate = config.get("framerate", 15)
|
||||||
|
self.local_player.loop = config.get("loop", True)
|
||||||
|
folder_path = config.get("folder_path", "")
|
||||||
|
|
||||||
|
# 更新UI组件
|
||||||
|
if self.local_mode_check:
|
||||||
|
self.local_mode_check.setChecked(self.local_mode_enabled)
|
||||||
|
if self.local_framerate_slider:
|
||||||
|
self.local_framerate_slider.setValue(self.local_player.framerate)
|
||||||
|
if self.loop_playback_check:
|
||||||
|
self.loop_playback_check.setChecked(self.local_player.loop)
|
||||||
|
if self.folder_path_edit:
|
||||||
|
self.folder_path_edit.setText(folder_path)
|
||||||
|
|
||||||
|
self.local_player.folder_path = folder_path
|
||||||
|
|
||||||
|
logging.info(f"加载本地图像模式设置: 启用={self.local_mode_enabled}, 帧率={self.local_player.framerate}, 循环={self.local_player.loop}")
|
||||||
|
|
||||||
|
# 触发模式变更信号,通知主窗口和其他组件
|
||||||
|
if self.local_mode_enabled:
|
||||||
|
self.signal_local_mode_changed.emit(True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"加载本地图像模式设置时发生错误: {e}")
|
||||||
|
|
||||||
|
def save_local_mode_settings(self):
|
||||||
|
"""保存本地图像模式的配置"""
|
||||||
|
try:
|
||||||
|
# 获取当前设置
|
||||||
|
enabled = self.local_mode_enabled
|
||||||
|
folder_path = self.folder_path_edit.text() if self.folder_path_edit else ""
|
||||||
|
framerate = self.local_framerate_slider.value() if self.local_framerate_slider else 15
|
||||||
|
loop = self.loop_playback_check.isChecked() if self.loop_playback_check else True
|
||||||
|
|
||||||
|
# 创建配置字典
|
||||||
|
config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "app_config.json")
|
||||||
|
|
||||||
|
# 读取现有配置(如果存在)
|
||||||
|
config = {}
|
||||||
|
if os.path.exists(config_path):
|
||||||
|
try:
|
||||||
|
with open(config_path, 'r', encoding='utf-8') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logging.warning(f"配置文件格式错误: {config_path}")
|
||||||
|
config = {}
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"读取配置文件失败: {e}")
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
# 更新本地图像模式设置
|
||||||
|
if "local_image_mode" not in config:
|
||||||
|
config["local_image_mode"] = {}
|
||||||
|
|
||||||
|
config["local_image_mode"].update({
|
||||||
|
"enabled": enabled,
|
||||||
|
"folder_path": folder_path,
|
||||||
|
"framerate": framerate,
|
||||||
|
"loop": loop
|
||||||
|
})
|
||||||
|
|
||||||
|
# 保存配置
|
||||||
|
with open(config_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(config, f, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
|
logging.info(f"已保存本地图像模式设置: enabled={enabled}, folder='{folder_path}', "
|
||||||
|
f"framerate={framerate}, loop={loop}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"保存本地图像模式设置失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_local_mode_ui(self):
|
||||||
|
"""更新本地图像模式UI状态"""
|
||||||
|
# 获取当前复选框状态(如果复选框存在)
|
||||||
|
enabled = self.local_mode_check.isChecked() if self.local_mode_check else self.local_mode_enabled
|
||||||
|
|
||||||
|
# 更新内部状态
|
||||||
|
if enabled != self.local_mode_enabled:
|
||||||
|
logging.info(f"本地图像模式状态变化: {self.local_mode_enabled} -> {enabled}")
|
||||||
|
self.local_mode_enabled = enabled
|
||||||
|
|
||||||
|
# 打印日志,帮助调试
|
||||||
|
logging.debug(f"更新本地图像模式UI: 启用={enabled}, 按钮状态={self.folder_path_button is not None}")
|
||||||
|
|
||||||
|
# 更新相关控件状态
|
||||||
|
if self.folder_path_edit:
|
||||||
|
self.folder_path_edit.setEnabled(enabled)
|
||||||
|
logging.debug(f"文件夹路径编辑框已设置为: {enabled}")
|
||||||
|
|
||||||
|
if self.folder_path_button:
|
||||||
|
self.folder_path_button.setEnabled(enabled)
|
||||||
|
logging.debug(f"文件夹选择按钮已设置为: {enabled}")
|
||||||
|
|
||||||
|
if self.local_framerate_slider:
|
||||||
|
self.local_framerate_slider.setEnabled(enabled)
|
||||||
|
|
||||||
|
if self.loop_playback_check:
|
||||||
|
self.loop_playback_check.setEnabled(enabled)
|
||||||
|
|
||||||
|
if self.play_button:
|
||||||
|
has_folder = bool(self.folder_path_edit and self.folder_path_edit.text())
|
||||||
|
self.play_button.setEnabled(enabled and has_folder)
|
||||||
|
logging.debug(f"播放按钮已设置为: {enabled and has_folder}, 有文件夹={has_folder}")
|
||||||
|
|
||||||
|
if self.stop_button:
|
||||||
|
self.stop_button.setEnabled(enabled and self.is_playing)
|
||||||
|
|
||||||
|
# 强制更新UI
|
||||||
|
if self.parent:
|
||||||
|
self.parent.update()
|
||||||
|
|
||||||
|
def toggle_local_mode(self, state):
|
||||||
|
"""切换本地图像模式"""
|
||||||
|
# 修复:直接比较整数值,Qt.Checked.value 是 2
|
||||||
|
is_checked = (state == 2) # 或者 state == Qt.Checked.value
|
||||||
|
|
||||||
|
# 停止可能正在进行的播放
|
||||||
|
if self.is_playing:
|
||||||
|
self.stop_local_images()
|
||||||
|
|
||||||
|
# 打印详细日志,帮助调试
|
||||||
|
logging.info(f"切换本地图像模式: {state} -> {is_checked}")
|
||||||
|
|
||||||
|
# 先设置相机管理器的本地模式
|
||||||
|
self.camera_manager.set_local_mode(is_checked)
|
||||||
|
|
||||||
|
# 更新本地模式状态
|
||||||
|
self.local_mode_enabled = is_checked
|
||||||
|
|
||||||
|
# 更新UI状态
|
||||||
|
self.update_local_mode_ui()
|
||||||
|
|
||||||
|
# 如果打开本地模式,关闭相机;如果关闭本地模式,尝试打开之前的相机
|
||||||
|
if is_checked:
|
||||||
|
# 如果相机正在工作,先关闭它
|
||||||
|
if self.camera_manager.isOpen and not self.camera_manager.local_mode:
|
||||||
|
self.close_camera()
|
||||||
|
else:
|
||||||
|
# 如果存在实际相机,尝试打开最后选择的相机
|
||||||
|
if self.camera_manager.has_real_camera:
|
||||||
|
# 获取当前选择的设备索引
|
||||||
|
device_index = self.get_selected_device_index()
|
||||||
|
if device_index >= 0:
|
||||||
|
# 关闭虚拟相机
|
||||||
|
if self.camera_manager.isOpen:
|
||||||
|
self.camera_manager.close_device()
|
||||||
|
# 开启实际相机
|
||||||
|
self.open_camera()
|
||||||
|
|
||||||
|
# 发送模式变更信号
|
||||||
|
self.signal_local_mode_changed.emit(is_checked)
|
||||||
|
|
||||||
|
# 保存设置
|
||||||
|
self.save_local_mode_settings()
|
||||||
|
|
||||||
|
logging.info(f"本地图像模式已{'启用' if is_checked else '禁用'}")
|
||||||
|
|
||||||
|
# 刷新设备列表
|
||||||
|
self.refresh_devices()
|
||||||
|
|
||||||
|
def choose_image_folder(self):
|
||||||
|
"""选择图像文件夹"""
|
||||||
|
# 检查复选框状态而不是类属性
|
||||||
|
is_enabled = self.local_mode_check.isChecked() if self.local_mode_check else False
|
||||||
|
|
||||||
|
# 即使控件状态可能不对,也尝试打开文件选择对话框
|
||||||
|
logging.info(f"选择图像文件夹: 本地模式启用={is_enabled}, 按钮状态={self.folder_path_button is not None}")
|
||||||
|
|
||||||
|
# 获取当前设置的文件夹路径作为初始目录
|
||||||
|
current_path = self.folder_path_edit.text() if self.folder_path_edit else ""
|
||||||
|
if not current_path or not os.path.isdir(current_path):
|
||||||
|
current_path = os.path.expanduser("~") # 默认使用用户主目录
|
||||||
|
|
||||||
|
# 打开文件夹选择对话框
|
||||||
|
folder_path = QFileDialog.getExistingDirectory(self.parent, "选择图像文件夹", current_path)
|
||||||
|
|
||||||
|
if folder_path:
|
||||||
|
logging.info(f"已选择图像文件夹: {folder_path}")
|
||||||
|
|
||||||
|
# 更新路径显示
|
||||||
|
if self.folder_path_edit:
|
||||||
|
self.folder_path_edit.setText(folder_path)
|
||||||
|
|
||||||
|
# 设置到本地播放器
|
||||||
|
if self.local_player:
|
||||||
|
self.local_player.set_folder(folder_path)
|
||||||
|
|
||||||
|
# 更新UI状态
|
||||||
|
self.update_local_mode_ui()
|
||||||
|
|
||||||
|
# 保存设置
|
||||||
|
self.save_local_mode_settings()
|
||||||
|
else:
|
||||||
|
logging.info("未选择任何文件夹")
|
||||||
|
|
||||||
|
def update_local_framerate_value(self, value):
|
||||||
|
"""更新本地模式帧率显示"""
|
||||||
|
if self.local_framerate_value:
|
||||||
|
self.local_framerate_value.setText(f"{value} fps")
|
||||||
|
|
||||||
|
# 更新本地播放器帧率
|
||||||
|
if self.local_player:
|
||||||
|
self.local_player.set_framerate(value)
|
||||||
|
|
||||||
|
def update_loop_playback(self, state):
|
||||||
|
"""更新循环播放设置"""
|
||||||
|
loop = (state == Qt.Checked)
|
||||||
|
|
||||||
|
# 更新本地播放器设置
|
||||||
|
if self.local_player:
|
||||||
|
self.local_player.set_loop(loop)
|
||||||
|
|
||||||
|
# 保存设置
|
||||||
|
self.save_local_mode_settings()
|
||||||
|
|
||||||
|
def play_local_images(self):
|
||||||
|
"""播放本地图像序列"""
|
||||||
|
if not self.local_mode_enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.is_playing:
|
||||||
|
# 如果正在播放,则暂停/恢复
|
||||||
|
if self.local_player:
|
||||||
|
if self.local_player.is_paused:
|
||||||
|
self.local_player.resume_playback()
|
||||||
|
self.play_button.setText("暂停")
|
||||||
|
else:
|
||||||
|
self.local_player.pause_playback()
|
||||||
|
self.play_button.setText("继续")
|
||||||
|
else:
|
||||||
|
# 开始播放
|
||||||
|
if self.local_player:
|
||||||
|
folder_path = self.folder_path_edit.text() if self.folder_path_edit else ""
|
||||||
|
|
||||||
|
if not folder_path:
|
||||||
|
QMessageBox.warning(self.parent, "错误", "请先选择图像文件夹")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 设置播放参数
|
||||||
|
framerate = self.local_framerate_slider.value() if self.local_framerate_slider else 15
|
||||||
|
loop = self.loop_playback_check.isChecked() if self.loop_playback_check else True
|
||||||
|
|
||||||
|
self.local_player.set_framerate(framerate)
|
||||||
|
self.local_player.set_loop(loop)
|
||||||
|
|
||||||
|
# 确保文件夹路径已设置
|
||||||
|
if not self.local_player.folder_path:
|
||||||
|
self.local_player.set_folder(folder_path)
|
||||||
|
|
||||||
|
# 开始播放
|
||||||
|
if self.local_player.start_playback():
|
||||||
|
self.is_playing = True
|
||||||
|
self.play_button.setText("暂停")
|
||||||
|
self.stop_button.setEnabled(True)
|
||||||
|
|
||||||
|
# 更新预览状态
|
||||||
|
if self.preview_status:
|
||||||
|
self.preview_status.setText("播放中...")
|
||||||
|
|
||||||
|
# 关键修复:发送信号通知主窗口更新UI,显示相机画面
|
||||||
|
logging.warning("====> 开始播放本地图像序列,发送信号通知主窗口更新UI")
|
||||||
|
# 先确保本地模式已启用
|
||||||
|
self.camera_manager.set_local_mode(True)
|
||||||
|
# 发送信号,通知主窗口更新UI
|
||||||
|
self.signal_local_mode_changed.emit(True)
|
||||||
|
|
||||||
|
logging.info(f"开始播放本地图像序列,帧率={framerate} fps,循环={loop}")
|
||||||
|
|
||||||
|
def stop_local_images(self):
|
||||||
|
"""停止播放本地图像序列"""
|
||||||
|
if self.local_player and self.is_playing:
|
||||||
|
self.local_player.stop_playback()
|
||||||
|
self.is_playing = False
|
||||||
|
self.play_button.setText("播放")
|
||||||
|
self.stop_button.setEnabled(False)
|
||||||
|
|
||||||
|
# 更新预览状态
|
||||||
|
if self.preview_status:
|
||||||
|
self.preview_status.setText("已停止")
|
||||||
|
|
||||||
|
logging.info("停止播放本地图像序列")
|
||||||
|
|
||||||
|
def handle_local_player_status(self, message):
|
||||||
|
"""处理本地播放器状态消息"""
|
||||||
|
if self.preview_status:
|
||||||
|
self.preview_status.setText(message)
|
||||||
|
|
||||||
|
def handle_local_player_error(self, message):
|
||||||
|
"""处理本地播放器错误消息"""
|
||||||
|
logging.error(f"本地图像播放器错误: {message}")
|
||||||
|
|
||||||
|
if self.preview_status:
|
||||||
|
self.preview_status.setText(f"错误: {message}")
|
||||||
|
|
||||||
|
QMessageBox.warning(self.parent, "错误", message)
|
||||||
|
|
||||||
|
def handle_local_player_progress(self, current, total):
|
||||||
|
"""处理本地播放器进度消息"""
|
||||||
|
if self.preview_status:
|
||||||
|
self.preview_status.setText(f"播放中... {current}/{total}")
|
||||||
|
|
||||||
|
def handle_local_player_frame(self, frame):
|
||||||
|
"""处理本地播放器帧准备好消息"""
|
||||||
|
# 确保本地模式已启用
|
||||||
|
if not self.local_mode_enabled or not self.local_player:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 添加明显的调试日志
|
||||||
|
logging.warning(f"====> 接收到本地图像帧: {frame.shape if frame is not None else 'None'}")
|
||||||
|
|
||||||
|
# 获取窗口ID (默认方法)
|
||||||
|
window_id = int(self.preview_frame.winId()) if self.preview_frame else 0
|
||||||
|
|
||||||
|
# 将帧传递给camera_manager处理
|
||||||
|
logging.warning(f"====> 传递给camera_manager处理,窗口ID: {window_id}")
|
||||||
|
success = self.camera_manager.handle_local_frame(frame, window_id)
|
||||||
|
if not success:
|
||||||
|
logging.error("相机管理器处理本地图像帧失败")
|
||||||
|
|
||||||
|
# 修复:查找主窗口的改进方式
|
||||||
|
try:
|
||||||
|
# 获取应用实例
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
app = QApplication.instance()
|
||||||
|
|
||||||
|
# 查找主窗口
|
||||||
|
from widgets.main_window import MainWindow
|
||||||
|
main_window = None
|
||||||
|
|
||||||
|
# 遍历所有顶层窗口
|
||||||
|
for window in app.topLevelWidgets():
|
||||||
|
logging.warning(f"====> 找到顶层窗口: {window.__class__.__name__}")
|
||||||
|
if isinstance(window, MainWindow):
|
||||||
|
main_window = window
|
||||||
|
logging.warning("====> 找到 MainWindow 实例")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 如果通过顶层窗口没找到,尝试通过父窗口查找
|
||||||
|
if not main_window and hasattr(self, 'parent') and self.parent:
|
||||||
|
parent = self.parent
|
||||||
|
# 循环查找父窗口,直到找到 MainWindow 或达到顶层
|
||||||
|
while parent:
|
||||||
|
logging.warning(f"====> 检查父窗口: {parent.__class__.__name__}")
|
||||||
|
if isinstance(parent, MainWindow):
|
||||||
|
main_window = parent
|
||||||
|
logging.warning("====> 在父窗口链中找到 MainWindow 实例")
|
||||||
|
break
|
||||||
|
if hasattr(parent, 'parent'):
|
||||||
|
parent = parent.parent()
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
# 如果找到了主窗口,更新相机显示
|
||||||
|
if main_window:
|
||||||
|
logging.warning(f"====> MainWindow 实例 ID: {id(main_window)}")
|
||||||
|
|
||||||
|
# 添加详细调试日志,查看主窗口的属性
|
||||||
|
logging.warning(f"====> MainWindow 的属性: {dir(main_window)}")
|
||||||
|
|
||||||
|
# 检查 camera_display 是否存在
|
||||||
|
has_camera_display = hasattr(main_window, 'camera_display')
|
||||||
|
logging.warning(f"====> MainWindow 是否有 camera_display 属性: {has_camera_display}")
|
||||||
|
|
||||||
|
if has_camera_display:
|
||||||
|
camera_display = main_window.camera_display
|
||||||
|
logging.warning(f"====> camera_display 对象 ID: {id(camera_display) if camera_display else 'None'}")
|
||||||
|
logging.warning(f"====> camera_display 是否为 None: {camera_display is None}")
|
||||||
|
|
||||||
|
# 检查 material_placeholder 是否存在
|
||||||
|
has_placeholder = hasattr(main_window, 'material_placeholder')
|
||||||
|
logging.warning(f"====> MainWindow 是否有 material_placeholder 属性: {has_placeholder}")
|
||||||
|
|
||||||
|
# 检查camera_enabled状态
|
||||||
|
current_camera_enabled = getattr(main_window, 'camera_enabled', False)
|
||||||
|
logging.warning(f"====> 当前 camera_enabled 状态: {current_camera_enabled}")
|
||||||
|
|
||||||
|
# 关键修复:确保camera_enabled为True
|
||||||
|
if hasattr(main_window, 'camera_enabled'):
|
||||||
|
logging.warning("====> 设置 camera_enabled 为 True")
|
||||||
|
main_window.camera_enabled = True
|
||||||
|
|
||||||
|
# 检查是否已调用过 init_camera_display 方法
|
||||||
|
if hasattr(main_window, 'init_camera_display'):
|
||||||
|
logging.warning("====> MainWindow 有 init_camera_display 方法")
|
||||||
|
# 尝试再次调用初始化方法
|
||||||
|
logging.warning("====> 尝试再次调用 init_camera_display 方法")
|
||||||
|
main_window.init_camera_display()
|
||||||
|
else:
|
||||||
|
logging.error("MainWindow 没有 init_camera_display 方法")
|
||||||
|
|
||||||
|
# 再次检查camera_display是否已创建
|
||||||
|
if hasattr(main_window, 'camera_display') and main_window.camera_display:
|
||||||
|
logging.warning("====> init_camera_display后,camera_display已创建")
|
||||||
|
# 确保相机显示组件可见
|
||||||
|
main_window.camera_display.setVisible(True)
|
||||||
|
main_window.camera_display.raise_()
|
||||||
|
|
||||||
|
# 隐藏占位符
|
||||||
|
if hasattr(main_window, 'material_placeholder') and main_window.material_placeholder:
|
||||||
|
main_window.material_placeholder.setVisible(False)
|
||||||
|
|
||||||
|
# 调用update_local_frame方法
|
||||||
|
if hasattr(main_window.camera_display, 'update_local_frame'):
|
||||||
|
main_window.camera_display.update_local_frame(frame)
|
||||||
|
logging.warning("====> 直接调用camera_display.update_local_frame成功")
|
||||||
|
else:
|
||||||
|
logging.error("camera_display没有update_local_frame方法")
|
||||||
|
|
||||||
|
# 强制更新UI
|
||||||
|
main_window.update_camera_ui(True)
|
||||||
|
else:
|
||||||
|
logging.error("MainWindow中没有找到camera_display组件或它仍然是None")
|
||||||
|
else:
|
||||||
|
logging.error("未找到MainWindow实例")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"尝试直接更新主窗口相机组件失败: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
logging.error(traceback.format_exc())
|
||||||
|
|
||||||
|
# 保留原来的第二种方法作为备选
|
||||||
|
try:
|
||||||
|
# 尝试使用Qt方式更新图像
|
||||||
|
if not success:
|
||||||
|
from PySide6.QtGui import QImage, QPixmap
|
||||||
|
|
||||||
|
# 将OpenCV的RGB图像转换为Qt的QImage
|
||||||
|
height, width, channel = frame.shape
|
||||||
|
bytes_per_line = channel * width
|
||||||
|
q_img = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888)
|
||||||
|
|
||||||
|
# 创建QPixmap
|
||||||
|
pixmap = QPixmap.fromImage(q_img)
|
||||||
|
|
||||||
|
# 获取主窗口实例
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
app = QApplication.instance()
|
||||||
|
from widgets.main_window import MainWindow
|
||||||
|
|
||||||
|
for window in app.topLevelWidgets():
|
||||||
|
if isinstance(window, MainWindow):
|
||||||
|
main_window = window
|
||||||
|
# 确保camera_enabled为True
|
||||||
|
if hasattr(main_window, 'camera_enabled'):
|
||||||
|
main_window.camera_enabled = True
|
||||||
|
|
||||||
|
# 尝试初始化相机显示
|
||||||
|
if hasattr(main_window, 'init_camera_display'):
|
||||||
|
main_window.init_camera_display()
|
||||||
|
|
||||||
|
if hasattr(main_window, 'camera_display') and main_window.camera_display:
|
||||||
|
# 确保相机显示组件可见
|
||||||
|
main_window.camera_display.setVisible(True)
|
||||||
|
main_window.camera_display.raise_()
|
||||||
|
|
||||||
|
# 给相机显示组件设置属性,强制更新
|
||||||
|
main_window.camera_display._current_pixmap = pixmap
|
||||||
|
main_window.camera_display.update() # 强制重绘
|
||||||
|
|
||||||
|
logging.warning("====> 通过设置_current_pixmap并强制更新成功")
|
||||||
|
|
||||||
|
# 隐藏占位符
|
||||||
|
if hasattr(main_window, 'material_placeholder'):
|
||||||
|
main_window.material_placeholder.setVisible(False)
|
||||||
|
|
||||||
|
# 强制更新UI
|
||||||
|
main_window.update_camera_ui(True)
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"尝试使用Qt方式更新图像失败: {e}")
|
||||||
|
import traceback
|
||||||
|
logging.error(traceback.format_exc())
|
||||||
|
|
||||||
|
def handle_local_player_completed(self):
|
||||||
|
"""处理本地播放器播放完成消息"""
|
||||||
|
if not self.local_player.loop:
|
||||||
|
self.is_playing = False
|
||||||
|
self.play_button.setText("播放")
|
||||||
|
|
||||||
|
# 更新预览状态
|
||||||
|
if self.preview_status:
|
||||||
|
self.preview_status.setText("播放完成")
|
||||||
|
|||||||
@ -75,6 +75,10 @@ class MainWindow(MainWindowUI):
|
|||||||
self._weight_processed = False # 新增:标记当前重量是否已处理,避免重复处理
|
self._weight_processed = False # 新增:标记当前重量是否已处理,避免重复处理
|
||||||
self._last_processed_weight = 0.0 # 新增:记录上次处理的重量
|
self._last_processed_weight = 0.0 # 新增:记录上次处理的重量
|
||||||
|
|
||||||
|
# 线径数据处理相关属性
|
||||||
|
self._last_diameter_value = 0 # 最后一次有效的线径值
|
||||||
|
self._diameter_stable = False # 保留此属性以避免引用错误
|
||||||
|
|
||||||
# 初始化数据加载状态标志
|
# 初始化数据加载状态标志
|
||||||
self._loading_data_in_progress = False # 数据加载状态标志,防止循环调用
|
self._loading_data_in_progress = False # 数据加载状态标志,防止循环调用
|
||||||
self._current_order_code = None # 存储当前订单号
|
self._current_order_code = None # 存储当前订单号
|
||||||
@ -393,6 +397,17 @@ class MainWindow(MainWindowUI):
|
|||||||
self.settings_window = SettingsWindow(self)
|
self.settings_window = SettingsWindow(self)
|
||||||
# 连接设置改变信号
|
# 连接设置改变信号
|
||||||
self.settings_window.settings_changed.connect(self.on_settings_changed)
|
self.settings_window.settings_changed.connect(self.on_settings_changed)
|
||||||
|
|
||||||
|
# 连接相机设置控制器的信号
|
||||||
|
if hasattr(self.settings_window, 'camera_settings'):
|
||||||
|
# 连接相机连接状态变化信号
|
||||||
|
self.settings_window.camera_settings.signal_camera_connection.connect(self.handle_camera_connection)
|
||||||
|
# 连接相机参数变化信号
|
||||||
|
self.settings_window.camera_settings.signal_camera_params_changed.connect(self.handle_camera_params_changed)
|
||||||
|
# 连接相机错误信号
|
||||||
|
self.settings_window.camera_settings.signal_camera_error.connect(self.handle_camera_error)
|
||||||
|
# 连接本地图像模式变更信号
|
||||||
|
self.settings_window.camera_settings.signal_local_mode_changed.connect(self.handle_local_mode_changed)
|
||||||
|
|
||||||
# 显示设置窗口
|
# 显示设置窗口
|
||||||
self.settings_window.show()
|
self.settings_window.show()
|
||||||
@ -418,7 +433,7 @@ class MainWindow(MainWindowUI):
|
|||||||
# 重新加载托盘号
|
# 重新加载托盘号
|
||||||
self.load_pallet_codes()
|
self.load_pallet_codes()
|
||||||
|
|
||||||
logging.info("设置已更新,重新加载配置并重新打开串口,已重新注册扫码器回调")
|
logging.info("设置已更新,重新加载配置并重新打开串口,已重新注册米电阻、线径和扫码器回调")
|
||||||
|
|
||||||
def handle_input(self):
|
def handle_input(self):
|
||||||
"""处理上料按钮点击事件"""
|
"""处理上料按钮点击事件"""
|
||||||
@ -770,7 +785,9 @@ class MainWindow(MainWindowUI):
|
|||||||
logging.info(f"输入的工程号: {gc_note}")
|
logging.info(f"输入的工程号: {gc_note}")
|
||||||
#判断是否是接口,如果不是接口直接添加如果是则走接口
|
#判断是否是接口,如果不是接口直接添加如果是则走接口
|
||||||
# 如果开启接口模式,则需要调用接口同步到业务库
|
# 如果开启接口模式,则需要调用接口同步到业务库
|
||||||
|
|
||||||
self.add_new_inspection_row(gc_note, self._current_order_code)
|
self.add_new_inspection_row(gc_note, self._current_order_code)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logging.warning("工程号为空")
|
logging.warning("工程号为空")
|
||||||
QMessageBox.warning(self, "输入提示", "请输入有效的工程号")
|
QMessageBox.warning(self, "输入提示", "请输入有效的工程号")
|
||||||
@ -2721,46 +2738,59 @@ class MainWindow(MainWindowUI):
|
|||||||
if "线径数据:" in data_str:
|
if "线径数据:" in data_str:
|
||||||
value_str = data_str.split("线径数据:")[1].strip()
|
value_str = data_str.split("线径数据:")[1].strip()
|
||||||
try:
|
try:
|
||||||
# 转换为浮点数
|
# 转换为浮点数,除以10000并保留三位小数
|
||||||
xj_value = float(value_str)
|
xj_value = round(float(value_str)/10000, 3)
|
||||||
|
|
||||||
# 查找线径对应的检验项配置
|
# 更新UI显示,实时回显最新测量值
|
||||||
xj_config = None
|
self.statusBar().showMessage(f"线径数据: {xj_value:.3f}", 2000)
|
||||||
enabled_configs = self.inspection_manager.get_enabled_configs()
|
|
||||||
for config in enabled_configs:
|
|
||||||
if config.get('name') == 'xj' or config.get('display_name') == '线径':
|
|
||||||
xj_config = config
|
|
||||||
break
|
|
||||||
|
|
||||||
if xj_config:
|
# 如果当前值不为0,记录为最后一次有效值
|
||||||
from dao.inspection_dao import InspectionDAO
|
if xj_value > 0:
|
||||||
inspection_dao = InspectionDAO()
|
self._last_diameter_value = xj_value
|
||||||
bccd, tccd = inspection_dao.get_xj_range(self._current_order_code)
|
logging.info(f"更新最后一次有效线径值: {xj_value:.3f}")
|
||||||
|
|
||||||
if bccd is not None and tccd is not None:
|
# 如果当前值为0,并且之前有非零值,说明产品已拿开,保存最后一次有效值
|
||||||
if bccd <= xj_value <= tccd:
|
elif xj_value == 0 and hasattr(self, '_last_diameter_value') and self._last_diameter_value > 0:
|
||||||
self.set_inspection_value('xj', xj_config, xj_value)
|
final_value = self._last_diameter_value
|
||||||
else:
|
logging.info(f"检测到线径值变为0,使用最后一次有效值 {final_value:.3f} 作为最终结果")
|
||||||
logging.warning(f"线径 {xj_value} 不在公差范围内 ({bccd} - {tccd})")
|
|
||||||
reply = QMessageBox.question(
|
|
||||||
self,
|
|
||||||
'确认保存',
|
|
||||||
f"线径 {xj_value} 不在公差范围内 ({bccd} - {tccd}),\n是否继续保存?",
|
|
||||||
QMessageBox.Yes | QMessageBox.No,
|
|
||||||
QMessageBox.No
|
|
||||||
)
|
|
||||||
if reply == QMessageBox.Yes:
|
|
||||||
self.set_inspection_value('xj', xj_config, xj_value)
|
|
||||||
else:
|
|
||||||
logging.info(f"用户取消保存超出范围的线径值: {xj_value}")
|
|
||||||
# TODO:后续根据实际情况实现
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
logging.info(f"未找到订单 {self._current_order_code} 的线径公差范围,直接保存值 {xj_value}")
|
|
||||||
self.set_inspection_value('xj', xj_config, xj_value)
|
|
||||||
else:
|
|
||||||
logging.warning("未找到线径对应的检验项配置")
|
|
||||||
|
|
||||||
|
# 查找线径对应的检验项配置
|
||||||
|
xj_config = None
|
||||||
|
enabled_configs = self.inspection_manager.get_enabled_configs()
|
||||||
|
for config in enabled_configs:
|
||||||
|
if config.get('name') == 'xj' or config.get('display_name') == '线径':
|
||||||
|
xj_config = config
|
||||||
|
break
|
||||||
|
|
||||||
|
if xj_config:
|
||||||
|
from dao.inspection_dao import InspectionDAO
|
||||||
|
inspection_dao = InspectionDAO()
|
||||||
|
bccd, tccd = inspection_dao.get_xj_range(self._current_order_code)
|
||||||
|
|
||||||
|
if bccd is not None and tccd is not None:
|
||||||
|
if bccd - 0.5 <= final_value <= tccd + 0.5: # 允许±0.5的误差范围
|
||||||
|
self.set_inspection_value('xj', xj_config, final_value)
|
||||||
|
else:
|
||||||
|
logging.warning(f"线径 {final_value:.3f} 不在公差范围内 ({bccd} - {tccd}),误差超过±0.5")
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
self,
|
||||||
|
'确认保存',
|
||||||
|
f"线径 {final_value:.3f} 不在公差范围内 ({bccd} - {tccd}),\n是否继续保存?",
|
||||||
|
QMessageBox.Yes | QMessageBox.No,
|
||||||
|
QMessageBox.No
|
||||||
|
)
|
||||||
|
if reply == QMessageBox.Yes:
|
||||||
|
self.set_inspection_value('xj', xj_config, final_value)
|
||||||
|
else:
|
||||||
|
logging.info(f"用户取消保存超出范围的线径值: {final_value:.3f}")
|
||||||
|
else:
|
||||||
|
logging.info(f"未找到订单 {self._current_order_code} 的线径公差范围,直接保存值 {final_value:.3f}")
|
||||||
|
self.set_inspection_value('xj', xj_config, final_value)
|
||||||
|
|
||||||
|
# 重置最后一次有效值,避免重复处理
|
||||||
|
self._last_diameter_value = 0
|
||||||
|
else:
|
||||||
|
logging.warning("未找到线径对应的检验项配置")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logging.warning(f"线径数据格式错误: {value_str}")
|
logging.warning(f"线径数据格式错误: {value_str}")
|
||||||
else:
|
else:
|
||||||
@ -2850,7 +2880,7 @@ class MainWindow(MainWindowUI):
|
|||||||
data_row = None
|
data_row = None
|
||||||
for row in range(2, self.process_table.rowCount()):
|
for row in range(2, self.process_table.rowCount()):
|
||||||
cell_item = self.process_table.item(row, col_index)
|
cell_item = self.process_table.item(row, col_index)
|
||||||
if not cell_item or not cell_item.text().strip():
|
if not cell_item or not cell_item.text().strip() or (data_type == 'xj' and cell_item.text().strip() == '0'):
|
||||||
data_row = row
|
data_row = row
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -2882,8 +2912,11 @@ class MainWindow(MainWindowUI):
|
|||||||
# 格式化值并设置单元格
|
# 格式化值并设置单元格
|
||||||
formatted_value = str(value)
|
formatted_value = str(value)
|
||||||
if config.get('data_type') == 'number':
|
if config.get('data_type') == 'number':
|
||||||
# 格式化数字,保留2位小数
|
# 格式化数字,线径保留3位小数,其他保留2位小数
|
||||||
formatted_value = f"{value:.2f}"
|
if data_type == 'xj':
|
||||||
|
formatted_value = f"{value:.3f}"
|
||||||
|
else:
|
||||||
|
formatted_value = f"{value:.2f}"
|
||||||
|
|
||||||
# 设置单元格值
|
# 设置单元格值
|
||||||
item = QTableWidgetItem(formatted_value)
|
item = QTableWidgetItem(formatted_value)
|
||||||
@ -3017,7 +3050,7 @@ class MainWindow(MainWindowUI):
|
|||||||
"""初始化相机并显示画面"""
|
"""初始化相机并显示画面"""
|
||||||
try:
|
try:
|
||||||
if not self.camera_enabled:
|
if not self.camera_enabled:
|
||||||
return
|
self.material_placeholder.setText("相机功能已禁用")
|
||||||
|
|
||||||
logging.info("开始初始化相机...")
|
logging.info("开始初始化相机...")
|
||||||
|
|
||||||
@ -3083,7 +3116,16 @@ class MainWindow(MainWindowUI):
|
|||||||
is_camera_ready: 相机是否准备就绪
|
is_camera_ready: 相机是否准备就绪
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if is_camera_ready and self.camera_enabled:
|
from widgets.camera_manager import CameraManager
|
||||||
|
camera_manager = CameraManager.get_instance()
|
||||||
|
local_mode_active = getattr(camera_manager, 'local_mode', False)
|
||||||
|
|
||||||
|
# 关键修复:如果本地模式激活,则强制认为相机已就绪
|
||||||
|
camera_feature_enabled = self.camera_enabled or local_mode_active
|
||||||
|
if local_mode_active:
|
||||||
|
is_camera_ready = True
|
||||||
|
|
||||||
|
if is_camera_ready and camera_feature_enabled:
|
||||||
# 显示相机画面,隐藏占位符
|
# 显示相机画面,隐藏占位符
|
||||||
if self.camera_display:
|
if self.camera_display:
|
||||||
self.camera_display.setVisible(True)
|
self.camera_display.setVisible(True)
|
||||||
@ -3092,7 +3134,7 @@ class MainWindow(MainWindowUI):
|
|||||||
self.material_placeholder.setVisible(False)
|
self.material_placeholder.setVisible(False)
|
||||||
logging.info("相机UI已更新:显示相机画面")
|
logging.info("相机UI已更新:显示相机画面")
|
||||||
else:
|
else:
|
||||||
# 隐藏相机画面,显示占位符
|
# 显示占位符,隐藏相机画面
|
||||||
if self.camera_display:
|
if self.camera_display:
|
||||||
self.camera_display.setVisible(False)
|
self.camera_display.setVisible(False)
|
||||||
if self.material_placeholder:
|
if self.material_placeholder:
|
||||||
@ -3104,7 +3146,7 @@ class MainWindow(MainWindowUI):
|
|||||||
self.material_placeholder.setText("相机未就绪")
|
self.material_placeholder.setText("相机未就绪")
|
||||||
logging.info(f"相机UI已更新:显示占位符 (相机启用={self.camera_enabled}, 相机就绪={is_camera_ready})")
|
logging.info(f"相机UI已更新:显示占位符 (相机启用={self.camera_enabled}, 相机就绪={is_camera_ready})")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"更新相机UI失败: {str(e)}")
|
logging.error(f"更新相机UI时出错: {e}")
|
||||||
|
|
||||||
def handle_camera_status(self, is_connected, message):
|
def handle_camera_status(self, is_connected, message):
|
||||||
"""处理相机状态变化"""
|
"""处理相机状态变化"""
|
||||||
@ -3331,4 +3373,103 @@ class MainWindow(MainWindowUI):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"删除数据失败: {str(e)}")
|
logging.error(f"删除数据失败: {str(e)}")
|
||||||
QMessageBox.critical(self, "错误", f"删除数据失败: {str(e)}")
|
QMessageBox.critical(self, "错误", f"删除数据失败: {str(e)}")
|
||||||
|
|
||||||
|
# 在MainWindow类中添加处理本地图像模式变更的代码
|
||||||
|
|
||||||
|
# 在init_settings_window()方法中添加信号连接
|
||||||
|
def init_settings_window(self):
|
||||||
|
# 创建设置窗口
|
||||||
|
from widgets.settings_window import SettingsWindow
|
||||||
|
self.settings_window = SettingsWindow(self)
|
||||||
|
self.settings_window.setWindowTitle("系统设置")
|
||||||
|
# 设置窗口大小
|
||||||
|
self.settings_window.resize(900, 650)
|
||||||
|
|
||||||
|
# 连接设置变更信号
|
||||||
|
self.settings_window.settings_changed.connect(self.handle_settings_changed)
|
||||||
|
|
||||||
|
# 连接相机设置控制器
|
||||||
|
if hasattr(self.settings_window, 'camera_settings'):
|
||||||
|
# 连接相机连接状态变化信号
|
||||||
|
self.settings_window.camera_settings.signal_camera_connection.connect(self.handle_camera_connection)
|
||||||
|
# 连接相机参数变化信号
|
||||||
|
self.settings_window.camera_settings.signal_camera_params_changed.connect(self.handle_camera_params_changed)
|
||||||
|
# 连接相机错误信号
|
||||||
|
self.settings_window.camera_settings.signal_camera_error.connect(self.handle_camera_error)
|
||||||
|
# 连接本地图像模式变更信号
|
||||||
|
self.settings_window.camera_settings.signal_local_mode_changed.connect(self.handle_local_mode_changed)
|
||||||
|
|
||||||
|
# 添加处理本地图像模式变更的方法
|
||||||
|
def handle_local_mode_changed(self, enabled):
|
||||||
|
"""处理本地图像模式变更
|
||||||
|
|
||||||
|
Args:
|
||||||
|
enabled: 是否启用本地图像模式
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logging.warning(f"====> 主窗口处理本地图像模式变更: {enabled}")
|
||||||
|
|
||||||
|
# 关键修复:只要启用本地模式,就认为相机功能已就绪,并强制更新UI
|
||||||
|
if enabled:
|
||||||
|
self.camera_enabled = True
|
||||||
|
self.update_camera_ui(True) # 强制刷新UI,隐藏占位符
|
||||||
|
if self.camera_display:
|
||||||
|
self.camera_display.setVisible(True) # 确保相机显示区域可见
|
||||||
|
|
||||||
|
# 获取相机管理器
|
||||||
|
from widgets.camera_manager import CameraManager
|
||||||
|
camera_manager = CameraManager.get_instance()
|
||||||
|
|
||||||
|
# 设置相机管理器的本地模式
|
||||||
|
camera_manager.set_local_mode(enabled)
|
||||||
|
|
||||||
|
# 如果当前在主页面,更新相机显示
|
||||||
|
if hasattr(self, 'camera_display') and self.camera_display and self.stacked_widget.currentWidget() == self.central_widget:
|
||||||
|
# 确保相机显示组件可见
|
||||||
|
self.camera_display.setVisible(True)
|
||||||
|
self.camera_display.raise_()
|
||||||
|
if self.material_placeholder:
|
||||||
|
self.material_placeholder.setVisible(False)
|
||||||
|
|
||||||
|
# 确保相机显示组件连接了本地图像帧信号
|
||||||
|
if enabled:
|
||||||
|
# 本地模式下,停止实时相机显示但保持相机显示组件可见
|
||||||
|
if camera_manager.isGrabbing and not camera_manager.local_mode:
|
||||||
|
self.camera_display.stop_display()
|
||||||
|
|
||||||
|
# 确保本地图像帧能够被显示
|
||||||
|
if hasattr(self.settings_window, 'camera_settings'):
|
||||||
|
camera_settings = self.settings_window.camera_settings
|
||||||
|
if hasattr(camera_settings, 'local_player') and camera_settings.local_player:
|
||||||
|
# 直接连接本地播放器的信号到相机显示组件
|
||||||
|
try:
|
||||||
|
if hasattr(camera_settings.local_player, 'signal_frame_ready'):
|
||||||
|
# 先断开可能存在的连接,避免重复
|
||||||
|
try:
|
||||||
|
camera_settings.local_player.signal_frame_ready.disconnect()
|
||||||
|
except Exception as e:
|
||||||
|
logging.debug(f"断开信号连接时发生异常(这是正常的): {e}")
|
||||||
|
|
||||||
|
# 建立新的连接
|
||||||
|
camera_settings.local_player.signal_frame_ready.connect(self.camera_display.update_local_frame)
|
||||||
|
logging.warning("====> 已直接连接本地播放器信号到相机显示组件")
|
||||||
|
|
||||||
|
# 确认显示区域已准备好
|
||||||
|
self.camera_display._current_pixmap = None
|
||||||
|
self.camera_display.update()
|
||||||
|
|
||||||
|
# 如果已在播放,确保信号重新连接后再次触发更新
|
||||||
|
if camera_settings.is_playing and camera_settings.local_player.last_frame is not None:
|
||||||
|
QTimer.singleShot(100, lambda: self.camera_display.update_local_frame(camera_settings.local_player.last_frame))
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"连接本地播放器信号失败: {e}")
|
||||||
|
|
||||||
|
logging.warning("====> 已切换到本地图像模式,等待本地图像播放")
|
||||||
|
else:
|
||||||
|
# 非本地模式下,如果相机已打开则开始显示
|
||||||
|
if camera_manager.isOpen:
|
||||||
|
self.camera_display.start_display()
|
||||||
|
logging.warning("====> 已切换到实时相机模式")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"处理本地图像模式变更时发生错误: {e}")
|
||||||
@ -194,6 +194,18 @@ class SerialSettingsWidget(SerialSettingsUI):
|
|||||||
if index >= 0:
|
if index >= 0:
|
||||||
self.xj_parity_combo.setCurrentIndex(index)
|
self.xj_parity_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
|
# 设置查询指令
|
||||||
|
xj_query_cmd = xj_config.get('query_cmd', '01 41 0d')
|
||||||
|
self.xj_query_cmd.setText(xj_query_cmd)
|
||||||
|
|
||||||
|
# 设置查询间隔
|
||||||
|
xj_query_interval = xj_config.get('query_interval', 5)
|
||||||
|
self.xj_query_interval.setValue(xj_query_interval)
|
||||||
|
|
||||||
|
# 设置自动查询
|
||||||
|
xj_auto_query = xj_config.get('auto_query', True)
|
||||||
|
self.xj_auto_query.setChecked(xj_auto_query)
|
||||||
|
|
||||||
# 加载扫码器设置
|
# 加载扫码器设置
|
||||||
scanner_config = self.config.get_config('scanner')
|
scanner_config = self.config.get_config('scanner')
|
||||||
if scanner_config:
|
if scanner_config:
|
||||||
@ -272,12 +284,18 @@ class SerialSettingsWidget(SerialSettingsUI):
|
|||||||
xj_data_bits = int(self.xj_data_bits_combo.currentText())
|
xj_data_bits = int(self.xj_data_bits_combo.currentText())
|
||||||
xj_stop_bits = float(self.xj_stop_bits_combo.currentText())
|
xj_stop_bits = float(self.xj_stop_bits_combo.currentText())
|
||||||
xj_parity = self.xj_parity_combo.currentData()
|
xj_parity = self.xj_parity_combo.currentData()
|
||||||
|
xj_query_cmd = self.xj_query_cmd.text().strip()
|
||||||
|
xj_query_interval = self.xj_query_interval.value()
|
||||||
|
xj_auto_query = self.xj_auto_query.isChecked()
|
||||||
|
|
||||||
xj_config = {
|
xj_config = {
|
||||||
'port': xj_baud,
|
'port': xj_baud,
|
||||||
'data_bits': xj_data_bits,
|
'data_bits': xj_data_bits,
|
||||||
'stop_bits': xj_stop_bits,
|
'stop_bits': xj_stop_bits,
|
||||||
'parity': xj_parity
|
'parity': xj_parity,
|
||||||
|
'query_cmd': xj_query_cmd,
|
||||||
|
'query_interval': xj_query_interval,
|
||||||
|
'auto_query': xj_auto_query
|
||||||
}
|
}
|
||||||
|
|
||||||
# 只有当用户选择了串口时才保存串口配置
|
# 只有当用户选择了串口时才保存串口配置
|
||||||
@ -394,7 +412,27 @@ class SerialSettingsWidget(SerialSettingsUI):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
QMessageBox.information(self, "测试成功", f"串口 {port} 打开成功")
|
# 尝试发送查询指令
|
||||||
|
query_cmd = self.xj_query_cmd.text()
|
||||||
|
if query_cmd:
|
||||||
|
try:
|
||||||
|
# 转换查询指令为字节
|
||||||
|
cmd_bytes = bytes.fromhex(query_cmd.replace(' ', ''))
|
||||||
|
self.serial_manager.write_data(port, cmd_bytes)
|
||||||
|
time.sleep(0.5) # 等待响应
|
||||||
|
|
||||||
|
# 读取响应
|
||||||
|
response = self.serial_manager.read_data(port)
|
||||||
|
if response:
|
||||||
|
# 将字节转换为十六进制字符串
|
||||||
|
hex_str = ' '.join(f'{b:02X}' for b in response)
|
||||||
|
QMessageBox.information(self, "测试成功", f"串口打开成功,收到响应:\n{hex_str}")
|
||||||
|
else:
|
||||||
|
QMessageBox.information(self, "测试成功", "串口打开成功,但未收到响应")
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.warning(self, "测试结果", f"串口打开成功,但发送指令失败: {e}")
|
||||||
|
else:
|
||||||
|
QMessageBox.information(self, "测试成功", "串口打开成功")
|
||||||
|
|
||||||
# 关闭串口
|
# 关闭串口
|
||||||
self.serial_manager.close_port(port)
|
self.serial_manager.close_port(port)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user