Compare commits
23 Commits
38c29de69f
...
ef1a624099
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef1a624099 | ||
|
|
f53c356c19 | ||
|
|
5b7a0bb439 | ||
|
|
343178c1a3 | ||
|
|
3e8ee647eb | ||
|
|
7457cd8b4c | ||
|
|
ce8483d298 | ||
|
|
ead4cfcc95 | ||
|
|
7023d7561a | ||
|
|
336ba26bc8 | ||
|
|
7da216ec58 | ||
|
|
486cdb0cad | ||
|
|
7a2351cbe6 | ||
|
|
145a350fb8 | ||
|
|
92905294aa | ||
|
|
6d60e77743 | ||
|
|
11c6b15d3e | ||
|
|
5c5a8f44e8 | ||
|
|
968b4108b9 | ||
|
|
074b656bbc | ||
|
|
320d715f83 | ||
|
|
77c6f9f480 | ||
|
|
99bca76e08 |
112
README.md
Normal file
112
README.md
Normal file
@ -0,0 +1,112 @@
|
||||
# 腾智微丝产线包装系统
|
||||
|
||||
## 项目架构分析
|
||||
|
||||
这是一个基于PySide6(Qt for Python)开发的腾智微丝产线包装系统,采用了典型的MVC(模型-视图-控制器)架构:
|
||||
|
||||
1. **模型层(Model)**:
|
||||
- 使用DAO(数据访问对象)模式访问数据库
|
||||
- 支持多种数据库(SQLite、PostgreSQL、MySQL)
|
||||
- 主要数据表包括用户表、检验配置表、检验数据表、电力消耗表等
|
||||
|
||||
2. **视图层(View)**:
|
||||
- 使用PySide6构建UI界面
|
||||
- 主要界面包括登录界面、主窗口、设置界面等
|
||||
- 采用分离的UI类设计,如LoginUI、MainWindowUI等
|
||||
|
||||
3. **控制器层(Controller)**:
|
||||
- 主要控制逻辑在widgets目录下的类中实现
|
||||
- 使用信号槽机制进行组件间通信
|
||||
- 采用单例模式管理全局资源
|
||||
|
||||
4. **工具层(Utils)**:
|
||||
- 提供各种工具类,如配置加载器、Modbus通信、串口管理等
|
||||
- 采用单例模式确保资源共享
|
||||
|
||||
## 技术栈
|
||||
|
||||
1. **前端技术**:
|
||||
- PySide6(Qt 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/`目录下创建对应的控制器类
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
124
dao/electricity_dao.py
Normal 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 []
|
||||
@ -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 {}
|
||||
|
||||
@ -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):
|
||||
"""保存托盘档案
|
||||
|
||||
|
||||
BIN
db/jtDB.db
BIN
db/jtDB.db
Binary file not shown.
@ -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 (
|
||||
|
||||
@ -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
21
main.py
@ -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
20
requirements.txt
Normal 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
2973
tests/main_window_old.py
Normal file
File diff suppressed because it is too large
Load Diff
231
ui/electricity_settings_ui.py
Normal file
231
ui/electricity_settings_ui.py
Normal 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)}")
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
193
utils/electricity_monitor.py
Normal file
193
utils/electricity_monitor.py
Normal 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
|
||||
@ -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')
|
||||
|
||||
|
||||
@ -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):
|
||||
"""通知所有注册的处理器"""
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -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()
|
||||
@ -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
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
@ -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)
|
||||
|
||||
@ -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
@ -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, "成功", "所有配置已保存")
|
||||
|
||||
141
widgets/plc_settings_widget.py
Normal file
141
widgets/plc_settings_widget.py
Normal 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()
|
||||
76
widgets/refresh_devices_fix.py
Normal file
76
widgets/refresh_devices_fix.py
Normal 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
|
||||
@ -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}")
|
||||
@ -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()
|
||||
@ -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()
|
||||
@ -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):
|
||||
"""确认按钮处理,保存所有设置并发送设置变更信号"""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user