diff --git a/.gitignore b/.gitignore index 62b0bd5..054d570 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ PySide2_Fluent_Widgets.egg-info/ __pycache__ /core/__pycache__ /vision/__pycache__ +/opcua/__pycache__ +/feeding/__pycache__ diff --git a/11.jpg b/11.jpg new file mode 100644 index 0000000..51fe6d9 Binary files /dev/null and b/11.jpg differ diff --git a/LED_send/README.md b/LED_send/README.md new file mode 100644 index 0000000..5c91586 --- /dev/null +++ b/LED_send/README.md @@ -0,0 +1,75 @@ +# LED 信息屏动态显示 Python 调用C/C++ SDK + +本项目提供一个完整的 Python 示例,用于生成 LED 信息屏显示内容并通过 SDK 发送动态区域帧到 LED 屏。 +它支持: +自动加载依赖的 .so 库(包括 libiconv)和主 SDK 库 libbx_sdkDual.so +生成参数化 LED 表格图片 +使用 SDK 的动态区接口发送帧到 LED 屏 + +## 目录结构 + +LED_send/ +├── led_send.py # 主程序脚本 +├── libbx_sdkDual.so # SDK 动态库 +├── libiconv.so.2 # SDK 依赖库 +├── simsun.ttc # 字体文件,用于生成 LED 表格 +└── README.md # 说明文档 + + +## 配置 + +### 配置库路径 + +脚本为方便调用默认加载当前目录下的 libbx_sdkDual.so,如需修改: +```bash +MAIN_SO_NAME = "libbx_sdkDual.so" +CURRENT_DIR = "/your/path/to/so" +``` +### 数据对齐 +#### data数据 +```bash +data = { + "PlateVolume": "2.00", + "MouldCode": "SHR2B1-3", + "ProduceStartTime": "15:06", + "ArtifactID": "QR2B13099115D", + "Temper": "18.6℃", + "PlateIDSerial": "85", + "CheckResult": "合格", + "TotMete": "353.2", + "LowBucketWeighingValue": "75", + "HighBucketWeighingValue": "115", + "WorkshopTemperature": "12.4℃", + "VibrationFrequency": "10min/220HZ", + "FormulaProportion": "水泥:砂:石:粉煤灰:矿粉:外加剂:水\r\n0.70:1.56:2.78:0.15:0.15:0.006:0.33", + "DayStrengthValue": "白班:2024/11/27 22:00抗压 龄期:15h 强度25.9", + "NihtStrengthValue": "晚班:2024/11/26 07:55抗压 龄期:12h 强度25.2" +} +``` + +#### LED 屏表格数据对应关系 + +| LED 区域 | 示例图片位置 | 对应数据字段 | 示例值 | +|----------- |-----------------|-----------------------------------------|-------------------------------| +| 表头 | 屏幕最上方 | 固定文字 | 浇筑工序信息屏测试 | +| 本盘方量 | 第一行第1列 | PlateVolume | 2.00 | +| 当前模具 | 第一行第2列 | MouldCode | SHR2B1-3 | +| 高斗称值 | 第一行第3列 | HighBucketWeighingValue | 115 | +| 低斗称值 | 第一行第4列 | LowBucketWeighingValue | 75 | +| 投料时间 | 第二行第1列 | ProduceStartTime | 15:06 | +| 当前管片 | 第二行第2列 | ArtifactID | QR2B13099115D | +| 砼出料温度 | 第二行第3列 | Temper | 18.6℃ | +| 振捣频率 | 第二行第4列 | VibrationFrequency | 10min/220HZ | +| 累计盘次 | 第三行第1列 | PlateIDSerial | 85 | +| 隐蔽验收 | 第三行第2列 | CheckResult | 合格 | +| 车间环温 | 第三行第3列 | WorkshopTemperature | 12.4℃ | +| 任务方量 | 第三行第4列 | TotMete / 固定值 | 353.2 / 214.1 | +| 配方比例 | 第四行(跨3列) | FormulaProportion | 水泥:砂:石:粉煤灰:矿粉:外加剂:水... | +| 拆模强度 | 第五行(跨3列) | DayStrengthValue / NihtStrengthValue | 白班:2024/11/27 22:00抗压 龄期:15h 强度25.9 / 晚班:2024/11/26 07:55抗压 龄期:12h 强度25.2 | + + +## 函数调用 +```bash +from led_send import send_led_data +send_led_data(data) +``` diff --git a/LED_send/led_send.py b/LED_send/led_send.py new file mode 100644 index 0000000..645d70a --- /dev/null +++ b/LED_send/led_send.py @@ -0,0 +1,292 @@ +#!/usr/bin/env python3 +# coding: utf-8 +import os +import cv2 +from PIL import Image, ImageDraw, ImageFont +import ctypes +from ctypes import * +import glob +import sys + +# ============================================================ +# SDK Load +# ============================================================ + +CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) +MAIN_SO_NAME = "libbx_sdkDual.so" +MAIN_SO = os.path.join(CURRENT_DIR, MAIN_SO_NAME) + +def preload_shared_objects(so_dir): + print(f"自动加载 so 路径:{so_dir}") + if not os.path.isdir(so_dir): + print(f"错误:目录不存在: {so_dir}") + return None + + so_list = glob.glob(os.path.join(so_dir, "*.so*")) + iconv_files = [s for s in so_list if "libiconv" in os.path.basename(s)] + loaded = set() + + for f in iconv_files: + try: + ctypes.CDLL(f, mode=ctypes.RTLD_GLOBAL) + print(f"已加载 libiconv: {f}") + loaded.add(f) + except Exception as e: + print(f"加载失败 {f}: {e}") + + for f in so_list: + if os.path.basename(f) == MAIN_SO_NAME or f in loaded: + continue + try: + ctypes.CDLL(f, mode=ctypes.RTLD_GLOBAL) + print(f"已加载依赖库: {f}") + except Exception as e: + print(f"跳过无法加载的库 {f}: {e}") + + if os.path.exists(MAIN_SO): + try: + lib = ctypes.CDLL(MAIN_SO, mode=ctypes.RTLD_GLOBAL) + print(f"成功加载主库: {MAIN_SO}") + return lib + except Exception as e: + print(f"主库加载失败: {MAIN_SO} -> {e}") + return None + else: + print(f"主库不存在: {MAIN_SO}") + return None + +os.environ["LD_LIBRARY_PATH"] = CURRENT_DIR + ":" + os.environ.get("LD_LIBRARY_PATH", "") +os.environ["PATH"] = CURRENT_DIR + ":" + os.environ.get("PATH", "") + +lib = preload_shared_objects(CURRENT_DIR) +if lib is None: + print("无法加载主库,程序退出") + sys.exit(1) + +# ====================== 生成 LED 表格 ====================== +def generate_led_table(data, output_path="led_send.png", font_path="simsun.ttc"): + """ + 根据接口返回的 Data 生成 LED 显示表格,适配 640x448 LED 屏 + """ + try: + font_title = ImageFont.truetype(font_path, 24) + font_data = ImageFont.truetype(font_path, 20) + font_data_big = ImageFont.truetype(font_path, 22) + font_small = ImageFont.truetype(font_path, 16) + header_font = ImageFont.truetype(font_path, 30) + except IOError: + print("字体未找到,使用默认字体") + font_title = font_data = font_data_big = font_small = ImageFont.load_default() + header_font = ImageFont.load_default() + + total_width, total_height = 640, 448 + img = Image.new("RGB", (total_width, total_height), (0, 0, 0)) + draw = ImageDraw.Draw(img) + + col_count = 4 + row_count = 8 + row_heights = [int(total_height * 0.095)] * 6 + [int(total_height * 0.15), int(total_height * 0.15)] + y_positions = [0] + for h in row_heights[:-1]: + y_positions.append(y_positions[-1] + h) + col_width = total_width // col_count + + header_text = "浇筑工序信息屏测试" + bbox = draw.textbbox((0, 0), header_text, font=header_font) + tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1] + draw.text(((total_width - tw) // 2, 7), header_text, fill="Yellow", font=header_font) + + # safe float parse + try: + task_quantity = float(data.get("TotMete", 0)) + except Exception: + task_quantity = 0.0 + fixed_value = 214.1 + task_quantity_str = f"{task_quantity}" + fixed_value_str = f"/{fixed_value}" + + table_data = [ + ["本盘方量", "当前模具", "高斗称值", "低斗称值"], + [str(data.get("PlateVolume", "")), str(data.get("MouldCode", "")), str(data.get("HighBucketWeighingValue", "")), str(data.get("LowBucketWeighingValue", ""))], + ["投料时间", "当前管片", "砼出料温度", "振捣频率"], + [str(data.get("ProduceStartTime", "")), str(data.get("ArtifactID", "")), str(data.get("Temper", "")), str(data.get("VibrationFrequency", ""))], + ["累计盘次", "隐蔽验收", "车间环温", "任务方量"], + [str(data.get("PlateIDSerial", "任务方量")), str(data.get("CheckResult", "")), str(data.get("WorkshopTemperature", "")), ""], + ["配方比例", "", "", ""], + ["拆模强度", "", "", ""] + ] + + # 画表格框 + for r in range(row_count): + y1 = y_positions[r] + 40 + h = row_heights[r] + for c in range(col_count): + x1 = c * col_width + if r >= 6 and c == 1: + draw.rectangle([x1, y1, total_width - 1, y1 + h - 1], outline="white", width=1) + break + elif r >= 6 and c > 1: + continue + else: + draw.rectangle([x1, y1, x1 + col_width - 1, y1 + h - 1], outline="white", width=1) + + # 绘制文本 + for r in range(row_count): + y1 = y_positions[r] + 40 + h = row_heights[r] + for c in range(col_count): + x1 = c * col_width + content = table_data[r][c] + if not content.strip(): + if r == 5 and c == 3: + bbox_task = draw.textbbox((0, 0), task_quantity_str, font=font_data) + tw_task = bbox_task[2] - bbox_task[0] + th_task = bbox_task[3] - bbox_task[1] + draw.text((x1 + (col_width - 1.8 * tw_task) // 2, y1 + (h - th_task) // 2), task_quantity_str, fill="red", font=font_data) + bbox_fixed = draw.textbbox((0, 0), fixed_value_str, font=font_data) + tw_fixed = bbox_fixed[2] - bbox_fixed[0] + draw.text((x1 + (col_width - tw_fixed) // 2 + 0.78 * tw_task, y1 + (h - th_task) // 2), fixed_value_str, fill="green", font=font_data) + continue + + is_header = r in (0, 2, 4, 6, 7) + color = "green" if is_header else "red" + if color == "red" and r < 3: + font = font_data_big + elif color == "red" and r >= 6: + font = font_small + else: + font = font_title if is_header else font_data + + bbox = draw.textbbox((0, 0), content, font=font) + tw = bbox[2] - bbox[0] + th = bbox[3] - bbox[1] + draw.text((x1 + (col_width - tw) // 2, y1 + (h - th) // 2), content, fill=color, font=font) + + # 多行文本居中函数 + def draw_multiline_text_center(draw_obj, x, y, width, height, text, font_obj, fill="red"): + lines = text.split('\n') + bboxs = [draw_obj.textbbox((0, 0), line, font=font_obj) for line in lines] + total_h = sum(b[3] - b[1] for b in bboxs) + y_start = y + (height - total_h) // 2 + curr_y = y_start + for line, b in zip(lines, bboxs): + w = b[2] - b[0] + h = b[3] - b[1] + draw_obj.text((x + (width - w) // 2, curr_y), line, fill=fill, font=font_obj) + curr_y += h + + draw_multiline_text_center(draw, col_width * 1, y_positions[6] + 40, col_width * 3, row_heights[6], + str(data.get("FormulaProportion", "")).replace("\r", ""), font_small) + draw_multiline_text_center(draw, col_width * 1, y_positions[7] + 40, col_width * 3, row_heights[7], + f"{data.get('DayStrengthValue', '')}\n{data.get('NihtStrengthValue', '')}", font_small) + + img.save(output_path) + print(f"已生成参数化表格:{output_path}") + +# ====================== 动态区结构体 ====================== +class EQpageHeader_G6(Structure): + _fields_ = [ + ("PageStyle", c_uint8), ("DisplayMode", c_uint8), ("ClearMode", c_uint8), + ("Speed", c_uint8), ("StayTime", c_uint16), ("RepeatTime", c_uint8), + ("ValidLen", c_uint8), ("CartoonFrameRate", c_uint8), ("BackNotValidFlag", c_uint8), + ("arrMode", c_uint8), ("fontSize", c_uint8), ("color", c_uint8), + ("fontBold", c_uint8), ("fontItalic", c_uint8), ("tdirection", c_uint8), + ("txtSpace", c_uint8), ("Valign", c_uint8), ("Halign", c_uint8) + ] + +lib.bxDual_dynamicArea_DelArea_6G.argtypes = [c_char_p, c_uint32, c_uint8] +lib.bxDual_dynamicArea_AddAreaPic_6G.argtypes = [ + c_char_p, c_uint32, c_uint8, c_uint8, c_uint16, c_uint16, + c_uint16, c_uint16, POINTER(EQpageHeader_G6), c_char_p +] +lib.bxDual_dynamicArea_DelArea_6G.restype = c_int +lib.bxDual_dynamicArea_AddAreaPic_6G.restype = c_int + +# ====================== 发送动态区帧 ====================== +def send_dynamic_frame(ip="10.6.242.2", port=5005, frame=None, filename="led_send.png"): + if frame is None: + print("frame 为空!") #因为相机SDK接口需要的是待发送图片的地址,所以加上确认需要发送图片是否存在。 + return + + target_w, target_h = 640, 448 + resized = cv2.resize(frame, (target_w, target_h)) + + current_dir = os.path.dirname(os.path.abspath(__file__)) + save_path = os.path.join(current_dir, filename) + + # 使用 cv2.imwrite 保存确保文件编码一致 + cv2.imwrite(save_path, resized) + # 这些参数都可以设置,我备注一下参数名称和调节的信息 + page = EQpageHeader_G6() + page.PageStyle = 0 #数据页类型,默认为0 + page.DisplayMode = 2 #显示方式: 0x00 :随机显示 0x01 :静止显示 0x02 :快速打出 0x03 :向左移动 0x04 :向左连移 0x05 :向上移动 0x06 :向上连移 0x07 :闪烁 ...... + page.ClearMode = 1 #退出方式/清屏方式 + page.Speed = 10 #速度等级/背景速度等级 + page.StayTime = 1000 #停留时间, 单位为 10ms + page.RepeatTime = 1 #重复次数/背景拼接步长(左右拼接下为宽度, 上下拼接为高度) + page.ValidLen = 64 #用法比较复杂请参考协议,默认不动 + page.CartoonFrameRate = 0 #特技为动画方式时,该值代表其帧率 + page.BackNotValidFlag = 0 #背景无效标志 + #字体信息 + page.arrMode = 1 #排列方式--单行多行 + page.fontSize = 16 #字体大小 + page.color = 1 #字体颜色 E_Color_G56此通过此枚举值可以直接配置七彩色,如果大于枚举范围使用RGB888模式 + page.fontBold = 0 #是否为粗体 + page.fontItalic = 0 #是否为斜体 + page.tdirection = 0 #文字方向 + page.txtSpace = 0 #文字间隔 + page.Valign = 2 #纵向对齐方式(0系统自适应、1上对齐、2居中、3下对齐) + page.Halign = 1 #横向对齐方式(0系统自适应、1左对齐、2居中、3右对齐) + + print("删除旧动态区 ...") + try: + ret_del = lib.bxDual_dynamicArea_DelArea_6G(ip.encode(), port, 0xFF) + print("删除返回码:", ret_del) + except Exception as e: + print("调用 DelArea 失败:", e) + + try: + ret = lib.bxDual_dynamicArea_AddAreaPic_6G( + ip.encode("ascii"), port, 2, 0, 0, 0, target_w, target_h, + byref(page), save_path.encode("gb2312") + ) + if ret == 0: + print("Frame 发送成功!") + else: + print("Frame 发送失败,返回码:", ret) + except Exception as e: + print("调用 AddAreaPic 失败:", e) + +def send_led_data(data: dict): + img_path = os.path.join(CURRENT_DIR, "led_send.png") + generate_led_table(data, output_path=img_path) + #这里读取图片是为了保证生成图片函数已经在改文件夹下生成了图片,因为相机SDK接口需要的是待发送图片的地址,所以加上确认。 + frame = cv2.imread(img_path) + send_dynamic_frame(frame=frame, filename="led_send.png") + +# ============================================================ +# 主程序示例 +# ============================================================ + +if __name__ == "__main__": + data = { + "PlateVolume": "2.00", + "MouldCode": "SHR2B1-3", + "ProduceStartTime": "15:06", + "ArtifactID": "QR2B13099115D", + "Temper": "18.6℃", + "PlateIDSerial": "85", + "CheckResult": "合格", + "TotMete": "353.2", + "LowBucketWeighingValue": "75", + "HighBucketWeighingValue": "115", + "WorkshopTemperature": "12.4℃", + "VibrationFrequency": "10min/220HZ", + "FormulaProportion": "水泥:砂:石:粉煤灰:矿粉:外加剂:水\r\n0.70:1.56:2.78:0.15:0.15:0.006:0.33", + "DayStrengthValue": "白班:2024/11/27 22:00抗压 龄期:15h 强度25.9", + "NihtStrengthValue": "晚班:2024/11/26 07:55抗压 龄期:12h 强度25.2" + } + + send_led_data(data) + + diff --git a/LED_send/libbx_sdkDual.so b/LED_send/libbx_sdkDual.so new file mode 100644 index 0000000..48ed23b Binary files /dev/null and b/LED_send/libbx_sdkDual.so differ diff --git a/LED_send/libiconv.so.2 b/LED_send/libiconv.so.2 new file mode 100644 index 0000000..5f53274 Binary files /dev/null and b/LED_send/libiconv.so.2 differ diff --git a/LED_send/simsun.ttc b/LED_send/simsun.ttc new file mode 100644 index 0000000..e64e92e Binary files /dev/null and b/LED_send/simsun.ttc differ diff --git a/busisness/blls.py b/busisness/blls.py index 432cd84..8111cc7 100644 --- a/busisness/blls.py +++ b/busisness/blls.py @@ -20,12 +20,63 @@ class ArtifactBll: def get_artifact_task(self) -> List[ArtifactInfoModel]: """获取官片任务数据""" - return self.dal.get_top_artifact(5, "ArtifactID asc") + return self.dal.get_top_artifact(5, "ID desc") + def update_artifact_task(self, artifact_id: str, status: int) -> bool: + """更新管片任务状态""" + return self.dal.update_artifact(artifact_id, {"Status": status}) + + def finish_artifact_task(self, artifact_id: str,beton_volume) -> bool: + """完成管片任务""" + return self.dal.update_artifact(artifact_id, {"Status": 3,"BetonVolume":beton_volume,"EndTime":datetime.now()}) + + def insert_artifact_task(self,model:ArtifactInfoModel) -> bool: + + """插入管片任务""" + if self.dal.exists_by_id(model.ArtifactID): + return False + return self.dal.insert_artifact({ + "ArtifactID": model.ArtifactID, + "ArtifactActionID": model.ArtifactActionID, + "ArtifactIDVice1": model.ArtifactIDVice1, + "ProduceRingNumber": model.ProduceRingNumber, + "MouldCode": model.MouldCode, + "SkeletonID": model.SkeletonID, + "RingTypeCode": model.RingTypeCode, + "SizeSpecification": model.SizeSpecification, + "BuriedDepth": model.BuriedDepth, + "BlockNumber": model.BlockNumber, + "BetonVolume": model.BetonVolume, + "BetonTaskID": model.BetonTaskID, + "HoleRingMarking": model.HoleRingMarking, + "GroutingPipeMarking": model.GroutingPipeMarking, + "PolypropyleneFiberMarking": model.PolypropyleneFiberMarking, + "PStatus":1, + "Status": 2, + "Source": model.Source, + "BeginTime": model.BeginTime, + "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": 1, + "Source": 2, + "OptTime": datetime.now(), + }) def get_artifacting_task(self) -> ArtifactInfoModel: - """获取正在进行的官片任务数据""" - loc_item= self.dal.get_top_artifact(1,"","Status=2") + """获取正在进行的管片任务数据""" + loc_item= self.dal.get_top_artifact(1,"ID desc","Status=2") if loc_item: return loc_item[0] else: diff --git a/busisness/dals.py b/busisness/dals.py index 28ff0c9..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): @@ -42,7 +42,7 @@ class ArtifactDal(BaseDal): print(f"获取所有构件任务失败: {e}") return [] - def get_top_artifact(self, top: int,desc:str="ArtifactID asc",where:str="1=1") -> List[ArtifactInfoModel]: + def get_top_artifact(self, top: int,desc:str="ID desc",where:str="1=1") -> List[ArtifactInfoModel]: """获取top条数数据,根据ArtifactID升序""" try: # 确保top为正整数 @@ -59,6 +59,8 @@ class ArtifactDal(BaseDal): # 保证row的变量和模板变量一致 artifact = ArtifactInfoModel() artifact.ArtifactID=row["ArtifactID"] + artifact.ArtifactActionID=row["ArtifactActionID"] + artifact.ArtifactIDVice1=row["ArtifactIDVice1"] artifact.ProduceRingNumber=row["ProduceRingNumber"] artifact.MouldCode=row["MouldCode"] artifact.SkeletonID=row["SkeletonID"] @@ -68,6 +70,9 @@ class ArtifactDal(BaseDal): artifact.BlockNumber=row["BlockNumber"] artifact.BetonVolume=row["BetonVolume"] artifact.BetonTaskID=row["BetonTaskID"] + artifact.HoleRingMarking=row["HoleRingMarking"] + artifact.GapRingMarking=row["GroutingPipeMarking"] + artifact.PolypropyleneFiberMarking=row["PolypropyleneFiberMarking"] artifact.Status=row["Status"] artifact.BeginTime=row["BeginTime"] artifacts.append(artifact) @@ -78,6 +83,36 @@ class ArtifactDal(BaseDal): return [] + def exists_by_id(self, artifact_id: int) -> bool: + """根据构件ID获取构件任务""" + try: + sql = "SELECT count(1) FROM ArtifactTask WHERE ArtifactID = ?" + results = self.db_dao.execute_read(sql, (artifact_id,)) + + rows = list(results) + if rows[0][0] == 1: + return True + + return False + 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获取构件任务""" try: @@ -108,7 +143,7 @@ class ArtifactDal(BaseDal): """更新构件任务记录""" try: # 构建WHERE条件 - where_condition = {"ArtifactID": artifact_id} + where_condition = f"ArtifactID='{artifact_id}'" # 使用update方法更新数据 affected_rows = self.db_dao.update("ArtifactTask", update_data, where_condition) return affected_rows > 0 diff --git a/busisness/models.py b/busisness/models.py index 6b9b36f..b4289a2 100644 --- a/busisness/models.py +++ b/busisness/models.py @@ -308,3 +308,45 @@ class PDRecordModel: CreateTime: str="" #派单时间(下发) OptTime: str="" + +@dataclass +class LEDInfo: + """LED信息模型""" + # 任务单号 + TaskID: str + # 盘方量 + PlateVolume: str + # 模具编号 + MouldCode: str + # 生产投料时间 + ProduceStartTime: str + # 管片编号 + ArtifactID: str + # 环类型编码(无) + RingTypeCode: str + # 累计盘次 + PlateIDSerial: str + # 检验结果 + CheckResult: str + # 高斗称值(无) + UpperWeight:float + # 砼出料温度温度 + Temper: str + # 车间温度 + WorkshopTemperature: str + # 低桶称量值 + LowBucketWeighingValue: str + # 振动频率 + VibrationFrequency: str + # 总计量 + TotMete: str + # 已浇筑方量 + BetonVolumeAlready: str + # 水温 + WaterTemperature: str + # 配方比例 + FormulaProportion: str + # 白班拆模强度文本描述 + DayStrengthValue: str + # 夜班拆模强度文本描述 + NihtStrengthValue: str \ No newline at end of file diff --git a/close_test.py b/close_test.py new file mode 100644 index 0000000..2d142b3 --- /dev/null +++ b/close_test.py @@ -0,0 +1,16 @@ +# main.py +import time +from config.settings import app_set_config +from hardware.relay import RelayController +from hardware.inverter import InverterController +from hardware.transmitter import TransmitterController +import time +import vision.visual_callback_1203 as angle_visual + + +def main(): + replay_controller=RelayController() + replay_controller.close_all() + +if __name__ == "__main__": + main() diff --git a/config/ini_manager.py b/config/ini_manager.py index f58499c..4f62c01 100644 --- a/config/ini_manager.py +++ b/config/ini_manager.py @@ -141,7 +141,31 @@ class IniManager: def log_path(self): """获取日志文件路径""" return self._read_config_value('app', 'LOG_PATH', 'logs/app.log', str) + + @property + def opcua_endpoint(self): + """获取OPC UA服务器端点""" + return self._read_config_value('app', 'OPCUA_ENDPOINT', 'opc.tcp://192.168.250.64:4840/zjsh_feed/server/', str) + @property + def upper_transmitter_ip(self): + """获取上料斗变送器IP""" + return self._read_config_value('app', 'UPPER_TRANSMITTER_IP', '192.168.250.63', str) + + @property + def upper_transmitter_port(self): + """获取上料斗变送器端口""" + return self._read_config_value('app', 'UPPER_TRANSMITTER_PORT', 502, int) + + @property + def lower_transmitter_ip(self): + """获取下料斗变送器IP""" + return self._read_config_value('app', 'LOWER_TRANSMITTER_IP', '192.168.250.66', str) + + @property + def lower_transmitter_port(self): + """获取下料斗变送器端口""" + return self._read_config_value('app', 'LOWER_TRANSMITTER_PORT', 8234, int) ini_manager = IniManager() diff --git a/config/settings.py b/config/settings.py index 528c404..d5006ad 100644 --- a/config/settings.py +++ b/config/settings.py @@ -5,11 +5,15 @@ class Settings: def __init__(self): # 项目根目录 self.project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - + self.test_need_weight=2000 # 网络继电器配置 - self.relay_host = '192.168.0.18' + self.relay_host = '192.168.250.62' self.relay_port = 50000 + self.debug_feeding=False + #调试模式上,网络继点器禁用,模型推理启用 + self.debug_mode=False + # 摄像头配置 self.camera_type = "ip" self.camera_ip = "192.168.1.51" @@ -18,17 +22,37 @@ class Settings: self.camera_password = "XJ123456" self.camera_channel = 1 + self.camera_configs = { + # 'cam1': { + # 'type': 'ip', + # 'ip': '192.168.250.60', + # 'port': 554, + # 'username': 'admin', + # 'password': 'XJ123456', + # 'channel': 1 + # }, + 'cam2': { + 'type': 'ip', + 'ip': '192.168.250.61', + 'port': 554, + 'username': 'admin', + 'password': 'XJ123456', + 'channel': 1 + } + } + # 下料控制参数 self.min_required_weight = 500 # 模具车最小需要重量(kg) self.target_vehicle_weight = 5000 # 目标模具车重量(kg) self.upper_buffer_weight = 500 # 上料斗缓冲重量(kg) self.single_batch_weight = 2500 # 单次下料重量(kg) + # 角度控制参数 - self.target_angle = 20.0 # 目标角度 + self.target_angle = 30.0 # 目标角度 self.min_angle = 10.0 # 最小角度 self.max_angle = 80.0 # 最大角度 - self.angle_threshold = 60.0 # 角度阈值 + self.angle_threshold = 50.0 # 角度阈值 self.angle_tolerance = 5.0 # 角度容差 # 变频器配置 @@ -36,19 +60,19 @@ class Settings: self.frequencies = [220.0, 230.0, 240.0] # 下料阶段频率(Hz) # 模型路径配置 - self.models_dir = os.path.join(self.project_root, 'vision', 'models') - self.angle_model_path = os.path.join(self.models_dir, 'angle.pt') - self.overflow_model_path = os.path.join(self.models_dir, 'overflow.pt') - self.alignment_model_path = os.path.join(self.models_dir, 'alig.pt') + self.models_dir = os.path.join(self.project_root, 'vision') + self.angle_model_path = os.path.join(self.models_dir, 'obb_angle_model', 'obb.rknn') + self.overflow_model_path = os.path.join(self.models_dir,'overflow_model', 'yiliao_cls.rknn') + # self.alignment_model_path = os.path.join(self.models_dir, 'align_model', 'yolov11_cls_640v6.rknn') # ROI路径配置 - self.roi_file_path = os.path.join(self.project_root, 'vision', 'roi_coordinates', '1_rois.txt') + self.roi_file_path = os.path.join(self.models_dir, 'overflow_model', 'roi_coordinates', '1_rois.txt') # 系统控制参数 self.visual_check_interval = 1.0 # 视觉检查间隔(秒) self.alignment_check_interval = 0.5 # 对齐检查间隔(秒) self.max_error_count = 3 # 最大错误计数 - self.lower_feeding_interval = 1.0 # 下料轮询间隔(秒) + self.lower_feeding_interval = 0.1 # 下料轮询间隔(秒) # RFID配置 self.rfid_host = '192.168.1.190' @@ -60,6 +84,11 @@ class Settings: # self.block_numbers=['B1','B2','B3','L1','L2','F'] #需核实上下位漏斗容量 self.max_upper_volume = 2.4 # 上料斗容量(方) - self.max_lower_volume = 2.4 # 下料斗容量(方) - + #下料到下料斗最大下到多少,并非最大容量 + self.max_lower_volume = 2.2 # 下料斗容量(方) + + #led + self.led_interval = 2 # LED闪烁间隔(秒) + +app_set_config = Settings() diff --git a/controller/main_controller.py b/controller/main_controller.py index 19db865..082d9c9 100644 --- a/controller/main_controller.py +++ b/controller/main_controller.py @@ -1,15 +1,13 @@ from PySide6.QtCore import QTimer, Signal, QObject, Qt from PySide6.QtWidgets import QApplication # 用于获取主线程 import threading -from hardware import transmitter from view.main_window import MainWindow from .camera_controller import CameraController from .bottom_control_controller import BottomControlController from .hopper_controller import HopperController from .hopper_controller import UpperHopperPosition - -from service.msg_recorder import MessageRecorder from common.constant_config_manager import ConfigManager +from service.msg_recorder import MessageRecorder from service.opcua_ui_client import OpcuaUiClient from service.artifact_query_thread import ArtifactInfoQueryThread # 管片任务查询 @@ -24,7 +22,7 @@ class MainController: self.msg_recorder = MessageRecorder() self.msg_recorder.normal_record("开始自动智能浇筑系统") # 记录系统状态消息 - + # 初始化子界面和控制器 self._initSubViews() self._initSubControllers() @@ -72,7 +70,8 @@ class MainController: def _initSubViews(self): pass - + + def __connectSignals(self): self.main_window.about_to_close.connect(self.handleMainWindowClose) # 处理主界面关闭 @@ -84,11 +83,11 @@ class MainController: def handleMainWindowClose(self): """主界面关闭""" - self.msg_recorder.normal_record("关闭自动智能浇筑系统") # 记录系统状态消息 - + self.msg_recorder.normal_record("关闭自动智能浇筑系统") + # 停止系统底部控制器中的线程 if hasattr(self, 'bottom_control_controller'): - self.bottom_control_controller.stop_threads() + self.bottom_control_controller.stop_threads() # 停止opc客户端 if hasattr(self, 'opc_client'): self._stop_opc_client() diff --git a/core/state.py b/core/state.py index 6914504..5966d86 100644 --- a/core/state.py +++ b/core/state.py @@ -7,12 +7,34 @@ class SystemState(QObject): state_updated=Signal(str,object) def __init__(self): super().__init__() + # + self._watched_props = [] + self.lock = threading.RLock() + # 系统运行状态 - self.running = False + self.running = True - # 下料控制相关 + # 上料斗控制相关 self._upper_door_position = 'default' # default(在搅拌楼下接料), over_lower(在下料斗上方), returning(返回中) - self._lower_feeding_stage = 0 # 0:未下料, 1:第一阶段, 2:第二阶段, 3:第三阶段, 4:等待模具车对齐 + # 是否破拱 + self._upper_is_arch_=False + self._upper_door_closed=True + self._upper_weight=0 + self._upper_volume=0.0 + + #下料斗状态想着 + self.lower_feeding_stage = 0 # 0:未下料, 1:第一阶段, 2:第二阶段, 3:第三阶段, 4:等待模具车对齐 + self._lower_is_arch_=False + self._lower_weight=0 + self._lower_angle=0.0 + + #模具车状态 + self._mould_weight=0 + self._mould_frequency=220 + self._mould_vibrate_status=0 #1振动中0未振动 + #记录模具开始振动的时间 + self.mould_vibrate_time=0 + self.lower_feeding_cycle = 0 # 下料斗下料循环次数 self.upper_feeding_count = 0 # 上料斗已下料次数 self.upper_feeding_max = 2 #上料斗最大下料次数 @@ -21,9 +43,14 @@ class SystemState(QObject): self.last_upper_weight = 0 self.last_lower_weight = 0 self.last_weight_time = 0 - self.need_total_weight=0 + #需要下料的总重量 + self._mould_need_weight=0 + #完成下料的总重量 + self._mould_finish_weight=0 + self.initial_upper_weight=0 self.initial_lower_weight=0 + # 错误计数 self.upper_weight_error_count = 0 @@ -31,7 +58,8 @@ class SystemState(QObject): # 视觉系统状态 self.angle_control_mode = "normal" # 角度控制模式: normal, reducing, maintaining, recovery - self.overflow_detected = False # 堆料检测 + self.overflow_detected = "0" # 堆料检测 + self.current_finish_status=False # 当前是否完成浇筑满 self.door_opening_large = False # 夹角 self.vehicle_aligned = False # 模具车是否对齐 self.last_angle = None # 上次检测角度 @@ -42,16 +70,10 @@ class SystemState(QObject): #当前生产的管片 self.current_artifact=None #当前生产状态 - self.feed_status=FeedStatus.FNone + self._feed_status=FeedStatus.FNone #每方重量 - self.density=2500 - - - # 记录需要监听的属性名(筛选掉不需要发信号的内部变量) - #是否破拱 - self._upper_is_arch_=False - self._lower_is_arch_=False - self.lock = threading.RLock() + self.density=2416.4 + # self._watched_props = [k for k in self.__dict__ if k.startswith('_')] def __setattr__(self, name, value): @@ -82,4 +104,6 @@ class FeedStatus(IntEnum): # 下料3 FFeed3 = 8 #完成(管片生产完成) - FFinished = 11 \ No newline at end of file + FFinished = 11 + + FFeed=12 \ No newline at end of file diff --git a/core/system.py b/core/system.py index 18d452a..e94e9f3 100644 --- a/core/system.py +++ b/core/system.py @@ -2,35 +2,41 @@ import threading import time import cv2 -from config.settings import Settings from core.state import SystemState from hardware.relay import RelayController from hardware.inverter import InverterController from hardware.transmitter import TransmitterController from hardware.RFID.rfid_service import rfid_service -from vision.camera import CameraController +from vision.camera import DualCameraController from vision.detector import VisionDetector from feeding.controller import FeedingController +from service.mould_service import app_web_service +from config.settings import app_set_config class FeedingControlSystem: - def __init__(self, settings: Settings): - self.settings = settings + def __init__(self): self.state = SystemState() # 初始化硬件控制器 self.relay_controller = RelayController( - host=settings.relay_host, - port=settings.relay_port + host=app_set_config.relay_host, + port=app_set_config.relay_port ) self.inverter_controller = InverterController(self.relay_controller) self.transmitter_controller = TransmitterController(self.relay_controller) # 初始化视觉系统 - self.camera_controller = CameraController() - self.vision_detector = VisionDetector(settings) + self.camera_controller = DualCameraController(app_set_config.camera_configs) + self.vision_detector = VisionDetector() + + # 初始化RFID控制器 + self.rfid_controller = rfid_service( + host=app_set_config.rfid_host, + port=app_set_config.rfid_port + ) # 初始化下料控制器 self.feeding_controller = FeedingController( self.relay_controller, @@ -39,68 +45,49 @@ class FeedingControlSystem: self.vision_detector, self.camera_controller, self.rfid_controller, - self.state, - settings + self.state ) - # 初始化RFID控制器 - self.rfid_controller = rfid_service( - host=settings.rfid_host, - port=settings.rfid_port - ) + # 线程管理 self.monitor_thread = None self.visual_control_thread = None self.alignment_check_thread = None self.lower_feeding_thread = None + self.led_thread = None def initialize(self): """初始化系统""" print("初始化控制系统...") + + # self.check_device_connectivity() - # 设置摄像头配置 - self.camera_controller.set_config( - camera_type=self.settings.camera_type, - ip=self.settings.camera_ip, - port=self.settings.camera_port, - username=self.settings.camera_username, - password=self.settings.camera_password, - channel=self.settings.camera_channel - ) - - # 初始化摄像头 - if not self.camera_controller.setup_capture(): - raise Exception("摄像头初始化失败") - - # 加载视觉模型 - if not self.vision_detector.load_models(): - raise Exception("视觉模型加载失败") - - if not self.check_device_connectivity(): - raise Exception("设备连接失败") - - # 启动系统监控 + # self.camera_controller.start_cameras() + # if not app_set_config.debug_feeding: + # 启动系统监控(要料,破拱)线程 self.start_monitoring() + + # 启动视觉控制(角度、溢出)线程 + # self.start_visual_control() - # 启动视觉控制 - self.start_visual_control() - - # 启动对齐检查 + # 启动对齐检查线程 self.start_alignment_check() - # 启动下料轮询线程 + # 启动下料线程 self.start_lower_feeding() - + #LED屏 + # self.start_led() print("控制系统初始化完成") def start_monitoring(self): """启动系统监控""" - self.state.running = True + print('振动和要料监控线程启动') self.monitor_thread = threading.Thread( target=self._monitor_loop, - daemon=True + daemon=True, + name='monitor' ) self.monitor_thread.start() @@ -108,7 +95,7 @@ class FeedingControlSystem: """监控循环""" while self.state.running: try: - self.feeding_controller.check_upper_material_request() + # self.feeding_controller.check_upper_material_request() self.feeding_controller.check_arch_blocking() time.sleep(1) except Exception as e: @@ -116,9 +103,11 @@ class FeedingControlSystem: def start_visual_control(self): """启动视觉控制""" + print('视觉控制线程启动') self.visual_control_thread = threading.Thread( target=self._visual_control_loop, - daemon=True + daemon=True, + name='visual_control' ) self.visual_control_thread.start() @@ -126,39 +115,84 @@ class FeedingControlSystem: """视觉控制循环""" while self.state.running: try: - current_frame = self.camera_controller.capture_frame() + # print('visual_control') + current_frame = self.camera_controller.get_single_latest_frame() if current_frame is not None: # 执行视觉控制逻辑 self.feeding_controller.visual_control(current_frame) - time.sleep(self.settings.visual_check_interval) + time.sleep(app_set_config.visual_check_interval) except Exception as e: print(f"视觉控制循环错误: {e}") - time.sleep(self.settings.visual_check_interval) + time.sleep(app_set_config.visual_check_interval) def start_alignment_check(self): """启动对齐检查""" + print('对齐检查线程启动') self.alignment_check_thread = threading.Thread( target=self._alignment_check_loop, - daemon=True + daemon=True, + name='align_check' ) self.alignment_check_thread.start() def _alignment_check_loop(self): """对齐检查循环""" + loc_align_status=False + loc_before_status=None while self.state.running: try: - if self.state._lower_feeding_stage == 4: # 等待对齐阶段 - current_frame = self.camera_controller.capture_frame() + if self.state.lower_feeding_stage == 4: # 等待对齐阶段 + current_frame = self.camera_controller.get_single_latest_frame() if current_frame is not None: - self.state.vehicle_aligned = self.vision_detector.detect_vehicle_alignment(current_frame) + self.state.vehicle_aligned = self.alignment_check_status() if self.state.vehicle_aligned: + # loc_count+=1 print("检测到模具车对齐") else: print("模具车未对齐") - time.sleep(self.settings.alignment_check_interval) + # time.sleep(app_set_config.alignment_check_interval) + # loc_align_status=self.alignment_check_status() + # if loc_align_status and not loc_before_status: + # print("模具车由未对齐到对齐") + # self.state.vehicle_aligned=True + # elif not loc_align_status and loc_before_status: + # print("模具车由对齐到未对齐") + # self.state.vehicle_aligned=False + + # if loc_before_status!=loc_align_status: + # loc_before_status=loc_align_status + except Exception as e: print(f"对齐检查循环错误: {e}") - time.sleep(self.settings.alignment_check_interval) + finally: + time.sleep(app_set_config.alignment_check_interval) + + + def alignment_check_status(self)->bool: + """对齐检查循环""" + loc_aligned=False + loc_count=0 + for i in range(4): + try: + current_frame = self.camera_controller.get_single_latest_frame() + if current_frame is not None: + loc_aligned = self.vision_detector.detect_vehicle_alignment(current_frame) + if loc_aligned: + loc_count+=1 + print("检测到模具车对齐") + else: + loc_count=0 + print("模具车未对齐") + time.sleep(app_set_config.alignment_check_interval) + except Exception as e: + print(f"对齐检查循环错误: {e}") + time.sleep(app_set_config.alignment_check_interval) + + if loc_count>=3: + loc_aligned=True + else: + loc_aligned=False + return loc_aligned def start_lower_feeding(self): """启动下料流程""" @@ -173,7 +207,33 @@ class FeedingControlSystem: """启动下料流程""" while self.state.running: self.feeding_controller.start_feeding() - time.sleep(self.settings.lower_feeding_interval) + time.sleep(app_set_config.lower_feeding_interval) + + + + def start_led(self): + """启动LED流程""" + self.led_thread = threading.Thread( + target=self._start_led, + name="LED", + daemon=True + ) + self.led_thread.start() + + def _start_led(self): + """启动LED流程""" + while self.state.running: + led_info = app_web_service.get_pouring_led() + if led_info: + if self.state.current_artifact.MouldCode==led_info.MouldCode: + led_info.RingTypeCode=self.state.current_artifact.RingTypeCode + led_info.UpperWeight=self.state._upper_weight + led_info.LowerWeight=self.state._lower_weight + led_info.VibrationFrequency=self.state._mould_frequency + + #发送到LED屏 + + time.sleep(app_set_config.led_interval) def check_device_connectivity(self) -> bool: """检查关键设备连接状态""" @@ -190,15 +250,15 @@ class FeedingControlSystem: return False # 尝试读取变频器一个寄存器(测试连接) - test_result = self.relay_controller.modbus_client.read_holding_registers( - address=0x00, - count=1, - slave=self.inverter_controller.config['slave_id'] - ) + # test_result = self.relay_controller.modbus_client.read_holding_registers( + # address=0x00, + # count=1, + # slave=self.inverter_controller.config['slave_id'] + # ) - if isinstance(test_result, Exception): - print("变频器连接测试失败") - return False + # if isinstance(test_result, Exception): + # print("变频器连接测试失败") + # return False # 检查下料斗变送器连接 test_weight = self.transmitter_controller.read_data(2) diff --git a/db/messages.db b/db/messages.db new file mode 100644 index 0000000..a4553b1 Binary files /dev/null and b/db/messages.db differ diff --git a/db/three.db b/db/three.db index 6681482..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/table表设计.doc b/doc/table表设计.doc index 9d8a23d..f19a6bb 100644 Binary files a/doc/table表设计.doc and b/doc/table表设计.doc differ diff --git a/doc/控制程序对接.docx b/doc/控制程序对接.docx new file mode 100644 index 0000000..d65a2da Binary files /dev/null and b/doc/控制程序对接.docx 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/__pycache__/__init__.cpython-39.pyc b/feeding/__pycache__/__init__.cpython-39.pyc index 28e57b6..386b49b 100644 Binary files a/feeding/__pycache__/__init__.cpython-39.pyc and b/feeding/__pycache__/__init__.cpython-39.pyc differ diff --git a/feeding/__pycache__/controller.cpython-39.pyc b/feeding/__pycache__/controller.cpython-39.pyc index e56c404..311175d 100644 Binary files a/feeding/__pycache__/controller.cpython-39.pyc and b/feeding/__pycache__/controller.cpython-39.pyc differ diff --git a/feeding/__pycache__/process.cpython-39.pyc b/feeding/__pycache__/process.cpython-39.pyc index cf4b481..4f5bdf1 100644 Binary files a/feeding/__pycache__/process.cpython-39.pyc and b/feeding/__pycache__/process.cpython-39.pyc differ diff --git a/feeding/controller.py b/feeding/controller.py index dd31d53..e1bd80a 100644 --- a/feeding/controller.py +++ b/feeding/controller.py @@ -1,13 +1,15 @@ # feeding/controller.py import time +from core.state import FeedStatus from feeding.process import FeedingProcess from busisness.blls import ArtifactBll +from config.settings import app_set_config class FeedingController: def __init__(self, relay_controller, inverter_controller, transmitter_controller, vision_detector, - camera_controller, rfid_controller,state, settings): + camera_controller, rfid_controller,state): self.relay_controller = relay_controller self.inverter_controller = inverter_controller self.transmitter_controller = transmitter_controller @@ -15,14 +17,13 @@ class FeedingController: self.camera_controller = camera_controller self.rfid_controller = rfid_controller self.state = state - self.settings = settings self.artifact_bll = ArtifactBll() # 初始化下料流程 self.process = FeedingProcess( relay_controller, inverter_controller, transmitter_controller, vision_detector, - camera_controller, state, settings + camera_controller, state ) @@ -41,18 +42,21 @@ class FeedingController: if current_weight is None: self.state.upper_weight_error_count += 1 print(f"上料斗重量读取失败,错误计数: {self.state.upper_weight_error_count}") - if self.state.upper_weight_error_count >= self.settings.max_error_count: + if self.state.upper_weight_error_count >= app_set_config.max_error_count: print("警告:上料斗传感器连续读取失败,请检查连接") return False #需要搅拌楼通知下完料后移到上料斗上方 - if self.state._upper_door_position != 'over_lower': - self.state._upper_door_position = 'over_lower' + self.state.upper_weight_error_count = 0 # 判断是否需要要料:当前重量 < 目标重量 + 缓冲重量 - if current_weight < (self.settings.single_batch_weight + self.settings.min_required_weight): - print("上料斗重量不足,通知搅拌楼要料") - self.request_material_from_mixing_building() # 请求搅拌楼下料 - return True + if self.state._feed_status != FeedStatus.FUpperToLower: + if current_weight < (app_set_config.min_required_weight): + print("上料斗重量不足,通知搅拌楼要料") + self.request_material_from_mixing_building() # 请求搅拌楼下料 + return True + else: + if self.state._upper_door_position != 'over_lower': + self.state._upper_door_position = 'over_lower' return False def request_material_from_mixing_building(self): @@ -60,7 +64,7 @@ class FeedingController: 请求搅拌楼下料 """ print("发送要料请求至搅拌楼...") - # self.settings. + # self.process.return_upper_door_to_default() @@ -71,14 +75,14 @@ class FeedingController: def check_arch_blocking(self): """检查是否需要破拱""" current_time = time.time() - + # 检查下料斗破拱(只有在下料过程中才检查) - if self.state._lower_feeding_stage in [1, 2, 3]: # 在所有下料阶段检查 + if self.state.lower_feeding_stage in [1, 2, 3]: # 在所有下料阶段检查 lower_weight = self.transmitter_controller.read_data(2) if lower_weight is not None: # 检查重量变化是否过慢(小于0.1kg变化且时间超过10秒) - if (abs(lower_weight - self.state.last_lower_weight) < 0.1) and \ - (current_time - self.state.last_weight_time) > 10: + if (abs(lower_weight - self.state.last_lower_weight) < 10) and \ + (current_time - self.state.last_weight_time) > 1: print("下料斗可能堵塞,启动破拱") self.state._lower_is_arch_=True self.relay_controller.control(self.relay_controller.BREAK_ARCH_LOWER, 'open') @@ -89,17 +93,17 @@ class FeedingController: self.state.last_lower_weight = lower_weight # 检查上料斗破拱(在上料斗向下料斗下料时检查) - if (self.state._upper_door_position == 'over_lower' and - self.state._lower_feeding_stage in [0, 1, 2, 3, 4]): # 在任何阶段都可能需要上料斗破拱 + if self.state._feed_status == FeedStatus.FUpperToLower: # 在任何阶段都可能需要上料斗破拱 + print('上料斗振动线程启用中...') upper_weight = self.transmitter_controller.read_data(1) if upper_weight is not None: - # 检查重量变化是否过慢(小于0.1kg变化且时间超过10秒) - if (abs(upper_weight - self.state.last_upper_weight) < 0.1) and \ - (current_time - self.state.last_weight_time) > 10: + # 检查重量变化是否过慢(小于0.1kg变化且时间超过10秒),觉得有点小。改成 + if (abs(upper_weight - self.state.last_upper_weight) < 25) and \ + (current_time - self.state.last_weight_time) > 1: print("上料斗可能堵塞,启动破拱") self.state._upper_is_arch_=True self.relay_controller.control(self.relay_controller.BREAK_ARCH_UPPER, 'open') - time.sleep(2) + time.sleep(5) self.relay_controller.control(self.relay_controller.BREAK_ARCH_UPPER, 'close') self.state._upper_is_arch_=False @@ -115,78 +119,128 @@ class FeedingController: 视觉控制主逻辑 """ # 检测是否溢料 - overflow = self.vision_detector.detect_overflow(current_frame) - - # 获取当前角度 - current_angle = self.vision_detector.detect_angle(image=current_frame) - + # print('视觉控制线程启用中...') + # self.state.overflow_detected = self.vision_detector.detect_overflow(current_frame) + overflow=self.state.overflow_detected in ["大堆料", "小堆料"] + current_angle =self.vision_detector.detect_angle(image=current_frame) if current_angle is None: print("无法获取当前角度,跳过本次调整") return - - print(f"当前角度: {current_angle:.2f}°, 溢料状态: {overflow}, 控制模式: {self.state.angle_control_mode}") - - # 状态机控制逻辑 - if self.state.angle_control_mode == "normal": - # 正常模式 - if overflow and current_angle > self.settings.angle_threshold: - # 检测到堆料且角度过大,进入角度减小模式 - print("检测到堆料且角度过大,关闭出砼门开始减小角度") - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'close') - self.state.angle_control_mode = "reducing" - else: - # 保持正常开门 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'open') - - elif self.state.angle_control_mode == "reducing": - # 角度减小模式 - if current_angle <= self.settings.target_angle + self.settings.angle_tolerance: - # 角度已达到目标范围 - if overflow: - # 仍有堆料,进入维持模式 - print(f"角度已降至{current_angle:.2f}°,仍有堆料,进入维持模式") - self.state.angle_control_mode = "maintaining" - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'open') # 先打开门 - else: - # 无堆料,恢复正常模式 - print(f"角度已降至{current_angle:.2f}°,无堆料,恢复正常模式") - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'open') - self.state.angle_control_mode = "normal" - - elif self.state.angle_control_mode == "maintaining": - # 维持模式 - 使用脉冲控制 - if not overflow: - # 堆料已消除,恢复正常模式 - print("堆料已消除,恢复正常模式") - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'open') - self.state.angle_control_mode = "normal" - else: - # 继续维持角度控制 - self.pulse_control_door_for_maintaining() - - elif self.state.angle_control_mode == "recovery": - # 恢复模式 - 逐步打开门 - if overflow: - # 又出现堆料,回到角度减小模式 - print("恢复过程中又检测到堆料,回到角度减小模式") - self.state.angle_control_mode = "maintaining" - else: - # 堆料已消除,恢复正常模式 - print("堆料已消除,恢复正常模式") - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'open') - self.state.angle_control_mode = "normal" - self.state.last_angle = current_angle + self.state._lower_angle=current_angle + print(f"当前角度: {current_angle:.2f}°") + # return + if self.state.overflow_detected!="浇筑满": + if current_angle is None: + print("无法获取当前角度,跳过本次调整") + return + + # print(f"当前角度: {current_angle:.2f}°, 溢料状态: {overflow}, 控制模式: {self.state.angle_control_mode}") + + # 状态机控制逻辑 + if self.state.angle_control_mode == "normal": + # 正常模式大于app_set_config.angle_threshold=60度 + if overflow: + # if current_angle > app_set_config.angle_threshold: + # # 检测到堆料且角度过大,进入角度减小模式 + # print("检测到堆料且角度过大,关闭出砼门开始减小角度") + # self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + # self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') + self.state.angle_control_mode = "reducing" + else: + # 保持正常开门 30 + # self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + print(f'当前重量:{self.state._mould_finish_weight:.2f}kg, 目标重量:{self.state._mould_need_weight:.2f}kg') + if self.state._mould_need_weight>0: + + if self.state._mould_finish_weight/self.state._mould_need_weight>=0.8: + print(f"完成重量占比{self.state._mould_finish_weight/self.state._mould_need_weight:.2f},半开出砼门") + #半开出砼门 + if current_angle >app_set_config.target_angle: + # 角度已降至目标范围,关闭出砼门 + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') + time.sleep(0.3) + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + else: + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + time.sleep(0.32) + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + else: + #全开砼门 + if current_angle > app_set_config.angle_threshold: + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + + + elif self.state.angle_control_mode == "reducing": + # 角度减小模式 + if overflow: + if current_angle <= app_set_config.target_angle: + # 角度已达到目标范围 + + # 仍有堆料,进入维持模式 + print(f"角度已降至{current_angle:.2f}°,仍有堆料,进入维持模式") + if current_angle<=app_set_config.min_angle: + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + time.sleep(0.1) + self.state.angle_control_mode = "maintaining" + # self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + # self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') + else: + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') + # time.sleep(0.05) + # 先打开门 + else: + # 无堆料,恢复正常模式 + print(f"角度已降至{current_angle:.2f}°,无堆料,恢复正常模式") + # self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + # self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self.state.angle_control_mode = "normal" + + elif self.state.angle_control_mode == "maintaining": + # 维持模式 - 使用脉冲控制 + if not overflow: + # 堆料已消除,恢复正常模式 + print("堆料已消除,恢复正常模式") + # self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + # self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self.state.angle_control_mode = "normal" + else: + # 继续维持角度控制 + self.pulse_control_door_for_maintaining() + + elif self.state.angle_control_mode == "recovery": + # 恢复模式 - 逐步打开门 + if overflow: + # 又出现堆料,回到角度减小模式 + print("恢复过程中又检测到堆料,回到角度减小模式") + self.state.angle_control_mode = "maintaining" + else: + # 堆料已消除,恢复正常模式 + print("堆料已消除,恢复正常模式") + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self.state.angle_control_mode = "normal" + else: + # self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control_lower_close() + def pulse_control_door_for_maintaining(self): """ 用于维持模式的脉冲控制 保持角度在目标范围内 """ - print("执行维持脉冲控制") - # 关门1秒 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'close') - time.sleep(1.0) - # 开门1秒 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'open') - time.sleep(1.0) + print("进入维持模式") + # 关门时间 + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') + time.sleep(0.2) + # 开门时间 + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + time.sleep(0.25) + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') diff --git a/feeding/process.py b/feeding/process.py index 63815bd..e33dd97 100644 --- a/feeding/process.py +++ b/feeding/process.py @@ -2,124 +2,64 @@ from enum import IntEnum from core.state import FeedStatus from service.mould_service import MouldService from busisness.blls import ArtifactBll -from busisness.models import ArtifactInfoModel +from busisness.models import ArtifactInfoModel,ArtifactInfo +import time +from datetime import datetime +from hardware.RFID.rfid_service import rfid_service +from config.settings import app_set_config class FeedingProcess: def __init__(self, relay_controller, inverter_controller, transmitter_controller, vision_detector, - camera_controller, state, settings): + camera_controller, state): self.relay_controller = relay_controller self.artifact_bll = ArtifactBll() + self.mould_service = MouldService() self.inverter_controller = inverter_controller self.transmitter_controller = transmitter_controller self.vision_detector = vision_detector self.camera_controller = camera_controller self.state = state - self.state.feed_status = FeedStatus.FNone + self.state._feed_status = FeedStatus.FNone - self.settings = settings + #标志位用,是否是第一次运行 + self.is_first_flag=True def start_feeding(self): loc_state=self.state + loc_state._upper_weight=self.transmitter_controller.read_data(1) + loc_state._lower_weight=self.transmitter_controller.read_data(2) + # loc_state._upper_volume=round(loc_state._upper_weight/self.state.density,1) """开始生产管片""" - if loc_state.feed_status == FeedStatus.FNone: - loc_state.feed_status = FeedStatus.FCheckM + if loc_state._feed_status == FeedStatus.FNone: return - elif loc_state.feed_status == FeedStatus.FCheckM: - loc_state._lower_feeding_stage = 4 - self.wait_for_vehicle_alignment() - loc_state.feed_status = FeedStatus.FStart - print("生产已检查模车") + elif loc_state._feed_status == FeedStatus.FCheckM: + print("---------------初始化数据------------------") + loc_state._mould_need_weight= 1200 + loc_state._feed_status = FeedStatus.FFeed return - elif loc_state.feed_status == FeedStatus.FApiCheck: - print("生产已开始") - time.sleep(2) - loc_modules = MouldService.get_not_pour_artifacts() - if loc_modules: - # 取第一个未浇筑的管片 - loc_module = loc_modules[0] - self.state.current_artifact = loc_module - else: - #未读取到AIP接口数据. - self.state.current_artifact = None - return - elif loc_state.feed_status == FeedStatus.FRFID: - print("生产已检查RFID") - #RFID格式:模具编号,分块号,尺寸规格,方量 - rfid_info ='' - loc_MouldCode='SHZB1-4' - loc_SizeSpecification='6600*1200' - loc_BlockNumber='B1' - loc.BetonVolume=1.56 - if self.state.current_artifact: - #检测是否和RFID识别的管理一致 - self.state.feed_status = FeedStatus.FCheckGB - else: - #以RFID为准 - loc_module= ArtifactInfoModel() - loc_module.MouldCode=loc_MouldCode - loc_module.SizeSpecification=loc_SizeSpecification - loc_module.BlockNumber=loc_BlockNumber - loc_module.BetonVolume=loc.BetonVolume - self.state.current_artifact = loc_module - - #确认是否保存到数据库 - self.state.feed_status = FeedStatus.FCheckGB - return - elif loc_state.feed_status == FeedStatus.FCheckGB: - print("检查盖板对齐,") - time.sleep(10) - loc_state.feed_status = FeedStatus.FUpperToLower - #计算本次生产需要的总重量 - print(f"本次生产需要的总重量:{self.state.need_total_weight}") - return - elif loc_state.feed_status == FeedStatus.FUpperToLower: - print("上料斗向下料斗转移") + elif loc_state._feed_status == FeedStatus.FFeed: + print("----------------下料------------------") + loc_state.mould_vibrate_time=time.time() + loc_state._mould_frequency=app_set_config.frequencies[0] + loc_state._mould_vibrate_status=1 #上料斗重量 - loc_state.initial_upper_weight=self.transmitter_controller.read_data(1) + loc_state.initial_upper_weight=loc_state._upper_weight #下料斗重量 - loc_state.initial_lower_weight=self.transmitter_controller.read_data(2) - #需要的总重量 - loc_state.need_total_weight=loc_state.current_artifact.BetonVolume*loc_state.density - if loc_state.need_total_weight > loc_state.initial_upper_weight + loc_state.initial_lower_weight: - # 等待上料斗重量增加(多久不够报警,可能出现F块不足的情况) - print('重量不够,需要增加') - return - - if loc_state.need_total_weight>loc_state.initial_lower_weight: - if self.state._upper_door_position != 'over_lower': - #是否需要等待上料斗下料,如果下料斗够重量,则不需要等待 - return - else: - # 需要等待上料斗下料 - # 最后一块进行尾数控制 - # 最后一块F块,前面多要0.25,0.3,F块直接下料(先多下0.3后续) - loc_FWeight=0.3*loc_state.density - loc_feed_weight=loc_state.need_total_weight-loc_state.initial_lower_weight-loc_FWeight - self.transfer_material_from_upper_to_lower(loc_state.initial_upper_weight,loc_state.initial_lower_weight,loc_feed_weight) - - self.state.feed_status = FeedStatus.FFeed1 - # time.sleep(10) - return - elif self.state.feed_status == FeedStatus.FFeed1: - #下料 - # self._start_feeding_stage() - print("下料1") - return - elif self.state.feed_status == FeedStatus.FFeed2: - #上料 - # self._start_feeding_stage() - print("下料2") - return - elif self.state.feed_status == FeedStatus.FFeed3: - #下料 - # self._start_feeding_stage() - print("下料3") - return - elif self.state.feed_status == FeedStatus.FFinished: - print("生产已完成") - self.state.feed_status = FeedStatus.FCheckM + loc_state.initial_lower_weight=loc_state._lower_weight + self.feeding_stage(loc_state) + # if loc_state._mould_need_weight>loc_state.initial_lower_weight: + # self.transfer_material_from_upper_to_lower(loc_state,loc_state.initial_upper_weight,loc_state.initial_lower_weight) + # loc_state._feed_status = FeedStatus.FFeed1 + # else: + # loc_state._feed_status = FeedStatus.FFeed1 return + elif loc_state._feed_status == FeedStatus.FFinished: + """完成当前批次下料""" + print("振捣完成") + print("关闭所有网络继电器") + self.relay_controller.close_all() + return def _start_feeding_stage(self): """启动指定下料阶段""" @@ -127,34 +67,41 @@ class FeedingProcess: print("开始分步下料过程") self.transfer_material_from_upper_to_lower() - def transfer_material_from_upper_to_lower(self,initial_upper_weight,initial_lower_weight,feed_weight): + def transfer_material_from_upper_to_lower(self,loc_state,initial_upper_weight,initial_lower_weight): """target_upper_weight:转移后剩下的上料斗重量""" # 如果低于单次,全部卸掉 - _max_lower_weight=self.settings.max_lower_volume*self.state.density - if (initial_lower_weight+feed_weight>_max_lower_weight): - feed_weight=_max_lower_weight-initial_lower_weight + _max_lower_weight=app_set_config.max_lower_volume*loc_state.density + # if (initial_lower_weight+feed_weight>_max_lower_weight): + feed_weight=_max_lower_weight-initial_lower_weight target_upper_weight=initial_upper_weight-feed_weight target_upper_weight = max(target_upper_weight, 0) - # 确保下料斗出砼门关闭 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'close') + # 确保下料斗出砼门关闭,同步关5秒 + self.relay_controller.control_lower_close() # 打开上料斗出砼门 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_1, 'open') - + # self.relay_controller.control_upper_open() + #一直打开3秒 + self.relay_controller.control(self.relay_controller.DOOR_UPPER_OPEN, 'open') + time.sleep(3) + loc_state._upper_door_closed=False # 等待物料流入下料斗,基于上料斗重量变化控制 - import time + start_time = time.time() # timeout = 30 # 30秒超时 - while self.state.running: + while loc_state.running: + # self.relay_controller.control_upper_open_sync() + self.relay_controller.control(self.relay_controller.DOOR_UPPER_OPEN, 'close') current_upper_weight = self.transmitter_controller.read_data(1) # 如果无法读取重量,继续尝试 if current_upper_weight is None: print("无法读取上料斗重量,继续尝试...") time.sleep(1) continue - + + loc_state._upper_weight=current_upper_weight + loc_state._upper_volume=round(loc_state._upper_weight/self.state.density,1) print(f"上料斗当前重量: {current_upper_weight:.2f}kg") # 如果达到目标重量,则关闭上料斗出砼门 @@ -162,184 +109,59 @@ class FeedingProcess: print(f"达到目标重量,当前重量: {current_upper_weight:.2f}kg") print(f"花费时间 {time.time() - start_time:.2f}秒") break - elif time.time() - start_time > 25: # 如果25秒后重量变化过小 - weight_change = initial_upper_weight - current_upper_weight - if weight_change < 100: # 如果重量变化小于100kg - #需要增加报警处理 - print("重量变化过小,可能存在堵塞") - time.sleep(1) - # 关闭上料斗出砼门 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_1, 'close') + # time.sleep(1) + # self.relay_controller.control(self.relay_controller.DOOR_UPPER_OPEN, 'open') + # time.sleep(0.2) + + loc_state._upper_door_closed=True + # 关闭上料斗出砼门d + self.relay_controller.control_upper_close() + + #测试用 print("上料斗下料完成") - def wait_for_vehicle_alignment(self): - """等待模具车对齐""" - print("等待模具车对齐...") - self.state._lower_feeding_stage = 4 - - import time - while self.state._lower_feeding_stage == 4 and self.state.running: - if self.state.vehicle_aligned: - print("模具车已对齐,开始下料") - self.state._lower_feeding_stage = 1 - # self.feeding_stage_one() - break - time.sleep(self.settings.alignment_check_interval) - - def feeding_stage_one(self): + def feeding_stage(self,loc_state): """第一阶段下料:下料斗向模具车下料(低速)""" - print("开始第一阶段下料:下料斗低速下料") - self.inverter_controller.set_frequency(self.settings.frequencies[0]) - self.inverter_controller.control('start') - - # 确保上料斗出砼门关闭 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_1, 'close') - # 打开下料斗出砼门 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'open') - - import time - start_time = time.time() - initial_weight = self.transmitter_controller.read_data(2) - if initial_weight is None: - print("无法获取初始重量,取消下料") - self.finish_feeding_process() - return - - target_weight = initial_weight + self.settings.single_batch_weight - - while self.state._lower_feeding_stage == 1: - current_weight = self.transmitter_controller.read_data(2) - if current_weight is None: - self.state.lower_weight_error_count += 1 - if self.state.lower_weight_error_count >= self.settings.max_error_count: - print("下料斗传感器连续读取失败,停止下料") - self.finish_feeding_process() - return - else: - self.state.lower_weight_error_count = 0 - - if (current_weight is not None and current_weight >= target_weight) or (time.time() - start_time) > 30: - self.state._lower_feeding_stage = 2 - self.feeding_stage_two() + print("开始下料") + # self.relay_controller.control + first_finish_weight=0 + while True: + current_weight = loc_state._lower_weight + loc_state._mould_finish_weight=loc_state.initial_lower_weight-current_weight + first_finish_weight=loc_state._mould_finish_weight + if current_weight<100: + #关5秒 + self.relay_controller.control_lower_close() break - time.sleep(2) - def feeding_stage_two(self): - """第二阶段下料:下料斗向模具车下料(中速)""" - print("开始第二阶段下料:下料斗中速下料") - self.inverter_controller.set_frequency(self.settings.frequencies[1]) + time.sleep(1) - # 保持下料斗出砼门打开 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'open') - # 确保上料斗出砼门关闭 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_1, 'close') - - import time - start_time = time.time() - initial_weight = self.transmitter_controller.read_data(2) - if initial_weight is None: - print("无法获取初始重量,取消下料") - self.finish_feeding_process() - return - - target_weight = initial_weight + self.settings.single_batch_weight - - while self.state._lower_feeding_stage == 2: - current_weight = self.transmitter_controller.read_data(2) - if current_weight is None: - self.state.lower_weight_error_count += 1 - if self.state.lower_weight_error_count >= self.settings.max_error_count: - print("下料斗传感器连续读取失败,停止下料") - self.finish_feeding_process() - return - else: - self.state.lower_weight_error_count = 0 - - if (current_weight is not None and current_weight >= target_weight) or (time.time() - start_time) > 30: - self.state._lower_feeding_stage = 3 - self.feeding_stage_three() + #打开上料斗出砼门 + self.relay_controller.control_upper_open_sync(5) + while True: + + if loc_state._upper_weight<3000: + #关5秒 + self.relay_controller.control_upper_close() break - time.sleep(2) - - def feeding_stage_three(self): - """第三阶段下料:下料斗向模具车下料(高速)""" - print("开始第三阶段下料:下料斗高速下料") - self.inverter_controller.set_frequency(self.settings.frequencies[2]) - - # 保持下料斗出砼门打开 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'open') - # 确保上料斗出砼门关闭 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_1, 'close') - - import time - start_time = time.time() - initial_weight = self.transmitter_controller.read_data(2) - if initial_weight is None: - print("无法获取初始重量,取消下料") - self.finish_feeding_process() - return - - target_weight = initial_weight + self.settings.single_batch_weight - - while self.state._lower_feeding_stage == 3: - current_weight = self.transmitter_controller.read_data(2) - if current_weight is None: - self.state.lower_weight_error_count += 1 - if self.state.lower_weight_error_count >= self.settings.max_error_count: - print("下料斗传感器连续读取失败,停止下料") - self.finish_feeding_process() - return - else: - self.state.lower_weight_error_count = 0 - - if (current_weight is not None and current_weight >= target_weight) or (time.time() - start_time) > 30: - self.state._lower_feeding_stage = 4 - self.finish_current_batch() + loc_state.initial_lower_weight=loc_state.lower_weight + while True: + current_weight = loc_state._lower_weight + loc_state._mould_finish_weight=first_finish_weight+loc_state.initial_lower_weight-current_weight + if current_weight<100: + #关5秒 + self.relay_controller.control_lower_close() break - time.sleep(2) - def finish_current_batch(self): - """完成当前批次下料""" - print("当前批次下料完成,关闭出砼门") - self.inverter_controller.control('stop') - self.relay_controller.control(self.relay_controller.DOOR_LOWER_1, 'close') - self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'close') + time.sleep(1) + - # 增加三阶段下料轮次计数 - self.state.lower_feeding_cycle += 1 - - # 检查是否完成两轮三阶段下料 - if self.state.lower_feeding_cycle >= self.state.upper_feeding_max: - print("完成两轮三阶段下料,5吨下料任务完成") - self.finish_feeding_process() - return - - # 如果只完成一轮三阶段下料,进行第二次上料 - print("第一轮三阶段下料完成,准备第二次上料") - # 上料斗第二次向下料斗下料 - try: - self.transfer_material_from_upper_to_lower() - except Exception as e: - print(f"第二次上料失败: {e}") - print("停止下料流程") - self.finish_feeding_process() # 出现严重错误时结束整个流程 - return - - # 继续等待当前模具车对齐(不需要重新等待对齐,因为是同一辆模具车) - print("第二次上料完成,继续三阶段下料") - self.state._lower_feeding_stage = 1 # 直接进入第一阶段下料 - self.feeding_stage_one() # 开始第二轮第一阶段下料 - - def finish_feeding_process(self): - """完成整个下料流程""" - print("整个下料流程完成") - self.state._lower_feeding_stage = 0 - self.state.lower_feeding_cycle = 0 - self.state.upper_feeding_count = 0 - # self.return_upper_door_to_default() + + + def return_upper_door_to_default(self): """上料斗回到默认位置(搅拌楼下接料位置)""" print("上料斗回到默认位置") - self.relay_controller.control(self.relay_controller.DOOR_UPPER, 'close') + # self.relay_controller.control(self.relay_controller.UPPER_TO_JBL, 'open') self.state._upper_door_position = 'default' diff --git a/feeding/process_bak.py b/feeding/process_bak.py new file mode 100644 index 0000000..8690307 --- /dev/null +++ b/feeding/process_bak.py @@ -0,0 +1,450 @@ +from enum import IntEnum +from core.state import FeedStatus +from service.mould_service import MouldService +from busisness.blls import ArtifactBll +from busisness.models import ArtifactInfoModel,ArtifactInfo +import time +from datetime import datetime +from hardware.RFID.rfid_service import rfid_service +from config.settings import app_set_config + +class FeedingProcess22: + def __init__(self, relay_controller, inverter_controller, + transmitter_controller, vision_detector, + camera_controller, state): + self.relay_controller = relay_controller + self.artifact_bll = ArtifactBll() + self.mould_service = MouldService() + self.inverter_controller = inverter_controller + self.transmitter_controller = transmitter_controller + self.vision_detector = vision_detector + self.camera_controller = camera_controller + self.state = state + self.state._feed_status = FeedStatus.FCheckM + + #标志位用,是否是第一次运行 + self.is_first_flag=True + + #RFID服务 + self.rfid_service=rfid_service() + # self.rfid_service.callback_signal.connect(self._rfid_callback) + self.rfid_flag_succ=False + self.rfid_data='' + # self.rfid_service.start_receiver(self._rfid_callback) + + + def _rfid_callback(self,status,data): + try: + if status==1: + #成功读取到RFID标签 + #检查标识是否符号要求 + if data: + loc_array=data.split(',') + if len(loc_array)==4: + if self.state.current_artifact is None or loc_array[0]!=self.state.current_artifact.MouldCode: + self.rfid_flag_succ=True + self.state.current_artifact={ + 'MouldCode':loc_array[0], + 'BlockNumber':loc_array[1], + 'SizeSpecification':loc_array[2], + 'BetonVolume':float(loc_array[3]) + } + print(f"RFID-生产模具车号:{loc_array[0]}") + else: + print("RFID-生产模具车号与当前模具车号一致") + else: + print("RFID标签格式错误") + print(f"成功读取到RFID标签:{data}") + else: + self.rfid_flag_succ=False + print("读取RFID标签失败") + except Exception as e: + print(f"RFID回调处理异常: {e}") + + #读取失败 + + + + def start_feeding(self): + loc_state=self.state + loc_state._upper_weight=self.transmitter_controller.read_data(1) + loc_state._lower_weight=self.transmitter_controller.read_data(2) + loc_state._upper_volume=round(loc_state._upper_weight/self.state.density,1) + """开始生产管片""" + if loc_state._feed_status == FeedStatus.FNone: + # loc_state._feed_status = FeedStatus.FCheckM + return + elif loc_state._feed_status == FeedStatus.FCheckM: + """开始生产管片""" + print("检查盖板对齐,") + loc_state.lower_feeding_stage = 4 + + self.rfid_flag_succ=False + # if app_set_config.debug_feeding: + # loc_state._feed_status = FeedStatus.FApiCheck + if self.state.vehicle_aligned: + loc_state._feed_status = FeedStatus.FApiCheck + print("检查模车") + return + elif loc_state._feed_status == FeedStatus.FApiCheck: + print("————————————————生产已开始————————————————————") + # time.sleep(2) + #模拟数据 + module_obj=ArtifactInfoModel() + module_obj.ArtifactID="GR2B13082624F" + module_obj.MouldCode="SHR2B2-4" + module_obj.BetonVolume=1.9 + loc_modules=[module_obj] + # loc_modules =self.mould_service.get_not_pour_artifacts() + if loc_modules and loc_modules[0].ArtifactID : + # 取第一个未浇筑的管片 + #后续放入队列处理 + + loc_module = loc_modules[0] + #API + loc_module.Source = 1 + loc_module.BeginTime=datetime.now() + + # self.artifact_bll.insert_artifact_task(loc_module) + loc_state.current_artifact = loc_module + loc_state._mould_need_weight=loc_module.BetonVolume*self.state.density + print(f"已获取到未浇筑的管片:{loc_module.MouldCode}") + # self.artifact_bll.finish_artifact_task(loc_state.current_artifact.ArtifactID,1.92) + loc_state._feed_status = FeedStatus.FCheckGB + else: + #未读取到AIP接口数据. + print("未获取到未浇筑的管片") + loc_artifacting_task=self.artifact_bll.get_artifacting_task() + if loc_artifacting_task: + loc_state.current_artifact = loc_artifacting_task + loc_state._mould_need_weight=loc_artifacting_task.BetonVolume*self.state.density + loc_state._feed_status = FeedStatus.FCheckGB + else: + loc_state.current_artifact = None + return + elif loc_state._feed_status == FeedStatus.FRFID: + print("检查RFID") + + #RFID格式:模具编号,分块号,尺寸规格,方量 + while loc_state.running: + if self.rfid_flag_succ: + loc_state._feed_status = FeedStatus.FCheckGB + break + else: + time.sleep(1) + return + elif loc_state._feed_status == FeedStatus.FCheckGB: + # print("检查盖板对齐,") + # time.sleep(5) + loc_state._feed_status = FeedStatus.FUpperToLower + #计算本次生产需要的总重量 + print(f"本次生产需要的总重量:{self.state._mould_need_weight}") + return + elif loc_state._feed_status == FeedStatus.FUpperToLower: + print("上料斗向下料斗转移") + #上料斗重量 + loc_state.initial_upper_weight=loc_state._upper_weight + #下料斗重量 + loc_state.initial_lower_weight=loc_state._lower_weight + #需要的总重量 + # loc_state._mould_need_weight=loc_state.current_artifact.BetonVolume*loc_state.density + if loc_state._mould_need_weight > loc_state.initial_upper_weight + loc_state.initial_lower_weight: + # 等待上料斗重量增加(多久不够报警,可能出现F块不足的情况) + print('重量不够,需要增加') + return + + if loc_state._mould_need_weight>loc_state.initial_lower_weight: + if self.state._upper_door_position != 'over_lower': + #是否需要等待上料斗下料,如果下料斗够重量,则不需要等待 + return + else: + # 需要等待上料斗下料 + # 最后一块进行尾数控制 + # 最后一块F块,前面多要0.25,0.3,F块直接下料(先多下0.3后续) + # loc_FWeight=0.3*loc_state.density + # loc_feed_weight=loc_state.need_total_weight-loc_state.initial_lower_weight-loc_FWeight + self.transfer_material_from_upper_to_lower(loc_state,loc_state.initial_upper_weight,loc_state.initial_lower_weight) + #完成了上料斗重量转移才进入下料斗 + #测试返回 + loc_state._feed_status = FeedStatus.FFeed1 + # loc_state._feed_status = FeedStatus.FNone + else: + loc_state._feed_status = FeedStatus.FFeed1 + + # time.sleep(10) + return + elif loc_state._feed_status == FeedStatus.FFeed1: + #下料 + # self._start_feeding_stage() + self.feeding_stage_one(loc_state) + print("下料1") + return + elif loc_state._feed_status == FeedStatus.FFeed2: + #上料 + # self._start_feeding_stage() + self.feeding_stage_two(loc_state) + print("下料2") + return + elif loc_state._feed_status == FeedStatus.FFeed3: + #下料 + # self._start_feeding_stage() + self.feeding_stage_three(loc_state) + print("下料3") + return + elif loc_state._feed_status == FeedStatus.FFinished: + """完成当前批次下料""" + print("当前批次下料完成,关闭出砼门") + + if loc_state.overflow_detected=="浇筑满": + + # self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + # self.relay_controller.control_upper_close() + #更新数据库状态 + # self.artifact_bll.finish_artifact_task(loc_state.current_artifact.ArtifactID,loc_state._mould_finish_weight/loc_state.density) + print("浇筑完成") + + # loc_state._feed_status = FeedStatus.FCheckM + # if loc_state.mould_vibrate_time>0: + # while True: + # if loc_state.mould_vibrate_time-time.time()>=5*60: + # self.inverter_controller.control('stop') + # loc_state._mould_vibrate_status=0 + # break + # time.sleep(1) + print("振捣完成") + return + + def _start_feeding_stage(self): + """启动指定下料阶段""" + """开始分步下料""" + print("开始分步下料过程") + self.transfer_material_from_upper_to_lower() + + def transfer_material_from_upper_to_lower(self,loc_state,initial_upper_weight,initial_lower_weight): + + """target_upper_weight:转移后剩下的上料斗重量""" + # 如果低于单次,全部卸掉 + _max_lower_weight=app_set_config.max_lower_volume*loc_state.density + # if (initial_lower_weight+feed_weight>_max_lower_weight): + feed_weight=_max_lower_weight-initial_lower_weight + + target_upper_weight=initial_upper_weight-feed_weight + target_upper_weight = max(target_upper_weight, 0) + # 确保下料斗出砼门关闭 + self.relay_controller.control_lower_close() + # 打开上料斗出砼门 + # self.relay_controller.control_upper_open() + self.relay_controller.control(self.relay_controller.DOOR_UPPER_OPEN, 'open') + time.sleep(3) + loc_state._upper_door_closed=False + # 等待物料流入下料斗,基于上料斗重量变化控制 + + start_time = time.time() + # timeout = 30 # 30秒超时 + + while loc_state.running: + # self.relay_controller.control_upper_open_sync() + self.relay_controller.control(self.relay_controller.DOOR_UPPER_OPEN, 'close') + current_upper_weight = self.transmitter_controller.read_data(1) + # 如果无法读取重量,继续尝试 + if current_upper_weight is None: + print("无法读取上料斗重量,继续尝试...") + time.sleep(1) + continue + loc_state._upper_weight=current_upper_weight + loc_state._upper_volume=round(loc_state._upper_weight/self.state.density,1) + print(f"上料斗当前重量: {current_upper_weight:.2f}kg") + + # 如果达到目标重量,则关闭上料斗出砼门 + if current_upper_weight <= target_upper_weight + 50: # 允许50kg的误差范围 + print(f"达到目标重量,当前重量: {current_upper_weight:.2f}kg") + print(f"花费时间 {time.time() - start_time:.2f}秒") + break + elif time.time() - start_time > 25: # 如果25秒后重量变化过小 + weight_change = initial_upper_weight - current_upper_weight + if weight_change < 100: # 如果重量变化小于100kg + #需要增加报警处理 + print("重量变化过小,可能存在堵塞") + time.sleep(1) + self.relay_controller.control(self.relay_controller.DOOR_UPPER_OPEN, 'open') + time.sleep(0.2) + + loc_state._upper_door_closed=True + # 关闭上料斗出砼门 + self.relay_controller.control_upper_close() + + #测试用 + print("上料斗下料完成") + + def wait_for_vehicle_alignment(self): + """等待模具车对齐""" + print("等待模具车对齐...") + self.state.lower_feeding_stage = 4 + + import time + while self.state.lower_feeding_stage == 4 and self.state.running: + if self.state.vehicle_aligned: + print("模具车已对齐,开始下料") + self.state.lower_feeding_stage = 1 + # self.feeding_stage_one() + break + time.sleep(app_set_config.alignment_check_interval) + + def feeding_stage_one(self,loc_state): + """第一阶段下料:下料斗向模具车下料(低速)""" + print("开始第一阶段下料:下料斗低速下料") + if self.is_first_flag: + # self.inverter_controller.set_frequency(app_set_config.frequencies[0]) + # self.inverter_controller.control('start') + loc_state.mould_vibrate_time=time() + loc_state._mould_frequency=app_set_config.frequencies[0] + loc_state._mould_vibrate_status=1 + + # 确保上料斗出砼门关闭 + # self.relay_controller.control(self.relay_controller.DOOR_UPPER_CLOSE, 'close') + # 打开下料斗出砼门 + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + loc_cur_weight = loc_state._lower_weight + if loc_cur_weight is None: + #报警处理 + print("无法获取初始重量,取消下料") + # self.finish_feeding_process() + return + loc_state.initial_lower_weight=loc_cur_weight + self.is_first_flag=False + + + start_time = time.time() + + current_weight = loc_state._lower_weight + if current_weight is None: + #报警处理 + print("无法获取当前重量,取消下料") + # self.finish_feeding_process() + return + loc_state._mould_finish_weight=loc_state.initial_lower_weight-current_weight + target_weight = loc_state._mould_need_weight/3 + # or (time.time() - start_time) > 30 + if (current_weight is not None and loc_state._mould_finish_weight >= target_weight): + loc_state._feed_status = FeedStatus.FFeed2 + loc_state.lower_feeding_stage = 2 + self.is_first_flag=True + return + else: + time.sleep(1) + + def feeding_stage_two(self,loc_state): + """第二阶段下料:下料斗向模具车下料(中速)""" + if self.is_first_flag: + print("开始第二阶段下料:下料斗中速下料") + # self.inverter_controller.set_frequency(app_set_config.frequencies[1]) + # self.inverter_controller.control('start') + # 保持下料斗出砼门打开 + # self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + # 确保上料斗出砼门关闭 + self.relay_controller.control_upper_close() + + loc_state._mould_frequency=app_set_config.frequencies[1] + loc_state._mould_vibrate_status=1 + # loc_cur_weight = self.transmitter_controller.read_data(2) + # if loc_cur_weight is None: + # #报警处理 + # print("无法获取初始重量,取消下料") + # # self.finish_feeding_process() + # return + # loc_state.initial_lower_weight=loc_cur_weight + self.is_first_flag=False + + start_time = time.time() + current_weight = loc_state._lower_weight + if current_weight is None: + #报警处理 + print("无法获取当前重量,取消下料") + # self.finish_feeding_process() + return + loc_state._mould_finish_weight=loc_state.initial_lower_weight-current_weight + target_weight = (loc_state._mould_need_weight/3)*2 + # or (time.time() - start_time) > 30 + if (current_weight is not None and loc_state._mould_finish_weight >= target_weight): + loc_state._feed_status = FeedStatus.FFeed3 + loc_state.lower_feeding_stage = 3 + self.is_first_flag=True + return + else: + time.sleep(1) + + def feeding_stage_three(self,loc_state): + """第三阶段下料:下料斗向模具车下料(高速)""" + if self.is_first_flag: + print("开始第三阶段下料:下料斗高速下料") + # self.inverter_controller.set_frequency(app_set_config.frequencies[2]) + # self.inverter_controller.control('start') + loc_state._mould_frequency=app_set_config.frequencies[2] + # 保持下料斗出砼门打开 + # self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + # 确保上料斗出砼门关闭 + self.relay_controller.control_upper_close() + self.is_first_flag=False + + current_weight = loc_state._lower_weight + if current_weight is None: + #报警处理 + print("无法获取当前重量,取消下料") + # self.finish_feeding_process() + return + loc_state._mould_finish_weight=loc_state.initial_lower_weight-current_weight + target_weight = loc_state._mould_need_weight + + if (current_weight is not None and loc_state._mould_finish_weight >= target_weight): + loc_state._feed_status = FeedStatus.FFinished + loc_state.lower_feeding_stage = 5 + self.is_first_flag=True + return + else: + time.sleep(1) + + def finish_current_batch(self): + """完成当前批次下料""" + print("当前批次下料完成,关闭出砼门") + # self.inverter_controller.control('stop') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_1, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_2, 'close') + + # 增加三阶段下料轮次计数 + self.state.lower_feeding_cycle += 1 + + # 检查是否完成两轮三阶段下料 + if self.state.lower_feeding_cycle >= self.state.upper_feeding_max: + print("完成两轮三阶段下料,5吨下料任务完成") + self.finish_feeding_process() + return + + # 如果只完成一轮三阶段下料,进行第二次上料 + print("第一轮三阶段下料完成,准备第二次上料") + # 上料斗第二次向下料斗下料 + try: + self.transfer_material_from_upper_to_lower() + except Exception as e: + print(f"第二次上料失败: {e}") + print("停止下料流程") + self.finish_feeding_process() # 出现严重错误时结束整个流程 + return + + # 继续等待当前模具车对齐(不需要重新等待对齐,因为是同一辆模具车) + print("第二次上料完成,继续三阶段下料") + self.state.lower_feeding_stage = 1 # 直接进入第一阶段下料 + self.feeding_stage_one() # 开始第二轮第一阶段下料 + + def finish_feeding_process(self): + """完成整个下料流程""" + print("整个下料流程完成") + self.state.lower_feeding_stage = 0 + self.state.lower_feeding_cycle = 0 + self.state.upper_feeding_count = 0 + # self.return_upper_door_to_default() + + def return_upper_door_to_default(self): + """上料斗回到默认位置(搅拌楼下接料位置)""" + print("上料斗回到默认位置") + self.relay_controller.control(self.relay_controller.UPPER_TO_JBL, 'open') + self.state._upper_door_position = 'default' diff --git a/hardware/RFID/command_hex.py b/hardware/RFID/command_hex.py index 3448350..ff38a5b 100644 --- a/hardware/RFID/command_hex.py +++ b/hardware/RFID/command_hex.py @@ -470,9 +470,10 @@ class command_hex: """ # 寻找连续三个0x2C的情况 - for idx in range(len(response) - 2): - if response[idx] == 0x2C and response[idx+1] == 0x2C and response[idx+2] == 0x2C: - response=response[:idx] + # for idx in range(len(response) - 2): + # if response[idx] == 0x2C and response[idx+1] == 0x2C and response[idx+2] == 0x2C: + # response=response[:idx] + # break # 验证响应长度 if len(response) <= 1 or len(response) != response[0] + 1: raise ValueError("应答数据长度不正确") @@ -499,7 +500,12 @@ class command_hex: loc_string = '' if data_part: try: + loc_string = data_part.decode('ascii') + first_empty = loc_string.find(',,,') + if first_empty != -1: + loc_string = loc_string[:first_empty] + print('收到数据:',loc_string) except UnicodeDecodeError: print(f"无法将数据转换为ASCII字符串: {data_part}") return loc_string diff --git a/hardware/RFID/rfid_service.py b/hardware/RFID/rfid_service.py index 6eaa2b3..2f16baf 100644 --- a/hardware/RFID/rfid_service.py +++ b/hardware/RFID/rfid_service.py @@ -1,9 +1,12 @@ import socket import threading import time +from datetime import datetime import binascii from typing import Optional, Callable, Dict, Any, Set from collections import Counter + +from PySide6.QtCore import QObject from .crc16 import crc16 from .command_hex import command_hex @@ -12,7 +15,8 @@ class rfid_service: """ RFID读写器服务 """ - def __init__(self, host='192.168.1.190', port=6000): + # callback_signal=Signal(int,str) + def __init__(self, host='192.168.250.67', port=6000): """ 初始化RFID控制器 @@ -34,9 +38,9 @@ class rfid_service: # 需要过滤掉的数据(字符串) self._filter_value = None - self.check_time_seconds = 60.0 # 采集数据时间(秒) + self.check_time_seconds = 5.0 # 采集数据时间(秒) # 超时设置 - self._connect_timeout = 5.0 # 连接超时时间(秒) + self._connect_timeout = 1.0 # 连接超时时间(秒) self.client_socket = None self.connected = False #链接失败次数 @@ -349,7 +353,7 @@ class rfid_service: #endregion - def start_receiver(self, callback: Optional[Callable[[str], None]] = None)->bool: + def start_receiver(self, callback: Optional[Callable[[int,str], None]] = None)->bool: """ 开始接收RFID推送的数据 @@ -380,7 +384,7 @@ class rfid_service: 接收线程的主循环,用于接收RFID推送的数据 """ while self._thread_signal: - + # self._pause_receive=False if self._pause_receive: time.sleep(1) continue @@ -405,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: @@ -439,14 +443,21 @@ class rfid_service: if data: loc_str = command_hex.parse_user_data_hex(data) - print(f"收到RFID推送数据: {binascii.hexlify(data).decode()}") + # 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") + if loc_str: # 将数据添加到缓冲区 with self._data_lock: if self._filter_value == loc_str: continue self._data_buffer.append(loc_str) - + time.sleep(1) self._pause_receive = True self._process_collected_data() @@ -471,12 +482,15 @@ class rfid_service: most_common_string, count = counter.most_common(1)[0] print(f"出现次数最多的字符串是: '{most_common_string}', 出现次数: {count}") - # 使用出现次数最多的字符串作为结果传递给回调 - self._callback(most_common_string) + # 使用出现次数最多的字符串作为结果传递给回调函数 + # self.callback_signal.emit(1,most_common_string) + self._callback(1,most_common_string) else: # 空缓冲区情况 print("数据缓冲区为空") - self._callback(None) + # 使用None作为结果传递给回调函数 + # self.callback_signal.emit(0,'') + self._callback(0,'') print(f"收集了{len(self._data_buffer)}条RFID数据,过滤后数据为{most_common_string}") except Exception as e: @@ -500,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 049942e..4ce7b71 100644 --- a/hardware/relay.py +++ b/hardware/relay.py @@ -1,30 +1,50 @@ # hardware/relay.py import socket import binascii +import time +import threading from pymodbus.client import ModbusTcpClient from pymodbus.exceptions import ModbusException +from config.settings import app_set_config class RelayController: # 继电器映射 - DOOR_UPPER = 'door_upper' # DO0 - 上料斗滑动 - DOOR_LOWER_1 = 'door_lower_1' # DO1 - 上料斗出砼门 - DOOR_LOWER_2 = 'door_lower_2' # DO2 - 下料斗出砼门 - BREAK_ARCH_UPPER = 'break_arch_upper' # DO3 - 上料斗破拱 - BREAK_ARCH_LOWER = 'break_arch_lower' # DO4 - 下料斗破拱 + RING = 'ring' # DO1 - 响铃 + UPPER_TO_JBL = 'upper_to_jbl' # DO2 - 上料斗到搅拌楼 + UPPER_TO_ZD = 'upper_to_zd' # DO3 - 上料斗到振捣室 + # DOOR_UPPER = 'door_upper' # DO0 - 上料斗滑动 + DOOR_LOWER_OPEN = 'door_lower_open' # DO1 - 下料斗出砼门开角度 + DOOR_LOWER_CLOSE = 'door_lower_close' # DO2 - 下料斗出砼门关角度(角度在7.5以下可关闭信号) + DOOR_UPPER_OPEN = 'door_upper_open' # DO3 - 上料斗开 + DOOR_UPPER_CLOSE = 'door_upper_close' # DO4 - 上料斗关 + BREAK_ARCH_UPPER = 'break_arch_upper' # DO3 - 上料斗震动 + BREAK_ARCH_LOWER = 'break_arch_lower' # DO4 - 下料斗震动 + DIRECT_LOWER_FRONT = 'direct_lower_front' # DO5 - 下料斗前 + DIRECT_LOWER_BEHIND = 'direct_lower_behind' # DO6 - 下料斗后 + DIRECT_LOWER_TOP = 'direct_lower_top' # DO7 - 下料斗上 + DIRECT_LOWER_BELOW = 'direct_lower_below' # DO8 - 下料斗下 - def __init__(self, host='192.168.0.18', port=50000): + def __init__(self, host='192.168.250.62', port=50000): self.host = host self.port = port self.modbus_client = ModbusTcpClient(host, port=port) - +#遥1 DO 7 左 DO8 右 角度 摇2:DO 15下 13上 12 往后 14往前 下料斗DO7开 D09关 # 继电器命令(原始Socket) self.relay_commands = { - self.DOOR_UPPER: {'open': '00000000000601050000FF00', 'close': '000000000006010500000000'}, - self.DOOR_LOWER_1: {'open': '00000000000601050001FF00', 'close': '000000000006010500010000'}, - self.DOOR_LOWER_2: {'open': '00000000000601050002FF00', 'close': '000000000006010500020000'}, - self.BREAK_ARCH_UPPER: {'open': '00000000000601050003FF00', 'close': '000000000006010500030000'}, - self.BREAK_ARCH_LOWER: {'open': '00000000000601050004FF00', 'close': '000000000006010500040000'} + self.RING: {'open': '00000000000601050000FF00', 'close': '000000000006010500000000'}, + self.UPPER_TO_JBL: {'open': '00000000000601050001FF00', 'close': '000000000006010500010000'}, + self.UPPER_TO_ZD: {'open': '00000000000601050002FF00', 'close': '000000000006010500020000'}, + self.DOOR_LOWER_OPEN: {'open': '00000000000601050006FF00', 'close': '000000000006010500060000'}, + self.DOOR_LOWER_CLOSE: {'open': '00000000000601050008FF00', 'close':'000000000006010500080000'}, + self.DOOR_UPPER_OPEN: {'open': '00000000000601050003FF00', 'close': '000000000006010500030000'}, + self.DOOR_UPPER_CLOSE: {'open': '00000000000601050004FF00', 'close': '000000000006010500040000'}, + self.BREAK_ARCH_UPPER: {'open': '0000000000060105000AFF00', 'close': '0000000000060105000A0000'}, + self.BREAK_ARCH_LOWER: {'open': '00000000000601050005FF00', 'close': '000000000006010500050000'}, + self.DIRECT_LOWER_FRONT: {'open': '0000000000060105000DFF00', 'close': '0000000000060105000D0000'}, + self.DIRECT_LOWER_BEHIND: {'open': '0000000000060105000BFF00', 'close': '0000000000060105000B0000'}, + self.DIRECT_LOWER_TOP: {'open': '0000000000060105000CFF00', 'close': '0000000000060105000C0000'}, + self.DIRECT_LOWER_BELOW: {'open': '0000000000060105000EFF00', 'close': '0000000000060105000E0000'} } # 读取状态命令 @@ -32,22 +52,28 @@ class RelayController: # 设备位映射 self.device_bit_map = { - self.DOOR_UPPER: 0, - self.DOOR_LOWER_1: 1, - self.DOOR_LOWER_2: 2, + self.RING: 0, + self.UPPER_TO_JBL: 1, + self.UPPER_TO_ZD: 2, self.BREAK_ARCH_UPPER: 3, self.BREAK_ARCH_LOWER: 4 } + + # 添加线程锁,保护对下料斗控制的并发访问 + self.door_control_lock = threading.Lock() def send_command(self, command_hex): """发送原始Socket命令""" + # if app_set_config.debug_mode: + # return None + try: byte_data = binascii.unhexlify(command_hex) with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: 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}") @@ -71,7 +97,218 @@ 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}") + + def control_upper_close(self): + """控制上料斗关""" + # 关闭上料斗出砼门 + self.control(self.DOOR_UPPER_OPEN, 'close') + 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') + self.control(self.DOOR_UPPER_CLOSE, 'open') + time.sleep(duration) + self.control(self.DOOR_UPPER_CLOSE, 'close') + + + def control_lower_close(self): + """控制下料斗关""" + thread_name = threading.current_thread().name + print(f"[{thread_name}] 尝试控制下料斗关闭") + + with self.door_control_lock: + print(f"[{thread_name}] 获得下料斗控制锁,执行关闭操作") + # 关闭下料斗出砼门 + self.control(self.DOOR_LOWER_OPEN, 'close') + self.control(self.DOOR_LOWER_CLOSE, 'open') + time.sleep(3) + self.control(self.DOOR_LOWER_CLOSE, 'close') + print(f"[{thread_name}] 下料斗关闭完成,释放控制锁") + # 异步5秒后关闭 + # threading.Thread(target=self._close_lower_5s, daemon=True,name="close_lower_5s").start() + + def control_upper_open_sync(self,duration): + self.control(self.DOOR_UPPER_CLOSE, 'close') + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(duration) + self.control(self.DOOR_UPPER_OPEN, 'close') + + def control_upper_close_sync(self,duration): + thread_name = threading.current_thread().name + print(f"[{thread_name}] 尝试执行上料斗同步关闭,实际操作下料斗") + + with self.door_control_lock: + print(f"[{thread_name}] 获得下料斗控制锁,执行同步关闭操作") + self.control(self.DOOR_UPPER_OPEN, 'close') + self.control(self.DOOR_UPPER_CLOSE, 'open') + time.sleep(duration) + self.control(self.DOOR_UPPER_CLOSE, 'close') + print(f"[{thread_name}] 同步关闭操作完成,释放控制锁") + + def control_upper_open(self): + #关闭信号才能生效 + self.control(self.DOOR_UPPER_CLOSE, 'close') + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.2) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.2) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(0.1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(0.1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(0.1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(0.1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + #保持8秒 + time.sleep(8) + #8秒后再开5秒 + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(0.1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(0.1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(0.1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(0.1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(0.1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(0.1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + + def control_ring_open(self): + """控制下料斗关""" + # 关闭下料斗出砼门 + self.control(self.RING, 'open') + # 异步5秒后关闭 + threading.Thread(target=self._close_ring, daemon=True,name="_close_ring").start() + + def _close_upper_s(self): + time.sleep(16) + 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') + + def _close_ring(self): + time.sleep(3) + self.control(self.RING, 'close') + + def control_arch_lower_open(self): + """控制下料斗关""" + # 关闭下料斗出砼门 + 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_async(self,delay_seconds: float = 15): + """异步控制上料斗振动 + + Args: + delay_seconds: 延迟关闭时间(秒),默认15秒 + """ + # 关闭下料斗出砼门 + self.control(self.BREAK_ARCH_UPPER, 'open') + # 异步5秒后关闭 + 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') + + + + def close_all(self): + """关闭所有继电器""" + self.control(self.UPPER_TO_JBL, 'close') + self.control(self.UPPER_TO_ZD, 'close') + self.control(self.BREAK_ARCH_UPPER, 'close') + self.control(self.BREAK_ARCH_LOWER, 'close') + self.control(self.RING, 'close') + self.control(self.DOOR_LOWER_OPEN, 'close') + self.control(self.DOOR_LOWER_CLOSE, 'close') + self.control(self.DOOR_UPPER_OPEN, 'close') + self.control(self.DOOR_UPPER_CLOSE, 'close') diff --git a/hardware/transmitter copy.py b/hardware/transmitter copy.py new file mode 100644 index 0000000..0070070 --- /dev/null +++ b/hardware/transmitter copy.py @@ -0,0 +1,184 @@ +# hardware/transmitter.py +from pymodbus.exceptions import ModbusException +import socket +from config.ini_manager import ini_manager +from config.settings import app_set_config + + + +class TransmitterController: + def __init__(self, relay_controller): + self.relay_controller = relay_controller + # 变送器配置 + self.config = { + 1: { # 上料斗 + 'slave_id': 1, + 'weight_register': 0x01, + 'register_count': 2 + }, + 2: { # 下料斗 + 'slave_id': 2, + 'weight_register': 0x01, + 'register_count': 2 + } + } + + # 备份 (modbus 读取数据) + def read_data_bak(self, transmitter_id): + """读取变送器数据""" + try: + if transmitter_id not in self.config: + print(f"无效变送器ID: {transmitter_id}") + return None + + config = self.config[transmitter_id] + + if not self.relay_controller.modbus_client.connect(): + print("无法连接网络继电器Modbus服务") + return None + + result = self.relay_controller.modbus_client.read_holding_registers( + address=config['weight_register'], + count=config['register_count'], + slave=config['slave_id'] + ) + + if isinstance(result, Exception): + print(f"读取变送器 {transmitter_id} 失败: {result}") + return None + + # 根据图片示例,正确解析数据 + if config['register_count'] == 2: + # 获取原始字节数组 + raw_data = result.registers + # 组合成32位整数 + weight = (raw_data[0] << 16) + raw_data[1] + weight = weight / 1000.0 # 单位转换为千克 + elif config['register_count'] == 1: + weight = float(result.registers[0]) + else: + print(f"不支持的寄存器数量: {config['register_count']}") + return None + + print(f"变送器 {transmitter_id} 读取重量: {weight}kg") + return weight + + except ModbusException as e: + print(f"Modbus通信错误: {e}") + return None + except Exception as e: + print(f"数据解析错误: {e}") + return None + finally: + self.relay_controller.modbus_client.close() + + # 直接读取 变送器返回的数据并解析 + def read_data(self, transmitter_id): + + """ + Args: transmitter_id 为1 表示上料斗, 为2 表示下料斗 + return: 读取成功返回重量 weight: int, 失败返回 None + """ + TIMEOUT = 2 # 超时时间为 2秒 + BUFFER_SIZE= 1024 + IP = None + PORT = None + weight = 0 + + + if transmitter_id == 1: + # 上料斗变送器的信息: + IP = ini_manager.upper_transmitter_ip + PORT = ini_manager.upper_transmitter_port + elif transmitter_id == 2: + # 下料斗变送器的信息: + IP = ini_manager.lower_transmitter_ip + PORT = ini_manager.lower_transmitter_port + + if not IP or not PORT: + print(f"未配置变送器 {transmitter_id} 的IP或PORT") + return 0 + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + s.settimeout(TIMEOUT) + s.connect((IP, PORT)) + # print(f"连接上料斗变送器 {IP}:{PORT} 成功") + + # 接收数据(变送器主动推送,recv即可获取数据) + data = s.recv(BUFFER_SIZE) + if data: + # print(f"收到原始数据:{data}") + + # 提取出完整的一个数据包 (\r\n结尾) + packet = self.get_latest_valid_packet(data) + if not packet: + print("未获取到有效数据包!!") + return None + # 解析重量 + weight = self.parse_weight(packet) + else: + print("未收到设备数据") + + except ConnectionRefusedError: + print(f"变送器连接失败:{IP}:{PORT} 拒绝连接(设备离线/端口错误)") + except socket.timeout: + print(f"读取变送器数据超时:{TIMEOUT}秒内未收到数据") + except Exception as e: + print(f"读取异常:{e}") + + # 成功返回重量(int),失败返回None + return weight + + def get_latest_valid_packet(self, raw_data): + """ + 解决TCP粘包: + 从原始数据中,筛选所有有效包,返回最新的一个有效包 + 有效包标准: 1. 能UTF-8解码 2. 按逗号拆分≥3个字段 3. 第三个字段含数字(重量) + """ + DELIMITER = b'\r\n' + # 1. 按分隔符拆分,过滤空包 + packets = [p for p in raw_data.split(DELIMITER) if p] + if not packets: + return None + + valid_packets = [] + for packet in packets: + try: + # 过滤无效ASCII字符(只保留可见字符) + valid_chars = [c for c in packet if 32 <= c <= 126] + filtered_packet = bytes(valid_chars) + # 2. 验证解码 + data_str = filtered_packet.decode('utf-8').strip() + # 3. 验证字段数量 + parts = data_str.split(',') + if len(parts) < 3: + continue + # 4. 验证重量字段含数字 + weight_part = parts[2].strip() + if not any(char.isdigit() for char in weight_part): + continue + # 满足所有条件,加入有效包列表 + valid_packets.append(packet) + except (UnicodeDecodeError, IndexError): + # 解码失败或字段异常,跳过该包 + continue + + # 返回最后一个有效包(最新),无有效包则返回None + return valid_packets[-1] if valid_packets else None + + def parse_weight(self, packet_data): + """解析重量函数:提取重量数值(如从 b'ST,NT,+0000175\r\n' 中提取 175)""" + try: + data_str = packet_data.decode('utf-8').strip() + parts = data_str.split(',') + # 确保有完整的数据包,三个字段 + if len(parts) < 3: + print(f"parse_weight: 包格式错误(字段不足):{data_str}") + return None + + weight_part = parts[2].strip() + return int(''.join(filter(str.isdigit, weight_part))) + except (IndexError, ValueError, UnicodeDecodeError) as e: + # print(f"数据解析失败:{e},原始数据包:{packet_data}") + return None + \ No newline at end of file diff --git a/hardware/transmitter.py b/hardware/transmitter.py index d22b033..d50cb94 100644 --- a/hardware/transmitter.py +++ b/hardware/transmitter.py @@ -1,11 +1,21 @@ # hardware/transmitter.py from pymodbus.exceptions import ModbusException +import socket +from config.ini_manager import ini_manager +from config.settings import app_set_config +import time + class TransmitterController: def __init__(self, relay_controller): self.relay_controller = relay_controller - + self.test_upper_weight=5043 + self.test_lower_weight=1256 + self.is_start_upper=False + self.is_start_lower=False + self.start_time_upper=None + self.start_time_lower=None # 变送器配置 self.config = { 1: { # 上料斗 @@ -70,51 +80,140 @@ class TransmitterController: self.relay_controller.modbus_client.close() # 直接读取 变送器返回的数据并解析 - def read_data(self, transmitter_id): + def read_data_sub(self, transmitter_id): + """ Args: transmitter_id 为1 表示上料斗, 为2 表示下料斗 return: 读取成功返回重量 weight: int, 失败返回 None """ + TIMEOUT = 2 # 超时时间为 2秒 + BUFFER_SIZE= 1024 + IP = None + PORT = None + weight = None if transmitter_id == 1: # 上料斗变送器的信息: - IP = "192.168.250.63" - PORT = 502 - TIMEOUT = 2 # 超时时间为 2秒 - BUFFER_SIZE= 1024 - weight = None - - import socket - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - try: - s.settimeout(TIMEOUT) - s.connect((IP, PORT)) - # print(f"连接上料斗变送器 {IP}:{PORT} 成功") - - # 接收数据(变送器主动推送,recv即可获取数据) - data = s.recv(BUFFER_SIZE) - if data: - # print(f"收到原始数据:{data}") - - # 提取出完整的一个数据包 (\r\n结尾) - packet = self.get_latest_valid_packet(data) - if not packet: - print("未获取到有效数据包!!") - return None - # 解析重量 - weight = self.parse_weight(packet) - else: - print("未收到设备数据") - - except ConnectionRefusedError: - print(f"变送器连接失败:{IP}:{PORT} 拒绝连接(设备离线/端口错误)") - except socket.timeout: - print(f"读取变送器数据超时:{TIMEOUT}秒内未收到数据") - except Exception as e: - print(f"读取异常:{e}") + IP = ini_manager.upper_transmitter_ip + PORT = ini_manager.upper_transmitter_port + elif transmitter_id == 2: + # 下料斗变送器的信息: + IP = ini_manager.lower_transmitter_ip + PORT = ini_manager.lower_transmitter_port - # 成功返回重量(int),失败返回None - return weight + if not IP or not PORT: + print(f"未配置变送器 {transmitter_id} 的IP或PORT") + return None + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + s.settimeout(TIMEOUT) + s.connect((IP, PORT)) + # print(f"连接上料斗变送器 {IP}:{PORT} 成功") + + # 接收数据(变送器主动推送,recv即可获取数据) + data = s.recv(BUFFER_SIZE) + if data: + # print(f"收到原始数据:{data}") + + # 提取出完整的一个数据包 (\r\n结尾) + packet = self.get_latest_valid_packet(data) + if not packet: + print("未获取到有效数据包!!") + return None + # 解析重量 + weight = self.parse_weight(packet) + else: + print("未收到设备数据") + + except ConnectionRefusedError: + print(f"变送器连接失败:{IP}:{PORT} 拒绝连接(设备离线/端口错误)") + except socket.timeout: + print(f"读取变送器数据超时:{TIMEOUT}秒内未收到数据") + except Exception as e: + print(f"读取异常:{e}") + # 成功返回重量(int),失败返回None + return weight + + def read_data_sub_test(self, transmitter_id): + + """ + 测试用:模拟读取变送器数据mock + Args: transmitter_id 为1 表示上料斗, 为2 表示下料斗 + return: 读取成功返回重量 weight: int, 失败返回 None + """ + TIMEOUT = 2 # 超时时间为 2秒 + BUFFER_SIZE= 1024 + IP = None + PORT = None + weight = 0 + + + if transmitter_id == 1: + # 上料斗变送器的信息: + IP = ini_manager.upper_transmitter_ip + PORT = ini_manager.upper_transmitter_port + + if self.is_start_upper: + if self.start_time_upper is None: + self.start_time_upper=time.time() + weight=self.test_upper_weight-50*(time.time()-self.start_time_upper) + if weight<0: + weight=0 + else: + weight=self.test_upper_weight + self.start_time_upper=None + + return weight + + elif transmitter_id == 2: + # 下料斗变送器的信息: + IP = ini_manager.lower_transmitter_ip + PORT = ini_manager.lower_transmitter_port + + if self.is_start_lower: + if self.start_time_lower is None: + self.start_time_lower=time.time() + weight=self.test_lower_weight-50*(time.time()-self.start_time_lower) + else: + weight=self.test_lower_weight + self.start_time_lower=None + + return weight + + if not IP or not PORT: + print(f"未配置变送器 {transmitter_id} 的IP或PORT") + return 0 + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + s.settimeout(TIMEOUT) + s.connect((IP, PORT)) + # print(f"连接上料斗变送器 {IP}:{PORT} 成功") + + # 接收数据(变送器主动推送,recv即可获取数据) + data = s.recv(BUFFER_SIZE) + if data: + # print(f"收到原始数据:{data}") + + # 提取出完整的一个数据包 (\r\n结尾) + packet = self.get_latest_valid_packet(data) + if not packet: + print("未获取到有效数据包!!") + return None + # 解析重量 + weight = self.parse_weight(packet) + else: + print("未收到设备数据") + + except ConnectionRefusedError: + print(f"变送器连接失败:{IP}:{PORT} 拒绝连接(设备离线/端口错误)") + except socket.timeout: + print(f"读取变送器数据超时:{TIMEOUT}秒内未收到数据") + except Exception as e: + print(f"读取异常:{e}") + + # 成功返回重量(int),失败返回None + return weight + def get_latest_valid_packet(self, raw_data): """ 解决TCP粘包: @@ -167,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 {new_status.value} {message}") + + for callback in self._status_callbacks: + try: + callback(old_status, new_status, message) + except Exception as e: + self.logger.error(f"状态回调执行失败: {e}") + + def _send_and_receive(self, data: bytes, expected_length: int = 1024) -> bytes: + """发送数据并接收响应""" + if not self._socket: + raise ConnectionError("Socket未连接") + + # print("_send_and_receive发送:",data) + self._socket.send(data) + response = self._socket.recv(expected_length) + return response + + def _check_handshake_response(self, response: bytes): + """检查握手响应""" + if len(response) < 24: + raise ConnectionError("握手响应数据不完整") + + # 检查响应头 + if response[0:4] != b'FINS': + raise ConnectionError("无效的FINS响应头") + + # 检查命令代码 + command_code = struct.unpack('>I', response[8:12])[0] + if command_code != 0x01: + raise ConnectionError(f"握手命令代码错误: 0x{command_code:08X}") + + # 检查错误代码 + error_code = struct.unpack('>I', response[12:16])[0] + if error_code == 0x20: + raise FinsPoolFullError("FINS连接池已满") + elif error_code != 0x00: + raise ConnectionError(f"握手错误代码: 0x{error_code:08X}") + + self.logger.info("握手成功") + + def _check_query_response(self, response: bytes) -> int: + """检查查询响应并返回数据""" + if len(response) < 30: + raise ConnectionError("查询响应数据不完整") + + # 检查响应头 + if response[0:4] != b'FINS': + raise ConnectionError("无效的FINS响应头") + + # 检查命令代码 + command_code = struct.unpack('>I', response[8:12])[0] + if command_code != 0x02: + raise ConnectionError(f"查询命令代码错误: 0x{command_code:08X}") + + # 检查错误代码 + error_code = struct.unpack('>I', response[12:16])[0] + if error_code != 0x00: + raise ConnectionError(f"查询错误代码: 0x{error_code:08X}") + + # 提取数据字节(最后一个字节) + data_byte = response[-1] + return data_byte + + def _check_logout_response(self, response: bytes): + """检查注销响应""" + if len(response) < 16: + raise ConnectionError("注销响应数据不完整") + + # 检查响应头 + if response[0:4] != b'FINS': + raise ConnectionError("无效的FINS响应头") + + # 检查命令代码 + command_code = struct.unpack('>I', response[8:12])[0] + if command_code != 0x03: + raise ConnectionError(f"注销命令代码错误: 0x{command_code:08X}") + + # 检查错误代码 + error_code = struct.unpack('>I', response[12:16])[0] + if error_code != 0x02: + raise ConnectionError(f"注销错误代码: 0x{error_code:08X}") + + self.logger.info("注销成功") + + def connect(self) -> bool: + """ + 连接到PLC并完成握手 + + Returns: + bool: 连接是否成功 + """ + if self._status == FinsServiceStatus.CONNECTED: + self.logger.warning("已经连接到PLC") + return True + + self._update_status(FinsServiceStatus.CONNECTING, "开始连接PLC") + + try: + # 创建socket连接 + self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._socket.settimeout(10.0) + self._socket.connect((self.plc_ip, self.plc_port)) + self.logger.info(f"TCP连接已建立: {self.plc_ip}:{self.plc_port}") + + # 指令1: 握手 + # 46 49 4E 53 00 00 00 0C 00 00 00 00 00 00 00 00 00 00 00 DC + handshake_cmd = bytes.fromhex("46 49 4E 53 00 00 00 0C 00 00 00 00 00 00 00 00 00 00 00 DC") + self.logger.debug("发送握手指令") + response = self._send_and_receive(handshake_cmd, 24) + + # 检查握手响应 + self._check_handshake_response(response) + + self._update_status(FinsServiceStatus.CONNECTED, "握手成功") + return True + + except FinsPoolFullError: + self._update_status(FinsServiceStatus.ERROR, "连接池已满") + raise + except Exception as e: + self._update_status(FinsServiceStatus.ERROR, f"连接失败: {e}") + if self._socket: + self._socket.close() + self._socket = None + raise + + def query_data(self) -> Optional[int]: + """ + 查询PLC数据 + + Returns: + int: 数据值(0-255) + """ + + if self._status != FinsServiceStatus.POLLING: + raise ConnectionError("未连接到PLC,无法查询") + + try: + # 指令2: 查询 + # 46 49 4E 53 00 00 00 1A 00 00 00 02 00 00 00 00 80 00 30 00 E9 00 00 DC 00 00 01 01 B0 00 00 00 00 01 + query_cmd = bytes.fromhex("46 49 4E 53 00 00 00 1A 00 00 00 02 00 00 00 00 80 00 30 00 E9 00 00 DC 00 00 01 01 B0 00 00 00 00 01") + self.logger.debug("发送查询指令") + response = self._send_and_receive(query_cmd, 1024) + + # 检查查询响应并提取数据 + data_byte = self._check_query_response(response) + + self._latest_data = data_byte + self._last_update_time = time.time() + + # 触发数据回调 + binary_str = bin(data_byte) + for callback in self._data_callbacks: + try: + callback(data_byte, binary_str) + except Exception as e: + self.logger.error(f"数据回调执行失败: {e}") + + self.logger.debug(f"查询成功: 数据=0x{data_byte:02X} ({binary_str})") + return data_byte + + except Exception as e: + self.logger.error(f"查询失败: {e}") + raise + + def disconnect(self): + """断开连接""" + if self._socket: + try: + # 指令3: 注销 + # 46 49 4E 53 00 00 00 10 00 00 00 02 00 00 00 00 DC E9 + logout_cmd = bytes.fromhex("46 49 4E 53 00 00 00 10 00 00 00 02 00 00 00 00 00 00 00 DC 00 00 00 E9") + self.logger.debug("发送注销指令") + response = self._send_and_receive(logout_cmd, 24) + + # 检查注销响应 + self._check_logout_response(response) + + except Exception as e: + self.logger.error(f"注销过程中出错: {e}") + finally: + self._socket.close() + self._socket = None + + self._update_status(FinsServiceStatus.DISCONNECTED, "连接已关闭") + + def _polling_loop(self): + """轮询循环""" + self.logger.info("数据轮询循环启动") + + while not self._stop_event.is_set(): + try: + if self._status == FinsServiceStatus.CONNECTED: + self._update_status(FinsServiceStatus.POLLING, "正在查询数据") + self.query_data() + self._update_status(FinsServiceStatus.CONNECTED, "查询完成") + else: + # 尝试重新连接 + try: + self.connect() + except FinsPoolFullError: + self.logger.error("连接池已满,等待后重试...") + time.sleep(5) + except Exception as e: + self.logger.error(f"连接失败: {e}, 等待后重试...") + time.sleep(2) + + except Exception as e: + self.logger.error(f"轮询查询失败: {e}") + self._update_status(FinsServiceStatus.ERROR, f"查询错误: {e}") + # 查询失败不影响连接状态,保持CONNECTED状态 + self._update_status(FinsServiceStatus.CONNECTED, "准备下一次查询") + time.sleep(1) + + # 等待轮询间隔 + self._stop_event.wait(self._polling_interval) + + self.logger.info("数据轮询循环停止") + + def start_polling(self, interval: float = 1.0): + """ + 启动数据轮询服务 + + Args: + interval: 轮询间隔(秒) + """ + if self._polling_thread and self._polling_thread.is_alive(): + self.logger.warning("轮询服务已在运行") + return + + self._polling_interval = interval + self._stop_event.clear() + + # 先建立连接 + try: + self.connect() + except Exception as e: + self.logger.error(f"初始连接失败: {e}") + # 继续启动轮询,轮询循环会尝试重连 + + # 启动轮询线程 + self._polling_thread = threading.Thread(target=self._polling_loop, daemon=True) + self._polling_thread.start() + self.logger.info(f"数据轮询服务已启动,间隔: {interval}秒") + + def stop_polling(self): + """停止数据轮询服务""" + self.logger.info("正在停止数据轮询服务...") + self._stop_event.set() + + if self._polling_thread and self._polling_thread.is_alive(): + self._polling_thread.join(timeout=5.0) + + self.disconnect() + self._update_status(FinsServiceStatus.STOPPED, "服务已停止") + self.logger.info("数据轮询服务已停止") + + # === 公共接口 === + + def get_service_status(self) -> dict: + """获取服务状态""" + return { + 'status': self._status.value, + 'is_connected': self._status == FinsServiceStatus.CONNECTED, + 'is_polling': self._polling_thread and self._polling_thread.is_alive(), + 'latest_data': self._latest_data, + 'latest_data_binary': bin(self._latest_data) if self._latest_data is not None else None, + 'last_update_time': self._last_update_time, + 'plc_ip': self.plc_ip, + 'plc_port': self.plc_port, + 'polling_interval': self._polling_interval + } + + def get_latest_data(self) -> Optional[int]: + """获取最新数据""" + return self._latest_data + + def get_latest_data_binary(self) -> Optional[str]: + """获取最新数据的二进制表示""" + return bin(self._latest_data) if self._latest_data is not None else None + + # === 回调注册接口 === + + def register_data_callback(self, callback: Callable[[int, str], None]): + """注册数据更新回调""" + self._data_callbacks.append(callback) + + def register_status_callback(self, callback: Callable[[FinsServiceStatus, FinsServiceStatus, str], None]): + """注册状态变化回调""" + self._status_callbacks.append(callback) + + def __enter__(self): + """上下文管理器入口""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """上下文管理器出口,确保资源释放""" + self.stop_polling() + + +# 使用示例和测试 +if __name__ == "__main__": + def on_data_update(data: int, binary: str): + #4即将振捣室5振捣室 64即将搅拌楼 66到达搅拌楼 + print(f"[数据回调] 数值: 0x{data:02X} | 十进制: {data:3d} | 二进制: {binary}") + + def on_status_change(old_status: FinsServiceStatus, new_status: FinsServiceStatus, message: str): + print(f"[状态回调] {old_status.value} -> {new_status.value} : {message}") + + # 创建服务实例 + service = OmronFinsPollingService("192.168.250.233") # 替换为实际PLC IP + + # 注册回调 + service.register_data_callback(on_data_update) + service.register_status_callback(on_status_change) + + print("欧姆龙FINS数据轮询服务") + print("=" * 50) + + try: + # 启动轮询服务,每2秒查询一次 + service.start_polling(interval=2.0) + + # 主循环,定期显示服务状态 + counter = 0 + while True: + status = service.get_service_status() + counter += 1 + time.sleep(1) + + except KeyboardInterrupt: + print("\n\n接收到Ctrl+C,正在停止服务...") + finally: + # 确保服务正确停止 + service.stop_polling() + print("服务已安全停止") \ No newline at end of file diff --git a/main.py b/main.py index e323843..1048d54 100644 --- a/main.py +++ b/main.py @@ -1,37 +1,183 @@ # main.py import time -from config.settings import Settings +from config.settings import app_set_config from core.system import FeedingControlSystem +from hardware import relay +from hardware.relay import RelayController +from hardware.inverter import InverterController +from hardware.transmitter import TransmitterController +import threading +import time +import cv2 +import vision.visual_callback_1203 as angle_visual def main(): # 加载配置 - settings = Settings() - # 初始化系统 - system = FeedingControlSystem(settings) - - try: - # 系统初始化 - system.initialize() - - print("系统准备就绪,5秒后开始下料...") - time.sleep(5) - - # 启动下料流程 - system.start_lower_feeding() - - # 保持运行 - while True: - time.sleep(1) - - except KeyboardInterrupt: - print("收到停止信号") - except Exception as e: - print(f"系统错误: {e}") - finally: - system.stop() + # angle_visual.angle_visual_callback(2,'未堆料') + replay_controller=RelayController() + # transmitter_controller=TransmitterController(replay_controller) + # upper_weight=transmitter_controller.read_data(2) + # print(upper_weight) + + # replay_controller.control(replay_controller.DOOR_LOWER_CLOSE, 'close') + #5-10度徘徊 + # replay_controller.control(replay_controller.DOOR_LOWER_CLOSE, 'open') + # time.sleep(0.1) + # replay_controller.control(replay_controller.DOOR_LOWER_CLOSE, 'close') + + #5-15度徘徊 + # replay_controller.control(replay_controller.DOOR_LOWER_CLOSE, 'close') + replay_controller.control(replay_controller.DOOR_LOWER_OPEN, 'open') + time.sleep(0.3) + replay_controller.control(replay_controller.DOOR_LOWER_OPEN, 'close') + time.sleep(0.3) + replay_controller.control(replay_controller.DOOR_LOWER_CLOSE, 'open') + time.sleep(0.4) + replay_controller.control(replay_controller.DOOR_LOWER_CLOSE, 'close') + time.sleep(3) + replay_controller.control(replay_controller.DOOR_LOWER_OPEN, 'open') + time.sleep(0.3) + replay_controller.control(replay_controller.DOOR_LOWER_OPEN, 'close') + time.sleep(0.3) + replay_controller.control(replay_controller.DOOR_LOWER_CLOSE, 'open') + time.sleep(0.4) + replay_controller.control(replay_controller.DOOR_LOWER_CLOSE, 'close') + + # replay_controller.close_all() + # replay_controller.control(replay_controller.DOOR_UPPER_OPEN, 'open') + + + # replay_controller.control(replay_controller.DOOR_UPPER_OPEN, 'close') + # time.sleep(0.4) + # replay_controller.control(replay_controller.DOOR_LOWER_OPEN, 'close') + # replay_controller.control(replay_controller.DOOR_LOWER_CLOSE, 'open') + # time.sleep(0.4) + # replay_controller.control(replay_controller.DOOR_LOWER_CLOSE, 'close') + + # replay_controller.control(replay_controller.DOOR_UPPER_OPEN, 'open') + # time.sleep(4) + # while True: + # # replay_controller.control(replay_controller.DOOR_UPPER_OPEN, 'close') + # time.sleep(2) + # replay_controller.control(replay_controller.DOOR_UPPER_OPEN, 'open') + # time.sleep(0.1) + + # time.sleep(0.5) + # replay_controller.control(replay_controller.DOOR_UPPER_OPEN, 'open') + # time.sleep(0.5) + # replay_controller.control(replay_controller.DOOR_UPPER_OPEN, 'close') + # time.sleep(0.5) + # replay_controller.control(replay_controller.DOOR_UPPER_OPEN, 'open') + # time.sleep(0.5) + # replay_controller.control(replay_controller.DOOR_UPPER_OPEN, 'close') + + # time.sleep(3) + + + + # inverter_controller=InverterController(replay_controller) + # inverter_controller.control('start') + + # inverter_controller.control('stop') + + + + # return + # replay_controller.control_upper_open() + #3秒开关 + # replay_controller.control(replay_controller.DOOR_LOWER_OPEN, 'open') + # time.sleep(1) + # replay_controller.control(replay_controller.DOOR_LOWER_OPEN, 'close') + + # replay_controller.control(replay_controller.DOOR_LOWER_CLOSE, 'open') + # time.sleep(1) + # replay_controller.control(replay_controller.DOOR_LOWER_CLOSE, 'close') + + + # while True: + # time.sleep(1) + + # replay_controller.control(system.relay_controller.DOOR_LOWER_OPEN, 'open') + + # system = FeedingControlSystem() + + # system.state.vehicle_aligned=True + #假设在振捣室 + # system.state._upper_door_position='over_lower' + # system.relay_controller.control(system.relay_controller.DOOR_LOWER_OPEN, 'open') + # system.initialize() + + # system.state._feed_status=FeedStatus.FCheckM + + # cv2.namedWindow("控制系统", cv2.WND_PROP_VISIBLE) + # cv2.setWindowProperty("控制系统", cv2.WND_PROP_VISIBLE, 0) + # while True: + # key = cv2.waitKey(100) & 0xFF + # if key == ord('q') or key == 27: # 'q'键或ESC键 + # print("接收到退出信号,正在关闭系统...") + # break + + # time.sleep(1) # 减少CPU占用 + + # replay_controller.close_all() + + + # system.relay_controller.control(system.relay_controller.DOOR_UPPER_OPEN, 'open') + + # system.relay_controller.control(system.relay_controller.DOOR_UPPER_CLOSE, 'close') + # system.relay_controller.control(system.relay_controller.DOOR_UPPER_OPEN, 'open') + # time.sleep(5) + # system.relay_controller.control_upper_close() + + + # system.relay_controller.control(system.relay_controller, 'close') + + # system.relay_controller.control(system.relay_controller.DOOR_UPPER_OPEN, 'open') + + # system.camera_controller.start_cameras() + # loc_all_close=True + # if loc_all_close: + # relay=RelayController() + # relay.control(relay.UPPER_TO_JBL, 'close') + # relay.control(relay.UPPER_TO_ZD, 'close') + # relay.control(relay.DOOR_LOWER_OPEN, 'close') + # relay.control(relay.DOOR_LOWER_CLOSE, 'close') + # relay.control(relay.DOOR_UPPER_OPEN, 'close') + # relay.control(relay.DOOR_UPPER_CLOSE, 'close') + # relay.control(relay.BREAK_ARCH_UPPER, 'close') + # relay.control(relay.BREAK_ARCH_LOWER, 'close') + + + # time.sleep(2) + # system._alignment_check_loop() + # system.relay_controller.control(system.relay_controller.DOOR_LOWER_OPEN, 'open') + # time.sleep(2) + # system.relay_controller.control(system.relay_controller.DOOR_UPPER_OPEN, 'close') + + # time.sleep(2) + # system.relay_controller.control(system.relay_controller.DOOR_LOWER_CLOSE, 'open') + # time.sleep(2) + # system.relay_controller.control(system.relay_controller.DOOR_UPPER_CLOSE, 'close') + + # system.relay_controller.control(system.relay_controller.DOOR_LOWER_CLOSE, 'open') + # time.sleep(5) + # system.relay_controller.control(system.relay_controller.DOOR_LOWER_CLOSE, 'close') + # system._visual_control_loop() + # system.transmitter_controller.test_upper_weight=2*2500 + # system.transmitter_controller.test_lower_weight=1000 + # system.state.vehicle_aligned=True + # # 启动调整线程 + # weight_thread = threading.Thread(target=adjust_weights, args=(system,), daemon=True) + + # weight_thread.start() + # system.state._upper_door_position='over_lower' + # system._start_lower_feeding() + + + if __name__ == "__main__": main() diff --git a/main2.py b/main2.py new file mode 100644 index 0000000..371555e --- /dev/null +++ b/main2.py @@ -0,0 +1,133 @@ +# main.py +import time +from config.settings import app_set_config +from core.system import FeedingControlSystem +from hardware import relay +from hardware.relay import RelayController +import threading +import time +import cv2 +import os +from core.state import FeedStatus + + +def main(): + system = FeedingControlSystem() + system.relay_controller.close_all() + # 启动视觉控制 + # system.camera_controller.start_cameras() + # system.start_visual_control() + system.state._feed_status = FeedStatus.FCheckM + system.start_lower_feeding() + system.state.overflow_detected='未堆料' + system.initialize() + while True: + time.sleep(5) + + + # time.sleep(1) + # system.relay_controller.control(system.relay_controller.DOOR_LOWER_OPEN, 'open') + + # system.vision_detector.detect_angle() + # while True: + # system.feeding_controller.pulse_control_door_for_maintaining() + # time.sleep(5) + # system.state.vehicle_aligned=True + #假设在 fertilize room + # system.state._upper_door_position='over_lower' + # while True: + # system.feeding_controller.pulse_control_door_for_maintaining() + # time.sleep(4) + # system.relay_controller.control(system.relay_controller.DOOR_LOWER_OPEN, 'open') + + # 加载配置 + # 初始化系统 + + + # replay_controller.control(replay_controller.DOOR_LOWER_OPEN, 'open') + # time.sleep(1) + # replay_controller.control(replay_controller.DOOR_LOWER_OPEN, 'close') + # replay_controller.control(replay_controller.DOOR_LOWER_CLOSE, 'close') + + # replay_controller.control_upper_open() + + # replay_controller.control(replay_controller.DOOR_LOWER_OPEN, 'open') + # time.sleep(5) + # replay_controller.control(replay_controller.DOOR_LOWER_OPEN, 'close') + + # image=cv2.imread(os.path.join(app_set_config.project_root,'test.jpeg')) + # image=cv2.flip(image, 0) + # cv2.imshow('test',image) + # cv2.waitKey(1) + + # replay_controller.control(system.relay_controller.DOOR_LOWER_OPEN, 'open') + # system.state._feed_status=FeedStatus.FCheckM + + # cv2.namedWindow("控制系统", cv2.WND_PROP_VISIBLE) + # cv2.setWindowProperty("控制系统", cv2.WND_PROP_VISIBLE, 0) + # while True: + # key = cv2.waitKey(100) & 0xFF + # if key == ord('q') or key == 27: # 'q'键或ESC键 + # print("接收到退出信号,正在关闭系统...") + # break + + # time.sleep(1) # 减少CPU占用 + + # replay_controller.close_all() + + + # system.relay_controller.control(system.relay_controller.DOOR_UPPER_OPEN, 'open') + + # system.relay_controller.control(system.relay_controller.DOOR_UPPER_CLOSE, 'close') + # system.relay_controller.control(system.relay_controller.DOOR_UPPER_OPEN, 'open') + # time.sleep(5) + # system.relay_controller.control_upper_close() + + + # system.relay_controller.control(system.relay_controller, 'close') + + # system.relay_controller.control(system.relay_controller.DOOR_UPPER_OPEN, 'open') + + # system.camera_controller.start_cameras() + # loc_all_close=True + # if loc_all_close: + # relay=RelayController() + # relay.control(relay.UPPER_TO_JBL, 'close') + # relay.control(relay.UPPER_TO_ZD, 'close') + # relay.control(relay.DOOR_LOWER_OPEN, 'close') + # relay.control(relay.DOOR_LOWER_CLOSE, 'close') + # relay.control(relay.DOOR_UPPER_OPEN, 'close') + # relay.control(relay.DOOR_UPPER_CLOSE, 'close') + # relay.control(relay.BREAK_ARCH_UPPER, 'close') + # relay.control(relay.BREAK_ARCH_LOWER, 'close') + + + # time.sleep(2) + # system._alignment_check_loop() + # system.relay_controller.control(system.relay_controller.DOOR_LOWER_OPEN, 'open') + # time.sleep(2) + # system.relay_controller.control(system.relay_controller.DOOR_UPPER_OPEN, 'close') + + # time.sleep(2) + # system.relay_controller.control(system.relay_controller.DOOR_LOWER_CLOSE, 'open') + # time.sleep(2) + # system.relay_controller.control(system.relay_controller.DOOR_UPPER_CLOSE, 'close') + + # system.relay_controller.control(system.relay_controller.DOOR_LOWER_CLOSE, 'open') + # time.sleep(5) + # system.relay_controller.control(system.relay_controller.DOOR_LOWER_CLOSE, 'close') + # system._visual_control_loop() + # system.transmitter_controller.test_upper_weight=2*2500 + # system.transmitter_controller.test_lower_weight=1000 + # system.state.vehicle_aligned=True + # # 启动调整线程 + # weight_thread = threading.Thread(target=adjust_weights, args=(system,), daemon=True) + + # weight_thread.start() + # system.state._upper_door_position='over_lower' + # system._start_lower_feeding() + + + +if __name__ == "__main__": + main() diff --git a/opc/opcua_client_subscription.py b/opc/opcua_client_subscription.py new file mode 100644 index 0000000..ff896ed --- /dev/null +++ b/opc/opcua_client_subscription.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +""" +OPC UA 客户端订阅模式示例 +使用订阅机制实现实时数据更新 +""" + +from opcua import Client, Subscription +from opcua.ua import DataChangeNotification +import time +import sys +import threading + +class SubHandler: + """ + 订阅处理器,处理数据变化通知 + """ + def __init__(self): + self.data_changes = {} + self.change_count = 0 + + def datachange_notification(self, node, val, data): + """ + 数据变化时的回调函数 + """ + self.change_count += 1 + node_name = node.get_display_name().Text + self.data_changes[node_name] = { + 'value': val, + 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'), + 'node_id': str(node) + } + + print(f"🔔 数据变化 #{self.change_count}") + print(f" 节点: {node_name}") + print(f" 数值: {val}") + print(f" 时间: {self.data_changes[node_name]['timestamp']}") + print(f" 节点ID: {node}") + print("-" * 50) + +class OPCUAClientSubscription: + """ + 使用订阅机制的 OPC UA 客户端 + """ + def __init__(self, server_url="opc.tcp://localhost:4840/zjsh_feed/server/"): + self.client = Client(server_url) + self.connected = False + self.subscription = None + self.handler = SubHandler() + self.monitored_nodes = {} + + def connect(self): + """连接到服务器""" + try: + self.client.connect() + self.connected = True + print(f"✅ 成功连接到 OPC UA 服务器: {self.client.server_url}") + return True + except Exception as e: + print(f"❌ 连接服务器失败: {e}") + return False + + def disconnect(self): + """断开连接""" + if self.subscription: + self.subscription.delete() + print("🗑️ 已删除订阅") + + if self.connected: + self.client.disconnect() + self.connected = False + print("🔌 已断开与 OPC UA 服务器的连接") + + def setup_subscription(self, publishing_interval=500): + """ + 设置订阅 + + Args: + publishing_interval: 发布间隔(毫秒) + """ + if not self.connected: + print("请先连接到服务器") + return False + + try: + # 创建订阅 + self.subscription = self.client.create_subscription(publishing_interval, self.handler) + print(f"📡 已创建订阅,发布间隔: {publishing_interval}ms") + + # 获取要监控的节点 + objects = self.client.get_objects_node() + upper_device = objects.get_child("2:upper") + lower_device = objects.get_child("2:lower") + + # 订阅重量数据 + upper_weight_node = upper_device.get_child("2:upper_weight") + lower_weight_node = lower_device.get_child("2:lower_weight") + + # 开始监控 + upper_handle = self.subscription.subscribe_data_change(upper_weight_node) + lower_handle = self.subscription.subscribe_data_change(lower_weight_node) + + self.monitored_nodes = { + 'upper_weight': {'node': upper_weight_node, 'handle': upper_handle}, + 'lower_weight': {'node': lower_weight_node, 'handle': lower_handle} + } + + print(f"📊 已订阅 {len(self.monitored_nodes)} 个数据节点") + return True + + except Exception as e: + print(f"❌ 设置订阅失败: {e}") + return False + + def get_current_values(self): + """获取当前值""" + if not self.monitored_nodes: + return {} + + values = {} + for name, info in self.monitored_nodes.items(): + try: + values[name] = info['node'].get_value() + except Exception as e: + values[name] = f"读取失败: {e}" + + return values + + def run_subscription_test(self, duration=30): + """ + 运行订阅测试 + + Args: + duration: 测试持续时间(秒) + """ + if not self.setup_subscription(): + return + + print(f"\n🚀 开始订阅模式测试,持续 {duration} 秒...") + print("💡 等待数据变化通知...") + print("=" * 60) + + start_time = time.time() + last_stats_time = start_time + + try: + while time.time() - start_time < duration: + current_time = time.time() + + # 每5秒显示一次统计信息 + if current_time - last_stats_time >= 5: + elapsed = current_time - start_time + changes_per_minute = (self.handler.change_count / elapsed) * 60 + + print(f"\n📈 统计信息 (运行时间: {elapsed:.1f}s)") + print(f" 总变化次数: {self.handler.change_count}") + print(f" 变化频率: {changes_per_minute:.1f} 次/分钟") + + if self.handler.data_changes: + print(f" 最新数据:") + for name, data in list(self.handler.data_changes.items())[-2:]: # 显示最后2个 + print(f" {name}: {data['value']} ({data['timestamp']})") + + last_stats_time = current_time + + time.sleep(0.1) # 小延迟避免CPU占用过高 + + except KeyboardInterrupt: + print("\n⏹️ 测试被用户中断") + + finally: + print(f"\n🏁 测试完成") + print(f"📊 总变化次数: {self.handler.change_count}") + print(f"⏱️ 平均变化频率: {(self.handler.change_count / duration) * 60:.1f} 次/分钟") + +def main(): + """主函数""" + client = OPCUAClientSubscription("opc.tcp://localhost:4840/zjsh_feed/server/") + + try: + # 连接到服务器 + if not client.connect(): + return + + # 运行订阅测试 + client.run_subscription_test(duration=60) # 运行60秒 + + except Exception as e: + print(f"❌ 客户端运行错误: {e}") + finally: + client.disconnect() + +if __name__ == "__main__": + if len(sys.argv) > 1: + server_url = sys.argv[1] + client = OPCUAClientSubscription(server_url) + else: + client = OPCUAClientSubscription() + + try: + main() + except KeyboardInterrupt: + print("\n👋 用户中断程序") + sys.exit(0) \ No newline at end of file diff --git a/opc/opcua_client_test.py b/opc/opcua_client_test.py new file mode 100644 index 0000000..852ef84 --- /dev/null +++ b/opc/opcua_client_test.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +""" +OPC UA 客户端测试脚本 +用于连接和测试 OPC UA 服务器 +""" + +from opcua import Client +import time +import sys + +class OPCUAClientTest: + def __init__(self, server_url="opc.tcp://localhost:4840/zjsh_feed/server/"): + """ + 初始化 OPC UA 客户端 + + Args: + server_url: 服务器URL地址 + """ + self.client = Client(server_url) + self.connected = False + + def connect(self): + """连接到服务器""" + try: + self.client.connect() + self.connected = True + print(f"成功连接到 OPC UA 服务器: {self.client.server_url}") + return True + except Exception as e: + print(f"连接服务器失败: {e}") + return False + + def disconnect(self): + """断开连接""" + if self.connected: + self.client.disconnect() + self.connected = False + print("已断开与 OPC UA 服务器的连接") + + def browse_nodes(self): + """浏览服务器节点结构""" + if not self.connected: + print("请先连接到服务器") + return + + try: + # 获取根节点 + root = self.client.get_root_node() + print(f"根节点: {root}") + + # 获取对象节点 + objects = self.client.get_objects_node() + print(f"对象节点: {objects}") + + upper_device = objects.get_child("2:upper") + print(f"\n上料斗对象: {upper_device}") + + lower_device = objects.get_child("2:lower") + print(f"下料斗对象: {lower_device}") + + print("\n=== 当前对象属性===") + self.read_object_properties(upper_device, lower_device) + + except Exception as e: + print(f"浏览节点时出错: {e}") + + def read_object_properties(self, upper_device, lower_device): + """读取重量数值""" + try: + # 读取重量 + upper_weight = upper_device.get_child("2:upper_weight").get_value() + lower_weight = lower_device.get_child("2:lower_weight").get_value() + print(f"上料斗重量: {upper_weight}") + print(f"下料斗重量: {lower_weight}") + + + except Exception as e: + print(f"读取数据时出错: {e}") + + def monitor_data(self, duration=30): + """监控数据变化""" + if not self.connected: + print("请先连接到服务器") + return + + print(f"\n开始监控数据变化,持续 {duration} 秒...") + + try: + # 获取传感器节点 + objects = self.client.get_objects_node() + upper_device = objects.get_child("2:upper") + lower_device = objects.get_child("2:lower") + + + + + start_time = time.time() + while time.time() - start_time < duration: + print(f"\n--- {time.strftime('%H:%M:%S')} ---") + self.read_sensor_values(upper_device, lower_device) + time.sleep(5) # 每5秒读取一次 + + except KeyboardInterrupt: + print("\n监控被用户中断") + except Exception as e: + print(f"监控数据时出错: {e}") + + + +def main(): + """主函数""" + # 创建客户端 + client = OPCUAClientTest("opc.tcp://localhost:4840/zjsh_feed/server/") + + try: + # 连接到服务器 + if not client.connect(): + return + + # 浏览节点结构 + client.browse_nodes() + + # 监控数据变化 + client.monitor_data(duration=30) + + # 测试写入数据 + # client.write_test_data() + + # 继续监控 + print("\n继续监控数据...") + client.monitor_data(duration=15) + + except KeyboardInterrupt: + print("\n客户端被用户中断") + finally: + # 断开连接 + client.disconnect() + + +if __name__ == "__main__": + if len(sys.argv) > 1: + # 支持自定义服务器地址 + server_url = sys.argv[1] + client = OPCUAClientTest(server_url) + else: + client = OPCUAClientTest() + + try: + main() + except Exception as e: + print(f"客户端运行错误: {e}") + sys.exit(1) \ No newline at end of file diff --git a/opc/opcua_server.py b/opc/opcua_server.py new file mode 100644 index 0000000..52988b7 --- /dev/null +++ b/opc/opcua_server.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +""" +简单的OPC UA服务器示例 +用于工业自动化数据通信 +""" + +from opcua import Server, ua +import time +import random +import threading +from datetime import datetime +from core.system import SystemState +from config.ini_manager import ini_manager + +class SimpleOPCUAServer: + def __init__(self, state, endpoint=ini_manager.opcua_endpoint, name="Feed_Server"): + """ + 初始化OPC UA服务器 + + Args: + endpoint: 服务器端点地址 + name: 服务器名称 + """ + self.server = Server() + self.server.set_endpoint(endpoint) + self.server.set_server_name(name) + self.state = state + + # 设置服务器命名空间 + self.namespace = self.server.register_namespace("Feed_Control_System") + + # 获取对象节点 + self.objects = self.server.get_objects_node() + + # 创建自定义对象 + self.create_object_structure() + + # 运行标志 + self.running = False + + def create_object_structure(self): + """创建OPC UA对象结构""" + # 创建上料斗对象 + self.upper = self.objects.add_object(self.namespace, "upper") + self.lower=self.objects.add_object(self.namespace, "lower") + self.sys=self.objects.add_object(self.namespace, "sys") + + # 创建变量 + self.create_variables() + + def create_variables(self): + """创建OPC UA变量""" + # 上料斗重量变量 + self.upper_weight = self.upper.add_variable(self.namespace, "upper_weight", 0.0) + self.lower_weight = self.lower.add_variable(self.namespace, "lower_weight", 0.0) + + # 设置变量为可写 + # self.upper_weight.set_writable() + # self.lower_weight.set_writable() + + def setup_state_listeners(self): + """设置状态监听器 - 事件驱动更新""" + if hasattr(self.state, 'state_updated'): + self.state.state_updated.connect(self.on_state_changed) + print("状态监听器已设置 - 事件驱动模式") + + def on_state_changed(self, property_name, value): + """状态变化时的回调函数""" + try: + # 根据属性名更新对应的OPC UA变量 + if property_name == "upper_weight": + self.upper_weight.set_value(value) + elif property_name == "lower_weight": + self.lower_weight.set_value(value) + + # 可以在这里添加更多状态映射 + print(f"状态更新: {property_name} = {value}") + + except Exception as e: + print(f"状态更新错误: {e}") + + def start(self): + """启动服务器""" + try: + self.server.start() + self.running = True + print(f"服务器端点: opc.tcp://0.0.0.0:4840/freeopcua/server/") + + # 初始化当前值 + if self.state: + self.upper_weight.set_value(self.state._upper_weight) + self.lower_weight.set_value(self.state._lower_weight) + print("已同步初始状态值") + + # 设置状态监听器 - 关键步骤! + self.setup_state_listeners() + + # # 只有在没有状态系统时才使用模拟线程 + # if not self.state: + # print("使用模拟数据模式") + # self.simulation_thread = threading.Thread(target=self.simulate_data) + # self.simulation_thread.daemon = True + # self.simulation_thread.start() + + except Exception as e: + print(f"启动服务器失败: {e}") + + def stop(self): + """停止服务器""" + self.running = False + self.server.stop() + print("OPC UA服务器已停止") + + # 断开状态监听器 + if hasattr(self.state, 'state_updated'): + try: + self.state.state_updated.disconnect(self.on_state_changed) + except: + pass + + +def main(): + """主函数""" + # 创建系统状态实例 + state = SystemState() + + # 创建并启动服务器 + server = SimpleOPCUAServer( + state=state, + endpoint="opc.tcp://0.0.0.0:4840/freeopcua/server/", + name="工业自动化 OPC UA 服务器" + ) + + try: + server.start() + + + print("服务器正在运行,按 Ctrl+C 停止...") + + # 保持服务器运行 + while True: + time.sleep(1) + + except KeyboardInterrupt: + print("\n正在停止服务器...") + server.stop() + + except Exception as e: + print(f"服务器运行错误: {e}") + server.stop() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/resources/resources_rc.py b/resources/resources_rc.py index b661162..a07b775 100644 --- a/resources/resources_rc.py +++ b/resources/resources_rc.py @@ -14370,139 +14370,139 @@ qt_resource_struct = b"\ \x00\x00\x00\x10\x00\x02\x00\x00\x00C\x00\x00\x00\x03\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x05\xb8\x00\x00\x00\x00\x00\x01\x00\x02\xdd\x16\ -\x00\x00\x01\x9a>\xc4\x05p\ +\x00\x00\x01\x9a\x80)\xd9\x84\ \x00\x00\x04\x02\x00\x00\x00\x00\x00\x01\x00\x01\xfb\xa6\ -\x00\x00\x01\x9a>\xc4\x05s\ +\x00\x00\x01\x9a\x80)\xd3W\ \x00\x00\x05(\x00\x00\x00\x00\x00\x01\x00\x02\xbc.\ -\x00\x00\x01\x9a\x14\xe2\x1b\xbe\ +\x00\x00\x01\x9a\x80)\xd8a\ \x00\x00\x01\x04\x00\x00\x00\x00\x00\x01\x00\x00+8\ -\x00\x00\x01\x9a>\xc4\x05s\ +\x00\x00\x01\x9a\x80)\xdc9\ \x00\x00\x00T\x00\x00\x00\x00\x00\x01\x00\x00\x11r\ -\x00\x00\x01\x9a>\xc4\x05v\ +\x00\x00\x01\x9a\x80)\xd8\xb7\ \x00\x00\x05\xd0\x00\x00\x00\x00\x00\x01\x00\x02\xddk\ -\x00\x00\x01\x9a>\xc4\x05x\ +\x00\x00\x01\x9a\x80)\xda\x98\ \x00\x00\x02\xa6\x00\x00\x00\x00\x00\x01\x00\x01\x08\xf0\ -\x00\x00\x01\x9a>\xc4\x05x\ +\x00\x00\x01\x9a\x80)\xd9b\ \x00\x00\x00:\x00\x00\x00\x00\x00\x01\x00\x00\x01\x05\ -\x00\x00\x01\x9a>\xc4\x05u\ +\x00\x00\x01\x9a\x80)\xd6\x9c\ \x00\x00\x02\xd8\x00\x00\x00\x00\x00\x01\x00\x01\x1d/\ -\x00\x00\x01\x9a\x14\xe2\x1b\xbb\ +\x00\x00\x01\x9a\x80)\xdc\x8a\ \x00\x00\x01\xda\x00\x00\x00\x00\x00\x01\x00\x00\x84\x83\ -\x00\x00\x01\x9a>\xc4\x05\x81\ +\x00\x00\x01\x9a\x80)\xdd\x04\ \x00\x00\x03\x8e\x00\x00\x00\x00\x00\x01\x00\x01N/\ -\x00\x00\x01\x9a>\xc4\x05r\ +\x00\x00\x01\x9a\x80)\xd8\xfc\ \x00\x00\x04\x84\x00\x00\x00\x00\x00\x01\x00\x02\x92\xa4\ -\x00\x00\x01\x9a>\xc4\x05r\ +\x00\x00\x01\x9a\x80)\xd9\xff\ \x00\x00\x01$\x00\x00\x00\x00\x00\x01\x00\x00-\x82\ -\x00\x00\x01\x9a\x14\xe2\x1b\xb9\ +\x00\x00\x01\x9a\x80)\xdaB\ \x00\x00\x02x\x00\x00\x00\x00\x00\x01\x00\x00\xf5\xe3\ -\x00\x00\x01\x9a>\xc4\x05\x83\ +\x00\x00\x01\x9a\x80)\xda]\ \x00\x00\x01L\x00\x00\x00\x00\x00\x01\x00\x00c\xfb\ -\x00\x00\x01\x9a>\xc4\x05|\ +\x00\x00\x01\x9a\x80)\xdb\xf3\ \x00\x00\x02\x92\x00\x00\x00\x00\x00\x01\x00\x01\x02\xaa\ -\x00\x00\x01\x9a>\xc4\x05|\ +\x00\x00\x01\x9a\x80)\xd4\x9b\ \x00\x00\x03\x02\x00\x00\x00\x00\x00\x01\x00\x01%\xbb\ -\x00\x00\x01\x9a>\xc4\x05\x85\ +\x00\x00\x01\x9a\x80)\xda\xf5\ \x00\x00\x05p\x00\x00\x00\x00\x00\x01\x00\x02\xc7\x9f\ -\x00\x00\x01\x9a>\xc4\x05~\ +\x00\x00\x01\x9a\x80)\xd3\xfd\ \x00\x00\x03\xae\x00\x00\x00\x00\x00\x01\x00\x01Q\xcb\ -\x00\x00\x01\x9a>\xc4\x05\x86\ +\x00\x00\x01\x9a\x80)\xd7\x04\ \x00\x00\x00\x22\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x9a>\xc4\x05~\ +\x00\x00\x01\x9a\x80)\xd8A\ \x00\x00\x04\xc0\x00\x00\x00\x00\x00\x01\x00\x02\x96\x82\ -\x00\x00\x01\x9a>\xc4\x05w\ +\x00\x00\x01\x9a\x80)\xddw\ \x00\x00\x00\x88\x00\x00\x00\x00\x00\x01\x00\x00 \x01\ -\x00\x00\x01\x9a\x14\xe2\x1b\xbe\ +\x00\x00\x01\x9a\x80)\xd67\ \x00\x00\x06\x9a\x00\x00\x00\x00\x00\x01\x00\x03Yo\ -\x00\x00\x01\x9a>\xc4\x05}\ +\x00\x00\x01\x9a\x80)\xdbu\ \x00\x00\x04\xa4\x00\x00\x00\x00\x00\x01\x00\x02\x94\xf7\ -\x00\x00\x01\x9a>\xc4\x05v\ +\x00\x00\x01\x9a\x80)\xd9\x1e\ \x00\x00\x03^\x00\x00\x00\x00\x00\x01\x00\x010\x95\ -\x00\x00\x01\x9a>\xc4\x05\x82\ +\x00\x00\x01\x9a\x80)\xda|\ \x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x00((\ -\x00\x00\x01\x9a>\xc4\x05w\ +\x00\x00\x01\x9a\x80)\xd4Y\ \x00\x00\x06l\x00\x00\x00\x00\x00\x01\x00\x036\xa7\ -\x00\x00\x01\x9a>\xc4\x05\x80\ +\x00\x00\x01\x9a\x80)\xdcp\ \x00\x00\x02\x18\x00\x00\x00\x00\x00\x01\x00\x00\xe1c\ -\x00\x00\x01\x9a>\xc4\x05h\ +\x00\x00\x01\x9a\x80)\xdd;\ \x00\x00\x02`\x00\x00\x00\x00\x00\x01\x00\x00\xf4\xe4\ -\x00\x00\x01\x9a>\xc4\x05~\ +\x00\x00\x01\x9a\x80)\xd4\xba\ \x00\x00\x03\xc4\x00\x00\x00\x00\x00\x01\x00\x01\xa8}\ -\x00\x00\x01\x9a>\xc4\x05\x7f\ +\x00\x00\x01\x9a\x80)\xd5\x9f\ \x00\x00\x03>\x00\x00\x00\x00\x00\x01\x00\x01/\xe2\ -\x00\x00\x01\x9a>\xc4\x05k\ +\x00\x00\x01\x9a\x80)\xd9\xa3\ \x00\x00\x01\xf8\x00\x00\x00\x00\x00\x01\x00\x00\xe0\xd7\ -\x00\x00\x01\x9a>\xc4\x05l\ +\x00\x00\x01\x9a\x80)\xdd\x8f\ \x00\x00\x01\x80\x00\x00\x00\x00\x00\x01\x00\x00z\x9d\ -\x00\x00\x01\x9a>\xc4\x05l\ +\x00\x00\x01\x9a\x80)\xdb\xb6\ \x00\x00\x05\xea\x00\x00\x00\x00\x00\x01\x00\x02\xeb\xec\ -\x00\x00\x01\x9a>\xc4\x05m\ +\x00\x00\x01\x9a\x80)\xd3\x96\ \x00\x00\x06T\x00\x00\x00\x00\x00\x01\x00\x032L\ -\x00\x00\x01\x9a>\xc4\x05\x86\ +\x00\x00\x01\x9a\x80)\xd5|\ \x00\x00\x06\x86\x00\x00\x00\x00\x00\x01\x00\x03?\x19\ -\x00\x00\x01\x9a>\xc4\x05{\ +\x00\x00\x01\x9a\x80)\xd4\xfa\ \x00\x00\x04h\x00\x00\x00\x00\x00\x01\x00\x02\x8a\xb0\ -\x00\x00\x01\x9a>\xc4\x05\x84\ +\x00\x00\x01\x9a\x80)\xd7t\ \x00\x00\x05>\x00\x00\x00\x00\x00\x01\x00\x02\xbc\xd1\ -\x00\x00\x01\x9a>\xc4\x05\x85\ +\x00\x00\x01\x9a\x80)\xda#\ \x00\x00\x05\x0c\x00\x00\x00\x00\x00\x01\x00\x02\xb9\x8e\ -\x00\x00\x01\x9a>\xc4\x05t\ +\x00\x00\x01\x9a\x80)\xd4;\ \x00\x00\x00\xcc\x00\x00\x00\x00\x00\x01\x00\x00#\xb3\ -\x00\x00\x01\x9a>\xc4\x05t\ +\x00\x00\x01\x9a\x80)\xdc\x17\ \x00\x00\x00\x9e\x00\x00\x00\x00\x00\x01\x00\x00!\x14\ -\x00\x00\x01\x9a\x14\xe2\x1b\xbb\ +\x00\x00\x01\x9a\x80)\xd6z\ \x00\x00\x06\xb2\x00\x00\x00\x00\x00\x01\x00\x03_2\ -\x00\x00\x01\x9a>\xc4\x05{\ +\x00\x00\x01\x9a\x80)\xda\xda\ \x00\x00\x01`\x00\x00\x00\x00\x00\x01\x00\x00rC\ -\x00\x00\x01\x9a>\xc4\x05\x84\ +\x00\x00\x01\x9a\x80)\xd9?\ \x00\x00\x064\x00\x00\x00\x00\x00\x01\x00\x03)\xfc\ -\x00\x00\x01\x9a>\xc4\x05\x83\ +\x00\x00\x01\x9a\x80)\xdb\x19\ \x00\x00\x05\xa6\x00\x00\x00\x00\x00\x01\x00\x02\xd7\xf5\ -\x00\x00\x01\x9a\x14\xe2\x1b\xbe\ +\x00\x00\x01\x9a\x80)\xddX\ \x00\x00\x01\xa0\x00\x00\x00\x00\x00\x01\x00\x00{\x07\ -\x00\x00\x01\x9a>\xc4\x05\x82\ +\x00\x00\x01\x9a\x80)\xd5\xc2\ \x00\x00\x03\x1e\x00\x00\x00\x00\x00\x01\x00\x01&\xcb\ -\x00\x00\x01\x9a>\xc4\x05\x82\ +\x00\x00\x01\x9a\x80)\xdb\xd3\ \x00\x00\x04\xdc\x00\x00\x00\x00\x00\x01\x00\x02\x98\x0d\ -\x00\x00\x01\x9a>\xc4\x05\x7f\ +\x00\x00\x01\x9a\x80)\xdcT\ \x00\x00\x06\x0a\x00\x00\x00\x00\x00\x01\x00\x02\xecg\ -\x00\x00\x01\x9a>\xc4\x05x\ +\x00\x00\x01\x9a\x80)\xd8\x9b\ \x00\x00\x01\xc0\x00\x00\x00\x00\x00\x01\x00\x00\x84.\ -\x00\x00\x01\x9a>\xc4\x05i\ +\x00\x00\x01\x9a\x80)\xdbU\ \x00\x00\x04\xf2\x00\x00\x00\x00\x00\x01\x00\x02\x9c\x95\ -\x00\x00\x01\x9a>\xc4\x05q\ +\x00\x00\x01\x9a\x80)\xd7O\ \x00\x00\x04L\x00\x00\x00\x00\x00\x01\x00\x02h\xd9\ -\x00\x00\x01\x9a>\xc4\x05j\ +\x00\x00\x01\x9a\x80)\xd7*\ \x00\x00\x03\xd6\x00\x00\x00\x00\x00\x01\x00\x01\xaa\x0a\ -\x00\x00\x01\x9a>\xc4\x05\x82\ +\x00\x00\x01\x9a\x80)\xd4\x1c\ \x00\x00\x00n\x00\x00\x00\x00\x00\x01\x00\x00\x1d\x5c\ -\x00\x00\x01\x9a>\xc4\x05\x85\ +\x00\x00\x01\x9a\x80)\xd3\xdc\ \x00\x00\x05\x88\x00\x00\x00\x00\x00\x01\x00\x02\xc8\xac\ -\x00\x00\x01\x9a>\xc4\x05\x80\ +\x00\x00\x01\x9a\x80)\xd6\xdf\ \x00\x00\x02\xea\x00\x00\x00\x00\x00\x01\x00\x01\x22\x08\ -\x00\x00\x01\x9a>\xc4\x05t\ +\x00\x00\x01\x9a\x80)\xd8\x03\ \x00\x00\x06 \x00\x00\x00\x00\x00\x01\x00\x03&\x04\ -\x00\x00\x01\x9a>\xc4\x05q\ +\x00\x00\x01\x9a\x80)\xd6\x06\ \x00\x00\x020\x00\x00\x00\x00\x00\x01\x00\x00\xea\x99\ -\x00\x00\x01\x9a>\xc4\x05}\ +\x00\x00\x01\x9a\x80)\xd5\x1a\ \x00\x00\x05Z\x00\x00\x00\x00\x00\x01\x00\x02\xc5\x8e\ -\x00\x00\x01\x9a\x14\xe2\x1b\xbe\ +\x00\x00\x01\x9a\x80)\xd6\xbe\ \x00\x00\x04\x22\x00\x00\x00\x00\x00\x01\x00\x01\xffL\ -\x00\x00\x01\x9a>\xc4\x05y\ +\x00\x00\x01\x9a\x80)\xda\xbe\ \x00\x00\x01:\x00\x00\x00\x00\x00\x01\x00\x00.\xdc\ -\x00\x00\x01\x9a>\xc4\x05z\ +\x00\x00\x01\x9a\x80)\xdd\x22\ \x00\x00\x03\xf0\x00\x00\x00\x00\x00\x01\x00\x01\xabj\ -\x00\x00\x01\x9a>\xc4\x05z\ +\x00\x00\x01\x9a\x80)\xd5\xe3\ \x00\x00\x03v\x00\x00\x00\x00\x00\x01\x00\x01I\x0f\ -\x00\x00\x01\x9a>\xc4\x05|\ +\x00\x00\x01\x9a\x80)\xd9\xc9\ \x00\x00\x02H\x00\x00\x00\x00\x00\x01\x00\x00\xf4\x8f\ -\x00\x00\x01\x9a>\xc4\x05n\ +\x00\x00\x01\x9a\x80)\xd8\xda\ \x00\x00\x02\xc0\x00\x00\x00\x00\x00\x01\x00\x01\x1c\xda\ -\x00\x00\x01\x9a>\xc4\x05n\ +\x00\x00\x01\x9a\x80)\xd4x\ \x00\x00\x044\x00\x00\x00\x00\x00\x01\x00\x02h\x84\ -\x00\x00\x01\x9a>\xc4\x05p\ +\x00\x00\x01\x9a\x80)\xd7\x99\ \x00\x00\x00\xb4\x00\x00\x00\x00\x00\x01\x00\x00#^\ -\x00\x00\x01\x9a>\xc4\x05o\ +\x00\x00\x01\x9a\x80)\xd57\ " def qInitResources(): diff --git a/service/mould_service.py b/service/mould_service.py index 115f7ee..5d52ed2 100644 --- a/service/mould_service.py +++ b/service/mould_service.py @@ -1,8 +1,8 @@ from datetime import datetime, timedelta from common.sqlite_handler import SQLiteHandler from typing import Optional, List -from api_http_client import api_http_client -from busisness.models import ArtifactInfo, TaskInfo, LoginRequest +from .api_http_client import api_http_client +from busisness.models import ArtifactInfo, TaskInfo, LoginRequest, LEDInfo from config.ini_manager import ini_manager @@ -56,7 +56,7 @@ class MouldService: Returns: 未浇筑管片列表,如果失败返回None """ - url = f"{self._host}/api/ext/artifact/not_pour" + url = f"{self._host}/api/ext/mould/not_pour_hidden" try: # 调用API获取数据 @@ -80,59 +80,116 @@ class MouldService: except Exception as e: print(f"请求未浇筑管片信息异常: {e}") return None - - -if __name__ == "__main__": - # 创建模具服务实例 - mould_service = MouldService() - db = SQLiteHandler.get_instance("db/three.db", max_readers=50, busy_timeout=4000) + def get_pouring_led(self) -> Optional[LEDInfo]: + """ + 获取生产动态信息 + + Returns: + LEDInfo对象,如果失败返回None + """ + url = f"{self._host}/api/ext/produce/pouring_led" + + try: + # 调用API获取数据 + response_data = self._api_client.get(url, auth=True) + + # 检查响应状态 + if response_data.get('Code') != 200: + print(f"获取生产动态信息失败: {response_data.get('Message')}") + return None + + # 解析数据 + data = response_data.get('Data', {}) + if not data: + print(f"未获取到 pouring_led 信息") + return None + + # 转换为管片信息对象列表 - 使用更安全的字段过滤方式 + # 只提取 LEDInfo 类中定义的字段,忽略多余字段和缺失字段 + led_info = LEDInfo( + TaskID=data.get('TaskID', ''), + PlateVolume=data.get('PlateVolume', ''), + MouldCode=data.get('MouldCode', ''), + ProduceStartTime=data.get('ProduceStartTime', ''), + ArtifactID=data.get('ArtifactID', ''), + RingTypeCode=data.get('RingTypeCode', ''), + PlateIDSerial=data.get('PlateIDSerial', ''), + CheckResult=data.get('CheckResult', ''), + UpperWeight=data.get('UpperWeight', 0.0), + Temper=data.get('Temper', ''), + WorkshopTemperature=data.get('WorkshopTemperature', ''), + LowBucketWeighingValue=data.get('LowBucketWeighingValue', ''), + VibrationFrequency=data.get('VibrationFrequency', ''), + TotMete=data.get('TotMete', ''), + BetonVolumeAlready=data.get('BetonVolumeAlready', ''), + WaterTemperature=data.get('WaterTemperature', ''), + FormulaProportion=data.get('FormulaProportion', ''), + DayStrengthValue=data.get('DayStrengthValue', ''), + NihtStrengthValue=data.get('NihtStrengthValue', '') + ) + return led_info + + except Exception as e: + print(f"请求 pouring_led 信息异常: {e}") + return None + +app_web_service = MouldService() + +# if __name__ == "__main__": +# # 创建模具服务实例 +# mould_service = MouldService() +# led_info = mould_service.get_pouring_led() +# if led_info: +# print(led_info) + + # db = SQLiteHandler.get_instance("db/three.db", max_readers=50, busy_timeout=4000) # 测试获取未浇筑管片信息 - not_poured = mould_service.get_not_pour_artifacts() - if not_poured: - for item in not_poured: - artifact = db.fetch_one("SELECT * FROM ArtifactTask WHERE ArtifactID = ?", (item.ArtifactID,)) - if not artifact: - dict={ - "ArtifactID": item.ArtifactID, - "ArtifactActionID": item.ArtifactActionID, - "ArtifactIDVice1": item.ArtifactIDVice1, - "ProduceRingNumber": item.ProduceRingNumber, - "MouldCode": item.MouldCode, - "SkeletonID": item.SkeletonID, - "RingTypeCode": item.RingTypeCode, - "SizeSpecification": item.SizeSpecification, - "BuriedDepth": item.BuriedDepth, - "BlockNumber": item.BlockNumber, - "BetonVolume": item.BetonVolume, - "BetonTaskID": item.BetonTaskID, - "HoleRingMarking": item.HoleRingMarking, - "GroutingPipeMarking": item.GroutingPipeMarking, - "PolypropyleneFiberMarking": item.PolypropyleneFiberMarking, - "Status": 1, - "Source": 1 - } - db.insert("ArtifactTask", dict) + # not_poured = mould_service.get_not_pour_artifacts() + # if not_poured: + # for item in not_poured: + # artifact = db.fetch_one("SELECT * FROM ArtifactTask WHERE ArtifactID = ?", (item.ArtifactID,)) + # if not artifact: + # dict={ + # "ArtifactID": item.ArtifactID, + # "ArtifactActionID": item.ArtifactActionID, + # "ArtifactIDVice1": item.ArtifactIDVice1, + # "ProduceRingNumber": item.ProduceRingNumber, + # "MouldCode": item.MouldCode, + # "SkeletonID": item.SkeletonID, + # "RingTypeCode": item.RingTypeCode, + # "SizeSpecification": item.SizeSpecification, + # "BuriedDepth": item.BuriedDepth, + # "BlockNumber": item.BlockNumber, + # "BetonVolume": item.BetonVolume, + # "BetonTaskID": item.BetonTaskID, + # "HoleRingMarking": item.HoleRingMarking, + # "GroutingPipeMarking": item.GroutingPipeMarking, + # "PolypropyleneFiberMarking": item.PolypropyleneFiberMarking, + # "Status": 1, + # "Source": 1 + # } + # db.insert("ArtifactTask", dict) - dict={ - "TaskID": item.BetonTaskID, - "ProjectName": "上海市轨道交通19号线工程盾构区间管片生产2标", - "ProduceMixID": "20251030-02", - "VinNo": "", - "BetonVolume": item.BetonVolume, - "MouldCode": item.MouldCode, - "SkeletonID": item.SkeletonID, - "RingTypeCode": item.RingTypeCode, - "SizeSpecification": item.SizeSpecification, - "BuriedDepth": item.BuriedDepth, - "BlockNumber": item.BlockNumber, - "Mode": 1, - "Status": 1, - "Source": 1, - "OptTime": str(datetime.now() - timedelta(minutes=5)), - "CreateTime": str(datetime.now()) - } - db.insert("PDRecord", dict) + # dict={ + # "TaskID": item.BetonTaskID, + # "ProjectName": "上海市轨道交通19号线工程盾构区间管片生产2标", + # "ProduceMixID": "20251030-02", + # "VinNo": "", + # "BetonVolume": item.BetonVolume, + # "MouldCode": item.MouldCode, + # "SkeletonID": item.SkeletonID, + # "RingTypeCode": item.RingTypeCode, + # "SizeSpecification": item.SizeSpecification, + # "BuriedDepth": item.BuriedDepth, + # "BlockNumber": item.BlockNumber, + # "Mode": 1, + # "Status": 1, + # "Source": 1, + # "OptTime": str(datetime.now() - timedelta(minutes=5)), + # "CreateTime": str(datetime.now()) + # } + # db.insert("PDRecord", dict) # for i in range(2, 5): # row = db.fetch_one("SELECT * FROM ArtifactTask WHERE ID = ?", (i,)) # if row: diff --git a/settings.ini b/settings.ini index c491aa3..d7010b7 100644 --- a/settings.ini +++ b/settings.ini @@ -9,4 +9,9 @@ login_model = {"Program": 11, "SC": "1000000001", "loginName": "leduser", "passw [app] log_path = logs/app.log db_path = db/three.db +opcua_endpoint = opc.tcp://192.168.250.64:4840/zjsh_feed/server/ +upper_transmitter_ip = 192.168.250.63 +upper_transmitter_port = 502 +lower_transmitter_ip = 192.168.250.66 +lower_transmitter_port = 8234 diff --git a/test copy.py b/test copy.py new file mode 100644 index 0000000..de26e5b --- /dev/null +++ b/test copy.py @@ -0,0 +1,32 @@ +import socket + +# 设备信息 +IP = "192.168.250.63" +PORT = 502 +TIMEOUT = 5 # 超时时间(秒) + +# 创建TCP socket +with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + s.settimeout(TIMEOUT) # 设置超时,避免一直阻塞 + # 连接设备 + s.connect((IP, PORT)) + print(f"✅ 已通过TCP连接到 {IP}:{PORT}") + + # 尝试接收数据(不发送任何请求,纯等待) + print("等待设备发送数据...(若5秒内无响应则超时)") + data = s.recv(1024) # 最多接收1024字节 + + if data: + # 打印收到的原始数据(16进制和字节列表) + # print(f"收到数据(16进制):{data.hex()}") + print(f"收到数据(字节列表):{list(data)}") + else: + print("❌ 未收到任何数据(设备未主动发送)") + + except ConnectionRefusedError: + print(f"❌ 连接失败:{IP}:{PORT} 拒绝连接(设备离线/端口错误)") + except socket.timeout: + print(f"❌ 超时:{TIMEOUT}秒内未收到设备数据(设备未主动发送)") + except Exception as e: + print(f"❌ 发生错误:{str(e)}") \ No newline at end of file diff --git a/test_angle.py b/test_angle.py new file mode 100644 index 0000000..60f4d6b --- /dev/null +++ b/test_angle.py @@ -0,0 +1,193 @@ +# main.py +import time +from config.settings import app_set_config +from core.system import FeedingControlSystem +from hardware import relay +from hardware.relay import RelayController +import threading +import time +import cv2 +import os +import sys +import io + + +class OutputRedirector(io.StringIO): + """ + 自定义输出重定向器,用于处理所有线程的输出 + 确保输入提示能在其他线程输出后重新显示,且保留用户当前输入 + """ + def __init__(self, original_stdout): + super().__init__() + self.original_stdout = original_stdout + self.input_prompt = "请输入新值:" + self.input_thread_active = False + self.current_input = "" # 跟踪用户当前输入 + self.lock = threading.Lock() + + def write(self, text): + with self.lock: + # 写入原始输出 + self.original_stdout.write(text) + self.original_stdout.flush() + + # 如果输入线程活跃,并且输出是换行符,重新显示输入提示和当前输入 + if self.input_thread_active and '\n' in text: + # 清除当前行并重新显示输入提示和用户当前输入 + self.original_stdout.write("\r" + " " * 100 + "\r") + self.original_stdout.write(f"{self.input_prompt}{self.current_input}") + self.original_stdout.flush() + + def flush(self): + with self.lock: + self.original_stdout.flush() + + +def main(): + # 加载配置 + # 初始化系统 + + + # replay_controller.control(replay_controller.DOOR_LOWER_OPEN, 'open') + # time.sleep(1) + # replay_controller.control(replay_controller.DOOR_LOWER_OPEN, 'close') + # replay_controller.control(replay_controller.DOOR_LOWER_CLOSE, 'close') + + # replay_controller.control_upper_open() + + # replay_controller.control(replay_controller.DOOR_LOWER_OPEN, 'open') + # time.sleep(5) + # replay_controller.control(replay_controller.DOOR_LOWER_OPEN, 'close') + + # image=cv2.imread(os.path.join(app_set_config.project_root,'test.jpeg')) + # image=cv2.flip(image, 0) + # cv2.imshow('test',image) + # cv2.waitKey(1) + + # replay_controller.control(system.relay_controller.DOOR_LOWER_OPEN, 'open') + + system = FeedingControlSystem() + # system.vision_detector.detect_angle() + # while True: + # system.feeding_controller.pulse_control_door_for_maintaining() + # time.sleep(5) + + # 初始设置 + system.state.vehicle_aligned=True + #假设在 fertilize room + system.state._upper_door_position='over_lower' + + # 创建输出重定向器 + original_stdout = sys.stdout + output_redirector = OutputRedirector(original_stdout) + sys.stdout = output_redirector + + # 创建输入线程函数 + def input_thread(): + import sys + import threading + + # 初始提示只显示一次 + initial_prompt = """ + 输入线程启动提示 + -------------------------- + 可以随时输入新值来更新系统状态 + 格式:需求重量,完成重量,溢出状态 + 例如:500,300,大堆料 或 500,300,小堆料 或 500,300,未堆料 + 输入'q'退出程序 + -------------------------- + """ + print(initial_prompt) + + # 标记输入线程为活跃状态 + output_redirector.input_thread_active = True + + while True: + try: + import termios + import tty + + # 获取当前终端设置 + old_settings = termios.tcgetattr(sys.stdin) + try: + tty.setcbreak(sys.stdin.fileno()) # 设置为cbreak模式,允许逐字符读取 + + # 显示初始提示和当前输入 + sys.stdout.write("\r" + " " * 100 + "\r") + sys.stdout.write(f"{output_redirector.input_prompt}{output_redirector.current_input}") + sys.stdout.flush() + + while True: + char = sys.stdin.read(1) # 读取一个字符 + + if char == '\x03': # Ctrl+C + raise KeyboardInterrupt + elif char in ['\r', '\n']: # 回车键 + sys.stdout.write("\n") + sys.stdout.flush() + break + elif char == '\x7f': # 退格键 + if len(output_redirector.current_input) > 0: + output_redirector.current_input = output_redirector.current_input[:-1] + # 从显示中删除最后一个字符 + sys.stdout.write("\b \b") + sys.stdout.flush() + else: + output_redirector.current_input += char + sys.stdout.write(char) + sys.stdout.flush() + finally: + # 恢复终端设置 + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings) + + # 获取最终输入并重置当前输入 + user_input = output_redirector.current_input + output_redirector.current_input = "" # 重置为下一次输入准备 + + if user_input.lower() == 'q': + print("\n收到退出信号,正在关闭系统...") + output_redirector.input_thread_active = False + system.stop() + break + + # 分割输入值,处理不同参数数量 + input_parts = [part.strip() for part in user_input.split(',')] + if len(input_parts) >= 2: + # 更新基本参数 + system.state._mould_need_weight = float(input_parts[0]) + system.state._mould_finish_weight = float(input_parts[1]) + + if system.state._mould_finish_weight>400: + system.state.overflow_detected='大堆料' + + + # 输出更新结果,使用换行符分隔 + update_msg = f"\n已更新:\n 需求重量 = {system.state._mould_need_weight} kg\n 完成重量 = {system.state._mould_finish_weight} kg" + print(f"溢出状态 = {system.state.overflow_detected}") + print(update_msg) + else: + print("\n输入格式错误:至少需要输入需求重量和完成重量") + + except ValueError as e: + print(f"\n输入格式错误,请重新输入。错误信息:{e}") + except Exception as e: + print(f"\n发生错误:{e}") + output_redirector.input_thread_active = False + break + + # 启动输入线程 + input_thread = threading.Thread(target=input_thread, daemon=True) + input_thread.start() + system.relay_controller.control(system.relay_controller.DOOR_LOWER_OPEN, 'open') + # 启动视觉控制 + system.camera_controller.start_cameras() + system.start_visual_control() + + # 主线程保持运行 + while True: + # print(f'当前重量22:{system.state._mould_finish_weight:.2f}kg, 目标重量:{system.state._mould_need_weight:.2f}kg') + time.sleep(1) + + +if __name__ == "__main__": + main() diff --git a/test_dal_debug.py b/test_dal_debug.py new file mode 100644 index 0000000..0127847 --- /dev/null +++ b/test_dal_debug.py @@ -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为空列表") \ No newline at end of file diff --git a/test_door_close_startup.py b/test_door_close_startup.py new file mode 100644 index 0000000..7d90343 --- /dev/null +++ b/test_door_close_startup.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +测试脚本:模拟程序启动时门关闭的情况 +验证初始角度很小(门关闭)时的控制逻辑行为 +""" + +import time +from vision.visual_callback_1203 import VisualCallback +from datetime import datetime + +# 修改VisualCallback类,添加调试信息 +original_pulse_control = VisualCallback._pulse_control + +# 重写_pulse_control方法,添加详细调试信息 +def debug_pulse_control(self, action, duration): + print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} [DEBUG] 准备执行 {action} 脉冲,持续 {duration:.2f} 秒") + result = original_pulse_control(self, action, duration) + print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} [DEBUG] {action} 脉冲执行完成") + return result + +# 替换原始方法 +VisualCallback._pulse_control = debug_pulse_control + +def test_door_close_startup(): + """测试程序启动时门关闭的情况""" + print("=== 测试:程序启动时门关闭的情况 ===") + + # 创建VisualCallback实例(程序启动) + callback = VisualCallback() + + # 打印初始属性 + print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 初始angle_mode: {callback.angle_mode}") + print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 初始overflow: {callback.overflow}") + print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 初始_last_overflow_state: {callback._last_overflow_state}") + + # 模拟初始状态:门关闭(角度0°),无堆料 + print(f"\n{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 发送状态:门关闭(角度0°),无堆料") + callback.angle_visual_callback(0, "未堆料") + + # 等待一段时间,观察控制行为 + for i in range(3): + time.sleep(1) + print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 等待第{i+1}秒...") + + # 发送角度1°,无堆料 + print(f"\n{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 发送状态:角度1°,无堆料") + callback.angle_visual_callback(1, "未堆料") + + # 等待一段时间 + for i in range(3): + time.sleep(1) + print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 等待第{i+1}秒...") + + # 发送角度6°,无堆料(超过MIN_ANGLE) + print(f"\n{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 发送状态:角度6°,无堆料") + callback.angle_visual_callback(6, "未堆料") + + # 等待一段时间 + for i in range(3): + time.sleep(1) + print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 等待第{i+1}秒...") + + # 测试关闭机制 + callback.shutdown() + print("\n=== 测试完成 ===") + +if __name__ == "__main__": + test_door_close_startup() diff --git a/test_led.py b/test_led.py new file mode 100644 index 0000000..c056344 --- /dev/null +++ b/test_led.py @@ -0,0 +1,39 @@ +# main.py +import time +from hardware.relay import RelayController +from hardware.transmitter import TransmitterController +from service.mould_service import app_web_service +import time +# from LED_send.led_send import send_led_data + + +def main(): + + relay_c = RelayController() + transmitter_c = TransmitterController(relay_c) + while True: + led_info = app_web_service.get_pouring_led() + upper_weight=transmitter_c.read_data(1) + lower_weight=transmitter_c.read_data(2) + if led_info: + # 提取RingTypeCode,从第3个字符开始到"-"之前的部分 + if "-" in led_info.MouldCode: + # 找到"-"的位置 + dash_index = led_info.MouldCode.index("-") + # 从索引2开始提取到"-"之前的部分 + led_info.RingTypeCode = led_info.MouldCode[2:dash_index] + if led_info.MouldCode.find("F")>=0: + led_info.VibrationFrequency="4min/"+ led_info.VibrationFrequency + else: + led_info.VibrationFrequency="5min/"+ led_info.VibrationFrequency + + + led_info.UpperWeight=upper_weight + led_info.LowerWeight=lower_weight + # led_info.VibrationFrequency="5min/220hz" + send_led_data(led_info) + time.sleep(1) + + +if __name__ == "__main__": + main() diff --git a/test_overflow_state_change.py b/test_overflow_state_change.py new file mode 100644 index 0000000..e4fe7cc --- /dev/null +++ b/test_overflow_state_change.py @@ -0,0 +1,59 @@ +import time +from vision.visual_callback_1203 import VisualCallback + +# 测试堆料状态变化处理 +def test_overflow_state_change(): + print("=== 测试堆料状态变化处理 ===") + + # 获取单例实例 + callback = VisualCallback() + + # VisualCallback初始化时会自动启动线程 + time.sleep(1) # 等待线程初始化 + + try: + # 1. 初始状态:非堆料,应该立即处理 + print("\n1. 初始状态 - 非堆料") + callback.angle_visual_callback(30, "未堆料") + time.sleep(0.5) + + # 2. 变为堆料状态,应该立即处理(不受间隔限制) + print("\n2. 变为堆料状态") + callback.angle_visual_callback(30, "小堆料") + time.sleep(0.5) + + # 3. 保持堆料状态,应该立即处理(堆料优先) + print("\n3. 保持堆料状态") + callback.angle_visual_callback(30, "小堆料") + time.sleep(0.5) + + # 4. 从堆料变为非堆料,应该立即处理(状态变化) + print("\n4. 从堆料变为非堆料(状态变化)") + callback.angle_visual_callback(30, "未堆料") + time.sleep(0.5) + + # 5. 再次非堆料,应该受2秒间隔限制 + print("\n5. 再次非堆料(无状态变化)") + callback.angle_visual_callback(30, "未堆料") + time.sleep(0.5) + + # 6. 等待2秒后再次非堆料,应该处理 + print("\n6. 等待2秒后再次非堆料") + time.sleep(2) + callback.angle_visual_callback(30, "未堆料") + time.sleep(0.5) + + # 7. 从非堆料变为大堆料,应该立即处理 + print("\n7. 从非堆料变为大堆料") + callback.angle_visual_callback(30, "大堆料") + time.sleep(0.5) + + finally: + # 关闭线程 + callback.shutdown() + time.sleep(1) + + print("\n=== 测试完成 ===") + +if __name__ == "__main__": + test_overflow_state_change() diff --git a/test_visiton.zip b/test_visiton.zip new file mode 100644 index 0000000..27239c8 Binary files /dev/null and b/test_visiton.zip differ diff --git a/test_visiton/config/settings.py b/test_visiton/config/settings.py new file mode 100644 index 0000000..d5006ad --- /dev/null +++ b/test_visiton/config/settings.py @@ -0,0 +1,94 @@ +# config/settings.py +import os + +class Settings: + def __init__(self): + # 项目根目录 + self.project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + self.test_need_weight=2000 + # 网络继电器配置 + self.relay_host = '192.168.250.62' + self.relay_port = 50000 + + self.debug_feeding=False + #调试模式上,网络继点器禁用,模型推理启用 + self.debug_mode=False + + # 摄像头配置 + self.camera_type = "ip" + self.camera_ip = "192.168.1.51" + self.camera_port = 554 + self.camera_username = "admin" + self.camera_password = "XJ123456" + self.camera_channel = 1 + + self.camera_configs = { + # 'cam1': { + # 'type': 'ip', + # 'ip': '192.168.250.60', + # 'port': 554, + # 'username': 'admin', + # 'password': 'XJ123456', + # 'channel': 1 + # }, + 'cam2': { + 'type': 'ip', + 'ip': '192.168.250.61', + 'port': 554, + 'username': 'admin', + 'password': 'XJ123456', + 'channel': 1 + } + } + + # 下料控制参数 + self.min_required_weight = 500 # 模具车最小需要重量(kg) + self.target_vehicle_weight = 5000 # 目标模具车重量(kg) + self.upper_buffer_weight = 500 # 上料斗缓冲重量(kg) + self.single_batch_weight = 2500 # 单次下料重量(kg) + + + # 角度控制参数 + self.target_angle = 30.0 # 目标角度 + self.min_angle = 10.0 # 最小角度 + self.max_angle = 80.0 # 最大角度 + self.angle_threshold = 50.0 # 角度阈值 + self.angle_tolerance = 5.0 # 角度容差 + + # 变频器配置 + self.inverter_max_frequency = 400.0 # 频率最大值 + self.frequencies = [220.0, 230.0, 240.0] # 下料阶段频率(Hz) + + # 模型路径配置 + self.models_dir = os.path.join(self.project_root, 'vision') + self.angle_model_path = os.path.join(self.models_dir, 'obb_angle_model', 'obb.rknn') + self.overflow_model_path = os.path.join(self.models_dir,'overflow_model', 'yiliao_cls.rknn') + # self.alignment_model_path = os.path.join(self.models_dir, 'align_model', 'yolov11_cls_640v6.rknn') + + # ROI路径配置 + self.roi_file_path = os.path.join(self.models_dir, 'overflow_model', 'roi_coordinates', '1_rois.txt') + + # 系统控制参数 + self.visual_check_interval = 1.0 # 视觉检查间隔(秒) + self.alignment_check_interval = 0.5 # 对齐检查间隔(秒) + self.max_error_count = 3 # 最大错误计数 + self.lower_feeding_interval = 0.1 # 下料轮询间隔(秒) + + # RFID配置 + self.rfid_host = '192.168.1.190' + self.rfid_port = 6000 + + #是否在线生产 + self.is_online_control = True # 是否API在线 + #最后一块进行尾数控制 + # self.block_numbers=['B1','B2','B3','L1','L2','F'] + #需核实上下位漏斗容量 + self.max_upper_volume = 2.4 # 上料斗容量(方) + #下料到下料斗最大下到多少,并非最大容量 + self.max_lower_volume = 2.2 # 下料斗容量(方) + + #led + self.led_interval = 2 # LED闪烁间隔(秒) + +app_set_config = Settings() + diff --git a/test_visiton/hardware/relay.py b/test_visiton/hardware/relay.py new file mode 100644 index 0000000..fce64b6 --- /dev/null +++ b/test_visiton/hardware/relay.py @@ -0,0 +1,219 @@ +# hardware/relay.py +import socket +import binascii +import time +import threading +from pymodbus.client import ModbusTcpClient +from pymodbus.exceptions import ModbusException +from config.settings import app_set_config + + +class RelayController: + # 继电器映射 + RING = 'ring' # DO1 - 响铃 + UPPER_TO_JBL = 'upper_to_jbl' # DO2 - 上料斗到搅拌楼 + UPPER_TO_ZD = 'upper_to_zd' # DO3 - 上料斗到振捣室 + # DOOR_UPPER = 'door_upper' # DO0 - 上料斗滑动 + DOOR_LOWER_OPEN = 'door_lower_open' # DO1 - 下料斗出砼门开角度 + DOOR_LOWER_CLOSE = 'door_lower_close' # DO2 - 下料斗出砼门关角度(角度在7.5以下可关闭信号) + DOOR_UPPER_OPEN = 'door_upper_open' # DO3 - 上料斗开 + DOOR_UPPER_CLOSE = 'door_upper_close' # DO4 - 上料斗关 + BREAK_ARCH_UPPER = 'break_arch_upper' # DO3 - 上料斗震动 + BREAK_ARCH_LOWER = 'break_arch_lower' # DO4 - 下料斗震动 + DIRECT_LOWER_FRONT = 'direct_lower_front' # DO5 - 下料斗前 + DIRECT_LOWER_BEHIND = 'direct_lower_behind' # DO6 - 下料斗后 + DIRECT_LOWER_TOP = 'direct_lower_top' # DO7 - 下料斗上 + DIRECT_LOWER_BELOW = 'direct_lower_below' # DO8 - 下料斗下 + + def __init__(self, host='192.168.250.62', port=50000): + self.host = host + self.port = port + self.modbus_client = ModbusTcpClient(host, port=port) +#遥1 DO 7 左 DO8 右 角度 摇2:DO 15下 13上 12 往后 14往前 下料斗DO7开 D09关 + # 继电器命令(原始Socket) + self.relay_commands = { + self.RING: {'open': '00000000000601050000FF00', 'close': '000000000006010500000000'}, + self.UPPER_TO_JBL: {'open': '00000000000601050001FF00', 'close': '000000000006010500010000'}, + self.UPPER_TO_ZD: {'open': '00000000000601050002FF00', 'close': '000000000006010500020000'}, + self.DOOR_LOWER_OPEN: {'open': '00000000000601050006FF00', 'close': '000000000006010500060000'}, + self.DOOR_LOWER_CLOSE: {'open': '00000000000601050008FF00', 'close':'000000000006010500080000'}, + self.DOOR_UPPER_OPEN: {'open': '00000000000601050003FF00', 'close': '000000000006010500030000'}, + self.DOOR_UPPER_CLOSE: {'open': '00000000000601050004FF00', 'close': '000000000006010500040000'}, + self.BREAK_ARCH_UPPER: {'open': '0000000000060105000AFF00', 'close': '0000000000060105000A0000'}, + self.BREAK_ARCH_LOWER: {'open': '00000000000601050005FF00', 'close': '000000000006010500050000'}, + self.DIRECT_LOWER_FRONT: {'open': '0000000000060105000DFF00', 'close': '0000000000060105000D0000'}, + self.DIRECT_LOWER_BEHIND: {'open': '0000000000060105000BFF00', 'close': '0000000000060105000B0000'}, + self.DIRECT_LOWER_TOP: {'open': '0000000000060105000CFF00', 'close': '0000000000060105000C0000'}, + self.DIRECT_LOWER_BELOW: {'open': '0000000000060105000EFF00', 'close': '0000000000060105000E0000'} + } + + # 读取状态命令 + self.read_status_command = '000000000006010100000008' + + # 设备位映射 + self.device_bit_map = { + self.RING: 0, + self.UPPER_TO_JBL: 1, + self.UPPER_TO_ZD: 2, + self.BREAK_ARCH_UPPER: 3, + self.BREAK_ARCH_LOWER: 4 + } + + def send_command(self, command_hex): + """发送原始Socket命令""" + if app_set_config.debug_mode: + return None + + try: + byte_data = binascii.unhexlify(command_hex) + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.connect((self.host, self.port)) + sock.send(byte_data) + response = sock.recv(1024) + print(f"收到继电器响应: {binascii.hexlify(response)}") + return response + except Exception as e: + print(f"继电器通信错误: {e}") + return None + + def get_status(self): + """获取继电器状态""" + response = self.send_command(self.read_status_command) + status_dict = {} + + if response and len(response) >= 10: + status_byte = response[9] + status_bin = f"{status_byte:08b}"[::-1] + for key, bit_index in self.device_bit_map.items(): + status_dict[key] = status_bin[bit_index] == '1' + else: + print("读取继电器状态失败") + + return status_dict + + def control(self, device, action): + """控制继电器""" + if device in self.relay_commands and action in self.relay_commands[device]: + print(f"发送控制继电器命令 {device} {action}") + self.send_command(self.relay_commands[device][action]) + else: + print(f"无效设备或动作: {device}, {action}") + + def control_upper_close(self): + """控制上料斗关""" + # 关闭上料斗出砼门 + self.control(self.DOOR_UPPER_OPEN, 'close') + self.control(self.DOOR_UPPER_CLOSE, 'open') + # 异步5秒后关闭 + threading.Thread(target=self._close_upper_s, daemon=True,name="close_upper_s").start() + + def control_lower_close(self): + """控制下料斗关""" + # 关闭下料斗出砼门 + self.control(self.DOOR_LOWER_OPEN, 'close') + self.control(self.DOOR_LOWER_CLOSE, 'open') + time.sleep(5) + self.control(self.DOOR_LOWER_CLOSE, 'close') + # 异步5秒后关闭 + # threading.Thread(target=self._close_lower_5s, daemon=True,name="close_lower_5s").start() + + def control_upper_open_sync(self): + self.control(self.DOOR_UPPER_CLOSE, 'close') + self.control(self.DOOR_UPPER_OPEN, 'open') + + def control_upper_close_sync(self): + self.control(self.DOOR_LOWER_OPEN, 'close') + self.control(self.DOOR_LOWER_CLOSE, 'open') + + def control_upper_open(self): + #关闭信号才能生效 + self.control(self.DOOR_UPPER_CLOSE, 'close') + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.2) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.2) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(0.1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(0.1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(0.1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(0.1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + #保持8秒 + time.sleep(8) + #8秒后再开5秒 + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(0.1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(0.1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(0.1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(0.1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(0.1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + time.sleep(0.1) + self.control(self.DOOR_UPPER_OPEN, 'open') + time.sleep(0.5) + self.control(self.DOOR_UPPER_OPEN, 'close') + + def control_ring_open(self): + """控制下料斗关""" + # 关闭下料斗出砼门 + self.control(self.RING, 'open') + # 异步5秒后关闭 + threading.Thread(target=self._close_ring, daemon=True,name="close_ring").start() + + def _close_upper_s(self): + time.sleep(16) + self.control(self.DOOR_UPPER_CLOSE, 'close') + print("上料斗关闭完成") + + def _close_lower_5s(self): + time.sleep(6) + self.control(self.DOOR_LOWER_CLOSE, 'close') + + def _close_ring(self): + time.sleep(3) + self.control(self.RING, 'close') + + def close_all(self): + """关闭所有继电器""" + self.control(self.UPPER_TO_JBL, 'close') + self.control(self.UPPER_TO_ZD, 'close') + self.control(self.BREAK_ARCH_UPPER, 'close') + self.control(self.BREAK_ARCH_LOWER, 'close') + self.control(self.RING, 'close') + self.control(self.DOOR_LOWER_OPEN, 'close') + self.control(self.DOOR_LOWER_CLOSE, 'close') + self.control(self.DOOR_UPPER_OPEN, 'close') + self.control(self.DOOR_UPPER_CLOSE, 'close') diff --git a/test_visiton/vision/visual_callback.py b/test_visiton/vision/visual_callback.py new file mode 100644 index 0000000..7a708b2 --- /dev/null +++ b/test_visiton/vision/visual_callback.py @@ -0,0 +1,222 @@ + +from config.settings import app_set_config +from hardware.relay import RelayController +import time +import threading + +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.angle_mode = "normal" + self.relay_controller = RelayController() + self.init_weight = 100 + self.mould_finish_weight = 0 + self.mould_need_weight = 100 + self.finish_count = 0 + self.overflow = 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() + + # 创建并启动单个持续运行的线程 + self.callback_thread = threading.Thread( + target=self._run_thread_loop, + daemon=True + ) + self.callback_thread.start() + + self._initialized = True + + def angle_visual_callback(self, current_angle, overflow_detected): + """ + 视觉控制主逻辑,供外部推送数据 + 使用单个持续运行的线程,通过参数设置传递数据 + 如果线程正在处理数据,则丢弃此次推送 + """ + # 尝试获取处理锁,若失败则说明正在处理,丢弃数据 + if not self._is_processing.acquire(blocking=False): + print("回调线程仍在执行,丢弃此次推送数据") + return + + try: + # 更新参数 + self._current_angle = current_angle + self._overflow_detected = overflow_detected + # 通知线程有新数据可用 + self._new_data_available.set() + finally: + # 释放处理锁 + self._is_processing.release() + + + 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._process_angle_callback(current_angle, overflow_detected) + time.sleep(0.1) + + def _process_angle_callback(self, current_angle, overflow_detected): + """ + 内部方法,实际处理视觉回调逻辑 + 在异步线程中执行 + """ + try: + # print('current_angle:', current_angle, 'overflow_detected:', overflow_detected) + # return + # 检测溢出状态 + self.overflow = overflow_detected in ["大堆料", "小堆料"] + if current_angle is None: + print("无法获取当前角度,跳过本次调整") + return + + print(f"当前角度: {current_angle:.2f}°") + + if overflow_detected != "浇筑满": + # 状态机控制逻辑 + if self.angle_mode == "normal": + # 正常模式大于app_set_config.angle_threshold=60度 + if self.overflow: + self.angle_mode = "reducing" + else: + # 保持正常开门 + print(f'当前重量:{self.mould_finish_weight:.2f}kg, 目标重量:{self.mould_need_weight:.2f}kg') + if self.mould_need_weight > 0: + if self.mould_finish_weight / self.mould_need_weight >= 0.8: + print(f"完成重量占比{self.mould_finish_weight/self.mould_need_weight:.2f},半开出砼门") + # 半开出砼门 + if current_angle > app_set_config.target_angle: + # 角度已降至目标范围,关闭出砼门 + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') + time.sleep(0.3) + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + else: + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + time.sleep(0.32) + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + else: + # 全开砼门 + if current_angle > app_set_config.angle_threshold: + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + else: + # 全开砼门 + if current_angle > app_set_config.angle_threshold: + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + else: + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + elif self.angle_mode == "reducing": + # 角度减小模式 + if self.overflow: + if current_angle <= app_set_config.target_angle: + # 角度已达到目标范围,仍有堆料,进入维持模式 + print(f"角度已降至{current_angle:.2f}°,仍有堆料,进入维持模式") + if current_angle <= app_set_config.min_angle: + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + time.sleep(0.1) + self.angle_mode = "maintaining" + else: + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') + else: + # 无堆料,恢复正常模式 + print(f"角度已降至{current_angle:.2f}°,无堆料,恢复正常模式") + self.angle_mode = "normal" + + elif self.angle_mode == "maintaining": + # 维持模式 - 使用脉冲控制 + if not self.overflow: + # 堆料已消除,恢复正常模式 + print("堆料已消除,恢复正常模式") + self.angle_mode = "normal" + else: + # 继续维持角度控制 + print("进入维持模式") + # 关门时间 + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') + time.sleep(0.2) + # 开门时间 + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + time.sleep(0.25) + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + + elif self.angle_mode == "recovery": + # 恢复模式 - 逐步打开门 + if self.overflow: + # 又出现堆料,回到角度减小模式 + print("恢复过程中又检测到堆料,回到角度减小模式") + self.angle_mode = "maintaining" + else: + # 堆料已消除,恢复正常模式 + print("堆料已消除,恢复正常模式") + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self.angle_mode = "normal" + else: + # 浇筑满,关闭下料门 + self.relay_controller.control_lower_close() + except Exception as e: + print(f"处理视觉回调时发生异常: {e}") + + + @classmethod + def instance_exists(cls): + """检测实例是否存在""" + return cls._instance is not None + + def shutdown(self): + """关闭线程,清理资源""" + # 设置停止事件 + self._stop_event.set() + # 唤醒线程以便它能检测到停止事件 + self._new_data_available.set() + # 等待线程结束 + if self.callback_thread.is_alive(): + self.callback_thread.join(timeout=1.0) + +# 创建默认实例 +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) diff --git a/test_visual_callback.py b/test_visual_callback.py new file mode 100644 index 0000000..f15dbf2 --- /dev/null +++ b/test_visual_callback.py @@ -0,0 +1,56 @@ +import time +import threading +from vision.visual_callback import VisualCallback, angle_visual_callback + +# 创建VisualCallback实例 +visual_callback = VisualCallback() + +# 模拟safe_control_lower_close执行 +def simulate_safe_close(): + + + while True: + time.sleep(1) + """模拟safe_control_lower_close执行""" + print("\n=== 开始模拟safe_control_lower_close执行 ===") + + # 1. 首先,发送一些视觉回调数据,观察正常情况下的行为 + print("\n1. 正常运行阶段:") + + for i in range(5): + angle_visual_callback(50.0, "无堆料") + time.sleep(1) + + # 2. 模拟safe_control_lower_close开始执行 + print("\n2. 模拟safe_control_lower_close开始执行:") + visual_callback._is_safe_closing = True + + # 3. 再次发送视觉回调数据,观察是否会跳过relay操作 + print("\n3. safe_control_lower_close执行中:") + for i in range(5): + angle_visual_callback(50.0, "无堆料") + time.sleep(1) + + # 4. 模拟safe_control_lower_close执行完毕 + print("\n4. 模拟safe_control_lower_close执行完毕:") + visual_callback._is_safe_closing = False + + # 5. 再次发送视觉回调数据,观察是否恢复正常 + print("\n5. 恢复正常运行:") + for i in range(5): + angle_visual_callback(50.0, "无堆料") + time.sleep(1) + + print("\n=== 测试结束 ===") + visual_callback.shutdown() + +# 启动测试 +try: + simulate_safe_close() +except KeyboardInterrupt: + print("\n\n接收到Ctrl+C,正在停止服务...") +finally: + # 确保服务正确停止 + visual_callback.shutdown() + visual_callback.relay_controller.close_all() + print("服务已安全停止") diff --git a/test_weight.py b/test_weight.py new file mode 100644 index 0000000..880ad21 --- /dev/null +++ b/test_weight.py @@ -0,0 +1,25 @@ +# main.py +import time +from datetime import datetime +from hardware import relay +from hardware.relay import RelayController +from hardware.transmitter import TransmitterController +import time + + +def main(): + relay_c = RelayController() + transmitter_c = TransmitterController(relay_c) + while True: + data = '上料斗:' + str(int(transmitter_c.read_data(1))) + ',' +"下料斗:" + str(int(transmitter_c.read_data(2))) + timestamp = datetime.now().strftime('%H:%M:%S.%f')[:-3] + output = f"[{timestamp}] {data}" + print(output) + # 保存到文件 + with open('output.log', 'a') as f: + f.write(output + '\n') + time.sleep(0.5) + + +if __name__ == "__main__": + main() diff --git a/tests/485test.py b/tests/485test.py new file mode 100644 index 0000000..c362c3d --- /dev/null +++ b/tests/485test.py @@ -0,0 +1,339 @@ +import serial +import time +import struct + + +class InovanceMD520: + def __init__(self, port='COM4', baudrate=9600, timeout=1): + """ + 初始化汇川MD520变频器通信 + :param port: 串口名称,Windows为COMx,Linux为/dev/ttyUSBx + :param baudrate: 波特率,默认9600 + :param timeout: 超时时间,秒 + """ + self.port = port + self.baudrate = baudrate + self.timeout = timeout + self.ser = None + + def connect(self): + """连接串口""" + try: + self.ser = serial.Serial( + port=self.port, + baudrate=self.baudrate, + bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + timeout=self.timeout + ) + print(f"成功连接到串口 {self.port}") + return True + except serial.SerialException as e: + print(f"连接串口失败: {e}") + return False + + def disconnect(self): + """断开串口连接""" + if self.ser and self.ser.is_open: + self.ser.close() + print("串口连接已关闭") + + def calculate_crc(self, data): + """ + 计算Modbus CRC16校验码 + :param data: 字节数据 + :return: CRC校验码(低位在前,高位在后) + """ + crc = 0xFFFF + for byte in data: + crc ^= byte + for _ in range(8): + if crc & 0x0001: + crc = (crc >> 1) ^ 0xA001 + else: + crc = crc >> 1 + return struct.pack('> 8) & 0xFF, # 寄存器地址高字节 + register_addr & 0xFF, # 寄存器地址低字节 + (register_count >> 8) & 0xFF, # 寄存器数量高字节 + register_count & 0xFF # 寄存器数量低字节 + ]) + + # 计算CRC + crc = self.calculate_crc(cmd_data) + full_cmd = cmd_data + crc + + print(f"发送读取指令: {full_cmd.hex().upper()}") + + try: + self.ser.reset_input_buffer() + self.ser.write(full_cmd) + time.sleep(0.01) + + # 计算预期响应长度 + expected_length = 5 + 2 * register_count # 地址1 + 功能码1 + 字节数1 + 数据2*N + CRC2 + response = self.ser.read(expected_length) + + if len(response) < expected_length: + print(f"响应数据长度不足: {len(response)} 字节,期望 {expected_length} 字节") + return None + + print(f"收到响应: {response.hex().upper()}") + + # 验证CRC + received_crc = response[-2:] + calculated_crc = self.calculate_crc(response[:-2]) + if received_crc != calculated_crc: + print("CRC校验失败") + return None + + # 解析数据 + data_length = response[2] + data_bytes = response[3:3 + data_length] + + results = [] + for i in range(0, len(data_bytes), 2): + value = (data_bytes[i] << 8) | data_bytes[i + 1] + results.append(value) + + return results + + except Exception as e: + print(f"通信错误: {e}") + return None + + +def main(): + # 创建变频器对象 + inverter = InovanceMD520(port='COM3', baudrate=9600) + + # 连接串口 + if not inverter.connect(): + return + + try: + while True: + print("\n" + "=" * 50) + print("汇川MD520变频器频率查询") + print("=" * 50) + + # 设置允许频率 + frequency = inverter.set_frequency(slave_addr=0x01, frequency=210.0) + if frequency is not None: + print(f"✅ 设置成功") + else: + print("❌ 频率设置失败") + + # 查询运行频率 + frequency = inverter.query_frequency(slave_addr=0x01) + + if frequency is not None: + print(f"✅ 当前运行频率: {frequency:.2f} Hz") + else: + print("❌ 频率查询失败") + + + + # 可选:读取其他监控参数 + print("\n--- 其他监控参数 ---") + + # 读取母线电压 (地址1002H) + voltage_data = inverter.read_register(0x01, 0x1002) + if voltage_data: + voltage = voltage_data[0] / 10.0 # 单位0.1V + print(f"母线电压: {voltage:.1f} V") + + # 读取输出电压 (地址1003H) + output_voltage_data = inverter.read_register(0x01, 0x1003) + if output_voltage_data: + output_voltage = output_voltage_data[0] # 单位1V + print(f"输出电压: {output_voltage} V") + + # 读取输出电流 (地址1004H) + current_data = inverter.read_register(0x01, 0x1004) + if current_data: + current = current_data[0] / 100.0 # 单位0.01A + print(f"输出电流: {current:.2f} A") + + # 等待5秒后再次查询 + print("\n等待5秒后继续查询...") + time.sleep(5) + + except KeyboardInterrupt: + print("\n用户中断查询") + finally: + # 断开连接 + inverter.disconnect() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/Investor485test.py b/tests/Investor485test.py new file mode 100644 index 0000000..2de5d74 --- /dev/null +++ b/tests/Investor485test.py @@ -0,0 +1,259 @@ +import serial +import time +import struct + + +class InovanceMD520: + def __init__(self, port='COM4', baudrate=9600, timeout=1): + """ + 初始化汇川MD520变频器通信 + :param port: 串口名称,Windows为COMx,Linux为/dev/ttyUSBx + :param baudrate: 波特率,默认9600 + :param timeout: 超时时间,秒 + """ + self.port = port + self.baudrate = baudrate + self.timeout = timeout + self.ser = None + + def connect(self): + """连接串口""" + try: + self.ser = serial.Serial( + port=self.port, + baudrate=self.baudrate, + bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + timeout=self.timeout + ) + print(f"成功连接到串口 {self.port}") + return True + except serial.SerialException as e: + print(f"连接串口失败: {e}") + return False + + def disconnect(self): + """断开串口连接""" + if self.ser and self.ser.is_open: + self.ser.close() + print("串口连接已关闭") + + def calculate_crc(self, data): + """ + 计算Modbus CRC16校验码 + :param data: 字节数据 + :return: CRC校验码(低位在前,高位在后) + """ + crc = 0xFFFF + for byte in data: + crc ^= byte + for _ in range(8): + if crc & 0x0001: + crc = (crc >> 1) ^ 0xA001 + else: + crc = crc >> 1 + return struct.pack('> 8) & 0xFF, # 寄存器地址高字节 + register_addr & 0xFF, # 寄存器地址低字节 + (register_count >> 8) & 0xFF, # 寄存器数量高字节 + register_count & 0xFF # 寄存器数量低字节 + ]) + + # 计算CRC + crc = self.calculate_crc(cmd_data) + full_cmd = cmd_data + crc + + print(f"发送读取指令: {full_cmd.hex().upper()}") + + try: + self.ser.reset_input_buffer() + self.ser.write(full_cmd) + time.sleep(0.01) + + # 计算预期响应长度 + expected_length = 5 + 2 * register_count # 地址1 + 功能码1 + 字节数1 + 数据2*N + CRC2 + response = self.ser.read(expected_length) + + if len(response) < expected_length: + print(f"响应数据长度不足: {len(response)} 字节,期望 {expected_length} 字节") + return None + + print(f"收到响应: {response.hex().upper()}") + + # 验证CRC + received_crc = response[-2:] + calculated_crc = self.calculate_crc(response[:-2]) + if received_crc != calculated_crc: + print("CRC校验失败") + return None + + # 解析数据 + data_length = response[2] + data_bytes = response[3:3 + data_length] + + results = [] + for i in range(0, len(data_bytes), 2): + value = (data_bytes[i] << 8) | data_bytes[i + 1] + results.append(value) + + return results + + except Exception as e: + print(f"通信错误: {e}") + return None + + +def main(): + # 创建变频器对象 + inverter = InovanceMD520(port='COM3', baudrate=9600) + + # 连接串口 + if not inverter.connect(): + return + + try: + while True: + print("\n" + "=" * 50) + print("汇川MD520变频器频率查询") + print("=" * 50) + + # 查询运行频率 + frequency = inverter.query_frequency(slave_addr=0x01) + + if frequency is not None: + print(f"✅ 当前运行频率: {frequency:.2f} Hz") + else: + print("❌ 频率查询失败") + + # 可选:读取其他监控参数 + print("\n--- 其他监控参数 ---") + + # 读取母线电压 (地址1002H) + voltage_data = inverter.read_register(0x01, 0x1002) + if voltage_data: + voltage = voltage_data[0] / 10.0 # 单位0.1V + print(f"母线电压: {voltage:.1f} V") + + # 读取输出电压 (地址1003H) + output_voltage_data = inverter.read_register(0x01, 0x1003) + if output_voltage_data: + output_voltage = output_voltage_data[0] # 单位1V + print(f"输出电压: {output_voltage} V") + + # 读取输出电流 (地址1004H) + current_data = inverter.read_register(0x01, 0x1004) + if current_data: + current = current_data[0] / 100.0 # 单位0.01A + print(f"输出电流: {current:.2f} A") + + # 等待5秒后再次查询 + print("\n等待5秒后继续查询...") + time.sleep(5) + + except KeyboardInterrupt: + print("\n用户中断查询") + finally: + # 断开连接 + inverter.disconnect() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/hardware_test.py b/tests/hardware_test.py index ec501f3..63ddc6b 100644 --- a/tests/hardware_test.py +++ b/tests/hardware_test.py @@ -20,11 +20,12 @@ def test_relay_controls(relay): # 测试上料斗滑动门 print("测试上料斗滑动门开启...") - relay.control(relay.DOOR_UPPER, 'open') + relay.control(relay.UPPER_TO_JBL, 'open') + time.sleep(1) + relay.control(relay.UPPER_TO_JBL, 'close') time.sleep(1) - print("测试上料斗滑动门关闭...") - relay.control(relay.DOOR_UPPER, 'close') + relay.control(relay.UPPER_TO_ZD, 'open') time.sleep(1) # 测试上料斗出砼门 @@ -137,13 +138,13 @@ def main(): # 初始化控制器 print("初始化控制器...") relay = RelayController(host='192.168.0.18', port=50000) - inverter = InverterController(relay_controller=relay) - transmitter = TransmitterController(relay_controller=relay) + # inverter = InverterController(relay_controller=relay) + # transmitter = TransmitterController(relay_controller=relay) # 执行各项测试 test_relay_controls(relay) test_inverter_controls(inverter) - test_transmitter_reading(transmitter) + # test_transmitter_reading(transmitter) print("\n所有测试完成!") diff --git a/tests/test_feeding_process.py b/tests/test_feeding_process.py index fd8f25b..c45fe5c 100644 --- a/tests/test_feeding_process.py +++ b/tests/test_feeding_process.py @@ -3,6 +3,7 @@ import unittest from unittest.mock import patch, MagicMock import sys import os +from config.settings import app_set_config # 添加项目根目录到Python路径 sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) @@ -40,13 +41,13 @@ class TestFeedingProcess(unittest.TestCase): patch('feeding.process.InverterController'), \ patch('feeding.process.TransmitterController'): system = FeedingProcess() - # 通过settings修改参数 system.settings.single_batch_weight = 1500 - system.settings.min_required_weight = 300 - system.settings.target_vehicle_weight = 3000 + #修改参数 app_set_config.single_batch_weight = 1500 + app_set_config.min_required_weight = 300 + app_set_config.target_vehicle_weight = 3000 - self.assertEqual(system.settings.target_vehicle_weight, 3000) - self.assertEqual(system.settings.min_required_weight, 300) - self.assertEqual(system.settings.single_batch_weight, 1500) + self.assertEqual(app_set_config.target_vehicle_weight, 3000) + self.assertEqual(app_set_config.min_required_weight, 300) + self.assertEqual(app_set_config.single_batch_weight, 1500) if __name__ == '__main__': diff --git a/tests/test_relay_controller.py b/tests/test_relay_controller.py index ca030e8..5816aa4 100644 --- a/tests/test_relay_controller.py +++ b/tests/test_relay_controller.py @@ -13,7 +13,7 @@ class TestRelayController(unittest.TestCase): def setUp(self): """测试前的准备工作""" - self.relay_host = '192.168.0.18' + self.relay_host = '192.168.250.62' self.relay_port = 50000 self.relay = RelayController(host=self.relay_host, port=self.relay_port) @@ -24,7 +24,8 @@ class TestRelayController(unittest.TestCase): self.assertIsNotNone(self.relay.modbus_client) # 检查设备映射 - self.assertIn(RelayController.DOOR_UPPER, self.relay.device_bit_map) + self.assertIn(RelayController.UPPER_TO_JBL, self.relay.device_bit_map) + self.assertIn(RelayController.UPPER_TO_ZD, self.relay.device_bit_map) self.assertIn(RelayController.DOOR_LOWER_1, self.relay.device_bit_map) self.assertIn(RelayController.DOOR_LOWER_2, self.relay.device_bit_map) self.assertIn(RelayController.BREAK_ARCH_UPPER, self.relay.device_bit_map) @@ -62,7 +63,7 @@ class TestRelayController(unittest.TestCase): """测试控制有效设备和动作""" with patch.object(self.relay, 'send_command') as mock_send: # 测试打开上料斗门 - self.relay.control(RelayController.DOOR_UPPER, 'open') + self.relay.control(RelayController.UPPER_TO_JBL, 'open') mock_send.assert_called_once() def test_control_invalid_device(self): @@ -76,7 +77,7 @@ class TestRelayController(unittest.TestCase): """测试控制无效动作""" with patch.object(self.relay, 'send_command') as mock_send: # 测试无效动作 - self.relay.control(RelayController.DOOR_UPPER, 'invalid_action') + self.relay.control(RelayController.UPPER_TO_JBL, 'invalid_action') mock_send.assert_not_called() @patch.object(RelayController, 'send_command') @@ -89,7 +90,8 @@ class TestRelayController(unittest.TestCase): # 验证返回的状态字典 self.assertIsInstance(status, dict) - self.assertIn(RelayController.DOOR_UPPER, status) + self.assertIn(RelayController.UPPER_TO_JBL, status) + self.assertIn(RelayController.UPPER_TO_ZD, status) mock_send_command.assert_called_once_with(self.relay.read_status_command) @patch.object(RelayController, 'send_command') diff --git a/tests/test_rfid.py b/tests/test_rfid.py index 75ef5eb..c00ac0a 100644 --- a/tests/test_rfid.py +++ b/tests/test_rfid.py @@ -3,23 +3,58 @@ RFID """ import sys 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(raw_data): +rfid=None +rfid_before_mould='' +artifact_bll=ArtifactBll() +def test_data_callback(status,data): """ 测试用的数据接收回调函数 """ - print(f"[回调] 收到RFID数据: {raw_data}") + 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.1.190', port=6000) + rfid = rfid_service(host='192.168.250.67', port=6000) # print("=== RFID硬件测试开始 ===") @@ -35,12 +70,18 @@ def test_rfid_functions(): # mode_data = rfid.read_working_mode(address=0x00) # if mode_data: # print("读取到工作模式参数:") - # for key, value in mode_data.items(): + # for key, value in mode_data.items():. # print(f" {key}: {value:02X} ({value})") # rfid.set_working_mode(address=0x00, mode_params={ # 'word_num': 0x1E # }) + + # mode_data = rfid.read_working_mode(address=0x00) + # if mode_data: + # print("读取到工作模式参数:") + # for key, value in mode_data.items(): + # print(f" {key}: {value:02X} ({value})") # # 测试读取读写器信息 # print("\n3. 测试读取读写器信息:") @@ -53,8 +94,8 @@ def test_rfid_functions(): # print("读取读写器信息失败") # 测试设置功率 (仅演示,实际使用时根据需要调整) - # print("\n3. 测试设置功率 (演示,不实际执行):") - # power_success = rfid.set_power(address=0x00, power_value=6) + # print("\n3. 测试设置功率:") + # power_success = rfid.set_power(address=0x00, power_value=26) # print(f"功率设置{'成功' if power_success else '失败'}") # # 测试设置读卡间隔 (仅演示) @@ -85,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秒模拟接收过程 + # 等待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}") \ No newline at end of file diff --git a/tests/test_vision.py b/tests/test_vision.py new file mode 100644 index 0000000..b17c457 --- /dev/null +++ b/tests/test_vision.py @@ -0,0 +1,65 @@ +import os +import sys +# 添加项目根目录到Python路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +import unittest +from unittest.mock import MagicMock +from vision.camera import CameraController + +# from core.vision import Vision + +class TestVision(unittest.TestCase): + + def setUp(self): + self.jj=2 + # self.testclass2 = MagicMock() + # self.testclass = TestClass(self.testclass2) + + def test_capture_frame(self): + # 测试capture_frame方法 + camera=CameraController() + result = camera.capture_frame() + self.assertIsNone(result, msg="capture_frame方法测试失败") + camera.capture_frame_exec.assert_called_once() + + # def test_first(self): + # 测试TestClass的add方法 + # mock_testclass2 = MagicMock() + # # mock_testclass2.i = 1 + # # mock_testclass2.j = 2 + + # test_class = TestClass(mock_testclass2) + # result = test_class.add() + + # # 验证结果 + # self.assertEqual(result, 3, msg="add方法测试失败") + + # def test_second(self): + # 测试TestClass2的mock行为 + # mock_testclass2 = MagicMock(spec=TestClass2) + # mock_testclass2.sub.return_value = 1 + # result = mock_testclass2.sub() + + # # 测试返回值 + # self.assertEqual(result, 1, msg="sub方法测试失败") + # mock_testclass2.sub.assert_called_once() + + +class TestClass: + def __init__(self,testclass2): + self.testclass2 = testclass2 + pass + def add(self): + return self.testclass2.i + self.testclass2.j + +class TestClass2: + def __init__(self): + self.i = 1 + self.j = 2 + pass + + def sub(self): + return self.j - self.i + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/upper_plc.py b/upper_plc.py new file mode 100644 index 0000000..9582f6b --- /dev/null +++ b/upper_plc.py @@ -0,0 +1,405 @@ +import socket +import struct +import time +import threading +import logging +from enum import Enum +from typing import Optional, Callable + +class FinsServiceStatus(Enum): + """服务状态枚举""" + DISCONNECTED = "未连接" + CONNECTING = "连接中" + CONNECTED = "已连接" + POLLING = "轮询中" + ERROR = "错误" + STOPPED = "已停止" + +class FinsPoolFullError(Exception): + """连接池已满异常""" + pass + +class OmronFinsPollingService: + """欧姆龙FINS协议数据轮询服务(严格三指令版本)""" + + def __init__(self, plc_ip: str, plc_port: int = 9600): + """ + 初始化FINS服务 + + Args: + plc_ip: PLC IP地址 + plc_port: PLC端口,默认9600 + """ + self.plc_ip = plc_ip + self.plc_port = plc_port + + # 服务状态 + self._status = FinsServiceStatus.DISCONNECTED + self._socket: Optional[socket.socket] = None + + # 轮询控制 + self._polling_thread: Optional[threading.Thread] = None + self._stop_event = threading.Event() + self._polling_interval = 1.0 + + # 回调函数 + self._data_callbacks = [] + self._status_callbacks = [] + + # 最新数据 + self._latest_data: Optional[int] = None + self._last_update_time: Optional[float] = None + + # 配置日志 + self._setup_logging() + + def _setup_logging(self): + """配置日志""" + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + self.logger = logging.getLogger("FinsPollingService") + + def _update_status(self, new_status: FinsServiceStatus, message: str = ""): + """更新状态并触发回调""" + old_status = self._status + if old_status != new_status: + self._status = new_status + self.logger.info(f"状态变更: {old_status.value} -> {new_status.value} {message}") + + for callback in self._status_callbacks: + try: + callback(old_status, new_status, message) + except Exception as e: + self.logger.error(f"状态回调执行失败: {e}") + + def _send_and_receive(self, data: bytes, expected_length: int = 1024) -> bytes: + """发送数据并接收响应""" + if not self._socket: + raise ConnectionError("Socket未连接") + + print("_send_and_receive发送:",data) + self._socket.send(data) + response = self._socket.recv(expected_length) + return response + + def _check_handshake_response(self, response: bytes): + """检查握手响应""" + if len(response) < 24: + raise ConnectionError("握手响应数据不完整") + + # 检查响应头 + if response[0:4] != b'FINS': + raise ConnectionError("无效的FINS响应头") + + # 检查命令代码 + command_code = struct.unpack('>I', response[8:12])[0] + if command_code != 0x01: + raise ConnectionError(f"握手命令代码错误: 0x{command_code:08X}") + + # 检查错误代码 + error_code = struct.unpack('>I', response[12:16])[0] + if error_code == 0x20: + raise FinsPoolFullError("FINS连接池已满") + elif error_code != 0x00: + raise ConnectionError(f"握手错误代码: 0x{error_code:08X}") + + self.logger.info("握手成功") + + def _check_query_response(self, response: bytes) -> int: + """检查查询响应并返回数据""" + if len(response) < 30: + raise ConnectionError("查询响应数据不完整") + + # 检查响应头 + if response[0:4] != b'FINS': + raise ConnectionError("无效的FINS响应头") + + # 检查命令代码 + command_code = struct.unpack('>I', response[8:12])[0] + if command_code != 0x02: + raise ConnectionError(f"查询命令代码错误: 0x{command_code:08X}") + + # 检查错误代码 + error_code = struct.unpack('>I', response[12:16])[0] + if error_code != 0x00: + raise ConnectionError(f"查询错误代码: 0x{error_code:08X}") + + # 提取数据字节(最后一个字节) + data_byte = response[-1] + return data_byte + + def _check_logout_response(self, response: bytes): + """检查注销响应""" + if len(response) < 16: + raise ConnectionError("注销响应数据不完整") + + # 检查响应头 + if response[0:4] != b'FINS': + raise ConnectionError("无效的FINS响应头") + + # 检查命令代码 + command_code = struct.unpack('>I', response[8:12])[0] + if command_code != 0x03: + raise ConnectionError(f"注销命令代码错误: 0x{command_code:08X}") + + # 检查错误代码 + error_code = struct.unpack('>I', response[12:16])[0] + if error_code != 0x02: + raise ConnectionError(f"注销错误代码: 0x{error_code:08X}") + + self.logger.info("注销成功") + + def connect(self) -> bool: + """ + 连接到PLC并完成握手 + + Returns: + bool: 连接是否成功 + """ + if self._status == FinsServiceStatus.CONNECTED: + self.logger.warning("已经连接到PLC") + return True + + self._update_status(FinsServiceStatus.CONNECTING, "开始连接PLC") + + try: + # 创建socket连接 + self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._socket.settimeout(10.0) + self._socket.connect((self.plc_ip, self.plc_port)) + self.logger.info(f"TCP连接已建立: {self.plc_ip}:{self.plc_port}") + + # 指令1: 握手 + # 46 49 4E 53 00 00 00 0C 00 00 00 00 00 00 00 00 00 00 00 DC + handshake_cmd = bytes.fromhex("46 49 4E 53 00 00 00 0C 00 00 00 00 00 00 00 00 00 00 00 DC") + self.logger.debug("发送握手指令") + response = self._send_and_receive(handshake_cmd, 24) + + # 检查握手响应 + self._check_handshake_response(response) + + self._update_status(FinsServiceStatus.CONNECTED, "握手成功") + return True + + except FinsPoolFullError: + self._update_status(FinsServiceStatus.ERROR, "连接池已满") + raise + except Exception as e: + self._update_status(FinsServiceStatus.ERROR, f"连接失败: {e}") + if self._socket: + self._socket.close() + self._socket = None + raise + + def query_data(self) -> Optional[int]: + """ + 查询PLC数据 + + Returns: + int: 数据值(0-255) + """ + + if self._status != FinsServiceStatus.POLLING: + raise ConnectionError("未连接到PLC,无法查询") + + try: + # 指令2: 查询 + # 46 49 4E 53 00 00 00 1A 00 00 00 02 00 00 00 00 80 00 30 00 E9 00 00 DC 00 00 01 01 B0 00 00 00 00 01 + query_cmd = bytes.fromhex("46 49 4E 53 00 00 00 1A 00 00 00 02 00 00 00 00 80 00 30 00 E9 00 00 DC 00 00 01 01 B0 00 00 00 00 01") + self.logger.debug("发送查询指令") + response = self._send_and_receive(query_cmd, 1024) + + # 检查查询响应并提取数据 + data_byte = self._check_query_response(response) + + self._latest_data = data_byte + self._last_update_time = time.time() + + # 触发数据回调 + binary_str = bin(data_byte) + for callback in self._data_callbacks: + try: + callback(data_byte, binary_str) + except Exception as e: + self.logger.error(f"数据回调执行失败: {e}") + + self.logger.debug(f"查询成功: 数据=0x{data_byte:02X} ({binary_str})") + return data_byte + + except Exception as e: + self.logger.error(f"查询失败: {e}") + raise + + def disconnect(self): + """断开连接""" + if self._socket: + try: + # 指令3: 注销 + # 46 49 4E 53 00 00 00 10 00 00 00 02 00 00 00 00 DC E9 + logout_cmd = bytes.fromhex("46 49 4E 53 00 00 00 10 00 00 00 02 00 00 00 00 00 00 00 DC 00 00 00 E9") + self.logger.debug("发送注销指令") + response = self._send_and_receive(logout_cmd, 24) + + # 检查注销响应 + self._check_logout_response(response) + + except Exception as e: + self.logger.error(f"注销过程中出错: {e}") + finally: + self._socket.close() + self._socket = None + + self._update_status(FinsServiceStatus.DISCONNECTED, "连接已关闭") + + def _polling_loop(self): + """轮询循环""" + self.logger.info("数据轮询循环启动") + + while not self._stop_event.is_set(): + try: + if self._status == FinsServiceStatus.CONNECTED: + self._update_status(FinsServiceStatus.POLLING, "正在查询数据") + self.query_data() + self._update_status(FinsServiceStatus.CONNECTED, "查询完成") + else: + # 尝试重新连接 + try: + self.connect() + except FinsPoolFullError: + self.logger.error("连接池已满,等待后重试...") + time.sleep(5) + except Exception as e: + self.logger.error(f"连接失败: {e}, 等待后重试...") + time.sleep(2) + + except Exception as e: + self.logger.error(f"轮询查询失败: {e}") + self._update_status(FinsServiceStatus.ERROR, f"查询错误: {e}") + # 查询失败不影响连接状态,保持CONNECTED状态 + self._update_status(FinsServiceStatus.CONNECTED, "准备下一次查询") + time.sleep(1) + + # 等待轮询间隔 + self._stop_event.wait(self._polling_interval) + + self.logger.info("数据轮询循环停止") + + def start_polling(self, interval: float = 1.0): + """ + 启动数据轮询服务 + + Args: + interval: 轮询间隔(秒) + """ + if self._polling_thread and self._polling_thread.is_alive(): + self.logger.warning("轮询服务已在运行") + return + + self._polling_interval = interval + self._stop_event.clear() + + # 先建立连接 + try: + self.connect() + except Exception as e: + self.logger.error(f"初始连接失败: {e}") + # 继续启动轮询,轮询循环会尝试重连 + + # 启动轮询线程 + self._polling_thread = threading.Thread(target=self._polling_loop, daemon=True) + self._polling_thread.start() + self.logger.info(f"数据轮询服务已启动,间隔: {interval}秒") + + def stop_polling(self): + """停止数据轮询服务""" + self.logger.info("正在停止数据轮询服务...") + self._stop_event.set() + + if self._polling_thread and self._polling_thread.is_alive(): + self._polling_thread.join(timeout=5.0) + + self.disconnect() + self._update_status(FinsServiceStatus.STOPPED, "服务已停止") + self.logger.info("数据轮询服务已停止") + + # === 公共接口 === + + def get_service_status(self) -> dict: + """获取服务状态""" + return { + 'status': self._status.value, + 'is_connected': self._status == FinsServiceStatus.CONNECTED, + 'is_polling': self._polling_thread and self._polling_thread.is_alive(), + 'latest_data': self._latest_data, + 'latest_data_binary': bin(self._latest_data) if self._latest_data is not None else None, + 'last_update_time': self._last_update_time, + 'plc_ip': self.plc_ip, + 'plc_port': self.plc_port, + 'polling_interval': self._polling_interval + } + + def get_latest_data(self) -> Optional[int]: + """获取最新数据""" + return self._latest_data + + def get_latest_data_binary(self) -> Optional[str]: + """获取最新数据的二进制表示""" + return bin(self._latest_data) if self._latest_data is not None else None + + # === 回调注册接口 === + + def register_data_callback(self, callback: Callable[[int, str], None]): + """注册数据更新回调""" + self._data_callbacks.append(callback) + + def register_status_callback(self, callback: Callable[[FinsServiceStatus, FinsServiceStatus, str], None]): + """注册状态变化回调""" + self._status_callbacks.append(callback) + + def __enter__(self): + """上下文管理器入口""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """上下文管理器出口,确保资源释放""" + self.stop_polling() + + +# 使用示例和测试 +if __name__ == "__main__": + def on_data_update(data: int, binary: str): + #4即将振捣室5振捣室 64即将搅拌楼 66到达搅拌楼 + print(f"[数据回调] 数值: 0x{data:02X} | 十进制: {data:3d} | 二进制: {binary}") + + def on_status_change(old_status: FinsServiceStatus, new_status: FinsServiceStatus, message: str): + print(f"[状态回调] {old_status.value} -> {new_status.value} : {message}") + + # 创建服务实例 + service = OmronFinsPollingService("192.168.250.233") # 替换为实际PLC IP + + # 注册回调 + service.register_data_callback(on_data_update) + service.register_status_callback(on_status_change) + + print("欧姆龙FINS数据轮询服务") + print("=" * 50) + + try: + # 启动轮询服务,每2秒查询一次 + service.start_polling(interval=2.0) + + # 主循环,定期显示服务状态 + counter = 0 + while True: + status = service.get_service_status() + counter += 1 + time.sleep(1) + + except KeyboardInterrupt: + print("\n\n接收到Ctrl+C,正在停止服务...") + finally: + # 确保服务正确停止 + service.stop_polling() + print("服务已安全停止") \ No newline at end of file diff --git a/upper_to_lower.py b/upper_to_lower.py new file mode 100644 index 0000000..57f27a8 --- /dev/null +++ b/upper_to_lower.py @@ -0,0 +1,159 @@ +# main.py +import time +from config.settings import app_set_config +from core.system import FeedingControlSystem +from hardware import relay +from hardware.relay import RelayController +from hardware.inverter import InverterController +from hardware.transmitter import TransmitterController +import threading +import time +import cv2 +import vision.visual_callback_1203 as angle_visual + + +def main(): + # 加载配置 + # 初始化系统 + # angle_visual.angle_visual_callback(2,'未堆料') + replay_controller=RelayController() + # transmitter_controller=TransmitterController(replay_controller) + # upper_weight=transmitter_controller.read_data(2) + # print(upper_weight) + + + + replay_controller.close_all() + # replay_controller.control(replay_controller.DOOR_UPPER_OPEN, 'open') + + + # replay_controller.control(replay_controller.DOOR_UPPER_OPEN, 'close') + # time.sleep(0.4) + # replay_controller.control(replay_controller.DOOR_LOWER_OPEN, 'close') + # replay_controller.control(replay_controller.DOOR_LOWER_CLOSE, 'open') + # time.sleep(0.4) + # replay_controller.control(replay_controller.DOOR_LOWER_CLOSE, 'close') + + # replay_controller.control(replay_controller.DOOR_UPPER_OPEN, 'open') + # time.sleep(4) + # while True: + # # replay_controller.control(replay_controller.DOOR_UPPER_OPEN, 'close') + # time.sleep(2) + # replay_controller.control(replay_controller.DOOR_UPPER_OPEN, 'open') + # time.sleep(0.1) + + # time.sleep(0.5) + # replay_controller.control(replay_controller.DOOR_UPPER_OPEN, 'open') + # time.sleep(0.5) + # replay_controller.control(replay_controller.DOOR_UPPER_OPEN, 'close') + # time.sleep(0.5) + # replay_controller.control(replay_controller.DOOR_UPPER_OPEN, 'open') + # time.sleep(0.5) + # replay_controller.control(replay_controller.DOOR_UPPER_OPEN, 'close') + + # time.sleep(3) + + + + # inverter_controller=InverterController(replay_controller) + # inverter_controller.control('start') + + # inverter_controller.control('stop') + + + + # return + # replay_controller.control_upper_open() + #3秒开关 + # replay_controller.control(replay_controller.DOOR_LOWER_OPEN, 'open') + # time.sleep(1) + # replay_controller.control(replay_controller.DOOR_LOWER_OPEN, 'close') + + # replay_controller.control(replay_controller.DOOR_LOWER_CLOSE, 'open') + # time.sleep(1) + # replay_controller.control(replay_controller.DOOR_LOWER_CLOSE, 'close') + + + # while True: + # time.sleep(1) + + # replay_controller.control(system.relay_controller.DOOR_LOWER_OPEN, 'open') + + # system = FeedingControlSystem() + + # system.state.vehicle_aligned=True + #假设在振捣室 + # system.state._upper_door_position='over_lower' + # system.relay_controller.control(system.relay_controller.DOOR_LOWER_OPEN, 'open') + # system.initialize() + + # system.state._feed_status=FeedStatus.FCheckM + + # cv2.namedWindow("控制系统", cv2.WND_PROP_VISIBLE) + # cv2.setWindowProperty("控制系统", cv2.WND_PROP_VISIBLE, 0) + # while True: + # key = cv2.waitKey(100) & 0xFF + # if key == ord('q') or key == 27: # 'q'键或ESC键 + # print("接收到退出信号,正在关闭系统...") + # break + + # time.sleep(1) # 减少CPU占用 + + # replay_controller.close_all() + + + # system.relay_controller.control(system.relay_controller.DOOR_UPPER_OPEN, 'open') + + # system.relay_controller.control(system.relay_controller.DOOR_UPPER_CLOSE, 'close') + # system.relay_controller.control(system.relay_controller.DOOR_UPPER_OPEN, 'open') + # time.sleep(5) + # system.relay_controller.control_upper_close() + + + # system.relay_controller.control(system.relay_controller, 'close') + + # system.relay_controller.control(system.relay_controller.DOOR_UPPER_OPEN, 'open') + + # system.camera_controller.start_cameras() + # loc_all_close=True + # if loc_all_close: + # relay=RelayController() + # relay.control(relay.UPPER_TO_JBL, 'close') + # relay.control(relay.UPPER_TO_ZD, 'close') + # relay.control(relay.DOOR_LOWER_OPEN, 'close') + # relay.control(relay.DOOR_LOWER_CLOSE, 'close') + # relay.control(relay.DOOR_UPPER_OPEN, 'close') + # relay.control(relay.DOOR_UPPER_CLOSE, 'close') + # relay.control(relay.BREAK_ARCH_UPPER, 'close') + # relay.control(relay.BREAK_ARCH_LOWER, 'close') + + + # time.sleep(2) + # system._alignment_check_loop() + # system.relay_controller.control(system.relay_controller.DOOR_LOWER_OPEN, 'open') + # time.sleep(2) + # system.relay_controller.control(system.relay_controller.DOOR_UPPER_OPEN, 'close') + + # time.sleep(2) + # system.relay_controller.control(system.relay_controller.DOOR_LOWER_CLOSE, 'open') + # time.sleep(2) + # system.relay_controller.control(system.relay_controller.DOOR_UPPER_CLOSE, 'close') + + # system.relay_controller.control(system.relay_controller.DOOR_LOWER_CLOSE, 'open') + # time.sleep(5) + # system.relay_controller.control(system.relay_controller.DOOR_LOWER_CLOSE, 'close') + # system._visual_control_loop() + # system.transmitter_controller.test_upper_weight=2*2500 + # system.transmitter_controller.test_lower_weight=1000 + # system.state.vehicle_aligned=True + # # 启动调整线程 + # weight_thread = threading.Thread(target=adjust_weights, args=(system,), daemon=True) + + # weight_thread.start() + # system.state._upper_door_position='over_lower' + # system._start_lower_feeding() + + + +if __name__ == "__main__": + main() diff --git a/view/main_window.py b/view/main_window.py index 826470f..0c3fc8f 100644 --- a/view/main_window.py +++ b/view/main_window.py @@ -112,39 +112,73 @@ class MainWindow(QWidget): self.bottom_control_widget = BottomControlWidget() # 最下方: 控制的按钮 (系统诊断、系统中心等) def initSubWidgets(self): + # 初始化派单任务的 任务id self.dispatch_task_widget.set_task_id("task1", "PD0001") self.dispatch_task_widget.set_task_id("task2", "PD0002") self.dispatch_task_widget.set_task_id("task3", "PD0003") - # 读取数据库,初始化 管片任务的数据 - from busisness.blls import ArtifactBll, PDRecordBll - artifact_dal = ArtifactBll() - artifacts = artifact_dal.get_artifact_task() - # print("\n打印artifacts数据:") - for i, artifact in enumerate(artifacts): - # 如果是数据类对象,转换为字典输出 - # print(artifact.MouldCode) - # if hasattr(artifact, "__dataclass_fields__"): - # print(f"第{i+1}条: {artifact.__dict__}") - # else: - # print(f"第{i+1}条: {artifact}") - self.segment_task_widget.set_task_id(f"task{i + 1}", artifact.MouldCode) - self.segment_task_widget.set_task_volume(f"task{i + 1}", artifact.BetonVolume) - pass + # 初始化 管片任务 和 派单任务显示的数据 + self.update_segment_tasks() + self.update_dispatch_tasks() - pdrecord_dal = PDRecordBll() - pdrecords = pdrecord_dal.get_PD_record() - # print(pdrecords[0].MouldCode) - # print("\n打印pdrecords数据:") - for i, record in enumerate(pdrecords): - # 如果是数据类对象,转换为字典输出 - # print(record.__dict__["MouldCode"]) - # if hasattr(record, "__dataclass_fields__"): - # print(f"第{i+1}条: {record.__dict__}") - # else: - # print(f"第{i+1}条: {record}") - self.dispatch_task_widget.set_task_volume(f"task{i + 1}", record.BetonVolume) - pass + def update_segment_tasks(self): + """从数据库中读取管片任务数据并更新到UI""" + def convert_to_ampm(time_str: str) -> str: + """ + 将时间转换为"hh:mmAM/PM"形式(如03:22PM) + Args: + time_str: 原始时间字符串" + Returns: + 转换后的时间字符串(如"03:22PM") + """ + from datetime import datetime + # 可能的时间格式(优先尝试带微秒的格式) + time_formats = [ + "%Y-%m-%d %H:%M:%S", # 不带微秒 + "%Y-%m-%d %H:%M:%S.%f" # 带微秒(如.528453) + ] + + for fmt in time_formats: + try: + dt = datetime.strptime(time_str, fmt) + return dt.strftime("%I:%M%p") # 转换为12小时制时分+AM/PM + except ValueError: + continue # 格式不匹配,尝试下一种格式 + + # 所有格式都不匹配时,返回占位符 + return "--:--" + + try: + from busisness.blls import ArtifactBll + artifact_dal = ArtifactBll() + artifacts = artifact_dal.get_artifact_task() # 获取管片任务数据 + + # 遍历数据并更新UI + for i, artifact in enumerate(artifacts): + # 更新任务ID和方量到管片任务 + self.segment_task_widget.set_task_id(f"task{i + 1}", artifact.MouldCode) + self.segment_task_widget.set_task_volume(f"task{i + 1}", artifact.BetonVolume) + if artifact.BeginTime: # 更新时间 + # print("artifact.BeginTime: ", artifact.BeginTime) + self.segment_task_widget.set_task_time(f"task{i + 1}", convert_to_ampm(artifact.BeginTime)) + + except Exception as e: + print(f"更新管片任务数据失败: {e}") + + def update_dispatch_tasks(self): + """从数据库中读取派单任务数据并更新到UI""" + try: + from busisness.blls import PDRecordBll + pdrecord_dal = PDRecordBll() + pdrecords = pdrecord_dal.get_PD_record() # 获取派单任务数据 + + # 遍历数据并更新UI + for i, record in enumerate(pdrecords): + # 更新方量到派单任务widget + self.dispatch_task_widget.set_task_volume(f"task{i + 1}", record.BetonVolume) + + except Exception as e: + print(f"更新派单任务数据失败: {e}") def setupLayout(self): """设置垂直布局,从上到下排列部件""" diff --git a/vision/.py b/vision/.py new file mode 100644 index 0000000..7c4da50 --- /dev/null +++ b/vision/.py @@ -0,0 +1,137 @@ +# vision/camera.py +import cv2 + + +class CameraController: + def __init__(self): + self.camera = None + self.camera_type = "ip" + self.camera_ip = "192.168.1.51" + self.camera_port = 554 + self.camera_username = "admin" + self.camera_password = "XJ123456" + self.camera_channel = 1 + + def set_config(self, camera_type="ip", ip=None, port=None, username=None, password=None, channel=1): + """ + 设置摄像头配置 + """ + self.camera_type = camera_type + if ip: + self.camera_ip = ip + if port: + self.camera_port = port + if username: + self.camera_username = username + if password: + self.camera_password = password + self.camera_channel = channel + + def setup_capture(self, camera_index=0): + """ + 设置摄像头捕获 + """ + try: + rtsp_url = f"rtsp://{self.camera_username}:{self.camera_password}@{self.camera_ip}:{self.camera_port}/streaming/channels/{self.camera_channel}01" + self.camera = cv2.VideoCapture(rtsp_url) + + if not self.camera.isOpened(): + print(f"无法打开网络摄像头: {rtsp_url}") + return False + print(f"网络摄像头初始化成功,地址: {rtsp_url}") + return True + except Exception as e: + print(f"摄像头设置失败: {e}") + return False + + def capture_frame_exec(self): + """捕获当前帧并返回numpy数组,设置5秒总超时""" + try: + if self.camera is None: + print("摄像头未初始化") + return None + + # 设置总超时时间为5秒 + total_timeout = 5.0 # 5秒总超时时间 + start_time = time.time() + + # 跳20帧,获取最新图像 + frames_skipped = 0 + while frames_skipped < 20: + # 检查总超时 + if time.time() - start_time > total_timeout: + print("捕获图像总超时") + return None + self.camera.grab() + time.sleep(0.05) # 稍微增加延迟,确保有新帧到达 + frames_skipped += 1 + + # 尝试读取帧,使用同一超时计时器 + read_attempts = 0 + max_read_attempts = 3 + if self.camera.grab(): + while read_attempts < max_read_attempts: + # 使用同一个超时计时器检查 + if time.time() - start_time > total_timeout: + print("捕获图像总超时") + return None + + ret, frame = self.camera.retrieve() + if ret: + return frame + else: + print(f"尝试读取图像帧失败,重试 ({read_attempts+1}/{max_read_attempts})") + read_attempts += 1 + # 短暂延迟后重试 + time.sleep(0.05) + + print("多次尝试后仍无法捕获有效图像帧") + return None + except Exception as e: + print(f"图像捕获失败: {e}") + return None + + def capture_frame(self): + """捕获当前帧并返回numpy数组""" + try: + if self.camera is None: + # self.set_config() + self.setup_capture() + + + frame = self.capture_frame_exec() + if frame is not None: + return frame + else: + print("无法捕获图像帧") + return None + except Exception as e: + print(f"图像捕获失败: {e}") + return None + + def capture_frame_bak(self): + """捕获当前帧并返回numpy数组""" + try: + if self.camera is None: + print("摄像头未初始化") + return None + + ret, frame = self.camera.read() + if ret: + return frame + else: + print("无法捕获图像帧") + return None + except Exception as e: + print(f"图像捕获失败: {e}") + return None + + def release(self): + """释放摄像头资源""" + if self.camera is not None: + self.camera.release() + self.camera = None + + def __del__(self): + """析构函数,确保资源释放""" + self.release() diff --git a/vision/align_model/__init__.py b/vision/align_model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vision/align_model/aligment_inference.py b/vision/align_model/aligment_inference.py new file mode 100644 index 0000000..39b1695 --- /dev/null +++ b/vision/align_model/aligment_inference.py @@ -0,0 +1,167 @@ +import cv2 +import numpy as np +import platform +from .labels import labels # 确保这个文件存在 + + + +# ------------------- 核心:全局变量存储RKNN模型实例(确保只加载一次) ------------------- +# 初始化为None,首次调用时加载模型,后续直接复用 +_global_rknn_instance = None + +# device tree for RK356x/RK3576/RK3588 +DEVICE_COMPATIBLE_NODE = '/proc/device-tree/compatible' + +def get_host(): + # get platform and device type + system = platform.system() + machine = platform.machine() + os_machine = system + '-' + machine + if os_machine == 'Linux-aarch64': + try: + with open(DEVICE_COMPATIBLE_NODE) as f: + device_compatible_str = f.read() + if 'rk3562' in device_compatible_str: + host = 'RK3562' + elif 'rk3576' in device_compatible_str: + host = 'RK3576' + elif 'rk3588' in device_compatible_str: + host = 'RK3588' + else: + host = 'RK3566_RK3568' + except IOError: + print('Read device node {} failed.'.format(DEVICE_COMPATIBLE_NODE)) + exit(-1) + else: + host = os_machine + return host + +def get_top1_class_str(result): + """ + 从推理结果中提取出得分最高的类别,并返回字符串 + + 参数: + result (list): 模型推理输出结果(格式需与原函数一致,如 [np.ndarray]) + 返回: + str:得分最高类别的格式化字符串 + 若推理失败,返回错误提示字符串 + """ + if result is None: + print("Inference failed: result is None") + return + + # 解析推理输出(与原逻辑一致:展平输出为1维数组) + output = result[0].reshape(-1) + + # 获取得分最高的类别索引(np.argmax 直接返回最大值索引,比排序更高效) + top1_index = np.argmax(output) + + # 处理标签(确保索引在 labels 列表范围内,避免越界) + if 0 <= top1_index < len(labels): + top1_class_name = labels[top1_index] + else: + top1_class_name = "Unknown Class" # 应对索引异常的边界情况 + + # 5. 格式化返回字符串(包含索引、得分、类别名称,得分保留6位小数) + return top1_class_name + +def preprocess(raw_image, target_size=(640, 640)): + """ + 读取图像并执行预处理(BGR转RGB、调整尺寸、添加Batch维度) + + 参数: + image_path (str): 图像文件的完整路径(如 "C:/test.jpg" 或 "/home/user/test.jpg") + target_size (tuple): 预处理后图像的目标尺寸,格式为 (width, height),默认 (640, 640) + 返回: + img (numpy.ndarray): 预处理后的图像 + 异常: + FileNotFoundError: 图像路径不存在或无法读取时抛出 + ValueError: 图像读取成功但为空(如文件损坏)时抛出 + """ + # img = cv2.cvtColor(raw_image, cv2.COLOR_BGR2RGB) + # 调整尺寸 + img = cv2.resize(raw_image, target_size) + img = np.expand_dims(img, 0) # 添加batch维度 + + return img + +# ------------------- 新增:模型初始化函数(控制只加载一次) ------------------- +def init_rknn_model(model_path): + """ + 初始化RKNN模型(全局唯一实例): + - 首次调用:加载模型+初始化运行时,返回模型实例 + - 后续调用:直接返回已加载的全局实例,避免重复加载 + """ + from rknnlite.api import RKNNLite + + global _global_rknn_instance # 声明使用全局变量 + + # 若模型未加载过,执行加载逻辑 + if _global_rknn_instance is None: + # 1. 创建RKNN实例(关闭内置日志) + rknn_lite = RKNNLite(verbose=False) + + # 2. 加载RKNN模型 + ret = rknn_lite.load_rknn(model_path) + if ret != 0: + print(f'[ERROR] Load CLS_RKNN model failed (code: {ret})') + exit(ret) + + # 3. 初始化运行时(绑定NPU核心0) + ret = rknn_lite.init_runtime(core_mask=RKNNLite.NPU_CORE_0) + if ret != 0: + print(f'[ERROR] Init CLS_RKNN runtime failed (code: {ret})') + exit(ret) + + # 4. 将加载好的实例赋值给全局变量 + _global_rknn_instance = rknn_lite + print(f'[INFO] CLS_RKNN model loaded successfully (path: {model_path})') + + return _global_rknn_instance + +def yolov11_cls_inference(model_path, raw_image, target_size=(640, 640)): + """ + 根据平台进行推理,并返回最终的分类结果 + + 参数: + model_path (str): RKNN模型文件路径 + image_path (str): 图像文件的完整路径(如 "C:/test.jpg" 或 "/home/user/test.jpg") + target_size (tuple): 预处理后图像的目标尺寸,格式为 (width, height),默认 (640, 640) + """ + rknn_model = model_path + + img = preprocess(raw_image, target_size) + + rknn = init_rknn_model(rknn_model) + if rknn is None: + return None, img + outputs = rknn.inference([img]) + + # Show the classification results + class_name = get_top1_class_str(outputs) + + # rknn_lite.release() + + return class_name + +if __name__ == '__main__': + + # 调用yolov11_cls_inference函数(target_size使用默认值640x640,也可显式传参如(112,112)) + image_path = "/userdata/reenrr/inference_with_lite/cover_ready.jpg" + bgr_image = cv2.imread(image_path) + if bgr_image is None: + print(f"Failed to read image from {image_path}") + exit(-1) + + rgb_frame = cv2.cvtColor(bgr_image, cv2.COLOR_BGR2RGB) + print(f"Read image from {image_path}, shape: {rgb_frame.shape}") + + result = yolov11_cls_inference( + # model_path="/userdata/PyQt_main_test/app/view/yolo/yolov11_cls.rknn", + model_path="/userdata/chuyiwen/Feeding_control_system/vision/align_model/yolov11_cls_640v6.rknn", + raw_image=rgb_frame, + target_size=(640, 640) + ) + # 打印最终结果 + print(f"\n最终分类结果:{result}") + diff --git a/vision/align_model/labels.py b/vision/align_model/labels.py new file mode 100644 index 0000000..4ed38b9 --- /dev/null +++ b/vision/align_model/labels.py @@ -0,0 +1,6 @@ +# the labels come from synset.txt, download link: https://s3.amazonaws.com/onnx-model-zoo/synset.txt + +labels = \ +{0: 'cover_noready', + 1: 'cover_ready' +} \ No newline at end of file diff --git a/vision/align_model/yolo11_main.py b/vision/align_model/yolo11_main.py new file mode 100644 index 0000000..578ea9b --- /dev/null +++ b/vision/align_model/yolo11_main.py @@ -0,0 +1,93 @@ +# yolo11_main.py +import cv2 +import numpy as np +from collections import deque +import os + +# 导入模块(不是函数) +from .aligment_inference import yolov11_cls_inference + +# 模型路径 +CLS_MODEL_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "yolov11_cls_640v6.rknn") + +class ClassificationStabilizer: + """分类结果稳定性校验器,处理瞬时噪声帧""" + + def __init__(self, window_size=5, switch_threshold=2): + self.window_size = window_size # 滑动窗口大小(缓存最近N帧结果) + self.switch_threshold = switch_threshold # 状态切换需要连续N帧一致 + self.result_buffer = deque(maxlen=window_size) # 缓存最近结果 + self.current_state = "盖板未对齐" # 初始状态 + self.consecutive_count = 0 # 当前状态连续出现的次数 + + def stabilize(self, current_frame_result): + """ + 输入当前帧的分类结果,返回经过稳定性校验的结果 + Args: + current_frame_result: 当前帧的原始分类结果(str) + Returns: + str: 经过校验的稳定结果 + """ + # 1. 将当前帧结果加入滑动窗口 + self.result_buffer.append(current_frame_result) + + # 2. 统计窗口内各结果的出现次数(多数投票基础) + result_counts = {} + for res in self.result_buffer: + result_counts[res] = result_counts.get(res, 0) + 1 # 使用 result_counts 字典记录每个元素出现的总次数。 + + # 3. 找到窗口中出现次数最多的结果(候选结果) + candidate = max(result_counts, key=result_counts.get) + + # 4. 状态切换校验:只有候选结果连续出现N次才允许切换 + if candidate == self.current_state: + # 与当前状态一致,重置连续计数 + self.consecutive_count = 0 + else: + # 与当前状态不一致,累计连续次数 + self.consecutive_count += 1 + # 连续达到阈值,才更新状态 + if self.consecutive_count >= self.switch_threshold: + self.current_state = candidate + self.consecutive_count = 0 + + return self.current_state + +# 初始化稳定性校验器(全局唯一实例,确保状态连续) +cls_stabilizer = ClassificationStabilizer( + window_size=5, # 缓存最近5帧 + switch_threshold=2 # 连续2帧一致才切换状态 +) + +# ====================== 分类接口(可选,保持原逻辑) ====================== +def run_yolo_classification(rgb_frame): + """ + YOLO 图像分类接口函数 + Args: + rgb_frame: numpy array (H, W, 3), RGB 格式 + Returns: + str: 分类结果("盖板对齐" / "盖板未对齐" / "异常") + """ + if not isinstance(rgb_frame, np.ndarray): + print(f"[ERROR] 输入类型错误:需为 np.ndarray,当前为 {type(rgb_frame)}") + return "异常" + + try: + cover_cls = yolov11_cls_inference(CLS_MODEL_PATH, rgb_frame, target_size=(640, 640)) + except Exception as e: + print(f"[WARN] 分类推理失败: {e}") + cover_cls = "异常" + + raw_result = "盖板未对齐" # 默认值 + # 结果映射 + if cover_cls == "cover_ready": + raw_result = "盖板对齐" + elif cover_cls == "cover_noready": + raw_result = "盖板未对齐" + else: + raw_result = "异常" + # 通过稳定性校验器处理,返回最终结果 + stable_result = cls_stabilizer.stabilize(raw_result) + print("raw_result, stable_result:",raw_result, stable_result) + return stable_result + diff --git a/vision/align_model/yolov11_cls_640v6.rknn b/vision/align_model/yolov11_cls_640v6.rknn new file mode 100644 index 0000000..4d4f55f Binary files /dev/null and b/vision/align_model/yolov11_cls_640v6.rknn differ diff --git a/vision/alignment_detector.py b/vision/alignment_detector.py index b535c34..2ee0998 100644 --- a/vision/alignment_detector.py +++ b/vision/alignment_detector.py @@ -1,30 +1,35 @@ # vision/alignment_detector.py -def detect_vehicle_alignment(image_array, alignment_model): +from vision.align_model.yolo11_main import run_yolo_classification +def detect_vehicle_alignment(image_array): """ 通过图像检测模具车是否对齐 """ try: # 检查模型是否已加载 - if alignment_model is None: - print("对齐检测模型未加载") - return False - if image_array is None: print("输入图像为空") return False # 直接使用模型进行推理 - results = alignment_model(image_array) - pared_probs = results[0].probs.data.cpu().numpy().flatten() + # results = alignment_model(image_array) + # pared_probs = results[0].probs.data.cpu().numpy().flatten() - # 类别0: 未对齐, 类别1: 对齐 - class_id = int(pared_probs.argmax()) - confidence = float(pared_probs[class_id]) + # # 类别0: 未对齐, 类别1: 对齐 + # class_id = int(pared_probs.argmax()) + # confidence = float(pared_probs[class_id]) + + # # 只有当对齐且置信度>95%时才认为对齐 + # if class_id == 1 and confidence > 0.95: + # return True + # return False + + # 使用yolov11_cls_inference函数进行推理 + results = run_yolo_classification(image_array) + if results=="盖板对齐": + return True + else: + return False - # 只有当对齐且置信度>95%时才认为对齐 - if class_id == 1 and confidence > 0.95: - return True - return False except Exception as e: print(f"对齐检测失败: {e}") return False diff --git a/vision/anger_caculate.py b/vision/anger_caculate.py index 2a82dd3..997eba6 100644 --- a/vision/anger_caculate.py +++ b/vision/anger_caculate.py @@ -1,88 +1,235 @@ import cv2 -import os import numpy as np -from ultralytics import YOLO +import math +from shapely.geometry import Polygon +from rknnlite.api import RKNNLite +import os -def predict_obb_best_angle(model=None, model_path=None, image=None, image_path=None, save_path=None): +# ------------------- 全局配置变量 ------------------- +# 模型相关 +CLASSES = ['clamp'] +nmsThresh = 0.4 +objectThresh = 0.35 + +# 可视化与保存控制(全局变量,可外部修改) +DRAW_RESULT = True # 是否在输出图像上绘制旋转框 +SAVE_PATH = None # 保存路径,如 "./result.jpg";设为 None 则不保存 + +# RKNN 单例 +_rknn_instance = None + +# ------------------- RKNN 管理函数 ------------------- +def init_rknn(model_path): + """只加载一次 RKNN 模型""" + global _rknn_instance + if _rknn_instance is None: + _rknn_instance = RKNNLite(verbose=False) + ret = _rknn_instance.load_rknn(model_path) + if ret != 0: + print(f"[ERROR] Failed to load RKNN model: {ret}") + return None + ret = _rknn_instance.init_runtime(core_mask=RKNNLite.NPU_CORE_0) + if ret != 0: + print(f"[ERROR] Failed to init runtime: {ret}") + return None + return _rknn_instance + +def release_rknn(): + """释放 RKNN 对象""" + global _rknn_instance + if _rknn_instance: + _rknn_instance.release() + _rknn_instance = None + +# ------------------- 工具函数 ------------------- +def letterbox_resize(image, size, bg_color=114): + target_width, target_height = size + image_height, image_width, _ = image.shape + scale = min(target_width / image_width, target_height / image_height) + new_width, new_height = int(image_width * scale), int(image_height * scale) + image_resized = cv2.resize(image, (new_width, new_height), interpolation=cv2.INTER_AREA) + canvas = np.ones((target_height, target_width, 3), dtype=np.uint8) * bg_color + offset_x, offset_y = (target_width - new_width) // 2, (target_height - new_height) // 2 + canvas[offset_y:offset_y + new_height, offset_x:offset_x + new_width] = image_resized + return canvas, scale, offset_x, offset_y + +class DetectBox: + def __init__(self, classId, score, xmin, ymin, xmax, ymax, angle): + self.classId = classId + self.score = score + self.xmin = xmin + self.ymin = ymin + self.xmax = xmax + self.ymax = ymax + self.angle = angle + +def rotate_rectangle(x1, y1, x2, y2, a): + cx, cy = (x1 + x2) / 2, (y1 + y2) / 2 + cos_a, sin_a = math.cos(a), math.sin(a) + pts = [(x1, y1), (x1, y2), (x2, y2), (x2, y1)] + return [[int(cx + (xx - cx) * cos_a - (yy - cy) * sin_a), + int(cy + (xx - cx) * sin_a + (yy - cy) * cos_a)] for xx, yy in pts] + +def intersection(g, p): + g = Polygon(np.array(g).reshape(-1, 2)) + p = Polygon(np.array(p).reshape(-1, 2)) + if not g.is_valid or not p.is_valid: + return 0 + inter = g.intersection(p).area + union = g.area + p.area - inter + return 0 if union == 0 else inter / union + +def NMS(detectResult): + predBoxs = [] + sort_detectboxs = sorted(detectResult, key=lambda x: x.score, reverse=True) + for i in range(len(sort_detectboxs)): + if sort_detectboxs[i].classId == -1: + continue + p1 = rotate_rectangle(sort_detectboxs[i].xmin, sort_detectboxs[i].ymin, + sort_detectboxs[i].xmax, sort_detectboxs[i].ymax, + sort_detectboxs[i].angle) + predBoxs.append(sort_detectboxs[i]) + for j in range(i + 1, len(sort_detectboxs)): + if sort_detectboxs[j].classId == sort_detectboxs[i].classId: + p2 = rotate_rectangle(sort_detectboxs[j].xmin, sort_detectboxs[j].ymin, + sort_detectboxs[j].xmax, sort_detectboxs[j].ymax, + sort_detectboxs[j].angle) + if intersection(p1, p2) > nmsThresh: + sort_detectboxs[j].classId = -1 + return predBoxs + +def sigmoid(x): + x = np.clip(x, -709, 709) # 防止 exp 溢出 + return np.where(x >= 0, 1 / (1 + np.exp(-x)), np.exp(x) / (1 + np.exp(x))) + +def softmax(x, axis=-1): + exp_x = np.exp(x - np.max(x, axis=axis, keepdims=True)) + return exp_x / np.sum(exp_x, axis=axis, keepdims=True) + +def process(out, model_w, model_h, stride, angle_feature, index, scale_w=1, scale_h=1): + class_num = len(CLASSES) + angle_feature = angle_feature.reshape(-1) + xywh = out[:, :64, :] + conf = sigmoid(out[:, 64:, :]) + conf = conf.reshape(-1) + boxes = [] + for ik in range(model_h * model_w * class_num): + if conf[ik] > objectThresh: + w = ik % model_w + h = (ik % (model_w * model_h)) // model_w + c = ik // (model_w * model_h) + xywh_ = xywh[0, :, (h * model_w) + w].reshape(1, 4, 16, 1) + data = np.arange(16).reshape(1, 1, 16, 1) + xywh_ = softmax(xywh_, 2) + xywh_ = np.sum(xywh_ * data, axis=2).reshape(-1) + xywh_add = xywh_[:2] + xywh_[2:] + xywh_sub = (xywh_[2:] - xywh_[:2]) / 2 + angle = (angle_feature[index + (h * model_w) + w] - 0.25) * math.pi + cos_a, sin_a = math.cos(angle), math.sin(angle) + xy = xywh_sub[0] * cos_a - xywh_sub[1] * sin_a, xywh_sub[0] * sin_a + xywh_sub[1] * cos_a + xywh1 = np.array([xy[0] + w + 0.5, xy[1] + h + 0.5, xywh_add[0], xywh_add[1]]) + xywh1 *= stride + xmin = (xywh1[0] - xywh1[2] / 2) * scale_w + ymin = (xywh1[1] - xywh1[3] / 2) * scale_h + xmax = (xywh1[0] + xywh1[2] / 2) * scale_w + ymax = (xywh1[1] + xywh1[3] / 2) * scale_h + boxes.append(DetectBox(c, conf[ik], xmin, ymin, xmax, ymax, angle)) + return boxes + +# ------------------- 主推理函数 ------------------- +def detect_two_box_angle(model_path, rgb_frame): """ - 输入: - model: 预加载的YOLO模型实例(可选) - model_path: YOLO 权重路径(当model为None时使用) - image: 图像数组(numpy array) - image_path: 图片路径(当image为None时使用) - save_path: 可选,保存带标注图像 - 输出: - angle_deg: 置信度最高两个框的主方向夹角(度),如果检测少于两个目标返回 None - annotated_img: 可视化图像 + 输入模型路径和 RGB 图像(numpy array),输出夹角和结果图像。 + 可视化和保存由全局变量 DRAW_RESULT 和 SAVE_PATH 控制。 """ - # 1. 使用预加载的模型或加载新模型 - if model is not None: - loaded_model = model - elif model_path is not None: - loaded_model = YOLO(model_path) - else: - raise ValueError("必须提供model或model_path参数") + global _rknn_instance, DRAW_RESULT, SAVE_PATH - # 2. 读取图像(优先使用传入的图像数组) - if image is not None: - img = image - elif image_path is not None: - img = cv2.imread(image_path) - if img is None: - print(f"无法读取图像: {image_path}") - return None, None - else: - raise ValueError("必须提供image或image_path参数") + if not isinstance(rgb_frame, np.ndarray) or rgb_frame is None: + print(f"[ERROR] detect_two_box_angle 接收到错误类型: {type(rgb_frame)}") + return None, np.zeros((640, 640, 3), np.uint8) - # 3. 推理 OBB - results = loaded_model(img, save=False, imgsz=640, conf=0.5, mode='obb') - result = results[0] + # 注意:输入是 BGR(因为 cv2.imread 返回 BGR),但内部会转为 RGB 给模型 + img = rgb_frame.copy() + img_resized, scale, offset_x, offset_y = letterbox_resize(img, (640, 640)) + infer_img = np.expand_dims(cv2.cvtColor(img_resized, cv2.COLOR_BGR2RGB), 0) - # 4. 可视化 - annotated_img = result.plot() - if save_path: - os.makedirs(os.path.dirname(save_path), exist_ok=True) - cv2.imwrite(save_path, annotated_img) - print(f"推理结果已保存至: {save_path}") + try: + rknn = init_rknn(model_path) + if rknn is None: + return None, img + results = rknn.inference([infer_img]) + except Exception as e: + print(f"[ERROR] RKNN 推理失败: {e}") + return None, img - # 5. 提取旋转角度和置信度 - boxes = result.obb - if boxes is None or len(boxes) < 2: - print("检测到少于两个目标,无法计算夹角。") - return None, annotated_img + outputs = [] + for x in results[:-1]: + index, stride = 0, 0 + if x.shape[2] == 20: + stride, index = 32, 20*4*20*4 + 20*2*20*2 + elif x.shape[2] == 40: + stride, index = 16, 20*4*20*4 + elif x.shape[2] == 80: + stride, index = 8, 0 + feature = x.reshape(1, 65, -1) + outputs += process(feature, x.shape[3], x.shape[2], stride, results[-1], index) - box_info = [] - for box in boxes: - conf = box.conf.cpu().numpy()[0] - cx, cy, w, h, r_rad = box.xywhr.cpu().numpy()[0] - direction = r_rad if w >= h else r_rad + np.pi/2 - direction = direction % np.pi - box_info.append((conf, direction)) + predbox = NMS(outputs) + print(f"[DEBUG] 检测到 {len(predbox)} 个框") - # 6. 取置信度最高两个框 - box_info = sorted(box_info, key=lambda x: x[0], reverse=True) - dir1, dir2 = box_info[0][1], box_info[1][1] + if len(predbox) < 2: + print("检测少于两个目标,无法计算夹角。") + return None, img - # 7. 计算夹角(最小夹角,0~90°) + predbox = sorted(predbox, key=lambda x: x.score, reverse=True) + box1, box2 = predbox[:2] + + output_img = img.copy() if DRAW_RESULT else img # 若不绘制,则直接用原图 + + if DRAW_RESULT: + for box in [box1, box2]: + xmin = int((box.xmin - offset_x) / scale) + ymin = int((box.ymin - offset_y) / scale) + xmax = int((box.xmax - offset_x) / scale) + ymax = int((box.ymax - offset_y) / scale) + points = rotate_rectangle(xmin, ymin, xmax, ymax, box.angle) + cv2.polylines(output_img, [np.array(points, np.int32)], True, (0, 255, 0), 2) + + def main_direction(box): + w, h = (box.xmax - box.xmin)/scale, (box.ymax - box.ymin)/scale + direction = box.angle if w >= h else box.angle + np.pi/2 + return direction % np.pi + + dir1 = main_direction(box1) + dir2 = main_direction(box2) diff = abs(dir1 - dir2) diff = min(diff, np.pi - diff) angle_deg = np.degrees(diff) - print(f"置信度最高两个框主方向夹角: {angle_deg:.2f}°") - return angle_deg, annotated_img + # 保存结果(如果需要) + if SAVE_PATH: + save_dir = os.path.dirname(SAVE_PATH) + if save_dir: # 非空目录才创建 + os.makedirs(save_dir, exist_ok=True) + cv2.imwrite(SAVE_PATH, output_img) + return angle_deg, output_img -# ------------------- 测试 ------------------- +# ------------------- 示例调用 ------------------- # if __name__ == "__main__": -# weight_path = r'angle.pt' -# image_path = r"./test_image/3.jpg" -# save_path = "./inference_results/detected_3.jpg" -# -# #angle_deg, annotated_img = predict_obb_best_angle(weight_path, image_path, save_path) -# angle_deg,_ = predict_obb_best_angle(model_path=weight_path, image_path=image_path, save_path=save_path) -# annotated_img = None -# print(angle_deg) -# if annotated_img is not None: -# cv2.imshow("YOLO OBB Prediction", annotated_img) -# cv2.waitKey(0) -# cv2.destroyAllWindows() \ No newline at end of file + # MODEL_PATH = "./obb.rknn" + # IMAGE_PATH = "./11.jpg" + + # # === 全局控制开关 === + # DRAW_RESULT = True # 是否绘制框 + # SAVE_PATH = "./result11.jpg" # 保存路径,设为 None 则不保存 + + # frame = cv2.imread(IMAGE_PATH) + # if frame is None: + # print(f"[ERROR] 无法读取图像: {IMAGE_PATH}") + # else: + # angle_deg, output_image = detect_two_box_angle(MODEL_PATH, frame) + # if angle_deg is not None: + # print(f"检测到的角度差: {angle_deg:.2f}°") + # else: + # print("未能成功检测到目标或计算角度差") \ No newline at end of file diff --git a/vision/anger_caculate_old.py b/vision/anger_caculate_old.py new file mode 100644 index 0000000..af12cb0 --- /dev/null +++ b/vision/anger_caculate_old.py @@ -0,0 +1,88 @@ +import cv2 +import os +import numpy as np +from ultralytics import YOLO + +def predict_obb_best_angle(model=None, model_path=None, image=None, image_path=None, save_path=None): + """ + 输入: + model: 预加载的YOLO模型实例(可选) + model_path: YOLO 权重路径(当model为None时使用) + image: 图像数组(numpy array) + image_path: 图片路径(当image为None时使用) + save_path: 可选,保存带标注图像 + 输出: + angle_deg: 置信度最高两个框的主方向夹角(度),如果检测少于两个目标返回 None + annotated_img: 可视化图像 + """ + # 1. 使用预加载的模型或加载新模型 + if model is not None: + loaded_model = model + elif model_path is not None: + loaded_model = YOLO(model_path) + else: + raise ValueError("必须提供model或model_path参数") + + # 2. 读取图像(优先使用传入的图像数组) + if image is not None: + img = image + elif image_path is not None: + img = cv2.imread(image_path) + if img is None: + print(f"无法读取图像: {image_path}") + return None, None + else: + raise ValueError("必须提供image或image_path参数") + + # 3. 推理 OBB + results = loaded_model(img, save=False, imgsz=640, conf=0.5, mode='obb') + result = results[0] + + # 4. 可视化 + annotated_img = result.plot() + if save_path: + os.makedirs(os.path.dirname(save_path), exist_ok=True) + cv2.imwrite(save_path, annotated_img) + print(f"推理结果已保存至: {save_path}") + + # 5. 提取旋转角度和置信度 + boxes = result.obb + if boxes is None or len(boxes) < 2: + print("检测到少于两个目标,无法计算夹角。") + return None, annotated_img + + box_info = [] + for box in boxes: + conf = box.conf.cpu().numpy()[0] + cx, cy, w, h, r_rad = box.xywhr.cpu().numpy()[0] + direction = r_rad if w >= h else r_rad + np.pi/2 + direction = direction % np.pi + box_info.append((conf, direction)) + + # 6. 取置信度最高两个框 + box_info = sorted(box_info, key=lambda x: x[0], reverse=True) + dir1, dir2 = box_info[0][1], box_info[1][1] + + # 7. 计算夹角(最小夹角,0~90°) + diff = abs(dir1 - dir2) + diff = min(diff, np.pi - diff) + angle_deg = np.degrees(diff) + + print(f"置信度最高两个框主方向夹角: {angle_deg:.2f}°") + return angle_deg, annotated_img + + +# ------------------- 测试 ------------------- +# if __name__ == "__main__": +# weight_path = r'angle.pt' +# image_path = r"./test_image/3.jpg" +# save_path = "./inference_results/detected_3.jpg" +# +# #angle_deg, annotated_img = predict_obb_best_angle(weight_path, image_path, save_path) +# angle_deg,_ = predict_obb_best_angle(model_path=weight_path, image_path=image_path, save_path=save_path) +# annotated_img = None +# print(angle_deg) +# if annotated_img is not None: +# cv2.imshow("YOLO OBB Prediction", annotated_img) +# cv2.waitKey(0) +# cv2.destroyAllWindows() \ No newline at end of file diff --git a/vision/angle_detector.py b/vision/angle_detector.py index bb3e571..9f89989 100644 --- a/vision/angle_detector.py +++ b/vision/angle_detector.py @@ -1,12 +1,12 @@ # vision/angle_detector.py import sys import os -from vision.anger_caculate import predict_obb_best_angle +from vision.obb_angle_model.obb_angle import detect_two_box_angle # 添加项目根目录到Python路径 -sys.path.append(os.path.join(os.path.dirname(__file__), '..')) +# sys.path.append(os.path.join(os.path.dirname(__file__), '..')) -def get_current_door_angle(model=None, image=None, image_path=None): +def get_current_door_angle(model,image=None, image_path=None): """ 通过视觉系统获取当前出砼门角度 :param model: 模型实例 @@ -16,10 +16,10 @@ def get_current_door_angle(model=None, image=None, image_path=None): """ try: # 调用实际的角度检测函数 - angle_deg, _ = predict_obb_best_angle( - model=model, - image=image, - image_path=image_path + angle_deg, _ = detect_two_box_angle( + model_path=model, + rgb_frame=image + # ,image_path=image_path ) return angle_deg except Exception as e: diff --git a/vision/camera.py b/vision/camera.py index a26f0e5..5c417ab 100644 --- a/vision/camera.py +++ b/vision/camera.py @@ -1,67 +1,503 @@ # vision/camera.py import cv2 +import threading +import queue +import time +import numpy as np +from datetime import datetime +from typing import Optional, Tuple, Dict, Any -class CameraController: - def __init__(self): - self.camera = None - self.camera_type = "ip" - self.camera_ip = "192.168.1.51" - self.camera_port = 554 - self.camera_username = "admin" - self.camera_password = "XJ123456" - self.camera_channel = 1 - - def set_config(self, camera_type="ip", ip=None, port=None, username=None, password=None, channel=1): - """ - 设置摄像头配置 - """ - self.camera_type = camera_type - if ip: - self.camera_ip = ip - if port: - self.camera_port = port - if username: - self.camera_username = username - if password: - self.camera_password = password - self.camera_channel = channel - - def setup_capture(self, camera_index=0): - """ - 设置摄像头捕获 - """ - try: - rtsp_url = f"rtsp://{self.camera_username}:{self.camera_password}@{self.camera_ip}:{self.camera_port}/streaming/channels/{self.camera_channel}01" - self.camera = cv2.VideoCapture(rtsp_url) - - if not self.camera.isOpened(): - print(f"无法打开网络摄像头: {rtsp_url}") - return False - print(f"网络摄像头初始化成功,地址: {rtsp_url}") +class DualCameraController: + """双摄像头控制器 - 支持多线程捕获和同步帧获取""" + + def __init__(self, camera_configs: Dict[str, Dict[str, Any]], max_queue_size: int = 10, sync_threshold_ms: float = 50.0): + # 摄像头配置 + self.camera_configs = camera_configs + + # 摄像头对象和队列 + self.cameras: Dict[str, cv2.VideoCapture] = {} + self.frame_queues: Dict[str, queue.Queue] = {} + self.capture_threads: Dict[str, threading.Thread] = {} + + # 线程控制 + self.stop_event = threading.Event() + self.max_queue_size = max_queue_size + self.sync_threshold_ms = sync_threshold_ms + self.last_sync_pair: Tuple[Optional[np.ndarray], Optional[np.ndarray]] = (None, None) + + # 摄像头状态 + self.is_running = False + + def set_camera_config(self, camera_id: str, ip: str, username: str = "admin", + password: str = "XJ123456", port: int = 554, channel: int = 1): + """设置指定摄像头的配置""" + if camera_id in ['cam1', 'cam2']: + self.camera_configs[camera_id].update({ + 'ip': ip, + 'username': username, + 'password': password, + 'port': port, + 'channel': channel + }) + print(f"摄像头 {camera_id} 配置已更新: IP={ip}") + else: + raise ValueError(f"无效的摄像头ID: {camera_id}") + + def _build_rtsp_url(self, camera_id: str) -> str: + """构建RTSP URL""" + config = self.camera_configs[camera_id] + return f"rtsp://{config['username']}:{config['password']}@{config['ip']}:{config['port']}/Streaming/Channels/{config['channel']}01" + + def _capture_thread(self, camera_id: str): + """摄像头捕获线程""" + cap = self.cameras[camera_id] + q = self.frame_queues[camera_id] + rtsp_url = self._build_rtsp_url(camera_id) + + print(f"启动 {camera_id} 捕获线程") + + while not self.stop_event.is_set(): + try: + # print('aaaaa') + ret, frame = cap.read() + if ret and frame is not None: + # 在帧右上角添加时间戳 + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] + # 获取帧尺寸 + height, width = frame.shape[:2] + # 设置文字参数 + font = cv2.FONT_HERSHEY_SIMPLEX + font_scale = 0.6 + thickness = 2 + color = (0, 255, 0) # 绿色 + + # 计算文字位置(右上角) + text_size = cv2.getTextSize(current_time, font, font_scale, thickness)[0] + text_x = width - text_size[0] - 10 # 距离右边10像素 + text_y = 30 # 距离顶部30像素 + + # 添加文字背景(半透明) + overlay = frame.copy() + cv2.rectangle(overlay, (text_x - 5, text_y - text_size[1] - 5), + (text_x + text_size[0] + 5, text_y + 5), (0, 0, 0), -1) + cv2.addWeighted(overlay, 0.7, frame, 0.3, 0, frame) + + # 添加时间戳文字 + cv2.putText(frame, current_time, (text_x, text_y), font, font_scale, color, thickness) + + # 使用高精度时间戳 + timestamp = time.time() + # 检查队列是否已满 + if q.qsize() >= self.max_queue_size: + # 队列已满,丢弃最旧帧(FIFO) + try: + q.get_nowait() # 移除最旧帧 + q.put_nowait((timestamp, frame)) + except queue.Empty: + # 理论上不会发生,但安全处理 + pass + else: + # 队列未满,直接添加 + q.put_nowait((timestamp, frame)) + else: + print(f"{camera_id} 读取失败,重连中...") + time.sleep(1) + cap.open(rtsp_url) + + except Exception as e: + print(f"{camera_id} 捕获异常: {e}") + time.sleep(1) + + print(f"{camera_id} 捕获线程已停止") + + def start_cameras(self) -> bool: + """启动双摄像头""" + if self.is_running: + print("摄像头已在运行中") return True - except Exception as e: - print(f"摄像头设置失败: {e}") - return False - - def capture_frame(self): - """捕获当前帧并返回numpy数组""" + try: - if self.camera is None: - print("摄像头未初始化") - return None - - ret, frame = self.camera.read() - if ret: - return frame - else: - print("无法捕获图像帧") - return None + # 初始化摄像头和队列 + for camera_id in ['cam2']: + rtsp_url = self._build_rtsp_url(camera_id) + cap = cv2.VideoCapture(rtsp_url) + + if not cap.isOpened(): + print(f"无法打开摄像头 {camera_id}: {rtsp_url}") + # 清理已打开的摄像头 + self.release() + return False + + self.cameras[camera_id] = cap + self.frame_queues[camera_id] = queue.Queue(maxsize=self.max_queue_size) + print(f"摄像头 {camera_id} 初始化成功: {rtsp_url}") + + # 启动捕获线程 + self.stop_event.clear() + for camera_id in ['cam2']: + thread = threading.Thread( + target=self._capture_thread, + args=(camera_id,), + daemon=True + ) + self.capture_threads[camera_id] = thread + thread.start() + + self.is_running = True + print("双摄像头系统启动成功") + return True + except Exception as e: - print(f"图像捕获失败: {e}") + print(f"启动摄像头失败: {e}") + self.release() + return False + + def get_latest_frames(self, sync_threshold_ms: Optional[float] = None) -> Optional[Tuple[np.ndarray, np.ndarray]]: + """获取最新的同步帧对""" + return + if not self.is_running: + print("摄像头未运行") + return None + + sync_threshold = sync_threshold_ms or self.sync_threshold_ms + sync_threshold_sec = sync_threshold / 1000.0 + + # 检查队列是否有数据 + if (self.frame_queues['cam1'].empty() or + self.frame_queues['cam2'].empty()): + return None + + try: + # 获取最新帧 + ts1, f1 = self.frame_queues['cam1'].queue[-1] + ts2, f2 = self.frame_queues['cam2'].queue[-1] + + dt = abs(ts1 - ts2) + + if dt < sync_threshold_sec: + # 时间差在阈值内,认为是同步的 + frame1, frame2 = f1.copy(), f2.copy() + self.last_sync_pair = (frame1, frame2) + return (frame1, frame2) + else: + # 搜索最近5帧找最小时间差 + min_dt = float('inf') + best_pair = None + + # 获取最近5帧 + cam1_frames = list(self.frame_queues['cam1'].queue)[-5:] + cam2_frames = list(self.frame_queues['cam2'].queue)[-5:] + + for t1_local, f1_local in cam1_frames: + for t2_local, f2_local in cam2_frames: + dt_local = abs(t1_local - t2_local) + if dt_local < min_dt and dt_local < sync_threshold_sec * 2: # 更宽松的阈值 + min_dt = dt_local + best_pair = (f1_local.copy(), f2_local.copy()) + + if best_pair: + self.last_sync_pair = best_pair + return best_pair + else: + # 没找到同步帧,返回最新非同步帧 + return (f1.copy(), f2.copy()) + + except Exception as e: + print(f"获取帧对失败: {e}") + return None + + def get_single_frame(self, camera_id: str) -> Optional[np.ndarray]: + """获取单个摄像头的最新帧""" + if not self.is_running: + print("摄像头未运行") + return None + + if camera_id not in self.frame_queues: + print(f"无效的摄像头ID: {camera_id}") + return None + + try: + if not self.frame_queues[camera_id].empty(): + _, frame = self.frame_queues[camera_id].queue[-1] + return frame.copy() + return None + except Exception as e: + print(f"获取单帧失败: {e}") return None + def get_single_latest_frame(self) -> Optional[np.ndarray]: + """获取单个摄像头的最新帧""" + if not self.is_running: + print("摄像头未运行") + return None + + try: + frame_latest = None + dt_t1 = None + + # 获取cam1的最新帧 + if not self.frame_queues['cam2'].empty(): + dt_t1, frame_latest = self.frame_queues['cam2'].queue[-1] + + # 获取cam2的最新帧,选择时间戳更新的那个 + # if frame_latest is None: + # if not self.frame_queues['cam2'].empty(): + # dt_t2, frame2 = self.frame_queues['cam2'].queue[-1] + # if dt_t1 is None or dt_t2 > dt_t1: + # frame_latest = frame2 + + # 返回最新帧的副本(如果找到) + return frame_latest.copy() if frame_latest is not None else None + + except Exception as e: + print(f"获取单帧失败: {e}") + return None + + def get_single_latest_frame2(self) -> Optional[np.ndarray]: + """获取单个摄像头的最新帧""" + if not self.is_running: + print("摄像头未运行") + return None + + try: + frame_latest = None + dt_t1 = None + + # 获取cam1的最新帧 + if not self.frame_queues['cam2'].empty(): + dt_t1, frame_latest = self.frame_queues['cam2'].queue[-1] + + # 获取cam2的最新帧,选择时间戳更新的那个 + if frame_latest is None: + if not self.frame_queues['cam1'].empty(): + dt_t2, frame2 = self.frame_queues['cam1'].queue[-1] + if dt_t1 is None or dt_t2 > dt_t1: + frame_latest = frame2 + + # 返回最新帧的副本(如果找到) + return frame_latest.copy() if frame_latest is not None else None + + except Exception as e: + print(f"获取单帧失败: {e}") + return None + + def get_notification_frame(self, camera_id: str = None, use_sync: bool = True) -> Optional[np.ndarray]: + """根据通知参数获取最近的帧 + + Args: + camera_id: 摄像头ID ('cam1', 'cam2'),如果为None则根据use_sync决定 + use_sync: 是否使用同步帧对,如果为True则返回同步帧对,否则返回指定摄像头的单帧 + + Returns: + 单帧图像或同步帧对 + """ + if not self.is_running: + print("摄像头未运行") + return None + + if use_sync: + # 获取同步帧对,返回拼接后的图像 + frames = self.get_latest_frames() + if frames: + frame1, frame2 = frames + # 调整大小并拼接 + h, w = 480, 640 + frame1_resized = cv2.resize(frame1, (w, h)) + frame2_resized = cv2.resize(frame2, (w, h)) + combined = np.hstack((frame1_resized, frame2_resized)) + + # 添加时间戳信息 + ts1 = time.time() + cv2.putText(combined, f"Sync: {datetime.fromtimestamp(ts1).strftime('%H:%M:%S.%f')[:-3]}", + (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) + return combined + return None + else: + # 获取指定摄像头的单帧 + if camera_id is None: + camera_id = 'cam1' # 默认返回cam1 + return self.get_single_frame(camera_id) + + def display_live_feed(self): + """实时显示双摄像头画面(调试用)""" + if not self.is_running: + print("请先启动摄像头") + return + + print("按 'q' 退出显示,按 's' 保存同步帧") + + while True: + frame = self.get_notification_frame(use_sync=True) + if frame is not None: + cv2.imshow("Dual Camera Feed", frame) + + key = cv2.waitKey(1) & 0xFF + if key == ord('q'): + break + elif key == ord('s'): + # 保存同步帧 + sync_frames = self.get_latest_frames() + if sync_frames: + frame1, frame2 = sync_frames + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:20] + cv2.imwrite(f"cam1_{timestamp}.jpg", frame1) + cv2.imwrite(f"cam2_{timestamp}.jpg", frame2) + print(f"✅ 保存同步帧: cam1_{timestamp}.jpg & cam2_{timestamp}.jpg") + + cv2.destroyAllWindows() + def release(self): """释放摄像头资源""" - if self.camera is not None: - self.camera.release() + print("正在释放摄像头资源...") + + # 停止捕获线程 + if self.is_running: + self.stop_event.set() + # 等待线程结束 + for camera_id, thread in self.capture_threads.items(): + if thread.is_alive(): + thread.join(timeout=2) + print(f"{camera_id} 捕获线程已停止") + + self.capture_threads.clear() + self.is_running = False + + # 释放摄像头 + for camera_id, cap in self.cameras.items(): + if cap is not None: + cap.release() + print(f"摄像头 {camera_id} 已释放") + + self.cameras.clear() + self.frame_queues.clear() + print("摄像头资源释放完成") + + def __del__(self): + """析构函数,确保资源释放""" + self.release() + + # 类方法:快速创建和启动 + @classmethod + def create_and_start(cls, camera_configs: Dict[str, Dict[str, Any]]) -> Optional['DualCameraController']: + """快速创建并启动双摄像头控制器""" + controller = cls(camera_configs) + if controller.start_cameras(): + return controller + else: + return None + + +# 向后兼容的单摄像头控制器 +class CameraController: + """单摄像头控制器 - 向后兼容""" + + def __init__(self): + self.dual_controller = DualCameraController() + self.default_camera = 'cam1' + + def set_config(self, camera_type="ip", ip=None, port=None, username=None, password=None, channel=1): + """设置摄像头配置 - 兼容旧接口""" + self.dual_controller.set_camera_config( + 'cam1', ip or "192.168.1.51", username or "admin", + password or "XJ123456", port or 554, channel + ) + + def setup_capture(self, camera_index=0): + """设置摄像头捕获 - 兼容旧接口""" + return self.dual_controller.start_cameras() + + def capture_frame(self): + """捕获当前帧 - 兼容旧接口""" + return self.dual_controller.capture_frame(self.default_camera) + + def capture_frame_bak(self): + """捕获当前帧(备用) - 兼容旧接口""" + return self.dual_controller.capture_frame_bak(self.default_camera) + + def release(self): + """释放摄像头资源""" + self.dual_controller.release() + + def __del__(self): + """析构函数""" + self.release() + + +# 使用示例和测试 +if __name__ == "__main__": + # 创建双摄像头控制器 + camera_configs = { + 'cam2': { + 'type': 'ip', + 'ip': '192.168.250.61', + 'port': 554, + 'username': 'admin', + 'password': 'XJ123456', + 'channel': 1 + } + } + controller = DualCameraController.create_and_start(camera_configs) + + if controller: + print("双摄像头系统启动成功!") + + # 示例1:获取同步帧对 + print("\n=== 获取同步帧 ===") + while True: + single_frame = controller.get_single_latest_frame() + + if single_frame is not None: + print(f"获取到帧形状: {single_frame.shape}") + cv2.imshow("Single Camera Frame", single_frame) + else: + print("未获取到帧") + key = cv2.waitKey(1) & 0xFF + if key == ord('s') and single_frame is not None: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:20] + cv2.imwrite(f"single_frame_{timestamp}.jpg", single_frame) + print(f"✅ 保存单帧: single_frame_{timestamp}.jpg") + if key == ord('q'): + break + time.sleep(1) + + # controller.get_single_latest_frame('cam2') + # sync_frames = controller.get_latest_frames(sync_threshold_ms=50) + # if sync_frames: + # frame1, frame2 = sync_frames + # print(f"获取到同步帧对 - 帧1形状: {frame1.shape}, 帧2形状: {frame2.shape}") + # else: + # print("未获取到同步帧对") + + # 示例2:根据通知参数获取帧 + # print("\n=== 根据通知参数获取帧 ===") + + # # 获取同步拼接帧(用于显示) + # combined_frame = controller.get_notification_frame(use_sync=True) + # if combined_frame is not None: + # print(f"获取到同步拼接帧,形状: {combined_frame.shape}") + # cv2.imshow("Sync Frame", combined_frame) + # cv2.waitKey(1000) # 显示1秒 + # cv2.destroyAllWindows() + + # # 获取单个摄像头帧 + # single_frame = controller.get_notification_frame(camera_id='cam1', use_sync=False) + # if single_frame is not None: + # print(f"获取到cam1单帧,形状: {single_frame.shape}") + + # # 示例3:实时显示 + # print("\n=== 实时显示模式 ===") + # print("按 'q' 退出显示,按 's' 保存同步帧") + # # controller.display_live_feed() # 取消注释以启用实时显示 + + # # 示例4:兼容性测试 + # print("\n=== 兼容性测试 ===") + # old_frame = controller.capture_frame('cam1') + # if old_frame is not None: + # print(f"旧接口兼容 - 帧形状: {old_frame.shape}") + + # 清理 + controller.release() + print("\n摄像头资源已释放") + else: + print("双摄像头系统启动失败!") diff --git a/vision/detector.py b/vision/detector.py index 2f982c8..178ee78 100644 --- a/vision/detector.py +++ b/vision/detector.py @@ -1,86 +1,105 @@ # vision/detector.py import os -from ultralytics import YOLO +import cv2 from vision.angle_detector import get_current_door_angle from vision.overflow_detector import detect_overflow_from_image from vision.alignment_detector import detect_vehicle_alignment +from config.settings import app_set_config class VisionDetector: - def __init__(self, settings): - self.settings = settings - - # 模型实例 - self.angle_model = None - self.overflow_model = None - self.alignment_model = None - + def __init__(self): + pass + #model路径在对应的模型里面 + # self.alignment_model = os.path.join(current_dir, "align_model/yolov11_cls_640v6.rknn") + def load_models(self): """ 加载所有视觉检测模型 """ + from ultralytics import YOLO success = True # 加载夹角检测模型 try: - if not os.path.exists(self.settings.angle_model_path): - print(f"夹角检测模型不存在: {self.settings.angle_model_path}") + if not os.path.exists(app_set_config.angle_model_path): + print(f"夹角检测模型不存在: {app_set_config.angle_model_path}") success = False else: # 注意:angle.pt模型通过predict_obb_best_angle函数使用,不需要预加载 - print(f"夹角检测模型路径: {self.settings.angle_model_path}") + print(f"夹角检测模型路径: {app_set_config.angle_model_path}") except Exception as e: print(f"检查夹角检测模型失败: {e}") success = False # 加载堆料检测模型 try: - if not os.path.exists(self.settings.overflow_model_path): - print(f"堆料检测模型不存在: {self.settings.overflow_model_path}") + if not os.path.exists(app_set_config.overflow_model_path): + print(f"堆料检测模型不存在: {app_set_config.overflow_model_path}") success = False else: - self.overflow_model = YOLO(self.settings.overflow_model_path) - print(f"成功加载堆料检测模型: {self.settings.overflow_model_path}") + self.overflow_model = YOLO(app_set_config.overflow_model_path) + print(f"成功加载堆料检测模型: {app_set_config.overflow_model_path}") except Exception as e: print(f"加载堆料检测模型失败: {e}") success = False # 加载对齐检测模型 try: - if not os.path.exists(self.settings.alignment_model_path): - print(f"对齐检测模型不存在: {self.settings.alignment_model_path}") + if not os.path.exists(app_set_config.alignment_model_path): + print(f"对齐检测模型不存在: {app_set_config.alignment_model_path}") success = False else: - self.alignment_model = YOLO(self.settings.alignment_model_path) - print(f"成功加载对齐检测模型: {self.settings.alignment_model_path}") + self.alignment_model = YOLO(app_set_config.alignment_model_path) + print(f"成功加载对齐检测模型: {app_set_config.alignment_model_path}") except Exception as e: print(f"加载对齐检测模型失败: {e}") success = False return success - def detect_angle(self, image=None, image_path=None): + def detect_angle(self, image=None): """ 通过视觉系统获取当前出砼门角度 """ + + # if image is None: + # image=cv2.imread(os.path.join(app_set_config.project_root,'test.jpg')) + image=cv2.flip(image, 0) + # image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + # cv2.imshow('test',image) return get_current_door_angle( - model=self.angle_model, - image=image, - image_path=image_path + model=app_set_config.angle_model_path, + image=image ) def detect_overflow(self, image_array): """ 通过图像检测是否溢料 """ + image_array=cv2.flip(image_array, 0) + # cv2.imwrite('test.jpg', image_array) + # cv2.namedWindow("Alignment", cv2.WINDOW_NORMAL) + # cv2.resizeWindow("Alignment", 640, 480) + # cv2.imshow("Alignment", image_array) + # cv2.waitKey(1) + # print('path:', app_set_config.overflow_model_path) + # print('roi:', app_set_config.roi_file_path) return detect_overflow_from_image( - image_array, - self.overflow_model, - self.settings.roi_file_path + app_set_config.overflow_model_path, + app_set_config.roi_file_path, + image_array ) - def detect_vehicle_alignment(self, image_array): + def detect_vehicle_alignment(self, image_array)->bool: """ 通过图像检测模具车是否对齐 """ - return detect_vehicle_alignment(image_array, self.alignment_model) + image_array = cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB) + image_array=cv2.flip(image_array, 0) + # cv2.namedWindow("Alignment", cv2.WINDOW_NORMAL) + # cv2.resizeWindow("Alignment", 640, 480) + # cv2.imshow("Alignment", image_array) + # cv2.waitKey(1) + + return detect_vehicle_alignment(image_array) diff --git a/vision/muju_cls/main.py b/vision/muju_cls/main.py new file mode 100644 index 0000000..0ecf790 --- /dev/null +++ b/vision/muju_cls/main.py @@ -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 + ) + diff --git a/vision/muju_cls/muju_cls - 副本.rknn b/vision/muju_cls/muju_cls - 副本.rknn new file mode 100644 index 0000000..ca52394 Binary files /dev/null and b/vision/muju_cls/muju_cls - 副本.rknn differ diff --git a/vision/muju_cls/muju_cls.rknn b/vision/muju_cls/muju_cls.rknn new file mode 100644 index 0000000..41f7055 Binary files /dev/null and b/vision/muju_cls/muju_cls.rknn differ diff --git a/vision/muju_cls/muju_cls_rknn.py b/vision/muju_cls/muju_cls_rknn.py new file mode 100644 index 0000000..d4b4a51 --- /dev/null +++ b/vision/muju_cls/muju_cls_rknn.py @@ -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() +''' +# --------------------------- 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/obb_angle_model/obb.rknn b/vision/obb_angle_model/obb.rknn new file mode 100644 index 0000000..746872b Binary files /dev/null and b/vision/obb_angle_model/obb.rknn differ diff --git a/vision/obb_angle_model/obb_angle.py b/vision/obb_angle_model/obb_angle.py new file mode 100644 index 0000000..cf28d9c --- /dev/null +++ b/vision/obb_angle_model/obb_angle.py @@ -0,0 +1,238 @@ +import cv2 +import numpy as np +import math +from shapely.geometry import Polygon +import os + +# ------------------- 全局配置变量 ------------------- +# 模型相关 +CLASSES = ['clamp'] +nmsThresh = 0.4 +objectThresh = 0.35 + +# 可视化与保存控制(全局变量,可外部修改) +DRAW_RESULT = True # 是否在输出图像上绘制旋转框 +SAVE_PATH = None # 保存路径,如 "./result.jpg";设为 None 则不保存 + +# RKNN 单例 +_rknn_instance = None + +# ------------------- RKNN 管理函数 ------------------- +def init_rknn(model_path): + from rknnlite.api import RKNNLite + + """只加载一次 RKNN 模型""" + global _rknn_instance + if _rknn_instance is None: + _rknn_instance = RKNNLite(verbose=False) + ret = _rknn_instance.load_rknn(model_path) + if ret != 0: + print(f"[ERROR] Failed to load RKNN model: {ret}") + return None + ret = _rknn_instance.init_runtime(core_mask=RKNNLite.NPU_CORE_0) + if ret != 0: + print(f"[ERROR] Failed to init runtime: {ret}") + return None + return _rknn_instance + +def release_rknn(): + """释放 RKNN 对象""" + global _rknn_instance + if _rknn_instance: + _rknn_instance.release() + _rknn_instance = None + +# ------------------- 工具函数 ------------------- +def letterbox_resize(image, size, bg_color=114): + target_width, target_height = size + image_height, image_width, _ = image.shape + scale = min(target_width / image_width, target_height / image_height) + new_width, new_height = int(image_width * scale), int(image_height * scale) + image_resized = cv2.resize(image, (new_width, new_height), interpolation=cv2.INTER_AREA) + canvas = np.ones((target_height, target_width, 3), dtype=np.uint8) * bg_color + offset_x, offset_y = (target_width - new_width) // 2, (target_height - new_height) // 2 + canvas[offset_y:offset_y + new_height, offset_x:offset_x + new_width] = image_resized + return canvas, scale, offset_x, offset_y + +class DetectBox: + def __init__(self, classId, score, xmin, ymin, xmax, ymax, angle): + self.classId = classId + self.score = score + self.xmin = xmin + self.ymin = ymin + self.xmax = xmax + self.ymax = ymax + self.angle = angle + +def rotate_rectangle(x1, y1, x2, y2, a): + cx, cy = (x1 + x2) / 2, (y1 + y2) / 2 + cos_a, sin_a = math.cos(a), math.sin(a) + pts = [(x1, y1), (x1, y2), (x2, y2), (x2, y1)] + return [[int(cx + (xx - cx) * cos_a - (yy - cy) * sin_a), + int(cy + (xx - cx) * sin_a + (yy - cy) * cos_a)] for xx, yy in pts] + +def intersection(g, p): + g = Polygon(np.array(g).reshape(-1, 2)) + p = Polygon(np.array(p).reshape(-1, 2)) + if not g.is_valid or not p.is_valid: + return 0 + inter = g.intersection(p).area + union = g.area + p.area - inter + return 0 if union == 0 else inter / union + +def NMS(detectResult): + predBoxs = [] + sort_detectboxs = sorted(detectResult, key=lambda x: x.score, reverse=True) + for i in range(len(sort_detectboxs)): + if sort_detectboxs[i].classId == -1: + continue + p1 = rotate_rectangle(sort_detectboxs[i].xmin, sort_detectboxs[i].ymin, + sort_detectboxs[i].xmax, sort_detectboxs[i].ymax, + sort_detectboxs[i].angle) + predBoxs.append(sort_detectboxs[i]) + for j in range(i + 1, len(sort_detectboxs)): + if sort_detectboxs[j].classId == sort_detectboxs[i].classId: + p2 = rotate_rectangle(sort_detectboxs[j].xmin, sort_detectboxs[j].ymin, + sort_detectboxs[j].xmax, sort_detectboxs[j].ymax, + sort_detectboxs[j].angle) + if intersection(p1, p2) > nmsThresh: + sort_detectboxs[j].classId = -1 + return predBoxs + +def sigmoid(x): + x = np.clip(x, -709, 709) # 防止 exp 溢出 + return np.where(x >= 0, 1 / (1 + np.exp(-x)), np.exp(x) / (1 + np.exp(x))) + +def softmax(x, axis=-1): + exp_x = np.exp(x - np.max(x, axis=axis, keepdims=True)) + return exp_x / np.sum(exp_x, axis=axis, keepdims=True) + +def process(out, model_w, model_h, stride, angle_feature, index, scale_w=1, scale_h=1): + class_num = len(CLASSES) + angle_feature = angle_feature.reshape(-1) + xywh = out[:, :64, :] + conf = sigmoid(out[:, 64:, :]) + conf = conf.reshape(-1) + boxes = [] + for ik in range(model_h * model_w * class_num): + if conf[ik] > objectThresh: + w = ik % model_w + h = (ik % (model_w * model_h)) // model_w + c = ik // (model_w * model_h) + xywh_ = xywh[0, :, (h * model_w) + w].reshape(1, 4, 16, 1) + data = np.arange(16).reshape(1, 1, 16, 1) + xywh_ = softmax(xywh_, 2) + xywh_ = np.sum(xywh_ * data, axis=2).reshape(-1) + xywh_add = xywh_[:2] + xywh_[2:] + xywh_sub = (xywh_[2:] - xywh_[:2]) / 2 + angle = (angle_feature[index + (h * model_w) + w] - 0.25) * math.pi + cos_a, sin_a = math.cos(angle), math.sin(angle) + xy = xywh_sub[0] * cos_a - xywh_sub[1] * sin_a, xywh_sub[0] * sin_a + xywh_sub[1] * cos_a + xywh1 = np.array([xy[0] + w + 0.5, xy[1] + h + 0.5, xywh_add[0], xywh_add[1]]) + xywh1 *= stride + xmin = (xywh1[0] - xywh1[2] / 2) * scale_w + ymin = (xywh1[1] - xywh1[3] / 2) * scale_h + xmax = (xywh1[0] + xywh1[2] / 2) * scale_w + ymax = (xywh1[1] + xywh1[3] / 2) * scale_h + boxes.append(DetectBox(c, conf[ik], xmin, ymin, xmax, ymax, angle)) + return boxes + +# ------------------- 主推理函数 ------------------- +def detect_two_box_angle(model_path, rgb_frame): + """ + 输入模型路径和 RGB 图像(numpy array),输出夹角和结果图像。 + 可视化和保存由全局变量 DRAW_RESULT 和 SAVE_PATH 控制。 + """ + global _rknn_instance, DRAW_RESULT, SAVE_PATH + + if not isinstance(rgb_frame, np.ndarray) or rgb_frame is None: + print(f"[ERROR] detect_two_box_angle 接收到错误类型: {type(rgb_frame)}") + return None, np.zeros((640, 640, 3), np.uint8) + + # 注意:输入是 BGR(因为 cv2.imread 返回 BGR),但内部会转为 RGB 给模型 + img = rgb_frame.copy() + img_resized, scale, offset_x, offset_y = letterbox_resize(img, (640, 640)) + infer_img = np.expand_dims(cv2.cvtColor(img_resized, cv2.COLOR_BGR2RGB), 0) + + try: + rknn = init_rknn(model_path) + if rknn is None: + return None, img + results = rknn.inference([infer_img]) + except Exception as e: + print(f"[ERROR] RKNN 推理失败: {e}") + return None, img + + outputs = [] + for x in results[:-1]: + index, stride = 0, 0 + if x.shape[2] == 20: + stride, index = 32, 20*4*20*4 + 20*2*20*2 + elif x.shape[2] == 40: + stride, index = 16, 20*4*20*4 + elif x.shape[2] == 80: + stride, index = 8, 0 + feature = x.reshape(1, 65, -1) + outputs += process(feature, x.shape[3], x.shape[2], stride, results[-1], index) + + predbox = NMS(outputs) + print(f"[DEBUG] 检测到 {len(predbox)} 个框") + + if len(predbox) < 2: + print("检测少于两个目标,无法计算夹角。") + return None, img + + predbox = sorted(predbox, key=lambda x: x.score, reverse=True) + box1, box2 = predbox[:2] + + output_img = img.copy() if DRAW_RESULT else img # 若不绘制,则直接用原图 + + if DRAW_RESULT: + for box in [box1, box2]: + xmin = int((box.xmin - offset_x) / scale) + ymin = int((box.ymin - offset_y) / scale) + xmax = int((box.xmax - offset_x) / scale) + ymax = int((box.ymax - offset_y) / scale) + points = rotate_rectangle(xmin, ymin, xmax, ymax, box.angle) + cv2.polylines(output_img, [np.array(points, np.int32)], True, (0, 255, 0), 2) + + def main_direction(box): + w, h = (box.xmax - box.xmin)/scale, (box.ymax - box.ymin)/scale + direction = box.angle if w >= h else box.angle + np.pi/2 + return direction % np.pi + + dir1 = main_direction(box1) + dir2 = main_direction(box2) + diff = abs(dir1 - dir2) + diff = min(diff, np.pi - diff) + angle_deg = np.degrees(diff) + + # 保存结果(如果需要) + if SAVE_PATH: + save_dir = os.path.dirname(SAVE_PATH) + if save_dir: # 非空目录才创建 + os.makedirs(save_dir, exist_ok=True) + cv2.imwrite(SAVE_PATH, output_img) + + return angle_deg, output_img + +# ------------------- 示例调用 ------------------- +if __name__ == "__main__": + MODEL_PATH = "./obb.rknn" + IMAGE_PATH = "./11.jpg" + + # # === 全局控制开关 === + # DRAW_RESULT = True # 是否绘制框 + # SAVE_PATH = "./result11.jpg" # 保存路径,设为 None 则不保存 + + # frame = cv2.imread(IMAGE_PATH) + # if frame is None: + # print(f"[ERROR] 无法读取图像: {IMAGE_PATH}") + # else: + # image=cv2.flip(image, 0) + # image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + # angle_deg, output_image = detect_two_box_angle(MODEL_PATH, frame) + # if angle_deg is not None: + # print(f"检测到的角度差: {angle_deg:.2f}°") + # else: + # print("未能成功检测到目标或计算角度差") \ No newline at end of file diff --git a/vision/overflow_detector.py b/vision/overflow_detector.py index 359e8be..2b257a9 100644 --- a/vision/overflow_detector.py +++ b/vision/overflow_detector.py @@ -1,47 +1,33 @@ # vision/overflow_detector.py import sys import os -from vision.resize_tuili_image_main import classify_image_weighted, load_global_rois, crop_and_resize +from typing import Optional +from vision.overflow_model.yiliao_main_rknn import classify_frame_with_rois # 添加项目根目录到Python路径 -sys.path.append(os.path.join(os.path.dirname(__file__), '..')) +# sys.path.append(os.path.join(os.path.dirname(__file__), '..')) -def detect_overflow_from_image(image_array, overflow_model, roi_file_path): +def detect_overflow_from_image(overflow_model,roi_file_path,image_array)->Optional[str]: """ 通过图像检测是否溢料 :param image_array: 图像数组 :param overflow_model: 溢料检测模型 :param roi_file_path: ROI文件路径 - :return: 是否检测到溢料 (True/False) + :return: 检测到的溢料类别 (未堆料、小堆料、大堆料、未浇筑满、浇筑满) 或 None """ try: - # 检查模型是否已加载 - if overflow_model is None: - print("堆料检测模型未加载") - return False + outputs = classify_frame_with_rois(overflow_model, image_array, roi_file_path) + print("溢料检测结果:", outputs) + for res in outputs: - # 加载ROI区域 - rois = load_global_rois(roi_file_path) - - if not rois: - print(f"没有有效的ROI配置: {roi_file_path}") - return False - - if image_array is None: - print("输入图像为空") - return False - - # 裁剪和调整图像大小 - crops = crop_and_resize(image_array, rois, 640) - - # 对每个ROI区域进行分类检测 - for roi_resized, _ in crops: - final_class, _, _, _ = classify_image_weighted(roi_resized, overflow_model, threshold=0.4) - if "大堆料" in final_class or "小堆料" in final_class: - print(f"检测到溢料: {final_class}") - return True - - return False + return res["class"] + # if "大堆料" in res["class"] or "小堆料" in res["class"]: + # print(f"检测到溢料: {res['class']}") + # return True + + # return False + return None except Exception as e: print(f"溢料检测失败: {e}") - return False + return None + diff --git a/vision/overflow_detector_old.py b/vision/overflow_detector_old.py new file mode 100644 index 0000000..359e8be --- /dev/null +++ b/vision/overflow_detector_old.py @@ -0,0 +1,47 @@ +# vision/overflow_detector.py +import sys +import os +from vision.resize_tuili_image_main import classify_image_weighted, load_global_rois, crop_and_resize + +# 添加项目根目录到Python路径 +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +def detect_overflow_from_image(image_array, overflow_model, roi_file_path): + """ + 通过图像检测是否溢料 + :param image_array: 图像数组 + :param overflow_model: 溢料检测模型 + :param roi_file_path: ROI文件路径 + :return: 是否检测到溢料 (True/False) + """ + try: + # 检查模型是否已加载 + if overflow_model is None: + print("堆料检测模型未加载") + return False + + # 加载ROI区域 + rois = load_global_rois(roi_file_path) + + if not rois: + print(f"没有有效的ROI配置: {roi_file_path}") + return False + + if image_array is None: + print("输入图像为空") + return False + + # 裁剪和调整图像大小 + crops = crop_and_resize(image_array, rois, 640) + + # 对每个ROI区域进行分类检测 + for roi_resized, _ in crops: + final_class, _, _, _ = classify_image_weighted(roi_resized, overflow_model, threshold=0.4) + if "大堆料" in final_class or "小堆料" in final_class: + print(f"检测到溢料: {final_class}") + return True + + return False + except Exception as e: + print(f"溢料检测失败: {e}") + return False diff --git a/vision/overflow_model/README.md b/vision/overflow_model/README.md new file mode 100644 index 0000000..269efdf --- /dev/null +++ b/vision/overflow_model/README.md @@ -0,0 +1,78 @@ +# RKNN 堆料分类推理系统 README + +本项目用于在 RK3588 平台上运行 RKNN 分类模型,对多个 ROI 区域进行堆料状态分类,包括: + +未堆料 0 +小堆料 1 +大堆料 2 +未浇筑满 3 +浇筑满 4 + +项目中支持 多 ROI 裁剪、模型推理、加权判断(小/大堆料) 和分类结果输出。 + +## 目录结构 + +project/ +│── yiliao_cls.rknn # RKNN 模型 +│── best.pt # pt 模型 +│── roi_coordinates/ # ROI 坐标文件目录 +│ └── 1_rois.txt +│── test_image/ # 测试图片目录 +│ └── 1.jpg + └── 2.jpg + └── 3.jpg +│── yiliao_main_rknn.py # RKNN主推理脚本 +│── yiliao_main_pc.py # PC推理脚本 +│── README.md + + +## 配置(略) +## 安装依赖(略) + + +## 调用示例 +单张图片推理调用示例 + +```bash + +from yiliao_main_rknn import classify_frame_with_rois + +if __name__ == "__main__": + model_path = "yiliao_cls.rknn" + roi_file = "./roi_coordinates/1_rois.txt" + + frame = cv2.imread("./test_image/2.jpg") + + outputs = classify_frame_with_rois(model_path, frame, roi_file) + + for res in outputs: + print(res) + + +``` + +##小堆料 / 大堆料加权判定说明 + +模型原始输出中,小堆料(class 1)与大堆料(class 2)相比时容易出现概率接近的情况。 + +通过加权机制: + +✔ 可以避免因整体概率偏低导致分类不稳定 +✔ 优先放大“大堆料 的可能性”(因为 w2 > w1) +✔ score 更能反映堆料大小的趋势,而不是绝对概率 + +为提高判断稳定性,采用了加权评分方式:(这些参数都可以根据实际情况在文件中对weighted_small_large中参数进行修改) +score = (0.3 * p1 + 0.7 * p2) / (p1 + p2) +score ≥ 0.4 → 大堆料 +score < 0.4 → 小堆料 + +p1:小堆料概率 +p2:大堆料概率 +score 越接近 1 越倾向于大堆料 +score 越接近 0 越倾向于小堆料 + + + + + + diff --git a/vision/overflow_model/best.pt b/vision/overflow_model/best.pt new file mode 100644 index 0000000..3fd6d73 Binary files /dev/null and b/vision/overflow_model/best.pt differ diff --git a/vision/overflow_model/roi_coordinates/1_rois.txt b/vision/overflow_model/roi_coordinates/1_rois.txt new file mode 100644 index 0000000..bb8f71d --- /dev/null +++ b/vision/overflow_model/roi_coordinates/1_rois.txt @@ -0,0 +1 @@ +859,810,696,328 diff --git a/vision/overflow_model/test_image/1.jpg b/vision/overflow_model/test_image/1.jpg new file mode 100644 index 0000000..2882cd5 Binary files /dev/null and b/vision/overflow_model/test_image/1.jpg differ diff --git a/vision/overflow_model/test_image/2.jpg b/vision/overflow_model/test_image/2.jpg new file mode 100644 index 0000000..dcd5369 Binary files /dev/null and b/vision/overflow_model/test_image/2.jpg differ diff --git a/vision/overflow_model/test_image/3.jpg b/vision/overflow_model/test_image/3.jpg new file mode 100644 index 0000000..8df7feb Binary files /dev/null and b/vision/overflow_model/test_image/3.jpg differ diff --git a/vision/overflow_model/yiliao_cls.rknn b/vision/overflow_model/yiliao_cls.rknn new file mode 100644 index 0000000..ebd4825 Binary files /dev/null and b/vision/overflow_model/yiliao_cls.rknn differ diff --git a/vision/overflow_model/yiliao_main_pc.py b/vision/overflow_model/yiliao_main_pc.py new file mode 100644 index 0000000..539ff30 --- /dev/null +++ b/vision/overflow_model/yiliao_main_pc.py @@ -0,0 +1,168 @@ +import os +from pathlib import Path +import cv2 +import numpy as np +from ultralytics import YOLO + +# --------------------------- +# 类别映射 +# --------------------------- +CLASS_NAMES = { + 0: "未堆料", + 1: "小堆料", + 2: "大堆料", + 3: "未浇筑满", + 4: "浇筑满" +} + +# --------------------------- +# 加载 ROI 列表 +# --------------------------- +def load_global_rois(txt_path): + rois = [] + if not os.path.exists(txt_path): + print(f"❌ ROI 文件不存在: {txt_path}") + return rois + with open(txt_path, 'r') as f: + for line in f: + s = line.strip() + if s: + try: + x, y, w, h = map(int, s.split(',')) + rois.append((x, y, w, h)) + except Exception as e: + print(f"无法解析 ROI 行 '{s}': {e}") + return rois + +# --------------------------- +# 裁剪并 resize ROI +# --------------------------- +def crop_and_resize(img, rois, target_size=640): + crops = [] + h_img, w_img = img.shape[:2] + for i, (x, y, w, h) in enumerate(rois): + if x < 0 or y < 0 or x + w > w_img or y + h > h_img: + continue + roi = img[y:y+h, x:x+w] + roi_resized = cv2.resize(roi, (target_size, target_size), interpolation=cv2.INTER_AREA) + crops.append((roi_resized, i)) + return crops + +# --------------------------- +# class1/class2 加权判断 +# --------------------------- +def weighted_small_large(pred_probs, threshold=0.4, w1=0.3, w2=0.7): + p1 = float(pred_probs[1]) + p2 = float(pred_probs[2]) + total = p1 + p2 + if total > 0: + score = (w1 * p1 + w2 * p2) / total + else: + score = 0.0 + final_class = "大堆料" if score >= threshold else "小堆料" + return final_class, score, p1, p2 + +# --------------------------- +# 单张图片推理函数 +# --------------------------- +def classify_image_weighted(image, model, threshold=0.4): + results = model(image) + pred_probs = results[0].probs.data.cpu().numpy().flatten() + class_id = int(pred_probs.argmax()) + confidence = float(pred_probs[class_id]) + class_name = CLASS_NAMES.get(class_id, f"未知类别({class_id})") + + # class1/class2 使用加权得分 + if class_id in [1, 2]: + final_class, score, p1, p2 = weighted_small_large(pred_probs, threshold=threshold) + else: + final_class = class_name + score = confidence + p1 = float(pred_probs[1]) + p2 = float(pred_probs[2]) + + return final_class, score, p1, p2 + +# --------------------------- +# 批量推理主函数 +# --------------------------- +def batch_classify_images(model_path, input_folder, output_root, roi_file, target_size=640, threshold=0.5): + # 加载模型 + model = YOLO(model_path) + + # 确保输出根目录存在 + output_root = Path(output_root) + output_root.mkdir(parents=True, exist_ok=True) + + # 为所有类别创建目录 + class_dirs = {} + for name in CLASS_NAMES.values(): + d = output_root / name + d.mkdir(exist_ok=True) + class_dirs[name] = d + + rois = load_global_rois(roi_file) + if not rois: + print("❌ 没有有效 ROI,退出") + return + + # 遍历图片 + for img_path in Path(input_folder).glob("*.*"): + if img_path.suffix.lower() not in ['.jpg', '.jpeg', '.png', '.bmp', '.tif']: + continue + try: + img = cv2.imread(str(img_path)) + if img is None: + continue + + crops = crop_and_resize(img, rois, target_size) + + for roi_resized, roi_idx in crops: + final_class, score, p1, p2 = classify_image_weighted(roi_resized, model, threshold=threshold) + + # 文件名中保存 ROI、类别、加权分数、class1/class2 置信度 + suffix = f"_roi{roi_idx}_{final_class}_score{score:.2f}_p1{p1:.2f}_p2{p2:.2f}" + dst_path = class_dirs[final_class] / f"{img_path.stem}{suffix}{img_path.suffix}" + cv2.imwrite(dst_path, roi_resized) + print(f"{img_path.name}{suffix} -> {final_class} (score={score:.2f}, p1={p1:.2f}, p2={p2:.2f})") + + except Exception as e: + print(f"处理失败 {img_path.name}: {e}") + + +# --------------------------- +# 单张图片使用示例(保留 ROI,不保存文件) +# --------------------------- +if __name__ == "__main__": + model_path = r"best.pt" + image_path = r"./test_image/2.jpg" # 单张图片路径 + roi_file = r"./roi_coordinates/1_rois.txt" + target_size = 640 + threshold = 0.4 #加权得分阈值可以根据大小堆料分类结果进行调整 + + # 加载模型 + model = YOLO(model_path) + + # 读取 ROI + rois = load_global_rois(roi_file) + if not rois: + print("❌ 没有有效 ROI,退出") + exit(1) + + # 读取图片 + img = cv2.imread(image_path) + if img is None: + print(f"❌ 无法读取图片: {image_path}") + exit(1) + + # 注意:必须裁剪 ROI 并推理,因为训练的时候输入的图像是经过resize的 + crops = crop_and_resize(img, rois, target_size) + for roi_resized, roi_idx in crops: + #final_class, score, p1, p2 = classify_image_weighted(roi_resized, model, threshold=threshold) + final_class,_,_,_ = classify_image_weighted(roi_resized, model, threshold=threshold) + # 只输出信息,不保存文件 + #print(f"ROI {roi_idx} -> 类别: {final_class}, 加权分数: {score:.2f}, " + #f"class1 置信度: {p1:.2f}, class2 置信度: {p2:.2f}") + print(f"类别: {final_class}") + + diff --git a/vision/overflow_model/yiliao_main_rknn.py b/vision/overflow_model/yiliao_main_rknn.py new file mode 100644 index 0000000..1b840f8 --- /dev/null +++ b/vision/overflow_model/yiliao_main_rknn.py @@ -0,0 +1,185 @@ +import os +from pathlib import Path +import cv2 +import numpy as np +import platform + + +# --------------------------- +# 类别映射 +# --------------------------- +CLASS_NAMES = { + 0: "未堆料", + 1: "小堆料", + 2: "大堆料", + 3: "未浇筑满", + 4: "浇筑满" +} + +# --------------------------- +# RKNN 全局实例(只加载一次) +# --------------------------- +_global_rknn = None +DEVICE_COMPATIBLE_NODE = '/proc/device-tree/compatible' + + +# ===================================================== +# RKNN MODEL +# ===================================================== +def init_rknn_model(model_path): + from rknnlite.api import RKNNLite + + 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 + + +# --------------------------- +# 图像预处理(统一 640×640) +# --------------------------- +def preprocess(img, size=(640, 640)): + img = cv2.resize(img, size) + img = np.expand_dims(img, 0) + return img + + +# --------------------------- +# 单次 RKNN 分类 +# --------------------------- +def rknn_classify(img_resized, model_path): + rknn = init_rknn_model(model_path) + input_tensor = preprocess(img_resized) + outs = rknn.inference([input_tensor]) + + pred = outs[0].reshape(-1) + class_id = int(np.argmax(pred)) + return class_id, pred.astype(float) + + +# ===================================================== +# ROI 逻辑 +# ===================================================== +def load_rois(txt_path): + rois = [] + if not os.path.exists(txt_path): + print(f"❌ ROI 文件不存在: {txt_path}") + return rois + + with open(txt_path) as f: + for line in f: + s = line.strip() + if s: + try: + x, y, w, h = map(int, s.split(',')) + rois.append((x, y, w, h)) + except: + print("ROI 格式错误:", s) + return rois + + +def crop_and_resize(img, rois, target_size=640): + crops = [] + h_img, w_img = img.shape[:2] + + for idx, (x, y, w, h) in enumerate(rois): + if x < 0 or y < 0 or x + w > w_img or y + h > h_img: + continue + roi = img[y:y + h, x:x + w] + roi_resized = cv2.resize(roi, (target_size, target_size), interpolation=cv2.INTER_AREA) + crops.append((roi_resized, idx)) + return crops + + +# ===================================================== +# class1/class2 加权分类增强 +# ===================================================== +def weighted_small_large(pred, threshold=0.4, w1=0.3, w2=0.7): + p1 = float(pred[1]) + p2 = float(pred[2]) + total = p1 + p2 + + score = (w1 * p1 + w2 * p2) / total if total > 0 else 0.0 + final_class = "大堆料" if score >= threshold else "小堆料" + + return final_class, score, p1, p2 + + +# ===================================================== +# ⭐ 高复用:一行完成 ROI + 推理 ⭐ +# ===================================================== +def classify_frame_with_rois(model_path, frame, roi_file, threshold=0.4): + """ + 输入: + - frame: BGR 图像 (numpy array) + - model_path: RKNN 模型路径 + - roi_file: ROI 的 txt 文件 + - threshold: class1/class2 小/大堆料判断阈值 + + 输出: + [ + { "roi": idx, "class": 类别, "score": 0.93, "p1": 0.22, "p2": 0.71 }, + ... + ] + """ + + if frame is None or not isinstance(frame, np.ndarray): + raise RuntimeError("❌ classify_frame_with_rois 传入的 frame 无效") + + rois = load_rois(roi_file) + if not rois: + raise RuntimeError("ROI 文件为空") + + crops = crop_and_resize(frame, rois) + + results = [] + + for roi_img, idx in crops: + class_id, pred = rknn_classify(roi_img, model_path) + class_name = CLASS_NAMES.get(class_id, f"未知类别({class_id})") + + if class_id in [1, 2]: + final_class, score, p1, p2 = weighted_small_large(pred, threshold) + else: + final_class = class_name + score = float(pred[class_id]) + p1, p2 = float(pred[1]), float(pred[2]) + + results.append({ + "roi": idx, + "class": final_class, + "score": round(score, 4), + "p1": round(p1, 4), + "p2": round(p2, 4) + }) + + return results + + +# ===================================================== +# 示例调用 +# ===================================================== +if __name__ == "__main__": + model_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "yiliao_cls.rknn") + roi_file = "./roi_coordinates/1_rois.txt" + + frame = cv2.imread("./test_image/2.jpg") + + outputs = classify_frame_with_rois(model_path, frame, roi_file) + + for res in outputs: + print(res) + diff --git a/vision/roi_coordinates/1_rois.txt b/vision/roi_coordinates/1_rois.txt index bb8f71d..a01adc9 100644 --- a/vision/roi_coordinates/1_rois.txt +++ b/vision/roi_coordinates/1_rois.txt @@ -1 +1,2 @@ -859,810,696,328 +644, 608, 522, 246 + 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/test_feed.py b/vision/test_feed.py new file mode 100644 index 0000000..809b9a5 --- /dev/null +++ b/vision/test_feed.py @@ -0,0 +1,62 @@ +import time +from hardware.relay import RelayController +from hardware.transmitter import TransmitterController +from test_feed import start_feeding + +def start_feeding(): + """第一阶段下料:下料斗向模具车下料(低速)""" + print("开始下料") + # self.relay_controller.control + loc_relay=RelayController() + loc_mitter=TransmitterController(loc_relay) + initial_lower_weight=loc_mitter.read_data(2) + initial_upper_weight=loc_mitter.read_data(1) + first_finish_weight=0 + start_time=None + # mould_need_weight=4000 + while True: + current_weight = loc_mitter.read_data(2) + first_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<100: + start_time=None + loc_relay.control_lower_close() + break + print(f'------------已下料: {first_finish_weight}kg-------------') + time.sleep(1) + + #打开上料斗出砼门,开5就,开三分之一下 + loc_relay.control_upper_open_sync(5) + while True: + current_upper_weight = loc_mitter.read_data(1) + if current_upper_weight<3500: + #关5秒 + loc_relay.control_upper_close() + break + time.sleep(1) + + initial_lower_weight=loc_mitter.read_data(2) + while True: + current_weight = loc_mitter.read_data(2) + first_finish_weight=first_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<100: + start_time=None + loc_relay.control_lower_close() + break + print(f'------------已下料: {first_finish_weight}kg-------------') + time.sleep(1) + +if __name__ == "__main__": + start_feeding() + + \ No newline at end of file diff --git a/vision/test_safe_close.py b/vision/test_safe_close.py new file mode 100644 index 0000000..4d0ddb2 --- /dev/null +++ b/vision/test_safe_close.py @@ -0,0 +1,52 @@ +import time +import threading +import sys +import os + +# 添加项目根目录到sys.path +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from visual_callback import VisualCallback + +# 创建VisualCallback实例 +visual_callback = VisualCallback() + +# 模拟safe_control_lower_close执行 +def simulate_safe_close(): + """模拟safe_control_lower_close执行""" + print("\n=== 开始模拟safe_control_lower_close执行 ===") + # 直接调用safe_control_lower_close函数 + # 注意:这里需要访问_run_feed方法中的内部函数,所以我们需要一个间接的方式来测试 + # 我们可以通过修改标志位来模拟这个过程 + + # 1. 首先,让线程循环运行一段时间,观察正常情况下的行为 + print("\n1. 正常运行阶段 (5秒):") + time.sleep(5) + + # 2. 模拟safe_control_lower_close开始执行 + print("\n2. 模拟safe_control_lower_close开始执行:") + visual_callback._is_safe_closing = True + + # 3. 让线程循环运行一段时间,观察是否会跳过relay操作 + print("\n3. safe_control_lower_close执行中 (5秒):") + time.sleep(5) + + # 4. 模拟safe_control_lower_close执行完毕 + print("\n4. 模拟safe_control_lower_close执行完毕:") + visual_callback._is_safe_closing = False + + # 5. 再次观察正常运行 + print("\n5. 恢复正常运行 (5秒):") + time.sleep(5) + + print("\n=== 测试结束 ===") + +# 创建测试线程 +test_thread = threading.Thread(target=simulate_safe_close) +test_thread.start() + +# 主线程等待测试线程结束 +test_thread.join() + +# 关闭视觉回调实例 +visual_callback.shutdown() \ No newline at end of file diff --git a/vision/visual_callback copy 2.py b/vision/visual_callback copy 2.py new file mode 100644 index 0000000..4e7cd05 --- /dev/null +++ b/vision/visual_callback copy 2.py @@ -0,0 +1,277 @@ + +from config.settings import app_set_config +from hardware.relay import RelayController +import time +import threading +from datetime import datetime + +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.angle_mode = "normal" + self.relay_controller = RelayController() + self.init_weight = 100 + self.mould_finish_weight = 0 + self.mould_need_weight = 100 + self.finish_count = 0 + self.overflow = 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() + + # 创建并启动单个持续运行的线程 + self.callback_thread = threading.Thread( + target=self._run_thread_loop, + daemon=True + ) + self.callback_thread.start() + + # 初始化控制间隔和堆料状态跟踪属性 + self._last_overflow_state = False + self._last_control_time = 0 + self._initialized = True + + def angle_visual_callback(self, current_angle, overflow_detected): + """ + 视觉控制主逻辑,供外部推送数据 + 使用单个持续运行的线程,通过参数设置传递数据 + 如果线程正在处理数据,则丢弃此次推送 + """ + print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 回调:{current_angle}") + # 尝试获取处理锁,若失败则说明正在处理,丢弃数据 + if not self._is_processing.acquire(blocking=False): + print("回调线程仍在执行,丢弃此次推送数据") + return + + try: + # 更新参数 + self._current_angle = current_angle + if overflow_detected is not None: + self._overflow_detected = overflow_detected + # 通知线程有新数据可用 + self._new_data_available.set() + finally: + # 释放处理锁 + self._is_processing.release() + + + 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._process_angle_callback(current_angle, overflow_detected) + time.sleep(0.5) + + + + + def __del__(self): + self.relay_controller.close_all() + + def _process_angle_callback(self, current_angle, overflow_detected): + """ + 修复版控制逻辑 - 增加控制间隔和状态稳定性 + 堆料情况优先处理,非堆料情况保持适当控制间隔 + 堆料状态变化时立即处理,不受间隔限制 + """ + try: + # 先检查堆料状态 + is_overflow = overflow_detected in ["大堆料", "小堆料"] + self.overflow = is_overflow + + print(f"{self.angle_mode}") + + # 添加控制间隔检查 + current_time = time.time() + # 检查堆料状态是否发生变化 + state_changed = self._last_overflow_state != is_overflow + + if hasattr(self, '_last_control_time'): + time_since_last = current_time - self._last_control_time + # 防止抖动逻辑: + # 1. 非堆料且状态未变化:2秒控制间隔 + # 2. 其他情况(堆料或状态变化):0.5秒最小间隔,避免频繁操作 + if not is_overflow and not state_changed: + # 正常情况:2秒控制间隔 + if time_since_last < 2: + return + else: + # 堆料或状态变化情况:0.5秒最小间隔,防止抖动 + MIN_INTERVAL = 0.5 + if time_since_last < MIN_INTERVAL: + return + # 更新最后控制时间和堆料状态 + self._last_control_time = current_time + self._last_overflow_state = is_overflow + + if current_angle is None: + print("无法获取当前角度,跳过本次调整") + return + + print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 当前角度: {current_angle:.2f}°") + + # 定义控制参数 + TARGET_ANGLE = 30.0 + UPPER_DEAD_ZONE = 35.0 # 上死区 + LOWER_DEAD_ZONE = 25.0 # 下死区 + MIN_ANGLE = 5.0 # 最小安全角度 + + # 状态机逻辑 + if self.angle_mode == "normal": + if self.overflow and current_angle > UPPER_DEAD_ZONE: + # 只有角度较高且有堆料时才切换到reducing + self.angle_mode = "reducing" + print("检测到堆料且角度偏高,切换到减小模式") + else: + # 正常模式:维持适当开度 + if current_angle < MIN_ANGLE: + # 角度过小,适当开门 + self._pulse_control("open", 0.3) + elif current_angle > 60.0: # 安全上限 + self._pulse_control("close", 0.5) + else: + # 在正常范围内,根据重量比例控制 + if hasattr(self, 'mould_need_weight') and hasattr(self, 'mould_finish_weight'): + weight_ratio = self.mould_finish_weight / self.mould_need_weight if self.mould_need_weight > 0 else 0 + if weight_ratio < 0.8: + # 需要大量下料,保持开门 + if current_angle < 40.0: + self._pulse_control("open", 0.2) + else: + # 接近完成,半开控制 + if current_angle > 15.0: + self._pulse_control("close", 0.1) + else: + # 默认保持中等开度 + if current_angle < 20.0: + self._pulse_control("open", 0.2) + elif current_angle > 40.0: + self._pulse_control("close", 0.2) + + elif self.angle_mode == "reducing": + if not self.overflow: + # 堆料消失,但需要角度达到安全范围才切换 + if current_angle <= TARGET_ANGLE + 5.0: # 留有余量 + self.angle_mode = "normal" + print("堆料消失且角度合适,返回正常模式") + else: + # 堆料消失但角度仍高,缓慢恢复 + self._pulse_control("close", 0.1) + else: + # 仍有堆料,继续减小角度 + if current_angle > TARGET_ANGLE: + # 角度仍高,继续关门 + pulse_time = min(0.5, (current_angle - TARGET_ANGLE) * 0.02) + self._pulse_control("close", pulse_time) + else: + # 角度已达标,切换到维持模式 + self.angle_mode = "maintaining" + print(f"角度已降至{current_angle:.1f}°,进入维持模式") + + elif self.angle_mode == "maintaining": + if not self.overflow: + self.angle_mode = "normal" + print("堆料消除,返回正常模式") + else: + # 维持模式精确控制 + error = current_angle - TARGET_ANGLE + dead_zone = 3.0 + + if abs(error) < dead_zone: + # 在死区内,停止动作 + self._stop_door() + print(f"角度{current_angle:.1f}°在目标附近,保持静止") + elif error > 0: + # 角度偏高,轻微关门 + pulse_time = min(0.3, error * 0.01) + self._pulse_control("close", pulse_time) + print(f"角度偏高{error:.1f}°,关门{pulse_time:.2f}秒") + else: + # 角度偏低,轻微开门 + pulse_time = min(0.3, abs(error) * 0.01) + self._pulse_control("open", pulse_time) + print(f"角度偏低{abs(error):.1f}°,开门{pulse_time:.2f}秒") + + except Exception as e: + print(f"处理视觉回调时发生异常: {e}") + + def _pulse_control(self, action, duration): + """统一的脉冲控制方法""" + if duration <= 0: + return + + 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"开门脉冲: {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"关门脉冲: {duration:.2f}秒") + + def _stop_door(self): + """停止门运动""" + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + + @classmethod + def instance_exists(cls): + """检测实例是否存在""" + return cls._instance is not None + + def shutdown(self): + """关闭线程,清理资源""" + # 设置停止事件 + self._stop_event.set() + # 唤醒线程以便它能检测到停止事件 + self._new_data_available.set() + # 等待线程结束 + if self.callback_thread.is_alive(): + self.callback_thread.join(timeout=1.0) + +# 创建默认实例 +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) diff --git a/vision/visual_callback copy 3.py b/vision/visual_callback copy 3.py new file mode 100644 index 0000000..e40259c --- /dev/null +++ b/vision/visual_callback copy 3.py @@ -0,0 +1,273 @@ + +from config.settings import app_set_config +from hardware.relay import RelayController +import time +import threading +from datetime import datetime + +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.angle_mode = "normal" + self.relay_controller = RelayController() + self.init_weight = 100 + self.mould_finish_weight = 0 + self.mould_need_weight = 100 + self.finish_count = 0 + self.overflow = 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() + + # 创建并启动单个持续运行的线程 + self.callback_thread = threading.Thread( + target=self._run_thread_loop, + daemon=True + ) + self.callback_thread.start() + + # 初始化控制间隔和堆料状态跟踪属性 + self._last_overflow_state = False + self._last_control_time = 0 + self._initialized = True + + def angle_visual_callback(self, current_angle, overflow_detected): + """ + 视觉控制主逻辑,供外部推送数据 + 使用单个持续运行的线程,通过参数设置传递数据 + 如果线程正在处理数据,则丢弃此次推送 + """ + print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 回调:{current_angle}") + # 尝试获取处理锁,若失败则说明正在处理,丢弃数据 + if not self._is_processing.acquire(blocking=False): + print("回调线程仍在执行,丢弃此次推送数据") + return + + try: + # 更新参数 + self._current_angle = current_angle + if overflow_detected is not None: + self._overflow_detected = overflow_detected + # 通知线程有新数据可用 + self._new_data_available.set() + finally: + # 释放处理锁 + self._is_processing.release() + + + 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._process_angle_callback(current_angle, overflow_detected) + time.sleep(0.5) + + + def _process_angle_callback(self, current_angle, overflow_detected): + """ + 修复版控制逻辑 - 增加控制间隔和状态稳定性 + 堆料情况优先处理,非堆料情况保持适当控制间隔 + 堆料状态变化时立即处理,不受间隔限制 + """ + try: + # 先检查堆料状态 + is_overflow = overflow_detected in ["大堆料", "小堆料"] + self.overflow = is_overflow + + print(f"{self.angle_mode}") + + # 添加控制间隔检查 + current_time = time.time() + # 检查堆料状态是否发生变化 + state_changed = self._last_overflow_state != is_overflow + + if hasattr(self, '_last_control_time'): + time_since_last = current_time - self._last_control_time + # 防止抖动逻辑: + # 1. 非堆料且状态未变化:2秒控制间隔 + # 2. 其他情况(堆料或状态变化):0.5秒最小间隔,避免频繁操作 + # if not is_overflow and not state_changed: + if 1==1: + # 正常情况:2秒控制间隔 + if time_since_last < 1: + return + else: + # 堆料或状态变化情况:0.5秒最小间隔,防止抖动 + MIN_INTERVAL = 0.5 + if time_since_last < MIN_INTERVAL: + return + # 更新最后控制时间和堆料状态 + self._last_control_time = current_time + self._last_overflow_state = is_overflow + + if current_angle is None: + print("无法获取当前角度,跳过本次调整") + return + + print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 当前角度: {current_angle:.2f}°") + + # 定义控制参数 + TARGET_ANGLE = 30.0 + UPPER_DEAD_ZONE = 35.0 # 上死区 + LOWER_DEAD_ZONE = 25.0 # 下死区 + MIN_ANGLE = 5.0 # 最小安全角度 + + # 状态机逻辑 + if self.angle_mode == "normal": + if self.overflow and current_angle > UPPER_DEAD_ZONE: + # 只有角度较高且有堆料时才切换到reducing + self.angle_mode = "reducing" + print("检测到堆料且角度偏高,切换到减小模式") + else: + # 正常模式:维持适当开度 + if current_angle < MIN_ANGLE: + # 角度过小,适当开门 + self._pulse_control("open", 0.3) + elif current_angle > 60.0: # 安全上限 + self._pulse_control("close", 0.5) + else: + # 在正常范围内,根据重量比例控制 + if hasattr(self, 'mould_need_weight') and hasattr(self, 'mould_finish_weight'): + weight_ratio = self.mould_finish_weight / self.mould_need_weight if self.mould_need_weight > 0 else 0 + if weight_ratio < 0.8: + # 需要大量下料,保持开门 + if current_angle < 40.0: + self._pulse_control("open", 0.2) + else: + # 接近完成,半开控制 + if current_angle > 15.0: + self._pulse_control("close", 0.1) + else: + # 默认保持中等开度 + if current_angle < 20.0: + self._pulse_control("open", 0.2) + elif current_angle > 40.0: + self._pulse_control("close", 0.2) + + elif self.angle_mode == "reducing": + if not self.overflow: + # 堆料消失,但需要角度达到安全范围才切换 + if current_angle <= TARGET_ANGLE + 5.0: # 留有余量 + self.angle_mode = "normal" + print("堆料消失且角度合适,返回正常模式") + else: + # 堆料消失但角度仍高,缓慢恢复 + self._pulse_control("close", 0.1) + else: + # 仍有堆料,继续减小角度 + if current_angle > TARGET_ANGLE: + # 角度仍高,继续关门 + pulse_time = min(0.5, (current_angle - TARGET_ANGLE) * 0.02) + self._pulse_control("close", pulse_time) + else: + # 角度已达标,切换到维持模式 + self.angle_mode = "maintaining" + print(f"角度已降至{current_angle:.1f}°,进入维持模式") + + elif self.angle_mode == "maintaining": + if not self.overflow: + self.angle_mode = "normal" + print("堆料消除,返回正常模式") + else: + # 维持模式精确控制 + error = current_angle - TARGET_ANGLE + dead_zone = 3.0 + + if abs(error) < dead_zone: + # 在死区内,停止动作 + self._stop_door() + print(f"角度{current_angle:.1f}°在目标附近,保持静止") + elif error > 0: + # 角度偏高,轻微关门 + pulse_time = min(0.3, error * 0.01) + self._pulse_control("close", pulse_time) + print(f"角度偏高{error:.1f}°,关门{pulse_time:.2f}秒") + else: + # 角度偏低,轻微开门 + pulse_time = min(0.3, abs(error) * 0.01) + self._pulse_control("open", pulse_time) + print(f"角度偏低{abs(error):.1f}°,开门{pulse_time:.2f}秒") + + except Exception as e: + print(f"处理视觉回调时发生异常: {e}") + + def _pulse_control(self, action, duration): + """统一的脉冲控制方法""" + if duration <= 0: + return + + 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"开门脉冲: {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"关门脉冲: {duration:.2f}秒") + + def _stop_door(self): + """停止门运动""" + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + + @classmethod + def instance_exists(cls): + """检测实例是否存在""" + return cls._instance is not None + + def shutdown(self): + """关闭线程,清理资源""" + # 设置停止事件 + self._stop_event.set() + # 唤醒线程以便它能检测到停止事件 + self._new_data_available.set() + # 等待线程结束 + if self.callback_thread.is_alive(): + self.callback_thread.join(timeout=1.0) + +# 创建默认实例 +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) diff --git a/vision/visual_callback copy 4.py b/vision/visual_callback copy 4.py new file mode 100644 index 0000000..d4a4a15 --- /dev/null +++ b/vision/visual_callback copy 4.py @@ -0,0 +1,452 @@ + +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 + +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.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() + + # 添加下料斗门控制锁,防止两个线程同时控制 + self._door_control_lock = threading.Lock() + # 记录当前控制门的线程名称,用于调试 + self._current_controlling_thread = None + # 新增标志位:指示safe_control_lower_close是否正在执行 + self._is_safe_closing = False + + self._is_feed_start=False + + # 创建并启动单个持续运行的线程 + 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._last_overflow_state = False + self._last_control_time = 0 + self._initialized = True + + def angle_visual_callback(self, current_angle, overflow_detected): + """ + 视觉控制主逻辑,供外部推送数据 + 使用单个持续运行的线程,通过参数设置传递数据 + 如果线程正在处理数据,则丢弃此次推送 + """ + 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 + # 通知线程有新数据可用 + self._new_data_available.set() + finally: + # 释放处理锁 + self._is_processing.release() + + + 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: + print("------------下料启动----------------") + print("------------下料启动----------------") + print("------------下料启动----------------") + self.run_feed() + break + time.sleep(0.5) + + + + def run_feed(self): + """第一阶段下料:下料斗向模具车下料(低速)""" + print("--------------------开始下料--------------------") + loc_relay=self.relay_controller + loc_mitter=self.transmitter_controller + + initial_lower_weight=loc_mitter.read_data(2) + initial_upper_weight=loc_mitter.read_data(1) + first_finish_weight=0 + start_time=None + self.is_start_visual=True + + def safe_control_lower_close(): + """线程安全的下料斗关闭方法""" + 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}] 获得下料斗控制权,执行关闭操作") + loc_relay.control_lower_close() + self._current_controlling_thread = None + print(f"[{thread_name}] 释放下料斗控制权") + finally: + # 无论成功失败,都要重置标志位 + self._is_safe_closing = False + + while True: + loc_mitter.is_start_lower=True + 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<100: + start_time=None + self.is_start_visual=False + loc_mitter.is_start_lower=False + time.sleep(0.5) + safe_control_lower_close() + break + print(f'------------已下料: {first_finish_weight}kg-------------') + time.sleep(1) + + #打开上料斗出砼门,开5就,开三分之一下 + loc_relay.control_upper_open_sync(4) + upper_open_time=time.time() + while True: + print(f'------------上料斗向下料斗转移-------------') + loc_mitter.is_start_upper=True + current_upper_weight = loc_mitter.read_data(1) + if current_upper_weight<4000: + #关5秒 + loc_relay.control_upper_close() + loc_mitter.is_start_upper=False + break + else: + if time.time()-upper_open_time>2: + upper_open_time=time.time() + loc_relay.control_upper_open_sync(0.5) + else: + time.sleep(0.5) + # time.sleep(0.4) + + 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 True: + 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<100: + start_time=None + self.is_start_visual=False + loc_mitter.is_start_lower=False + time.sleep(0.5) + + safe_control_lower_close() + break + print(f'------------已下料: {first_finish_weight+second_finish_weight}kg-------------') + time.sleep(1) + + print(f'------------已完成-------------') + print(f'------------已完成-------------') + 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]} 当前角度: {current_angle:.2f}°") + + # 根据溢料状态动态调整目标角度 + if overflow_detected == "大堆料": + TARGET_ANGLE = 15.0 # 大堆料时控制在15度左右 + elif overflow_detected == "小堆料": + TARGET_ANGLE = 35.0 # 小堆料时控制在35度左右 + else: + TARGET_ANGLE = 56.0 # 未溢料时开到最大56度 + + # 确保目标角度在硬件范围内(5-56度) + TARGET_ANGLE = max(5.0, min(56.0, TARGET_ANGLE)) + + # PID控制参数 + KP = 0.1 # 比例系数 + KI = 0.01 # 积分系数 + KD = 0.05 # 微分系数 + + # 计算误差 + 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) + + # 更新历史值 + self._last_error = error + self._last_control_time = current_time + + # 状态机 + PID控制 + + if self.angle_mode == "normal": + self._normal_mode_advanced(current_angle, pid_output) + + 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): + """高级正常模式控制""" + if self.overflow: + self.angle_mode = "reducing" + print("检测到溢料,切换到减小模式") + return + + # 基于PID输出的智能控制 + control_threshold = 2.0 # 控制死区 + + 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: + # 在死区内,保持静止 + 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}] 释放下料斗控制权") + + @classmethod + def instance_exists(cls): + """检测实例是否存在""" + return cls._instance is not None + + def shutdown(self): + """关闭线程,清理资源""" + # 设置停止事件 + self._stop_event.set() + # 唤醒线程以便它能检测到停止事件 + self._new_data_available.set() + # 等待线程结束 + if self.callback_thread.is_alive(): + self.callback_thread.join(timeout=1.0) + +# 创建默认实例 +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) diff --git a/vision/visual_callback copy.py b/vision/visual_callback copy.py new file mode 100644 index 0000000..1beb39b --- /dev/null +++ b/vision/visual_callback copy.py @@ -0,0 +1,240 @@ + +from config.settings import app_set_config +from hardware.relay import RelayController +import time +import threading +from datetime import datetime + +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.angle_mode = "normal" + self.relay_controller = RelayController() + self.init_weight = 100 + self.mould_finish_weight = 0 + self.mould_need_weight = 100 + self.finish_count = 0 + self.overflow = 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() + + # 创建并启动单个持续运行的线程 + self.callback_thread = threading.Thread( + target=self._run_thread_loop, + daemon=True + ) + self.callback_thread.start() + + self._initialized = True + + def angle_visual_callback(self, current_angle, overflow_detected): + """ + 视觉控制主逻辑,供外部推送数据 + 使用单个持续运行的线程,通过参数设置传递数据 + 如果线程正在处理数据,则丢弃此次推送 + """ + print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 回调:{current_angle}") + # 尝试获取处理锁,若失败则说明正在处理,丢弃数据 + if not self._is_processing.acquire(blocking=False): + print("回调线程仍在执行,丢弃此次推送数据") + return + + try: + # 更新参数 + self._current_angle = current_angle + if overflow_detected is not None: + self._overflow_detected = overflow_detected + # 通知线程有新数据可用 + self._new_data_available.set() + finally: + # 释放处理锁 + self._is_processing.release() + + + 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._process_angle_callback(current_angle, overflow_detected) + time.sleep(0.5) + + + + + def __del__(self): + self.relay_controller.close_all() + + def _process_angle_callback(self, current_angle, overflow_detected): + """ + 内部方法,实际处理视觉回调逻辑 + 在异步线程中执行 + """ + try: + # print('current_angle:', current_angle, 'overflow_detected:', overflow_detected) + # return + # 检测溢出状态 + print(f"{self.angle_mode}") + self.overflow = overflow_detected in ["大堆料", "小堆料"] + if current_angle is None: + print("无法获取当前角度,跳过本次调整") + return + + print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 当前角度: {current_angle:.2f}°") + + if True: + # 状态机控制逻辑 + if self.angle_mode == "normal": + # 正常模式大于app_set_config.angle_threshold=60度 + if self.overflow: + self.angle_mode = "reducing" + else: + # 保持正常开门 + # print(f'当前重量:{self.mould_finish_weight:.2f}kg, 目标重量:{self.mould_need_weight:.2f}kg') + if self.mould_need_weight > 0: + if self.mould_finish_weight / self.mould_need_weight >= 0.8: + print(f"完成重量占比{self.mould_finish_weight/self.mould_need_weight:.2f},半开出砼门") + # 半开出砼门 + if current_angle > app_set_config.target_angle: + # 角度已降至目标范围,关闭出砼门 + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') + time.sleep(0.3) + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + else: + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + time.sleep(0.32) + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + else: + # 全开砼门 + if current_angle > app_set_config.angle_threshold: + #self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE,'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + else: + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE,'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN,'open') + else: + # 全开砼门 + if current_angle > app_set_config.angle_threshold: + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + else: + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + elif self.angle_mode == "reducing": + # 角度减小模式 + if self.overflow: + if current_angle <= app_set_config.target_angle: + # 角度已达到目标范围,仍有堆料,进入维持模式 + print(f"角度已降至{current_angle:.2f}°,仍有堆料,进入维持模式") + if current_angle <= app_set_config.min_angle: + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + time.sleep(0.1) + self.angle_mode = "maintaining" + else: + print(f"角度大于30,继续关闭") + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') + else: + # 无堆料,恢复正常模式 + print(f"角度已降至{current_angle:.2f}°,无堆料,恢复正常模式") + self.angle_mode = "normal" + + elif self.angle_mode == "maintaining": + # 维持模式 - 使用脉冲控制 + if not self.overflow: + # 堆料已消除,恢复正常模式 + print("堆料已消除,恢复正常模式") + self.angle_mode = "normal" + else: + # 继续维持角度控制 + print("进入维持模式") + # 关门时间 + if current_angle > app_set_config.target_angle: + # 角度已降至目标范围,关闭出砼门 + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') + #time.sleep(0.3) + # self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + else: + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + #time.sleep(0.32) + # self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + + elif self.angle_mode == "recovery": + # 恢复模式 - 逐步打开门 + if self.overflow: + # 又出现堆料,回到角度减小模式 + print("恢复过程中又检测到堆料,回到角度减小模式") + self.angle_mode = "maintaining" + else: + # 堆料已消除,恢复正常模式 + print("堆料已消除,恢复正常模式") + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self.angle_mode = "normal" + # else: + # 浇筑满,关闭下料门 + # self.relay_controller.control_lower_close() + except Exception as e: + print(f"处理视觉回调时发生异常: {e}") + + + @classmethod + def instance_exists(cls): + """检测实例是否存在""" + return cls._instance is not None + + def shutdown(self): + """关闭线程,清理资源""" + # 设置停止事件 + self._stop_event.set() + # 唤醒线程以便它能检测到停止事件 + self._new_data_available.set() + # 等待线程结束 + if self.callback_thread.is_alive(): + self.callback_thread.join(timeout=1.0) + +# 创建默认实例 +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) diff --git a/vision/visual_callback.py b/vision/visual_callback.py new file mode 100644 index 0000000..403bff2 --- /dev/null +++ b/vision/visual_callback.py @@ -0,0 +1,903 @@ + +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.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._initialized = True + self.plc_data=None + + + def angle_visual_callback(self, current_angle, overflow_detected): + """ + 视觉控制主逻辑,供外部推送数据 + 使用单个持续运行的线程,通过参数设置传递数据 + 如果线程正在处理数据,则丢弃此次推送 + """ + #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 + # 通知线程有新数据可用 + self._new_data_available.set() + finally: + # 释放处理锁 + 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): + """ + 接受视觉回调数据 + 线程主循环,持续运行 + 等待新数据,然后调用处理方法 + """ + 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._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(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("%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("--------------------开始下料(普通块)--------------------") + 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(11) + loc_time_count=1 + upper_open_time=time.time() + #第二次到下料斗还需要的量 + #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<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>2: + # if loc_time_count<6: + upper_open_time=time.time() + 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 + 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 = 25.0 # 未溢料时开到最大56度 + 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() + + 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) diff --git a/vision/visual_callback_1203.py b/vision/visual_callback_1203.py new file mode 100644 index 0000000..5476bde --- /dev/null +++ b/vision/visual_callback_1203.py @@ -0,0 +1,480 @@ + +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 + +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.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=True + + # 线程安全的参数传递 + self._current_angle = None + self._overflow_detected = None + 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 + # 新增标志位:指示safe_control_lower_close是否正在执行 + self._is_safe_closing = False + + self._is_feed_start=False + + # 创建并启动单个持续运行的线程 + 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._before_finish_time=None + self._is_finish=False + + # 初始化控制间隔和堆料状态跟踪属性 + self._last_overflow_state = False + self._last_control_time = 0 + self._initialized = True + + def angle_visual_callback(self, current_angle, overflow_detected): + """ + 视觉控制主逻辑,供外部推送数据 + 使用单个持续运行的线程,通过参数设置传递数据 + 如果线程正在处理数据,则丢弃此次推送 + """ + 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 + # 通知线程有新数据可用 + self._new_data_available.set() + finally: + # 释放处理锁 + self._is_processing.release() + + + 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: + print("------------下料启动----------------") + print("------------下料启动----------------") + print("------------下料启动----------------") + self.run_feed() + break + time.sleep(0.5) + + + 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 run_feed(self): + """第一阶段下料:下料斗向模具车下料(低速)""" + print("--------------------开始下料--------------------") + loc_relay=self.relay_controller + loc_mitter=self.transmitter_controller + + initial_lower_weight=loc_mitter.read_data(2) + initial_upper_weight=loc_mitter.read_data(1) + first_finish_weight=0 + start_time=None + self.is_start_visual=True + + + + while True: + loc_mitter.is_start_lower=True + 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<100: + 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-------------') + time.sleep(1) + + #打开上料斗出砼门,开5就,开三分之一下 + loc_relay.control_upper_open_sync(6) + loc_time_count=1 + upper_open_time=time.time() + while True: + print(f'------------上料斗向下料斗转移-------------') + loc_mitter.is_start_upper=True + current_upper_weight = loc_mitter.read_data(1) + if current_upper_weight<4000: + #关5秒 + loc_relay.control_upper_close_sync(4+loc_time_count) + loc_mitter.is_start_upper=False + break + else: + if time.time()-upper_open_time>3: + upper_open_time=time.time() + loc_relay.control_upper_open_sync(0.5) + loc_time_count=loc_time_count+0.5 + else: + time.sleep(0.5) + # time.sleep(0.4) + + 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 True: + 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<100: + 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-------------') + time.sleep(1) + + print(f'------------已完成-------------') + print(f'------------已完成-------------') + 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]} 当前角度: {current_angle:.2f}°") + + if overflow_detected == "未浇筑满": + if self._before_finish_time is None: + self._before_finish_time=current_time + self.safe_control_lower_close(1) + if time.time()-self._before_finish_time>3: + TARGET_ANGLE=25 + elif overflow_detected == "浇筑满": + self.is_start_visual=False + self._is_finish=True + self.safe_control_lower_close(3) + return + else: + TARGET_ANGLE=25 + # 根据溢料状态动态调整目标角度 + # 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.1 # 比例系数 + KI = 0.01 # 积分系数 + KD = 0.05 # 微分系数 + + # 计算误差 + 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) + + # 更新历史值 + self._last_error = error + self._last_control_time = current_time + + # 状态机 + PID控制 + + if self.angle_mode == "normal": + self._normal_mode_advanced(current_angle, pid_output) + + 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): + """高级正常模式控制""" + if self.overflow: + self.angle_mode = "reducing" + print("检测到溢料,切换到减小模式") + return + + # 基于PID输出的智能控制 + control_threshold = 2.0 # 控制死区 + + 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: + # 在死区内,保持静止 + 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) + + @classmethod + def instance_exists(cls): + """检测实例是否存在""" + return cls._instance is not None + + def shutdown(self): + """关闭线程,清理资源""" + # 设置停止事件 + self._stop_event.set() + # 唤醒线程以便它能检测到停止事件 + self._new_data_available.set() + # 等待线程结束 + if self.callback_thread.is_alive(): + self.callback_thread.join(timeout=1.0) + +# 创建默认实例 +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) diff --git a/vision/visual_callback_dq.py b/vision/visual_callback_dq.py new file mode 100644 index 0000000..32d7276 --- /dev/null +++ b/vision/visual_callback_dq.py @@ -0,0 +1,1064 @@ + +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 +import queue +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 + + #重量大于95%,停留时间2秒,其他的1秒 + self._weight_ratio_955=0.955 + #完成多少,忽略未浇筑满 + 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.db_queue=queue.Queue() + + # 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() + + """启动数据库监控""" + # self.db_thread = threading.Thread( + # target=self._monitor_db_loop, + # daemon=True, + # name='db_monitor' + # ) + # self.db_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_lower_weight=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 + if self._last_lower_weight>0: + _current_weight=self.transmitter_controller.read_data(2) + if _current_weight is not None: + with open('weight.txt', 'a') as f: + f.write(f"{self._last_lower_weight-_current_weight}\n") + 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'--------关闭完成-----------') + # try: + # self.db_queue.put_nowait({ + # "f":self._is_small_f, + # "Status": 3 + # }) + # except queue.Full: + # print("数据库队列已满,无法添加数据") + #记录重量 + _current_weight=self.transmitter_controller.read_data(2) + if _current_weight is not None: + self._last_lower_weight=_current_weight + 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 + _two_lower_weight=loc_mitter.read_data(2) + if _two_lower_weight is None: + _two_lower_weight=0 + if (current_upper_weight<3000 and current_upper_weight>0) or _two_lower_weight>3200: + #关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>=1: + #关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) + print(f'--------比例:{self._is_finish_ratio}-----------') + if self._is_finish_ratio>= self._weight_ratio_955: + time.sleep(2) + else: + 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) + # print(f'--------比例:{self._is_finish_ratio}-----------') + self._pulse_control('open',0.6) + time.sleep(0.3) + self._pulse_control('close',0.7) + if self._is_finish_ratio>= self._weight_ratio_955: + time.sleep(2) + else: + 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 + self.is_start_visual=False + # #关闭下料斗 + # self.safe_control_lower_close() + 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) + + # self.relay_controller._close_lower_5s + + 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