Compare commits

...

23 Commits

Author SHA1 Message Date
zhu-mengmeng
ef1a624099 feat: 更新数据库文件 2025-07-01 18:39:22 +08:00
zhu-mengmeng
f53c356c19 feat:修改托盘类型 2025-07-01 16:16:35 +08:00
zhu-mengmeng
5b7a0bb439 feat:新增上料下料按钮触发 2025-07-01 16:04:18 +08:00
zhu-mengmeng
343178c1a3 feat: 解决 sql 游标循环调用问题 2025-07-01 15:32:40 +08:00
zhu-mengmeng
3e8ee647eb feat: 修复无法正常存储贴标信息问题 2025-07-01 10:56:16 +08:00
zhu-mengmeng
7457cd8b4c feat: 修复急停信号异常 2025-07-01 10:15:57 +08:00
zhu-mengmeng
ce8483d298 feat: 完成扫码操作 2025-07-01 09:36:16 +08:00
zhu-mengmeng
ead4cfcc95 feat: 正常打开相机 2025-06-30 20:46:56 +08:00
zhu-mengmeng
7023d7561a feat: 完善相机 2025-06-30 19:40:02 +08:00
zhu-mengmeng
336ba26bc8 feat: 优化相机显示区域 2025-06-30 18:40:50 +08:00
zhu-mengmeng
7da216ec58 feat:解决相机无法回显的问题 2025-06-30 18:24:24 +08:00
zhu-mengmeng
486cdb0cad feat: 修复扫码器 BUG 2025-06-30 17:35:19 +08:00
zhu-mengmeng
7a2351cbe6 feat: 更新电力监控配置,禁用自动启动功能;优化主窗口和串口设置界面,增强用户交互体验;修复部分代码格式和逻辑问题。 2025-06-30 15:52:10 +08:00
zhu-mengmeng
145a350fb8 add: 拆码垛急停,复位层数 2025-06-30 11:53:01 +08:00
zhu-mengmeng
92905294aa feat: 新增急停信号 2025-06-30 11:19:07 +08:00
zhu-mengmeng
6d60e77743 feat: 修复一些已知问题 2025-06-30 09:58:16 +08:00
zhu-mengmeng
11c6b15d3e feat: 新增依赖 2025-06-28 15:04:53 +08:00
zhu-mengmeng
5c5a8f44e8 新增 readme 2025-06-28 15:01:36 +08:00
zhu-mengmeng
968b4108b9 feat: 更新相机控制库加载逻辑 2025-06-28 13:02:34 +08:00
zhu-mengmeng
074b656bbc feat:完成相机配置 2025-06-27 15:14:30 +08:00
zhu-mengmeng
320d715f83 feat:修正一些错误逻辑 2025-06-26 18:26:22 +08:00
zhu-mengmeng
77c6f9f480 feat: 更新SDK初始化逻辑,添加异常处理以增强稳定性;在设置界面中新增Modbus配置功能,包含主机地址和端口输入框及连接测试按钮;优化Modbus工具类以支持动态主机和端口配置;增强设置窗口信号连接功能以处理设置变更。 2025-06-26 10:44:15 +08:00
zhu-mengmeng
99bca76e08 feat: 更新托盘类型管理功能,添加托盘类型的增删改查接口,优化UI交互,简化托盘类型配置页面 2025-06-26 10:14:30 +08:00
39 changed files with 7652 additions and 1652 deletions

112
README.md Normal file
View File

@ -0,0 +1,112 @@
# 腾智微丝产线包装系统
## 项目架构分析
这是一个基于PySide6Qt for Python开发的腾智微丝产线包装系统采用了典型的MVC模型-视图-控制器)架构:
1. **模型层Model**
- 使用DAO数据访问对象模式访问数据库
- 支持多种数据库SQLite、PostgreSQL、MySQL
- 主要数据表包括用户表、检验配置表、检验数据表、电力消耗表等
2. **视图层View**
- 使用PySide6构建UI界面
- 主要界面包括登录界面、主窗口、设置界面等
- 采用分离的UI类设计如LoginUI、MainWindowUI等
3. **控制器层Controller**
- 主要控制逻辑在widgets目录下的类中实现
- 使用信号槽机制进行组件间通信
- 采用单例模式管理全局资源
4. **工具层Utils**
- 提供各种工具类如配置加载器、Modbus通信、串口管理等
- 采用单例模式确保资源共享
## 技术栈
1. **前端技术**
- PySide6Qt for Python用于GUI开发
- 使用Qt的信号槽机制实现组件间通信
2. **后端技术**
- Python作为主要开发语言
- SQLite作为默认数据库支持PostgreSQL和MySQL
- Modbus协议用于与设备通信
- 多线程处理耗时操作
3. **通信技术**
- Modbus TCP用于与PLC设备通信
- 串口通信用于与称重设备、条码扫描器等外设通信
4. **设计模式**
- 单例模式(配置加载器、监控器等)
- DAO模式数据访问
- 观察者模式(信号槽)
- 工厂模式(数据库连接)
## 代码结构
1. **主要目录**
- `widgets/`:包含所有窗口控制器类
- `ui/`包含所有UI定义类
- `utils/`:包含工具类
- `dao/`:包含数据访问对象
- `db/`:包含数据库文件
- `config/`:包含配置文件
- `logs/`:包含日志文件
2. **核心文件**
- `main.py`:程序入口点
- `widgets/login_widget.py`:登录窗口控制器
- `widgets/main_window.py`:主窗口控制器
- `utils/config_loader.py`:配置加载器
- `utils/modbus_utils.py`Modbus通信工具
- `utils/sql_utils.py`:数据库工具
3. **应用流程**
- 程序启动后初始化日志系统
- 加载配置文件
- 显示登录窗口
- 验证登录后显示主窗口
- 主窗口中进行产线包装系统的操作
## 功能特点
1. **用户认证**:支持用户登录和权限控制
2. **产线监控**:实时监控产线状态、电力消耗等
3. **数据采集**:采集称重数据、检验数据等
4. **报表生成**:生成各类统计报表
5. **设备通信**与PLC、称重设备等通信
6. **多模式支持**:支持单机模式和接口模式
## 运行环境
- Python 3.7+
- PySide6
- 支持的数据库SQLite、PostgreSQL、MySQL
- 操作系统Windows、macOS、Linux
## 安装与配置
1. 安装依赖:
```
pip install -r requirements.txt
```
2. 配置数据库:
- 默认使用SQLite数据库位于`db/jtDB.db`
- 可在`config/app_config.json`中配置其他数据库
3. 运行程序:
```
python main.py
```
## 开发与扩展
系统采用模块化设计,可以方便地进行功能扩展:
1. 添加新的数据源:扩展`utils/sql_utils.py`
2. 添加新的设备通信协议:参考`utils/modbus_utils.py`
3. 添加新的UI界面在`ui/`目录下创建新的UI类在`widgets/`目录下创建对应的控制器类

View File

@ -107,8 +107,10 @@ class CameraOperation:
return MV_E_CALLORDER
# ch:选择设备并创建句柄 | en:Select device and create handle
# 确保索引是整数类型
nConnectionNum = int(self.n_connect_num)
stDeviceList = cast(self.st_device_list.pDeviceInfo[int(nConnectionNum)],
print(f"打开设备,使用设备索引: {nConnectionNum}, 类型: {type(nConnectionNum)}")
stDeviceList = cast(self.st_device_list.pDeviceInfo[nConnectionNum],
POINTER(MV_CC_DEVICE_INFO)).contents
self.obj_cam = MvCamera()
ret = self.obj_cam.MV_CC_CreateHandle(stDeviceList)

View File

@ -3,6 +3,9 @@
import sys
import copy
import ctypes
import platform
import os
import logging
from ctypes import *
@ -11,15 +14,50 @@ from CameraParams_const import *
from CameraParams_header import *
from MvErrorDefine_const import *
# Python3.8版本修改Dll加载策略, 默认不再搜索Path环境变量, 同时增加winmode参数以兼容旧版本
dllname = "MvCameraControl.dll"
# 初始化全局变量
MvCamCtrldll = None
# 根据操作系统加载不同的库文件
system_name = platform.system()
try:
if "winmode" in ctypes.WinDLL.__init__.__code__.co_varnames:
MvCamCtrldll = WinDLL(dllname, winmode=0)
if system_name == "Windows":
dllname = "MvCameraControl.dll"
# Python3.8版本修改Dll加载策略, 默认不再搜索Path环境变量, 同时增加winmode参数以兼容旧版本
if "winmode" in ctypes.WinDLL.__init__.__code__.co_varnames:
MvCamCtrldll = WinDLL(dllname, winmode=0)
else:
MvCamCtrldll = WinDLL(dllname)
logging.info(f"Windows系统: 成功加载 {dllname}")
elif system_name == "Darwin": # macOS
# macOS通常使用.dylib或.so文件尝试多种可能的库名
possible_libs = [
"libMvCameraControl.dylib",
"/Library/Frameworks/MvCameraControl.framework/MvCameraControl",
"libMvCameraControl.so",
"MvCameraControl.so"
]
lib_loaded = False
for lib in possible_libs:
try:
MvCamCtrldll = CDLL(lib)
logging.info(f"macOS系统: 成功加载 {lib}")
lib_loaded = True
break
except OSError:
continue
if not lib_loaded:
logging.error("macOS系统: 无法加载相机控制库请确保已安装Hikvision SDK并设置正确的库路径")
elif system_name == "Linux":
dllname = "libMvCameraControl.so"
MvCamCtrldll = CDLL(dllname)
logging.info(f"Linux系统: 成功加载 {dllname}")
else:
MvCamCtrldll = WinDLL(dllname)
logging.error(f"不支持的操作系统: {system_name}")
except Exception as e:
print(f"加载MvCameraControl.dll失败: {e}")
logging.error(f"加载相机控制库失败: {str(e)}")
MvCamCtrldll = None
@ -47,9 +85,13 @@ class MvCamera():
# ch:初始化SDK | en: Initialize SDK
@staticmethod
def MV_CC_Initialize():
MvCamCtrldll.MV_CC_Initialize.restype = c_int
# C原型int __stdcall MV_CC_Initialize();
return MvCamCtrldll.MV_CC_Initialize()
try:
MvCamCtrldll.MV_CC_Initialize.restype = c_int
# C原型int __stdcall MV_CC_Initialize();
return MvCamCtrldll.MV_CC_Initialize()
except Exception as e:
print(f"初始化SDK失败: {e}")
return -1
# ch:反初始化SDK | en: Finalize SDK
@staticmethod

View File

@ -3,19 +3,19 @@
"name": "腾智微丝产线包装系统",
"version": "1.0.0",
"features": {
"enable_serial_ports": false,
"enable_serial_ports": true,
"enable_keyboard_listener": false,
"enable_camera": false
},
"base_url": "http://localhost:8084",
"mode": "api"
"mode": "standalone"
},
"apis": {
"get_tray_info": "/apjt/xcsc/tpda/getByTp_note/",
"get_gc_info": "/jsjt/xcsc/tprk/getBZGCInfoToWsbz.do",
"get_order_info": "/jsjt/xcsc/tprk/getXsddBzrkGridListToWsbz.do",
"add_order_info": "/jsjt/xcsc/tprk/bzrkAdd01.do",
"get_xpack":"/jsjt/xcsc/tprk/getXpackToWsbz.do"
"get_xpack": "/jsjt/xcsc/tprk/getXpackToWsbz.do"
},
"database": {
"default": "sqlite",
@ -78,6 +78,18 @@
"stable_threshold": 10,
"stop_bits": 1,
"timeout": 1
},
"scanner": {
"code": "scanner",
"data_bits": 8,
"parity": "N",
"port": "9600",
"ser": "COM3",
"stop_bits": 1,
"timeout": 1
}
},
"electricity": {
"auto_start": true
}
}

124
dao/electricity_dao.py Normal file
View File

@ -0,0 +1,124 @@
import logging
from datetime import datetime
from utils.sql_utils import SQLUtils
class ElectricityDAO:
"""电力消耗数据访问对象"""
def __init__(self):
"""初始化数据访问对象"""
self.db = SQLUtils('sqlite', database='db/jtDB.db')
def __del__(self):
"""析构函数,确保数据库连接关闭"""
if hasattr(self, 'db'):
self.db.close()
def create_table_if_not_exists(self):
"""创建电力消耗表(如果不存在)"""
try:
sql = """
CREATE TABLE IF NOT EXISTS wsbz_electricity_consumption (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sync_time TIMESTAMP,
electricity_number REAL
)
"""
self.db.execute_query(sql)
logging.info("电力消耗表检查/创建成功")
return True
except Exception as e:
logging.error(f"创建电力消耗表失败: {str(e)}")
return False
def save_electricity_data(self, electricity_number):
"""保存电力消耗数据
Args:
electricity_number: 电力消耗数值
Returns:
bool: 保存是否成功
"""
try:
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
sql = """
INSERT INTO wsbz_electricity_consumption (
sync_time, electricity_number
) VALUES (?, ?)
"""
params = (current_time, electricity_number)
self.db.execute_update(sql, params)
logging.info(f"保存电力消耗数据成功: {electricity_number}")
return True
except Exception as e:
logging.error(f"保存电力消耗数据失败: {str(e)}")
return False
def get_latest_electricity_data(self):
"""获取最新的电力消耗数据
Returns:
dict: 最新的电力消耗数据未找到则返回None
"""
try:
sql = """
SELECT id, sync_time, electricity_number
FROM wsbz_electricity_consumption
ORDER BY id DESC
LIMIT 1
"""
self.db.cursor.execute(sql)
row = self.db.cursor.fetchone()
if row:
data = {
'id': row[0],
'sync_time': row[1],
'electricity_number': row[2]
}
return data
else:
return None
except Exception as e:
logging.error(f"获取最新电力消耗数据失败: {str(e)}")
return None
def get_electricity_data_by_date_range(self, start_date, end_date):
"""根据日期范围获取电力消耗数据
Args:
start_date: 开始日期YYYY-MM-DD格式
end_date: 结束日期YYYY-MM-DD格式
Returns:
list: 电力消耗数据列表
"""
try:
sql = """
SELECT id, sync_time, electricity_number
FROM wsbz_electricity_consumption
WHERE sync_time BETWEEN ? AND ?
ORDER BY sync_time
"""
params = (f"{start_date} 00:00:00", f"{end_date} 23:59:59")
self.db.cursor.execute(sql, params)
results = self.db.cursor.fetchall()
data_list = []
for row in results:
data = {
'id': row[0],
'sync_time': row[1],
'electricity_number': row[2]
}
data_list.append(data)
return data_list
except Exception as e:
logging.error(f"获取电力消耗数据失败: {str(e)}")
return []

View File

@ -8,12 +8,13 @@ class InspectionDAO:
def __init__(self):
"""初始化数据访问对象"""
self.db = SQLUtils('sqlite', database='db/jtDB.db')
# 不再在初始化时创建数据库连接,而是在需要时创建
pass
def __del__(self):
"""析构函数,确保数据库连接关闭"""
if hasattr(self, 'db'):
self.db.close()
# 不再需要在这里关闭连接,由上下文管理器处理
pass
def get_all_inspection_configs(self, include_disabled=False):
"""获取所有检验项目配置
@ -44,8 +45,9 @@ class InspectionDAO:
"""
params = ()
self.db.cursor.execute(sql, params)
results = self.db.cursor.fetchall()
with SQLUtils('sqlite', database='db/jtDB.db') as db:
db.cursor.execute(sql, params)
results = db.cursor.fetchall()
configs = []
for row in results:
@ -96,8 +98,9 @@ class InspectionDAO:
"""
params = (position,)
self.db.cursor.execute(sql, params)
row = self.db.cursor.fetchone()
with SQLUtils('sqlite', database='db/jtDB.db') as db:
db.cursor.execute(sql, params)
row = db.cursor.fetchone()
if row:
config = {
@ -182,7 +185,8 @@ class InspectionDAO:
WHERE id = ?
"""
self.db.execute_update(sql, params)
with SQLUtils('sqlite', database='db/jtDB.db') as db:
db.execute_update(sql, params)
return True
except Exception as e:
logging.error(f"更新检验项目配置失败: {str(e)}")
@ -209,7 +213,8 @@ class InspectionDAO:
"""
params = (enabled, current_time, username, position)
self.db.execute_update(sql, params)
with SQLUtils('sqlite', database='db/jtDB.db') as db:
db.execute_update(sql, params)
return True
except Exception as e:
logging.error(f"更新检验项目启用状态失败: {str(e)}")
@ -227,122 +232,130 @@ class InspectionDAO:
if not data:
return False
# 先检查是否存在记录
check_sql = "SELECT ddmo FROM wsbz_order_info WHERE ddmo = ?"
self.db.cursor.execute(check_sql, (data.get("mo", ""),))
existing_record = self.db.cursor.fetchone()
# 使用单一连接实例处理整个操作
with SQLUtils('sqlite', database='db/jtDB.db') as db:
db.begin_transaction() # 开始事务
# 先检查是否存在记录
check_cursor = db.get_new_cursor()
check_sql = "SELECT ddmo FROM wsbz_order_info WHERE ddmo = ?"
check_cursor.execute(check_sql, (data.get("mo", ""),))
existing_record = check_cursor.fetchone()
check_cursor.close() # 使用后关闭新游标
if existing_record:
# 如果记录存在,执行更新
sql = """
UPDATE wsbz_order_info SET
data_corp = ?, user_id = ?, user_name = ?, gzl_zl = ?,
xpack = ?, qd = ?, spack_type = ?, mxzs = ?, jt = ?,
ddnote = ?, code = ?, type = ?, lable = ?, lib = ?,
gzl = ?, maxsl = ?, cz = ?, size = ?, cd = ?, luno = ?,
qfqd = ?, pono = ?, xj = ?, ysl = ?, dycz = ?,
zx_code = ?, edit_id = ?, remarks = ?, zx_name = ?
bccd = ? ,tccd = ?
WHERE ddmo = ?
"""
params = (
data.get("data_corp", "JT"),
data.get("user_id", ""),
data.get("user_name", ""),
data.get("zx_zl", ""),
data.get("xpack", ""),
data.get("qd", ""),
data.get("spack_type", ""),
data.get("mxzs", ""),
data.get("jt", ""),
data.get("note", ""),
data.get("code", ""),
data.get("type", ""),
data.get("template_name", ""),
data.get("lib", ""),
data.get("zx_code", ""),
data.get("maxsl", ""),
data.get("cz", ""),
data.get("size", ""),
data.get("cd", ""),
data.get("luno", ""),
data.get("qfqd", ""),
data.get("khno", ""),
data.get("size", ""),
data.get("ysl", ""),
data.get("dycz", ""),
data.get("zx_code", ""),
data.get("edit_id", ""),
data.get("remarks", ""),
data.get("zx_name", ""),
data.get("bccd", ""),
data.get("tccd", ""),
data.get("mo", "") # WHERE 条件参数
)
logging.info(f"更新订单信息: ddmo={data.get('mo', '')}")
else:
# 如果记录不存在,执行插入
sql = """
INSERT INTO wsbz_order_info (
data_corp, user_id, user_name, gzl_zl, ddmo, xpack,
qd, spack_type, mxzs, jt, ddnote, code, type,
lable, lib, gzl, maxsl, cz, size, cd, luno, qfqd,
pono, xj, ysl, dycz, zx_code, edit_id, remarks,zx_name,bccd,tccd
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
if existing_record:
# 如果记录存在,执行更新
sql = """
UPDATE wsbz_order_info SET
data_corp = ?, user_id = ?, user_name = ?, gzl_zl = ?,
xpack = ?, qd = ?, spack_type = ?, mxzs = ?, jt = ?,
ddnote = ?, code = ?, type = ?, lable = ?, lib = ?,
gzl = ?, maxsl = ?, cz = ?, size = ?, cd = ?, luno = ?,
qfqd = ?, pono = ?, xj = ?, ysl = ?, dycz = ?,
zx_code = ?, edit_id = ?, remarks = ?, zx_name = ?
bccd = ? ,tccd = ?
WHERE ddmo = ?
"""
params = (
data.get("data_corp", "JT"),
data.get("user_id", ""),
data.get("user_name", ""),
data.get("zx_zl", ""),
data.get("xpack", ""),
data.get("qd", ""),
data.get("spack_type", ""),
data.get("mxzs", ""),
data.get("jt", ""),
data.get("note", ""),
data.get("code", ""),
data.get("type", ""),
data.get("template_name", ""),
data.get("lib", ""),
data.get("zx_code", ""),
data.get("maxsl", ""),
data.get("cz", ""),
data.get("size", ""),
data.get("cd", ""),
data.get("luno", ""),
data.get("qfqd", ""),
data.get("khno", ""),
data.get("size", ""),
data.get("ysl", ""),
data.get("dycz", ""),
data.get("zx_code", ""),
data.get("edit_id", ""),
data.get("remarks", ""),
data.get("zx_name", ""),
data.get("bccd", ""),
data.get("tccd", ""),
data.get("mo", "") # WHERE 条件参数
)
"""
params = (
data.get("data_corp", "JT"),
data.get("user_id", ""),
data.get("user_name", ""),
data.get("zx_zl", ""),
data.get("mo", ""),
data.get("xpack", ""),
data.get("qd", ""),
data.get("spack_type", ""),
data.get("mxzs", ""),
data.get("jt", ""),
data.get("note", ""),
data.get("code", ""),
data.get("type", ""),
data.get("template_name", ""),
data.get("lib", ""),
data.get("zx_code", ""),
data.get("maxsl", ""),
data.get("cz", ""),
data.get("size", ""),
data.get("cd", ""),
data.get("luno", ""),
data.get("qfqd", ""),
data.get("khno", ""),
data.get("size", ""),
data.get("ysl", ""),
data.get("dycz", ""),
data.get("zx_code", ""),
data.get("edit_id", ""),
data.get("remarks", ""),
data.get("zx_name", ""),
data.get("bccd", ""),
data.get("tccd", "")
)
logging.info(f"插入新订单信息: ddmo={data.get('mo', '')}")
logging.info(f"更新订单信息: ddmo={data.get('mo', '')}")
else:
# 如果记录不存在,执行插入
sql = """
INSERT INTO wsbz_order_info (
data_corp, user_id, user_name, gzl_zl, ddmo, xpack,
qd, spack_type, mxzs, jt, ddnote, code, type,
lable, lib, gzl, maxsl, cz, size, cd, luno, qfqd,
pono, xj, ysl, dycz, zx_code, edit_id, remarks,zx_name,bccd,tccd
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
"""
params = (
data.get("data_corp", "JT"),
data.get("user_id", ""),
data.get("user_name", ""),
data.get("zx_zl", ""),
data.get("mo", ""),
data.get("xpack", ""),
data.get("qd", ""),
data.get("spack_type", ""),
data.get("mxzs", ""),
data.get("jt", ""),
data.get("note", ""),
data.get("code", ""),
data.get("type", ""),
data.get("template_name", ""),
data.get("lib", ""),
data.get("zx_code", ""),
data.get("maxsl", ""),
data.get("cz", ""),
data.get("size", ""),
data.get("cd", ""),
data.get("luno", ""),
data.get("qfqd", ""),
data.get("khno", ""),
data.get("size", ""),
data.get("ysl", ""),
data.get("dycz", ""),
data.get("zx_code", ""),
data.get("edit_id", ""),
data.get("remarks", ""),
data.get("zx_name", ""),
data.get("bccd", ""),
data.get("tccd", "")
)
logging.info(f"插入新订单信息: ddmo={data.get('mo', '')}")
self.db.cursor.execute(sql, params)
self.db.conn.commit()
# 执行SQL
db.execute_update(sql, params)
db.commit_transaction()
return True
except Exception as e:
logging.error(f"保存订单信息失败: {str(e)}")
self.db.conn.rollback()
return False
def save_inspection_data(self, order_id,gc_note, data, username='system'):
def save_inspection_data(self, order_id, gc_note, data, username='system'):
"""保存检验数据
Args:
order_id: 工程号
data: 检验数据列表格式: [{'position': 1, 'config_id': 1, 'value': '合格'}, ...]
order_id: 订单号
gc_note: 工程号
data: 检验数据列表每项包含position, config_id, value, status, remark
username: 操作用户
Returns:
@ -351,56 +364,62 @@ class InspectionDAO:
try:
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.db.begin_transaction()
for item in data:
position = item.get('position')
config_id = item.get('config_id')
value = item.get('value')
status = item.get('status', 'pass')
remark = item.get('remark', '')
tray_id = item.get('tray_id', '')
# 检查是否已存在该工程号和位置的记录
check_sql = """
SELECT id FROM wsbz_inspection_data
WHERE order_id = ? and gc_note = ? AND position = ? AND is_deleted = FALSE
"""
check_params = (order_id,gc_note, position)
# 使用上下文管理器自动处理连接和游标
with SQLUtils('sqlite', database='db/jtDB.db') as db:
db.begin_transaction() # 开始事务
self.db.cursor.execute(check_sql, check_params)
existing = self.db.cursor.fetchone()
for item in data:
position = item.get('position')
config_id = item.get('config_id')
value = item.get('value', '')
status = item.get('status', '')
remark = item.get('remark', '')
tray_id = item.get('tray_id', '')
# 获取新游标执行查询,避免递归使用
check_cursor = db.get_new_cursor()
check_sql = """
SELECT id FROM wsbz_inspection_data
WHERE order_id = ? AND gc_note = ? AND position = ? AND tray_id = ?
"""
check_params = (order_id, gc_note, position, tray_id)
check_cursor.execute(check_sql, check_params)
existing_record = check_cursor.fetchone()
check_cursor.close() # 使用后关闭新游标
if existing_record:
# 更新现有记录
update_sql = """
UPDATE wsbz_inspection_data
SET config_id = ?, value = ?, status = ?, remark = ?,
update_time = ?, update_by = ?
WHERE order_id = ? AND gc_note = ? AND position = ? AND tray_id = ?
"""
update_params = (
config_id, value, status, remark,
current_time, username,
order_id, gc_note, position, tray_id
)
db.execute_update(update_sql, update_params)
else:
# 插入新记录
insert_sql = """
INSERT INTO wsbz_inspection_data (
order_id, gc_note, position, config_id, value, status, remark,
create_time, create_by, update_time, update_by, tray_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
insert_params = (
order_id, gc_note, position, config_id, value, status, remark,
current_time, username, current_time, username, tray_id
)
db.execute_update(insert_sql, insert_params)
if existing:
# 更新已有记录
update_sql = """
UPDATE wsbz_inspection_data
SET config_id = ?, value = ?, status = ?, remark = ?,
update_time = ?, update_by = ?, tray_id = ?
WHERE id = ?
"""
update_params = (
config_id, value, status, remark,
current_time, username, tray_id, existing[0]
)
self.db.cursor.execute(update_sql, update_params)
else:
# 插入新记录
insert_sql = """
INSERT INTO wsbz_inspection_data (
order_id, position, config_id, value, status, remark,
create_time, create_by, is_deleted, tray_id,gc_note
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, FALSE, ?, ?)
"""
insert_params = (
order_id, position, config_id, value, status, remark,
current_time, username, tray_id,gc_note
)
self.db.cursor.execute(insert_sql, insert_params)
db.commit_transaction() # 提交事务
self.db.commit_transaction()
return True
except Exception as e:
self.db.rollback_transaction()
logging.error(f"保存检验数据失败: {str(e)}")
return False
def get_inspection_data_unfinished(self, tray_id):
@ -418,8 +437,9 @@ class InspectionDAO:
AND d.position = 11 AND COALESCE(d.value,'') = ''
"""
params = (tray_id,)
self.db.cursor.execute(sql_orders, params)
gc_notes = self.db.cursor.fetchall()
with SQLUtils('sqlite', database='db/jtDB.db') as db:
db.cursor.execute(sql_orders, params)
gc_notes = db.cursor.fetchall()
if not gc_notes:
return []
@ -440,8 +460,9 @@ class InspectionDAO:
"""
params = [tray_id] + gc_notes
self.db.cursor.execute(sql, params)
results = self.db.cursor.fetchall()
with SQLUtils('sqlite', database='db/jtDB.db') as db:
db.cursor.execute(sql, params)
results = db.cursor.fetchall()
data_list = []
for row in results:
@ -485,8 +506,9 @@ class InspectionDAO:
"""
params = (order_id, gc_note, tray_id)
self.db.cursor.execute(sql, params)
results = self.db.cursor.fetchall()
with SQLUtils('sqlite', database='db/jtDB.db') as db:
db.cursor.execute(sql, params)
results = db.cursor.fetchall()
data_list = []
for row in results:
@ -535,8 +557,9 @@ class InspectionDAO:
ORDER BY pack_time DESC
"""
params = (tray_id,)
self.db.cursor.execute(sql, params)
results = self.db.cursor.fetchall()
with SQLUtils('sqlite', database='db/jtDB.db') as db:
db.cursor.execute(sql, params)
results = db.cursor.fetchall()
return results
except Exception as e:
logging.error(f"获取包装记录失败: {str(e)}")
@ -558,31 +581,41 @@ class InspectionDAO:
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,?)
"""
params = (order_id, tray_id, label_value, weight_value, net_weight_value, finish_time, datetime.now(), 'system', datetime.now(), 'system', False,gc_note)
self.db.cursor.execute(sql, params)
self.db.conn.commit()
with SQLUtils('sqlite', database='db/jtDB.db') as db:
db.begin_transaction()
db.execute_update(sql, params)
db.commit_transaction()
return True
except Exception as e:
logging.error(f"保存包装记录失败: {str(e)}")
self.db.conn.rollback()
def delete_inspection_data(self, order_id, tray_id):
return False
def delete_inspection_data(self, order_id, gc_note, tray_id):
"""删除检验数据
Args:
order_id: 工程号
order_id: 订单号
gc_note: 工程号
tray_id: 托盘号
"""
try:
sql = """
UPDATE wsbz_inspection_data
SET is_deleted = TRUE
WHERE order_id = ? AND tray_id = ?
WHERE order_id = ? AND gc_note = ? AND tray_id = ?
"""
params = (order_id, tray_id)
self.db.cursor.execute(sql, params)
self.db.conn.commit()
params = (order_id, gc_note, tray_id)
with SQLUtils('sqlite', database='db/jtDB.db') as db:
db.begin_transaction()
db.execute_update(sql, params)
db.commit_transaction()
return True
except Exception as e:
logging.error(f"删除检验数据失败: {str(e)}")
self.db.conn.rollback()
return False
def get_axios_num_by_order_id(self, order_id):
"""获取托盘号对应的轴号"""
try:
@ -591,8 +624,9 @@ class InspectionDAO:
AND is_deleted = FALSE
"""
params = (order_id,)
self.db.cursor.execute(sql, params)
result = self.db.cursor.fetchone()
with SQLUtils('sqlite', database='db/jtDB.db') as db:
db.cursor.execute(sql, params)
result = db.cursor.fetchone()
return int(result[0]) if result[0] else 0
except Exception as e:
logging.error(f"获取轴号失败: {str(e)}")
@ -605,8 +639,9 @@ class InspectionDAO:
AND is_deleted = FALSE
"""
params = (tray_id,)
self.db.cursor.execute(sql, params)
result = self.db.cursor.fetchone()
with SQLUtils('sqlite', database='db/jtDB.db') as db:
db.cursor.execute(sql, params)
result = db.cursor.fetchone()
return int(result[0]) if result else 0
except Exception as e:
logging.error(f"获取轴号失败: {str(e)}")
@ -618,8 +653,9 @@ class InspectionDAO:
SELECT gzl_zl FROM wsbz_order_info WHERE ddmo = ?
"""
params = (order_id,)
self.db.cursor.execute(sql, params)
result = self.db.cursor.fetchone()
with SQLUtils('sqlite', database='db/jtDB.db') as db:
db.cursor.execute(sql, params)
result = db.cursor.fetchone()
return result[0] if result else 0
except Exception as e:
logging.error(f"获取工字轮重量失败: {str(e)}")
@ -631,8 +667,9 @@ class InspectionDAO:
SELECT bccd, tccd FROM wsbz_order_info WHERE ddmo = ?
"""
params = (order_id,)
self.db.cursor.execute(sql, params)
result = self.db.cursor.fetchone()
with SQLUtils('sqlite', database='db/jtDB.db') as db:
db.cursor.execute(sql, params)
result = db.cursor.fetchone()
return result[0],result[1] if result else None,None
except Exception as e:
logging.error(f"获取线径范围失败: {str(e)}")
@ -651,8 +688,9 @@ class InspectionDAO:
SELECT MIN(create_time) FROM wsbz_inspection_data
WHERE order_id = ? AND is_deleted = FALSE
"""
self.db.cursor.execute(sql, (order_id,))
result = self.db.cursor.fetchone()
with SQLUtils('sqlite', database='db/jtDB.db') as db:
db.cursor.execute(sql, (order_id,))
result = db.cursor.fetchone()
return result[0] if result and result[0] else None
except Exception as e:
logging.error(f"获取工程号创建时间失败: {str(e)}")
@ -683,8 +721,9 @@ class InspectionDAO:
ORDER BY first_create_time
"""
self.db.cursor.execute(sql, order_ids)
results = self.db.cursor.fetchall()
with SQLUtils('sqlite', database='db/jtDB.db') as db:
db.cursor.execute(sql, order_ids)
results = db.cursor.fetchall()
# 提取排序后的工程号
sorted_order_ids = [row[0] for row in results]
@ -715,14 +754,15 @@ class InspectionDAO:
FROM wsbz_order_info WHERE ddmo = ?
"""
params = (order_id,)
self.db.cursor.execute(sql, params)
result = self.db.cursor.fetchone()
with SQLUtils('sqlite', database='db/jtDB.db') as db:
db.cursor.execute(sql, params)
result = db.cursor.fetchone()
if not result:
return {}
# 获取列名
column_names = [desc[0] for desc in self.db.cursor.description]
column_names = [desc[0] for desc in db.cursor.description]
# 转换为字典
result_dict = {}
@ -755,8 +795,9 @@ class InspectionDAO:
AND COALESCE(value, '') != ''
"""
params = (gc_note, order_id, tray_id)
self.db.cursor.execute(sql, params)
results = self.db.cursor.fetchall()
with SQLUtils('sqlite', database='db/jtDB.db') as db:
db.cursor.execute(sql, params)
results = db.cursor.fetchall()
if not results:
return {}

View File

@ -80,255 +80,8 @@ class PalletTypeDAO:
except Exception as e:
logging.error(f"获取托盘类型失败: {str(e)}")
return None
def get_pallet_types_by_operation(self, operation_type, include_disabled=False):
"""根据操作类型获取托盘类型
Args:
operation_type: 操作类型 (input/output)
include_disabled: 是否包含禁用的类型
Returns:
list: 托盘类型列表
"""
try:
if include_disabled:
sql = """
SELECT id, type_name, operation_type, description, enabled, sort_order
FROM wsbz_pallet_types
WHERE operation_type = ? AND is_deleted = FALSE
ORDER BY sort_order
"""
params = (operation_type,)
else:
sql = """
SELECT id, type_name, operation_type, description, enabled, sort_order
FROM wsbz_pallet_types
WHERE operation_type = ? AND is_deleted = FALSE AND enabled = TRUE
ORDER BY sort_order
"""
params = (operation_type,)
self.db.cursor.execute(sql, params)
results = self.db.cursor.fetchall()
pallet_types = []
for row in results:
pallet_type = {
'id': row[0],
'type_name': row[1],
'operation_type': row[2],
'description': row[3],
'enabled': bool(row[4]),
'sort_order': row[5]
}
pallet_types.append(pallet_type)
return pallet_types
except Exception as e:
logging.error(f"获取托盘类型失败: {str(e)}")
return []
def get_pallet_type_by_id(self, pallet_type_id):
"""根据ID获取托盘类型
Args:
pallet_type_id: 托盘类型ID
Returns:
dict: 托盘类型信息未找到则返回None
"""
try:
sql = """
SELECT id, type_name, operation_type, description, enabled, sort_order
FROM wsbz_pallet_types
WHERE id = ? AND is_deleted = FALSE
"""
params = (pallet_type_id,)
self.db.cursor.execute(sql, params)
row = self.db.cursor.fetchone()
if row:
pallet_type = {
'id': row[0],
'type_name': row[1],
'operation_type': row[2],
'description': row[3],
'enabled': bool(row[4]),
'sort_order': row[5]
}
return pallet_type
else:
return None
except Exception as e:
logging.error(f"获取托盘类型失败: {str(e)}")
return None
def get_pallet_type_by_type(self,pallet_type):
"""根据托盘类型获取托盘信息
Args:
pallet_type: 托盘类型
Returns:
dict: 托盘类型信息未找到则返回None
"""
try:
sql = """
SELECT id, type_name, operation_type, description, enabled, sort_order
FROM wsbz_pallet_types
WHERE type_name = ? AND is_deleted = FALSE
"""
params = (pallet_type, )
self.db.cursor.execute(sql, params)
results = self.db.cursor.fetchall()
pallet_type_dict = {row[1]:row[5] for row in results}
return pallet_type_dict
except Exception as e:
logging.error(f"获取托盘类型失败: {str(e)}")
return {}
def create_pallet_type(self, data, username='system'):
"""创建托盘类型
Args:
data: 托盘类型数据
username: 操作用户
Returns:
int: 新创建的托盘类型ID失败返回None
"""
try:
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
sql = """
INSERT INTO wsbz_pallet_types (
type_name, operation_type, description, enabled, sort_order,
create_time, create_by, update_time, update_by, is_deleted
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
params = (
data.get('type_name'),
data.get('operation_type'),
data.get('description', ''),
data.get('enabled', True),
data.get('sort_order', 999),
current_time,
username,
current_time,
username,
False
)
self.db.execute_update(sql, params)
# 获取新插入的ID
self.db.cursor.execute("SELECT last_insert_rowid()")
new_id = self.db.cursor.fetchone()[0]
return new_id
except Exception as e:
logging.error(f"创建托盘类型失败: {str(e)}")
return None
def update_pallet_type(self, pallet_type_id, data, username='system'):
"""更新托盘类型
Args:
pallet_type_id: 托盘类型ID
data: 更新数据
username: 操作用户
Returns:
bool: 更新是否成功
"""
try:
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# 构建更新SQL
update_fields = []
params = []
# 可更新的字段
allowed_fields = [
'type_name', 'operation_type', 'description', 'enabled', 'sort_order'
]
for field in allowed_fields:
if field in data:
update_fields.append(f"{field} = ?")
params.append(data[field])
# 添加更新时间和更新人
update_fields.append("update_time = ?")
params.append(current_time)
update_fields.append("update_by = ?")
params.append(username)
# 添加ID到参数列表
params.append(pallet_type_id)
# 构建SQL
sql = f"""
UPDATE wsbz_pallet_types
SET {', '.join(update_fields)}
WHERE id = ?
"""
self.db.execute_update(sql, params)
return True
except Exception as e:
logging.error(f"更新托盘类型失败: {str(e)}")
return False
def delete_pallet_type(self, pallet_type_id, username='system'):
"""删除托盘类型(软删除)
Args:
pallet_type_id: 托盘类型ID
username: 操作用户
Returns:
bool: 删除是否成功
"""
try:
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
sql = """
UPDATE wsbz_pallet_types
SET is_deleted = TRUE, update_time = ?, update_by = ?
WHERE id = ?
"""
params = (current_time, username, pallet_type_id)
self.db.execute_update(sql, params)
return True
except Exception as e:
logging.error(f"删除托盘类型失败: {str(e)}")
return False
def toggle_pallet_type(self, pallet_type_id, enabled, username='system'):
"""启用或禁用托盘类型
Args:
pallet_type_id: 托盘类型ID
enabled: 是否启用
username: 操作用户
Returns:
bool: 操作是否成功
"""
try:
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
sql = """
UPDATE wsbz_pallet_types
SET enabled = ?, update_time = ?, update_by = ?
WHERE id = ? AND is_deleted = FALSE
"""
params = (enabled, current_time, username, pallet_type_id)
self.db.execute_update(sql, params)
return True
except Exception as e:
logging.error(f"更新托盘类型启用状态失败: {str(e)}")
return False
def save_pallet_info(self, pallet_code, pallet_info, user_id):
"""保存托盘信息
@ -446,6 +199,131 @@ class PalletTypeDAO:
logging.error(f"获取托盘类型失败: {str(e)}")
return {}
def get_all_pallet_types_list(self):
"""获取所有托盘类型列表
Returns:
list: 包含所有托盘类型信息的列表每个元素是一个字典包含id和name
"""
try:
sql = """
SELECT pallet_id, pallet_name FROM wsbz_pallet_type ORDER BY pallet_id
"""
self.db.cursor.execute(sql)
results = self.db.cursor.fetchall()
pallet_types = []
for row in results:
pallet_type = {
'id': row[0],
'name': row[1]
}
pallet_types.append(pallet_type)
return pallet_types
except Exception as e:
logging.error(f"获取托盘类型列表失败: {str(e)}")
return []
def add_pallet_type(self, pallet_id, pallet_name):
"""添加托盘类型
Args:
pallet_id: 托盘ID
pallet_name: 托盘名称
Returns:
bool: 是否添加成功
"""
try:
sql = """
INSERT INTO wsbz_pallet_type (pallet_id, pallet_name)
VALUES (?, ?)
"""
params = (pallet_id, pallet_name)
self.db.execute_update(sql, params)
logging.info(f"添加托盘类型成功: ID={pallet_id}, 名称={pallet_name}")
return True
except Exception as e:
logging.error(f"添加托盘类型失败: {str(e)}")
return False
def update_pallet_type(self, pallet_id, pallet_name):
"""更新托盘类型
Args:
pallet_id: 托盘ID
pallet_name: 托盘名称
Returns:
bool: 是否更新成功
"""
try:
sql = """
UPDATE wsbz_pallet_type
SET pallet_name = ?
WHERE pallet_id = ?
"""
params = (pallet_name, pallet_id)
self.db.execute_update(sql, params)
logging.info(f"更新托盘类型成功: ID={pallet_id}, 名称={pallet_name}")
return True
except Exception as e:
logging.error(f"更新托盘类型失败: {str(e)}")
return False
def delete_pallet_type(self, pallet_id):
"""删除托盘类型
Args:
pallet_id: 托盘ID
Returns:
bool: 是否删除成功
"""
try:
sql = """
DELETE FROM wsbz_pallet_type
WHERE pallet_id = ?
"""
params = (pallet_id,)
self.db.execute_update(sql, params)
logging.info(f"删除托盘类型成功: ID={pallet_id}")
return True
except Exception as e:
logging.error(f"删除托盘类型失败: {str(e)}")
return False
def get_pallet_type_by_id(self, pallet_id):
"""根据ID获取托盘类型
Args:
pallet_id: 托盘ID
Returns:
dict: 托盘类型信息未找到则返回None
"""
try:
sql = """
SELECT pallet_id, pallet_name
FROM wsbz_pallet_type
WHERE pallet_id = ?
"""
params = (pallet_id,)
self.db.cursor.execute(sql, params)
row = self.db.cursor.fetchone()
if row:
pallet_type = {
'id': row[0],
'name': row[1]
}
return pallet_type
else:
return None
except Exception as e:
logging.error(f"根据ID获取托盘类型失败: {str(e)}")
return None
def save_pallet_archives(self, pallet_code, tier, user_id, user_name):
"""保存托盘档案

Binary file not shown.

View File

@ -55,25 +55,23 @@ SET enum_values = '["A区", "B区", "C区", "D区"]'
WHERE name = 'fzd' AND is_deleted = FALSE;
-- 包装记录表
drop table if exists wsbz_inspection_pack_data;
create table if not exists wsbz_inspection_pack_data
DROP TABLE IF EXISTS wsbz_inspection_pack_data;
CREATE TABLE IF NOT EXISTS wsbz_inspection_pack_data
(
--
order_id VARCHAR(50),
--
material VARCHAR(50),
--
spec VARCHAR(50),
--
tray_id VARCHAR(50),
--
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id VARCHAR(50) NOT NULL,
tray_id VARCHAR(50) NOT NULL,
gc_note VARCHAR(50),
axis_package_id VARCHAR(50),
weight REAL,
net_weight REAL,
pack_time TIMESTAMP,
create_time TIMESTAMP NOT NULL,
create_by VARCHAR(50) NOT NULL,
update_time TIMESTAMP,
update_by VARCHAR(50),
is_deleted BOOLEAN
);
is_deleted BOOLEAN DEFAULT FALSE
)
-- 创建托盘类型配置表
CREATE TABLE IF NOT EXISTS pallet_types (

View File

@ -2,20 +2,19 @@ from pymodbus.client import ModbusTcpClient
import time
client = ModbusTcpClient('localhost', port=5020)
client.connect()
client.write_registers(address=11, values=[2242])
client.write_registers(address=13, values=[0])
client.write_registers(address=21, values=[0])
# client.write_registers(address=11, values=[2247])
# client.write_registers(address=3, values=[0])
time.sleep(2)
# client.write_registers(address=21, values=[1])
client.write_registers(address=0, values=[0])
client.write_registers(address=4, values=[0])
client.write_registers(address=20, values=[0])
# client.write_registers(address=30, values=[25])
# client.write_registers(address=5, values=[16])
# 贴标完成
# client.write_registers(address=24, values=[1])s
client.write_registers(address=13, values=[1])
# client.write_registers(address=13, values=[1])
result = client.read_holding_registers(address=1, count=1)
result = client.read_holding_registers(address=0, count=1)
print(result.registers[0],"123===")
client.close()

21
main.py
View File

@ -134,6 +134,12 @@ def main():
# 键盘监听器配置信息
enable_keyboard_listener = config.get_value('serial.keyboard.enabled', False)
logging.info(f"配置信息 - 启用串口: {enable_serial_ports}, 启用键盘监听: {enable_keyboard_listener}")
# 初始化电力监控器
from utils.electricity_monitor import ElectricityMonitor
electricity_monitor = ElectricityMonitor.get_instance()
electricity_monitor.start()
logging.info("电力监控器已启动")
# 设置中文翻译器
translator = QTranslator(app)
@ -196,10 +202,23 @@ def main():
exit_code = app.exec()
logging.info(f"应用程序退出,退出码: {exit_code}")
# 关闭所有数据库连接
# 记录数据库连接池状态
from utils.sql_utils import SQLUtils
logging.info("记录数据库连接池状态:")
SQLUtils.log_connection_pool_status()
# 关闭所有数据库连接
SQLUtils.close_all_connections()
# 停止电力监控器
try:
from utils.electricity_monitor import ElectricityMonitor
electricity_monitor = ElectricityMonitor.get_instance()
electricity_monitor.stop()
logging.info("电力监控器已停止")
except Exception as e:
logging.error(f"停止电力监控器时发生错误: {e}")
sys.exit(exit_code)
except Exception as e:

20
requirements.txt Normal file
View File

@ -0,0 +1,20 @@
# 基础GUI框架
PySide6>=6.2.0
# 数据库连接器
psycopg2-binary>=2.9.3 # PostgreSQL连接器二进制版本无需编译
mysql-connector-python>=8.0.27 # MySQL连接器
# 通信和协议
pymodbus>=3.0.0 # Modbus通信协议
pyserial>=3.5 # 串口通信
# 工具库
python-dateutil>=2.8.2 # 日期时间处理
pytz>=2022.1 # 时区处理
pandas>=1.4.0 # 数据分析和处理
# 可选依赖
pillow>=9.0.0 # 图像处理,用于相机功能
pynput>=1.7.6 # 键盘监听
requests>=2.27.1 # HTTP请求

2973
tests/main_window_old.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,231 @@
from PySide6.QtWidgets import (
QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout,
QFormLayout, QSpinBox, QGroupBox, QFrame
)
from PySide6.QtCore import Qt, Signal, QTimer
from PySide6.QtGui import QFont
from datetime import datetime, timedelta
from utils.electricity_monitor import ElectricityMonitor
from dao.electricity_dao import ElectricityDAO
import logging
class ElectricitySettingsUI(QWidget):
"""电力监控设置UI组件"""
# 定义信号
settings_changed = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.parent = parent
self.electricity_monitor = ElectricityMonitor.get_instance()
self.electricity_dao = ElectricityDAO()
# 确保电力消耗表已创建
self.electricity_dao.create_table_if_not_exists()
self.init_ui()
# 创建定时器用于更新UI
self.update_timer = QTimer(self)
self.update_timer.timeout.connect(self.update_status)
self.update_timer.start(1000) # 每秒更新一次
# 初始更新状态
self.update_status()
def init_ui(self):
"""初始化UI"""
# 设置字体
self.title_font = QFont("微软雅黑", 14, QFont.Bold)
self.normal_font = QFont("微软雅黑", 10)
self.small_font = QFont("微软雅黑", 9)
# 创建主布局
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(20, 20, 20, 20)
main_layout.setSpacing(15)
# 创建标题
title_label = QLabel("电力监控设置")
title_label.setFont(self.title_font)
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("color: #1a237e; margin-bottom: 10px;")
main_layout.addWidget(title_label)
# 创建状态组
status_group = QGroupBox("监控状态")
status_group.setFont(self.normal_font)
status_layout = QFormLayout()
# 监控状态
self.status_label = QLabel("未知")
self.status_label.setFont(self.normal_font)
status_layout.addRow("监控状态:", self.status_label)
# 当前电量
self.electricity_label = QLabel("--")
self.electricity_label.setFont(self.normal_font)
status_layout.addRow("当前电量:", self.electricity_label)
# 上次监听时间
self.last_time_label = QLabel("--")
self.last_time_label.setFont(self.normal_font)
status_layout.addRow("上次监听时间:", self.last_time_label)
# 下次监听时间
self.next_time_label = QLabel("--")
self.next_time_label.setFont(self.normal_font)
status_layout.addRow("下次监听时间:", self.next_time_label)
status_group.setLayout(status_layout)
main_layout.addWidget(status_group)
# 创建设置组
settings_group = QGroupBox("监控设置")
settings_group.setFont(self.normal_font)
settings_layout = QFormLayout()
# 监听间隔
self.interval_spinbox = QSpinBox()
self.interval_spinbox.setFont(self.normal_font)
self.interval_spinbox.setRange(1, 60) # 1-60分钟
self.interval_spinbox.setValue(self.electricity_monitor.interval_minutes) # 使用当前设置的值
self.interval_spinbox.setSuffix(" 分钟")
self.interval_spinbox.valueChanged.connect(self.on_interval_changed)
settings_layout.addRow("监听间隔:", self.interval_spinbox)
settings_group.setLayout(settings_layout)
main_layout.addWidget(settings_group)
# 创建按钮组
button_layout = QHBoxLayout()
button_layout.setSpacing(20)
# 开始监听按钮
self.start_button = QPushButton("开始监听")
self.start_button.setFont(self.normal_font)
self.start_button.setMinimumHeight(40)
self.start_button.setStyleSheet("background-color: #4CAF50; color: white;")
self.start_button.clicked.connect(self.start_monitoring)
button_layout.addWidget(self.start_button)
# 暂停监听按钮
self.stop_button = QPushButton("暂停监听")
self.stop_button.setFont(self.normal_font)
self.stop_button.setMinimumHeight(40)
self.stop_button.setStyleSheet("background-color: #F44336; color: white;")
self.stop_button.clicked.connect(self.stop_monitoring)
button_layout.addWidget(self.stop_button)
main_layout.addLayout(button_layout)
# 添加分隔线
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
separator.setFrameShadow(QFrame.Sunken)
main_layout.addWidget(separator)
# 添加历史数据标签
history_label = QLabel("历史数据")
history_label.setFont(self.title_font)
history_label.setAlignment(Qt.AlignCenter)
history_label.setStyleSheet("color: #1a237e; margin: 10px 0;")
main_layout.addWidget(history_label)
# 占位符,用于未来扩展
placeholder = QLabel("历史数据功能将在未来版本中提供")
placeholder.setFont(self.normal_font)
placeholder.setAlignment(Qt.AlignCenter)
placeholder.setStyleSheet("color: #888888; padding: 20px;")
main_layout.addWidget(placeholder)
# 添加弹性空间
main_layout.addStretch(1)
def update_status(self):
"""更新状态显示"""
try:
# 检查监控器状态
is_monitoring = self.electricity_monitor.is_monitoring()
# 更新状态标签
if is_monitoring:
self.status_label.setText("正在监听")
self.status_label.setStyleSheet("color: green; font-weight: bold;")
self.start_button.setEnabled(False)
self.stop_button.setEnabled(True)
else:
self.status_label.setText("已暂停")
self.status_label.setStyleSheet("color: red; font-weight: bold;")
self.start_button.setEnabled(True)
self.stop_button.setEnabled(False)
# 获取最新电力数据
latest_data = self.electricity_dao.get_latest_electricity_data()
if latest_data:
self.electricity_label.setText(str(latest_data['electricity_number']))
self.last_time_label.setText(latest_data['sync_time'])
# 计算下次监听时间
if is_monitoring:
next_time = self.electricity_monitor.get_next_read_time()
if next_time:
self.next_time_label.setText(next_time.strftime('%Y-%m-%d %H:%M:%S'))
else:
self.next_time_label.setText("计算中...")
else:
self.next_time_label.setText("监听已暂停")
else:
self.electricity_label.setText("--")
self.last_time_label.setText("--")
if is_monitoring:
self.next_time_label.setText("等待首次读取...")
else:
self.next_time_label.setText("监听已暂停")
except Exception as e:
logging.error(f"更新电力监控状态时发生错误: {str(e)}")
def on_interval_changed(self, value):
"""监听间隔变更处理
Args:
value: 新的间隔值分钟
"""
try:
# 更新监控器间隔
self.electricity_monitor.set_interval(value)
# 发出设置变更信号
self.settings_changed.emit()
except Exception as e:
logging.error(f"设置电力监控间隔时发生错误: {str(e)}")
def start_monitoring(self):
"""开始监听"""
try:
# 启动电力监控器
self.electricity_monitor.start()
# 更新UI
self.update_status()
# 发出设置变更信号
self.settings_changed.emit()
except Exception as e:
logging.error(f"启动电力监控时发生错误: {str(e)}")
def stop_monitoring(self):
"""暂停监听"""
try:
# 停止电力监控器
self.electricity_monitor.stop()
# 更新UI
self.update_status()
# 发出设置变更信号
self.settings_changed.emit()
except Exception as e:
logging.error(f"停止电力监控时发生错误: {str(e)}")

View File

@ -18,7 +18,7 @@ class LoginUI(QWidget):
font_version = QFont("微软雅黑", 8)
# 标题
self.label_title = QLabel("腾龙集团MES管理系统")
self.label_title = QLabel("MES管理系统")
self.label_title.setFont(font_title)
self.label_title.setStyleSheet("color: #1a237e;")
self.label_title.setAlignment(Qt.AlignCenter)

View File

@ -210,7 +210,7 @@ class MainWindowUI(QMainWindow):
self.material_frame = QFrame()
self.material_frame.setFrameShape(QFrame.StyledPanel)
self.material_frame.setLineWidth(1)
self.material_frame.setFixedHeight(380) # 增加高度以匹配图片
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)
@ -226,20 +226,24 @@ class MainWindowUI(QMainWindow):
# 上料区内容 - 这里可以添加更多控件
self.material_content = QWidget()
self.material_content.setStyleSheet("background-color: black;") # 黑色背景适合显示相机画面
# 使用透明背景,让相机画面可以正常显示
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包裹添加边框
# 托盘号区域 - 使用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_layout.setContentsMargins(0, 0, 0, 0) # 移除内边距,确保没有上下边界 # 移除所有边距
self.tray_layout.setSpacing(0) # 移除所有间距
self.tray_label = QLabel("托盘号")
self.tray_label.setFont(self.normal_font)
@ -249,7 +253,8 @@ class MainWindowUI(QMainWindow):
self.tray_layout.addWidget(self.tray_label)
self.tray_edit = QComboBox()
self.tray_edit.setStyleSheet("border: none; padding: 5px 10px;")
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) # 不自动插入用户输入到列表中
@ -267,7 +272,7 @@ class MainWindowUI(QMainWindow):
self.output_frame = QFrame()
self.output_frame.setFrameShape(QFrame.StyledPanel)
self.output_frame.setLineWidth(1)
self.output_frame.setFixedHeight(150) # 增加高度以匹配图片
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)
@ -285,7 +290,7 @@ class MainWindowUI(QMainWindow):
self.output_content = QWidget()
self.output_content.setStyleSheet("background-color: white;")
self.output_content_layout = QVBoxLayout(self.output_content)
self.output_content_layout.setContentsMargins(10, 10, 10, 10)
self.output_content_layout.setContentsMargins(5, 5, 5, 5) # 减小内部边距
self.output_layout.addWidget(self.output_content)
self.left_layout.addWidget(self.output_frame)

View File

@ -32,62 +32,12 @@ class PalletTypeSettingsUI(QWidget):
# 创建下料类型配置页面
self.output_widget = self.create_pallet_type_widget("output")
self.main_layout.addWidget(self.output_widget, 1)
# 底部按钮区域
self.button_layout = QHBoxLayout()
self.button_layout.setContentsMargins(0, 10, 0, 0)
self.save_button = QPushButton("保存配置")
self.save_button.setFont(self.normal_font)
self.save_button.setFixedSize(120, 40)
self.save_button.setStyleSheet("""
QPushButton {
background-color: #4caf50;
color: white;
border: none;
border-radius: 5px;
}
QPushButton:hover {
background-color: #45a049;
}
QPushButton:pressed {
background-color: #3d8b40;
}
""")
self.reset_button = QPushButton("重置")
self.reset_button.setFont(self.normal_font)
self.reset_button.setFixedSize(120, 40)
self.reset_button.setStyleSheet("""
QPushButton {
background-color: #f44336;
color: white;
border: none;
border-radius: 5px;
}
QPushButton:hover {
background-color: #e53935;
}
QPushButton:pressed {
background-color: #d32f2f;
}
""")
self.button_layout.addStretch()
self.button_layout.addWidget(self.reset_button)
self.button_layout.addSpacing(20)
self.button_layout.addWidget(self.save_button)
self.main_layout.addLayout(self.button_layout)
# 创建托盘类型配置页面
self.pallet_type_widget = self.create_pallet_type_widget()
self.main_layout.addWidget(self.pallet_type_widget, 1)
def create_pallet_type_widget(self, operation_type):
def create_pallet_type_widget(self):
"""创建托盘类型配置部件
Args:
operation_type: 操作类型 (input/output)
Returns:
QWidget: 托盘类型配置部件
@ -98,19 +48,17 @@ class PalletTypeSettingsUI(QWidget):
layout.setSpacing(15)
# 创建表格
table = QTableWidget()
table.setFont(self.normal_font)
table.setColumnCount(4)
table.setHorizontalHeaderLabels(["类型名称", "描述", "排序", "启用"])
table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents)
table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeToContents)
table.verticalHeader().setVisible(False)
table.setSelectionBehavior(QAbstractItemView.SelectRows)
table.setSelectionMode(QAbstractItemView.SingleSelection)
table.setAlternatingRowColors(True)
table.setStyleSheet("""
self.pallet_table = QTableWidget()
self.pallet_table.setFont(self.normal_font)
self.pallet_table.setColumnCount(2) # 只有两列托盘ID和托盘名称
self.pallet_table.setHorizontalHeaderLabels(["托盘ID", "托盘名称"])
self.pallet_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
self.pallet_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
self.pallet_table.verticalHeader().setVisible(False)
self.pallet_table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.pallet_table.setSelectionMode(QAbstractItemView.SingleSelection)
self.pallet_table.setAlternatingRowColors(True)
self.pallet_table.setStyleSheet("""
QTableWidget {
border: 1px solid #ddd;
border-radius: 5px;
@ -124,8 +72,7 @@ class PalletTypeSettingsUI(QWidget):
font-weight: bold;
}
""")
table.setObjectName(f"{operation_type}_table")
layout.addWidget(table)
layout.addWidget(self.pallet_table)
# 创建表单
form_group = QGroupBox("编辑托盘类型")
@ -134,42 +81,26 @@ class PalletTypeSettingsUI(QWidget):
form_layout.setContentsMargins(15, 15, 15, 15)
form_layout.setSpacing(10)
# 类型名称
type_name_input = QLineEdit()
type_name_input.setFont(self.normal_font)
type_name_input.setObjectName(f"{operation_type}_type_name_input")
form_layout.addRow("类型名称:", type_name_input)
# 托盘ID
self.pallet_id_input = QLineEdit()
self.pallet_id_input.setFont(self.normal_font)
self.pallet_id_input.setPlaceholderText("请输入托盘ID")
form_layout.addRow("托盘ID:", self.pallet_id_input)
# 描述
desc_input = QLineEdit()
desc_input.setFont(self.normal_font)
desc_input.setObjectName(f"{operation_type}_desc_input")
form_layout.addRow("描述:", desc_input)
# 排序
sort_order_spin = QSpinBox()
sort_order_spin.setFont(self.normal_font)
sort_order_spin.setRange(1, 999)
sort_order_spin.setValue(100)
sort_order_spin.setObjectName(f"{operation_type}_sort_order_spin")
form_layout.addRow("排序:", sort_order_spin)
# 启用
enabled_check = QCheckBox("启用")
enabled_check.setFont(self.normal_font)
enabled_check.setChecked(True)
enabled_check.setObjectName(f"{operation_type}_enabled_check")
form_layout.addRow("", enabled_check)
# 托盘名称
self.pallet_name_input = QLineEdit()
self.pallet_name_input.setFont(self.normal_font)
self.pallet_name_input.setPlaceholderText("请输入托盘名称")
form_layout.addRow("托盘名称:", self.pallet_name_input)
layout.addWidget(form_group)
# 创建按钮区域
button_layout = QHBoxLayout()
add_button = QPushButton("添加")
add_button.setFont(self.normal_font)
add_button.setObjectName(f"{operation_type}_add_button")
add_button.setStyleSheet("""
self.add_button = QPushButton("添加")
self.add_button.setFont(self.normal_font)
self.add_button.setStyleSheet("""
QPushButton {
background-color: #4caf50;
color: white;
@ -182,10 +113,9 @@ class PalletTypeSettingsUI(QWidget):
}
""")
update_button = QPushButton("更新")
update_button.setFont(self.normal_font)
update_button.setObjectName(f"{operation_type}_update_button")
update_button.setStyleSheet("""
self.update_button = QPushButton("更新")
self.update_button.setFont(self.normal_font)
self.update_button.setStyleSheet("""
QPushButton {
background-color: #2196f3;
color: white;
@ -198,10 +128,9 @@ class PalletTypeSettingsUI(QWidget):
}
""")
delete_button = QPushButton("删除")
delete_button.setFont(self.normal_font)
delete_button.setObjectName(f"{operation_type}_delete_button")
delete_button.setStyleSheet("""
self.delete_button = QPushButton("删除")
self.delete_button.setFont(self.normal_font)
self.delete_button.setStyleSheet("""
QPushButton {
background-color: #f44336;
color: white;
@ -214,10 +143,9 @@ class PalletTypeSettingsUI(QWidget):
}
""")
cancel_button = QPushButton("取消")
cancel_button.setFont(self.normal_font)
cancel_button.setObjectName(f"{operation_type}_cancel_button")
cancel_button.setStyleSheet("""
self.cancel_button = QPushButton("取消")
self.cancel_button.setFont(self.normal_font)
self.cancel_button.setStyleSheet("""
QPushButton {
background-color: #9e9e9e;
color: white;
@ -230,10 +158,10 @@ class PalletTypeSettingsUI(QWidget):
}
""")
button_layout.addWidget(add_button)
button_layout.addWidget(update_button)
button_layout.addWidget(delete_button)
button_layout.addWidget(cancel_button)
button_layout.addWidget(self.add_button)
button_layout.addWidget(self.update_button)
button_layout.addWidget(self.delete_button)
button_layout.addWidget(self.cancel_button)
layout.addLayout(button_layout)

View File

@ -28,7 +28,7 @@ class SerialSettingsUI(QWidget):
enable_layout.addStretch()
main_layout.addLayout(enable_layout)
# 创建串口设置组
# # 创建串口设置组
serial_group = QGroupBox("串口设置")
serial_layout = QGridLayout(serial_group)
@ -39,6 +39,7 @@ class SerialSettingsUI(QWidget):
# 串口选择
mdz_port_layout = QHBoxLayout()
self.mdz_port_combo = QComboBox()
self.mdz_port_combo.addItem("不使用", "") # 添加空选项
self.mdz_refresh_btn = QPushButton("刷新")
mdz_port_layout.addWidget(self.mdz_port_combo)
mdz_port_layout.addWidget(self.mdz_refresh_btn)
@ -85,6 +86,7 @@ class SerialSettingsUI(QWidget):
# 串口选择
xj_port_layout = QHBoxLayout()
self.xj_port_combo = QComboBox()
self.xj_port_combo.addItem("不使用", "") # 添加空选项
self.xj_refresh_btn = QPushButton("刷新")
xj_port_layout.addWidget(self.xj_port_combo)
xj_port_layout.addWidget(self.xj_refresh_btn)
@ -114,10 +116,47 @@ class SerialSettingsUI(QWidget):
self.xj_parity_combo.addItem(parity[0], parity[1])
xj_layout.addRow("校验位:", self.xj_parity_combo)
# 扫码器串口设置
scanner_group = QGroupBox("扫码器串口")
scanner_layout = QFormLayout(scanner_group)
# 串口选择
scanner_port_layout = QHBoxLayout()
self.scanner_port_combo = QComboBox()
self.scanner_port_combo.addItem("不使用", "") # 添加空选项
self.scanner_refresh_btn = QPushButton("刷新")
scanner_port_layout.addWidget(self.scanner_port_combo)
scanner_port_layout.addWidget(self.scanner_refresh_btn)
scanner_layout.addRow("串口:", scanner_port_layout)
# 波特率
self.scanner_baud_combo = QComboBox()
for baud in ["1200", "2400", "4800", "9600", "19200", "38400", "57600", "115200"]:
self.scanner_baud_combo.addItem(baud)
scanner_layout.addRow("波特率:", self.scanner_baud_combo)
# 数据位
self.scanner_data_bits_combo = QComboBox()
for bits in ["5", "6", "7", "8"]:
self.scanner_data_bits_combo.addItem(bits)
scanner_layout.addRow("数据位:", self.scanner_data_bits_combo)
# 停止位
self.scanner_stop_bits_combo = QComboBox()
for bits in ["1", "1.5", "2"]:
self.scanner_stop_bits_combo.addItem(bits)
scanner_layout.addRow("停止位:", self.scanner_stop_bits_combo)
# 校验位
self.scanner_parity_combo = QComboBox()
for parity in [("无校验", "N"), ("奇校验", "O"), ("偶校验", "E")]:
self.scanner_parity_combo.addItem(parity[0], parity[1])
scanner_layout.addRow("校验位:", self.scanner_parity_combo)
# 将三个组添加到布局
serial_layout.addWidget(mdz_group, 0, 0)
serial_layout.addWidget(xj_group, 0, 1)
serial_layout.addWidget(scanner_group, 1, 0) # 添加扫码器串口设置,放在第二行第一列
# 设置列伸缩因子使两列等宽比例1:1
serial_layout.setColumnStretch(0, 1)
@ -129,8 +168,10 @@ class SerialSettingsUI(QWidget):
test_layout = QHBoxLayout()
self.test_mdz_btn = QPushButton("测试米电阻串口")
self.test_xj_btn = QPushButton("测试线径串口")
self.test_scanner_btn = QPushButton("测试扫码器串口") # 添加测试扫码器串口按钮
test_layout.addWidget(self.test_mdz_btn)
test_layout.addWidget(self.test_xj_btn)
test_layout.addWidget(self.test_scanner_btn) # 添加到布局
test_layout.addStretch()
main_layout.addLayout(test_layout)

View File

@ -3,7 +3,7 @@ from PySide6.QtWidgets import (
QTabWidget, QFrame, QFormLayout, QGroupBox, QRadioButton, QSpacerItem, QSizePolicy,
QTableWidget, QTableWidgetItem, QHeaderView, QSlider, QCheckBox, QButtonGroup
)
from PySide6.QtGui import QFont, QBrush, QColor
from PySide6.QtGui import QFont, QBrush, QColor, QIntValidator
from PySide6.QtCore import Qt, Signal, QSize
class SettingsUI(QWidget):
@ -39,6 +39,7 @@ class SettingsUI(QWidget):
self.create_database_tab()
self.create_inspection_tab()
self.create_plc_tab()
self.create_electricity_tab() # 新增电量监控标签页
self.create_push_tab()
self.create_auth_tab()
self.create_user_tab()
@ -64,6 +65,7 @@ class SettingsUI(QWidget):
self.refresh_button = QPushButton("刷新设备")
self.refresh_button.setFont(self.normal_font)
self.refresh_button.setObjectName("refresh_button") # 设置对象名称,方便查找
self.camera_select_layout.addWidget(self.camera_label)
self.camera_select_layout.addWidget(self.camera_combo)
@ -106,12 +108,12 @@ class SettingsUI(QWidget):
self.exposure_label = QLabel("曝光时间:")
self.exposure_label.setFont(self.normal_font)
self.exposure_slider = QSlider(Qt.Horizontal)
self.exposure_slider.setMinimum(1000)
self.exposure_slider.setMaximum(50000)
self.exposure_slider.setValue(20000)
self.exposure_slider.setMinimum(0)
self.exposure_slider.setMaximum(100) # 使用0-100的范围在代码中映射到0-20000μs
self.exposure_slider.setValue(50) # 默认值设为50%对应10000μs
self.exposure_slider.setTickPosition(QSlider.TicksBelow)
self.exposure_slider.setTickInterval(5000)
self.exposure_value = QLabel("20000 μs")
self.exposure_slider.setTickInterval(10)
self.exposure_value = QLabel("10000 μs")
self.exposure_value.setFont(self.normal_font)
self.exposure_value.setMinimumWidth(80)
@ -354,11 +356,71 @@ class SettingsUI(QWidget):
self.plc_layout = QVBoxLayout(self.plc_tab)
self.plc_layout.setContentsMargins(20, 20, 20, 20)
# 占位标签
self.plc_placeholder = QLabel("PLC设置待实现")
self.plc_placeholder.setFont(self.normal_font)
self.plc_placeholder.setAlignment(Qt.AlignCenter)
self.plc_layout.addWidget(self.plc_placeholder)
# 创建Modbus配置组
self.modbus_group = QGroupBox("Modbus 配置")
self.modbus_group.setFont(self.normal_font)
self.modbus_layout = QFormLayout(self.modbus_group)
self.modbus_layout.setContentsMargins(15, 20, 15, 15)
self.modbus_layout.setSpacing(15)
# 主机地址输入框
self.modbus_host_input = QLineEdit()
self.modbus_host_input.setFont(self.normal_font)
self.modbus_host_input.setPlaceholderText("请输入Modbus主机地址")
self.modbus_layout.addRow(QLabel("主机地址:"), self.modbus_host_input)
# 端口输入框
self.modbus_port_input = QLineEdit()
self.modbus_port_input.setFont(self.normal_font)
self.modbus_port_input.setPlaceholderText("请输入Modbus端口")
self.modbus_port_input.setValidator(QIntValidator(1, 65535))
self.modbus_layout.addRow(QLabel("端口:"), self.modbus_port_input)
# 测试连接按钮
self.modbus_test_button = QPushButton("测试连接")
self.modbus_test_button.setFont(self.normal_font)
self.modbus_test_button.setStyleSheet("""
QPushButton {
background-color: #2196f3;
color: white;
border: none;
border-radius: 3px;
padding: 5px 15px;
}
QPushButton:hover {
background-color: #1e88e5;
}
""")
self.modbus_layout.addRow("", self.modbus_test_button)
# 添加Modbus配置组到布局
self.plc_layout.addWidget(self.modbus_group)
# 添加保存按钮
self.plc_save_button = QPushButton("保存配置")
self.plc_save_button.setFont(self.normal_font)
self.plc_save_button.setFixedSize(120, 40)
self.plc_save_button.setStyleSheet("""
QPushButton {
background-color: #4caf50;
color: white;
border: none;
border-radius: 5px;
}
QPushButton:hover {
background-color: #45a049;
}
QPushButton:pressed {
background-color: #3d8b40;
}
""")
# 添加按钮布局
button_layout = QHBoxLayout()
button_layout.addStretch(1)
button_layout.addWidget(self.plc_save_button)
self.plc_layout.addLayout(button_layout)
self.plc_layout.addStretch(1)
self.tab_widget.addTab(self.plc_tab, "PLC设置")
@ -423,6 +485,24 @@ class SettingsUI(QWidget):
self.tab_widget.addTab(self.param_tab, "参数配置")
def create_electricity_tab(self):
"""创建电力监控选项卡"""
# 电力监控选项卡
self.electricity_tab = QWidget()
self.electricity_layout = QVBoxLayout(self.electricity_tab)
self.electricity_layout.setContentsMargins(0, 0, 0, 0)
self.electricity_layout.setSpacing(0)
# 添加一个临时提示标签表示此处将由ElectricitySettingsUI替换
self.electricity_placeholder = QLabel("正在加载电力监控设置...")
self.electricity_placeholder.setFont(self.normal_font)
self.electricity_placeholder.setAlignment(Qt.AlignCenter)
self.electricity_placeholder.setStyleSheet("color: #888888; padding: 20px;")
self.electricity_layout.addWidget(self.electricity_placeholder)
# 添加到选项卡
self.tab_widget.addTab(self.electricity_tab, "电力监控")
def create_pallet_type_tab(self):
# 托盘类型设置选项卡
self.pallet_type_tab = QWidget()

View File

@ -0,0 +1,193 @@
import logging
import time
from threading import Thread, Event
from PySide6.QtCore import QTimer
from utils.modbus_utils import ModbusUtils
from utils.modbus_monitor import RegisterHandler
from dao.electricity_dao import ElectricityDAO
from utils.config_loader import ConfigLoader
class ElectricityHandler(RegisterHandler):
"""电力消耗寄存器处理器"""
def __init__(self):
"""初始化处理器"""
self.dao = ElectricityDAO()
# 确保表已创建
self.dao.create_table_if_not_exists()
def handle_change(self, value):
"""处理寄存器值变化
Args:
value: 寄存器值
"""
try:
# 保存电力消耗数据
self.dao.save_electricity_data(value)
logging.info(f"已记录电力消耗数据: {value}")
except Exception as e:
logging.error(f"处理电力消耗数据时发生错误: {str(e)}")
class ElectricityMonitor:
"""电力消耗监控器"""
_instance = None
@classmethod
def get_instance(cls):
"""获取单例实例"""
if cls._instance is None:
cls._instance = ElectricityMonitor()
return cls._instance
def __init__(self):
"""初始化电力监控器"""
if ElectricityMonitor._instance:
raise Exception("ElectricityMonitor is a singleton class.")
self.modbus = ModbusUtils()
self.dao = ElectricityDAO()
self.config = ConfigLoader.get_instance()
# 确保表已创建
self.dao.create_table_if_not_exists()
# 创建定时器
self.timer = None
self.client = None
self.stop_event = Event()
self.monitor_thread = None
# 电力寄存器地址
self.electricity_register = 30
# 从配置中读取监听间隔(分钟)
self.interval_minutes = self.config.get_value('electricity.interval_minutes', 1)
# 从配置中读取是否自动启动
self.auto_start = self.config.get_value('electricity.auto_start', True)
# 上次读取时间
self.last_read_time = None
# 如果配置为自动启动,则启动监控
if self.auto_start:
self.start()
def start(self):
"""启动监控"""
if self.timer and self.timer.isActive():
logging.warning("电力监控器已经在运行中")
return
# 创建并启动定时器,每分钟读取一次
self.timer = QTimer()
self.timer.timeout.connect(self._read_electricity_data)
self.timer.start(self.interval_minutes * 60000) # 转换为毫秒
# 立即执行一次读取
self._read_electricity_data()
# 更新配置
self.config.set_value('electricity.auto_start', True)
logging.info(f"电力监控器已启动,每{self.interval_minutes}分钟读取一次寄存器30的数据")
def stop(self):
"""停止监控"""
if self.timer:
self.timer.stop()
if self.client:
self.modbus.close_client(self.client)
self.client = None
# 更新配置
self.config.set_value('electricity.auto_start', False)
logging.info("电力监控器已停止")
def set_interval(self, minutes):
"""设置监听间隔
Args:
minutes: 间隔分钟数
"""
if minutes < 1:
minutes = 1
self.interval_minutes = minutes
# 更新配置
self.config.set_value('electricity.interval_minutes', minutes)
# 如果定时器正在运行,则更新间隔
if self.timer and self.timer.isActive():
self.timer.setInterval(minutes * 60000) # 转换为毫秒
logging.info(f"电力监控间隔已更新为{minutes}分钟")
def is_monitoring(self):
"""检查是否正在监控
Returns:
bool: 是否正在监控
"""
return self.timer is not None and self.timer.isActive()
def get_last_read_time(self):
"""获取上次读取时间
Returns:
datetime: 上次读取时间如果未读取过则返回None
"""
return self.last_read_time
def get_next_read_time(self):
"""获取下次读取时间
Returns:
datetime: 下次读取时间如果未在监控则返回None
"""
if not self.is_monitoring() or not self.last_read_time:
return None
from datetime import datetime, timedelta
return self.last_read_time + timedelta(minutes=self.interval_minutes)
def _read_electricity_data(self):
"""读取电力消耗数据"""
try:
# 如果客户端未连接,则创建连接
if not self.client:
self.client = self.modbus.get_client()
if not self.client:
logging.error("无法连接到Modbus服务器电力数据读取失败")
return
# 读取寄存器30的值
result = self.modbus.read_holding_register(self.client, self.electricity_register)
if result is None or len(result) == 0:
logging.warning(f"读取寄存器D{self.electricity_register}失败")
return
# 获取电力消耗值
electricity_value = result[0]
# 保存到数据库
success = self.dao.save_electricity_data(electricity_value)
if success:
logging.info(f"已记录电力消耗数据: {electricity_value}")
# 更新上次读取时间
from datetime import datetime
self.last_read_time = datetime.now()
else:
logging.error("保存电力消耗数据失败")
except Exception as e:
logging.error(f"读取或保存电力消耗数据时发生错误: {str(e)}")
# 关闭连接,下次重新尝试
if self.client:
self.modbus.close_client(self.client)
self.client = None

View File

@ -35,6 +35,17 @@ def init_database():
"""
db.execute_query(create_user_table_sql)
# 创建电力消耗表
create_electricity_table_sql = """
CREATE TABLE IF NOT EXISTS wsbz_electricity_consumption (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sync_time TIMESTAMP,
electricity_number REAL
);
"""
db.execute_query(create_electricity_table_sql)
logging.info("已创建电力消耗表")
# 获取当前时间
current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')

View File

@ -53,9 +53,9 @@ class ModbusMonitor(QObject):
初始化Modbus监控器
Args:
polling_interval: 轮询间隔单位秒
polling_interval: 轮询间隔默认1.0比原来的1.0秒增加了一倍
max_errors: 最大错误次数超过此次数将暂停特定寄存器的监控
retry_interval: 重试间隔单位秒
retry_interval: 重试间隔
"""
super().__init__()
self.polling_interval = polling_interval
@ -77,7 +77,7 @@ class ModbusMonitor(QObject):
def _initialize_registers(self):
"""初始化要监控的寄存器列表"""
# 默认监控的寄存器地址
register_addresses = [5, 6, 11, 13, 20, 21, 22, 23, 24]
register_addresses = [0, 4, 5, 6, 11, 13, 20, 21, 22, 23, 24, 25, 30]
for address in register_addresses:
self.registers[address] = RegisterValue(address)
@ -175,50 +175,132 @@ class ModbusMonitor(QObject):
def _read_registers(self):
"""读取所有注册的寄存器"""
for address, reg_value in self.registers.items():
# 批量处理寄存器,减少连接次数
grouped_registers = self._group_registers()
for group, addresses in grouped_registers.items():
if self.stop_event.is_set():
break
# 如果错误次数超过阈值且未到重试时间,则跳过此寄存器
if reg_value.error_count >= self.max_errors:
current_time = time.time()
last_read = reg_value.last_read_time or 0
if current_time - last_read < self.retry_interval:
continue
# 检查组内是否有需要读取的寄存器
has_valid_registers = False
for address in addresses:
reg_value = self.registers[address]
# 如果错误次数超过阈值且未到重试时间,则跳过
if reg_value.error_count >= self.max_errors:
current_time = time.time()
last_read = reg_value.last_read_time or 0
if current_time - last_read < self.retry_interval:
continue
has_valid_registers = True
break
if not has_valid_registers:
continue
try:
# 读取寄存器值
result = self.modbus.read_holding_register(self.client, address)
if result is None or len(result) == 0:
error_count = reg_value.record_error()
error_msg = f"读取寄存器D{address}失败,这是第{error_count}次失败"
logging.warning(error_msg)
self.register_error.emit(address, error_msg)
# 如果连续失败次数达到阈值,尝试重连
if error_count >= self.max_errors:
logging.error(f"寄存器D{address}连续{error_count}次读取失败,将在{self.retry_interval}秒后重试")
# 下次将在retry_interval后尝试读取此寄存器
continue
# 为每组创建一个重试计数
retry_count = 0
max_retries = 2
delay = 0.5
# 成功读取,重置错误计数
reg_value.reset_error()
# 更新值并检查是否发生变化
if reg_value.update(result[0]):
logging.info(f"寄存器D{address}值变化: {reg_value.last_value} -> {reg_value.value}")
# 发出信号
self.register_changed.emit(address, reg_value.value)
# 调用注册的处理器
self._notify_handlers(address, reg_value.value)
while retry_count <= max_retries:
try:
# 批量读取该组寄存器
for address in addresses:
reg_value = self.registers[address]
# 如果错误次数超过阈值且未到重试时间,则跳过
if reg_value.error_count >= self.max_errors:
current_time = time.time()
last_read = reg_value.last_read_time or 0
if current_time - last_read < self.retry_interval:
continue
try:
# 读取寄存器值
result = self.modbus.read_holding_register(self.client, address)
if result is None or len(result) == 0:
error_count = reg_value.record_error()
error_msg = f"读取寄存器D{address}失败,这是第{error_count}次失败"
logging.warning(error_msg)
self.register_error.emit(address, error_msg)
# 如果连续失败次数达到阈值,尝试重连
if error_count >= self.max_errors:
logging.error(f"寄存器D{address}连续{error_count}次读取失败,将在{self.retry_interval}秒后重试")
continue
# 成功读取,重置错误计数
reg_value.reset_error()
# 更新值并检查是否发生变化
if reg_value.update(result[0]):
logging.info(f"寄存器D{address}值变化: {reg_value.last_value} -> {reg_value.value}")
# 发出信号
self.register_changed.emit(address, reg_value.value)
# 调用注册的处理器
self._notify_handlers(address, reg_value.value)
except Exception as e:
error_count = reg_value.record_error()
error_msg = f"读取寄存器D{address}时发生异常: {str(e)}"
logging.error(error_msg)
self.register_error.emit(address, error_msg)
if error_count >= self.max_errors:
logging.error(f"寄存器D{address}连续{error_count}次读取异常,将在{self.retry_interval}秒后重试")
# 如果是连接错误,尝试重新连接并退出循环
if "Connection" in str(e):
retry_count += 1
if retry_count <= max_retries:
# 释放连接,重新获取
if self.client:
self.modbus.close_client(self.client)
self.client = None
delay_time = delay * (2 ** (retry_count - 1))
logging.warning(f"连接错误,等待 {delay_time:.1f} 秒后重试 ({retry_count}/{max_retries})...")
time.sleep(delay_time)
# 重新连接
self._reconnect()
break
# 如果已成功读取所有寄存器,跳出重试循环
break
except Exception as e:
retry_count += 1
if retry_count <= max_retries:
delay_time = delay * (2 ** (retry_count - 1))
logging.warning(f"批量读取寄存器时发生错误,等待 {delay_time:.1f} 秒后重试 ({retry_count}/{max_retries}): {str(e)}")
time.sleep(delay_time)
# 尝试重新连接
if self.client:
self.modbus.close_client(self.client)
self.client = None
self._reconnect()
else:
logging.error(f"批量读取寄存器失败,已达到最大重试次数: {str(e)}")
break
except Exception as e:
error_count = reg_value.record_error()
error_msg = f"读取寄存器D{address}时发生异常: {str(e)}"
logging.error(error_msg)
self.register_error.emit(address, error_msg)
if error_count >= self.max_errors:
logging.error(f"寄存器D{address}连续{error_count}次读取异常,将在{self.retry_interval}秒后重试")
logging.error(f"处理寄存器组时发生异常: {str(e)}", exc_info=True)
def _group_registers(self):
"""将寄存器分组,以便批量处理
Returns:
dict: 分组后的寄存器格式为 {group_id: [address1, address2, ...]}
"""
# 简单分组每5个寄存器为一组
group_size = 5
grouped = {}
addresses = list(self.registers.keys())
for i in range(0, len(addresses), group_size):
group_id = i // group_size
grouped[group_id] = addresses[i:i+group_size]
return grouped
def _notify_handlers(self, address, value):
"""通知所有注册的处理器"""

View File

@ -3,43 +3,144 @@ from pymodbus.client import ModbusTcpClient
import time
import logging
from .config_loader import ConfigLoader
import threading
# 配置 Flask 日志级别
log = logging.getLogger('werkzeug')
log.setLevel(logging.WARNING)
# 添加连接池管理类
class ModbusConnectionPool:
_instance = None
_lock = threading.Lock()
@classmethod
def get_instance(cls):
"""获取单例实例"""
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = ModbusConnectionPool()
return cls._instance
def __init__(self):
"""初始化连接池"""
self._connections = {} # 保存每个目标地址的连接
self._locks = {} # 每个连接的锁,避免并发问题
self._in_use = {} # 标记连接是否在使用中
self._last_used = {} # 记录连接最后使用时间
# 启动清理线程
self._cleanup_thread = threading.Thread(target=self._cleanup_idle_connections, daemon=True)
self._cleanup_thread.start()
def get_connection(self, host, port):
"""获取指定地址的连接,如果不存在则创建"""
key = f"{host}:{port}"
# 如果不存在该地址的锁,创建一个
if key not in self._locks:
with self._lock:
if key not in self._locks:
self._locks[key] = threading.Lock()
# 获取该地址的锁
with self._locks[key]:
# 如果连接不存在或已断开,创建新连接
if key not in self._connections or not self._connections[key].is_socket_open():
try:
logging.info(f"Creating new Modbus connection to {host}:{port}")
client = ModbusTcpClient(host, port=port, timeout=10)
connected = client.connect()
if not connected:
logging.error(f"Failed to connect to {host}:{port}")
return None
self._connections[key] = client
self._in_use[key] = True
self._last_used[key] = time.time()
return client
except Exception as e:
logging.error(f"Error creating Modbus connection to {host}:{port}: {e}")
return None
else:
# 连接存在,标记为使用中并更新时间
self._in_use[key] = True
self._last_used[key] = time.time()
return self._connections[key]
def release_connection(self, host, port):
"""释放连接,将其标记为不再使用"""
key = f"{host}:{port}"
with self._locks.get(key, self._lock):
if key in self._in_use:
self._in_use[key] = False
self._last_used[key] = time.time()
def _cleanup_idle_connections(self):
"""清理空闲连接的后台线程"""
idle_timeout = 300 # 5分钟空闲超时
while True:
time.sleep(60) # 每分钟检查一次
current_time = time.time()
keys_to_check = list(self._connections.keys())
for key in keys_to_check:
try:
with self._locks.get(key, self._lock):
# 如果连接空闲超过5分钟关闭并移除
if (key in self._in_use and not self._in_use[key] and
current_time - self._last_used.get(key, 0) > idle_timeout):
logging.info(f"Closing idle connection: {key}")
try:
if self._connections[key].is_socket_open():
self._connections[key].close()
except:
pass # 忽略关闭连接时的错误
# 从所有字典中移除
self._connections.pop(key, None)
self._in_use.pop(key, None)
self._last_used.pop(key, None)
except Exception as e:
logging.error(f"Error during connection cleanup for {key}: {e}")
# Modbus TCP 配置
class ModbusUtils:
def __init__(self) -> None:
def __init__(self, host=None, port=None) -> None:
# 初始化 modbus 配置
config = ConfigLoader.get_instance()
self.MODBUS_HOST = config.get_value("modbus.host")
self.MODBUS_PORT = config.get_value("modbus.port")
self.MODBUS_HOST = host if host is not None else config.get_value("modbus.host")
self.MODBUS_PORT = port if port is not None else int(config.get_value("modbus.port"))
self.connection_pool = ModbusConnectionPool.get_instance()
self.retry_max = 3 # 最大重试次数
self.retry_delay = 0.5 # 初始重试延迟(秒)
def get_client(self):
# 创建Modbus TCP客户端实例指定服务器的IP地址和端口号
# client = ModbusTcpClient('localhost', port=5020)
client = ModbusTcpClient(self.MODBUS_HOST, port=self.MODBUS_PORT, timeout=10) # 增加超时时间
logging.info(f"Attempting to connect to Modbus server {self.MODBUS_HOST}:{self.MODBUS_PORT}")
try:
is_connected = client.connect() #确保客户端已连接
if is_connected:
logging.info(f"Successfully connected to Modbus server {self.MODBUS_HOST}:{self.MODBUS_PORT}")
else:
logging.error(f"Failed to connect to Modbus server {self.MODBUS_HOST}:{self.MODBUS_PORT}. client.connect() returned False.")
except Exception as e:
logging.error(f"Exception during connection to Modbus server {self.MODBUS_HOST}:{self.MODBUS_PORT}: {e}")
# Optionally, re-raise or handle as appropriate
return None # Or raise an exception
return client
"""获取Modbus客户端连接使用连接池"""
# 使用指数退避重试获取连接
retry_count = 0
delay = self.retry_delay
while retry_count < self.retry_max:
client = self.connection_pool.get_connection(self.MODBUS_HOST, self.MODBUS_PORT)
if client and client.is_socket_open():
return client
# 连接失败,等待后重试
logging.warning(f"Connection attempt {retry_count+1} failed, retrying in {delay:.1f}s...")
time.sleep(delay)
delay *= 2 # 指数退避
retry_count += 1
logging.error(f"Failed to connect to Modbus server {self.MODBUS_HOST}:{self.MODBUS_PORT} after {self.retry_max} attempts")
return None
def close_client(self, client):
# 关闭客户端连接
"""释放客户端连接回连接池,不再实际关闭连接"""
if client:
client.close()
self.connection_pool.release_connection(self.MODBUS_HOST, self.MODBUS_PORT)
# 新增十进制转成二进制
@staticmethod

View File

@ -248,4 +248,79 @@ class PalletTypeManager:
Returns:
bool: 是否保存成功
"""
return self.dao.save_pallet_archives(pallet_code, tier, user_id, user_name)
return self.dao.save_pallet_archives(pallet_code, tier, user_id, user_name)
def reload_pallet_types(self):
"""重新加载托盘类型数据"""
try:
self.pallet_types = self.dao.get_all_pallet_types_list()
logging.info(f"已重新加载托盘类型数据,共 {len(self.pallet_types)}")
return True
except Exception as e:
logging.error(f"重新加载托盘类型数据失败: {str(e)}")
return False
def get_all_pallet_types(self):
"""获取所有托盘类型
Returns:
list: 托盘类型列表
"""
if not self.pallet_types:
self.reload_pallet_types()
return self.pallet_types
def add_pallet_type(self, pallet_id, pallet_name):
"""添加托盘类型
Args:
pallet_id: 托盘ID
pallet_name: 托盘名称
Returns:
bool: 是否添加成功
"""
result = self.dao.add_pallet_type(pallet_id, pallet_name)
if result:
self.reload_pallet_types()
return result
def update_pallet_type(self, pallet_id, pallet_name):
"""更新托盘类型
Args:
pallet_id: 托盘ID
pallet_name: 托盘名称
Returns:
bool: 是否更新成功
"""
result = self.dao.update_pallet_type(pallet_id, pallet_name)
if result:
self.reload_pallet_types()
return result
def delete_pallet_type_by_id(self, pallet_id):
"""删除托盘类型
Args:
pallet_id: 托盘ID
Returns:
bool: 是否删除成功
"""
result = self.dao.delete_pallet_type(pallet_id)
if result:
self.reload_pallet_types()
return result
def get_pallet_type_by_id_new(self, pallet_id):
"""根据ID获取托盘类型新版本
Args:
pallet_id: 托盘ID
Returns:
dict: 托盘类型信息未找到则返回None
"""
return self.dao.get_pallet_type_by_id(pallet_id)

View File

@ -173,7 +173,6 @@ class Error2Handler(RegisterHandler):
self.callback = callback
self.error_map = {
0: "无报警",
1: "急停报警",
2: "贴标故障"
}
def handle_change(self, value):
@ -243,4 +242,63 @@ class NGHandler(RegisterHandler):
#如果有回调函数,则调用
if self.callback:
self.callback(value)
self.callback(value)
class EmergencyStopHandler:
"""处理急停信号"""
def __init__(self, callback=None):
"""初始化处理器
Args:
callback: 回调函数接收两个参数value(int), desc(str)
"""
self.callback = callback
self.prev_value = 0
# 急停状态描述映射
self.status_map = {
0: "正常",
1: "急停触发"
}
def handle(self, value):
"""处理急停信号
Args:
value: 寄存器值
Returns:
tuple: (是否变化, 状态描述)
"""
# 检查值是否有效
if value not in [0, 1]:
return False, f"无效的急停状态值: {value}"
# 检查值是否变化
if value == self.prev_value:
return False, self.status_map.get(value, "未知状态")
# 更新上一次的值
self.prev_value = value
# 获取状态描述
desc = self.status_map.get(value, "未知状态")
# 调用回调函数
if self.callback:
self.callback(value, desc)
return True, desc
def handle_change(self, value):
"""实现 handle_change 方法以兼容 ModbusMonitor._notify_handlers
Args:
value: 寄存器值
"""
changed, desc = self.handle(value)
if changed:
logging.info(f"急停状态变化: {desc} (值={value})")
return changed, desc

View File

@ -32,6 +32,7 @@ class SerialManager:
self.read_threads: Dict[str, threading.Thread] = {} # 存储读取线程
self.running_flags: Dict[str, bool] = {} # 存储线程运行标志
self.callbacks: Dict[str, Callable] = {} # 存储数据回调函数
self.port_types: Dict[str, str] = {} # 存储端口类型,用于线程重启
# 添加文件操作暂停控制
self._file_operations_suspended = False
@ -54,7 +55,8 @@ class SerialManager:
self.data = {
'mdz': 0,
'xj': 0, # 添加线径数据
'cz': 0
'cz': 0,
'scanner': '' # 添加扫码器数据
}
# 是否自动查询米电阻数据默认为False只通过PageUp键触发
@ -84,6 +86,9 @@ class SerialManager:
else:
logging.info("键盘监听功能已在配置中禁用,跳过初始化键盘监听器")
self.keyboard_listener = None
# 启动线程监控
self._start_thread_monitor()
def _load_config(self):
"""加载配置"""
@ -102,6 +107,7 @@ class SerialManager:
self.mdz_config = self.config.get_config('mdz')
self.cz_config = self.config.get_config('cz')
self.xj_config = self.config.get_config('xj') # 添加线径配置
self.scanner_config = self.config.get_config('scanner') # 添加扫码器配置
# 检查操作系统类型在macOS上处理COM端口名称问题
os_type = platform.system()
@ -118,7 +124,7 @@ class SerialManager:
# 检查是否自动查询米电阻数据
self.auto_query_mdz = self.config.get_value('serial.keyboard.auto_query', False)
logging.info(f"已加载串口配置mdz={self.mdz_config}, xj={self.xj_config}, cz={self.cz_config}, data_file={self.data_file}")
logging.info(f"已加载串口配置mdz={self.mdz_config}, xj={self.xj_config}, cz={self.cz_config}, scanner={self.scanner_config}, data_file={self.data_file}")
logging.info(f"米电阻自动查询: {'开启' if self.auto_query_mdz else '关闭'}")
except Exception as e:
logging.error(f"加载配置出错: {e}")
@ -126,6 +132,8 @@ class SerialManager:
self.data_file = os.path.abspath('data.txt')
self.mdz_config = {'port': 9600, 'ser': 'COM5'}
self.cz_config = {'port': 9600, 'ser': 'COM2', 'stable_threshold': 10}
self.xj_config = {'port': 9600, 'ser': 'COM3'}
self.scanner_config = {'port': 9600, 'ser': 'COM4'}
self.stable_threshold = 10
self.auto_query_mdz = False
logging.info(f"使用默认配置,数据文件: {self.data_file}")
@ -194,88 +202,135 @@ class SerialManager:
Args:
port_name: 串口名称如COM1
port_type: 串口类型'cz'表示称重'mdz'表示米电阻, 'xj'表示线径
port_type: 串口类型'cz'表示称重'mdz'表示米电阻, 'xj'表示线径, 'scanner'表示扫码器
baud_rate: 波特率如果为None则从配置文件读取
data_bits: 数据位
stop_bits: 停止位
parity: 校验位N-无校验E-偶校验O-校验
parity: 校验位'N'表示无校验'O'表示奇校验'E'表示偶校验
timeout: 超时时间单位秒
callback: 数据回调函数接收参数为(port_name, data)
callback: 回调函数接收(port_name, data)作为参数
Returns:
是否成功打开
bool: 成功返回True失败返回False
"""
# 如果串口已经打开,先关闭
if port_name in self.serial_ports and self.serial_ports[port_name]:
try:
self.close_port(port_name)
except Exception as e:
logging.error(f"关闭已打开的串口失败: {e}")
# 配置串口参数
try:
# 如果波特率为None从配置文件读取
# 从配置读取波特率(如果未提供)
if baud_rate is None:
if port_type == 'cz' and self.cz_config:
baud_rate = self.cz_config.get('port', 9600)
elif port_type == 'mdz' and self.mdz_config:
baud_rate = self.mdz_config.get('port', 9600)
elif port_type == 'xj' and self.xj_config: # 添加线径配置
elif port_type == 'xj' and self.xj_config:
baud_rate = self.xj_config.get('port', 9600)
elif port_type == 'scanner' and self.scanner_config:
baud_rate = self.scanner_config.get('port', 9600)
else:
baud_rate = 9600 # 默认波特率
# 如果串口已经打开,先关闭
if port_name in self.serial_ports:
self.close_port(port_name)
# 转换校验位为PySerial常量
if parity.upper() == 'N':
parity_constant = serial.PARITY_NONE
elif parity.upper() == 'O':
parity_constant = serial.PARITY_ODD
elif parity.upper() == 'E':
parity_constant = serial.PARITY_EVEN
else:
parity_constant = serial.PARITY_NONE
# 打开串口
ser = serial.Serial(
self.serial_ports[port_name] = serial.Serial(
port=port_name,
baudrate=baud_rate,
bytesize=data_bits,
stopbits=stop_bits,
parity=parity,
parity=parity_constant,
timeout=timeout
)
if not ser.is_open:
ser.open()
# 存储串口对象
self.serial_ports[port_name] = ser
logging.info(f"串行对象 for {port_name} 存储在 self.serial_ports 中. 当前活跃端口: {list(self.serial_ports.keys())}")
logging.info(f"打开串口成功: {port_name}, 类型: {port_type}, 波特率: {baud_rate}")
# 设置回调
if callback:
self.callbacks[port_name] = callback
# 启动读取线程
# 记录端口类型,用于线程重启
self.port_types[port_name] = port_type
# 创建并启动读取线程
self.running_flags[port_name] = True
# 根据串口类型选择不同的读取线程
# 统一线程创建和管理方式
if port_type == 'cz':
thread = threading.Thread(target=self._read_weight_thread, args=(port_name, self.stable_threshold))
thread.daemon = True
thread.start()
self.read_threads[port_name] = thread
# 称重数据需要特殊处理
thread = threading.Thread(
target=self._read_weight_thread,
args=(port_name, self.stable_threshold),
daemon=True,
name=f"Thread-{port_type}-{port_name}"
)
elif port_type == 'mdz':
thread = threading.Thread(target=self._read_resistance_thread, args=(port_name,))
thread.daemon = True
thread.start()
self.read_threads[port_name] = thread
elif port_type == 'xj': # 添加线径读取线程
thread = threading.Thread(target=self._read_diameter_thread, args=(port_name,))
thread.daemon = True
thread.start()
self.read_threads[port_name] = thread
# 米电阻数据需要特殊处理
thread = threading.Thread(
target=self._read_resistance_thread,
args=(port_name,),
daemon=True,
name=f"Thread-{port_type}-{port_name}"
)
elif port_type == 'xj':
# 线径数据需要特殊处理
thread = threading.Thread(
target=self._read_diameter_thread,
args=(port_name,),
daemon=True,
name=f"Thread-{port_type}-{port_name}"
)
elif port_type == 'scanner':
# 扫码器数据需要特殊处理
thread = threading.Thread(
target=self._read_scanner_thread,
args=(port_name,),
daemon=True,
name=f"Thread-{port_type}-{port_name}"
)
else:
# 默认读取线程
thread = threading.Thread(target=self._read_thread, args=(port_name,))
thread.daemon = True
thread.start()
self.read_threads[port_name] = thread
# 其他类型使用通用处理
thread = threading.Thread(
target=self._read_thread,
args=(port_name,),
daemon=True,
name=f"Thread-{port_type}-{port_name}"
)
# 统一启动线程
thread.start()
self.read_threads[port_name] = thread
logging.info(f"已启动串口读取线程: {thread.name}")
logging.info(f"串口 {port_name} ({port_type}) 已打开,波特率={baud_rate}")
return True
except Exception as e:
logging.error(f"打开串口 {port_name} 失败: {str(e)}")
if port_name in self.serial_ports: # 清理,以防部分成功
del self.serial_ports[port_name]
logging.info(f"打开 {port_name} 失败后, 当前活跃端口: {list(self.serial_ports.keys())}")
logging.error(f"打开串口失败: {port_name}, 错误: {e}")
# 确保清理好资源
if port_name in self.serial_ports:
try:
self.serial_ports[port_name].close()
except:
pass
self.serial_ports.pop(port_name, None)
# 停止相关线程
self.running_flags[port_name] = False
if port_name in self.read_threads:
self.read_threads.pop(port_name, None)
return False
def close_port(self, port_name: str) -> bool:
@ -306,10 +361,13 @@ class SerialManager:
del self.serial_ports[port_name]
logging.info(f"串行对象 for {port_name} 从 self.serial_ports 中删除. 当前活跃端口: {list(self.serial_ports.keys())}")
# 删除回调
# 删除回调和端口类型记录
if port_name in self.callbacks:
del self.callbacks[port_name]
if port_name in self.port_types:
del self.port_types[port_name]
logging.info(f"串口 {port_name} 已关闭")
return True
@ -351,6 +409,35 @@ class SerialManager:
logging.error(f"向串口 {port_name} 写入数据失败: {str(e)}")
return False
def read_data(self, port_name: str, size: int = None) -> bytes:
"""
从串口读取数据
Args:
port_name: 串口名称
size: 要读取的字节数如果为None则读取所有可用数据
Returns:
读取的数据如果失败则返回空字节
"""
try:
if not self.is_port_open(port_name):
logging.error(f"尝试从未打开的串口 {port_name} 读取数据")
return b''
if size is None:
# 读取所有可用数据
if self.serial_ports[port_name].in_waiting > 0:
return self.serial_ports[port_name].read(self.serial_ports[port_name].in_waiting)
return b''
else:
# 读取指定数量的字节
return self.serial_ports[port_name].read(size)
except Exception as e:
logging.error(f"从串口 {port_name} 读取数据失败: {str(e)}")
return b''
def _read_thread(self, port_name: str):
"""
串口读取线程
@ -381,16 +468,46 @@ class SerialManager:
logging.error(f"串口 {port_name} 读取线程异常: {str(e)}")
def _read_weight_thread(self, port_name: str, stable_threshold: int = 10):
logging.info(f"[{port_name}] 称重线程启动")
# 重置状态变量,确保线程重启时能正确处理称重数据
self.weight_written = False
self.stable_count = 0
self.last_weight = 0
self.last_weights = [0] * 3
self.weight_changed_time = time.time()
self.last_write_time = 0 # 添加一个变量来跟踪最后一次写入时间
self.stability_start_time = 0 # 重置稳定性检测开始时间
"""
称重串口读取线程
Args:
port_name: 串口名称
stable_threshold: 稳定阈值
"""
try:
logging.info(f"[{port_name}] 称重线程启动")
# 重置状态变量,确保线程重启时能正确处理称重数据
self.weight_written = False
self.stable_count = 0
self.last_weight = 0
self.last_weights = [0] * 3
self.weight_changed_time = time.time()
self.last_write_time = 0 # 添加一个变量来跟踪最后一次写入时间
self.stability_start_time = 0 # 重置稳定性检测开始时间
# 添加实际的读取逻辑
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:
data = self.serial_ports[port_name].read(self.serial_ports[port_name].in_waiting)
weight = self._process_weight_data(data)
if weight is not None:
# 更新数据
self.data['cz'] = weight
self._write_data_to_file()
time.sleep(0.1)
except Exception as e:
logging.error(f"称重串口 {port_name} 读取线程异常: {e}")
# 线程异常时,尝试重置状态
self.running_flags[port_name] = False
def _process_weight_data(self, data_bytes):
"""
TODO: 需要将线径数据写入文件这个方法需要修改成线径的串口数据获取
@ -443,6 +560,7 @@ class SerialManager:
port_name: 串口名称
"""
try:
logging.info(f"[{port_name}] 米电阻线程启动")
while self.running_flags.get(port_name, False):
if not self.is_port_open(port_name):
time.sleep(0.1)
@ -483,7 +601,9 @@ class SerialManager:
except Exception as e:
logging.error(f"米电阻串口 {port_name} 读取线程异常: {e}")
# 线程异常时,尝试重置状态
self.running_flags[port_name] = False
def _process_mdz_response(self, port_name, response_bytes: bytes):
"""处理米电阻响应数据"""
try:
@ -534,7 +654,7 @@ class SerialManager:
return
# 构建数据字符串
data_str = f"mdz:{self.data['mdz']}|cz:{self.data['cz']}|"
data_str = f"mdz:{self.data['mdz']}|cz:{self.data['cz']}|scanner:{self.data['scanner']}|"
# 确保目录存在
data_dir = os.path.dirname(self.data_file)
@ -792,7 +912,7 @@ class SerialManager:
"""通知所有相关回调函数"""
try:
# 端口特定回调 (通常用于原始串口数据)
if port_name in self.callbacks and port_name not in ['mdz_data', 'xj_data']: # 避免重复处理
if port_name in self.callbacks and port_name not in ['mdz_data', 'xj_data', 'scanner_data']: # 避免重复处理
try:
# 假设这种回调期望原始的 value (可能是字节串,也可能是其他类型)
self.callbacks[port_name](port_name, value)
@ -874,6 +994,37 @@ class SerialManager:
else:
logging.warning(f"回调失败: xj_data 中实际值为None. 初始 value: {value}")
# 全局回调, 特别处理 'scanner_data'
if 'scanner_data' in self.callbacks and port_name == 'scanner_data':
actual_scanner_value = None
source_info = "unknown"
if isinstance(value, dict):
actual_scanner_value = value.get('value')
source_info = value.get('source', source_info)
elif isinstance(value, (str, bytes)):
if isinstance(value, bytes):
try:
actual_scanner_value = value.decode('utf-8').strip()
except:
actual_scanner_value = str(value)
else:
actual_scanner_value = value
if actual_scanner_value is not None:
callback_data_str = f"扫码数据: {actual_scanner_value}"
try:
triggering_port = port_name if port_name not in ['scanner_data', 'scanner'] else self.scanner_config.get('ser', 'N/A') if self.scanner_config else 'N/A'
if source_info.startswith("mock"):
triggering_port = f"mock_{port_name}"
self.callbacks['scanner_data'](triggering_port, callback_data_str.encode('utf-8'))
logging.info(f"通知 'scanner_data' 回调. 值: {actual_scanner_value}, 源: {source_info}, 触发源端口: {triggering_port}")
except Exception as e:
logging.error(f"调用全局回调 'scanner_data' 失败: {e}", exc_info=True)
else:
logging.warning(f"回调失败: scanner_data 中实际值为None. 初始 value: {value}")
except Exception as e:
logging.error(f"通知回调失败: {e}", exc_info=True)
@ -893,14 +1044,15 @@ class SerialManager:
os_type = platform.system()
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'))
(self.cz_config and 'ser' in self.cz_config and self.cz_config['ser'] and self.cz_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']:
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)
@ -917,10 +1069,10 @@ class SerialManager:
else:
logging.info(f"线径串口 {port_name} 已经打开,无需重新打开")
else:
logging.warning("线径串口未配置,跳过自动打开")
logging.warning("线径串口未配置或设置为不使用,跳过自动打开")
# 尝试打开米电阻串口
if self.mdz_config and 'ser' in self.mdz_config and self.mdz_config['ser']:
if self.mdz_config and 'ser' in self.mdz_config and self.mdz_config['ser'] and self.mdz_config['ser'].strip():
port_name = self.mdz_config['ser']
baud_rate = self.mdz_config.get('port', 9600)
@ -937,7 +1089,27 @@ class SerialManager:
else:
logging.info(f"米电阻串口 {port_name} 已经打开,无需重新打开")
else:
logging.warning("米电阻串口未配置,跳过自动打开")
logging.warning("米电阻串口未配置或设置为不使用,跳过自动打开")
# 尝试打开扫码器串口
if self.scanner_config and 'ser' in self.scanner_config and self.scanner_config['ser'] and self.scanner_config['ser'].strip():
port_name = self.scanner_config['ser']
baud_rate = self.scanner_config.get('port', 9600)
if not self.is_port_open(port_name):
try:
if self.open_port(port_name, 'scanner', 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("扫码器串口未配置或设置为不使用,跳过自动打开")
# 注意不在这里启动键盘监听器而是在MainWindow的handle_start方法中显式调用start_keyboard_listener
@ -970,6 +1142,7 @@ class SerialManager:
port_name: 串口名称
"""
try:
logging.info(f"[{port_name}] 线径线程启动")
while self.running_flags.get(port_name, False):
if not self.is_port_open(port_name):
time.sleep(0.1)
@ -984,6 +1157,8 @@ class SerialManager:
except Exception as e:
logging.error(f"线径串口 {port_name} 读取线程异常: {e}")
# 线程异常时,尝试重置状态
self.running_flags[port_name] = False
def _process_diameter_response(self, port_name, response_bytes: bytes):
"""处理线径响应数据"""
@ -1022,4 +1197,132 @@ class SerialManager:
return False
except Exception as e:
logging.error(f"处理线径数据总体异常: {e}")
return False
return False
def _read_scanner_thread(self, port_name: str):
"""
扫码器串口读取线程
Args:
port_name: 串口名称
"""
try:
logging.info(f"[{port_name}] 扫码器线程启动")
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_scanner_response(port_name, response)
time.sleep(0.1)
except Exception as e:
logging.error(f"扫码器串口 {port_name} 读取线程异常: {e}")
# 线程异常时,尝试重置状态
self.running_flags[port_name] = False
def _process_scanner_response(self, port_name, response_bytes: bytes):
"""处理扫码器响应数据"""
try:
if response_bytes: # 确保有响应数据
try:
# 尝试解码为字符串
scanner_value = response_bytes.decode('utf-8').strip()
# 记录日志
logging.info(f"[{port_name}] 扫码数据: {scanner_value}")
# 更新数据
self.data['scanner'] = scanner_value
# 写入文件并通知回调
self._write_data_to_file()
# 使用"扫码数据: xxx"格式通知回调
callback_data = f"扫码数据: {scanner_value}".encode('utf-8')
if 'scanner_data' in self.callbacks:
self.callbacks['scanner_data'](port_name, callback_data)
return True
except Exception as e:
logging.error(f"处理扫码数据异常: {e}")
# 解码失败,尝试直接使用字节数据
# 记录日志(十六进制字符串)
hex_str = ' '.join(f'{b:02X}' for b in response_bytes)
logging.warning(f"[{port_name}] 扫码数据(十六进制): {hex_str}")
# 更新数据(使用十六进制字符串)
self.data['scanner'] = hex_str
# 写入文件并通知回调
self._write_data_to_file()
# 使用"扫码数据: xxx"格式通知回调
callback_data = f"扫码数据: {hex_str}".encode('utf-8')
if 'scanner_data' in self.callbacks:
self.callbacks['scanner_data'](port_name, callback_data)
return True
else:
logging.warning("扫码响应数据为空")
# 如果无法解析,则直接返回失败
return False
except Exception as e:
logging.error(f"处理扫码数据总体异常: {e}")
return False
def _start_thread_monitor(self):
"""启动线程监控"""
threading.Thread(target=self._monitor_threads, daemon=True).start()
def _monitor_threads(self):
"""监控线程状态"""
try:
logging.info("线程监控已启动")
while True:
try:
# 创建当前线程的副本,避免在迭代过程中修改字典
thread_items = list(self.read_threads.items())
for port_name, thread in thread_items:
# 检查线程是否存活
if not thread.is_alive():
# 检查串口是否仍然打开
if port_name in self.serial_ports and self.is_port_open(port_name):
port_type = self.port_types.get(port_name)
callback = self.callbacks.get(port_name)
logging.warning(f"线程 {thread.name} 已终止但串口仍然打开,尝试重新启动线程")
# 重置线程状态
self.running_flags[port_name] = False
if port_name in self.read_threads:
self.read_threads.pop(port_name, None)
# 如果有端口类型记录,尝试重新启动线程
if port_type:
# 短暂等待,确保资源释放
time.sleep(0.5)
try:
# 重新打开串口
self.open_port(port_name, port_type, callback=callback)
logging.info(f"已重新启动线程: {port_name} ({port_type})")
except Exception as restart_error:
logging.error(f"重新启动线程失败: {port_name}, 错误: {restart_error}")
else:
logging.warning(f"无法重启线程: {port_name}, 未找到端口类型记录")
else:
# 串口已关闭,清理线程记录
if port_name in self.read_threads:
self.read_threads.pop(port_name, None)
logging.info(f"线程 {thread.name} 已终止,串口已关闭,清理线程记录")
# 每隔5秒检查一次
time.sleep(5)
except Exception as loop_error:
logging.error(f"线程监控循环异常: {loop_error}")
time.sleep(5) # 出错后等待一段时间再继续
except Exception as e:
logging.error(f"线程监控主循环异常: {e}")
# 尝试重新启动监控
time.sleep(10)
self._start_thread_monitor()

View File

@ -1,5 +1,7 @@
import sys
import logging
import threading
import time
from utils.config_loader import ConfigLoader
try:
@ -19,8 +21,73 @@ except ImportError:
class SQLUtils:
# 存储连接池,避免重复创建连接
# 存储连接池,使用线程ID作为键的一部分
_connection_pool = {}
# 连接引用计数
_connection_refs = {}
# 最后使用时间记录
_last_used = {}
# 轻量级锁,仅用于连接池访问
_pool_lock = threading.RLock()
# 空闲连接超时时间(秒)
_idle_timeout = 300 # 5分钟
# 初始化清理线程标志
_cleanup_thread_started = False
@classmethod
def _start_cleanup_thread(cls):
"""启动清理空闲连接的后台线程"""
if cls._cleanup_thread_started:
return
def cleanup_idle_connections():
"""定期清理空闲连接的线程函数"""
logging.info("数据库连接清理线程已启动")
while True:
time.sleep(60) # 每分钟检查一次
try:
current_time = time.time()
with cls._pool_lock:
# 复制键列表,避免在迭代过程中修改字典
conn_keys = list(cls._connection_pool.keys())
for conn_key in conn_keys:
# 检查引用计数和最后使用时间
if (conn_key in cls._connection_refs and
cls._connection_refs[conn_key] <= 0 and
conn_key in cls._last_used and
current_time - cls._last_used[conn_key] > cls._idle_timeout):
try:
# 获取连接和游标
conn, cursor = cls._connection_pool[conn_key]
# 关闭资源
if cursor:
cursor.close()
if conn:
conn.close()
# 从所有集合中移除
cls._connection_pool.pop(conn_key, None)
cls._connection_refs.pop(conn_key, None)
cls._last_used.pop(conn_key, None)
logging.debug(f"已清理空闲连接: {conn_key}")
except Exception as e:
logging.error(f"清理空闲连接时出错: {e}")
except Exception as e:
logging.error(f"连接清理线程执行异常: {e}")
# 创建并启动后台线程
cleanup_thread = threading.Thread(
target=cleanup_idle_connections,
daemon=True,
name="DB-Connection-Cleanup"
)
cleanup_thread.start()
cls._cleanup_thread_started = True
def __init__(self, db_type=None, source_name=None, **kwargs):
"""初始化SQLUtils对象
@ -30,6 +97,10 @@ class SQLUtils:
source_name: 数据源名称用于从配置中获取特定的数据源'sqlite', 'postgresql', 'mysql'
**kwargs: 连接参数如果没有提供则使用配置文件中的参数
"""
# 确保清理线程已启动
if not SQLUtils._cleanup_thread_started:
SQLUtils._start_cleanup_thread()
self.conn = None
self.cursor = None
@ -83,35 +154,76 @@ class SQLUtils:
# 尝试从连接池获取连接,如果没有则创建新连接
self._get_connection()
def __enter__(self):
"""上下文管理器入口方法支持with语句"""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""上下文管理器退出方法,自动关闭游标"""
self.close()
return False # 不抑制异常
def _get_connection(self):
"""从连接池获取连接,如果没有则创建新连接"""
# 创建连接键,包含数据库类型和连接参数
conn_key = f"{self.db_type}:{str(self.kwargs)}"
"""从连接池获取连接基于线程ID"""
# 使用线程ID作为连接键的一部分
thread_id = threading.get_ident()
conn_key = f"{self.db_type}:{str(self.kwargs)}:{thread_id}"
# 检查连接池中是否已有此连接
if conn_key in SQLUtils._connection_pool:
try:
# 尝试执行简单查询,确认连接有效
conn, cursor = SQLUtils._connection_pool[conn_key]
cursor.execute("SELECT 1")
# 连接有效,直接使用
self.conn = conn
self.cursor = cursor
return
except Exception:
# 连接已失效,从连接池移除
del SQLUtils._connection_pool[conn_key]
# 创建新连接
# 检查连接池中是否已有此线程的连接
# 只在访问共享资源时使用锁
with self._pool_lock:
if conn_key in self._connection_pool:
try:
# 尝试执行简单查询,确认连接有效
conn, cursor = self._connection_pool[conn_key]
cursor.execute("SELECT 1")
# 连接有效,直接使用
self.conn = conn
self.cursor = cursor
# 更新引用计数和最后使用时间
self._connection_refs[conn_key] = self._connection_refs.get(conn_key, 0) + 1
self._last_used[conn_key] = time.time()
return
except Exception:
# 连接已失效,从连接池移除
self._cleanup_connection(conn_key)
# 创建新连接 - 这部分不需要锁
self.connect()
# 将新连接添加到连接池
# 将新连接添加到连接池 - 需要锁
if self.conn and self.cursor:
SQLUtils._connection_pool[conn_key] = (self.conn, self.cursor)
with self._pool_lock:
self._connection_pool[conn_key] = (self.conn, self.cursor)
self._connection_refs[conn_key] = 1
self._last_used[conn_key] = time.time()
def _cleanup_connection(self, conn_key):
"""清理指定的连接"""
try:
if conn_key in self._connection_pool:
conn, cursor = self._connection_pool[conn_key]
if cursor:
try:
cursor.close()
except:
pass
if conn:
try:
conn.close()
except:
pass
# 移除相关引用
self._connection_pool.pop(conn_key, None)
self._connection_refs.pop(conn_key, None)
self._last_used.pop(conn_key, None)
logging.debug(f"已清理连接: {conn_key}")
except Exception as e:
logging.error(f"清理连接失败: {e}")
def connect(self):
"""连接到数据库"""
"""连接到数据库 - 不需要全局锁"""
try:
if self.db_type in ['pgsql', 'postgresql']:
if not psycopg2:
@ -120,7 +232,7 @@ class SQLUtils:
elif self.db_type in ['sqlite', 'sqlite3']:
if not sqlite3:
raise ImportError('sqlite3 is not installed')
self.conn = sqlite3.connect(self.kwargs.get('database', ':memory:'))
self.conn = sqlite3.connect(self.kwargs.get('database', ':memory:'), check_same_thread=False)
elif self.db_type == 'mysql':
if not mysql:
raise ImportError('mysql.connector is not installed')
@ -135,63 +247,126 @@ class SQLUtils:
raise
def execute_query(self, sql, params=None):
"""执行查询 - 不使用全局锁,仅使用单个连接"""
if params is None:
params = ()
self.cursor.execute(sql, params)
self.conn.commit()
try:
# 直接执行查询,因为每个线程有自己的连接
self.cursor.execute(sql, params)
# 更新最后使用时间
thread_id = threading.get_ident()
conn_key = f"{self.db_type}:{str(self.kwargs)}:{thread_id}"
with self._pool_lock:
if conn_key in self._last_used:
self._last_used[conn_key] = time.time()
return self.cursor
except Exception as e:
logging.error(f"执行查询失败: {e}, SQL: {sql}, 参数: {params}")
raise
def execute_update(self, sql, params=None):
"""执行更新 - 不使用全局锁,仅使用单个连接"""
try:
self.cursor.execute(sql,params)
if params is None:
params = ()
# 直接执行更新
self.cursor.execute(sql, params)
self.conn.commit()
# 更新最后使用时间
thread_id = threading.get_ident()
conn_key = f"{self.db_type}:{str(self.kwargs)}:{thread_id}"
with self._pool_lock:
if conn_key in self._last_used:
self._last_used[conn_key] = time.time()
return self.cursor.rowcount
except Exception as e:
self.conn.rollback()
logging.error(f"执行更新失败: {e}, SQL: {sql}, 参数: {params}")
raise e
def begin_transaction(self) -> None:
"""开始事务"""
"""开始事务 - 不使用全局锁"""
if self.db_type in ['sqlite', 'sqlite3']:
self.execute_query('BEGIN TRANSACTION')
else:
self.conn.autocommit = False
def commit_transaction(self) -> None:
"""提交事务"""
"""提交事务 - 不使用全局锁"""
self.conn.commit()
if self.db_type not in ['sqlite', 'sqlite3']:
self.conn.autocommit = True
def rollback_transaction(self) -> None:
"""回滚事务"""
"""回滚事务 - 不使用全局锁"""
self.conn.rollback()
if self.db_type not in ['sqlite', 'sqlite3']:
self.conn.autocommit = True
def fetchone(self):
"""获取一行数据 - 不使用全局锁"""
return self.cursor.fetchone()
def fetchall(self):
"""获取所有数据 - 不使用全局锁"""
return self.cursor.fetchall()
def get_new_cursor(self):
"""获取一个新的游标,用于避免游标递归使用问题
Returns:
cursor: 数据库游标对象
"""
if self.conn:
return self.conn.cursor()
return None
def close(self):
"""关闭连接(实际上是将连接返回到连接池)"""
# 这里不再实际关闭连接,让连接池管理连接生命周期
pass
"""关闭当前游标,减少引用计数,必要时释放连接"""
thread_id = threading.get_ident()
conn_key = f"{self.db_type}:{str(self.kwargs)}:{thread_id}"
with self._pool_lock:
if conn_key in self._connection_refs:
# 减少引用计数
self._connection_refs[conn_key] -= 1
# 如果引用计数为0关闭连接并从池中移除
if self._connection_refs[conn_key] <= 0:
self._cleanup_connection(conn_key)
def real_close(self):
"""强制关闭连接,无论引用计数"""
thread_id = threading.get_ident()
conn_key = f"{self.db_type}:{str(self.kwargs)}:{thread_id}"
with self._pool_lock:
self._cleanup_connection(conn_key)
@staticmethod
def close_all_connections():
"""关闭所有连接池中的连接"""
for conn, cursor in SQLUtils._connection_pool.values():
try:
if cursor:
cursor.close()
if conn:
conn.close()
except Exception as e:
logging.error(f"关闭数据库连接失败: {e}")
SQLUtils._connection_pool.clear()
logging.info("已关闭所有数据库连接")
with SQLUtils._pool_lock:
conn_keys = list(SQLUtils._connection_pool.keys())
for conn_key in conn_keys:
try:
conn, cursor = SQLUtils._connection_pool[conn_key]
if cursor:
cursor.close()
if conn:
conn.close()
except Exception as e:
logging.error(f"关闭数据库连接失败: {e}")
# 清空所有字典
SQLUtils._connection_pool.clear()
SQLUtils._connection_refs.clear()
SQLUtils._last_used.clear()
logging.info("已关闭所有数据库连接")
@staticmethod
def get_sqlite_connection():
@ -206,4 +381,64 @@ class SQLUtils:
@staticmethod
def get_mysql_connection():
"""获取MySQL连接"""
return SQLUtils(source_name='mysql')
return SQLUtils(source_name='mysql')
@classmethod
def get_connection_pool_stats(cls):
"""获取连接池统计信息
Returns:
dict: 包含连接池统计信息的字典
"""
with cls._pool_lock:
stats = {
'active_connections': len(cls._connection_pool),
'connection_details': [],
'connection_count_by_type': {},
'active_threads': {},
}
# 统计不同类型连接数量
for conn_key in cls._connection_pool:
parts = conn_key.split(':')
if len(parts) > 0:
db_type = parts[0]
stats['connection_count_by_type'][db_type] = stats['connection_count_by_type'].get(db_type, 0) + 1
# 获取线程ID
if len(parts) > 2:
thread_id = parts[2]
stats['active_threads'][thread_id] = stats['active_threads'].get(thread_id, 0) + 1
# 连接详情
refs = cls._connection_refs.get(conn_key, 0)
last_used = cls._last_used.get(conn_key, 0)
idle_time = time.time() - last_used if last_used else 0
stats['connection_details'].append({
'key': conn_key,
'references': refs,
'idle_time_seconds': int(idle_time),
'is_idle': refs <= 0
})
return stats
@classmethod
def log_connection_pool_status(cls):
"""记录当前连接池状态到日志"""
stats = cls.get_connection_pool_stats()
logging.info(f"数据库连接池状态: 活动连接数={stats['active_connections']}")
# 记录每种数据库类型的连接数
for db_type, count in stats['connection_count_by_type'].items():
logging.info(f" - {db_type}: {count}个连接")
# 记录空闲连接
idle_connections = [d for d in stats['connection_details'] if d['is_idle']]
if idle_connections:
logging.info(f" - 空闲连接: {len(idle_connections)}")
for conn in idle_connections:
logging.debug(f" * {conn['key']} (空闲{conn['idle_time_seconds']}秒)")
return stats

View File

@ -5,11 +5,13 @@ from ctypes import *
# 确定使用哪个UI框架
try:
from PySide6.QtWidgets import QWidget, QVBoxLayout, QFrame
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QWidget, QVBoxLayout, QFrame, QSizePolicy
from PySide6.QtCore import Qt, Signal, QSize
from PySide6.QtGui import QPalette, QColor
except ImportError:
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QFrame
from PyQt5.QtCore import Qt, pyqtSignal as Signal
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QFrame, QSizePolicy
from PyQt5.QtCore import Qt, pyqtSignal as Signal, QSize
from PyQt5.QtGui import QPalette, QColor
# 添加相机模块路径
sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), "camera"))
@ -44,6 +46,12 @@ class CameraDisplayWidget(QWidget):
# 设置大小变化事件处理
self.resizeEvent = self.on_resize_event
# 设置固定尺寸策略,防止超出父容器
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# 设置最小尺寸
self.setMinimumSize(QSize(320, 240))
def init_ui(self):
"""初始化UI - 只包含相机显示框架"""
@ -59,6 +67,8 @@ class CameraDisplayWidget(QWidget):
self.frame.setFrameShape(QFrame.NoFrame)
# 设置黑色背景
self.frame.setStyleSheet("background-color: #000000;")
# 设置框架的尺寸策略
self.frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# 添加到布局
layout.addWidget(self.frame)
@ -89,7 +99,7 @@ class CameraDisplayWidget(QWidget):
success = self.camera_manager.start_grabbing(window_id)
if success:
logging.info("相机画面显示开始")
logging.info(f"相机画面显示开始,显示区域大小: {self.frame.width()}x{self.frame.height()}")
self.signal_camera_status.emit(True, "")
else:
logging.error("开始显示相机画面失败")
@ -125,6 +135,11 @@ class CameraDisplayWidget(QWidget):
def on_resize_event(self, event):
"""处理大小变化事件,确保相机画面适配上料区"""
super().resizeEvent(event)
# 记录大小变化
logging.debug(f"相机显示区域大小变化为: {self.width()}x{self.height()}")
# 当尺寸变化超过一定阈值时,重新调整相机显示
if self.camera_manager.isGrabbing:
# 停止当前显示
self.stop_display()

View File

@ -1,6 +1,8 @@
import sys
import os
import logging
import json
import ctypes
from ctypes import *
# 添加相机模块路径
@ -49,8 +51,14 @@ class CameraManager:
logging.info("相机SDK已初始化")
def enum_devices(self):
"""枚举相机设备"""
"""枚举相机设备完全参考BasicDemo.py的enum_devices实现"""
try:
# 确保Hikvision SDK已正确加载
from camera.MvCameraControl_class import MvCamCtrldll
if MvCamCtrldll is None:
logging.error("相机SDK未正确加载无法枚举设备")
return []
# 确保先关闭任何已打开的相机
if self.isOpen:
self.close_device()
@ -71,24 +79,27 @@ class CameraManager:
logging.info(f"找到 {self.deviceList.nDeviceNum} 个相机设备")
# 定义解码函数
def decoding_char(c_ubyte_value):
c_char_p_value = ctypes.cast(c_ubyte_value, ctypes.c_char_p)
try:
decode_str = c_char_p_value.value.decode('gbk') # 中文字符
except UnicodeDecodeError:
decode_str = str(c_char_p_value.value)
return decode_str
# 构造设备信息列表
devices_info = []
for i in range(0, self.deviceList.nDeviceNum):
mvcc_dev_info = cast(self.deviceList.pDeviceInfo[i], POINTER(MV_CC_DEVICE_INFO)).contents
# 详细日志
logging.debug(f"设备 {i} 类型: {mvcc_dev_info.nTLayerType}")
if mvcc_dev_info.nTLayerType == MV_GIGE_DEVICE:
# GigE相机
user_defined_name = ""
for per in mvcc_dev_info.SpecialInfo.stGigEInfo.chUserDefinedName:
if per == 0:
break
user_defined_name = user_defined_name + chr(per)
model_name = ""
for per in mvcc_dev_info.SpecialInfo.stGigEInfo.chModelName:
if per == 0:
break
model_name = model_name + chr(per)
user_defined_name = decoding_char(mvcc_dev_info.SpecialInfo.stGigEInfo.chUserDefinedName)
model_name = decoding_char(mvcc_dev_info.SpecialInfo.stGigEInfo.chModelName)
nip1 = ((mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp & 0xff000000) >> 24)
nip2 = ((mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp & 0x00ff0000) >> 16)
@ -96,48 +107,81 @@ class CameraManager:
nip4 = (mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp & 0x000000ff)
ip = f"{nip1}.{nip2}.{nip3}.{nip4}"
display = f"[{i}]GigE: {user_defined_name} {model_name} ({ip})"
logging.debug(f"GigE相机: {display}")
device_info = {
"index": i,
"type": "GigE",
"name": user_defined_name,
"model": model_name,
"ip": ip,
"display": f"[{i}]GigE: {user_defined_name} {model_name} ({ip})"
"display": display
}
devices_info.append(device_info)
elif mvcc_dev_info.nTLayerType == MV_USB_DEVICE:
# USB相机
user_defined_name = ""
for per in mvcc_dev_info.SpecialInfo.stUsb3VInfo.chUserDefinedName:
if per == 0:
break
user_defined_name = user_defined_name + chr(per)
user_defined_name = decoding_char(mvcc_dev_info.SpecialInfo.stUsb3VInfo.chUserDefinedName)
model_name = decoding_char(mvcc_dev_info.SpecialInfo.stUsb3VInfo.chModelName)
model_name = ""
for per in mvcc_dev_info.SpecialInfo.stUsb3VInfo.chModelName:
# 序列号
strSerialNumber = ""
for per in mvcc_dev_info.SpecialInfo.stUsb3VInfo.chSerialNumber:
if per == 0:
break
model_name = model_name + chr(per)
strSerialNumber = strSerialNumber + chr(per)
display = f"[{i}]USB: {user_defined_name} {model_name} ({strSerialNumber})"
logging.debug(f"USB相机: {display}")
device_info = {
"index": i,
"type": "USB",
"name": user_defined_name,
"model": model_name,
"display": f"[{i}]USB: {user_defined_name} {model_name}"
"serial": strSerialNumber,
"display": display
}
devices_info.append(device_info)
elif mvcc_dev_info.nTLayerType == MV_GENTL_CAMERALINK_DEVICE:
# CameraLink相机
user_defined_name = decoding_char(mvcc_dev_info.SpecialInfo.stCMLInfo.chUserDefinedName)
model_name = decoding_char(mvcc_dev_info.SpecialInfo.stCMLInfo.chModelName)
strSerialNumber = ""
for per in mvcc_dev_info.SpecialInfo.stCMLInfo.chSerialNumber:
if per == 0:
break
strSerialNumber = strSerialNumber + chr(per)
display = f"[{i}]CML: {user_defined_name} {model_name} ({strSerialNumber})"
logging.debug(f"CML相机: {display}")
device_info = {
"index": i,
"type": "CML",
"name": user_defined_name,
"model": model_name,
"serial": strSerialNumber,
"display": display
}
devices_info.append(device_info)
else:
# 其他类型相机
display = f"[{i}]Other"
device_info = {
"index": i,
"type": "Other",
"display": f"[{i}]Other"
"display": display
}
devices_info.append(device_info)
logging.debug(f"其他类型相机: {display}")
# 添加详细日志
logging.debug(f"枚举到的设备数量: {len(devices_info)}")
return devices_info
except Exception as e:
@ -146,7 +190,7 @@ class CameraManager:
return None
def open_device(self, device_index):
"""打开相机设备
"""打开相机设备参考BasicDemo.py的open_device实现
Args:
device_index: 设备索引
@ -159,14 +203,18 @@ class CameraManager:
logging.warning("相机已经打开!")
return False
# 检查设备索引是否有效
if device_index < 0 or (self.deviceList and device_index >= self.deviceList.nDeviceNum):
logging.error(f"无效的设备索引: {device_index}")
# 确保有效的设备索引
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
try:
logging.debug(f"准备打开相机,设备索引: {device_index}")
logging.info(f"开始打开相机,设备索引: {device_index}")
# 设置当前选中的相机索引
self.nSelCamIndex = device_index
# 创建相机操作对象
@ -178,14 +226,25 @@ class CameraManager:
self.isOpen = False
return False
# 设置连续模式
self.obj_cam_operation.Set_trigger_mode(False)
# 设置连续模式 (非触发模式)
ret = self.obj_cam_operation.Set_trigger_mode(False)
if ret != 0:
error_msg = f"设置连续模式失败! 错误码: 0x{ret:x}"
logging.error(error_msg)
# 出错时关闭设备
self.obj_cam_operation.Close_device()
self.isOpen = False
return False
# 获取参数
self.obj_cam_operation.Get_parameter()
ret = self.obj_cam_operation.Get_parameter()
if ret != 0:
error_msg = f"获取相机参数失败! 错误码: 0x{ret:x}"
logging.error(error_msg)
# 继续执行,不返回失败
self.isOpen = True
logging.info(f"相机已打开,设备索引: {device_index}")
logging.info(f"相机已成功打开,设备索引: {device_index}")
return True
@ -351,4 +410,52 @@ class CameraManager:
except Exception as e:
error_msg = f"设置相机参数时发生异常: {str(e)}"
logging.error(error_msg)
return False
def save_params_to_config(self, exposure, gain, frame_rate):
"""保存相机参数到配置文件
Args:
exposure: 曝光值(滑块值)
gain: 增益(滑块值)
frame_rate: 帧率(滑块值)
Returns:
bool: 是否成功保存参数
"""
try:
# 创建相机参数配置
config = {
"exposure": exposure,
"gain": gain,
"frame_rate": frame_rate
}
# 保存到配置文件
config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "camera_config.json")
os.makedirs(os.path.dirname(config_path), exist_ok=True)
# 检查文件是否存在并读取
if os.path.exists(config_path):
with open(config_path, 'r', encoding='utf-8') as f:
try:
full_config = json.load(f)
except json.JSONDecodeError:
full_config = {}
else:
full_config = {}
# 更新配置
full_config["camera_params"] = config
# 写入文件
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(full_config, f, indent=4, ensure_ascii=False)
logging.info(f"相机参数已保存到配置文件: {config_path}")
return True
except Exception as e:
error_msg = f"保存相机参数到配置文件时发生异常: {str(e)}"
logging.error(error_msg)
return False

View File

@ -28,32 +28,97 @@ from ui.settings_ui import SettingsUI
from widgets.camera_manager import CameraManager
class CameraSettingsWidget(SettingsUI):
"""相机设置控制器,管理相机设置并提供与主窗口相机显示部分的通信"""
from PySide6.QtCore import QObject
class CameraSettingsWidget(QObject):
"""相机设置控制器,管理相机设置并提供与主窗口相机显示部分的通信
注意这是一个QObject控制器不是QWidget只处理逻辑UI控件需要从父组件获取"""
# 定义信号
# 定义信号
signal_camera_connection = Signal(bool, str) # 相机连接状态信号 (是否连接, 错误消息)
signal_camera_params_changed = Signal(float, float, float) # 相机参数变化信号 (曝光, 增益, 帧率)
signal_camera_error = Signal(str) # 相机错误信号
settings_changed = Signal() # 设置变更信号,与 SettingsWindow 兼容
def __init__(self, parent=None):
"""初始化相机设置控制器
Args:
parent: 父组件需要包含所有必要的UI控件
"""
# 作为QObject初始化
super().__init__(parent)
# 记录父组件引用
self.parent = parent
# 获取相机管理器实例
self.camera_manager = CameraManager.get_instance()
# 初始化UI控件状态
self.update_controls()
# 初始化日志记录
logging.debug("CameraSettingsWidget初始化开始")
# 从父组件获取所有需要的UI控件
if parent is not None:
logging.info(f"父控件类型: {type(parent).__name__}")
# 从父组件获取所有需要的UI控件
self.camera_combo = getattr(parent, 'camera_combo', None)
self.refresh_button = getattr(parent, 'refresh_button', None)
self.open_button = getattr(parent, 'open_button', None)
self.close_button = getattr(parent, 'close_button', None)
self.test_button = getattr(parent, 'test_button', None)
self.exposure_slider = getattr(parent, 'exposure_slider', None)
self.gain_slider = getattr(parent, 'gain_slider', None)
self.framerate_slider = getattr(parent, 'framerate_slider', None)
self.exposure_value = getattr(parent, 'exposure_value', None)
self.gain_value = getattr(parent, 'gain_value', None)
self.framerate_value = getattr(parent, 'framerate_value', None)
self.get_params_button = getattr(parent, 'get_params_button', None)
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)
# 检查是否成功获取到了所有必要的UI控件
if self.camera_combo is None:
logging.error("未能从父组件获取camera_combo")
else:
logging.info(f"获取到父组件的camera_combo: ID={id(self.camera_combo)}, 项目数={self.camera_combo.count()}")
# 测试向下拉框添加项目
try:
self.camera_combo.addItem("控制器初始化测试项")
logging.info(f"成功添加测试项到camera_combo当前项目数={self.camera_combo.count()}")
except Exception as e:
logging.error(f"添加测试项失败: {e}")
else:
logging.error("CameraSettingsWidget必须有一个包含UI控件的父组件")
# 创建缺少的UI控件引用这些引用将为None
self.camera_combo = None
self.refresh_button = None
self.open_button = None
self.close_button = None
self.test_button = None
self.exposure_slider = None
self.gain_slider = None
self.framerate_slider = None
self.exposure_value = None
self.gain_value = None
self.framerate_value = None
self.get_params_button = None
self.set_params_button = None
self.save_camera_button = None
self.preview_frame = None
# 连接信号和槽
self.connect_signals()
# 初始化相机参数范围
# 初始化相机参数范围 - 注意:曝光参数的实际范围已改为线性映射
self.frame_rate_min = 1.0
self.frame_rate_max = 60.0
self.exposure_min = 20.0
self.exposure_max = 1000000.0
# 以下曝光范围仅用于兼容旧代码实际的映射在update_exposure_value方法中实现
self.exposure_min = 0.0 # 0 μs (最小值)
self.exposure_max = 20000.0 # 20000 μs (20ms) - 调整后的最大值
self.gain_min = 0.0
self.gain_max = 15.0
@ -63,7 +128,8 @@ class CameraSettingsWidget(SettingsUI):
def connect_signals(self):
"""连接信号和槽"""
# 设备选择和刷新
self.refresh_button.clicked.connect(self.refresh_devices)
if hasattr(self, 'refresh_button'):
self.refresh_button.clicked.connect(self.refresh_devices)
# 相机控制按钮
self.open_button.clicked.connect(self.open_camera)
@ -79,259 +145,396 @@ class CameraSettingsWidget(SettingsUI):
self.get_params_button.clicked.connect(self.get_camera_params)
self.set_params_button.clicked.connect(self.set_camera_params)
self.save_camera_button.clicked.connect(self.save_camera_params)
def refresh_devices(self):
"""刷新设备列表"""
self.camera_combo.clear()
logging.info("【设备刷新】开始...")
# 枚举设备
devices_info = self.camera_manager.enum_devices()
# 直接检查是否能访问正确的combobox
if not hasattr(self, 'camera_combo') or self.camera_combo is None:
logging.error("【设备刷新】无法访问camera_combo尝试从父组件获取")
# 尝试从父组件获取
if hasattr(self, 'parent') and self.parent is not None:
if hasattr(self.parent, 'camera_combo'):
self.camera_combo = self.parent.camera_combo
logging.info(f"【设备刷新】已从父组件获取camera_combo: {id(self.camera_combo)}")
else:
logging.error("【设备刷新】父组件也没有camera_combo")
return
else:
logging.error("【设备刷新】无法获取camera_combo且没有父组件")
return
# 直接测试下拉框是否可用
try:
# 先清理
self.camera_combo.clear()
# 添加一个测试项
self.camera_combo.addItem("刷新中...")
logging.info(f"【设备刷新】成功添加测试项,当前项目数={self.camera_combo.count()}")
# 更新UI
self.camera_combo.update()
self.camera_combo.repaint()
except Exception as e:
logging.error(f"【设备刷新】测试下拉框失败: {e}")
if not devices_info:
self.camera_combo.addItem("未发现相机设备")
self.camera_combo.setEnabled(False)
self.open_button.setEnabled(False)
return
# 添加设备到下拉框
for device in devices_info:
self.camera_combo.addItem(device["display"], device["index"])
self.camera_combo.setEnabled(True)
self.open_button.setEnabled(True)
# 更新控件状态
self.update_controls()
# 1. 枚举设备
try:
devices_info = self.camera_manager.enum_devices()
if not devices_info:
logging.warning("【设备刷新】未发现任何相机设备。")
devices_info = []
except Exception as e:
logging.error(f"【设备刷新】枚举设备时发生错误: {e}")
devices_info = []
# 2. 准备要显示到下拉列表的数据
devList = []
if devices_info:
for device in devices_info:
devList.append(device["display"])
logging.info(f"【设备刷新】找到 {len(devList)} 个设备: {devList}")
else:
devList.append("未发现相机设备")
logging.info(f"【设备刷新】将显示默认值 '未发现相机设备'")
# 3. 更新UI上的下拉列表
try:
if hasattr(self, 'camera_combo') and self.camera_combo is not None:
logging.info(f"【设备刷新】开始更新下拉列表,当前状态: 项目数={self.camera_combo.count()}, 是否可见={self.camera_combo.isVisible()}")
self.camera_combo.blockSignals(True)
self.camera_combo.clear()
# 确保项目数清零
if self.camera_combo.count() > 0:
logging.warning(f"【设备刷新】clear()后项目数仍为 {self.camera_combo.count()}")
# 直接添加项目 - 单个添加,避免批量添加可能的问题
for item in devList:
self.camera_combo.addItem(item)
logging.debug(f"【设备刷新】已添加项目: {item}")
# 确保设置当前项目
if self.camera_combo.count() > 0:
self.camera_combo.setCurrentIndex(0)
logging.info(f"【设备刷新】已设置当前索引为0显示文本: {self.camera_combo.currentText()}")
else:
logging.error("【设备刷新】未能添加任何项目到下拉列表")
self.camera_combo.blockSignals(False)
# 强制更新UI
self.camera_combo.update()
self.camera_combo.repaint()
# 确保ComboBox有足够的尺寸显示内容
self.camera_combo.adjustSize()
logging.info(f"【设备刷新】下拉列表更新完成。当前项目数: {self.camera_combo.count()}, 当前文本: {self.camera_combo.currentText()}")
else:
logging.error("【设备刷新】无法更新下拉列表camera_combo不存在")
except Exception as e:
logging.error(f"【设备刷新】更新下拉列表时发生错误: {e}")
# 4. 更新其他控件的状态
try:
self.update_controls()
logging.info("【设备刷新】控件状态已更新。")
except Exception as e:
logging.error(f"【设备刷新】更新控件状态时发生错误: {e}")
# 5. 如果下拉列表仍然为空,尝试最后一次强制添加
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}")
def get_selected_device_index(self):
"""获取当前选中的设备索引"""
if self.camera_combo.count() == 0:
"""获取当前选中的设备索引参考BasicDemo.py的TxtWrapBy实现"""
try:
if not hasattr(self, 'camera_combo') or self.camera_combo is None:
logging.error("无法获取设备索引camera_combo不存在")
return -1
if self.camera_combo.count() == 0:
logging.warning("设备下拉列表为空")
return -1
if self.camera_combo.currentText() == "未发现相机设备":
return -1
current_text = self.camera_combo.currentText()
start = current_text.find("[") + 1
end = current_text.find("]", start)
if start <= 0 or end <= 0 or start >= end:
logging.error(f"设备文本格式不正确: '{current_text}'")
return -1
return int(current_text[start:end])
except Exception as e:
logging.error(f"获取设备索引时出错: {e}, 文本: '{self.camera_combo.currentText() if hasattr(self, 'camera_combo') and self.camera_combo is not None else 'N/A'}'")
return -1
return self.camera_combo.currentData()
def update_controls(self):
"""更新控件状态"""
is_open = self.camera_manager.isOpen
is_grabbing = self.camera_manager.isGrabbing
# 使用安全的方式访问控件
if hasattr(self, 'refresh_button') and self.refresh_button is not None:
self.refresh_button.setEnabled(not is_open)
# 设备选择和刷新
self.camera_combo.setEnabled(not is_open)
self.refresh_button.setEnabled(not is_open)
if hasattr(self, 'camera_combo') and self.camera_combo is not None:
self.camera_combo.setEnabled(not is_open)
# 相机控制按钮
self.open_button.setEnabled(not is_open and self.camera_combo.count() > 0 and self.camera_combo.currentData() is not None)
self.close_button.setEnabled(is_open)
self.test_button.setEnabled(is_open and not is_grabbing)
has_valid_selection = self.get_selected_device_index() != -1
# 参数滑块
self.exposure_slider.setEnabled(is_open)
self.gain_slider.setEnabled(is_open)
self.framerate_slider.setEnabled(is_open)
if hasattr(self, 'open_button') and self.open_button is not None:
self.open_button.setEnabled(not is_open and has_valid_selection)
# 参数操作按钮
self.get_params_button.setEnabled(is_open)
self.set_params_button.setEnabled(is_open)
self.save_camera_button.setEnabled(is_open)
if hasattr(self, 'close_button') and self.close_button is not None:
self.close_button.setEnabled(is_open)
if hasattr(self, 'test_button') and self.test_button is not None:
self.test_button.setEnabled(is_open and not is_grabbing)
if hasattr(self, 'get_params_button') and self.get_params_button is not None:
self.get_params_button.setEnabled(is_open)
if hasattr(self, 'set_params_button') and self.set_params_button is not None:
self.set_params_button.setEnabled(is_open)
if hasattr(self, 'save_camera_button') and self.save_camera_button is not None:
self.save_camera_button.setEnabled(is_open)
if hasattr(self, 'exposure_slider') and self.exposure_slider is not None:
self.exposure_slider.setEnabled(is_open)
if hasattr(self, 'gain_slider') and self.gain_slider is not None:
self.gain_slider.setEnabled(is_open)
if hasattr(self, 'framerate_slider') and self.framerate_slider is not None:
self.framerate_slider.setEnabled(is_open)
def open_camera(self):
"""打开相机"""
if self.camera_manager.isOpen:
QMessageBox.warning(self, "错误", "相机已经打开!")
return
device_index = self.get_selected_device_index()
if device_index < 0:
QMessageBox.warning(self, "错误", "请先选择一个相机设备!")
return
success = self.camera_manager.open_device(device_index)
if success:
# 获取相机参数并更新UI
self.get_camera_params()
try:
device_index = self.get_selected_device_index()
if device_index < 0:
error_msg = "未选择相机设备"
self.signal_camera_error.emit(error_msg)
return False
logging.info(f"尝试打开相机设备,索引: {device_index}")
# 更新控件状态
self.update_controls()
# 使用相机管理器打开设备
success = self.camera_manager.open_device(device_index)
# 发送连接信号
self.signal_camera_connection.emit(True, "")
else:
# 发送连接失败信号
self.signal_camera_connection.emit(False, "打开相机失败")
if success:
logging.info(f"相机已成功打开")
# 更新按钮状态
self.update_controls()
# 向父窗口发送连接状态变化信号
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()
# 通知设置已更改
self.settings_changed.emit()
return True
else:
error_msg = "打开相机失败"
self.signal_camera_error.emit(error_msg)
return False
except Exception as e:
error_msg = f"打开相机时发生异常: {str(e)}"
logging.error(error_msg)
self.signal_camera_error.emit(error_msg)
return False
def close_camera(self):
"""关闭相机"""
if not self.camera_manager.isOpen:
return
success = self.camera_manager.close_device()
# 更新控件状态
self.update_controls()
# 发送连接信号
if success:
try:
logging.info("尝试关闭相机")
# 使用相机管理器关闭设备
self.camera_manager.close_device()
# 更新按钮状态
self.update_controls()
# 向父窗口发送连接状态变化信号
self.signal_camera_connection.emit(False, "")
else:
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()
# 通知设置已更改
self.settings_changed.emit()
logging.info("相机已关闭")
return True
except Exception as e:
error_msg = f"关闭相机时发生异常: {str(e)}"
logging.error(error_msg)
self.signal_camera_error.emit(error_msg)
return False
def test_camera(self):
"""测试相机(在预览窗口显示图像)"""
if not self.camera_manager.isOpen:
QMessageBox.warning(self, "错误", "请先打开相机!")
return
if self.camera_manager.isGrabbing:
# 停止预览
self.camera_manager.stop_grabbing()
self.test_button.setText("开始预览")
self.update_controls()
else:
# 获取预览窗口句柄
try:
# 尝试使用PySide6方式获取窗口句柄
window_id = int(self.preview_frame.winId())
except:
try:
# 尝试使用PyQt5方式获取窗口句柄
window_id = self.preview_frame.winId().__int__()
except:
# 其他情况
window_id = int(self.preview_frame.winId())
# 开始预览
success = self.camera_manager.start_grabbing(window_id)
if success:
self.test_button.setText("停止预览")
else:
QMessageBox.warning(self, "错误", "开始预览失败!")
# 更新控件状态
self.update_controls()
if self.camera_manager.start_grabbing(window_id):
self.test_button.setText("停止预览")
except Exception as e:
parent_widget = self.parent if hasattr(self, "parent") else None
QMessageBox.warning(parent_widget, "错误", f"开始预览失败: {e}")
self.update_controls()
def update_exposure_value(self, value):
"""更新曝光值显示"""
# 将滑块值转换为实际曝光值(对数映射
min_log = log10(self.exposure_min)
max_log = log10(self.exposure_max)
log_range = max_log - min_log
"""更新曝光值显示(使用线性映射)"""
# 直接使用滑块值0-100映射到曝光范围1000-50000 μs
# 使用UI中设置的实际曝光范围
min_exp = 0 # 0 μs
max_exp = 50000
log_value = min_log + (value / 100.0) * log_range
actual_value = 10 ** log_value
# 线性映射
actual_value = min_exp + (value * (max_exp - min_exp) / 100.0)
# 更新显示
self.exposure_value.setText(f"{actual_value:.1f} μs")
# 防止溢出
if actual_value > max_exp:
actual_value = max_exp
logging.warning(f"曝光值过大,已限制为{actual_value}μs")
# 显示整数值μs级别不需要小数点
self.exposure_value.setText(f"{int(actual_value)} μs")
logging.debug(f"曝光滑块值: {value}, 映射后曝光值: {int(actual_value)}μs")
def update_gain_value(self, value):
"""更新增益值显示"""
# 将滑块值转换为实际增益值
actual_value = self.gain_min + (value / 100.0) * (self.gain_max - self.gain_min)
# 更新显示
self.gain_value.setText(f"{actual_value:.1f} dB")
def update_frame_rate_value(self, value):
"""更新帧率值显示"""
# 将滑块值转换为实际帧率值
actual_value = self.frame_rate_min + (value / 100.0) * (self.frame_rate_max - self.frame_rate_min)
# 更新显示
self.framerate_value.setText(f"{actual_value:.1f} fps")
def get_camera_params(self):
"""获取相机参数"""
if not self.camera_manager.isOpen:
QMessageBox.warning(self, "错误", "请先打开相机!")
return
# 获取参数
params = self.camera_manager.get_parameters()
if not params:
QMessageBox.warning(self, "错误", "获取相机参数失败!")
return
exposure_time, gain, frame_rate = params
# 更新滑块值
# 曝光时间(对数映射)
min_log = log10(self.exposure_min)
max_log = log10(self.exposure_max)
log_range = max_log - min_log
log_value = log10(exposure_time)
slider_value = int(((log_value - min_log) / log_range) * 100)
self.exposure_slider.setValue(slider_value)
# 使用线性映射计算曝光滑块值
min_exp = 0 # 0 μs
max_exp = 20000 # 20000 μs
# 增益
slider_value = int(((gain - self.gain_min) / (self.gain_max - self.gain_min)) * 100)
self.gain_slider.setValue(slider_value)
# 将获取到的曝光时间限制在有效范围内
if exposure_time < min_exp:
exposure_time = min_exp
elif exposure_time > max_exp:
exposure_time = max_exp
# 线性映射到滑块值0-100
exposure_slider_value = int(((exposure_time - min_exp) / (max_exp - min_exp)) * 100)
# 帧率
slider_value = int(((frame_rate - self.frame_rate_min) / (self.frame_rate_max - self.frame_rate_min)) * 100)
self.framerate_slider.setValue(slider_value)
# 确保滑块值在有效范围内
if exposure_slider_value < 0:
exposure_slider_value = 0
elif exposure_slider_value > 100:
exposure_slider_value = 100
self.exposure_slider.setValue(exposure_slider_value)
# 发送参数变化信号
# 其他参数仍使用原始映射方法
self.gain_slider.setValue(int(((gain - self.gain_min) / (self.gain_max - self.gain_min)) * 100))
self.framerate_slider.setValue(int(((frame_rate - self.frame_rate_min) / (self.frame_rate_max - self.frame_rate_min)) * 100))
logging.debug(f"获取相机参数: 曝光时间={exposure_time}μs -> 滑块值={exposure_slider_value}, 增益={gain}dB, 帧率={frame_rate}fps")
self.signal_camera_params_changed.emit(exposure_time, gain, frame_rate)
def set_camera_params(self):
"""设置相机参数"""
if not self.camera_manager.isOpen:
QMessageBox.warning(self, "错误", "请先打开相机!")
return
# 从滑块获取参数值
# 曝光时间(对数映射)
min_log = log10(self.exposure_min)
max_log = log10(self.exposure_max)
log_range = max_log - min_log
log_value = min_log + (self.exposure_slider.value() / 100.0) * log_range
exposure_time = 10 ** log_value
# 使用线性映射计算曝光时间
min_exp = 0 # 0 μs
max_exp = 50000 # 50000 μs
# 增益
# 根据滑块值0-100线性映射到曝光时间
slider_value = self.exposure_slider.value()
exposure_time = min_exp + (slider_value * (max_exp - min_exp) / 100.0)
# 确保曝光时间在有效范围内
if exposure_time < min_exp:
exposure_time = min_exp
elif exposure_time > max_exp:
exposure_time = max_exp
# 增益和帧率保持原来的映射方式
gain = self.gain_min + (self.gain_slider.value() / 100.0) * (self.gain_max - self.gain_min)
# 帧率
frame_rate = self.frame_rate_min + (self.framerate_slider.value() / 100.0) * (self.frame_rate_max - self.frame_rate_min)
# 设置参数
success = self.camera_manager.set_parameters(frame_rate, exposure_time, gain)
if success:
QMessageBox.information(self, "成功", "相机参数设置成功!")
# 发送参数变化信号
self.signal_camera_params_changed.emit(exposure_time, gain, frame_rate)
else:
QMessageBox.warning(self, "错误", "相机参数设置失败!")
logging.debug(f"设置相机参数: 曝光滑块值={slider_value} -> 曝光时间={exposure_time}μs, 增益={gain}dB, 帧率={frame_rate}fps")
self.camera_manager.set_parameters(frame_rate, exposure_time, gain)
self.signal_camera_params_changed.emit(exposure_time, gain, frame_rate)
def save_camera_params(self):
"""保存相机参数到配置文件"""
if not self.camera_manager.isOpen:
QMessageBox.warning(self, "错误", "请先打开相机!")
return
# 获取当前参数
exposure = self.exposure_slider.value()
gain = self.gain_slider.value()
frame_rate = self.framerate_slider.value()
# 保存到配置文件
success = self.camera_manager.save_params_to_config(exposure, gain, frame_rate)
parent_widget = self.parent if hasattr(self, "parent") else None
if success:
QMessageBox.information(self, "成功", "相机参数已保存到配置文件")
self.settings_changed.emit() # 发送设置变更信号
logging.info(f"相机参数已保存: 曝光={exposure}μs, 增益={gain}dB, 帧率={frame_rate}fps")
if self.camera_manager.save_params_to_config(exposure, gain, frame_rate):
QMessageBox.information(parent_widget, "成功", "相机参数已保存")
self.settings_changed.emit()
else:
QMessageBox.critical(self, "错误", "保存相机参数失败")
logging.error("保存相机参数失败")
QMessageBox.critical(parent_widget, "错误", "保存相机参数失败")
def closeEvent(self, event):
"""窗口关闭事件"""
# 确保关闭相机
if self.camera_manager.isOpen:
self.camera_manager.close_device()
# 处理事件
super().closeEvent(event)
super().closeEvent(event)

View File

@ -38,7 +38,7 @@ def get_user_info(user_id):
return result
# 始终使用SQLite数据源获取用户信息
db = SQLUtils(source_name='sqlite')
db.execute_query("SELECT username, corp_id as corp_name, corp_id FROM wsbz_user WHERE username = ?", (user_id,))
db.execute_query("SELECT userid, username, corp_id as corp_name, corp_id FROM wsbz_user WHERE username = ?", (user_id,))
result = db.fetchone()
db.close()
if result:

File diff suppressed because it is too large Load Diff

View File

@ -26,384 +26,200 @@ class PalletTypeSettingsWidget(PalletTypeSettingsUI):
def connect_signals(self):
"""连接信号和槽"""
# 保存和重置按钮
self.save_button.clicked.connect(self.save_all_pallet_types)
self.reset_button.clicked.connect(self.load_pallet_types)
# 保存和重置按钮 - 这些按钮已被删除,因此移除这些连接
# self.save_button.clicked.connect(self.save_all)
# self.reset_button.clicked.connect(self.load_pallet_types)
# 下料类型表格和按钮
output_table = self.output_widget.findChild(QTableWidget, "output_table")
output_table.itemSelectionChanged.connect(lambda: self.handle_table_selection("output"))
# 托盘类型操作按钮
self.add_button.clicked.connect(self.add_pallet_type)
self.update_button.clicked.connect(self.update_pallet_type)
self.delete_button.clicked.connect(self.delete_pallet_type)
self.cancel_button.clicked.connect(self.cancel_edit)
output_add_button = self.output_widget.findChild(QPushButton, "output_add_button")
output_add_button.clicked.connect(lambda: self.add_pallet_type("output"))
output_update_button = self.output_widget.findChild(QPushButton, "output_update_button")
output_update_button.clicked.connect(lambda: self.update_pallet_type("output"))
output_delete_button = self.output_widget.findChild(QPushButton, "output_delete_button")
output_delete_button.clicked.connect(lambda: self.delete_pallet_type("output"))
output_cancel_button = self.output_widget.findChild(QPushButton, "output_cancel_button")
output_cancel_button.clicked.connect(lambda: self.cancel_edit("output"))
# 表格选择事件
self.pallet_table.itemClicked.connect(self.on_table_item_clicked)
def load_pallet_types(self):
"""加载托盘类型数据"""
try:
# 重新加载数据
self.pallet_type_manager.reload_pallet_types()
# 获取托盘类型数据
pallet_types = self.pallet_type_manager.get_all_pallet_types()
# 加载下料类型
self.load_operation_pallet_types("output")
# 清空表格
self.pallet_table.setRowCount(0)
logging.info("托盘类型数据已加载")
# 填充表格
for pallet_type in pallet_types:
row = self.pallet_table.rowCount()
self.pallet_table.insertRow(row)
# 设置托盘ID
id_item = QTableWidgetItem(str(pallet_type['id']))
id_item.setTextAlignment(Qt.AlignCenter)
self.pallet_table.setItem(row, 0, id_item)
# 设置托盘名称
name_item = QTableWidgetItem(pallet_type['name'])
name_item.setTextAlignment(Qt.AlignCenter)
self.pallet_table.setItem(row, 1, name_item)
# 清空输入框
self.cancel_edit()
logging.info(f"已加载 {len(pallet_types)} 个托盘类型")
except Exception as e:
logging.error(f"加载托盘类型数据失败: {str(e)}")
QMessageBox.critical(self, "错误", f"加载托盘类型数据失败: {str(e)}")
logging.error(f"加载托盘类型失败: {str(e)}")
QMessageBox.critical(self, "错误", f"加载托盘类型失败: {str(e)}")
def load_operation_pallet_types(self, operation_type):
"""加载指定操作类型的托盘类型数据
def on_table_item_clicked(self, item):
"""表格项点击事件
Args:
operation_type: 操作类型 (output)
item: 点击的表格项
"""
# 获取表格
table = self.get_table_by_operation_type(operation_type)
if not table:
return
row = item.row()
# 清空表格
table.setRowCount(0)
# 获取托盘ID和名称
pallet_id = self.pallet_table.item(row, 0).text()
pallet_name = self.pallet_table.item(row, 1).text()
# 获取数据
pallet_types = self.pallet_type_manager.get_pallet_types_by_operation(operation_type, include_disabled=True)
# 填充表格
for row, pallet_type in enumerate(pallet_types):
table.insertRow(row)
# 填充输入框
self.pallet_id_input.setText(pallet_id)
self.pallet_name_input.setText(pallet_name)
def add_pallet_type(self):
"""添加托盘类型"""
try:
# 获取输入值
pallet_id_text = self.pallet_id_input.text().strip()
pallet_name = self.pallet_name_input.text().strip()
# 类型名称
type_name_item = QTableWidgetItem(pallet_type['type_name'])
type_name_item.setData(Qt.UserRole, pallet_type['id'])
table.setItem(row, 0, type_name_item)
# 验证输入
if not pallet_id_text or not pallet_name:
QMessageBox.warning(self, "警告", "托盘ID和托盘名称不能为空")
return
# 描述
desc_item = QTableWidgetItem(pallet_type['description'] or "")
table.setItem(row, 1, desc_item)
# 检查ID是否为数字
try:
pallet_id = int(pallet_id_text)
except ValueError:
QMessageBox.warning(self, "警告", "托盘ID必须为数字")
return
# 排序
sort_order_item = QTableWidgetItem(str(pallet_type['sort_order']))
table.setItem(row, 2, sort_order_item)
# 添加托盘类型
result = self.pallet_type_manager.add_pallet_type(pallet_id, pallet_name)
if not result:
QMessageBox.critical(self, "错误", "添加托盘类型失败")
return
# 启用状态
enabled_check = QCheckBox()
enabled_check.setChecked(pallet_type['enabled'])
enabled_check.stateChanged.connect(lambda state, row=row, id=pallet_type['id']:
self.toggle_pallet_type(id, state == Qt.Checked))
table.setCellWidget(row, 3, enabled_check)
# 重置表单
self.reset_form(operation_type)
def get_table_by_operation_type(self, operation_type):
"""根据操作类型获取表格
Args:
operation_type: 操作类型 (output)
Returns:
QTableWidget: 表格部件
"""
if operation_type == "output":
return self.output_widget.findChild(QTableWidget, "output_table")
return None
def get_form_values(self, operation_type):
"""获取表单值
Args:
operation_type: 操作类型 (output)
Returns:
dict: 表单值
"""
widget = self.output_widget
type_name_input = widget.findChild(QLineEdit, f"{operation_type}_type_name_input")
desc_input = widget.findChild(QLineEdit, f"{operation_type}_desc_input")
sort_order_spin = widget.findChild(QSpinBox, f"{operation_type}_sort_order_spin")
enabled_check = widget.findChild(QCheckBox, f"{operation_type}_enabled_check")
return {
'type_name': type_name_input.text().strip(),
'operation_type': operation_type,
'description': desc_input.text().strip(),
'sort_order': sort_order_spin.value(),
'enabled': enabled_check.isChecked()
}
def set_form_values(self, operation_type, values):
"""设置表单值
Args:
operation_type: 操作类型 (output)
values: 表单值
"""
widget = self.output_widget
type_name_input = widget.findChild(QLineEdit, f"{operation_type}_type_name_input")
desc_input = widget.findChild(QLineEdit, f"{operation_type}_desc_input")
sort_order_spin = widget.findChild(QSpinBox, f"{operation_type}_sort_order_spin")
enabled_check = widget.findChild(QCheckBox, f"{operation_type}_enabled_check")
type_name_input.setText(values.get('type_name', ''))
desc_input.setText(values.get('description', ''))
sort_order_spin.setValue(values.get('sort_order', 100))
enabled_check.setChecked(values.get('enabled', True))
def reset_form(self, operation_type):
"""重置表单
Args:
operation_type: 操作类型 (output)
"""
widget = self.output_widget
# 重置表单值
self.set_form_values(operation_type, {
'type_name': '',
'description': '',
'sort_order': 100,
'enabled': True
})
# 重置当前编辑ID
widget.setProperty("current_edit_id", -1)
def handle_table_selection(self, operation_type):
"""处理表格选择事件
Args:
operation_type: 操作类型 (output)
"""
# 获取表格
table = self.get_table_by_operation_type(operation_type)
if not table:
return
# 获取选中行
selected_items = table.selectedItems()
if not selected_items:
return
# 获取行数据
row = selected_items[0].row()
# 获取ID
id_item = table.item(row, 0)
if not id_item:
return
pallet_type_id = id_item.data(Qt.UserRole)
# 获取托盘类型数据
pallet_type = self.pallet_type_manager.get_pallet_type_by_id(pallet_type_id)
if not pallet_type:
return
# 设置表单值
self.set_form_values(operation_type, pallet_type)
# 设置当前编辑ID
widget = self.output_widget
widget.setProperty("current_edit_id", pallet_type_id)
def validate_form(self, operation_type):
"""验证表单
Args:
operation_type: 操作类型 (output)
Returns:
bool: 验证是否通过
"""
values = self.get_form_values(operation_type)
if not values['type_name']:
QMessageBox.warning(self, "警告", "请输入类型名称")
return False
return True
def add_pallet_type(self, operation_type):
"""添加托盘类型
Args:
operation_type: 操作类型 (output)
"""
# 验证表单
if not self.validate_form(operation_type):
return
# 获取表单值
values = self.get_form_values(operation_type)
# 添加托盘类型
success = self.pallet_type_manager.add_pallet_type(values)
if success:
# 重新加载数据
self.load_operation_pallet_types(operation_type)
# 重置表单
self.reset_form(operation_type)
self.load_pallet_types()
# 发送信号
self.signal_pallet_types_changed.emit()
self.settings_changed.emit() # 发送设置变更信号
self.settings_changed.emit()
logging.info(f"已添加{operation_type}托盘类型: {values['type_name']}")
else:
QMessageBox.critical(self, "错误", f"添加{operation_type}托盘类型失败")
logging.error(f"添加{operation_type}托盘类型失败: {values['type_name']}")
def update_pallet_type(self, operation_type):
"""更新托盘类型
Args:
operation_type: 操作类型 (output)
"""
# 获取当前编辑ID
widget = self.output_widget
pallet_type_id = widget.property("current_edit_id")
if pallet_type_id < 0:
QMessageBox.warning(self, "警告", "请先选择要编辑的托盘类型")
return
# 验证表单
if not self.validate_form(operation_type):
return
# 获取表单值
values = self.get_form_values(operation_type)
# 更新托盘类型
success = self.pallet_type_manager.update_pallet_type(pallet_type_id, values)
if success:
# 重新加载数据
self.load_operation_pallet_types(operation_type)
# 重置表单
self.reset_form(operation_type)
# 发送信号
self.signal_pallet_types_changed.emit()
self.settings_changed.emit() # 发送设置变更信号
logging.info(f"已更新{operation_type}托盘类型: {values['type_name']}")
else:
QMessageBox.critical(self, "错误", f"更新{operation_type}托盘类型失败")
logging.error(f"更新{operation_type}托盘类型失败: {values['type_name']}")
def delete_pallet_type(self, operation_type):
"""删除托盘类型
Args:
operation_type: 操作类型 (output)
"""
# 获取选中行
table = self.get_table_by_operation_type(operation_type)
if not table:
return
selected_items = table.selectedItems()
if not selected_items:
QMessageBox.warning(self, "警告", "请先选择要删除的托盘类型")
return
# 获取托盘类型ID
row = selected_items[0].row()
id_item = table.item(row, 0)
if not id_item:
return
pallet_type_id = id_item.data(Qt.UserRole)
type_name = id_item.text()
# 确认删除
reply = QMessageBox.question(self, "确认删除", f"确定要删除{operation_type}托盘类型 [{type_name}] 吗?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply != QMessageBox.Yes:
return
# 删除托盘类型
success = self.pallet_type_manager.delete_pallet_type(pallet_type_id)
if success:
# 重新加载数据
self.load_operation_pallet_types(operation_type)
# 重置表单
self.reset_form(operation_type)
# 发送信号
self.signal_pallet_types_changed.emit()
self.settings_changed.emit() # 发送设置变更信号
logging.info(f"已删除{operation_type}托盘类型: {type_name}")
else:
QMessageBox.critical(self, "错误", f"删除{operation_type}托盘类型失败")
logging.error(f"删除{operation_type}托盘类型失败: {type_name}")
def cancel_edit(self, operation_type):
"""取消编辑
Args:
operation_type: 操作类型 (output)
"""
# 重置表单
self.reset_form(operation_type)
# 清除表格选择
table = self.get_table_by_operation_type(operation_type)
if table:
table.clearSelection()
# 提示成功
QMessageBox.information(self, "成功", f"已添加托盘类型: {pallet_name}")
except Exception as e:
logging.error(f"添加托盘类型失败: {str(e)}")
QMessageBox.critical(self, "错误", f"添加托盘类型失败: {str(e)}")
def toggle_pallet_type(self, pallet_type_id, enabled):
"""切换托盘类型启用状态
Args:
pallet_type_id: 托盘类型ID
enabled: 是否启用
"""
# 更新托盘类型启用状态
success = self.pallet_type_manager.update_pallet_type_status(pallet_type_id, enabled)
if success:
# 发送信号
self.signal_pallet_types_changed.emit()
self.settings_changed.emit() # 发送设置变更信号
def update_pallet_type(self):
"""更新托盘类型"""
try:
# 获取输入值
pallet_id_text = self.pallet_id_input.text().strip()
pallet_name = self.pallet_name_input.text().strip()
logging.info(f"{('启用' if enabled else '禁用')}托盘类型: {pallet_type_id}")
else:
QMessageBox.critical(self, "错误", f"更新托盘类型状态失败")
logging.error(f"更新托盘类型状态失败: {pallet_type_id}")
# 验证输入
if not pallet_id_text or not pallet_name:
QMessageBox.warning(self, "警告", "托盘ID和托盘名称不能为空")
return
def save_all_pallet_types(self):
"""保存所有托盘类型"""
# 保存所有托盘类型
success = self.pallet_type_manager.save_all_pallet_types()
if success:
QMessageBox.information(self, "成功", "所有托盘类型已保存")
# 检查ID是否为数字
try:
pallet_id = int(pallet_id_text)
except ValueError:
QMessageBox.warning(self, "警告", "托盘ID必须为数字")
return
# 更新托盘类型
result = self.pallet_type_manager.update_pallet_type(pallet_id, pallet_name)
if not result:
QMessageBox.critical(self, "错误", "更新托盘类型失败")
return
# 重新加载数据
self.load_pallet_types()
# 发送信号
self.signal_pallet_types_changed.emit()
self.settings_changed.emit() # 发送设置变更信号
self.settings_changed.emit()
logging.info("已保存所有托盘类型")
else:
QMessageBox.critical(self, "错误", "保存托盘类型失败")
logging.error("保存托盘类型失败")
# 提示成功
QMessageBox.information(self, "成功", f"已更新托盘类型: {pallet_name}")
except Exception as e:
logging.error(f"更新托盘类型失败: {str(e)}")
QMessageBox.critical(self, "错误", f"更新托盘类型失败: {str(e)}")
def delete_pallet_type(self):
"""删除托盘类型"""
try:
# 获取输入值
pallet_id_text = self.pallet_id_input.text().strip()
# 验证输入
if not pallet_id_text:
QMessageBox.warning(self, "警告", "请先选择要删除的托盘类型")
return
# 检查ID是否为数字
try:
pallet_id = int(pallet_id_text)
except ValueError:
QMessageBox.warning(self, "警告", "托盘ID必须为数字")
return
# 确认删除
reply = QMessageBox.question(
self,
"确认删除",
f"确定要删除托盘ID为 {pallet_id} 的托盘类型吗?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply != QMessageBox.Yes:
return
# 删除托盘类型
result = self.pallet_type_manager.delete_pallet_type_by_id(pallet_id)
if not result:
QMessageBox.critical(self, "错误", "删除托盘类型失败")
return
# 重新加载数据
self.load_pallet_types()
# 发送信号
self.signal_pallet_types_changed.emit()
self.settings_changed.emit()
# 提示成功
QMessageBox.information(self, "成功", f"已删除托盘类型: {pallet_id}")
except Exception as e:
logging.error(f"删除托盘类型失败: {str(e)}")
QMessageBox.critical(self, "错误", f"删除托盘类型失败: {str(e)}")
def cancel_edit(self):
"""取消编辑"""
# 清空输入框
self.pallet_id_input.clear()
self.pallet_name_input.clear()
# 此方法不再需要,因为相关按钮已被删除
# def save_all(self):
# """保存所有配置(实际上是空操作,因为每次修改都会立即保存)"""
# QMessageBox.information(self, "成功", "所有配置已保存")

View File

@ -0,0 +1,141 @@
import logging
import json
import os
from PySide6.QtWidgets import QWidget, QMessageBox
from PySide6.QtCore import Signal
from utils.config_loader import ConfigLoader
class PLCSettingsWidget(QWidget):
"""PLC设置控制器"""
# 定义信号
settings_changed = Signal() # 设置变更信号
def __init__(self, parent=None):
"""初始化控制器
Args:
parent: 父窗口
"""
super().__init__(parent)
self.parent = parent
self.config = ConfigLoader.get_instance()
# 获取UI控件引用
self.modbus_host_input = parent.modbus_host_input
self.modbus_port_input = parent.modbus_port_input
self.modbus_test_button = parent.modbus_test_button
self.plc_save_button = parent.plc_save_button
# 连接信号和槽
self.connect_signals()
# 加载配置
self.load_config()
def connect_signals(self):
"""连接信号和槽"""
self.modbus_test_button.clicked.connect(self.test_modbus_connection)
self.plc_save_button.clicked.connect(self.save_config)
def load_config(self):
"""加载配置"""
try:
# 加载Modbus配置
host = self.config.get_value("modbus.host")
port = self.config.get_value("modbus.port")
self.modbus_host_input.setText(host if host else "localhost")
self.modbus_port_input.setText(str(port) if port else "502")
logging.info("已加载PLC配置")
except Exception as e:
logging.error(f"加载PLC配置失败: {str(e)}")
QMessageBox.critical(self, "错误", f"加载PLC配置失败: {str(e)}")
def save_config(self):
"""保存配置"""
try:
# 获取输入值
host = self.modbus_host_input.text().strip()
port = self.modbus_port_input.text().strip()
# 验证输入
if not host:
QMessageBox.warning(self, "警告", "请输入Modbus主机地址")
return
if not port:
QMessageBox.warning(self, "警告", "请输入Modbus端口")
return
try:
port_int = int(port)
if port_int < 1 or port_int > 65535:
QMessageBox.warning(self, "警告", "端口号必须在1-65535之间")
return
except ValueError:
QMessageBox.warning(self, "警告", "端口号必须是整数")
return
# 更新配置
self.config.set_value("modbus.host", host)
self.config.set_value("modbus.port", port)
self.config.save_config()
# 发送信号
self.settings_changed.emit()
# 提示成功
QMessageBox.information(self, "成功", "PLC配置已保存")
logging.info(f"已保存PLC配置: host={host}, port={port}")
except Exception as e:
logging.error(f"保存PLC配置失败: {str(e)}")
QMessageBox.critical(self, "错误", f"保存PLC配置失败: {str(e)}")
def test_modbus_connection(self):
"""测试Modbus连接"""
try:
# 获取输入值
host = self.modbus_host_input.text().strip()
port = self.modbus_port_input.text().strip()
# 验证输入
if not host:
QMessageBox.warning(self, "警告", "请输入Modbus主机地址")
return
if not port:
QMessageBox.warning(self, "警告", "请输入Modbus端口")
return
try:
port_int = int(port)
except ValueError:
QMessageBox.warning(self, "警告", "端口号必须是整数")
return
# 导入ModbusUtils
from utils.modbus_utils import ModbusUtils
# 创建Modbus连接
modbus = ModbusUtils(host=host, port=port_int)
client = modbus.get_client()
if client:
QMessageBox.information(self, "成功", f"Modbus连接成功: {host}:{port}")
logging.info(f"Modbus连接测试成功: {host}:{port}")
else:
QMessageBox.warning(self, "警告", f"Modbus连接失败: {host}:{port}")
logging.warning(f"Modbus连接测试失败: {host}:{port}")
# 关闭连接
modbus.close_client(client)
except Exception as e:
logging.error(f"测试Modbus连接失败: {str(e)}")
QMessageBox.critical(self, "错误", f"测试Modbus连接失败: {str(e)}")
def on_settings_changed(self):
"""设置变更处理"""
# 重新加载配置
self.load_config()

View File

@ -0,0 +1,76 @@
"""
刷新相机设备按钮修复工具
此文件包含修复相机设备刷新按钮的工具函数
"""
import logging
from PySide6.QtWidgets import QPushButton
def fix_camera_refresh_button(settings_widget):
"""
修复相机设置中的刷新设备按钮
Args:
settings_widget: 设置窗口实例
Returns:
bool: 是否成功修复
"""
try:
logging.info("尝试修复相机刷新设备按钮...")
# 检查是否存在相机设置组件
if not hasattr(settings_widget, 'camera_settings'):
logging.error("设置窗口中没有camera_settings属性")
return False
# 检查是否存在刷新按钮
if hasattr(settings_widget, 'refresh_button'):
refresh_button = settings_widget.refresh_button
logging.info(f"在settings_widget中找到刷新按钮: {refresh_button}")
else:
# 尝试查找刷新按钮
refresh_button = None
# 方法1: 直接在camera_tab中查找
if hasattr(settings_widget, 'camera_tab'):
for child in settings_widget.camera_tab.findChildren(QPushButton):
if child.text() == "刷新设备":
refresh_button = child
logging.info(f"在camera_tab中找到刷新按钮: {refresh_button}")
break
# 方法2: 在camera_settings中查找
if refresh_button is None and hasattr(settings_widget.camera_settings, 'refresh_button'):
refresh_button = settings_widget.camera_settings.refresh_button
logging.info(f"在camera_settings中找到刷新按钮: {refresh_button}")
# 如果找到了刷新按钮,则绑定事件
if refresh_button:
# 断开所有现有连接
try:
refresh_button.clicked.disconnect()
except:
pass
# 连接到refresh_devices方法
refresh_button.clicked.connect(settings_widget.camera_settings.refresh_devices)
logging.info("成功绑定刷新按钮到refresh_devices方法")
# 测试调用一次
try:
settings_widget.camera_settings.refresh_devices()
logging.info("已手动调用refresh_devices方法初始化设备列表")
except Exception as e:
logging.error(f"调用refresh_devices失败: {str(e)}")
return True
else:
logging.error("未找到刷新设备按钮")
return False
except Exception as e:
logging.error(f"修复相机刷新按钮时发生错误: {str(e)}")
import traceback
logging.error(traceback.format_exc())
return False

View File

@ -7,6 +7,7 @@ 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):
"""串口设置组件"""
@ -32,6 +33,10 @@ class SerialSettingsWidget(SerialSettingsUI):
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)
@ -46,9 +51,18 @@ class SerialSettingsWidget(SerialSettingsUI):
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())
@ -56,15 +70,40 @@ class SerialSettingsWidget(SerialSettingsUI):
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:
@ -155,6 +194,39 @@ class SerialSettingsWidget(SerialSettingsUI):
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}")
@ -180,7 +252,6 @@ class SerialSettingsWidget(SerialSettingsUI):
mdz_query_interval = self.mdz_query_interval.value()
mdz_config = {
'ser': mdz_port,
'port': mdz_baud,
'data_bits': mdz_data_bits,
'stop_bits': mdz_stop_bits,
@ -189,6 +260,10 @@ class SerialSettingsWidget(SerialSettingsUI):
'query_interval': mdz_query_interval
}
# 只有当用户选择了串口时才保存串口配置
if mdz_port:
mdz_config['ser'] = mdz_port
self.config.set_config('mdz', mdz_config)
# 保存线径设置
@ -199,17 +274,37 @@ class SerialSettingsWidget(SerialSettingsUI):
xj_parity = self.xj_parity_combo.currentData()
xj_config = {
'ser': xj_port,
'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()
@ -224,15 +319,16 @@ class SerialSettingsWidget(SerialSettingsUI):
"""测试米电阻串口"""
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 not port:
QMessageBox.warning(self, "测试失败", "请选择串口")
return
# 关闭可能已经打开的串口
if self.serial_manager.is_port_open(port):
self.serial_manager.close_port(port)
@ -248,44 +344,46 @@ class SerialSettingsWidget(SerialSettingsUI):
if query_cmd:
try:
# 转换查询指令为字节
query_bytes = bytes.fromhex(query_cmd.replace(' ', ''))
cmd_bytes = bytes.fromhex(query_cmd.replace(' ', ''))
self.serial_manager.write_data(port, cmd_bytes)
time.sleep(0.1) # 等待响应
# 发送查询指令
self.serial_manager.write_data(port, query_bytes)
# 等待一段时间
time.sleep(0.5)
# 关闭串口
self.serial_manager.close_port(port)
QMessageBox.information(self, "测试成功", f"米电阻串口 {port} 测试成功,已发送查询指令")
# 读取响应
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:
self.serial_manager.close_port(port)
QMessageBox.warning(self, "测试失败", f"发送查询指令失败: {e}")
QMessageBox.warning(self, "测试结果", f"串口打开成功,但发送指令失败: {e}")
else:
QMessageBox.information(self, "测试成功", "串口打开成功")
# 关闭串口
self.serial_manager.close_port(port)
QMessageBox.information(self, "测试成功", f"米电阻串口 {port} 打开成功,但未发送查询指令")
else:
QMessageBox.warning(self, "测试失败", f"无法打开米电阻串口 {port}")
QMessageBox.critical(self, "测试失败", f"无法打开串口 {port}")
except Exception as e:
logging.error(f"测试米电阻串口失败: {e}")
QMessageBox.warning(self, "测试失败", 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 not port:
QMessageBox.warning(self, "测试失败", "请选择串口")
return
# 关闭可能已经打开的串口
if self.serial_manager.is_port_open(port):
self.serial_manager.close_port(port)
@ -296,15 +394,90 @@ class SerialSettingsWidget(SerialSettingsUI):
)
if success:
# 等待一段时间
time.sleep(2)
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)
QMessageBox.information(self, "测试成功", f"线径串口 {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.warning(self, "测试失败", f"无法打开线径串口 {port}")
QMessageBox.critical(self, "测试失败", f"无法打开串口 {port}")
except Exception as e:
logging.error(f"测试线径串口失败: {e}")
QMessageBox.warning(self, "测试失败", f"测试线径串口失败: {e}")
logging.error(f"测试扫码器串口失败: {e}")
QMessageBox.critical(self, "测试失败", f"测试扫码器串口失败: {e}")

View File

@ -1,57 +0,0 @@
import logging
try:
from PySide6.QtWidgets import QDialog, QVBoxLayout, QTabWidget, QDialogButtonBox
from PySide6.QtCore import Qt
except ImportError:
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTabWidget, QDialogButtonBox
from PyQt5.QtCore import Qt
class SettingsDialog(QDialog):
"""设置对话框,用于显示和管理各种设置页面"""
def __init__(self, parent=None):
super().__init__(parent)
# 设置对话框标题和大小
self.setWindowTitle("系统设置")
self.resize(800, 600)
# 创建布局
self.layout = QVBoxLayout(self)
# 创建选项卡控件
self.tab_widget = QTabWidget()
self.layout.addWidget(self.tab_widget)
# 创建按钮盒
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
self.layout.addWidget(self.button_box)
# 设置窗口模态
self.setModal(True)
logging.info("设置对话框已创建")
def add_settings_page(self, widget, title):
"""添加设置页面
Args:
widget: 设置页面部件
title: 页面标题
"""
self.tab_widget.addTab(widget, title)
logging.info(f"已添加设置页面: {title}")
def accept(self):
"""确认按钮处理"""
logging.info("设置已保存")
super().accept()
def reject(self):
"""取消按钮处理"""
logging.info("设置已取消")
super().reject()

View File

@ -1,5 +1,5 @@
from PySide6.QtWidgets import QMessageBox, QVBoxLayout
from PySide6.QtCore import Qt
from PySide6.QtCore import Qt, Signal
import logging
import json
import os
@ -7,19 +7,106 @@ from ui.settings_ui import SettingsUI
from utils.sql_utils import SQLUtils
from widgets.inspection_settings_widget import InspectionSettingsWidget
from widgets.pallet_type_settings_widget import PalletTypeSettingsWidget
from widgets.plc_settings_widget import PLCSettingsWidget
from utils.config_loader import ConfigLoader
from utils.app_mode import AppMode
from widgets.camera_settings_widget import CameraSettingsWidget
from widgets.serial_settings_widget import SerialSettingsWidget
from ui.electricity_settings_ui import ElectricitySettingsUI
class SettingsWidget(SettingsUI):
# 定义信号
settings_changed = Signal()
def __init__(self, parent=None):
"""初始化设置窗口"""
super().__init__(parent)
self.parent = parent
logging.info("正在初始化SettingsWidget")
# 创建检验设置部件
logging.info("创建InspectionSettingsWidget实例")
self.inspection_settings = InspectionSettingsWidget()
# 先检查UI中的相机下拉框
if hasattr(self, 'camera_combo') and self.camera_combo is not None:
logging.info(f"相机下拉框状态检查: 可见={self.camera_combo.isVisible()}, "
f"尺寸={self.camera_combo.size().width()}x{self.camera_combo.size().height()}, "
f"包含项={self.camera_combo.count()}")
else:
logging.error("无法找到相机下拉框(camera_combo)!")
# 检查刷新按钮
if hasattr(self, 'refresh_button') and self.refresh_button is not None:
logging.info(f"刷新按钮状态检查: 可见={self.refresh_button.isVisible()}, "
f"尺寸={self.refresh_button.size().width()}x{self.refresh_button.size().height()}")
else:
logging.error("无法找到刷新按钮(refresh_button)!")
# 创建子设置控制器
try:
# 先添加一个测试项到下拉框
if hasattr(self, 'camera_combo'):
self.camera_combo.clear()
self.camera_combo.addItem("测试项 - 初始化前")
logging.info(f"已添加测试项到下拉框,当前项数={self.camera_combo.count()}")
self.camera_settings = CameraSettingsWidget(self)
logging.info("相机设置组件创建成功")
# 注意不再手动调用connect_signals因为它已经在CameraSettingsWidget的__init__中调用了
logging.info("相机设置组件已在其初始化时连接信号,不再重复连接")
# 添加后处理:直接给刷新按钮添加事件处理
if hasattr(self, 'refresh_button'):
logging.info("手动绑定SettingsWidget中的刷新按钮点击事件")
try:
self.refresh_button.clicked.disconnect() # 断开所有现有连接
except Exception as e:
logging.warning(f"断开刷新按钮现有连接时出错: {e}")
self.refresh_button.clicked.connect(self.camera_settings.refresh_devices)
# 立即调用一次刷新方法
self.camera_settings.refresh_devices()
logging.info("已刷新相机设备列表")
# 再次检查相机下拉框状态
if hasattr(self, 'camera_combo'):
logging.info(f"刷新后相机下拉框状态: 项目数={self.camera_combo.count()}, "
f"当前文本={self.camera_combo.currentText() if self.camera_combo.count() > 0 else 'None'}")
# 尝试手动向下拉框添加一项
self.camera_combo.addItem("测试项 - 手动添加")
logging.info(f"手动添加后下拉框状态: 项目数={self.camera_combo.count()}")
except Exception as e:
logging.error(f"初始化相机设置组件失败: {e}")
self.serial_settings = SerialSettingsWidget(self)
self.inspection_settings = InspectionSettingsWidget(self)
self.pallet_type_settings = PalletTypeSettingsWidget(self)
self.plc_settings = PLCSettingsWidget(self)
# 创建电力监控设置组件
try:
self.electricity_settings = ElectricitySettingsUI(self)
logging.info("电力监控设置组件创建成功")
# 移除临时占位符标签并添加电力监控设置部件
if hasattr(self, 'electricity_placeholder'):
logging.info("移除电力监控临时占位符")
self.electricity_layout.removeWidget(self.electricity_placeholder)
self.electricity_placeholder.hide()
self.electricity_placeholder.deleteLater()
else:
logging.warning("未找到电力监控临时占位符标签")
# 检查布局是否可用
if hasattr(self, 'electricity_layout'):
logging.info("添加电力监控设置部件到布局")
self.electricity_layout.addWidget(self.electricity_settings)
else:
logging.error("无法找到electricity_layout布局")
except Exception as e:
logging.error(f"初始化电力监控设置组件失败: {e}")
# 移除临时占位符标签并添加检验设置部件
if hasattr(self, 'inspection_placeholder'):
@ -60,7 +147,7 @@ class SettingsWidget(SettingsUI):
# 加载配置文件
self.config_loader = ConfigLoader.get_instance()
# 连接信号和槽
# 连接信号
self.connect_signals()
# 初始化数据库类型UI状态
@ -72,6 +159,30 @@ class SettingsWidget(SettingsUI):
logging.info("SettingsWidget初始化完成")
def connect_signals(self):
"""连接信号和槽"""
# 连接子设置控制器的信号
if hasattr(self, 'camera_settings'):
try:
self.camera_settings.settings_changed.connect(self.on_settings_changed)
except Exception as e:
logging.error(f"连接相机设置信号时出错: {e}")
self.serial_settings.settings_changed.connect(self.on_settings_changed)
self.inspection_settings.settings_changed.connect(self.on_settings_changed)
self.pallet_type_settings.settings_changed.connect(self.on_settings_changed)
self.plc_settings.settings_changed.connect(self.on_settings_changed)
# 连接电力监控设置信号
if hasattr(self, 'electricity_settings'):
try:
self.electricity_settings.settings_changed.connect(self.on_settings_changed)
logging.info("已连接电力监控设置信号")
except Exception as e:
logging.error(f"连接电力监控设置信号时出错: {e}")
# 不再在这里连接刷新按钮,避免重复连接
logging.info("刷新按钮已在初始化时连接,不再重复连接")
# 数据库类型选择
self.db_type_combo.currentTextChanged.connect(self.update_db_ui_state)
@ -353,4 +464,10 @@ class SettingsWidget(SettingsUI):
def back_to_main(self):
"""返回主页"""
if self.parent and hasattr(self.parent, 'show_main_page'):
self.parent.show_main_page()
self.parent.show_main_page()
def on_settings_changed(self):
"""处理设置变更信号"""
logging.info("设置已变更发送settings_changed信号")
# 发送信号
self.settings_changed.emit()

View File

@ -3,6 +3,7 @@ from PySide6.QtCore import Signal
from PySide6.QtWidgets import QDialog, QVBoxLayout, QDialogButtonBox
from widgets.serial_settings_widget import SerialSettingsWidget
from widgets.settings_widget import SettingsWidget
from ui.electricity_settings_ui import ElectricitySettingsUI
class SettingsWindow(QDialog):
"""设置窗口直接使用SettingsWidget中的标签页"""
@ -31,6 +32,9 @@ class SettingsWindow(QDialog):
self.serial_settings = SerialSettingsWidget(self)
self.settings_widget.tab_widget.addTab(self.serial_settings, "串口设置")
# 应用相机刷新按钮修复
self._fix_camera_refresh_button()
# 添加按钮
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
@ -41,6 +45,64 @@ class SettingsWindow(QDialog):
self.serial_settings.settings_changed.connect(self.settings_changed.emit)
logging.info("SettingsWindow初始化完成")
def _fix_camera_refresh_button(self):
"""内联实现的刷新按钮修复逻辑,用于导入模块失败的情况"""
try:
logging.info("使用内联方法修复相机刷新按钮...")
# 获取设置部件
settings_widget = self.settings_widget
# 确保相机设置组件存在
if not hasattr(settings_widget, 'camera_settings'):
logging.error("设置窗口中没有camera_settings属性")
return
# 查找刷新按钮
refresh_button = None
# 方法1: 在settings_widget上查找
if hasattr(settings_widget, 'refresh_button'):
refresh_button = settings_widget.refresh_button
logging.info("在settings_widget中找到刷新按钮")
# 方法2: 在camera_tab中查找
elif hasattr(settings_widget, 'camera_tab'):
from PySide6.QtWidgets import QPushButton
for child in settings_widget.camera_tab.findChildren(QPushButton):
if child.text() == "刷新设备":
refresh_button = child
logging.info("在camera_tab中找到刷新按钮")
break
# 方法3: 在camera_settings中查找
elif hasattr(settings_widget.camera_settings, 'refresh_button'):
refresh_button = settings_widget.camera_settings.refresh_button
logging.info("在camera_settings中找到刷新按钮")
# 如果找到按钮,则绑定事件
if refresh_button:
from PySide6.QtCore import QObject
if isinstance(refresh_button, QObject):
try:
# 断开现有连接
refresh_button.clicked.disconnect()
except:
pass
# 连接到刷新方法
refresh_button.clicked.connect(settings_widget.camera_settings.refresh_devices)
logging.info("成功绑定刷新按钮到refresh_devices方法")
# 手动调用一次
settings_widget.camera_settings.refresh_devices()
logging.info("已手动调用refresh_devices初始化设备列表")
else:
logging.error(f"刷新按钮不是QObject: {type(refresh_button)}")
else:
logging.error("未找到刷新设备按钮")
except Exception as e:
logging.error(f"内联修复相机刷新按钮时发生错误: {str(e)}")
def accept(self):
"""确认按钮处理,保存所有设置并发送设置变更信号"""