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/models.py b/busisness/models.py index e0b68d3..b4289a2 100644 --- a/busisness/models.py +++ b/busisness/models.py @@ -345,4 +345,8 @@ class LEDInfo: # 水温 WaterTemperature: str # 配方比例 - FormulaProportion: str \ No newline at end of file + 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/settings.py b/config/settings.py index 3c26960..d5006ad 100644 --- a/config/settings.py +++ b/config/settings.py @@ -5,13 +5,13 @@ 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 # 摄像头配置 @@ -23,14 +23,14 @@ class Settings: self.camera_channel = 1 self.camera_configs = { - 'cam1': { - 'type': 'ip', - 'ip': '192.168.250.60', - 'port': 554, - 'username': 'admin', - 'password': 'XJ123456', - 'channel': 1 - }, + # 'cam1': { + # 'type': 'ip', + # 'ip': '192.168.250.60', + # 'port': 554, + # 'username': 'admin', + # 'password': 'XJ123456', + # 'channel': 1 + # }, 'cam2': { 'type': 'ip', 'ip': '192.168.250.61', @@ -46,12 +46,13 @@ class Settings: 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 = 60.0 # 角度阈值 + self.angle_threshold = 50.0 # 角度阈值 self.angle_tolerance = 5.0 # 角度容差 # 变频器配置 @@ -71,7 +72,7 @@ class Settings: 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' @@ -84,7 +85,7 @@ class Settings: #需核实上下位漏斗容量 self.max_upper_volume = 2.4 # 上料斗容量(方) #下料到下料斗最大下到多少,并非最大容量 - self.max_lower_volume = 2.4 # 下料斗容量(方) + self.max_lower_volume = 2.2 # 下料斗容量(方) #led self.led_interval = 2 # LED闪烁间隔(秒) diff --git a/controller/main_controller.py b/controller/main_controller.py index f1cb2d1..0a81782 100644 --- a/controller/main_controller.py +++ b/controller/main_controller.py @@ -61,7 +61,7 @@ class MainController: def update_ui_notify(self, prop:str,value): """更新UI状态""" - # print(f"更新UI状态: {prop} = {value}") + print(f"更新UI状态: {prop} = {value}") if prop == "feed_status" and value == 4: # 盖板到位的情况, 更新 管片任务 (目前) diff --git a/core/state.py b/core/state.py index 7ceb71a..5966d86 100644 --- a/core/state.py +++ b/core/state.py @@ -32,6 +32,8 @@ class SystemState(QObject): 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 # 上料斗已下料次数 @@ -48,6 +50,7 @@ class SystemState(QObject): self.initial_upper_weight=0 self.initial_lower_weight=0 + # 错误计数 self.upper_weight_error_count = 0 @@ -101,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 0f41802..e94e9f3 100644 --- a/core/system.py +++ b/core/system.py @@ -60,36 +60,19 @@ class FeedingControlSystem: def initialize(self): """初始化系统""" print("初始化控制系统...") - - # 设置摄像头配置 - # self.camera_controller.set_config( - # camera_type=app_set_config.camera_type, - # ip=app_set_config.camera_ip, - # port=app_set_config.camera_port, - # username=app_set_config.camera_username, - # password=app_set_config.camera_password, - # channel=app_set_config.camera_channel - # ) - - # # 初始化摄像头 - # if not self.camera_controller.setup_capture(): - # raise Exception("摄像头初始化失败") - - # 加载视觉模型 - # if not self.vision_detector.load_models(): - # raise Exception("视觉模型加载失败") - - self.check_device_connectivity() + + # self.check_device_connectivity() # self.camera_controller.start_cameras() # if not app_set_config.debug_feeding: # 启动系统监控(要料,破拱)线程 self.start_monitoring() + # 启动视觉控制(角度、溢出)线程 # self.start_visual_control() # 启动对齐检查线程 - # self.start_alignment_check() + self.start_alignment_check() # 启动下料线程 self.start_lower_feeding() @@ -132,7 +115,7 @@ class FeedingControlSystem: """视觉控制循环""" while self.state.running: try: - print('visual_control') + # print('visual_control') current_frame = self.camera_controller.get_single_latest_frame() if current_frame is not None: # 执行视觉控制逻辑 @@ -241,16 +224,15 @@ class FeedingControlSystem: """启动LED流程""" while self.state.running: led_info = app_web_service.get_pouring_led() - if led_info and self.state.current_artifact: + 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: diff --git a/db/messages.db b/db/messages.db index f160244..a4553b1 100644 Binary files a/db/messages.db and b/db/messages.db differ diff --git a/db/three.db b/db/three.db index f3c3c06..9370717 100644 Binary files a/db/three.db and b/db/three.db differ diff --git a/feeding/__pycache__/controller.cpython-39.pyc b/feeding/__pycache__/controller.cpython-39.pyc index 54fd215..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 25b86f1..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 327cb62..e1bd80a 100644 --- a/feeding/controller.py +++ b/feeding/controller.py @@ -77,20 +77,20 @@ class FeedingController: current_time = time.time() # 检查下料斗破拱(只有在下料过程中才检查) - # 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: - # print("下料斗可能堵塞,启动破拱") - # self.state._lower_is_arch_=True - # self.relay_controller.control(self.relay_controller.BREAK_ARCH_LOWER, 'open') - # time.sleep(2) - # self.relay_controller.control(self.relay_controller.BREAK_ARCH_LOWER, 'close') - # self.state._lower_is_arch_=False + 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) < 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') + time.sleep(2) + self.relay_controller.control(self.relay_controller.BREAK_ARCH_LOWER, 'close') + self.state._lower_is_arch_=False - # self.state.last_lower_weight = lower_weight + self.state.last_lower_weight = lower_weight # 检查上料斗破拱(在上料斗向下料斗下料时检查) if self.state._feed_status == FeedStatus.FUpperToLower: # 在任何阶段都可能需要上料斗破拱 @@ -98,8 +98,8 @@ class FeedingController: 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) < 100) and \ - (current_time - self.state.last_weight_time) > 5: + 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') @@ -119,58 +119,85 @@ class FeedingController: 视觉控制主逻辑 """ # 检测是否溢料 - self.state.overflow_detected = self.vision_detector.detect_overflow(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) + current_angle =self.vision_detector.detect_angle(image=current_frame) if current_angle is None: print("无法获取当前角度,跳过本次调整") return self.state.last_angle = current_angle self.state._lower_angle=current_angle - print(f"当前角度: {current_angle:.2f}°, 溢料状态: {overflow}, 溢料返回: {self.state.overflow_detected}") - return + 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}") + # 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 and 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') + 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: + else: # 保持正常开门 30 # self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') - 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') - else: - self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') - self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + 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 current_angle <= app_set_config.target_angle + app_set_config.angle_tolerance: - # 角度已达到目标范围 - if overflow: - # 仍有堆料,进入维持模式 - print(f"角度已降至{current_angle:.2f}°,仍有堆料,进入维持模式") - 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') # 先打开门 + 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.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": @@ -178,8 +205,8 @@ class FeedingController: 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.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: # 继续维持角度控制 @@ -198,7 +225,7 @@ class FeedingController: 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(self.relay_controller.DOOR_LOWER_OPEN, 'close') self.relay_controller.control_lower_close() @@ -207,12 +234,13 @@ class FeedingController: 用于维持模式的脉冲控制 保持角度在目标范围内 """ - print("执行维持脉冲控制") - # 关门1秒 + 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(1.0) - # 开门1秒 - self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + time.sleep(0.2) + # 开门时间 self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') - time.sleep(1.0) + 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 5e4d88d..e33dd97 100644 --- a/feeding/process.py +++ b/feeding/process.py @@ -2,9 +2,10 @@ 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: @@ -19,7 +20,7 @@ class FeedingProcess: self.vision_detector = vision_detector self.camera_controller = camera_controller self.state = state - self.state._feed_status = FeedStatus.FCheckM + self.state._feed_status = FeedStatus.FNone #标志位用,是否是第一次运行 self.is_first_flag=True @@ -28,143 +29,36 @@ class FeedingProcess: 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: - """开始生产管片""" - loc_state.lower_feeding_stage = 4 - - # if app_set_config.debug_feeding: - # loc_state._feed_status = FeedStatus.FApiCheck - if self.state.vehicle_aligned: - loc_state._feed_status = FeedStatus.FApiCheck - print("检查模车") + 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 = self.mould_service.get_not_pour_artifacts() - if loc_modules: - # 取第一个未浇筑的管片 - #后续放入队列处理 - - 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: - 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识别的管理一致 - loc_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 - - #确认是否保存到数据库 - loc_state._feed_status = FeedStatus.FCheckGB - 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("上料斗向下料斗转移") + 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=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 - - - # 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") + 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("当前批次下料完成,关闭出砼门") - - if loc_state.overflow_detected=="浇筑满": - self.inverter_controller.control('stop') - - loc_state._mould_vibrate_status=0 - 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) - loc_state._feed_status = FeedStatus.FCheckM + print("振捣完成") + print("关闭所有网络继电器") + self.relay_controller.close_all() return def _start_feeding_stage(self): @@ -177,31 +71,37 @@ class FeedingProcess: """target_upper_weight:转移后剩下的上料斗重量""" # 如果低于单次,全部卸掉 - _max_lower_weight=app_set_config.max_lower_volume*self.state.density + _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_OPEN, 'close') - # self.relay_controller.control_lower_close() + # 确保下料斗出砼门关闭,同步关5秒 + self.relay_controller.control_lower_close() # 打开上料斗出砼门 + # 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 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") # 如果达到目标重量,则关闭上料斗出砼门 @@ -209,187 +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) - # 关闭上料斗出砼门 + # 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(app_set_config.alignment_check_interval) - - def feeding_stage_one(self,loc_state): + def feeding_stage(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_frequency=app_set_config.frequencies[0] - loc_state._mould_vibrate_status=1 + 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 - # 确保上料斗出砼门关闭 - # 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 + time.sleep(1) - - 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.relay_controller.control_upper_open_sync(5) + while True: - 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) + if loc_state._upper_weight<3000: + #关5秒 + self.relay_controller.control_upper_close() + break + 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(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.relay_controller.control(self.relay_controller.UPPER_TO_JBL, 'open') self.state._upper_door_position = 'default' diff --git a/feeding/processbak_copy b/feeding/processbak_copy new file mode 100644 index 0000000..8690307 --- /dev/null +++ b/feeding/processbak_copy @@ -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/rfid_service.py b/hardware/RFID/rfid_service.py index 87953a9..fe335a4 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推送的数据 @@ -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: diff --git a/hardware/relay.py b/hardware/relay.py index 5f82b12..1b920fd 100644 --- a/hardware/relay.py +++ b/hardware/relay.py @@ -35,8 +35,8 @@ class RelayController: 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': '00000000000601050006FF00'}, - self.DOOR_LOWER_CLOSE: {'open': '00000000000601050008FF00', 'close': '000000000006010500080000'}, + 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'}, @@ -58,11 +58,14 @@ class RelayController: 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 + # if app_set_config.debug_mode: + # return None try: byte_data = binascii.unhexlify(command_hex) @@ -102,32 +105,164 @@ class RelayController: 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_5s, daemon=True,name="close_upper_5s").start() + threading.Thread(target=self._close_upper_s, daemon=True,name="close_upper_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): """控制下料斗关""" - # 关闭下料斗出砼门 - self.control(self.DOOR_LOWER_CLOSE, 'open') + 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() + # 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() + threading.Thread(target=self._close_ring, daemon=True,name="_close_ring").start() - def _close_upper_5s(self): - time.sleep(5) + def _close_upper_s(self): + time.sleep(16) self.control(self.DOOR_UPPER_CLOSE, 'close') + print("上料斗关闭完成") def _close_lower_5s(self): - time.sleep(5) + 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 _close_break_arch_lower(self): + time.sleep(3) + self.control(self.BREAK_ARCH_LOWER, 'close') + + + def control_arch_upper_open(self): + """控制上料斗关""" + # 关闭下料斗出砼门 + self.control(self.BREAK_ARCH_UPPER, 'open') + # 异步5秒后关闭 + threading.Thread(target=self._close_break_arch_upper, daemon=True,name="_close_break_arch_upper").start() + def _close_break_arch_upper(self): + time.sleep(3) + 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 cb764e4..83f527b 100644 --- a/hardware/transmitter.py +++ b/hardware/transmitter.py @@ -3,12 +3,19 @@ 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: { # 上料斗 @@ -73,7 +80,8 @@ class TransmitterController: self.relay_controller.modbus_client.close() # 直接读取 变送器返回的数据并解析 - def read_data(self, transmitter_id): + def read_data_normal(self, transmitter_id): + """ Args: transmitter_id 为1 表示上料斗, 为2 表示下料斗 return: 读取成功返回重量 weight: int, 失败返回 None @@ -82,9 +90,7 @@ class TransmitterController: BUFFER_SIZE= 1024 IP = None PORT = None - weight = 0 - - + weight = 0 if transmitter_id == 1: # 上料斗变送器的信息: IP = ini_manager.upper_transmitter_ip @@ -97,8 +103,86 @@ class TransmitterController: if not IP or not PORT: print(f"未配置变送器 {transmitter_id} 的IP或PORT") return 0 - if app_set_config.debug_mode: - print(f"调试模式,未读数据({transmitter_id},IP: {IP}:{PORT})") + 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(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) diff --git a/hardware/upper_plc.py b/hardware/upper_plc.py new file mode 100644 index 0000000..370f188 --- /dev/null +++ b/hardware/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/main.py b/main.py index 670259c..1048d54 100644 --- a/main.py +++ b/main.py @@ -4,31 +4,139 @@ 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.control(replay_controller.DOOR_LOWER_CLOSE, 'close') - # relay_c=RelayController() - # self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') - system = FeedingControlSystem() - # 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') - # system.relay_controller._close_upper_5s() + #5-10度徘徊 + # replay_controller.control(replay_controller.DOOR_LOWER_CLOSE, 'open') + # time.sleep(0.1) + # replay_controller.control(replay_controller.DOOR_LOWER_CLOSE, 'close') - # system.relay_controller.control(system.relay_controller.DOOR_UPPER_OPEN, 'open') + #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 - while True: - time.sleep(2) + + # 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 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/service/mould_service.py b/service/mould_service.py index 028adec..5d52ed2 100644 --- a/service/mould_service.py +++ b/service/mould_service.py @@ -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获取数据 @@ -124,7 +124,9 @@ class MouldService: TotMete=data.get('TotMete', ''), BetonVolumeAlready=data.get('BetonVolumeAlready', ''), WaterTemperature=data.get('WaterTemperature', ''), - FormulaProportion=data.get('FormulaProportion', '') + FormulaProportion=data.get('FormulaProportion', ''), + DayStrengthValue=data.get('DayStrengthValue', ''), + NihtStrengthValue=data.get('NihtStrengthValue', '') ) return led_info 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_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..7bb76f9 --- /dev/null +++ b/test_weight.py @@ -0,0 +1,31 @@ +# main.py +import time +from datetime import datetime +from config.settings import app_set_config +from core.system import FeedingControlSystem +from hardware import relay +from hardware.relay import RelayController +from hardware.transmitter import TransmitterController +import threading +import time +import cv2 +import os +from core.state import FeedStatus + + +def main(): + 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 index 8952bea..2de5d74 100644 --- a/tests/Investor485test.py +++ b/tests/Investor485test.py @@ -203,7 +203,7 @@ class InovanceMD520: def main(): # 创建变频器对象 - inverter = InovanceMD520(port='COM4', baudrate=9600) + inverter = InovanceMD520(port='COM3', baudrate=9600) # 连接串口 if not inverter.connect(): diff --git a/tests/test_rfid.py b/tests/test_rfid.py index 301bb4b..c73936e 100644 --- a/tests/test_rfid.py +++ b/tests/test_rfid.py @@ -3,15 +3,20 @@ 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 -def test_data_callback(raw_data): + +def test_data_callback(status,raw_data): """ 测试用的数据接收回调函数 """ - print(f"[回调] 收到RFID数据: {raw_data}") + if status: + print(f"[回调] 收到RFID数据: {raw_data}") + else: + print(f"[回调] 读取RFID数据为空或失败") def test_rfid_functions(): @@ -19,7 +24,7 @@ def test_rfid_functions(): 测试RFIDHardware的主要功能 """ # 初始化RFID控制器 - rfid = rfid_service(host='192.168.250.67', port=6000) + rfid = rfid_service(host='192.168.250.77', port=6000) # print("=== RFID硬件测试开始 ===") @@ -35,7 +40,7 @@ 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={ @@ -59,8 +64,8 @@ def test_rfid_functions(): # print("读取读写器信息失败") # 测试设置功率 (仅演示,实际使用时根据需要调整) - # print("\n3. 测试设置功率 (演示,不实际执行):") - # power_success = rfid.set_power(address=0x00, power_value=16) + # print("\n3. 测试设置功率:") + # power_success = rfid.set_power(address=0x00, power_value=26) # print(f"功率设置{'成功' if power_success else '失败'}") # # 测试设置读卡间隔 (仅演示) 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/vision/camera.py b/vision/camera.py index 664108e..5c417ab 100644 --- a/vision/camera.py +++ b/vision/camera.py @@ -119,7 +119,7 @@ class DualCameraController: try: # 初始化摄像头和队列 - for camera_id in ['cam1', 'cam2']: + for camera_id in ['cam2']: rtsp_url = self._build_rtsp_url(camera_id) cap = cv2.VideoCapture(rtsp_url) @@ -135,7 +135,7 @@ class DualCameraController: # 启动捕获线程 self.stop_event.clear() - for camera_id in ['cam1', 'cam2']: + for camera_id in ['cam2']: thread = threading.Thread( target=self._capture_thread, args=(camera_id,), @@ -155,6 +155,7 @@ class DualCameraController: 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 @@ -236,15 +237,15 @@ class DualCameraController: dt_t1 = None # 获取cam1的最新帧 - if not self.frame_queues['cam1'].empty(): - dt_t1, frame_latest = self.frame_queues['cam1'].queue[-1] + 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 + # 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 @@ -427,14 +428,6 @@ class CameraController: if __name__ == "__main__": # 创建双摄像头控制器 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', diff --git a/vision/detector.py b/vision/detector.py index d87c5a7..178ee78 100644 --- a/vision/detector.py +++ b/vision/detector.py @@ -62,8 +62,12 @@ class VisionDetector: """ 通过视觉系统获取当前出砼门角度 """ - # image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + + # 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=app_set_config.angle_model_path, image=image diff --git a/vision/obb_angle_model/obb_angle.py b/vision/obb_angle_model/obb_angle.py index 9b3dd46..cf28d9c 100644 --- a/vision/obb_angle_model/obb_angle.py +++ b/vision/obb_angle_model/obb_angle.py @@ -229,6 +229,8 @@ if __name__ == "__main__": # 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}°") diff --git a/vision/roi_coordinates/1_rois.txt b/vision/roi_coordinates/1_rois.txt index c9a20a1..a01adc9 100644 --- a/vision/roi_coordinates/1_rois.txt +++ b/vision/roi_coordinates/1_rois.txt @@ -1 +1,2 @@ -644,608,522,246 +644, 608, 522, 246 + 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..a831a22 --- /dev/null +++ b/vision/visual_callback.py @@ -0,0 +1,634 @@ + +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 +from hardware.upper_plc import OmronFinsPollingService + +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=True + + # 创建并启动单个持续运行的线程 + 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_before_finish=False + self._is_finish=False + # self._is_finish_ratio=0 + #是否震荡关闭平面(设想90%后关闭几秒,让液面平整) + # self._is_zd_close=False + + # 初始化控制间隔和堆料状态跟踪属性 + self._last_overflow_state = False + self._last_control_time = 0 + self._initialized = True + self.plc_data=None + # self.plc_data=5 + self.plc_service = OmronFinsPollingService("192.168.250.233") + self.plc_service.register_data_callback(self.on_plc_update) + # self.plc_service.register_status_callback(self.on_status_change) + self.plc_service.start_polling(interval=2.0) + + def angle_visual_callback(self, current_angle, overflow_detected): + """ + 视觉控制主逻辑,供外部推送数据 + 使用单个持续运行的线程,通过参数设置传递数据 + 如果线程正在处理数据,则丢弃此次推送 + """ + #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: + if self.plc_data==5: + self.run_feed() + break + else: + print("-----------上料斗未就位----------------") + print("-----------上料斗未就位----------------") + + 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 + # need_total_weight=1.91*2416 + start_time=None + self.is_start_visual=True + + if initial_lower_weight>100: + #下料斗的料全部下完 + while not self._is_finish: + loc_mitter.is_start_lower=True + current_weight = loc_mitter.read_data(2) + first_finish_weight=initial_lower_weight-current_weight + if current_weight<500: + # 破拱控制 + if start_time is None or time.time()-start_time>5: + start_time=time.time() + loc_relay.control_arch_lower_open() + + if current_weight<250: + start_time=None + self.is_start_visual=False + loc_mitter.is_start_lower=False + time.sleep(0.5) + self.safe_control_lower_close() + break + print(f'------------已下料: {first_finish_weight}kg-------------') + time.sleep(1) + + # _current_lower_weight=loc_mitter.read_data(2) + # 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-------------') + + if self.plc_data==5: + print(f'------------上料斗向下料斗转移(留3000KG)-------------') + #打开上料斗出砼门,开5就,开三分之一下 + loc_relay.control_upper_open_sync(6) + loc_time_count=1 + upper_open_time=time.time() + while not self._is_finish: + loc_mitter.is_start_upper=True + current_upper_weight = loc_mitter.read_data(1) + if current_upper_weight<3000: + #关5秒,loc_time_count多关一秒 + loc_relay.control_upper_close_sync(4+loc_time_count) + loc_mitter.is_start_upper=False + break + else: + if time.time()-upper_open_time>5: + upper_open_time=time.time() + loc_relay.control_upper_open_sync(0.5) + loc_time_count=loc_time_count+0.5 + 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 not self._is_finish: + loc_mitter.is_start_lower=True + current_weight = loc_mitter.read_data(2) + second_finish_weight=initial_lower_weight-current_weight + if current_weight<500: + #关5秒 + if start_time is None or time.time()-start_time>5: + start_time=time.time() + loc_relay.control_arch_lower_open() + if current_weight<250: + start_time=None + self.is_start_visual=False + loc_mitter.is_start_lower=False + time.sleep(0.5) + self.safe_control_lower_close() + break + print(f'------------已下料: {first_finish_weight+second_finish_weight}kg-------------') + 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-------------') + if self.plc_data==5: + #第二次上料斗向下料斗转移 + loc_relay.control_upper_open_sync(6) + loc_time_count=1 + upper_open_time=time.time() + loc_mitter.is_start_upper=False + loc_mitter.test_upper_weight=3000 + #第二次到下料斗还需要的量 + #loc_left_need_weight=need_total_weight-first_finish_weight + initial_upper_weight=loc_mitter.read_data(1) + start_time=None + while not self._is_finish: + # print(f'------------上料斗向下料斗转移22222-------------') + loc_mitter.is_start_upper=True + current_upper_weight = loc_mitter.read_data(1) + if current_upper_weight<500: + #关5秒,loc_time_count多关一秒 + if start_time is None or time.time()-start_time>4: + start_time=time.time() + loc_relay.control_arch_upper_open() + if current_upper_weight<100: + start_time=None + loc_relay.control_upper_close_sync(4+loc_time_count) + loc_mitter.is_start_upper=False + break + else: + if time.time()-upper_open_time>3: + upper_open_time=time.time() + loc_relay.control_upper_open_sync(0.8) + loc_time_count=loc_time_count+0.8 + else: + time.sleep(0.5) + # time.sleep(0.4) + + #第二次下料斗转移到模具车 + if not self._is_finish: + self.is_start_visual=True + # loc_mitter.is_start_lower=False + # loc_mitter.test_lower_weight=2000 + # initial_lower_weight=loc_mitter.read_data(2) + + # while not self._is_finish: + # loc_mitter.is_start_lower=True + # current_weight = loc_mitter.read_data(2) + # second_finish_weight=initial_lower_weight-current_weight + # # self._is_finish_ratio=(second_finish_weight+first_finish_weight)/need_total_weight + # # print(f'------------已下料比例: {self._is_finish_ratio}-------------') + + # if current_weight<500: + # #关5秒 + # if start_time is None or time.time()-start_time>5: + # start_time=time.time() + # loc_relay.control_arch_lower_open() + + # print(f'------------已下料: {first_finish_weight+second_finish_weight}kg-------------') + # time.sleep(1) + + # _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.is_start_visual=False + self._is_finish=True + self.safe_control_lower_close(3) + elif overflow_detected=="大堆料": + print(f'--------未浇筑满,大堆料,不开关-----------') + else: + self._pulse_control('open',0.5) + time.sleep(0.3) + self._pulse_control('close',0.6) + time.sleep(2) + self._is_before_finish=True + return + elif overflow_detected == "浇筑满": + self.is_start_visual=False + self._is_finish=True + self.safe_control_lower_close(3) + return + else: + self._before_finish_time=None + # if self._is_finish_ratio>=0.8: + # if overflow_detected == "大堆料": + # TARGET_ANGLE = 10.0 # 大堆料时控制在15度左右 + # elif overflow_detected == "小堆料": + # TARGET_ANGLE = 20.0 # 小堆料时控制在35度左右 + # else: + # TARGET_ANGLE = 25.0 # 未溢料时开到最大56度 + # if self._is_finish_ratio>=0.9 and not self._is_zd_close: + # #关闭10秒,直接关闭,不等待 + # print('-----------------达到90% 关闭--------------------') + # self._is_zd_close=True + # self.safe_control_lower_close() + # time.sleep(10) + # return + + # else: + #根据溢料状态动态调整目标角度 + if overflow_detected == "大堆料": + TARGET_ANGLE = 15.0 # 大堆料时控制在15度左右 + elif overflow_detected == "小堆料": + TARGET_ANGLE = 25.0 # 小堆料时控制在35度左右 + else: + TARGET_ANGLE = 45.0 # 未溢料时开到最大56度 + + # 确保目标角度在硬件范围内(5-56度) + TARGET_ANGLE = max(5.0, min(56.0, TARGET_ANGLE)) + + # PID控制参数 + KP = 0.15 # 比例系数 + 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) + + 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)