1228
This commit is contained in:
@ -58,6 +58,22 @@ class ArtifactBll:
|
||||
"OptTime": datetime.now(),
|
||||
})
|
||||
|
||||
def insert_artifact_bycode(self,model: dict) -> bool:
|
||||
|
||||
"""根据模具编号插入管片任务"""
|
||||
if self.dal.exists_by_module_code(model["MouldCode"]):
|
||||
return False
|
||||
return self.dal.insert_artifact({
|
||||
"MouldCode": model["MouldCode"],
|
||||
"SizeSpecification": model["SizeSpecification"],
|
||||
"BlockNumber": model["BlockNumber"],
|
||||
"BetonVolume": model["BetonVolume"],
|
||||
"PStatus":1,
|
||||
"Status": 2,
|
||||
"Source": 2,
|
||||
"OptTime": datetime.now(),
|
||||
})
|
||||
|
||||
def get_artifacting_task(self) -> ArtifactInfoModel:
|
||||
"""获取正在进行的管片任务数据"""
|
||||
loc_item= self.dal.get_top_artifact(1,"ID desc","Status=2")
|
||||
|
||||
@ -16,7 +16,7 @@ class BaseDal:
|
||||
def __init__(self) -> None:
|
||||
"""初始化数据访问层,创建数据库连接"""
|
||||
# 假设数据库文件在db目录下
|
||||
self.db_dao = SQLiteHandler.get_instance("db/three.db", max_readers=50, busy_timeout=4000)
|
||||
self.db_dao = SQLiteHandler.get_instance("../db/three.db", max_readers=50, busy_timeout=4000)
|
||||
|
||||
class ArtifactDal(BaseDal):
|
||||
def __init__(self):
|
||||
@ -97,6 +97,21 @@ class ArtifactDal(BaseDal):
|
||||
except Exception as e:
|
||||
print(f"根据ID获取构件任务失败: {e}")
|
||||
return False
|
||||
|
||||
def exists_by_module_code(self, module_code: str) -> bool:
|
||||
"""根据模具编号获取构件任务"""
|
||||
try:
|
||||
sql = "SELECT count(1) FROM ArtifactTask WHERE MouldCode = ? and OptTime>?"
|
||||
results = self.db_dao.execute_read(sql, (module_code,datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)))
|
||||
|
||||
rows = list(results)
|
||||
if rows[0][0] == 1:
|
||||
return True
|
||||
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"根据ID获取构件任务失败: {e}")
|
||||
return False
|
||||
|
||||
def get_by_id(self, artifact_id: int) -> Optional[ArtifactInfoModel]:
|
||||
"""根据构件ID获取构件任务"""
|
||||
|
||||
BIN
db/three.db
BIN
db/three.db
Binary file not shown.
BIN
db/three.db-shm
Normal file
BIN
db/three.db-shm
Normal file
Binary file not shown.
BIN
db/three.db-wal
Normal file
BIN
db/three.db-wal
Normal file
Binary file not shown.
BIN
doc/浇筑系统对接接口文档-20251223.doc
Normal file
BIN
doc/浇筑系统对接接口文档-20251223.doc
Normal file
Binary file not shown.
@ -384,7 +384,7 @@ class rfid_service:
|
||||
接收线程的主循环,用于接收RFID推送的数据
|
||||
"""
|
||||
while self._thread_signal:
|
||||
self._pause_receive=False
|
||||
# self._pause_receive=False
|
||||
if self._pause_receive:
|
||||
time.sleep(1)
|
||||
continue
|
||||
@ -409,11 +409,11 @@ class rfid_service:
|
||||
|
||||
received_data += chunk
|
||||
remaining_bytes -= len(chunk)
|
||||
print(f"[数据接收] 已接收 {len(received_data)}/{self._buffer_length} 字节")
|
||||
# print(f"[数据接收] 已接收 {len(received_data)}/{self._buffer_length} 字节")
|
||||
|
||||
# 只有接收到完整的数据才算成功
|
||||
if remaining_bytes == 0:
|
||||
print(f"[数据接收] 成功接收完整数据包 ({self._buffer_length} 字节)")
|
||||
# print(f"[数据接收] 成功接收完整数据包 ({self._buffer_length} 字节)")
|
||||
data = received_data # 保存完整的数据包
|
||||
self._error_count=0
|
||||
else:
|
||||
@ -443,13 +443,13 @@ class rfid_service:
|
||||
|
||||
if data:
|
||||
loc_str = command_hex.parse_user_data_hex(data)
|
||||
raw_data = binascii.hexlify(data).decode()
|
||||
print(f"收到RFID推送数据: {raw_data}")
|
||||
# raw_data = binascii.hexlify(data).decode()
|
||||
# print(f"收到RFID推送数据: {raw_data}")
|
||||
|
||||
# 保存到文件
|
||||
with open('rfid_data.log', 'a') as f:
|
||||
timestamp = datetime.now().strftime('%H:%M:%S.%f')[:-3]
|
||||
f.write(f"[{timestamp}] 解析数据: {loc_str}, 原始数据: {raw_data}\n")
|
||||
# with open('rfid_data.log', 'a') as f:
|
||||
# timestamp = datetime.now().strftime('%H:%M:%S.%f')[:-3]
|
||||
# f.write(f"[{timestamp}] 解析数据: {loc_str}, 原始数据: {raw_data}\n")
|
||||
|
||||
if loc_str:
|
||||
# 将数据添加到缓冲区
|
||||
@ -514,6 +514,7 @@ class rfid_service:
|
||||
"""
|
||||
停止接收RFID推送的数据
|
||||
"""
|
||||
print('RFID 线程停止')
|
||||
self._thread_signal = False
|
||||
if self._receive_thread:
|
||||
self._receive_thread.join(timeout=2.0)
|
||||
|
||||
@ -73,7 +73,7 @@ class RelayController:
|
||||
sock.connect((self.host, self.port))
|
||||
sock.send(byte_data)
|
||||
response = sock.recv(1024)
|
||||
print(f"收到继电器响应: {binascii.hexlify(response)}")
|
||||
# print(f"收到继电器响应: {binascii.hexlify(response)}")
|
||||
return response
|
||||
except Exception as e:
|
||||
print(f"继电器通信错误: {e}")
|
||||
@ -97,7 +97,7 @@ class RelayController:
|
||||
def control(self, device, action):
|
||||
"""控制继电器"""
|
||||
if device in self.relay_commands and action in self.relay_commands[device]:
|
||||
print(f"发送控制继电器命令 {device} {action}")
|
||||
# print(f"发送控制继电器命令 {device} {action}")
|
||||
self.send_command(self.relay_commands[device][action])
|
||||
else:
|
||||
print(f"无效设备或动作: {device}, {action}")
|
||||
@ -109,6 +109,13 @@ class RelayController:
|
||||
self.control(self.DOOR_UPPER_CLOSE, 'open')
|
||||
# 异步5秒后关闭
|
||||
threading.Thread(target=self._close_upper_s, daemon=True,name="close_upper_s").start()
|
||||
|
||||
def control_upper_close_after(self):
|
||||
"""控制上料斗关在几秒后"""
|
||||
# 关闭上料斗出砼门
|
||||
self.control(self.DOOR_UPPER_OPEN, 'close')
|
||||
# 异步5秒后关闭
|
||||
threading.Thread(target=self._close_upper_after_s, daemon=True,name="close_upper_after_s").start()
|
||||
|
||||
def control_upper_close_sync(self,duration=5):
|
||||
self.control(self.DOOR_UPPER_OPEN, 'close')
|
||||
@ -224,6 +231,24 @@ class RelayController:
|
||||
self.control(self.DOOR_UPPER_CLOSE, 'close')
|
||||
print("上料斗关闭完成")
|
||||
|
||||
def _close_upper_after_s(self):
|
||||
"""
|
||||
异步5秒后关闭上料斗20秒
|
||||
"""
|
||||
|
||||
# time.sleep(5)
|
||||
self.control_arch_upper_open_sync(5)
|
||||
self.control(self.DOOR_UPPER_CLOSE, 'open')
|
||||
time.sleep(1)
|
||||
self.control(self.DOOR_UPPER_CLOSE, 'close')
|
||||
self.control_arch_upper_open_sync(5)
|
||||
self.control_arch_upper_open_sync(5)
|
||||
self.control_arch_upper_open_async(8)
|
||||
self.control(self.DOOR_UPPER_CLOSE, 'open')
|
||||
time.sleep(20)
|
||||
self.control(self.DOOR_UPPER_CLOSE, 'close')
|
||||
print("上料斗关闭完成")
|
||||
|
||||
def _close_lower_5s(self):
|
||||
time.sleep(6)
|
||||
self.control(self.DOOR_LOWER_CLOSE, 'close')
|
||||
@ -238,19 +263,40 @@ class RelayController:
|
||||
self.control(self.BREAK_ARCH_LOWER, 'open')
|
||||
# 异步5秒后关闭
|
||||
threading.Thread(target=self._close_break_arch_lower, daemon=True,name="_close_break_arch_lower").start()
|
||||
|
||||
def control_arch_lower_open_sync(self,duration):
|
||||
"""控制下料斗振动"""
|
||||
self.control(self.BREAK_ARCH_LOWER, 'open')
|
||||
# 异步5秒后关闭
|
||||
time.sleep(duration)
|
||||
self.control(self.BREAK_ARCH_LOWER, 'close')
|
||||
|
||||
def control_arch_upper_open_sync(self,duration):
|
||||
"""控制下料斗振动"""
|
||||
self.control(self.BREAK_ARCH_UPPER, 'open')
|
||||
# 异步5秒后关闭
|
||||
time.sleep(duration)
|
||||
self.control(self.BREAK_ARCH_UPPER, 'close')
|
||||
|
||||
def _close_break_arch_lower(self):
|
||||
time.sleep(3)
|
||||
self.control(self.BREAK_ARCH_LOWER, 'close')
|
||||
|
||||
|
||||
def control_arch_upper_open(self):
|
||||
"""控制上料斗关"""
|
||||
def control_arch_upper_open_async(self,delay_seconds: float = 15):
|
||||
"""异步控制上料斗振动
|
||||
|
||||
Args:
|
||||
delay_seconds: 延迟关闭时间(秒),默认15秒
|
||||
"""
|
||||
# 关闭下料斗出砼门
|
||||
self.control(self.BREAK_ARCH_UPPER, 'open')
|
||||
# 异步5秒后关闭
|
||||
threading.Thread(target=self._close_break_arch_upper, daemon=True,name="_close_break_arch_upper").start()
|
||||
def _close_break_arch_upper(self):
|
||||
time.sleep(3)
|
||||
threading.Thread(target=lambda d: self._close_break_arch_upper(delay_seconds),args=(delay_seconds,), daemon=True, name="_close_break_arch_upper").start()
|
||||
|
||||
def _close_break_arch_upper(self, delay_seconds: float = 15):
|
||||
time.sleep(delay_seconds)
|
||||
print(f"上料斗振动关闭完成,延迟{delay_seconds}秒")
|
||||
self.control(self.BREAK_ARCH_UPPER, 'close')
|
||||
|
||||
|
||||
|
||||
@ -80,7 +80,7 @@ class TransmitterController:
|
||||
self.relay_controller.modbus_client.close()
|
||||
|
||||
# 直接读取 变送器返回的数据并解析
|
||||
def read_data_normal(self, transmitter_id):
|
||||
def read_data_sub(self, transmitter_id):
|
||||
|
||||
"""
|
||||
Args: transmitter_id 为1 表示上料斗, 为2 表示下料斗
|
||||
@ -90,7 +90,7 @@ class TransmitterController:
|
||||
BUFFER_SIZE= 1024
|
||||
IP = None
|
||||
PORT = None
|
||||
weight = 0
|
||||
weight = None
|
||||
if transmitter_id == 1:
|
||||
# 上料斗变送器的信息:
|
||||
IP = ini_manager.upper_transmitter_ip
|
||||
@ -102,7 +102,7 @@ class TransmitterController:
|
||||
|
||||
if not IP or not PORT:
|
||||
print(f"未配置变送器 {transmitter_id} 的IP或PORT")
|
||||
return 0
|
||||
return None
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
try:
|
||||
s.settimeout(TIMEOUT)
|
||||
@ -134,7 +134,7 @@ class TransmitterController:
|
||||
# 成功返回重量(int),失败返回None
|
||||
return weight
|
||||
|
||||
def read_data(self, transmitter_id):
|
||||
def read_data_sub_test(self, transmitter_id):
|
||||
|
||||
"""
|
||||
测试用:模拟读取变送器数据mock
|
||||
@ -266,4 +266,16 @@ class TransmitterController:
|
||||
except (IndexError, ValueError, UnicodeDecodeError) as e:
|
||||
# print(f"数据解析失败:{e},原始数据包:{packet_data}")
|
||||
return None
|
||||
|
||||
|
||||
def read_data(self,transmitter_id):
|
||||
"""获取重量函数:根据变送器ID获取当前重量,三次"""
|
||||
max_try_times=5
|
||||
try_times=0
|
||||
while try_times<max_try_times:
|
||||
weight=self.read_data_sub(transmitter_id)
|
||||
if weight is not None:
|
||||
return weight
|
||||
try_times+=1
|
||||
print(f'-----获取重量异常-------------- transmitter_id: {transmitter_id}')
|
||||
print(f'-----获取重量异常-------------- transmitter_id: {transmitter_id}')
|
||||
return None
|
||||
31
test_dal_debug.py
Normal file
31
test_dal_debug.py
Normal file
@ -0,0 +1,31 @@
|
||||
from busisness.dals import ArtifactDal
|
||||
|
||||
# 测试 exists_by_module_code 方法
|
||||
dal = ArtifactDal()
|
||||
|
||||
# 测试1: 不存在的模具编号
|
||||
print("=== 测试1: 不存在的模具编号 ===")
|
||||
result = dal.exists_by_module_code("不存在的模具编号")
|
||||
print(f"结果: {result}")
|
||||
print()
|
||||
|
||||
# 测试2: 存在的模具编号(如果有数据的话)
|
||||
print("=== 测试2: 存在的模具编号 ===")
|
||||
result = dal.exists_by_module_code("TEST001")
|
||||
print(f"结果: {result}")
|
||||
print()
|
||||
|
||||
# 测试3: 直接查询数据库验证
|
||||
print("=== 测试3: 直接SQL查询验证 ===")
|
||||
db_dao = dal.db_dao
|
||||
sql = "SELECT count(1) as cnt FROM ArtifactTask WHERE MouldCode = ?"
|
||||
results = db_dao.execute_read(sql, ("不存在的模具编号",))
|
||||
print(f"查询结果类型: {type(results)}")
|
||||
print(f"查询结果: {results}")
|
||||
rows = list(results)
|
||||
print(f"rows: {rows}")
|
||||
if rows:
|
||||
print(f"rows[0]: {rows[0]}")
|
||||
print(f"rows[0][0]: {rows[0][0]}")
|
||||
else:
|
||||
print("rows为空列表")
|
||||
@ -1,16 +1,10 @@
|
||||
# main.py
|
||||
import time
|
||||
from datetime import datetime
|
||||
from config.settings import app_set_config
|
||||
from core.system import FeedingControlSystem
|
||||
from hardware import relay
|
||||
from hardware.relay import RelayController
|
||||
from hardware.transmitter import TransmitterController
|
||||
import threading
|
||||
import time
|
||||
import cv2
|
||||
import os
|
||||
from core.state import FeedStatus
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@ -6,25 +6,55 @@ import os
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import time
|
||||
from hardware.RFID.rfid_service import rfid_service
|
||||
from busisness.blls import ArtifactBll
|
||||
from busisness.models import ArtifactInfoModel
|
||||
|
||||
|
||||
|
||||
def test_data_callback(status,raw_data):
|
||||
rfid=None
|
||||
rfid_before_mould=''
|
||||
artifact_bll=ArtifactBll()
|
||||
def test_data_callback(status,data):
|
||||
"""
|
||||
测试用的数据接收回调函数
|
||||
"""
|
||||
if status:
|
||||
print(f"[回调] 收到RFID数据: {raw_data}")
|
||||
else:
|
||||
print(f"[回调] 读取RFID数据为空或失败")
|
||||
global rfid_before_mould
|
||||
try:
|
||||
if status==1:
|
||||
#成功读取到RFID标签
|
||||
#检查标识是否符号要求
|
||||
if data:
|
||||
loc_array=data.strip(',').split(',')
|
||||
if len(loc_array)==4:
|
||||
if rfid_before_mould!=loc_array[0]:
|
||||
model={
|
||||
'MouldCode':loc_array[0],
|
||||
'BlockNumber':loc_array[1],
|
||||
'SizeSpecification':loc_array[2],
|
||||
'BetonVolume':loc_array[3]
|
||||
}
|
||||
artifact_bll.insert_artifact_bycode(model)
|
||||
rfid_before_mould=loc_array[0]
|
||||
print(f"RFID-生产模具车号:{loc_array[0]}")
|
||||
else:
|
||||
print(f"RFID-重复生产模具车号:{loc_array[0]}")
|
||||
|
||||
else:
|
||||
print("RFID标签格式错误")
|
||||
print(f"成功读取到RFID标签:{data}")
|
||||
else:
|
||||
self.rfid_flag_succ=False
|
||||
print("读取RFID标签失败")
|
||||
except Exception as e:
|
||||
print(f"RFID回调处理异常: {e}")
|
||||
|
||||
|
||||
def test_rfid_functions():
|
||||
"""
|
||||
测试RFIDHardware的主要功能
|
||||
"""
|
||||
global rfid
|
||||
# 初始化RFID控制器
|
||||
rfid = rfid_service(host='192.168.250.77', port=6000)
|
||||
rfid = rfid_service(host='192.168.250.67', port=6000)
|
||||
|
||||
# print("=== RFID硬件测试开始 ===")
|
||||
|
||||
@ -96,21 +126,38 @@ def test_rfid_functions():
|
||||
# rfid._data_buffer.append('THR B1-12,B1,6600 * 1500,1.900')
|
||||
# rfid._data_buffer.append('THR B1-12,B1,6600 * 1500,1.900')
|
||||
# rfid._process_collected_data()
|
||||
rfid.start_receiver(callback=test_data_callback)
|
||||
rfid.start_receiver(callback=test_data_callback)
|
||||
while True:
|
||||
rfid._pause_receive = False
|
||||
time.sleep(10)
|
||||
|
||||
|
||||
# print("接收线程已启动,等待接收数据...")
|
||||
# 等待5秒模拟接收过程1111111111111
|
||||
time.sleep(60*60)
|
||||
finally:
|
||||
# 确保停止接收线程
|
||||
# rfid.stop_receiver()
|
||||
rfid.stop_receiver()
|
||||
|
||||
print("\n=== RFID硬件测试结束 ===")
|
||||
# print("\n=== RFID硬件测试结束 ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
# model={
|
||||
# 'ArtifactID':0,
|
||||
# 'MouldCode':'SHR2B1-4',
|
||||
# 'BlockNumber':'B2',
|
||||
# 'SizeSpecification':'6600*1500',
|
||||
# 'BetonVolume':1.910
|
||||
# }
|
||||
# artifact_bll.insert_artifact_bycode(model)
|
||||
test_rfid_functions()
|
||||
except KeyboardInterrupt:
|
||||
if rfid is not None:
|
||||
rfid.stop_receiver()
|
||||
print("\n测试被用户中断")
|
||||
except Exception as e:
|
||||
if rfid is not None:
|
||||
rfid.stop_receiver()
|
||||
print(f"测试过程中发生错误: {e}")
|
||||
120
vision/muju_cls/main.py
Normal file
120
vision/muju_cls/main.py
Normal file
@ -0,0 +1,120 @@
|
||||
import os
|
||||
import cv2
|
||||
from rknnlite.api import RKNNLite
|
||||
|
||||
# classify_single_image, StableClassJudge, CLASS_NAMES 已在 muju_cls_rknn 中定义
|
||||
from muju_cls_rknn import classify_single_image, StableClassJudge, CLASS_NAMES
|
||||
|
||||
|
||||
def run_stable_classification_loop(
|
||||
model_path,
|
||||
roi_file,
|
||||
image_source,
|
||||
stable_frames=3,
|
||||
display_scale=0.5, # 显示缩放比例(0.5 = 显示为原来 50%)
|
||||
show_window=False # 是否显示窗口
|
||||
):
|
||||
"""
|
||||
image_source: cv2.VideoCapture 对象
|
||||
"""
|
||||
|
||||
judge = StableClassJudge(
|
||||
stable_frames=stable_frames,
|
||||
ignore_class=2 # 忽略“有遮挡”类别参与稳定判断
|
||||
)
|
||||
|
||||
cap = image_source
|
||||
if not hasattr(cap, "read"):
|
||||
raise TypeError("image_source 必须是 cv2.VideoCapture 实例")
|
||||
|
||||
# 可选:创建可缩放窗口
|
||||
if show_window:
|
||||
cv2.namedWindow("RTSP Stream - Press 'q' to quit", cv2.WINDOW_NORMAL)
|
||||
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
print("无法读取视频帧(可能是流断开或结束)")
|
||||
break
|
||||
|
||||
# 上下左右翻转
|
||||
frame = cv2.flip(frame, -1)
|
||||
|
||||
# ---------------------------
|
||||
# 单帧推理
|
||||
# ---------------------------
|
||||
result = classify_single_image(frame, model_path, roi_file)
|
||||
|
||||
class_id = result["class_id"]
|
||||
class_name = result["class"]
|
||||
score = result["score"]
|
||||
|
||||
print(f"[FRAME] {class_name} | conf={score:.3f}")
|
||||
|
||||
# ---------------------------
|
||||
# 稳定判断
|
||||
# ---------------------------
|
||||
stable_class_id = judge.update(class_id)
|
||||
|
||||
if stable_class_id is not None:
|
||||
print(f"\n稳定输出: {CLASS_NAMES[stable_class_id]}\n")
|
||||
|
||||
# ---------------------------
|
||||
# 显示画面(缩小窗口)
|
||||
# ---------------------------
|
||||
if show_window:
|
||||
h, w = frame.shape[:2]
|
||||
display_frame = cv2.resize(
|
||||
frame,
|
||||
(int(w * display_scale), int(h * display_scale)),
|
||||
interpolation=cv2.INTER_AREA
|
||||
)
|
||||
|
||||
cv2.imshow("RTSP Stream - Press 'q' to quit", display_frame)
|
||||
|
||||
if cv2.waitKey(1) & 0xFF == ord('q'):
|
||||
break
|
||||
|
||||
cap.release()
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# ---------------------------
|
||||
# 配置参数
|
||||
# ---------------------------
|
||||
MODEL_PATH = "muju_cls.rknn"
|
||||
ROI_FILE = "./roi_coordinates/muju_roi.txt"
|
||||
RTSP_URL = "rtsp://admin:XJ123456@192.168.250.61:554/streaming/channels/101"
|
||||
|
||||
STABLE_FRAMES = 3
|
||||
DISPLAY_SCALE = 0.5 # 显示窗口缩放比例
|
||||
SHOW_WINDOW = False # 部署时改成 False,测试的时候打开
|
||||
|
||||
# ---------------------------
|
||||
# 打开 RTSP 视频流
|
||||
# ---------------------------
|
||||
print(f"正在连接 RTSP 流: {RTSP_URL}")
|
||||
cap = cv2.VideoCapture(RTSP_URL)
|
||||
|
||||
# 降低 RTSP 延迟(部分摄像头支持)
|
||||
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
||||
|
||||
if not cap.isOpened():
|
||||
print("无法打开 RTSP 流,请检查网络、账号密码或 URL")
|
||||
exit(1)
|
||||
|
||||
print("RTSP 流连接成功,开始推理...")
|
||||
|
||||
# ---------------------------
|
||||
# 启动稳定分类循环三帧稳定判断
|
||||
# ---------------------------
|
||||
run_stable_classification_loop(
|
||||
model_path=MODEL_PATH,
|
||||
roi_file=ROI_FILE,
|
||||
image_source=cap,
|
||||
stable_frames=STABLE_FRAMES,
|
||||
display_scale=DISPLAY_SCALE,
|
||||
show_window=SHOW_WINDOW
|
||||
)
|
||||
|
||||
BIN
vision/muju_cls/muju_cls - 副本.rknn
Normal file
BIN
vision/muju_cls/muju_cls - 副本.rknn
Normal file
Binary file not shown.
BIN
vision/muju_cls/muju_cls.rknn
Normal file
BIN
vision/muju_cls/muju_cls.rknn
Normal file
Binary file not shown.
282
vision/muju_cls/muju_cls_rknn.py
Normal file
282
vision/muju_cls/muju_cls_rknn.py
Normal file
@ -0,0 +1,282 @@
|
||||
import os
|
||||
import cv2
|
||||
import numpy as np
|
||||
from rknnlite.api import RKNNLite
|
||||
|
||||
from collections import deque
|
||||
|
||||
class StableClassJudge:
|
||||
"""
|
||||
连续三帧稳定判决器:
|
||||
- class0 / class1 连续 3 帧 -> 输出
|
||||
- class2 -> 清空计数,重新统计
|
||||
"""
|
||||
|
||||
def __init__(self, stable_frames=3, ignore_class=2):
|
||||
self.stable_frames = stable_frames
|
||||
self.ignore_class = ignore_class
|
||||
self.buffer = deque(maxlen=stable_frames)
|
||||
|
||||
def reset(self):
|
||||
self.buffer.clear()
|
||||
|
||||
def update(self, class_id):
|
||||
"""
|
||||
输入单帧分类结果
|
||||
返回:
|
||||
- None:尚未稳定
|
||||
- class_id:稳定输出结果
|
||||
"""
|
||||
|
||||
# 遇到 class2,直接清空重新计数
|
||||
if class_id == self.ignore_class:
|
||||
self.reset()
|
||||
return None
|
||||
|
||||
self.buffer.append(class_id)
|
||||
|
||||
# 缓冲未满
|
||||
if len(self.buffer) < self.stable_frames:
|
||||
return None
|
||||
|
||||
# 三帧完全一致
|
||||
if len(set(self.buffer)) == 1:
|
||||
stable_class = self.buffer[0]
|
||||
self.reset() # 输出一次后重新计数(防止重复触发)
|
||||
return stable_class
|
||||
|
||||
return None
|
||||
|
||||
# ---------------------------
|
||||
# 三分类映射,模具车1是小的,模具车2是大的
|
||||
# ---------------------------
|
||||
CLASS_NAMES = {
|
||||
0: "模具车1",
|
||||
1: "模具车2",
|
||||
2: "有遮挡"
|
||||
}
|
||||
|
||||
# ---------------------------
|
||||
# RKNN 全局实例(只加载一次)
|
||||
# ---------------------------
|
||||
_global_rknn = None
|
||||
|
||||
|
||||
def init_rknn_model(model_path):
|
||||
global _global_rknn
|
||||
if _global_rknn is not None:
|
||||
return _global_rknn
|
||||
|
||||
rknn = RKNNLite(verbose=False)
|
||||
ret = rknn.load_rknn(model_path)
|
||||
if ret != 0:
|
||||
raise RuntimeError(f"Load RKNN failed: {ret}")
|
||||
|
||||
ret = rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0)
|
||||
if ret != 0:
|
||||
raise RuntimeError(f"Init runtime failed: {ret}")
|
||||
|
||||
_global_rknn = rknn
|
||||
print(f"[INFO] RKNN 模型加载成功: {model_path}")
|
||||
return rknn
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# 预处理
|
||||
# ---------------------------
|
||||
def letterbox(image, new_size=640, color=(114, 114, 114)):
|
||||
h, w = image.shape[:2]
|
||||
scale = min(new_size / h, new_size / w)
|
||||
nh, nw = int(h * scale), int(w * scale)
|
||||
resized = cv2.resize(image, (nw, nh))
|
||||
new_img = np.full((new_size, new_size, 3), color, dtype=np.uint8)
|
||||
top = (new_size - nh) // 2
|
||||
left = (new_size - nw) // 2
|
||||
new_img[top:top + nh, left:left + nw] = resized
|
||||
return new_img
|
||||
|
||||
|
||||
def resize_stretch(image, size=640):
|
||||
return cv2.resize(image, (size, size))
|
||||
|
||||
|
||||
def preprocess_image_for_rknn(
|
||||
img,
|
||||
size=640,
|
||||
resize_mode="stretch",
|
||||
to_rgb=True,
|
||||
normalize=False,
|
||||
layout="NHWC"
|
||||
):
|
||||
if resize_mode == "letterbox":
|
||||
img_box = letterbox(img, new_size=size)
|
||||
else:
|
||||
img_box = resize_stretch(img, size=size)
|
||||
|
||||
if to_rgb:
|
||||
img_box = cv2.cvtColor(img_box, cv2.COLOR_BGR2RGB)
|
||||
|
||||
img_f = img_box.astype(np.float32)
|
||||
|
||||
if normalize:
|
||||
img_f /= 255.0
|
||||
|
||||
if layout == "NHWC":
|
||||
out = np.expand_dims(img_f, axis=0)
|
||||
else:
|
||||
out = np.expand_dims(np.transpose(img_f, (2, 0, 1)), axis=0)
|
||||
|
||||
return np.ascontiguousarray(out)
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# 单次 RKNN 推理(三分类)
|
||||
# ---------------------------
|
||||
def rknn_classify_preprocessed(input_tensor, model_path):
|
||||
rknn = init_rknn_model(model_path)
|
||||
|
||||
input_tensor = np.ascontiguousarray(input_tensor.astype(np.float32))
|
||||
outs = rknn.inference([input_tensor])
|
||||
|
||||
pred = outs[0].reshape(-1).astype(float) # shape = (3,)
|
||||
class_id = int(np.argmax(pred))
|
||||
|
||||
return class_id, pred
|
||||
|
||||
# ---------------------------
|
||||
# ROI
|
||||
# ---------------------------
|
||||
def load_single_roi(txt_path):
|
||||
if not os.path.exists(txt_path):
|
||||
raise RuntimeError(f"ROI 文件不存在: {txt_path}")
|
||||
|
||||
with open(txt_path) as f:
|
||||
for line in f:
|
||||
s = line.strip()
|
||||
if not s:
|
||||
continue
|
||||
x, y, w, h = map(int, s.split(','))
|
||||
return (x, y, w, h)
|
||||
|
||||
raise RuntimeError("ROI 文件为空")
|
||||
|
||||
|
||||
def crop_and_return_roi(img, roi):
|
||||
x, y, w, h = roi
|
||||
h_img, w_img = img.shape[:2]
|
||||
|
||||
if x < 0 or y < 0 or x + w > w_img or y + h > h_img:
|
||||
raise RuntimeError(f"ROI 超出图像范围: {roi}")
|
||||
|
||||
return img[y:y + h, x:x + w]
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# 单张图片推理(三分类)
|
||||
# ---------------------------
|
||||
def classify_single_image(
|
||||
frame,
|
||||
model_path,
|
||||
roi_file,
|
||||
size=640,
|
||||
resize_mode="stretch",
|
||||
to_rgb=True,
|
||||
normalize=False,
|
||||
layout="NHWC"
|
||||
):
|
||||
if frame is None:
|
||||
raise FileNotFoundError("输入帧为空")
|
||||
|
||||
roi = load_single_roi(roi_file)
|
||||
roi_img = crop_and_return_roi(frame, roi)
|
||||
|
||||
input_tensor = preprocess_image_for_rknn(
|
||||
roi_img,
|
||||
size=size,
|
||||
resize_mode=resize_mode,
|
||||
to_rgb=to_rgb,
|
||||
normalize=normalize,
|
||||
layout=layout
|
||||
)
|
||||
|
||||
class_id, probs = rknn_classify_preprocessed(input_tensor, model_path)
|
||||
class_name = CLASS_NAMES.get(class_id, f"未知类别({class_id})")
|
||||
|
||||
return {
|
||||
"class_id": class_id,
|
||||
"class": class_name,
|
||||
"score": round(float(probs[class_id]), 4),
|
||||
"raw": probs.tolist()
|
||||
}
|
||||
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# 示例调用
|
||||
# ---------------------------
|
||||
if __name__ == "__main__":
|
||||
model_path = "muju_cls.rknn"
|
||||
roi_file = "./roi_coordinates/muju_roi.txt"
|
||||
image_path = "./test_image/test.png"
|
||||
|
||||
frame = cv2.imread(image_path)
|
||||
if frame is None:
|
||||
raise FileNotFoundError(f"无法读取图片: {image_path}")
|
||||
|
||||
result = classify_single_image(frame, model_path, roi_file)
|
||||
print("[RESULT]", result)
|
||||
|
||||
# ---------------------------
|
||||
# 示例判断逻辑
|
||||
'''
|
||||
import cv2
|
||||
from muju_cls_rknn import classify_single_image,StableClassJudge,CLASS_NAMES
|
||||
|
||||
def run_stable_classification_loop(
|
||||
model_path,
|
||||
roi_file,
|
||||
image_source,
|
||||
stable_frames=3
|
||||
):
|
||||
"""
|
||||
image_source:
|
||||
- cv2.VideoCapture
|
||||
"""
|
||||
judge = StableClassJudge(
|
||||
stable_frames=stable_frames,
|
||||
ignore_class=2 # 有遮挡
|
||||
)
|
||||
|
||||
cap = image_source
|
||||
if not hasattr(cap, "read"):
|
||||
raise TypeError("image_source 必须是 cv2.VideoCapture")
|
||||
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
# 上下左右翻转
|
||||
frame = cv2.flip(frame, -1)
|
||||
|
||||
if not ret:
|
||||
print("读取帧失败,退出")
|
||||
break
|
||||
|
||||
result = classify_single_image(frame, model_path, roi_file)
|
||||
|
||||
class_id = result["class_id"]
|
||||
class_name = result["class"]
|
||||
score = result["score"]
|
||||
|
||||
print(f"[FRAME] {class_name} conf={score}")
|
||||
|
||||
stable = judge.update(class_id)
|
||||
|
||||
if stable is not None:
|
||||
print(f"\n稳定输出: {CLASS_NAMES[stable]} \n")
|
||||
|
||||
if cv2.waitKey(1) & 0xFF == ord('q'):
|
||||
break
|
||||
|
||||
cap.release()
|
||||
cv2.destroyAllWindows()
|
||||
'''
|
||||
# ---------------------------
|
||||
87
vision/muju_cls/muju_utils.py
Normal file
87
vision/muju_cls/muju_utils.py
Normal file
@ -0,0 +1,87 @@
|
||||
import os
|
||||
import cv2
|
||||
from rknnlite.api import RKNNLite
|
||||
import time
|
||||
|
||||
# classify_single_image, StableClassJudge, CLASS_NAMES 已在 muju_cls_rknn 中定义
|
||||
from .muju_cls_rknn import classify_single_image, StableClassJudge, CLASS_NAMES
|
||||
|
||||
# 获取当前文件所在目录的绝对路径
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
def run_stable_classification_loop():
|
||||
"""
|
||||
image_source: cv2.VideoCapture 对象
|
||||
"""
|
||||
_ret=None
|
||||
# 使用相对于当前文件的绝对路径
|
||||
model_path = os.path.join(current_dir, "muju_cls.rknn")
|
||||
roi_file = os.path.join(current_dir, "roi_coordinates", "muju_roi.txt")
|
||||
RTSP_URL = "rtsp://admin:XJ123456@192.168.250.61:554/streaming/channels/101"
|
||||
stable_frames=3
|
||||
print(f"正在连接 RTSP 流: {RTSP_URL}")
|
||||
cap =None
|
||||
try:
|
||||
cap = cv2.VideoCapture(RTSP_URL)
|
||||
# 降低 RTSP 延迟(部分摄像头支持)
|
||||
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
||||
|
||||
if not cap.isOpened():
|
||||
print("无法打开 RTSP 流,请检查网络、账号密码或 URL")
|
||||
return None
|
||||
|
||||
print("RTSP 流连接成功,开始推理...")
|
||||
|
||||
judge = StableClassJudge(
|
||||
stable_frames=stable_frames,
|
||||
ignore_class=2 # 忽略“有遮挡”类别参与稳定判断
|
||||
)
|
||||
|
||||
if not hasattr(cap, "read"):
|
||||
raise TypeError("image_source 必须是 cv2.VideoCapture 实例")
|
||||
_max_count=10
|
||||
while True:
|
||||
_max_count=_max_count-1
|
||||
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
print("无法读取视频帧(可能是流断开或结束)")
|
||||
continue
|
||||
# 上下左右翻转
|
||||
frame = cv2.flip(frame, -1)
|
||||
|
||||
# ---------------------------
|
||||
# 单帧推理
|
||||
# ---------------------------
|
||||
result = classify_single_image(frame, model_path, roi_file)
|
||||
|
||||
class_id = result["class_id"]
|
||||
class_name = result["class"]
|
||||
score = result["score"]
|
||||
|
||||
print(f"[FRAME] {class_name} | conf={score:.3f}")
|
||||
|
||||
# ---------------------------
|
||||
# 稳定判断
|
||||
# ---------------------------
|
||||
stable_class_id = judge.update(class_id)
|
||||
|
||||
if stable_class_id is not None:
|
||||
_ret=CLASS_NAMES[stable_class_id]
|
||||
if _ret is None:
|
||||
print("-------当前类别为:为空,继续等待稳定------")
|
||||
continue
|
||||
if _ret=="模具车1" or _ret=="模具车2":
|
||||
break
|
||||
print(f"当前类别为:{_ret},继续等待稳定...")
|
||||
else:
|
||||
print("当前类别为:为空,继续等待稳定...")
|
||||
|
||||
time.sleep(0.1)
|
||||
finally:
|
||||
if cap is not None:
|
||||
cap.release()
|
||||
return _ret
|
||||
|
||||
|
||||
|
||||
|
||||
1
vision/muju_cls/roi_coordinates/muju_roi.txt
Normal file
1
vision/muju_cls/roi_coordinates/muju_roi.txt
Normal file
@ -0,0 +1 @@
|
||||
2,880,385,200
|
||||
BIN
vision/muju_cls/test.png
Normal file
BIN
vision/muju_cls/test.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 MiB |
1
vision/roi_coordinates/muju_roi.txt
Normal file
1
vision/roi_coordinates/muju_roi.txt
Normal file
@ -0,0 +1 @@
|
||||
2,880,385,200
|
||||
@ -6,7 +6,9 @@ from hardware.transmitter import TransmitterController
|
||||
import time
|
||||
import threading
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from hardware.upper_plc import OmronFinsPollingService
|
||||
from vision.muju_cls.muju_utils import run_stable_classification_loop
|
||||
|
||||
class VisualCallback:
|
||||
# 类变量,用于存储实例引用,实现单例检测
|
||||
@ -26,19 +28,10 @@ class VisualCallback:
|
||||
if hasattr(self, '_initialized') and self._initialized:
|
||||
return
|
||||
|
||||
self.angle_mode = "normal"
|
||||
self.relay_controller = RelayController()
|
||||
self.transmitter_controller = TransmitterController(self.relay_controller)
|
||||
self.init_weight = 100
|
||||
self.mould_finish_weight = 0
|
||||
self.mould_need_weight = 100
|
||||
self.finish_count = 0
|
||||
self.overflow = False
|
||||
self.is_start_visual=False
|
||||
|
||||
# 线程安全的参数传递
|
||||
self._current_angle = None
|
||||
self._overflow_detected = None
|
||||
self._new_data_available = threading.Event()
|
||||
self._is_processing = threading.Lock()
|
||||
self._stop_event = threading.Event()
|
||||
@ -47,43 +40,91 @@ class VisualCallback:
|
||||
self._door_control_lock = threading.Lock()
|
||||
# 记录当前控制门的线程名称,用于调试
|
||||
self._current_controlling_thread = None
|
||||
# 新增标志位:指示safe_control_lower_close是否正在执行
|
||||
self._is_safe_closing = False
|
||||
|
||||
self._is_feed_start=True
|
||||
#是否启动后的第一个模具
|
||||
self._is_first_module=True
|
||||
self.init_val()
|
||||
# self._setup_logging_2()
|
||||
#F块完成重量的70%,控制夹脚,F块多于这个比例就没有记录了(注意)
|
||||
self._max_f_angle_ratio=0.7
|
||||
#完成多少,调整角度比例 ,多于0.8就没记录了(注意)
|
||||
self._max_angle_radio=0.8
|
||||
#完成多少,忽略未浇筑满
|
||||
self._max_ignore_radio=0.5
|
||||
|
||||
# 创建并启动单个持续运行的线程
|
||||
# self.plc_data=5
|
||||
self.plc_service = OmronFinsPollingService("192.168.250.233")
|
||||
self.plc_service.register_data_callback(self.on_plc_update)
|
||||
# self.plc_service.register_status_callback(self.on_status_change)
|
||||
self.plc_service.start_polling(interval=2.0)
|
||||
|
||||
# 创建并启动单个持续运行的线程
|
||||
self.callback_thread = threading.Thread(
|
||||
target=self._run_thread_loop,
|
||||
daemon=True
|
||||
)
|
||||
self.callback_thread.start()
|
||||
|
||||
|
||||
self.feed_thread = threading.Thread(
|
||||
target=self._run_feed,
|
||||
daemon=True
|
||||
)
|
||||
self.feed_thread.start()
|
||||
|
||||
"""启动系统监控"""
|
||||
self.monitor_thread = threading.Thread(
|
||||
target=self._monitor_loop,
|
||||
daemon=True,
|
||||
name='monitor'
|
||||
)
|
||||
self.monitor_thread.start()
|
||||
|
||||
def init_val(self):
|
||||
#初始化值
|
||||
"""初始化视觉回调处理器"""
|
||||
self.angle_mode = "normal"
|
||||
self.overflow = False
|
||||
self.is_start_visual=False
|
||||
|
||||
# 线程安全的参数传递
|
||||
self._current_angle = None
|
||||
self._overflow_detected = None
|
||||
# 新增标志位:指示safe_control_lower_close是否正在执行
|
||||
self._is_safe_closing = False
|
||||
|
||||
self._is_feed_start=True
|
||||
#未浇筑满时间,用于确定是否进入未浇筑满
|
||||
self._before_finish_time=None
|
||||
#进入未浇筑满状态标志位
|
||||
self._is_before_finish=False
|
||||
#是否浇筑满标志位
|
||||
self._is_finish=False
|
||||
# self._is_finish_ratio=0
|
||||
#是否震荡关闭平面(设想90%后关闭几秒,让液面平整)
|
||||
# self._is_zd_close=False
|
||||
#浇筑完成比例(重量)
|
||||
self._is_finish_ratio=0
|
||||
|
||||
#下料阶段,汇总时用枚举
|
||||
self._is_feed_stage=0
|
||||
#振动相关参数
|
||||
self._last_arch_one_weight=0
|
||||
self._last_arch_two_weight=0
|
||||
self._last_arch_three_weight=0
|
||||
self._last_arch_four_weight=0
|
||||
self._last_arch_five_weight=0
|
||||
self._last_arch_time=0
|
||||
#是否为F块
|
||||
self._is_small_f=None
|
||||
#采集数据用,下料重量=之前下的重量+最后一阶段(下-->模具车)重量差
|
||||
#记录最后一次下料斗到模具车前的重量
|
||||
self._finish_weight=0
|
||||
#记录最后一次下料斗到车初始重量
|
||||
self._inital_finish_lweight=0
|
||||
|
||||
# 初始化控制间隔和堆料状态跟踪属性
|
||||
self._last_overflow_state = False
|
||||
self._last_control_time = 0
|
||||
self._initialized = True
|
||||
self.plc_data=None
|
||||
# self.plc_data=5
|
||||
self.plc_service = OmronFinsPollingService("192.168.250.233")
|
||||
self.plc_service.register_data_callback(self.on_plc_update)
|
||||
# self.plc_service.register_status_callback(self.on_status_change)
|
||||
self.plc_service.start_polling(interval=2.0)
|
||||
|
||||
|
||||
|
||||
def angle_visual_callback(self, current_angle, overflow_detected):
|
||||
"""
|
||||
视觉控制主逻辑,供外部推送数据
|
||||
@ -111,8 +152,67 @@ class VisualCallback:
|
||||
# 释放处理锁
|
||||
self._is_processing.release()
|
||||
|
||||
def _monitor_loop(self):
|
||||
"""监控循环"""
|
||||
while not self._is_finish:
|
||||
try:
|
||||
current_time = time.time()
|
||||
# 检查下料斗破拱(只有在下料过程中才检查)
|
||||
if self._is_feed_stage==1: # 下料斗--》模具车
|
||||
_arch_weight = self.transmitter_controller.read_data(2)
|
||||
if _arch_weight is not None and _arch_weight>0:
|
||||
# 检查重量变化是否过慢
|
||||
if (abs(_arch_weight - self._last_arch_one_weight) < 200) and \
|
||||
(current_time - self._last_arch_time) >= 2:
|
||||
print('---------------------第一阶段振动5秒-----------------')
|
||||
self.relay_controller.control_arch_lower_open_sync(5)
|
||||
self._last_arch_one_weight = _arch_weight
|
||||
elif self._is_feed_stage==2: #上料斗到下料斗,料多,谨慎振动
|
||||
_arch_weight = self.transmitter_controller.read_data(1)
|
||||
if _arch_weight is not None:
|
||||
# 检查重量变化是否过慢
|
||||
# if (abs(_arch_weight - self._last_arch_two_weight) < 50) and \
|
||||
# (current_time - self._last_arch_time) >= 2:
|
||||
# self.relay_controller.control_arch_upper_open_sync(3)
|
||||
self._last_arch_two_weight = _arch_weight
|
||||
elif self._is_feed_stage==3: #第二次下料斗-》模具车
|
||||
_arch_weight = self.transmitter_controller.read_data(2)
|
||||
if _arch_weight is not None:
|
||||
# 检查重量变化是否过慢
|
||||
|
||||
if (abs(_arch_weight - self._last_arch_three_weight) < 100) and \
|
||||
(current_time - self._last_arch_time) >= 2:
|
||||
print('---------------------第三阶段振动5秒-----------------')
|
||||
self.relay_controller.control_arch_lower_open_sync(3)
|
||||
self._last_arch_three_weight = _arch_weight
|
||||
elif self._is_feed_stage==4: #上料斗--》下料斗
|
||||
_arch_weight = self.transmitter_controller.read_data(1)
|
||||
if _arch_weight is not None:
|
||||
# 检查重量变化是否过慢
|
||||
# if (abs(_arch_weight - self._last_arch_four_weight) < 200) and \
|
||||
# (current_time - self._last_arch_time) > 2:
|
||||
# self.relay_controller.control_arch_upper_open_sync(5)
|
||||
self._last_arch_four_weight = _arch_weight
|
||||
elif self._is_feed_stage==5: #下料斗->模具车
|
||||
_arch_weight = self.transmitter_controller.read_data(2)
|
||||
if _arch_weight is not None:
|
||||
_min_arch_weight=20
|
||||
if self._is_finish_ratio<self._max_angle_radio:
|
||||
_min_arch_weight=50
|
||||
if (abs(_arch_weight - self._last_arch_five_weight) < _min_arch_weight) and \
|
||||
(current_time - self._last_arch_time) >= 2:
|
||||
print('---------------------第五阶段振动5秒-----------------')
|
||||
self.relay_controller.control_arch_lower_open_sync(5)
|
||||
self._last_arch_five_weight = _arch_weight
|
||||
# 更新最后读取时间
|
||||
self._last_arch_time = current_time
|
||||
time.sleep(2)
|
||||
except Exception as e:
|
||||
print(f"监控线程错误: {e}")
|
||||
|
||||
def _run_thread_loop(self):
|
||||
"""
|
||||
接受视觉回调数据
|
||||
线程主循环,持续运行
|
||||
等待新数据,然后调用处理方法
|
||||
"""
|
||||
@ -138,14 +238,25 @@ class VisualCallback:
|
||||
while True:
|
||||
print("------------已启动----------------")
|
||||
if self._is_feed_start:
|
||||
if self.plc_data==5:
|
||||
self.run_feed()
|
||||
break
|
||||
else:
|
||||
print("-----------上料斗未就位----------------")
|
||||
print("-----------上料斗未就位----------------")
|
||||
|
||||
# if self.plc_data==5:
|
||||
#_is_finish_ratio完成 比例,根据重量过滤一下
|
||||
if self._overflow_detected=='未堆料':
|
||||
if self._is_first_module:
|
||||
print('------------进入第一块111111-------------')
|
||||
self._is_first_module=False
|
||||
self.run_feed_all()
|
||||
break
|
||||
elif self._is_finish and self._is_finish_ratio>=0.7:
|
||||
print('-----------进入连续块111111-----------')
|
||||
# self.init_val()
|
||||
# self.run_feed_all()
|
||||
|
||||
# else:
|
||||
# print("-----------上料斗未就位----------------")
|
||||
# print("---------3--上料斗未就位----------------")
|
||||
|
||||
time.sleep(0.5)
|
||||
time.sleep(2)
|
||||
|
||||
def safe_control_lower_close(self,duration=3):
|
||||
"""线程安全的下料斗关闭方法"""
|
||||
@ -167,154 +278,294 @@ class VisualCallback:
|
||||
# 无论成功失败,都要重置标志位
|
||||
self._is_safe_closing = False
|
||||
|
||||
def close_lower_door_visual(self):
|
||||
"""关闭下料斗门"""
|
||||
self.is_start_visual=False
|
||||
time.sleep(0.5)
|
||||
self.safe_control_lower_close()
|
||||
|
||||
def _visual_close(self):
|
||||
self.is_start_visual=False
|
||||
self._is_finish=True
|
||||
self._is_feed_stage=0
|
||||
print(f'--------进入关闭-----------')
|
||||
self.safe_control_lower_close(3)
|
||||
print(f'--------关闭完成-----------')
|
||||
#记录重量
|
||||
_current_weight=self.transmitter_controller.read_data(2)
|
||||
if _current_weight is not None:
|
||||
self._finish_weight= self._finish_weight+(self._inital_finish_lweight-_current_weight)
|
||||
with open('weight.txt', 'a') as f:
|
||||
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||
f.write(f"{timestamp} - {self._finish_weight}\n")
|
||||
|
||||
|
||||
def run_feed_all(self):
|
||||
"""
|
||||
全流程下料:包括判断模具类型
|
||||
"""
|
||||
_is_f= run_stable_classification_loop()
|
||||
print(f'------------已判断出模具类型: {_is_f}-------------')
|
||||
if _is_f is not None:
|
||||
if _is_f=='模具车1':
|
||||
self._is_small_f=True
|
||||
print('-------------F块模具--------------')
|
||||
print('-------------F块模具--------------')
|
||||
print('-------------F块模具--------------')
|
||||
self.run_feed_f()
|
||||
elif _is_f=='模具车2':
|
||||
self._is_small_f=False
|
||||
self.run_feed()
|
||||
print('-------------其他模具---------------')
|
||||
|
||||
if self._is_small_f is None:
|
||||
print('-----------未判断出模具类型--------------')
|
||||
return
|
||||
|
||||
def run_feed_f(self):
|
||||
"""第一阶段下料:下料斗向模具车下料(低速)"""
|
||||
print("--------------------开始下料(F块)--------------------")
|
||||
# loc_relay=self.relay_controller
|
||||
loc_mitter=self.transmitter_controller
|
||||
max_weight_none=5
|
||||
cur_weight_none=0
|
||||
initial_lower_weight=loc_mitter.read_data(2)
|
||||
if initial_lower_weight is None:
|
||||
print("-----f上料斗重量异常-----")
|
||||
return
|
||||
first_finish_weight=0
|
||||
self._finish_weight=first_finish_weight
|
||||
self._inital_finish_lweight=initial_lower_weight
|
||||
need_total_weight=0.54*2416
|
||||
if initial_lower_weight>100:
|
||||
if not self._is_finish:
|
||||
self.is_start_visual=True
|
||||
initial_lower_weight=loc_mitter.read_data(2)
|
||||
if initial_lower_weight is None:
|
||||
print("-----f上料斗重量异常2-----")
|
||||
return
|
||||
self._is_feed_stage=5
|
||||
while not self._is_finish:
|
||||
current_weight = loc_mitter.read_data(2)
|
||||
if current_weight is None:
|
||||
cur_weight_none+=1
|
||||
if cur_weight_none>max_weight_none:
|
||||
#如果重量连续5次为None,认为下料斗未就位,跳出循环
|
||||
print('------------f下到模具车,下料斗重量异常----------------')
|
||||
print('------------f下到模具车,下料斗重量异常----------------')
|
||||
self.close_lower_door_visual()
|
||||
return
|
||||
#视觉处理关闭,异常的话重量没有生效
|
||||
continue
|
||||
cur_weight_none=0
|
||||
first_finish_weight=initial_lower_weight-current_weight
|
||||
self._is_finish_ratio=(first_finish_weight)/need_total_weight
|
||||
print(f'------------已下料比例: {self._is_finish_ratio}-------------')
|
||||
if self._is_finish_ratio>self._max_f_angle_ratio:
|
||||
#关5秒
|
||||
#大于0.7后不再检测了,直接交给视觉控制夹脚
|
||||
# print(f'------------已下料比例: {self._is_finish_ratio}-------------')
|
||||
break
|
||||
|
||||
# print(f'------------已下料: {first_finish_weight+second_finish_weight}kg-------------')
|
||||
time.sleep(1)
|
||||
# initial_lower_weight=_current_lower_weight
|
||||
print(f'------------已下料(F): {first_finish_weight}kg-------------')
|
||||
print(f'------------已下料(F): {first_finish_weight}kg-------------')
|
||||
|
||||
print(f'------------已完成-------------')
|
||||
|
||||
|
||||
def run_feed(self):
|
||||
"""第一阶段下料:下料斗向模具车下料(低速)"""
|
||||
print("--------------------开始下料--------------------")
|
||||
print("--------------------开始下料(普通块)--------------------")
|
||||
loc_relay=self.relay_controller
|
||||
loc_mitter=self.transmitter_controller
|
||||
max_weight_none=5
|
||||
cur_weight_none=0
|
||||
|
||||
initial_lower_weight=loc_mitter.read_data(2)
|
||||
initial_upper_weight=loc_mitter.read_data(1)
|
||||
# initial_upper_weight=loc_mitter.read_data(1)
|
||||
if initial_lower_weight is None:
|
||||
print("---------------下料斗重量异常----------------")
|
||||
return
|
||||
first_finish_weight=0
|
||||
# need_total_weight=1.91*2416
|
||||
start_time=None
|
||||
need_total_weight=1.91*2416
|
||||
# start_time=None
|
||||
self.is_start_visual=True
|
||||
|
||||
if initial_lower_weight>100:
|
||||
#下料斗的料全部下完
|
||||
while not self._is_finish:
|
||||
loc_mitter.is_start_lower=True
|
||||
self._is_feed_stage=1
|
||||
while not self._is_finish:
|
||||
current_weight = loc_mitter.read_data(2)
|
||||
first_finish_weight=initial_lower_weight-current_weight
|
||||
if current_weight<500:
|
||||
# 破拱控制
|
||||
if start_time is None or time.time()-start_time>5:
|
||||
start_time=time.time()
|
||||
loc_relay.control_arch_lower_open()
|
||||
|
||||
if current_weight<250:
|
||||
start_time=None
|
||||
self.is_start_visual=False
|
||||
loc_mitter.is_start_lower=False
|
||||
time.sleep(0.5)
|
||||
self.safe_control_lower_close()
|
||||
break
|
||||
print(f'------------已下料: {first_finish_weight}kg-------------')
|
||||
if current_weight is None:
|
||||
cur_weight_none+=1
|
||||
if cur_weight_none>max_weight_none:
|
||||
print("-----------下料斗重量异常(第一次下到模具车)--------------")
|
||||
self.close_lower_door_visual()
|
||||
return
|
||||
continue
|
||||
cur_weight_none=0
|
||||
if current_weight<250 and current_weight>0:
|
||||
self.close_lower_door_visual()
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
# _current_lower_weight=loc_mitter.read_data(2)
|
||||
# first_finish_weight=initial_lower_weight-_current_lower_weight
|
||||
_current_lower_weight=loc_mitter.read_data(2)
|
||||
if _current_lower_weight is None:
|
||||
print("-------下料斗重量异常---------")
|
||||
return
|
||||
first_finish_weight=initial_lower_weight-_current_lower_weight
|
||||
# initial_lower_weight=_current_lower_weight
|
||||
# print(f'------------已下料: {first_finish_weight}kg-------------')
|
||||
# print(f'------------已下料: {first_finish_weight}kg-------------')
|
||||
print(f'------------已下料(第一次): {first_finish_weight}kg-------------')
|
||||
print(f'------------已下料(第一次): {first_finish_weight}kg-------------')
|
||||
self._is_feed_stage=0
|
||||
|
||||
while self.plc_data!=5:
|
||||
print('------------上料斗未就位----------------')
|
||||
print('------------上料斗未就位----------------')
|
||||
time.sleep(1)
|
||||
|
||||
if self.plc_data==5:
|
||||
print(f'------------上料斗向下料斗转移(留3000KG)-------------')
|
||||
#打开上料斗出砼门,开5就,开三分之一下
|
||||
|
||||
loc_relay.control_upper_open_sync(6)
|
||||
self._is_feed_stage=2
|
||||
loc_time_count=1
|
||||
upper_open_time=time.time()
|
||||
|
||||
while not self._is_finish:
|
||||
loc_mitter.is_start_upper=True
|
||||
current_upper_weight = loc_mitter.read_data(1)
|
||||
if current_upper_weight<3000:
|
||||
if current_upper_weight is None:
|
||||
cur_weight_none+=1
|
||||
if cur_weight_none>max_weight_none:
|
||||
#如果重量连续5次为None,认为上料斗未就位,跳出循环
|
||||
print('------------第一次上到下,上料斗重量异常----------------')
|
||||
print('------------第一次上到下,上料斗重量异常----------------')
|
||||
loc_relay.control_upper_close_sync(5+loc_time_count)
|
||||
return
|
||||
continue
|
||||
cur_weight_none=0
|
||||
if current_upper_weight<3000 and current_upper_weight>0:
|
||||
#关5秒,loc_time_count多关一秒
|
||||
loc_relay.control_upper_close_sync(4+loc_time_count)
|
||||
loc_mitter.is_start_upper=False
|
||||
loc_relay.control_upper_close_sync(5+loc_time_count)
|
||||
break
|
||||
else:
|
||||
if time.time()-upper_open_time>5:
|
||||
upper_open_time=time.time()
|
||||
loc_relay.control_upper_open_sync(0.5)
|
||||
loc_time_count=loc_time_count+0.5
|
||||
if loc_time_count<6:
|
||||
upper_open_time=time.time()
|
||||
loc_relay.control_upper_open_sync(0.8)
|
||||
loc_time_count=loc_time_count+0.8
|
||||
else:
|
||||
time.sleep(0.5)
|
||||
# time.sleep(0.4)
|
||||
else:
|
||||
loc_relay.control_upper_close_sync(6+loc_time_count)
|
||||
|
||||
self.is_start_visual=True
|
||||
loc_mitter.is_start_lower=False
|
||||
loc_mitter.test_lower_weight=2000
|
||||
initial_lower_weight=loc_mitter.read_data(2)
|
||||
|
||||
if initial_lower_weight is None:
|
||||
print("-------下料斗重量异常(第二次下料到模具车)---------")
|
||||
return
|
||||
self._is_feed_stage=3
|
||||
while not self._is_finish:
|
||||
loc_mitter.is_start_lower=True
|
||||
current_weight = loc_mitter.read_data(2)
|
||||
second_finish_weight=initial_lower_weight-current_weight
|
||||
if current_weight<500:
|
||||
#关5秒
|
||||
if start_time is None or time.time()-start_time>5:
|
||||
start_time=time.time()
|
||||
loc_relay.control_arch_lower_open()
|
||||
if current_weight<250:
|
||||
start_time=None
|
||||
self.is_start_visual=False
|
||||
loc_mitter.is_start_lower=False
|
||||
time.sleep(0.5)
|
||||
self.safe_control_lower_close()
|
||||
break
|
||||
print(f'------------已下料: {first_finish_weight+second_finish_weight}kg-------------')
|
||||
if current_weight is None:
|
||||
cur_weight_none+=1
|
||||
if cur_weight_none>max_weight_none:
|
||||
print("-------下料斗重量异常(第二次下料到模具车)---------")
|
||||
self.close_lower_door_visual()
|
||||
return
|
||||
continue
|
||||
cur_weight_none=0
|
||||
# second_finish_weight=initial_lower_weight-current_weight
|
||||
if current_weight<250:
|
||||
self.close_lower_door_visual()
|
||||
break
|
||||
# print(f'------------已下料: {first_finish_weight+second_finish_weight}kg-------------')
|
||||
time.sleep(1)
|
||||
# _current_lower_weight=loc_mitter.read_data(2)
|
||||
# first_finish_weight=first_finish_weight+initial_lower_weight-_current_lower_weight
|
||||
# print(f'------------已下料: {first_finish_weight}kg-------------')
|
||||
# print(f'------------已下料: {first_finish_weight}kg-------------')
|
||||
_current_lower_weight=loc_mitter.read_data(2)
|
||||
if _current_lower_weight is None:
|
||||
print("-------下料斗重量异常(第二次下到模)---------")
|
||||
return
|
||||
first_finish_weight=first_finish_weight+initial_lower_weight-_current_lower_weight
|
||||
print(f'------------已下料(第二次): {first_finish_weight}kg-------------')
|
||||
print(f'------------已下料(第二次): {first_finish_weight}kg-------------')
|
||||
|
||||
self._is_feed_stage=0
|
||||
if self.plc_data==5:
|
||||
#第二次上料斗向下料斗转移
|
||||
loc_relay.control_upper_open_sync(6)
|
||||
loc_relay.control_upper_open_sync(11)
|
||||
loc_time_count=1
|
||||
upper_open_time=time.time()
|
||||
loc_mitter.is_start_upper=False
|
||||
loc_mitter.test_upper_weight=3000
|
||||
#第二次到下料斗还需要的量
|
||||
#loc_left_need_weight=need_total_weight-first_finish_weight
|
||||
initial_upper_weight=loc_mitter.read_data(1)
|
||||
start_time=None
|
||||
# initial_upper_weight=loc_mitter.read_data(1)
|
||||
# start_time=None
|
||||
self._is_feed_stage=4
|
||||
while not self._is_finish:
|
||||
# print(f'------------上料斗向下料斗转移22222-------------')
|
||||
loc_mitter.is_start_upper=True
|
||||
current_upper_weight = loc_mitter.read_data(1)
|
||||
if current_upper_weight<500:
|
||||
#关5秒,loc_time_count多关一秒
|
||||
if start_time is None or time.time()-start_time>4:
|
||||
start_time=time.time()
|
||||
loc_relay.control_arch_upper_open()
|
||||
if current_upper_weight<100:
|
||||
start_time=None
|
||||
loc_relay.control_upper_close_sync(4+loc_time_count)
|
||||
loc_mitter.is_start_upper=False
|
||||
if current_upper_weight is None:
|
||||
cur_weight_none+=1
|
||||
if cur_weight_none>max_weight_none:
|
||||
#如果重量连续5次为None,认为上料斗未就位,跳出循环
|
||||
print('------------第二次上到下,上料斗重量异常----------------')
|
||||
print('------------第二次上到下,上料斗重量异常----------------')
|
||||
loc_relay.control_upper_close_sync(15)
|
||||
break
|
||||
continue
|
||||
cur_weight_none=0
|
||||
if current_upper_weight<400 and current_upper_weight>0:
|
||||
loc_relay.control_arch_upper_open()
|
||||
loc_relay.control_upper_open_sync(5)
|
||||
# start_time=None
|
||||
#5秒后关闭
|
||||
loc_relay.control_upper_close_after()#control_upper_close_sync(8+loc_time_count)
|
||||
break
|
||||
else:
|
||||
if time.time()-upper_open_time>3:
|
||||
if time.time()-upper_open_time>2:
|
||||
# if loc_time_count<6:
|
||||
upper_open_time=time.time()
|
||||
loc_relay.control_upper_open_sync(0.8)
|
||||
loc_time_count=loc_time_count+0.8
|
||||
loc_relay.control_upper_open_sync(1)
|
||||
loc_time_count=loc_time_count+1
|
||||
else:
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
loc_relay.control_upper_close_sync(15)
|
||||
# time.sleep(0.4)
|
||||
|
||||
#第二次下料斗转移到模具车
|
||||
#第三次下料斗转移到模具车
|
||||
if not self._is_finish:
|
||||
self.is_start_visual=True
|
||||
# loc_mitter.is_start_lower=False
|
||||
# loc_mitter.test_lower_weight=2000
|
||||
# initial_lower_weight=loc_mitter.read_data(2)
|
||||
|
||||
# while not self._is_finish:
|
||||
# loc_mitter.is_start_lower=True
|
||||
# current_weight = loc_mitter.read_data(2)
|
||||
# second_finish_weight=initial_lower_weight-current_weight
|
||||
# # self._is_finish_ratio=(second_finish_weight+first_finish_weight)/need_total_weight
|
||||
# # print(f'------------已下料比例: {self._is_finish_ratio}-------------')
|
||||
|
||||
# if current_weight<500:
|
||||
# #关5秒
|
||||
# if start_time is None or time.time()-start_time>5:
|
||||
# start_time=time.time()
|
||||
# loc_relay.control_arch_lower_open()
|
||||
|
||||
# print(f'------------已下料: {first_finish_weight+second_finish_weight}kg-------------')
|
||||
# time.sleep(1)
|
||||
|
||||
initial_lower_weight=loc_mitter.read_data(2)
|
||||
self._finish_weight=first_finish_weight
|
||||
self._inital_finish_lweight=initial_lower_weight
|
||||
if initial_lower_weight is None:
|
||||
print("-------下料斗重量异常(第三次下到模具车)---------")
|
||||
return
|
||||
self._is_feed_stage=5
|
||||
while not self._is_finish:
|
||||
current_weight = loc_mitter.read_data(2)
|
||||
if current_weight is None:
|
||||
cur_weight_none+=1
|
||||
if cur_weight_none>max_weight_none:
|
||||
#重量异常退出
|
||||
print('------------第三次下到模具车,下料斗重量异常----------------')
|
||||
self.close_lower_door_visual()
|
||||
return
|
||||
continue
|
||||
cur_weight_none=0
|
||||
second_finish_weight=initial_lower_weight-current_weight
|
||||
self._is_finish_ratio=(second_finish_weight+first_finish_weight)/need_total_weight
|
||||
print(f'------------已下料比例: {self._is_finish_ratio}-------------')
|
||||
if self._is_finish_ratio>self._max_angle_radio:
|
||||
#关5秒
|
||||
# print(f'------------已下料比例: {self._is_finish_ratio}-------------')
|
||||
break
|
||||
|
||||
# print(f'------------已下料: {first_finish_weight+second_finish_weight}kg-------------')
|
||||
time.sleep(1)
|
||||
|
||||
# _current_lower_weight=loc_mitter.read_data(2)
|
||||
# first_finish_weight=first_finish_weight+initial_lower_weight-_current_lower_weight
|
||||
# print(f'------------已下料: {first_finish_weight}kg-------------')
|
||||
@ -354,54 +605,66 @@ class VisualCallback:
|
||||
# time.sleep(3)
|
||||
else:
|
||||
if overflow_detected=='浇筑满':
|
||||
self.is_start_visual=False
|
||||
self._is_finish=True
|
||||
self.safe_control_lower_close(3)
|
||||
self._visual_close()
|
||||
return
|
||||
# print(f'--------已关闭已关闭-----------')
|
||||
elif overflow_detected=="大堆料":
|
||||
print(f'--------未浇筑满,大堆料,不开关-----------')
|
||||
else:
|
||||
self._pulse_control('open',0.5)
|
||||
print(f'--------未浇筑满,大堆料-----------')
|
||||
self._pulse_control('open',0.3)
|
||||
time.sleep(0.3)
|
||||
self._pulse_control('close',0.6)
|
||||
time.sleep(2)
|
||||
self._pulse_control('close',0.4)
|
||||
|
||||
time.sleep(1)
|
||||
self._is_before_finish=True
|
||||
else:
|
||||
# self._pulse_control('open',0.5)
|
||||
# time.sleep(0.3)
|
||||
# self._pulse_control('close',0.6)
|
||||
self._pulse_control('open',0.6)
|
||||
time.sleep(0.3)
|
||||
self._pulse_control('close',0.7)
|
||||
|
||||
time.sleep(1)
|
||||
self._is_before_finish=True
|
||||
if self._is_finish_ratio<=self._max_ignore_radio:
|
||||
#如果重量未达到最大忽略角度,需要跳出
|
||||
self._is_before_finish=False
|
||||
return
|
||||
elif overflow_detected == "浇筑满":
|
||||
self.is_start_visual=False
|
||||
self._is_finish=True
|
||||
self.safe_control_lower_close(3)
|
||||
self._visual_close()
|
||||
return
|
||||
else:
|
||||
self._before_finish_time=None
|
||||
# if self._is_finish_ratio>=0.8:
|
||||
# if overflow_detected == "大堆料":
|
||||
# TARGET_ANGLE = 10.0 # 大堆料时控制在15度左右
|
||||
# elif overflow_detected == "小堆料":
|
||||
# TARGET_ANGLE = 20.0 # 小堆料时控制在35度左右
|
||||
# else:
|
||||
# TARGET_ANGLE = 25.0 # 未溢料时开到最大56度
|
||||
# if self._is_finish_ratio>=0.9 and not self._is_zd_close:
|
||||
# #关闭10秒,直接关闭,不等待
|
||||
# print('-----------------达到90% 关闭--------------------')
|
||||
# self._is_zd_close=True
|
||||
# self.safe_control_lower_close()
|
||||
# time.sleep(10)
|
||||
# return
|
||||
|
||||
# else:
|
||||
#根据溢料状态动态调整目标角度
|
||||
if overflow_detected == "大堆料":
|
||||
TARGET_ANGLE = 15.0 # 大堆料时控制在15度左右
|
||||
elif overflow_detected == "小堆料":
|
||||
TARGET_ANGLE = 25.0 # 小堆料时控制在35度左右
|
||||
if self._is_finish_ratio>=self._max_angle_radio or (self._is_finish_ratio>self._max_f_angle_ratio and self._is_small_f):
|
||||
if overflow_detected == "大堆料":
|
||||
TARGET_ANGLE = 5.0 # 大堆料时控制在15度左右
|
||||
elif overflow_detected == "小堆料":
|
||||
TARGET_ANGLE = 15.0 # 小堆料时控制在35度左右
|
||||
else:
|
||||
TARGET_ANGLE = 25.0 # 未溢料时开到最大56度
|
||||
else:
|
||||
TARGET_ANGLE = 45.0 # 未溢料时开到最大56度
|
||||
if self._is_feed_stage==1 or self._is_feed_stage==3:
|
||||
#根据溢料状态动态调整目标角度
|
||||
if overflow_detected == "大堆料":
|
||||
TARGET_ANGLE = 15.0 # 大堆料时控制在15度左右
|
||||
elif overflow_detected == "小堆料":
|
||||
TARGET_ANGLE = 45.0 # 小堆料时控制在35度左右
|
||||
else:
|
||||
TARGET_ANGLE = 55.0 # 未溢料时开到最大56度
|
||||
else:
|
||||
#根据溢料状态动态调整目标角度
|
||||
if overflow_detected == "大堆料":
|
||||
TARGET_ANGLE = 15.0 # 大堆料时控制在15度左右
|
||||
elif overflow_detected == "小堆料":
|
||||
TARGET_ANGLE = 25.0 # 小堆料时控制在35度左右
|
||||
else:
|
||||
TARGET_ANGLE = 45.0 # 未溢料时开到最大56度
|
||||
|
||||
# 确保目标角度在硬件范围内(5-56度)
|
||||
TARGET_ANGLE = max(5.0, min(56.0, TARGET_ANGLE))
|
||||
|
||||
# PID控制参数
|
||||
KP = 0.15 # 比例系数
|
||||
KP = 0.2 # 比例系数
|
||||
KI = 0 # 积分系数
|
||||
KD = 0 # 微分系数
|
||||
# KP = 0.15 # 比例系数
|
||||
@ -618,6 +881,12 @@ class VisualCallback:
|
||||
if self.callback_thread.is_alive():
|
||||
self.callback_thread.join(timeout=1.0)
|
||||
|
||||
if self.feed_thread.is_alive():
|
||||
self.feed_thread.join(timeout=1.0)
|
||||
|
||||
if self.monitor_thread.is_alive():
|
||||
self.monitor_thread.join(timeout=1.0)
|
||||
|
||||
def __del__(self):
|
||||
"""析构函数,确保线程安全关闭"""
|
||||
self.shutdown()
|
||||
|
||||
1018
vision/visual_callback_dq.py
Normal file
1018
vision/visual_callback_dq.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user