feat:修复线径测量问题

This commit is contained in:
zhu-mengmeng 2025-07-23 10:46:39 +08:00
parent 268a1cc16b
commit 23823d08f8
5 changed files with 289 additions and 133 deletions

View File

@ -103,7 +103,7 @@
"parity": "N",
"port": "19200",
"query_cmd": "01 41 0d",
"query_interval": 5,
"query_interval": 1,
"auto_query": true,
"ser": "COM3",
"stop_bits": 1,

View File

@ -1190,3 +1190,71 @@ class InspectionDAO:
except Exception as e:
logging.error(f"查询检验数据失败: {str(e)}")
return None
def get_package_statistics(self, order_id=None):
"""获取包装记录的统计数据
Args:
order_id: 订单号如果为None则获取所有订单的统计
Returns:
dict: 包含当前订单和所有订单的统计数据
{
'count': 当前订单的记录数量,
'weight': 当前订单的总重量,
'count_all': 所有订单的记录数量,
'weight_all': 所有订单的总重量
}
"""
try:
# 构建SQL查询
sql = """
SELECT SUM(weight) weight,
SUM(count) count,
SUM(count_all) count_all,
SUM(weight_all) weight_all
FROM (
SELECT COUNT(gc_note) AS count,
SUM(weight) AS weight,
'' AS count_all,
'' AS weight_all
FROM wsbz_inspection_pack_data
WHERE order_id = ?
UNION ALL
SELECT '', '',
COUNT(gc_note) AS count_all,
SUM(weight) AS weight_all
FROM wsbz_inspection_pack_data
) a
"""
with SQLUtils('sqlite', database='db/jtDB.db') as db:
# 执行查询
db.cursor.execute(sql, (order_id or '',))
# 获取结果
row = db.cursor.fetchone()
# 如果有结果,转换为字典
if row:
return {
'weight': float(row[0] or 0),
'count': int(row[1] or 0),
'count_all': int(row[2] or 0),
'weight_all': float(row[3] or 0)
}
else:
return {
'weight': 0,
'count': 0,
'count_all': 0,
'weight_all': 0
}
except Exception as e:
logging.error(f"获取包装记录统计数据失败: {str(e)}")
return {
'weight': 0,
'count': 0,
'count_all': 0,
'weight_all': 0
}

Binary file not shown.

View File

@ -1117,8 +1117,8 @@ class SerialManager:
byte_data = bytes.fromhex(query_cmd.replace(' ', ''))
self.serial_ports[port_name].write(byte_data)
# 等待响应
time.sleep(0.5)
# 等待响应 - 减少等待时间以加快数据获取
time.sleep(0.2)
if self.serial_ports[port_name].in_waiting > 0:
response = self.serial_ports[port_name].read(self.serial_ports[port_name].in_waiting)
@ -1128,14 +1128,14 @@ class SerialManager:
logging.error(f"线径数据处理异常: {e}")
# 查询间隔,从配置中获取或使用默认值
query_interval = self.xj_config.get('query_interval', 5) if self.xj_config else 5
wait_cycles = int(query_interval * 10) # 转换为0.1秒的周期数
query_interval = self.xj_config.get('query_interval', 1) if self.xj_config else 1
# 每隔query_interval秒查询一次
for i in range(wait_cycles):
if not self.running_flags.get(port_name, False):
break
time.sleep(0.1)
# 确保查询间隔不小于0.2秒
if query_interval < 0.2:
query_interval = 0.2
# 每隔query_interval秒查询一次使用更简单的等待方式
time.sleep(query_interval)
except Exception as e:
logging.error(f"线径串口 {port_name} 读取线程异常: {e}")

View File

@ -158,6 +158,9 @@ class MainWindow(MainWindowUI):
# 加载已完成检验数据
self.show_pack_item()
# 更新包装记录统计数据
self.update_package_statistics()
# 创建状态处理器实例
self.machine_handlers = MachineStatusHandlers()
@ -925,6 +928,11 @@ class MainWindow(MainWindowUI):
self._last_order_gc = gc_note
self._last_order_time = current_time
# 清除最近处理的线径工程号,确保新扫码的工程号不会使用之前的线径数据
if hasattr(self, '_last_processed_gc_note'):
logging.info(f"清除最近处理线径数据的工程号: {self._last_processed_gc_note}")
self._last_processed_gc_note = None
logging.info(f"输入的工程号: {gc_note}")
#判断是否是接口,如果不是接口直接添加如果是则走接口
# 如果开启接口模式,则需要调用接口同步到业务库
@ -999,6 +1007,9 @@ class MainWindow(MainWindowUI):
# 更新订单数量和产量统计数据
self.update_order_statistics()
# 确保完整加载所有检验数据,包括线径数据
self._safe_load_data()
else:
logging.warning("工程号为空,忽略处理")
@ -1027,28 +1038,14 @@ class MainWindow(MainWindowUI):
logging.info(f"已生成虚拟工程号并直接添加: {virtual_gc_note}")
# 确保完整加载所有检验数据,包括线径数据
self._safe_load_data()
except Exception as e:
logging.error(f"生成虚拟工程号失败: {str(e)}")
QMessageBox.critical(self, "错误", f"生成虚拟工程号失败: {str(e)}")
def _check_test_result(self):
"""检查测试结果"""
logging.info("检查快速连续扫码测试结果")
total_rows = self.process_table.rowCount()
if total_rows > 2:
last_gc_note = self.process_table.item(total_rows - 1, 1)
if last_gc_note:
logging.info(f"最后一行的工程号: {last_gc_note.text()}")
if last_gc_note.text() == "B":
logging.info("✅ 测试通过最新扫码的工程号B正确显示在最后")
else:
logging.error("❌ 测试失败最新扫码的工程号B没有显示在最后")
else:
logging.error("❌ 测试失败:无法获取最后一行的工程号")
else:
logging.error("❌ 测试失败:表格中没有数据行")
def add_new_inspection_row(self, gc_note, order_code):
"""在微丝产线表格中添加一条新记录,添加到表格末尾
@ -1131,9 +1128,6 @@ class MainWindow(MainWindowUI):
# 选中新添加的行
self.process_table.selectRow(data_start_row)
# 移除行数限制,允许显示更多数据
# self.limit_table_rows(10) # 最多保留10行数据
# 将工程号和托盘号保存到数据库,确保能够正确关联
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
@ -1181,29 +1175,6 @@ class MainWindow(MainWindowUI):
self._current_gc_note = None
self._current_gc_note_timestamp = None
def limit_table_rows(self, max_rows):
"""限制表格最大行数
Args:
max_rows: 最大行数不包括表头行
"""
try:
# 计算数据总行数
data_rows = self.process_table.rowCount() - 2 # 减去表头行
# 如果超过最大行数,删除多余的行
if data_rows > max_rows:
# 要删除的行数
rows_to_remove = data_rows - max_rows
# 从最后一行开始删除
for i in range(rows_to_remove):
self.process_table.removeRow(self.process_table.rowCount() - 1)
logging.info(f"已限制表格最大行数为 {max_rows} 行数据,删除了 {rows_to_remove}")
except Exception as e:
logging.error(f"限制表格行数失败: {str(e)}")
def handle_inspection_cell_changed(self, row, column):
"""处理微丝包装单元格内容变更
@ -1415,29 +1386,48 @@ class MainWindow(MainWindowUI):
# 获取当前托盘号,用于日志记录
tray_id = self.tray_edit.currentText()
# 防抖机制:记录上次加载的时间
current_time = time.time()
last_safe_load_time = getattr(self, '_last_safe_load_time', 0)
# 如果间隔小于0.5秒,则忽略此次加载
if current_time - last_safe_load_time < 0.5:
logging.debug(f"_safe_load_data 被频繁调用,忽略此次请求 (托盘号: {tray_id})")
# 检查是否有加载操作正在进行
if getattr(self, '_loading_data_in_progress', False):
logging.warning(f"已有数据加载操作正在进行,忽略此次请求 (托盘号: {tray_id})")
return
# 更新最后加载的时间
self._last_safe_load_time = current_time
# 检查是否有递归调用
call_stack_depth = getattr(self, '_safe_load_call_depth', 0) + 1
self._safe_load_call_depth = call_stack_depth
# 检查是否已有定时器在运行
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("已停止之前的加载定时器")
# 如果调用栈深度超过3可能存在递归调用
if call_stack_depth > 3:
logging.warning(f"检测到可能的递归调用 _safe_load_data调用栈深度: {call_stack_depth},忽略此次请求")
self._safe_load_call_depth -= 1
return
# 创建新的定时器,延迟执行实际加载操作
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毫秒后执行
try:
# 防抖机制:记录上次加载的时间
current_time = time.time()
last_safe_load_time = getattr(self, '_last_safe_load_time', 0)
# 如果间隔小于0.5秒,则忽略此次加载
if current_time - last_safe_load_time < 0.5:
logging.debug(f"_safe_load_data 被频繁调用,忽略此次请求 (托盘号: {tray_id})")
return
# 更新最后加载的时间
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毫秒后执行
finally:
# 减少调用栈深度计数
self._safe_load_call_depth -= 1
def _do_safe_load(self):
"""实际执行数据加载的方法,由定时器触发"""
@ -1605,7 +1595,7 @@ class MainWindow(MainWindowUI):
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']:
if position not in position_data or data.get('update_time', 0) > position_data[position].get('update_time', 0):
position_data[position] = data
# 添加检验数据到表格
@ -1636,6 +1626,26 @@ class MainWindow(MainWindowUI):
# 净重列索引 = 2(序号和工程号) + 检验列数 + 2(贴标和称重)
net_weight_col = 2 + len(enabled_configs) + 2
self.process_table.setItem(row_idx, net_weight_col, QTableWidgetItem(str(value)))
# 额外加载该工程号的线径数据(即使产品已完成)
for i, config in enumerate(enabled_configs):
if config.get('name') == 'xj' or config.get('display_name') == '线径':
xj_col = 2 + i
# 检查表格中是否已有线径数据
xj_item = self.process_table.item(row_idx, xj_col)
if not xj_item or not xj_item.text().strip():
# 如果表格中没有线径数据,尝试从数据库加载
xj_data = inspection_dao.get_inspection_data_by_config(
self._current_order_code, gc_note, tray_id, position=config.get('position'), config_id=config.get('id')
)
if xj_data and xj_data.get('value'):
# 设置线径数据到表格
xj_item = QTableWidgetItem(str(xj_data.get('value')))
xj_item.setTextAlignment(Qt.AlignCenter)
self.process_table.setItem(row_idx, xj_col, xj_item)
logging.info(f"从数据库加载线径数据: {xj_data.get('value')} 到工程号 {gc_note}")
break
row_idx += 1
# 设置表格为可编辑状态
@ -1806,30 +1816,32 @@ class MainWindow(MainWindowUI):
def update_package_statistics(self):
"""更新包装记录统计数据"""
try:
# 获取包装记录表的行数
package_count = self.record_table.rowCount()
# 使用DAO获取包装记录统计数据
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
stats = inspection_dao.get_package_statistics(self._current_order_code)
# 更新任务表格中的已完成数量
completed_item = QTableWidgetItem(str(package_count))
# 更新任务表格中的总生产数量(总数量)
total_count_item = QTableWidgetItem(str(stats['count_all']))
total_count_item.setTextAlignment(Qt.AlignCenter)
self.task_table.setItem(2, 0, total_count_item)
# 更新任务表格中的总生产公斤(总重量)
total_kg_item = QTableWidgetItem(f"{stats['weight_all']:.2f}")
total_kg_item.setTextAlignment(Qt.AlignCenter)
self.task_table.setItem(2, 1, total_kg_item)
# 更新任务表格中的已完成数量(当前订单数量)
completed_item = QTableWidgetItem(str(stats['count']))
completed_item.setTextAlignment(Qt.AlignCenter)
self.task_table.setItem(2, 2, completed_item)
# 计算已完成公斤数(如果称重列有数值)
completed_kg = 0
for row in range(self.record_table.rowCount()):
weight_item = self.record_table.item(row, 6) # 称重列
if weight_item and weight_item.text():
try:
completed_kg += float(weight_item.text())
except ValueError:
pass
# 更新任务表格中的已完成公斤
completed_kg_item = QTableWidgetItem(str(completed_kg))
# 更新任务表格中的已完成公斤(当前订单重量)
completed_kg_item = QTableWidgetItem(f"{stats['weight']:.2f}")
completed_kg_item.setTextAlignment(Qt.AlignCenter)
self.task_table.setItem(2, 3, completed_kg_item)
logging.info(f"已更新包装记录统计数据: 完成数量={package_count}, 完成公斤={completed_kg}")
logging.info(f"已更新包装记录统计数据: 总生产数量={stats['count_all']}, 总生产公斤={stats['weight_all']:.2f}, 已完成数量={stats['count']}, 已完成公斤={stats['weight']:.2f}")
except Exception as e:
logging.error(f"更新包装记录统计数据失败: {str(e)}")
@ -2820,6 +2832,9 @@ class MainWindow(MainWindowUI):
# 更新订单数量和产量统计数据
self.update_order_statistics()
# 更新包装记录统计数据
self.update_package_statistics()
# 重新连接单元格变更信号
self.process_table.cellChanged.connect(self.handle_inspection_cell_changed)
@ -3318,6 +3333,9 @@ class MainWindow(MainWindowUI):
# 更新订单数量和产量统计数据
self.update_order_statistics()
# 更新包装记录统计数据
self.update_package_statistics()
logging.info(f"NG信号处理完成: 工程号={order_id}, 托盘号={tray_id}, 贴标值={label_value}")
except Exception as e:
logging.error(f"处理NG信号时发生错误: {str(e)}")
@ -3389,6 +3407,24 @@ class MainWindow(MainWindowUI):
def on_diameter_data_received(self, port_name, data):
"""线径数据接收回调函数 - 采用类似称重的逻辑,不使用会话机制"""
# 添加处理锁,避免并发处理导致卡死
if hasattr(self, '_processing_diameter_lock') and self._processing_diameter_lock:
logging.warning(f"已有线径数据处理进行中,忽略本次请求")
return
# 设置处理锁,并添加超时保护
self._processing_diameter_lock = True
self._diameter_processing_start_time = time.time()
# 添加超时检测定时器
if hasattr(self, '_diameter_timeout_timer') and self._diameter_timeout_timer is not None:
self._diameter_timeout_timer.stop()
self._diameter_timeout_timer = QTimer()
self._diameter_timeout_timer.setSingleShot(True)
self._diameter_timeout_timer.timeout.connect(self._reset_diameter_processing_lock)
self._diameter_timeout_timer.start(5000) # 5秒超时保护
modbus_client = None
try:
data_str = data.decode('utf-8') if isinstance(data, bytes) else str(data)
@ -3400,19 +3436,8 @@ class MainWindow(MainWindowUI):
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
@ -3440,8 +3465,12 @@ class MainWindow(MainWindowUI):
# 使用类属性存储最近的测量值,用于稳定性检测
if not hasattr(self, '_diameter_measurements'):
self._diameter_measurements = []
# 添加当前测量值到列表
self._diameter_measurements.append(xj_value)
if len(self._diameter_measurements) > 5:
# 保留最近的10个测量值增加缓冲区大小以便更快收集足够的数据
if len(self._diameter_measurements) > 10:
self._diameter_measurements.pop(0)
# 显示临时值到状态栏
@ -3449,12 +3478,12 @@ class MainWindow(MainWindowUI):
self.statusBar().showMessage(f"线径数据收集中: {xj_value:.3f} ({len(self._diameter_measurements)}/5)", 2000)
return
# 检查稳定性
# 检查稳定性 - 使用最近的5个测量值
measurements = self._diameter_measurements[-5:]
min_value = min(measurements)
max_value = max(measurements)
avg_value = sum(measurements) / len(measurements)
error_range = avg_value * 0.04 # 允许4%误差
error_range = avg_value * 0.05 # 允许5%误差,增加容错范围
if max_value - min_value <= error_range:
# 数据稳定,可以保存
@ -3462,11 +3491,26 @@ class MainWindow(MainWindowUI):
# 查找第一个没有线径数据的行
data_row = None
for row in range(2, self.process_table.rowCount()):
cell_item = self.process_table.item(row, xj_column)
if not cell_item or not cell_item.text().strip() or cell_item.text().strip() == '0':
data_row = row
break
# 如果有最近处理的线径工程号,优先查找该工程号对应的行
if hasattr(self, '_last_processed_gc_note') and self._last_processed_gc_note:
for row in range(2, self.process_table.rowCount()):
order_id_item = self.process_table.item(row, 1)
if order_id_item and order_id_item.text().strip() == self._last_processed_gc_note:
# 检查该行的线径单元格是否为空
cell_item = self.process_table.item(row, xj_column)
if not cell_item or not cell_item.text().strip() or cell_item.text().strip() == '0':
data_row = row
logging.info(f"找到最近处理的线径工程号 {self._last_processed_gc_note} 对应的行: {data_row}")
break
# 如果没有找到最近处理的工程号对应的行,再查找第一个没有线径数据的行
if data_row is None:
for row in range(2, self.process_table.rowCount()):
cell_item = self.process_table.item(row, xj_column)
if not cell_item or not cell_item.text().strip() or cell_item.text().strip() == '0':
data_row = row
break
# 如果没找到空行,使用当前选中行或第一个数据行
if data_row is None:
@ -3487,6 +3531,10 @@ class MainWindow(MainWindowUI):
logging.warning("工程号为空")
return
# 记录最近处理的线径工程号,用于后续线径数据处理
self._last_processed_gc_note = gc_note
logging.info(f"记录最近处理线径数据的工程号: {gc_note}")
# 获取托盘号
tray_id = self.tray_edit.currentText()
@ -3566,12 +3614,11 @@ class MainWindow(MainWindowUI):
# 使用set_inspection_value保存数据
self.set_inspection_value('xj', xj_config, final_value)
logging.info(f"已将稳定的线径值 {final_value:.3f} 保存到工程号 {gc_note} (行 {data_row})")
# 重置测量列表,准备下一次测量
self._diameter_measurements = []
except Exception as e:
logging.error(f"保存线径数据到表格失败: {str(e)}")
# 重置测量列表,准备下一次测量
self._diameter_measurements = []
self.statusBar().showMessage(f"线径数据已保存: {final_value:.3f}", 2000)
else:
# 数据不稳定,继续收集
self.statusBar().showMessage(f"线径数据不稳定: {min_value:.3f} - {max_value:.3f}, 继续测量", 2000)
@ -3593,9 +3640,13 @@ class MainWindow(MainWindowUI):
except Exception as e:
logging.error(f"关闭Modbus客户端连接失败: {str(e)}")
# 确保清理测量列表
if hasattr(self, '_diameter_measurements') and len(self._diameter_measurements) >= 5:
self._diameter_measurements = []
# 释放处理锁
if hasattr(self, '_processing_diameter_lock'):
self._processing_diameter_lock = False
# 停止超时定时器
if hasattr(self, '_diameter_timeout_timer') and self._diameter_timeout_timer is not None:
self._diameter_timeout_timer.stop()
def _save_diameter_to_order(self, order_id, config, value):
"""基于工程号保存线径值,确保即使行号变化也能保存到正确的产品"""
@ -3751,6 +3802,13 @@ class MainWindow(MainWindowUI):
config: 检验项配置
value: 检验值
"""
# 添加防递归锁,避免死循环
if hasattr(self, '_set_inspection_value_lock') and self._set_inspection_value_lock:
logging.warning(f"检测到递归调用set_inspection_value忽略本次调用: {data_type}={value}")
return
self._set_inspection_value_lock = True
try:
# 获取检验项的列索引
config_id = config.get('id')
@ -3806,11 +3864,26 @@ class MainWindow(MainWindowUI):
# 如果没有找到特定行,使用默认逻辑查找第一个没有该检测数据的行
if data_row is None:
for row in range(2, self.process_table.rowCount()):
cell_item = self.process_table.item(row, col_index)
if not cell_item or not cell_item.text().strip() or (data_type == 'xj' and cell_item.text().strip() == '0'):
data_row = row
break
# 新增:检查是否有最近添加的工程号,如果有,优先保存到之前的行
if data_type == 'xj' and hasattr(self, '_last_processed_gc_note') and self._last_processed_gc_note:
# 查找最近处理的工程号对应的行
for row in range(2, self.process_table.rowCount()):
order_id_item = self.process_table.item(row, 1)
if order_id_item and order_id_item.text().strip() == self._last_processed_gc_note:
# 检查该行的线径单元格是否为空
cell_item = self.process_table.item(row, col_index)
if not cell_item or not cell_item.text().strip() or cell_item.text().strip() == '0':
data_row = row
logging.info(f"找到最近处理的工程号 {self._last_processed_gc_note} 对应的空线径单元格,行: {data_row}")
break
# 如果没有找到最近处理的工程号对应的行,或者该行已有线径数据,再查找第一个没有该检测数据的行
if data_row is None:
for row in range(2, self.process_table.rowCount()):
cell_item = self.process_table.item(row, col_index)
if not cell_item or not cell_item.text().strip() or (data_type == 'xj' and cell_item.text().strip() == '0'):
data_row = row
break
# 如果没有找到没有该检测数据的行,使用当前选中行或第一个数据行
if data_row is None:
@ -3831,6 +3904,11 @@ class MainWindow(MainWindowUI):
logging.warning("工程号为空")
return
# 记录最近处理的工程号,用于后续线径数据处理
if data_type == 'xj':
self._last_processed_gc_note = order_id
logging.info(f"记录最近处理线径数据的工程号: {order_id}")
# 暂时断开信号连接避免触发cellChanged信号
try:
self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed)
@ -3870,17 +3948,18 @@ class MainWindow(MainWindowUI):
# 不需要在这里主动触发数据重新加载因为handle_inspection_cell_changed会处理
# 重新连接信号
self.process_table.cellChanged.connect(self.handle_inspection_cell_changed)
logging.info(f"已将{data_type}数据 {formatted_value} 写入工程号 {order_id} (行 {data_row}, 列 {col_index})")
except Exception as e:
logging.error(f"设置检验项值失败: {str(e)}")
# 确保重新连接信号
try:
self.process_table.cellChanged.connect(self.handle_inspection_cell_changed)
except:
pass
except Exception as e:
logging.warning(f"重新连接cellChanged信号失败: {str(e)}")
logging.info(f"成功设置{data_type}{formatted_value} 到工程号 {order_id} 的行 {data_row}")
except Exception as e:
logging.error(f"设置检验值失败: {str(e)}")
finally:
# 释放锁
self._set_inspection_value_lock = False
def handle_tray_changed(self):
"""处理托盘号变更事件,启动监听并加载数据"""
@ -5025,5 +5104,14 @@ class MainWindow(MainWindowUI):
from PySide6.QtWidgets import QMessageBox
QMessageBox.critical(self, "错误", f"处理炉号选择失败: {str(e)}")
def _reset_diameter_processing_lock(self):
"""重置线径处理锁,用于超时保护"""
if hasattr(self, '_processing_diameter_lock') and self._processing_diameter_lock:
processing_time = time.time() - getattr(self, '_diameter_processing_start_time', time.time())
logging.warning(f"线径数据处理超时,强制释放锁。处理时间: {processing_time:.2f}")
self._processing_diameter_lock = False
def safe_str(val):
return "" if val is None else str(val)