feat:提交一下关于线径获取和相机回显的代码

This commit is contained in:
zhu-mengmeng 2025-07-07 15:33:56 +08:00
parent ef1a624099
commit 32740b7229
19 changed files with 5487 additions and 3267 deletions

View File

@ -23,6 +23,14 @@
- 提供各种工具类如配置加载器、Modbus通信、串口管理等
- 采用单例模式确保资源共享
5. **硬件集成层**
- **相机子系统**基于海康威视SDK进行集成
- 相机管理器CameraManager单例模式管理相机生命周期
- 相机显示组件CameraDisplayWidget用于实时显示相机画面
- 相机设置控制器CameraSettingsWidget管理相机参数设置
- **串口通信**:与称重设备、扫描器等外设通信
- **Modbus通信**与PLC设备通信
## 技术栈
1. **前端技术**
@ -38,9 +46,10 @@
3. **通信技术**
- Modbus TCP用于与PLC设备通信
- 串口通信用于与称重设备、条码扫描器等外设通信
- 海康威视SDK用于相机图像采集和处理
4. **设计模式**
- 单例模式(配置加载器、监控器等)
- 单例模式(配置加载器、监控器、相机管理器等)
- DAO模式数据访问
- 观察者模式(信号槽)
- 工厂模式(数据库连接)
@ -55,30 +64,75 @@
- `db/`:包含数据库文件
- `config/`:包含配置文件
- `logs/`:包含日志文件
- `camera/`包含相机模块和SDK接口类
2. **核心文件**
- `main.py`:程序入口点
- `widgets/login_widget.py`:登录窗口控制器
- `widgets/main_window.py`:主窗口控制器
- `widgets/camera_manager.py`:相机管理器
- `widgets/camera_display_widget.py`:相机显示组件
- `widgets/camera_settings_widget.py`:相机设置控制器
- `utils/config_loader.py`:配置加载器
- `utils/modbus_utils.py`Modbus通信工具
- `utils/sql_utils.py`:数据库工具
- `camera/CamOperation_class.py`:相机操作类
- `camera/MvCameraControl_class.py`海康威视相机控制SDK
- `utils/local_image_player.py`:本地图像序列播放器
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. **用户认证**:支持用户登录和权限控制
2. **产线监控**:实时监控产线状态、电力消耗等
3. **数据采集**:采集称重数据、检验数据等
4. **报表生成**:生成各类统计报表
5. **设备通信**与PLC、称重设备等通信
6. **多模式支持**:支持单机模式和接口模式
4. **相机集成**:支持实时图像采集、显示和参数调整
5. **报表生成**:生成各类统计报表
6. **设备通信**与PLC、称重设备等通信
7. **多模式支持**:支持单机模式和接口模式
## 运行环境
@ -98,7 +152,26 @@
- 默认使用SQLite数据库位于`db/jtDB.db`
- 可在`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
```
@ -110,3 +183,4 @@
1. 添加新的数据源:扩展`utils/sql_utils.py`
2. 添加新的设备通信协议:参考`utils/modbus_utils.py`
3. 添加新的UI界面在`ui/`目录下创建新的UI类在`widgets/`目录下创建对应的控制器类
4. 扩展相机功能:修改`widgets/camera_manager.py`和`camera/CamOperation_class.py`

8
app_config.json Normal file
View File

@ -0,0 +1,8 @@
{
"local_image_mode": {
"enabled": true,
"folder_path": "/Users/meng/Downloads/images",
"framerate": 15,
"loop": true
}
}

View File

@ -46,7 +46,19 @@
"enabled": false,
"default_exposure": 20000,
"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": {
"host": "localhost",
@ -79,6 +91,19 @@
"stop_bits": 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": {
"code": "scanner",
"data_bits": 8,

Binary file not shown.

View File

@ -18,3 +18,4 @@ pandas>=1.4.0 # 数据分析和处理
pillow>=9.0.0 # 图像处理,用于相机功能
pynput>=1.7.6 # 键盘监听
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
View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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}")

View File

@ -20,12 +20,27 @@ class SerialSettingsUI(QWidget):
main_layout = QVBoxLayout(self)
# 创建全局启用选项
enable_layout = QHBoxLayout()
enable_layout = QVBoxLayout()
# 第一行:串口功能和键盘监听
enable_row1 = QHBoxLayout()
self.enable_serial_checkbox = QCheckBox("启用串口功能")
self.enable_keyboard_checkbox = QCheckBox("启用键盘监听 (PageUp 触发米电阻查询)")
enable_layout.addWidget(self.enable_serial_checkbox)
enable_layout.addWidget(self.enable_keyboard_checkbox)
enable_layout.addStretch()
self.enable_keyboard_checkbox = QCheckBox("启用键盘监听")
enable_row1.addWidget(self.enable_serial_checkbox)
enable_row1.addWidget(self.enable_keyboard_checkbox)
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)
# # 创建串口设置组
@ -116,6 +131,20 @@ class SerialSettingsUI(QWidget):
self.xj_parity_combo.addItem(parity[0], parity[1])
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_layout = QFormLayout(scanner_group)

View File

@ -1,7 +1,7 @@
from PySide6.QtWidgets import (
QWidget, QLabel, QLineEdit, QPushButton, QComboBox, QGridLayout, QHBoxLayout, QVBoxLayout,
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.QtCore import Qt, Signal, QSize
@ -159,6 +159,72 @@ class SettingsUI(QWidget):
self.camera_params_group.setLayout(self.camera_params_layout)
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()
@ -192,7 +258,14 @@ class SettingsUI(QWidget):
self.preview_frame.setMinimumHeight(200)
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_status)
self.preview_group.setLayout(self.preview_layout)
self.camera_layout.addWidget(self.preview_group)

265
utils/local_image_player.py Normal file
View 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("====> 播放线程结束")

View File

@ -62,6 +62,9 @@ class SerialManager:
# 是否自动查询米电阻数据默认为False只通过PageUp键触发
self.auto_query_mdz = False
# 是否自动查询线径数据默认为True开启自动查询
self.auto_query_xj = True
logging.info("初始化 SerialManager")
# 加载配置
@ -73,11 +76,13 @@ class SerialManager:
try:
# 初始化键盘监听器
self.keyboard_listener = KeyboardListener()
# 从配置中获取触发键
# 从配置中获取米电阻触发键
trigger_key = self.config.get_value('serial.keyboard.trigger_key', 'Key.page_up')
# 注册触发键回调
# 注册米电阻触发键回调
self.keyboard_listener.register_callback(trigger_key, self.trigger_resistance_query)
logging.info(f"已注册{trigger_key}按键回调用于触发米电阻查询")
# 注意:不在这里启动键盘监听器,而是在点击"开始"按钮时启动
except Exception as 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.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"米电阻自动查询: {'开启' if self.auto_query_mdz else '关闭'}")
logging.info(f"线径自动查询: {'开启' if self.auto_query_xj else '关闭'}")
except Exception as e:
logging.error(f"加载配置出错: {e}")
# 设置默认值
@ -136,6 +145,7 @@ class SerialManager:
self.scanner_config = {'port': 9600, 'ser': 'COM4'}
self.stable_threshold = 10
self.auto_query_mdz = False
self.auto_query_xj = True # 默认为True确保线径自动查询开启
logging.info(f"使用默认配置,数据文件: {self.data_file}")
def _detect_macos_ports(self):
@ -717,26 +727,22 @@ class SerialManager:
def start_keyboard_listener(self):
"""启动键盘监听"""
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:
logging.warning("键盘监听器未初始化,无法启动")
return False
# 从配置中获取触发键
config = ConfigLoader.get_instance()
trigger_key = config.get_value('serial.keyboard.trigger_key', 'Key.page_up')
# 从配置中获取米电阻触发键
trigger_key = self.config.get_value('serial.keyboard.trigger_key', 'Key.page_up')
# 不再获取线径触发键
# 确保已注册触发键回调
# 确保已注册米电阻触发键回调
if trigger_key not in self.keyboard_listener.callbacks:
self.keyboard_listener.register_callback(trigger_key, self.trigger_resistance_query)
logging.info(f"已注册{trigger_key}按键回调用于触发米电阻数据查询")
# 移除线径触发键回调注册代码
# 启动键盘监听
result = self.keyboard_listener.start()
if result:
@ -790,123 +796,56 @@ class SerialManager:
if key in self.callbacks:
logging.warning(f"覆盖已存在的回调函数: {key}")
self.callbacks[key] = callback
logging.info(f"已注册回调函数: {key}")
logging.info(f"已注册回调函数: {key}, 回调对象类型: {callback.__self__.__class__.__name__}")
except Exception as e:
logging.error(f"注册回调失败: {e}")
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:
byte_data = bytes.fromhex(query_cmd_hex.replace(' ', ''))
logging.info(f"[SerialManager] 准备发送米电阻查询指令: {byte_data.hex(' ').upper()} 到端口 {mdz_port_name}")
print(f"\n[米电阻查询] 准备发送查询指令: {byte_data.hex(' ').upper()}\n")
# 检查米电阻配置是否存在
if not self.mdz_config:
logging.warning("未找到米电阻配置,无法触发查询")
return False
# 检查 SerialManager 是否已管理此端口且已打开
if self.is_port_open(mdz_port_name):
logging.info(f"[SerialManager] 米电阻串口 {mdz_port_name} 已由 SerialManager 管理并打开,直接发送指令。")
print(f"\n[米电阻查询] 串口 {mdz_port_name} 已打开,直接发送指令\n")
if self.write_data(mdz_port_name, byte_data):
logging.info(f"[SerialManager] 指令已发送到 {mdz_port_name} (通过已打开的串口)。响应将由读取线程处理。")
print("\n[米电阻查询] 指令发送成功,等待响应\n")
# 当串口已打开时,指令发送后,响应会由 _read_resistance_thread 捕获并处理。
# _read_resistance_thread 内部的 _process_mdz_response 会负责更新 self.data,
# 调用 _write_data_to_file 和 _notify_callbacks。
# 因此,这里不需要再显式地 sleep 后调用 _write_data_to_file 和 _notify_callbacks
# 以避免数据竞争或重复通知。
return # 指令已发送,等待线程处理
else:
logging.warning(f"[SerialManager] 向已打开的串口 {mdz_port_name} 发送指令失败。将尝试临时打开。")
print("\n[米电阻查询] 指令发送失败,尝试临时打开串口\n")
# 检查米电阻串口是否已打开
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
# 如果串口未被 SerialManager 管理或发送失败,则尝试临时打开
logging.info(f"[SerialManager] 米电阻串口 {mdz_port_name} 未打开或发送失败。尝试临时打开并查询...")
print(f"\n[米电阻查询] 串口 {mdz_port_name} 未打开,尝试临时打开\n")
temp_ser = serial.Serial(
port=mdz_port_name,
baudrate=baud_rate,
bytesize=data_bits,
stopbits=stop_bits,
parity=parity,
timeout=timeout
)
if not temp_ser.is_open:
temp_ser.open()
# 从配置获取查询命令,如果没有则使用默认命令
query_cmd = self.mdz_config.get('query_cmd', '01 03 00 01 00 07 55 C8')
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)
# 转换为字节数据
try:
byte_data = bytes.fromhex(query_cmd.replace(' ', ''))
except ValueError as e:
logging.error(f"米电阻查询命令格式错误: {query_cmd}, 错误: {e}")
return False
if response_bytes:
logging.info(f"[SerialManager] 收到来自 {mdz_port_name} (临时串口) 的响应: {response_bytes.hex(' ').upper()}")
print(f"\n[米电阻查询] 收到响应: {response_bytes.hex(' ').upper()}\n")
# 发送查询命令
result = self.write_data(ser_name, byte_data)
if result:
logging.info(f"已向米电阻串口 {ser_name} 发送查询命令: {query_cmd}")
# 将响应交给标准的处理函数
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 及文件写入和通知,这里无需额外操作。
# 特殊情况:如果触发了查询,但串口没有响应,尝试模拟一个合理的响应
# 这里我们先等待一段时间,让设备有机会响应
time.sleep(0.5)
# 如果没有数据可读,或者读取的数据不包含有效的米电阻值
if self.serial_ports[ser_name].in_waiting == 0:
logging.warning("米电阻串口未返回数据,检查设备连接")
# 在这里我们不再自动提供模拟数据,而是由用户决定是否重试
return True
else:
logging.warning(f"[SerialManager] 未收到来自 {mdz_port_name} (临时串口) 的响应。")
print("\n[米电阻查询] 未收到响应\n")
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")
logging.error(f"向米电阻串口 {ser_name} 发送查询命令失败")
return False
except Exception as e:
logging.error(f"[SerialManager] 触发米电阻查询时发生未知错误 (临时查询): {e}", exc_info=True)
print(f"\n[米电阻查询] 未知错误: {e}\n")
finally:
if temp_ser and temp_ser.is_open:
temp_ser.close()
logging.info("[SerialManager] 米电阻数据查询流程结束。")
print("\n[米电阻查询] 查询流程结束\n")
logging.error(f"触发米电阻查询时出错: {e}")
return False
def _notify_callbacks(self, port_name, value):
"""通知所有相关回调函数"""
@ -1045,20 +984,41 @@ class SerialManager:
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.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'))
):
logging.warning("检测到在macOS系统上配置了Windows格式的COM端口这些端口将无法正常打开")
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():
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):
try:
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} 成功")
else:
logging.error(f"自动打开线径串口 {port_name} 失败")
@ -1143,16 +1103,38 @@ class SerialManager:
"""
try:
logging.info(f"[{port_name}] 线径线程启动")
logging.info(f"线径自动查询已开启,将持续发送查询指令获取数据")
while self.running_flags.get(port_name, False):
if not self.is_port_open(port_name):
time.sleep(0.1)
continue
# 检查是否有数据可读
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)
try:
# 从配置获取查询命令,如果没有则使用默认命令
query_cmd = self.xj_config.get('query_cmd', '01 41 0d')
# 发送查询指令
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)
except Exception as e:
@ -1182,7 +1164,22 @@ class SerialManager:
# 更新数据
self.data['xj'] = xj_value
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})"})
return True
except ValueError:
logging.warning(f"线径数据字符串 '{number_str}' 无法转换为浮点数")

View File

@ -1,17 +1,18 @@
import sys
import os
import logging
import numpy as np
from ctypes import *
# 确定使用哪个UI框架
try:
from PySide6.QtWidgets import QWidget, QVBoxLayout, QFrame, QSizePolicy
from PySide6.QtCore import Qt, Signal, QSize
from PySide6.QtGui import QPalette, QColor
from PySide6.QtCore import Qt, Signal, QSize, QTimer
from PySide6.QtGui import QPalette, QColor, QImage, QPixmap
except ImportError:
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QFrame, QSizePolicy
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"))
@ -53,6 +54,11 @@ class CameraDisplayWidget(QWidget):
# 设置最小尺寸
self.setMinimumSize(QSize(320, 240))
# 添加本地图像处理功能
self._current_pixmap = None
self._is_local_frame_connected = False
self._connect_local_frame_signal()
def init_ui(self):
"""初始化UI - 只包含相机显示框架"""
# 创建布局
@ -75,6 +81,152 @@ class CameraDisplayWidget(QWidget):
# 设置布局
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):
"""开始显示相机图像"""
if not self.camera_manager.isOpen:
@ -139,8 +291,11 @@ class CameraDisplayWidget(QWidget):
# 记录大小变化
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()
# 使用新尺寸重新开始显示

View File

@ -4,6 +4,8 @@ import logging
import json
import ctypes
from ctypes import *
import numpy as np
import cv2
# 添加相机模块路径
sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), "camera"))
@ -36,23 +38,74 @@ class CameraManager:
else:
CameraManager._instance = self
# 初始化变量
self.deviceList = None
self.cam = MvCamera()
self.nSelCamIndex = -1
self.obj_cam_operation = None
# 初始化属性
self.isOpen = False
self.isGrabbing = False
self.last_device_index = -1
self.device_list = []
self.current_window_id = 0
# 初始化SDK (只在第一次时初始化)
if not CameraManager._initialized:
MvCamera.MV_CC_Initialize()
CameraManager._initialized = True
logging.info("相机SDK已初始化")
# 本地图像模式相关
self.local_mode = False
self.last_frame = None
self.has_real_camera = False # 是否有真实相机连接
# 初始化SDK
self.obj_cam_operation = CameraOperation()
# 将单例标记为已初始化
CameraManager._initialized = True
# 初始化日志
logging.info("相机管理器初始化")
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:
# 枚举设备
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已正确加载
from camera.MvCameraControl_class import MvCamCtrldll
if MvCamCtrldll is None:
@ -182,15 +235,17 @@ class CameraManager:
# 添加详细日志
logging.debug(f"枚举到的设备数量: {len(devices_info)}")
self.device_list = devices_info
return devices_info
except Exception as e:
error_msg = f"枚举设备时发生异常: {str(e)}"
error_msg = f"枚举相机设备时发生异常: {str(e)}"
logging.error(error_msg)
return None
self.has_real_camera = False
return []
def open_device(self, device_index):
"""打开相机设备参考BasicDemo.py的open_device实现
"""打开相机设备
Args:
device_index: 设备索引
@ -198,27 +253,32 @@ class CameraManager:
Returns:
bool: 是否成功打开设备
"""
# 检查是否已经打开
if self.isOpen:
logging.warning("相机已经打开!")
# 如果当前处于本地图像模式,且设备索引是虚拟设备
if self.local_mode and device_index == 0:
# 模拟打开设备成功
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
# 确保有效的设备索引
if device_index < 0 or self.deviceList is None or device_index >= self.deviceList.nDeviceNum:
error_msg = f"无效的设备索引: {device_index}, 设备列表: {self.deviceList is not None}"
if self.deviceList:
error_msg += f", 设备数量: {self.deviceList.nDeviceNum}"
logging.error(error_msg)
return False
# 如果之前已经打开设备,先关闭
if self.isOpen:
self.close_device()
try:
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()
if ret != 0:
error_msg = f"打开相机失败! 错误码: 0x{ret:x}"
@ -260,6 +320,16 @@ class CameraManager:
Returns:
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:
return True
@ -287,22 +357,28 @@ class CameraManager:
return False
def start_grabbing(self, window_id):
"""开始
"""开始像采集
Args:
window_id: 显示窗口句柄
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:
logging.error("相机未打开,无法开始取图")
return False
if self.isGrabbing:
logging.warning("相机已经在取图")
return True
try:
ret = self.obj_cam_operation.Start_grabbing(window_id)
if ret != 0:
@ -326,6 +402,12 @@ class CameraManager:
Returns:
bool: 是否成功停止取图
"""
# 如果处于本地图像模式,模拟停止采集
if self.local_mode:
self.isGrabbing = False
logging.info("停止本地图像模式采集")
return True
if not self.isOpen:
return True
@ -412,6 +494,64 @@ class CameraManager:
logging.error(error_msg)
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):
"""保存相机参数到配置文件

View File

@ -8,11 +8,11 @@ from camera.CameraParams_const import *
sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), "camera"))
try:
from PySide6.QtWidgets import QMessageBox
from PySide6.QtWidgets import QMessageBox, QFileDialog
from PySide6.QtCore import Qt, Signal
USE_PYSIDE6 = True
except ImportError:
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtWidgets import QMessageBox, QFileDialog
from PyQt5.QtCore import Qt
from PyQt5.QtCore import pyqtSignal as Signal
USE_PYSIDE6 = False
@ -27,8 +27,13 @@ from ui.settings_ui import SettingsUI
# 导入相机管理器
from widgets.camera_manager import CameraManager
# 导入本地图像播放器
from utils.local_image_player import LocalImagePlayer
# 导入配置加载器
from utils.config_loader import ConfigLoader
from PySide6.QtCore import QObject
import json
class CameraSettingsWidget(QObject):
"""相机设置控制器,管理相机设置并提供与主窗口相机显示部分的通信
@ -39,6 +44,7 @@ class CameraSettingsWidget(QObject):
signal_camera_params_changed = Signal(float, float, float) # 相机参数变化信号 (曝光, 增益, 帧率)
signal_camera_error = Signal(str) # 相机错误信号
settings_changed = Signal() # 设置变更信号,与 SettingsWindow 兼容
signal_local_mode_changed = Signal(bool) # 本地图像模式变更信号 (是否启用)
def __init__(self, parent=None):
"""初始化相机设置控制器
@ -55,6 +61,24 @@ class CameraSettingsWidget(QObject):
# 获取相机管理器实例
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初始化开始")
@ -78,6 +102,17 @@ class CameraSettingsWidget(QObject):
self.set_params_button = getattr(parent, 'set_params_button', None)
self.save_camera_button = getattr(parent, 'save_camera_button', 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控件
if self.camera_combo is None:
@ -109,6 +144,18 @@ class CameraSettingsWidget(QObject):
self.set_params_button = None
self.save_camera_button = 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()
@ -122,9 +169,24 @@ class CameraSettingsWidget(QObject):
self.gain_min = 0.0
self.gain_max = 15.0
# 从配置加载本地模式设置
self.load_local_mode_settings()
# 更新本地模式UI
self.update_local_mode_ui()
# 枚举设备
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):
"""连接信号和槽"""
# 设备选择和刷新
@ -146,6 +208,25 @@ class CameraSettingsWidget(QObject):
self.set_params_button.clicked.connect(self.set_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):
"""刷新设备列表"""
logging.info("【设备刷新】开始...")
@ -195,69 +276,24 @@ class CameraSettingsWidget(QObject):
devList = []
if 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}")
else:
devList.append("未发现相机设备")
logging.info(f"【设备刷新】将显示默认值 '未发现相机设备'")
devList = ["未发现相机设备"]
# 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()}")
# 3. 清空并更新下拉列表
self.camera_combo.clear()
for dev in devList:
self.camera_combo.addItem(dev)
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. 如果下拉列表仍然为空,尝试最后一次强制添加
try:
if hasattr(self, 'camera_combo') and self.camera_combo is not None:
if self.camera_combo.count() == 0:
logging.warning("【设备刷新】下拉列表仍然为空,尝试强制添加项目")
self.camera_combo.addItem("未发现相机设备(强制添加)")
self.camera_combo.setCurrentIndex(0)
self.camera_combo.update()
self.camera_combo.repaint()
except Exception as e:
logging.error(f"【设备刷新】强制添加项目时发生错误: {e}")
# 4. 更新UI状态
self.update_controls()
def get_selected_device_index(self):
"""获取当前选中的设备索引参考BasicDemo.py的TxtWrapBy实现"""
@ -351,7 +387,6 @@ class CameraSettingsWidget(QObject):
self.signal_camera_connection.emit(True, "")
# 更新配置
from utils.config_loader import ConfigLoader
config_loader = ConfigLoader.get_instance()
config_loader.set_value('camera.enabled', True)
config_loader.save_config()
@ -386,7 +421,6 @@ class CameraSettingsWidget(QObject):
self.signal_camera_connection.emit(False, "")
# 更新配置
from utils.config_loader import ConfigLoader
config_loader = ConfigLoader.get_instance()
config_loader.set_value('camera.enabled', False)
config_loader.save_config()
@ -538,3 +572,495 @@ class CameraSettingsWidget(QObject):
if self.camera_manager.isOpen:
self.camera_manager.close_device()
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("播放完成")

View File

@ -75,6 +75,10 @@ class MainWindow(MainWindowUI):
self._weight_processed = False # 新增:标记当前重量是否已处理,避免重复处理
self._last_processed_weight = 0.0 # 新增:记录上次处理的重量
# 线径数据处理相关属性
self._last_diameter_value = 0 # 最后一次有效的线径值
self._diameter_stable = False # 保留此属性以避免引用错误
# 初始化数据加载状态标志
self._loading_data_in_progress = False # 数据加载状态标志,防止循环调用
self._current_order_code = None # 存储当前订单号
@ -394,6 +398,17 @@ class MainWindow(MainWindowUI):
# 连接设置改变信号
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()
logging.info("显示设置窗口")
@ -418,7 +433,7 @@ class MainWindow(MainWindowUI):
# 重新加载托盘号
self.load_pallet_codes()
logging.info("设置已更新,重新加载配置并重新打开串口,已重新注册扫码器回调")
logging.info("设置已更新,重新加载配置并重新打开串口,已重新注册米电阻、线径和扫码器回调")
def handle_input(self):
"""处理上料按钮点击事件"""
@ -770,7 +785,9 @@ class MainWindow(MainWindowUI):
logging.info(f"输入的工程号: {gc_note}")
#判断是否是接口,如果不是接口直接添加如果是则走接口
# 如果开启接口模式,则需要调用接口同步到业务库
self.add_new_inspection_row(gc_note, self._current_order_code)
else:
logging.warning("工程号为空")
QMessageBox.warning(self, "输入提示", "请输入有效的工程号")
@ -2721,46 +2738,59 @@ class MainWindow(MainWindowUI):
if "线径数据:" in data_str:
value_str = data_str.split("线径数据:")[1].strip()
try:
# 转换为浮点数
xj_value = float(value_str)
# 转换为浮点数除以10000并保留三位小数
xj_value = round(float(value_str)/10000, 3)
# 查找线径对应的检验项配置
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
# 更新UI显示实时回显最新测量值
self.statusBar().showMessage(f"线径数据: {xj_value:.3f}", 2000)
if xj_config:
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
bccd, tccd = inspection_dao.get_xj_range(self._current_order_code)
# 如果当前值不为0记录为最后一次有效值
if xj_value > 0:
self._last_diameter_value = xj_value
logging.info(f"更新最后一次有效线径值: {xj_value:.3f}")
if bccd is not None and tccd is not None:
if bccd <= xj_value <= tccd:
self.set_inspection_value('xj', xj_config, xj_value)
else:
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)
# 如果当前值为0并且之前有非零值说明产品已拿开保存最后一次有效值
elif xj_value == 0 and hasattr(self, '_last_diameter_value') and self._last_diameter_value > 0:
final_value = self._last_diameter_value
logging.info(f"检测到线径值变为0使用最后一次有效值 {final_value:.3f} 作为最终结果")
# 查找线径对应的检验项配置
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.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("未找到线径对应的检验项配置")
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:
logging.warning(f"线径数据格式错误: {value_str}")
else:
@ -2850,7 +2880,7 @@ class MainWindow(MainWindowUI):
data_row = None
for row in range(2, self.process_table.rowCount()):
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
break
@ -2882,8 +2912,11 @@ class MainWindow(MainWindowUI):
# 格式化值并设置单元格
formatted_value = str(value)
if config.get('data_type') == 'number':
# 格式化数字保留2位小数
formatted_value = f"{value:.2f}"
# 格式化数字线径保留3位小数其他保留2位小数
if data_type == 'xj':
formatted_value = f"{value:.3f}"
else:
formatted_value = f"{value:.2f}"
# 设置单元格值
item = QTableWidgetItem(formatted_value)
@ -3017,7 +3050,7 @@ class MainWindow(MainWindowUI):
"""初始化相机并显示画面"""
try:
if not self.camera_enabled:
return
self.material_placeholder.setText("相机功能已禁用")
logging.info("开始初始化相机...")
@ -3083,7 +3116,16 @@ class MainWindow(MainWindowUI):
is_camera_ready: 相机是否准备就绪
"""
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:
self.camera_display.setVisible(True)
@ -3092,7 +3134,7 @@ class MainWindow(MainWindowUI):
self.material_placeholder.setVisible(False)
logging.info("相机UI已更新显示相机画面")
else:
# 隐藏相机画面,显示占位符
# 显示占位符,隐藏相机画面
if self.camera_display:
self.camera_display.setVisible(False)
if self.material_placeholder:
@ -3104,7 +3146,7 @@ class MainWindow(MainWindowUI):
self.material_placeholder.setText("相机未就绪")
logging.info(f"相机UI已更新显示占位符 (相机启用={self.camera_enabled}, 相机就绪={is_camera_ready})")
except Exception as e:
logging.error(f"更新相机UI失败: {str(e)}")
logging.error(f"更新相机UI时出错: {e}")
def handle_camera_status(self, is_connected, message):
"""处理相机状态变化"""
@ -3332,3 +3374,102 @@ class MainWindow(MainWindowUI):
except Exception as e:
logging.error(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}")

View File

@ -194,6 +194,18 @@ class SerialSettingsWidget(SerialSettingsUI):
if index >= 0:
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')
if scanner_config:
@ -272,12 +284,18 @@ class SerialSettingsWidget(SerialSettingsUI):
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_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 = {
'port': xj_baud,
'data_bits': xj_data_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:
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)