diff --git a/busisness/blls.py b/busisness/blls.py index 098e8c4..3ebd9f7 100644 --- a/busisness/blls.py +++ b/busisness/blls.py @@ -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") diff --git a/busisness/dals.py b/busisness/dals.py index f203d41..8f20bd4 100644 --- a/busisness/dals.py +++ b/busisness/dals.py @@ -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获取构件任务""" diff --git a/db/three.db b/db/three.db index 9370717..448d15b 100644 Binary files a/db/three.db and b/db/three.db differ diff --git a/db/three.db-shm b/db/three.db-shm new file mode 100644 index 0000000..c735afc Binary files /dev/null and b/db/three.db-shm differ diff --git a/db/three.db-wal b/db/three.db-wal new file mode 100644 index 0000000..92aa9a7 Binary files /dev/null and b/db/three.db-wal differ diff --git a/doc/浇筑系统对接接口文档-20251223.doc b/doc/浇筑系统对接接口文档-20251223.doc new file mode 100644 index 0000000..f9713bf Binary files /dev/null and b/doc/浇筑系统对接接口文档-20251223.doc differ diff --git a/feeding/processbak_copy b/feeding/process_bak.py similarity index 100% rename from feeding/processbak_copy rename to feeding/process_bak.py diff --git a/hardware/RFID/rfid_service.py b/hardware/RFID/rfid_service.py index fe335a4..2f16baf 100644 --- a/hardware/RFID/rfid_service.py +++ b/hardware/RFID/rfid_service.py @@ -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) diff --git a/hardware/relay.py b/hardware/relay.py index 1b920fd..d364768 100644 --- a/hardware/relay.py +++ b/hardware/relay.py @@ -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') diff --git a/hardware/transmitter.py b/hardware/transmitter.py index 83f527b..d50cb94 100644 --- a/hardware/transmitter.py +++ b/hardware/transmitter.py @@ -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 - \ No newline at end of file + + def read_data(self,transmitter_id): + """获取重量函数:根据变送器ID获取当前重量,三次""" + max_try_times=5 + try_times=0 + while try_times 输出 + - 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() +''' +# --------------------------- diff --git a/vision/muju_cls/muju_utils.py b/vision/muju_cls/muju_utils.py new file mode 100644 index 0000000..70d0190 --- /dev/null +++ b/vision/muju_cls/muju_utils.py @@ -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 + + + + diff --git a/vision/muju_cls/roi_coordinates/muju_roi.txt b/vision/muju_cls/roi_coordinates/muju_roi.txt new file mode 100644 index 0000000..17ec328 --- /dev/null +++ b/vision/muju_cls/roi_coordinates/muju_roi.txt @@ -0,0 +1 @@ +2,880,385,200 diff --git a/vision/muju_cls/test.png b/vision/muju_cls/test.png new file mode 100644 index 0000000..84415fc Binary files /dev/null and b/vision/muju_cls/test.png differ diff --git a/vision/roi_coordinates/muju_roi.txt b/vision/roi_coordinates/muju_roi.txt new file mode 100644 index 0000000..17ec328 --- /dev/null +++ b/vision/roi_coordinates/muju_roi.txt @@ -0,0 +1 @@ +2,880,385,200 diff --git a/vision/visual_callback.py b/vision/visual_callback.py index a831a22..403bff2 100644 --- a/vision/visual_callback.py +++ b/vision/visual_callback.py @@ -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= 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() diff --git a/vision/visual_callback_dq.py b/vision/visual_callback_dq.py new file mode 100644 index 0000000..906d2f9 --- /dev/null +++ b/vision/visual_callback_dq.py @@ -0,0 +1,1018 @@ + +from pickle import FALSE +from cv2.gapi import ov +from config.settings import app_set_config +from hardware.relay import RelayController +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: + # 类变量,用于存储实例引用,实现单例检测 + _instance = None + _lock = threading.Lock() + + def __new__(cls): + """检测实例是否存在,实现单例模式""" + with cls._lock: + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + """初始化视觉回调处理器""" + # 避免重复初始化 + if hasattr(self, '_initialized') and self._initialized: + return + + self.relay_controller = RelayController() + self.transmitter_controller = TransmitterController(self.relay_controller) + + # 线程安全的参数传递 + self._new_data_available = threading.Event() + self._is_processing = threading.Lock() + self._stop_event = threading.Event() + + # 添加下料斗门控制锁,防止两个线程同时控制 + self._door_control_lock = threading.Lock() + # 记录当前控制门的线程名称,用于调试 + self._current_controlling_thread = None + #是否启动后的第一个模具 + 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._mould_accept_aligned=None + self._mould_before_aligned=False + #模具开始浇筑时间 + self._time_mould_begin='' + #模具结束浇筑时间 + self._time_mould_end='' + + # 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 + + #下料阶段,汇总时用枚举 + 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._is_running=True + self._initialized = True + self.plc_data=None + + + def angle_visual_callback(self, current_angle, overflow_detected, mould_aligned): + """ + 视觉控制主逻辑,供外部推送数据 + 使用单个持续运行的线程,通过参数设置传递数据 + 如果线程正在处理数据,则丢弃此次推送 + """ + #print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 收到推送数据") + # 尝试获取处理锁,若失败则说明正在处理,丢弃数据 + if not self._is_processing.acquire(blocking=False): + print("回调线程仍在执行,丢弃此次推送数据") + return + + try: + # 更新参数 + + if overflow_detected is not None: + #print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 收到溢料:{overflow_detected}") + self._overflow_detected = overflow_detected + if current_angle is not None: + #print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 收到角度:{current_angle}") + self._current_angle = current_angle + if mould_aligned is not None: + #print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 收到对齐:{mould_aligned}") + self._mould_accept_aligned=mould_aligned + # 通知线程有新数据可用 + self._new_data_available.set() + finally: + # 释放处理锁 + self._is_processing.release() + + def _monitor_loop(self): + """监控循环""" + while self._is_running: + 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: + # 检查重量变化是否过慢 + _weight_changed=abs(_arch_weight - self._last_arch_one_weight) + #_last_arch_one_weight默认为0,一开始就进入振动 + print(f'---------------第一阶段,重量变化:{_weight_changed}------------------') + if (_weight_changed< 200) and \ + (current_time - self._last_arch_time) >= 2: + self._last_arch_time = current_time + print('---------------------第一阶段振动5秒(小于200KG)-----------------') + self.relay_controller.control_arch_lower_open_sync(5) + self._last_arch_one_weight = _arch_weight + continue + 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 and _arch_weight>0: + # 检查重量变化是否过慢 + _weight_changed=abs(_arch_weight - self._last_arch_two_weight) + print(f'---------------第二阶段,重量变化:{_weight_changed}------------------') + if (_weight_changed < 100) and \ + (current_time - self._last_arch_time) >= 2: + self._last_arch_time = current_time + print('---------------------第二阶段振动3秒-----------------') + self.relay_controller.control_arch_upper_open_sync(3) + self._last_arch_two_weight = _arch_weight + continue + 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 and _arch_weight>0: + #刚开始不需要振动,料太多 + if self._last_arch_three_weight>0: + _weight_changed=abs(_arch_weight - self._last_arch_three_weight) + # 检查重量变化是否过慢 + print(f'---------------第三阶段,重量变化:{_weight_changed}------------------') + if (_weight_changed < 100) and \ + (current_time - self._last_arch_time) >= 2: + self._last_arch_time = current_time + print('---------------------第三阶段振动5秒(小于100KG)-----------------') + self.relay_controller.control_arch_lower_open_sync(5) + self._last_arch_three_weight = _arch_weight + continue + 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 and _arch_weight>0: + # 检查重量变化是否过慢 + _weight_changed=abs(_arch_weight - self._last_arch_four_weight) + print(f'---------------第二阶段,重量变化:{_weight_changed}------------------') + if (_weight_changed < 200) and \ + (current_time - self._last_arch_time) > 2: + self._last_arch_time = current_time + print('---------------------第四阶段振动5秒-----------------') + self.relay_controller.control_arch_upper_open_sync(5) + self._last_arch_four_weight = _arch_weight + continue + 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 and _arch_weight>0: + if self._last_arch_five_weight>0: + _weight_changed=abs(_arch_weight - self._last_arch_five_weight) + print(f'---------------第五阶段,重量变化:{_weight_changed}------------------') + _min_arch_weight=20 + if self._is_finish_ratio= 2: + self._last_arch_time = current_time + print(f'---------------------第五阶段振动3秒(小于{_min_arch_weight}kg))-----------------') + self.relay_controller.control_arch_lower_open_sync(3) + self._last_arch_five_weight = _arch_weight + continue + self._last_arch_five_weight = _arch_weight + + # 更新最后读取时间 + self._last_arch_time = current_time + time.sleep(2) + except Exception as e: + print(f"监控线程错误: {e}") + + def _aligned_get_times(self,flag): + """ + 获取对齐,1为对齐,0为未对齐 + """ + _current_times=time.time() + _temp_aligned_count=0 + if flag==1: + while time.time()-_current_times<=2: + print(f'-------------{self._mould_accept_aligned}-----------------') + if self._mould_accept_aligned=='盖板对齐': + _temp_aligned_count=_temp_aligned_count+1 + else: + _temp_aligned_count=0 + print(f'-------------{datetime.now().strftime("%H:%M:%S")} 盖板对齐,次数:{_temp_aligned_count}-----------------') + time.sleep(0.2) + self._mould_accept_aligned='' + if _temp_aligned_count>=8: + return True + else: + return False + elif flag==2: + while time.time()-_current_times<=5: + + if self._mould_accept_aligned=='盖板未对齐': + _temp_aligned_count=_temp_aligned_count+1 + else: + _temp_aligned_count=0 + print(f'-------------{datetime.now().strftime("%H:%M:%S")} 盖板未对齐,次数:{_temp_aligned_count}-----------------') + + time.sleep(0.2) + + self._mould_accept_aligned='' + if _temp_aligned_count>=20: + return True + else: + return False + + + def _run_thread_loop(self): + """ + 接受视觉回调数据 + 线程主循环,持续运行 + 等待新数据,然后调用处理方法 + """ + while not self._stop_event.is_set(): + # 等待新数据可用 + self._new_data_available.wait() + + # 重置事件 + self._new_data_available.clear() + + # 获取当前参数(使用临时变量避免被其他线程修改) + current_angle = self._current_angle + overflow_detected = self._overflow_detected + self._is_feed_start=True + + if self.is_start_visual: + # 处理数据 + self._process_angle_callback(current_angle, overflow_detected) + time.sleep(0.1) + + def _run_feed(self): + + while True: + # print("------------已启动----------------") + if self._is_feed_start: + + # if self.plc_data==5: + #_is_finish_ratio完成 比例,根据重量过滤一下 + + if self._is_first_module and self._overflow_detected=='未堆料': + #第一次打开 ,未堆料,检测对齐 + _is_aligned=self._aligned_get_times(1) + if _is_aligned: + print('------------进入第一块111111-------------') + self._is_first_module=False + self._mould_before_aligned=True + # self.is_start_visual=True + self.run_feed_all() + elif self._is_finish and self._is_finish_ratio>=0.7: + #后续流程--》检查到未对齐,--》后又对齐+未堆料 + print('------------------进入连续块检测------------------') + if self._mould_before_aligned: + #未对齐,检测对齐 + _is_not_aligned=self._aligned_get_times(2) + if _is_not_aligned: + self._mould_before_aligned=False + print('------------连续盖板未对齐-------------') + else: + _is_aligned=self._aligned_get_times(1) + if _is_aligned and self._overflow_detected=='未堆料': + print('------------连续盖板已对齐-------------') + self._mould_before_aligned=True + print('-----------进入连续块111111-----------') + # self.is_start_visual=True + self.init_val() + self.run_feed_all() + + # else: + # print("-----------上料斗未就位----------------") + # print("---------3--上料斗未就位----------------") + + time.sleep(0.2) + + def safe_control_lower_close(self,duration=3): + """线程安全的下料斗关闭方法""" + thread_name = threading.current_thread().name + # print(f"[{thread_name}] 尝试关闭下料斗...") + # 设置标志位,指示正在执行安全关闭操作 + self._is_safe_closing = True + try: + with self._door_control_lock: + self._current_controlling_thread = thread_name + #print(f"[{thread_name}] 获得下料斗控制权,执行关闭操作") + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') + time.sleep(duration) + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self._current_controlling_thread = None + #print(f"[{thread_name}] 释放下料斗控制权") + finally: + # 无论成功失败,都要重置标志位 + 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("%Y-%m-%d %H:%M:%S") + if self._is_small_f: + f.write(f"{self._time_mould_begin},{timestamp},F,{self._finish_weight}\n") + else: + f.write(f"{self._time_mould_begin},{timestamp},B,{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块)--------------------") + self._time_mould_begin=datetime.now().strftime("%Y-%m-%d %H:%M:%S") + # 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("--------------------开始下料(普通块)--------------------") + self._time_mould_begin=datetime.now().strftime("%Y-%m-%d %H:%M:%S") + 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) + if initial_lower_weight is None: + print("---------------下料斗重量异常----------------") + return + first_finish_weight=0 + need_total_weight=1.91*2416 + # start_time=None + self.is_start_visual=True + if initial_lower_weight>100: + #下料斗的料全部下完 + self._is_feed_stage=1 + 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 + 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) + 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-------------') + 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: + current_upper_weight = loc_mitter.read_data(1) + 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(5+loc_time_count) + break + else: + if time.time()-upper_open_time>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) + else: + loc_relay.control_upper_close_sync(6+loc_time_count) + + self.is_start_visual=True + 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: + 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 + 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) + 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(12) + loc_time_count=1 + upper_open_time=time.time() + upper_open_time_2=None + #第二次到下料斗还需要的量 + #loc_left_need_weight=need_total_weight-first_finish_weight + # initial_upper_weight=loc_mitter.read_data(1) + # start_time=None + self._is_feed_stage=4 + while not self._is_finish: + # print(f'------------上料斗向下料斗转移22222-------------') + current_upper_weight = loc_mitter.read_data(1) + 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<600 and current_upper_weight>0) or upper_open_time_2 is not None: + if upper_open_time_2 is None: + upper_open_time_2=time.time() + if current_upper_weight<400 or time.time()-upper_open_time_2>5: + loc_relay.control_arch_upper_open_async(5) + # 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 + time.sleep(1) + else: + if time.time()-upper_open_time>2: + # if loc_time_count<6: + upper_open_time=time.time() + loc_relay.control_upper_open_sync(1.2) + 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 + 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-------------') + # print(f'------------已下料: {first_finish_weight}kg-------------') + + + print(f'------------已完成-------------') + + def _process_angle_callback(self, current_angle, overflow_detected): + """ + 实时精细控制 - 基于PID思想,无固定间隔 + """ + try: + # 记录控制时间戳(用于微分计算,而非限制) + current_time = time.time() + # 确保所有PID相关属性都被正确初始化 + if not hasattr(self, '_last_control_time'): + self._last_control_time = current_time + if not hasattr(self, '_last_error'): + self._last_error = 0 + if not hasattr(self, '_error_integral'): + self._error_integral = 0 + + # print(f"{self.angle_mode}") + self.overflow = overflow_detected in ["大堆料", "小堆料"] + + if current_angle is None: + return + + print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 角度11: {current_angle:.2f}°,{overflow_detected}") + + if overflow_detected == "未浇筑满" or self._is_before_finish: + if self._before_finish_time is None: + self._before_finish_time=current_time + self.safe_control_lower_close(3) + print('-----------------关闭(未浇筑满)--------------------') + # time.sleep(3) + else: + if overflow_detected=='浇筑满': + self._visual_close() + return + # print(f'--------已关闭已关闭-----------') + elif overflow_detected=="大堆料": + print(f'--------未浇筑满,大堆料-----------') + self._pulse_control('open',0.3) + time.sleep(0.3) + 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._visual_close() + return + else: + self._before_finish_time=None + 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 = 35.0 # 12.25由25--》35 + else: + 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.2 # 比例系数 + KI = 0 # 积分系数 + KD = 0 # 微分系数 + # KP = 0.15 # 比例系数 + # KI = 0.008 # 积分系数 + # KD = 0.08 # 微分系数 + # if TARGET_ANGLE <= 25.0: + # KP, KI, KD = 0.18, 0.008, 0.08 # 小角度,强控制 + # elif TARGET_ANGLE <= 40.0: + # KP, KI, KD = 0.15, 0.01, 0.06 # 中角度 + # else: + # KP, KI, KD = 0.12, 0.012, 0.04 # 大角度,温和控制 + # 计算误差 + error = current_angle - TARGET_ANGLE + dt = current_time - self._last_control_time + + # 积分项(抗饱和) + self._error_integral += error * dt + self._error_integral = max(min(self._error_integral, 50), -50) # 积分限幅 + + # 微分项 + error_derivative = (error - self._last_error) / dt if dt > 0 else 0 + + # PID输出 + pid_output = (KP * error + KI * self._error_integral + KD * error_derivative) + print(f"📊 PID计算: 误差={error:.2f}°, 积分={self._error_integral:.2f}, " + f"微分={error_derivative:.2f}, 输出={pid_output:.2f}") + # 更新历史值 + self._last_error = error + self._last_control_time = current_time + + # 状态机 + PID控制 + + if self.angle_mode == "normal": + self._normal_mode_advanced(current_angle, pid_output,TARGET_ANGLE) + + elif self.angle_mode == "reducing": + self._reducing_mode_advanced(current_angle, pid_output, TARGET_ANGLE) + + elif self.angle_mode == "maintaining": + self._maintaining_mode_advanced(current_angle, pid_output, TARGET_ANGLE) + + except Exception as e: + print(f"处理视觉回调时发生异常: {e}") + + def _normal_mode_advanced(self, current_angle, pid_output,target_angle): + """高级正常模式控制""" + if self.overflow: + self.angle_mode = "reducing" + print("检测到溢料,切换到减小模式") + return + + # 🎯 修复1: 添加强制控制机制 + + + # 基于PID输出的智能控制 + control_threshold = 2 # 从2.0减小到0.5,提高灵敏度 + + if abs(pid_output) > control_threshold: + if pid_output > 0: + # 需要减小角度(关门) + pulse_time = min(0.3, pid_output * 0.1) + self._pulse_control("close", pulse_time) + print(f"正常模式: 角度偏高{pid_output:.1f},关门{pulse_time:.2f}秒") + else: + # 需要增大角度(开门) + pulse_time = min(0.3, abs(pid_output) * 0.1) + self._pulse_control("open", pulse_time) + print(f"正常模式: 角度偏低{abs(pid_output):.1f},开门{pulse_time:.2f}秒") + else: + # 在死区内,保持静止 + error = current_angle - target_angle + abs_error = abs(error) + + # 强制控制:如果误差超过5度,强制控制 + if abs_error > 5: + if error > 0: # 当前角度 > 目标角度,需要关门 + pulse_time=0.1 # 根据误差计算脉冲时间 + self._pulse_control("close", pulse_time) + print(f"🚨 强制关门: 误差{abs_error:.1f}°过大,脉冲{pulse_time:.3f}s") + else: # 当前角度 < 目标角度,需要开门 + pulse_time =0.1 + self._pulse_control("open", pulse_time) + print(f"🚨 强制开门: 误差{abs_error:.1f}°过大,脉冲{pulse_time:.3f}s") + return + else: + self._stop_door() + print(f"正常模式: 角度在目标范围内,保持静止") + + def _reducing_mode_advanced(self, current_angle, pid_output, target_angle): + """高级减小模式控制""" + if not self.overflow: + if current_angle <= target_angle + 5.0: + self.angle_mode = "normal" + print("溢料消除且角度合适,返回正常模式") + else: + # 缓慢恢复 + self._pulse_control("close", 0.1) + return + + # 有溢料,积极减小角度 + if current_angle > target_angle: + # 使用PID输出计算控制量 + pulse_time = min(0.5, max(0.1, pid_output * 0.15)) + self._pulse_control("close", pulse_time) + print(f"减小模式: 积极关门{pulse_time:.2f}秒,PID输出:{pid_output:.1f}") + else: + self.angle_mode = "maintaining" + print("角度已达标,进入维持模式") + + def _maintaining_mode_advanced(self, current_angle, pid_output, target_angle): + """高级维持模式控制""" + if not self.overflow: + self.angle_mode = "normal" + print("溢料消除,返回正常模式") + return + + # 精确维持控制 + dead_zone = 1.5 # 更小的死区 + + if abs(pid_output) > dead_zone: + pulse_time = min(0.2, abs(pid_output) * 0.05) # 更精细的控制 + + if pid_output > 0: + self._pulse_control("close", pulse_time) + print(f"维持模式: 微调关门{pulse_time:.2f}秒") + else: + self._pulse_control("open", pulse_time) + print(f"维持模式: 微调开门{pulse_time:.2f}秒") + else: + self._stop_door() + print("维持模式: 角度精确控制中") + + def _pulse_control(self, action, duration): + """统一的脉冲控制方法""" + # 检查是否正在执行safe_control_lower_close,如果是则跳过relay操作 + if self._is_safe_closing: + thread_name = threading.current_thread().name + print(f"[{thread_name}] safe_control_lower_close正在执行,跳过脉冲控制 {action}") + return + + if duration <= 0: + return + + thread_name = threading.current_thread().name + print(f"[{thread_name}] 尝试脉冲控制 {action},时长 {duration:.2f}秒...") + + with self._door_control_lock: + self._current_controlling_thread = thread_name + print(f"[{thread_name}] 获得下料斗控制权,执行脉冲控制") + + if action == "open": + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + time.sleep(duration) + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + print(f"[{thread_name}] 开门脉冲: {duration:.2f}秒") + else: # close + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') + time.sleep(duration) + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + print(f"[{thread_name}] 关门脉冲: {duration:.2f}秒") + + self._current_controlling_thread = None + print(f"[{thread_name}] 释放下料斗控制权") + + def _stop_door(self): + """停止门运动""" + # 检查是否正在执行safe_control_lower_close,如果是则跳过relay操作 + if self._is_safe_closing: + thread_name = threading.current_thread().name + print(f"[{thread_name}] safe_control_lower_close正在执行,跳过停止门运动操作") + return + + thread_name = threading.current_thread().name + print(f"[{thread_name}] 尝试停止门运动...") + + with self._door_control_lock: + self._current_controlling_thread = thread_name + print(f"[{thread_name}] 获得下料斗控制权,执行停止操作") + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self._current_controlling_thread = None + print(f"[{thread_name}] 释放下料斗控制权") + + def _open_door(self, duration=0.5): + """打开门""" + self._pulse_control("open", 0.3) + + def _close_door(self, duration=0.5): + """关闭门""" + self._pulse_control("close", 1) + + def on_plc_update(self,data: int, binary: str): + #4即将振捣室5振捣室 64即将搅拌楼 66到达搅拌楼 + print(f"[数据回调] 数值: 0x{data:02X} | 十进制: {data:3d} | 二进制: {binary}") + self.plc_data=data + + @classmethod + def instance_exists(cls): + """检测实例是否存在""" + return cls._instance is not None + + def shutdown(self): + """关闭线程,清理资源""" + # 设置停止事件 + self._stop_event.set() + # 唤醒线程以便它能检测到停止事件 + self._new_data_available.set() + + self._is_running=False + self._is_finish=True + + if self.plc_service: + self.plc_service.stop_polling() + # 等待线程结束 + 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() + +# 创建默认实例 +# visual_callback_instance = VisualCallback() + +# 兼容层,保持原来的函数调用方式可用 +# def angle_visual_callback(current_angle, overflow_detected): +# """ +# 兼容旧版本的函数调用方式 +# 将调用转发到默认实例的angle_visual_callback方法 +# """ +# visual_callback_instance.angle_visual_callback(current_angle, overflow_detected) \ No newline at end of file