From 268a1cc16b025e7a7fc6309b022585f0e82bf776 Mon Sep 17 00:00:00 2001 From: zhu-mengmeng <15588200382@163.com> Date: Tue, 22 Jul 2025 18:34:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=A3=80=E9=AA=8C?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=9F=A5=E8=AF=A2=E6=8E=A5=E5=8F=A3=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=BB=E7=AA=97=E5=8F=A3=E9=98=B2=E6=8A=96?= =?UTF-8?q?=E6=9C=BA=E5=88=B6=E5=8F=8A=E6=95=B0=E6=8D=AE=E5=A4=84=E7=90=86?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dao/inspection_dao.py | 53 +++- utils/modbus_monitor.py | 2 +- widgets/main_window.py | 639 ++++++++++++++++++++++++++-------------- 3 files changed, 464 insertions(+), 230 deletions(-) diff --git a/dao/inspection_dao.py b/dao/inspection_dao.py index eda0d20..436f000 100644 --- a/dao/inspection_dao.py +++ b/dao/inspection_dao.py @@ -1138,4 +1138,55 @@ class InspectionDAO: return True except Exception as e: logging.error(f"删除包装记录失败: {str(e)}") - return False \ No newline at end of file + return False + def get_inspection_data_by_config(self, order_id, gc_note, tray_id, position, config_id): + """根据工程号、托盘号、位置和配置ID查询检验数据 + + Args: + order_id: 订单号 + gc_note: 工程号 + tray_id: 托盘号 + position: 位置序号 + config_id: 配置ID + + Returns: + dict: 检验数据记录,如果不存在则返回None + """ + try: + # 使用SQLUtils获取数据库连接 + sql = """ + SELECT id, order_id, gc_note, position, config_id, value, status, remark, tray_id, create_time, update_time + FROM wsbz_inspection_data + WHERE order_id = ? AND gc_note = ? AND tray_id = ? AND position = ? AND config_id = ? + ORDER BY update_time DESC + LIMIT 1 + """ + + with SQLUtils('sqlite', database='db/jtDB.db') as db: + # 执行查询 + db.cursor.execute(sql, (order_id, gc_note, tray_id, position, config_id)) + + # 获取结果 + row = db.cursor.fetchone() + + # 如果有结果,转换为字典 + if row: + return { + 'id': row[0], + 'order_id': row[1], + 'gc_note': row[2], + 'position': row[3], + 'config_id': row[4], + 'value': row[5], + 'status': row[6], + 'remark': row[7], + 'tray_id': row[8], + 'create_time': row[9], + 'update_time': row[10] + } + else: + return None + + except Exception as e: + logging.error(f"查询检验数据失败: {str(e)}") + return None \ No newline at end of file diff --git a/utils/modbus_monitor.py b/utils/modbus_monitor.py index d5d6a2d..23e5629 100644 --- a/utils/modbus_monitor.py +++ b/utils/modbus_monitor.py @@ -48,7 +48,7 @@ class ModbusMonitor(QObject): register_error = Signal(int, str) monitor_status_changed = Signal(bool, str) - def __init__(self, polling_interval=1.0, max_errors=3, retry_interval=5.0): + def __init__(self, polling_interval=0.5, max_errors=3, retry_interval=5.0): """ 初始化Modbus监控器 diff --git a/widgets/main_window.py b/widgets/main_window.py index e6abcda..13e0cd0 100644 --- a/widgets/main_window.py +++ b/widgets/main_window.py @@ -911,6 +911,20 @@ class MainWindow(MainWindowUI): # 获取当前输入的工程号 gc_note = self.order_edit.text().strip() if gc_note: + # 防抖机制:记录上次处理的工程号和时间 + current_time = time.time() + last_order_time = getattr(self, '_last_order_time', 0) + last_order_gc = getattr(self, '_last_order_gc', '') + + # 如果是相同的工程号且间隔小于1秒,则忽略此次处理 + if last_order_gc == gc_note and current_time - last_order_time < 1.0: + logging.info(f"忽略重复处理工程号: {gc_note},间隔太短") + return + + # 更新最后处理的工程号和时间 + self._last_order_gc = gc_note + self._last_order_time = current_time + logging.info(f"输入的工程号: {gc_note}") #判断是否是接口,如果不是接口直接添加如果是则走接口 # 如果开启接口模式,则需要调用接口同步到业务库 @@ -974,11 +988,19 @@ class MainWindow(MainWindowUI): self.update_info_table(order_info) self.add_new_inspection_row(gc_note, self._current_order_code) else: + # 直接添加新行 self.add_new_inspection_row(gc_note, self._current_order_code) - + + # 清空工程号输入框 + self.order_edit.clear() + + # 将光标重新定位到工程号输入框 + self.order_edit.setFocus() + + # 更新订单数量和产量统计数据 + self.update_order_statistics() else: - logging.warning("工程号为空") - QMessageBox.warning(self, "输入提示", "请输入有效的工程号") + logging.warning("工程号为空,忽略处理") def handle_virtual_order(self): """处理虚拟工程号按钮点击事件""" @@ -1009,26 +1031,6 @@ class MainWindow(MainWindowUI): logging.error(f"生成虚拟工程号失败: {str(e)}") QMessageBox.critical(self, "错误", f"生成虚拟工程号失败: {str(e)}") - def test_rapid_scanning(self): - """测试快速连续扫码的场景,确保最新扫码的工程号始终在最后""" - logging.info("开始测试快速连续扫码场景") - - # 模拟扫码工程号A - logging.info("模拟扫码工程号A") - self.order_edit.setText("A") - self.handle_order_enter() - - # 等待一小段时间 - QTimer.singleShot(100, lambda: self._test_scan_b()) - - def _test_scan_b(self): - """测试扫码工程号B""" - logging.info("模拟扫码工程号B") - self.order_edit.setText("B") - self.handle_order_enter() - - # 等待一小段时间后检查结果 - QTimer.singleShot(200, self._check_test_result) def _check_test_result(self): """检查测试结果""" @@ -1107,33 +1109,7 @@ class MainWindow(MainWindowUI): # 创建单元格 item = QTableWidgetItem("") item.setTextAlignment(Qt.AlignCenter) - - # 如果有order_info数据,尝试匹配字段并设置值 - # if order_info: - # config_name = config.get('name') - # # 检查order_info中是否有与config_name匹配的键 - # if config_name in order_info: - # value = str(order_info[config_name]) - # item = QTableWidgetItem(value) - # item.setTextAlignment(Qt.AlignCenter) - # # 设置单元格背景为浅绿色,表示自动填充 - # item.setBackground(QBrush(QColor("#c8e6c9"))) - - # # 保存到数据库 - # from dao.inspection_dao import InspectionDAO - # inspection_dao = InspectionDAO() - # tray_id = self.tray_edit.currentText() - # data = [{ - # 'position': config.get('position'), - # 'config_id': config.get('id'), - # 'value': value, - # 'status': 'init', # 设置初始状态 - # 'remark': '', - # 'tray_id': tray_id - # }] - # inspection_dao.save_inspection_data(self._current_order_code,gc_note, data) - # logging.info(f"自动填充字段 {config_name} 值为 {value}") - + # 设置单元格属性以标识其关联的检验项 item.setData(Qt.UserRole, config.get('id')) self.process_table.setItem(data_start_row, col_index, item) @@ -1236,21 +1212,30 @@ class MainWindow(MainWindowUI): row: 行索引 column: 列索引 """ - # 防抖机制:记录上次处理的单元格位置和时间 - current_time = time.time() - last_cell_key = f"{row}_{column}" - last_process_time = getattr(self, '_last_cell_change_time', 0) - last_cell = getattr(self, '_last_cell_changed', '') - - # 如果是同一单元格且间隔小于0.5秒,则忽略此次调用 - if last_cell == last_cell_key and current_time - last_process_time < 0.5: - return - - # 更新最后处理的单元格和时间 - self._last_cell_changed = last_cell_key - self._last_cell_change_time = current_time - + # 创建唯一键,包含行、列、工程号 try: + # 获取工程号,用于创建更精确的唯一键 + order_item = self.process_table.item(row, 1) + gc_note = order_item.text().strip() if order_item else "" + + # 创建包含行、列、工程号的唯一键 + cell_key = f"{row}_{column}_{gc_note}" + current_time = time.time() + + # 获取上次处理时间 + last_process_times = getattr(self, '_last_cell_process_times', {}) + last_process_time = last_process_times.get(cell_key, 0) + + # 如果同一单元格在1秒内被处理过,则忽略 + if current_time - last_process_time < 1.0: + logging.info(f"防抖:跳过重复处理单元格 [{row}, {column}], 工程号={gc_note},间隔小于1秒") + return + + # 更新处理时间 + if not hasattr(self, '_last_cell_process_times'): + self._last_cell_process_times = {} + self._last_cell_process_times[cell_key] = current_time + # 只处理数据行的检验列变更 if row < 2: # 忽略表头行 return @@ -1260,14 +1245,9 @@ class MainWindow(MainWindowUI): return # 获取工程号 - order_item = self.process_table.item(row, 1) - if not order_item: - return - - gc_note = order_item.text().strip() if not gc_note: return - + # 获取托盘号 tray_id = self.tray_edit.currentText() @@ -1300,6 +1280,7 @@ class MainWindow(MainWindowUI): # 显示临时状态消息 self.statusBar().showMessage(f"正在保存检验数据: {data_type}={value}", 1000) + # 验证数据有效性 # 设置单元格颜色为浅绿色,表示已填写 cell_item.setBackground(QBrush(QColor("#c8e6c9"))) @@ -1307,8 +1288,6 @@ class MainWindow(MainWindowUI): from dao.inspection_dao import InspectionDAO inspection_dao = InspectionDAO() status = inspection_dao.get_product_status(self._current_order_code, gc_note, tray_id) - - # 保存到数据库 self.save_inspection_data(self._current_order_code, gc_note, tray_id, config['position'], config['id'], value, status) # 判断是否是包装列 @@ -1343,7 +1322,7 @@ class MainWindow(MainWindowUI): # 检查是否完成检验并更新状态 self.check_inspection_completed(row) - + except Exception as e: logging.error(f"处理检验单元格变更失败: {str(e)}") self.statusBar().showMessage(f"处理检验数据失败: {str(e)[:50]}...", 3000) @@ -1351,7 +1330,14 @@ class MainWindow(MainWindowUI): # 延迟一段时间后再触发查询,避免频繁刷新UI # 但要避免在加载过程中触发新的加载 if not self._loading_data_in_progress: - QTimer.singleShot(1000, self._safe_load_data) + # 使用类变量保存定时器,避免创建多个定时器 + if hasattr(self, '_load_data_timer') and self._load_data_timer is not None: + self._load_data_timer.stop() + + self._load_data_timer = QTimer() + self._load_data_timer.setSingleShot(True) + self._load_data_timer.timeout.connect(self._safe_load_data) + self._load_data_timer.start(1000) @@ -1374,12 +1360,21 @@ class MainWindow(MainWindowUI): # 如果是相同的数据且间隔小于0.5秒,则忽略此次保存 if last_save_key == save_key and current_time - last_save_time < 0.5: + logging.info(f"防抖机制:跳过保存相同数据 {save_key},间隔小于0.5秒") return # 更新最后保存的数据和时间 self._last_save_key = save_key self._last_save_time = current_time + # 检查是否已存在相同记录 + from dao.inspection_dao import InspectionDAO + inspection_dao = InspectionDAO() + existing_data = inspection_dao.get_inspection_data_by_config(order_id, gc_note, tray_id, position, config_id) + if existing_data and str(existing_data.get('value')) == str(value): + logging.info(f"幂等处理:跳过保存已存在的检验数据 {gc_note}_{position}_{config_id}={value}") + return + try: from dao.inspection_dao import InspectionDAO inspection_dao = InspectionDAO() @@ -1432,6 +1427,23 @@ class MainWindow(MainWindowUI): # 更新最后加载的时间 self._last_safe_load_time = current_time + # 检查是否已有定时器在运行 + if hasattr(self, '_safe_load_timer') and self._safe_load_timer is not None: + self._safe_load_timer.stop() + self._safe_load_timer = None + logging.debug("已停止之前的加载定时器") + + # 创建新的定时器,延迟执行实际加载操作 + self._safe_load_timer = QTimer() + self._safe_load_timer.setSingleShot(True) + self._safe_load_timer.timeout.connect(self._do_safe_load) + self._safe_load_timer.start(500) # 500毫秒后执行 + + def _do_safe_load(self): + """实际执行数据加载的方法,由定时器触发""" + # 获取当前托盘号,用于日志记录 + tray_id = self.tray_edit.currentText() + if self._loading_data_in_progress: # 如果已经在加载数据,不要再次触发 logging.debug(f"已有数据加载正在进行,忽略此次请求 (托盘号: {tray_id})") @@ -1441,6 +1453,13 @@ class MainWindow(MainWindowUI): self._loading_data_in_progress = True self.load_finished_inspection_data() logging.info(f"数据加载完成,托盘号: {tray_id}") + + # 加载完成后显示包装记录 + try: + self.show_pack_item() + logging.info(f"在_do_safe_load中调用show_pack_item, 托盘号: {tray_id}") + except Exception as e: + logging.error(f"在_do_safe_load中调用show_pack_item失败: {str(e)}") except Exception as e: logging.error(f"安全加载数据失败: {str(e)}, 托盘号: {tray_id}") # 即使加载失败,也尝试显示包装记录 @@ -1451,10 +1470,23 @@ class MainWindow(MainWindowUI): logging.error(f"加载失败后显示包装记录失败: {str(ex)}, 托盘号: {tray_id}") finally: self._loading_data_in_progress = False + # 清理定时器引用 + self._safe_load_timer = None def load_finished_inspection_data(self): """加载未完成的检验数据并显示在表格中""" # 注意:此方法通常应通过_safe_load_data调用,以防止循环 + + # 添加表格更新锁,避免并发更新 + if hasattr(self, '_table_updating') and self._table_updating: + logging.info("表格正在更新中,忽略此次加载请求") + return + + self._table_updating = True + + # 记录信号连接状态 + signal_was_connected = False + try: # 获取托盘号 tray_id = self.tray_edit.currentText() @@ -1480,11 +1512,15 @@ class MainWindow(MainWindowUI): # 使用get_inspection_data_unfinished获取未完成的数据 unfinished_data = inspection_dao.get_inspection_data_unfinished(tray_id) - # 断开单元格变更信号,避免加载过程中触发保存 + # 检查信号是否已连接,并断开单元格变更信号 try: - self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed) - except: - pass + signal_was_connected = self.process_table.receivers(self.process_table.cellChanged) > 0 + if signal_was_connected: + self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed) + logging.debug("已断开单元格变更信号") + except Exception as e: + logging.debug(f"断开单元格变更信号失败: {str(e)}") + signal_was_connected = False # 清空表格现有数据行,只保留表头 while self.process_table.rowCount() > 2: @@ -1495,11 +1531,13 @@ class MainWindow(MainWindowUI): # 确保表格完全清空,只保留表头行 self.process_table.setRowCount(2) # 只保留表头的两行 - # 重新连接单元格变更信号 - try: - self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) - except: - pass + # 只有在之前信号已连接的情况下才重新连接 + if signal_was_connected: + try: + self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) + logging.debug("已重新连接单元格变更信号") + except Exception as e: + logging.warning(f"重新连接单元格变更信号失败: {str(e)}") # 加载包装记录 return @@ -1531,59 +1569,60 @@ class MainWindow(MainWindowUI): # 按创建时间排序,但确保新添加的工程号在最后 sorted_gc_notes = inspection_dao.get_orders_by_create_time(list(orders_data.keys())) - logging.info(f"按创建时间排序后的工程号列表: {sorted_gc_notes}") - # 如果当前有正在处理的工程号,确保它在最后 - if current_gc_note and current_gc_note in sorted_gc_notes: - # 将当前工程号移到列表末尾 + # 如果有当前正在处理的工程号,将其移到最后 + if current_gc_note and current_gc_note in sorted_gc_notes and current_timestamp: + logging.info(f"将当前处理的工程号 {current_gc_note} 移到最后") sorted_gc_notes.remove(current_gc_note) sorted_gc_notes.append(current_gc_note) - logging.info(f"将最新扫码的工程号 {current_gc_note} (时间戳: {current_timestamp}) 移到列表末尾,确保显示在最后") - logging.info(f"调整后的工程号列表: {sorted_gc_notes}") - elif current_gc_note: - logging.info(f"当前正在处理的工程号 {current_gc_note} 不在数据库记录中,可能是新添加的") - else: + + logging.info(f"按创建时间排序后的工程号列表: {sorted_gc_notes}") + + # 如果没有当前正在处理的工程号,记录日志 + if not current_gc_note: logging.info("没有当前正在处理的工程号") + # 收集所有要设置的单元格,批量设置 + cells_to_set = [] + + # 按排序后的工程号顺序添加数据 for gc_note in sorted_gc_notes: - items = orders_data[gc_note] - - # 添加新行 + # 添加工程号到表格的第二列 + item = QTableWidgetItem(gc_note) + item.setTextAlignment(Qt.AlignCenter) self.process_table.insertRow(row_idx) + self.process_table.setItem(row_idx, 1, item) - # 添加序号到第一列 - seq_item = QTableWidgetItem(str(row_idx - 1)) - seq_item.setTextAlignment(Qt.AlignCenter) - self.process_table.setItem(row_idx, 0, seq_item) + # 添加序号到表格的第一列 + item = QTableWidgetItem(str(row_idx - 1)) # 序号从1开始 + item.setTextAlignment(Qt.AlignCenter) + self.process_table.setItem(row_idx, 0, item) - # 添加工程号到第二列 - order_item = QTableWidgetItem(gc_note) - order_item.setTextAlignment(Qt.AlignCenter) - self.process_table.setItem(row_idx, 1, order_item) + # 获取该工程号的所有数据 + data_list = orders_data[gc_note] - # 添加检验数据 - for item in items: - position = item['position'] - value = item['value'] if item['value'] else "" - status = item['status'] - config_id = item['config_id'] - + # 按位置分组,确保每个位置只有一条数据(取最新的) + position_data = {} + for data in data_list: + position = data['position'] + if position not in position_data or data['update_time'] > position_data[position]['update_time']: + position_data[position] = data + + # 添加检验数据到表格 + for position, data in position_data.items(): + value = data['value'] + config_id = data['config_id'] # 找到对应的列索引 - col_index = None - for i, config in enumerate(enabled_configs): - if config.get('position') == position: - col_index = 2 + i # 检验列从第3列开始 - break + col_idx = None + + # 处理检验列(position 1-10) + if 1 <= position <= 10: + for i, config in enumerate(enabled_configs): + if config['id'] == config_id: + col_idx = 2 + i + break - if col_index is not None: - # 创建单元格并设置值 - cell_item = QTableWidgetItem(str(value)) - cell_item.setTextAlignment(Qt.AlignCenter) - # 存储配置ID,用于保存时确定是哪个检验项 - cell_item.setData(Qt.UserRole, config_id) - # 设置单元格 - self.process_table.setItem(row_idx, col_index, cell_item) # 添加贴标(11)和称重数据(12) if position == 11: # 贴标 # 贴标列索引 = 2(序号和工程号) + 检验列数 @@ -1602,37 +1641,20 @@ class MainWindow(MainWindowUI): # 设置表格为可编辑状态 self.process_table.setEditTriggers(QTableWidget.DoubleClicked | QTableWidget.EditKeyPressed) - # 重新连接单元格变更信号 - self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) - + # 只有在之前信号已连接的情况下才重新连接 + if signal_was_connected: + try: + self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) + logging.debug("已重新连接单元格变更信号") + except Exception as e: + logging.warning(f"重新连接单元格变更信号失败: {str(e)}") except Exception as e: logging.error(f"加载未完成的检验数据失败: {str(e)}") QMessageBox.warning(self, "加载失败", f"加载未完成的检验数据失败: {str(e)}") - finally: - # 加载包装记录,但要避免循环调用 - # 设置一个标志,防止 show_pack_item 触发更多的数据加载 - # 只有在_safe_load_data调用此方法,且没有明确设置加载状态的情况下才调用 - has_loading_flag = hasattr(self, '_loading_data_in_progress') - is_loading = getattr(self, '_loading_data_in_progress', False) - - # 如果是被_safe_load_data调用(即已经设置了_loading_data_in_progress),则无需额外设置 - if has_loading_flag and is_loading: - # 直接调用show_pack_item,不改变加载状态 - try: - self.show_pack_item() - logging.info("在load_finished_inspection_data中调用show_pack_item") - except Exception as e: - logging.error(f"在load_finished_inspection_data中调用show_pack_item失败: {str(e)}") - # 否则,这是直接调用此方法(非_safe_load_data),需要设置加载状态 - elif not is_loading: - self._loading_data_in_progress = True - try: - self.show_pack_item() - logging.info("在load_finished_inspection_data中直接调用show_pack_item") - finally: - self._loading_data_in_progress = False + # 释放表格更新锁 + self._table_updating = False def load_finished_record_to_package_record(self, order_id, gc_note, tray_id, axios_num=None): """加载已完成检验数据到包装记录 @@ -2017,22 +2039,57 @@ class MainWindow(MainWindowUI): # 连接电力数据变化信号 self.electricity_handler.electricity_data_changed.connect(self.update_electricity_statistics) - # 连接贴标信号 + # 连接贴标信号 - 先断开再连接,避免重复连接 + try: + self.machine_handlers.label_signal_changed.disconnect(self.handle_label_signal) + except Exception: + logging.debug("贴标信号未连接,无需断开") self.machine_handlers.label_signal_changed.connect(self.handle_label_signal) - # 连接称重数据变化信号 + # 连接称重数据变化信号 - 先断开再连接,避免重复连接 + try: + self.machine_handlers.weight_changed.disconnect(self.handle_weight_data) + except Exception: + logging.debug("称重信号未连接,无需断开") self.machine_handlers.weight_changed.connect(self.handle_weight_data) - # 连接NG信号 + # 连接NG信号 - 先断开再连接,避免重复连接 + try: + self.machine_handlers.ng_changed.disconnect(self.handle_ng) + except Exception: + logging.debug("NG信号未连接,无需断开") self.machine_handlers.ng_changed.connect(self.handle_ng) - # 连接故障信号 + # 连接故障信号 - 先断开再连接,避免重复连接 + try: + self.machine_handlers.error_1_changed.disconnect(self.handle_error_1) + except Exception: + logging.debug("故障1信号未连接,无需断开") self.machine_handlers.error_1_changed.connect(self.handle_error_1) + + try: + self.machine_handlers.error_2_changed.disconnect(self.handle_error_2) + except Exception: + logging.debug("故障2信号未连接,无需断开") self.machine_handlers.error_2_changed.connect(self.handle_error_2) + + try: + self.machine_handlers.error_3_changed.disconnect(self.handle_error_3) + except Exception: + logging.debug("故障3信号未连接,无需断开") self.machine_handlers.error_3_changed.connect(self.handle_error_3) - # 连接上下料反馈信号 + # 连接上下料反馈信号 - 先断开再连接,避免重复连接 + try: + self.machine_handlers.loading_feedback_changed.disconnect(self.handle_loading_feedback) + except Exception: + logging.debug("上料反馈信号未连接,无需断开") self.machine_handlers.loading_feedback_changed.connect(self.handle_loading_feedback) + + try: + self.machine_handlers.unloading_feedback_changed.disconnect(self.handle_unloading_feedback) + except Exception: + logging.debug("下料反馈信号未连接,无需断开") self.machine_handlers.unloading_feedback_changed.connect(self.handle_unloading_feedback) # 连接急停信号 @@ -2041,9 +2098,6 @@ class MainWindow(MainWindowUI): # 立即更新一次用电量数据 self.update_electricity_statistics() - # 立即更新一次订单数量和产量统计数据 - self.update_order_statistics() - logging.info("已连接所有Modbus信号") def update_electricity_statistics(self, value=None): @@ -2103,7 +2157,18 @@ class MainWindow(MainWindowUI): def handle_weight_data(self, weight_in_g): """处理称重数据变化""" try: + # 添加防抖机制,避免短时间内重复处理相同重量 current_time = time.time() + last_weight_time = getattr(self, '_last_weight_data_time', 0) + last_weight_value = getattr(self, '_last_weight_data_value', 0) + + # 如果是相同的重量且间隔小于0.2秒,则忽略此次调用 + if abs(weight_in_g - last_weight_value) < 10 and current_time - last_weight_time < 0.2: + return + + # 更新最后处理的重量和时间 + self._last_weight_data_time = current_time + self._last_weight_data_value = weight_in_g # 转换重量单位并立即更新UI显示 weight_in_kg = self._convert_to_kg(weight_in_g) @@ -2115,6 +2180,12 @@ class MainWindow(MainWindowUI): logging.info(f"检测到新产品放上,重量从 {self._current_weight}kg 变为 {weight_in_kg}kg") self._weight_processed = False # 重置处理标记,允许处理新产品 + # 检测重量从较大值变为接近0,判断为产品移除 + elif self._current_weight is not None and self._current_weight > 0.5 and weight_in_kg < 0.1: + logging.info(f"检测到产品被移除,重量从 {self._current_weight}kg 变为 {weight_in_kg}kg") + self._weight_processed = False # 重置处理标记,为下一个产品做准备 + self._last_processed_weight = 0 # 重置上次处理的重量 + # 更新当前重量和时间 self._current_weight = weight_in_kg self._last_weight_time = current_time @@ -2163,8 +2234,16 @@ class MainWindow(MainWindowUI): original_weight_kg: 开始检查时的重量(千克) """ try: + # 设置最小有效重量阈值,低于此值的重量不会被处理 + MIN_VALID_WEIGHT = 0.5 # 0.5kg + + # 如果当前重量低于最小有效重量阈值,则直接跳过 + if self._current_weight < MIN_VALID_WEIGHT: + logging.info(f"当前重量 {self._current_weight}kg 低于最小有效重量阈值 {MIN_VALID_WEIGHT}kg,跳过处理") + return + # 如果当前重量与定时器启动时的重量相同,说明这段时间内没有新的重量数据 - if self._current_weight == original_weight_kg and self._current_weight > 0.5: + if self._current_weight == original_weight_kg and self._current_weight > MIN_VALID_WEIGHT: logging.info(f"重量 {original_weight_kg}kg 在{self._weight_stable_threshold}秒内保持稳定") # 如果这个重量与上一次处理的重量接近(±0.1kg),且标记已处理,则跳过 @@ -2191,6 +2270,20 @@ class MainWindow(MainWindowUI): weight_kg: 稳定的重量值(千克) max_retries: 最大重试次数 """ + # 添加防抖机制,避免短时间内重复处理相同重量 + current_time = time.time() + last_stable_time = getattr(self, '_last_stable_weight_time', 0) + last_stable_value = getattr(self, '_last_stable_weight_value', 0) + + # 如果是相同的重量且间隔小于1秒,则忽略此次调用 + if abs(weight_kg - last_stable_value) < 0.1 and current_time - last_stable_time < 1.0: + logging.info(f"防抖机制:跳过处理相同的稳定重量 {weight_kg}kg,间隔小于1秒") + return + + # 更新最后处理的稳定重量和时间 + self._last_stable_weight_time = current_time + self._last_stable_weight_value = weight_kg + retry_count = 0 last_error = None modbus = ModbusUtils() @@ -2201,6 +2294,8 @@ class MainWindow(MainWindowUI): if not client: logging.error("无法获取Modbus客户端连接") return + + logging.info(f"开始处理稳定重量: {weight_kg}kg") # 这里不再写入D10=1,全部交由_process_stable_weight处理 self._process_stable_weight(weight_kg) # 调用打印方法 @@ -2209,6 +2304,11 @@ class MainWindow(MainWindowUI): self._weight_processed = True self._last_processed_weight = weight_kg logging.info(f"已标记重量 {weight_kg}kg 为已处理") + + # 重置当前重量为0,避免两个产品之间的重量判断问题 + # 这样可以确保下一个产品必须从接近0的值开始测量 + logging.info(f"已重置当前重量为0,等待下一个产品") + except Exception as e: logging.error(f"处理稳定重量时发生错误: {str(e)}") finally: @@ -2222,6 +2322,14 @@ class MainWindow(MainWindowUI): Args: weight_kg: 稳定的重量值(千克) """ + # 添加处理锁,避免并发处理 + if hasattr(self, '_processing_weight_lock') and self._processing_weight_lock: + logging.warning(f"已有称重处理进行中,忽略本次请求: {weight_kg}kg") + return + + # 设置处理锁 + self._processing_weight_lock = True + try: # 忽略接近0的重量值,这可能表示产品已被移除 if weight_kg < 0.1: # 小于100g的重量视为无效 @@ -2267,16 +2375,30 @@ class MainWindow(MainWindowUI): self._current_processing_row = data_row # 获取工程号 - gc_note = self.process_table.item(data_row, 1) - if not gc_note: + gc_note_item = self.process_table.item(data_row, 1) + if not gc_note_item: logging.warning("无法获取工程号") return - gc_note = gc_note.text().strip() + gc_note = gc_note_item.text().strip() if not gc_note: logging.warning("工程号为空") return - # 保存净重到数据库(毛重-工字轮重量,单位都是千克) + + # 创建唯一键,包含工程号、重量值和时间戳 + current_time = time.time() + process_key = f"{gc_note}_{weight_kg}_{int(current_time/60)}" # 按分钟级别去重 + + # 检查是否已处理过相同的键 + last_process_key = getattr(self, '_last_weight_process_key', '') + if process_key == last_process_key: + logging.info(f"跳过重复处理:工程号 {gc_note} 的重量 {weight_kg}kg 在短时间内已处理") + return + + # 更新处理键 + self._last_weight_process_key = process_key + + # 保存净重到数据库(毛重-工字轮重量,单位都是千克) from dao.inspection_dao import InspectionDAO inspection_dao = InspectionDAO() gzl_zl_raw = inspection_dao.get_gzl_zl(self._current_order_code) @@ -2312,9 +2434,12 @@ class MainWindow(MainWindowUI): self.warning_msg.setIcon(QMessageBox.Warning) self.warning_msg.setWindowTitle('重量超出范围') self.warning_msg.setText(f"称重值 {net_weight_kg:.2f}kg 不在轴重要求范围内 ({min_weight:.1f} - {max_weight:.1f}kg)") - self.warning_msg.setStandardButtons(QMessageBox.NoButton) + self.warning_msg.setStandardButtons(QMessageBox.Ok) # 添加确定按钮 self.warning_msg.setModal(False) # 确保非模态 self.warning_msg.setWindowFlags(self.warning_msg.windowFlags() | Qt.WindowStaysOnTopHint) # 置顶显示 + + # 连接按钮点击信号 + self.warning_msg.buttonClicked.connect(lambda btn: self.warning_msg.close()) # 使用 show 而非 exec_ 保持非阻塞 self.warning_msg.show() @@ -2377,12 +2502,24 @@ class MainWindow(MainWindowUI): weight_item.setTextAlignment(Qt.AlignCenter) self.process_table.setItem(data_row, weight_col, weight_item) - # 保存到数据库(使用千克) + # 检查是否已存在相同记录 tray_id = self.tray_edit.currentText() - self.save_inspection_data(self._current_order_code, gc_note, tray_id, 12, 12, str(weight_kg), "pass") + existing_data = inspection_dao.get_inspection_data_by_config(self._current_order_code, gc_note, tray_id, 12, 12) + if existing_data and str(existing_data.get('value')) == str(weight_kg): + logging.info(f"跳过保存:工程号 {gc_note} 的称重数据 {weight_kg} 已存在") + else: + # 保存到数据库(使用千克) + self.save_inspection_data(self._current_order_code, gc_note, tray_id, 12, 12, str(weight_kg), "pass") + logging.info(f"已保存称重数据: {weight_kg}kg 到工程号 {gc_note}") - - self.save_inspection_data(self._current_order_code, gc_note, tray_id, 13, 13, str(net_weight_kg), "pass") + # 检查是否已存在相同净重记录 + existing_net_data = inspection_dao.get_inspection_data_by_config(self._current_order_code, gc_note, tray_id, 13, 13) + if existing_net_data and str(existing_net_data.get('value')) == str(net_weight_kg): + logging.info(f"跳过保存:工程号 {gc_note} 的净重数据 {net_weight_kg} 已存在") + else: + # 保存净重数据 + self.save_inspection_data(self._current_order_code, gc_note, tray_id, 13, 13, str(net_weight_kg), "pass") + logging.info(f"已保存净重数据: {net_weight_kg}kg 到工程号 {gc_note}") # 设置净重单元格(显示千克) net_weight_item = QTableWidgetItem(str(net_weight_kg)) @@ -2514,6 +2651,7 @@ class MainWindow(MainWindowUI): logging.info(f"添加订单信息成功: {response.get('data',{})}") else: QMessageBox.warning(self, f"提示", response.get("message",{})) + # 新增:如果勾选了圆形标签checkbox,则向寄存器D15写入1 if hasattr(self, "round_label_checkbox") and self.round_label_checkbox.isChecked(): modbus = ModbusUtils() @@ -2522,21 +2660,21 @@ class MainWindow(MainWindowUI): modbus.close_client(client) logging.info("已通知PLC当前使用圆形标签") - # 保存贴标数据到数据库 + # 保存贴标数据到数据库 self.save_inspection_data(self._current_order_code, gc_note, tray_id, 11, 11, str(axios_num), "pass") - - # 重新连接信号 - self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) - logging.info(f"已将稳定的称重数据 {weight_kg}kg 写入行 {data_row}, 列 {weight_col}") - # 更新产品状态为weighed inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'weighed') logging.info(f"工程号 {gc_note} 的称重已完成,状态更新为weighed") + # 重新连接信号 + self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) + + logging.info(f"已将稳定的称重数据 {weight_kg}kg 写入行 {data_row}, 列 {weight_col}") + # 清除当前处理行的跟踪,因为称重完成后需要等待贴标 self._current_processing_row = None - + except Exception as e: logging.error(f"处理称重数据时发生错误: {str(e)}") # 确保重新连接信号 @@ -2544,6 +2682,9 @@ class MainWindow(MainWindowUI): self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) except: pass + finally: + # 释放处理锁 + self._processing_weight_lock = False def _print_weight_label(self, weight_kg): """ @@ -2574,7 +2715,7 @@ class MainWindow(MainWindowUI): try: # 获取Modbus客户端(现在使用连接池,不会每次都创建新连接) client = modbus.get_client() - time.sleep(0.5) + time.sleep(0.1) if not client: logging.error("无法获取Modbus客户端连接") return @@ -2935,14 +3076,6 @@ class MainWindow(MainWindowUI): # 显示事件消息 if "全部完成" in desc: - QMessageBox.information(self, "下料完成", desc) - # 任务完成,清除状态显示 - self.clear_operation_status("output") - if hasattr(self, 'unload_level_label'): - self.unload_level_label.setText("下料层数:--") - if hasattr(self, 'unload_position_label'): - self.unload_position_label.setText("下料位置:--") - # 下料任务完成,恢复下料按钮样式 self.restore_output_button_style() logging.info("下料任务完成,恢复下料按钮样式") elif "请启动" in desc: @@ -3005,11 +3138,6 @@ class MainWindow(MainWindowUI): # 获取Modbus连接 modbus = ModbusUtils() client = modbus.get_client() - - # 根据错误码可以添加不同的处理逻辑 - # 这里先简单处理,对所有错误都复位相关寄存器 - modbus.write_register_until_success(client, 2, 0) - modbus.write_register_until_success(client, 0, 0) modbus.close_client(client) @Slot(int, str) @@ -3030,10 +3158,6 @@ class MainWindow(MainWindowUI): modbus = ModbusUtils() client = modbus.get_client() - # 根据错误码可以添加不同的处理逻辑 - # 这里先简单处理,对所有错误都复位相关寄存器 - modbus.write_register_until_success(client, 3, 0) - modbus.write_register_until_success(client, 4, 0) modbus.close_client(client) @Slot(int, str) @@ -3265,6 +3389,7 @@ class MainWindow(MainWindowUI): def on_diameter_data_received(self, port_name, data): """线径数据接收回调函数 - 采用类似称重的逻辑,不使用会话机制""" + modbus_client = None try: data_str = data.decode('utf-8') if isinstance(data, bytes) else str(data) logging.info(f"收到线径数据: {data_str} 来自 {port_name}") @@ -3274,6 +3399,23 @@ class MainWindow(MainWindowUI): value_str = data_str.split("线径数据:")[1].strip() try: xj_value = round(float(value_str)/10000, 3) + + # 防抖机制:记录上次处理的线径数据和时间 + current_time = time.time() + last_diameter_time = getattr(self, '_last_diameter_time', 0) + last_diameter_value = getattr(self, '_last_diameter_value', None) + + # 如果是相同的线径值(精确到0.001)且间隔小于1秒,则忽略此次处理 + if (last_diameter_value is not None and + abs(last_diameter_value - xj_value) < 0.001 and + current_time - last_diameter_time < 1.0): + logging.info(f"忽略重复线径数据: {xj_value:.3f},间隔太短") + return + + # 更新最后处理的线径数据和时间 + self._last_diameter_value = xj_value + self._last_diameter_time = current_time + self.statusBar().showMessage(f"线径数据: {xj_value:.3f}", 2000) # 查找线径对应的检验项配置和列 @@ -3363,17 +3505,20 @@ class MainWindow(MainWindowUI): if final_value < min_diameter or final_value > max_diameter: # 写入寄存器D10 给值 3 表示线径超出范围 modbus = ModbusUtils() - client = modbus.get_client() - modbus.write_register_until_success(client, 10, 3) - modbus.close_client(client) + modbus_client = modbus.get_client() + modbus.write_register_until_success(modbus_client, 10, 3) + # 显示自动关闭的警告提示框 self.diameter_warning_msg = QMessageBox(self) self.diameter_warning_msg.setIcon(QMessageBox.Warning) self.diameter_warning_msg.setWindowTitle('线径超出范围') self.diameter_warning_msg.setText(f"线径值 {final_value:.3f}mm 不在线径公差范围内 ({min_diameter:.3f} - {max_diameter:.3f}mm)") - self.diameter_warning_msg.setStandardButtons(QMessageBox.NoButton) + self.diameter_warning_msg.setStandardButtons(QMessageBox.Ok) # 添加确定按钮 self.diameter_warning_msg.setModal(False) # 确保非模态 self.diameter_warning_msg.setWindowFlags(self.diameter_warning_msg.windowFlags() | Qt.WindowStaysOnTopHint) # 置顶显示 + + # 连接按钮点击信号 + self.diameter_warning_msg.buttonClicked.connect(lambda btn: self.diameter_warning_msg.close()) # 使用 show 而非 exec_ 保持非阻塞 self.diameter_warning_msg.show() @@ -3416,31 +3561,14 @@ class MainWindow(MainWindowUI): else: logging.debug("线径公差字段不存在或为空,跳过范围检查") - # 原有的数据库公差校验(保留作为备用) - from dao.inspection_dao import InspectionDAO - inspection_dao = InspectionDAO() - bccd, tccd = inspection_dao.get_xj_range(self._current_order_code) - - if bccd is not None and tccd is not None: - if float(bccd) - 0.02 <= final_value <= float(tccd) + 0.02: - # 使用set_inspection_value保存数据 - self.set_inspection_value('xj', xj_config, final_value) - logging.info(f"已将稳定的线径值 {final_value:.3f} 保存到工程号 {gc_note} (行 {data_row})") - else: - # 使用信号槽机制确保在主线程中显示弹框 - self.diameter_warning_signal.emit(final_value, bccd, tccd) - - # 重置测量列表,防止重复触发 - self._diameter_measurements = [] - - # 阻止继续执行,等待用户处理 - logging.warning(f"线径值 {final_value:.3f} 超出公差范围,已阻止保存,等待用户处理") - return - else: - # 无公差范围,直接保存 + # 保存线径数据到表格 + try: + # 使用set_inspection_value保存数据 self.set_inspection_value('xj', xj_config, final_value) - logging.info(f"已将线径值 {final_value:.3f} 保存到工程号 {gc_note} (行 {data_row})") - + logging.info(f"已将稳定的线径值 {final_value:.3f} 保存到工程号 {gc_note} (行 {data_row})") + except Exception as e: + logging.error(f"保存线径数据到表格失败: {str(e)}") + # 重置测量列表,准备下一次测量 self._diameter_measurements = [] self.statusBar().showMessage(f"线径数据已保存: {final_value:.3f}", 2000) @@ -3455,6 +3583,19 @@ class MainWindow(MainWindowUI): logging.warning(f"收到的数据不包含线径数据标记: {data_str}") except Exception as e: logging.error(f"处理线径数据失败: {str(e)}") + import traceback + logging.error(f"错误详情: {traceback.format_exc()}") + finally: + # 确保关闭Modbus客户端连接 + if modbus_client: + try: + ModbusUtils().close_client(modbus_client) + except Exception as e: + logging.error(f"关闭Modbus客户端连接失败: {str(e)}") + + # 确保清理测量列表 + if hasattr(self, '_diameter_measurements') and len(self._diameter_measurements) >= 5: + self._diameter_measurements = [] def _save_diameter_to_order(self, order_id, config, value): """基于工程号保存线径值,确保即使行号变化也能保存到正确的产品""" @@ -3493,10 +3634,13 @@ class MainWindow(MainWindowUI): self.diameter_warning_msg.setIcon(QMessageBox.Warning) self.diameter_warning_msg.setWindowTitle('线径超出范围') self.diameter_warning_msg.setText(f"线径 {final_value:.3f} 不在公差范围内 ({bccd} - {tccd})") - self.diameter_warning_msg.setStandardButtons(QMessageBox.NoButton) # 不显示按钮 + self.diameter_warning_msg.setStandardButtons(QMessageBox.Ok) # 添加确定按钮 self.diameter_warning_msg.setModal(False) # 确保非模态 self.diameter_warning_msg.setWindowFlags(self.diameter_warning_msg.windowFlags() | Qt.WindowStaysOnTopHint) # 置顶显示 + # 连接按钮点击信号 + self.diameter_warning_msg.buttonClicked.connect(lambda btn: self.diameter_warning_msg.close()) + # 显示提示框(非模态) self.diameter_warning_msg.show() @@ -3551,6 +3695,20 @@ class MainWindow(MainWindowUI): scan_data = data_str.split("扫码数据:")[1].strip() logging.info(f"提取到扫码数据: {scan_data}") + # 防抖机制:记录上次扫码的数据和时间 + current_time = time.time() + last_scan_time = getattr(self, '_last_scan_time', 0) + last_scan_data = getattr(self, '_last_scan_data', '') + + # 如果是相同的数据且间隔小于1秒,则忽略此次扫码 + if last_scan_data == scan_data and current_time - last_scan_time < 1.0: + logging.info(f"忽略重复扫码数据: {scan_data},间隔太短") + return + + # 更新最后扫码的数据和时间 + self._last_scan_data = scan_data + self._last_scan_time = current_time + # 使用焦点跟踪器设置文本并触发回车事件 from utils.focus_tracker import FocusTracker focus_tracker = FocusTracker.get_instance() @@ -4643,7 +4801,30 @@ class MainWindow(MainWindowUI): def handle_inspection_cell_changed(self, row, column): """处理检验表格单元格内容变更事件""" + # 创建唯一键,包含行、列、工程号 try: + # 获取工程号,用于创建更精确的唯一键 + order_item = self.process_table.item(row, 1) + gc_note = order_item.text().strip() if order_item else "" + + # 创建包含行、列、工程号的唯一键 + cell_key = f"{row}_{column}_{gc_note}" + current_time = time.time() + + # 获取上次处理时间 + last_process_times = getattr(self, '_last_cell_process_times', {}) + last_process_time = last_process_times.get(cell_key, 0) + + # 如果同一单元格在1秒内被处理过,则忽略 + if current_time - last_process_time < 1.0: + logging.info(f"防抖:跳过重复处理单元格 [{row}, {column}], 工程号={gc_note},间隔小于1秒") + return + + # 更新处理时间 + if not hasattr(self, '_last_cell_process_times'): + self._last_cell_process_times = {} + self._last_cell_process_times[cell_key] = current_time + # 只处理数据行的检验列变更 if row < 2: # 忽略表头行 return @@ -4653,14 +4834,9 @@ class MainWindow(MainWindowUI): return # 获取工程号 - order_item = self.process_table.item(row, 1) - if not order_item: - return - - gc_note = order_item.text().strip() if not gc_note: return - + # 获取托盘号 tray_id = self.tray_edit.currentText() @@ -4743,7 +4919,14 @@ class MainWindow(MainWindowUI): # 延迟一段时间后再触发查询,避免频繁刷新UI # 但要避免在加载过程中触发新的加载 if not self._loading_data_in_progress: - QTimer.singleShot(1000, self._safe_load_data) + # 使用类变量保存定时器,避免创建多个定时器 + if hasattr(self, '_load_data_timer') and self._load_data_timer is not None: + self._load_data_timer.stop() + + self._load_data_timer = QTimer() + self._load_data_timer.setSingleShot(True) + self._load_data_timer.timeout.connect(self._safe_load_data) + self._load_data_timer.start(1000)