From d6ad01274ac087e51297eb8863256a0d17ab8fb7 Mon Sep 17 00:00:00 2001 From: fujinliang Date: Mon, 9 Feb 2026 11:36:37 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=98=E9=A2=91=E5=99=A8=E9=9B=86=E6=88=90?= =?UTF-8?q?=E4=BB=A5=E5=8F=8A=E5=A2=9E=E5=8A=A0=E7=82=B9=E5=8A=A8=E6=8E=A7?= =?UTF-8?q?=E5=88=B6(0209)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- busisness/blls.py | 149 ++- busisness/dals.py | 186 ++- busisness/models.py | 196 ++- config/ini_manager.py | 21 + config/opc_config.ini | 1 + core/pd_system.py | 97 ++ core/state.py | 2 +- core/system copy.py | 288 ++++ core/system.py | 501 ++++--- core/system_state.py | 178 +++ db/messages.db | Bin 12288 -> 12288 bytes db/three.db | Bin 53248 -> 151552 bytes db/three.db-shm | Bin 32768 -> 0 bytes db/three.db-wal | Bin 4152 -> 0 bytes doc/table表设计.doc | Bin 74752 -> 78336 bytes doc/控制程序对接.docx | Bin 19357 -> 19607 bytes feeding/__pycache__/process.cpython-39.pyc | Bin 4232 -> 6892 bytes feeding/controller.py | 1381 +++++++++++++++++--- feeding/process copy.py | 167 +++ feeding/process.py | 480 +++++-- hardware/inverter.py | 163 ++- hardware/relay.py | 18 +- hardware/transmitter copy.py | 184 --- hardware/transmitter.py | 12 +- hardware/transmitter_bak.py | 200 +++ hardware/upper_plc.py | 2 + opc/debug_nodes.py | 183 +++ opc/opcua_client_feed.py | 274 ++++ opc/opcua_client_subscription.py | 47 +- opc/opcua_client_test.py | 363 ++++- opc/opcua_server_test.py | 237 ++++ service/api_http_client.py | 2 +- service/mould_service.py | 126 +- settings.ini | 6 + test copy.py | 88 +- test_weight.py | 3 +- tests/485test.py | 394 +----- tests/api_service_test.py | 93 ++ tests/test_rfid.py | 2 +- vision/camera_picture.py | 87 ++ vision/muju_cls/muju_utils.py | 33 +- vision/visual_callback.py | 9 +- vision/visual_callback_dq copy.py | 1192 +++++++++++++++++ vision/visual_callback_dq.py | 720 +++++++--- vision/weight.txt | 654 +++++++++ 45 files changed, 7161 insertions(+), 1578 deletions(-) create mode 100644 core/pd_system.py create mode 100644 core/system copy.py create mode 100644 core/system_state.py delete mode 100644 db/three.db-shm delete mode 100644 db/three.db-wal create mode 100644 feeding/process copy.py delete mode 100644 hardware/transmitter copy.py create mode 100644 hardware/transmitter_bak.py create mode 100644 opc/debug_nodes.py create mode 100644 opc/opcua_client_feed.py create mode 100644 opc/opcua_server_test.py create mode 100644 tests/api_service_test.py create mode 100644 vision/camera_picture.py create mode 100644 vision/visual_callback_dq copy.py create mode 100644 vision/weight.txt diff --git a/busisness/blls.py b/busisness/blls.py index 8111cc7..0155fa6 100644 --- a/busisness/blls.py +++ b/busisness/blls.py @@ -3,12 +3,11 @@ # # 添加项目根目录到Python路径 # sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) - from dataclasses import fields from typing import List, Optional, Dict, Any from datetime import datetime -from .models import ArtifactInfoModel, PDRecordModel -from .dals import ArtifactDal, PDRecordDal +from busisness.models import ArtifactInfoModel, PDRecordModel +from busisness.dals import ArtifactDal, PDRecordDal class ArtifactBll: @@ -26,37 +25,54 @@ class ArtifactBll: """更新管片任务状态""" return self.dal.update_artifact(artifact_id, {"Status": status}) - def finish_artifact_task(self, artifact_id: str,beton_volume) -> bool: + def start_produce(self, code: str) -> bool: + """开始管片生产""" + return self.dal.update_by_modulecode(code, {'Status': 2,'BeginTime':datetime.now()}) + + def finish_produce(self, code: str,weight:float) -> bool: + """完成管片生产""" + return self.dal.update_by_modulecode(code, {'Status': 3,'EndTime':datetime.now(),'FBetonVolume':weight}) + + + def finish_artifact_task(self, artifact_id: str,beton_volume:float) -> bool: """完成管片任务""" return self.dal.update_artifact(artifact_id, {"Status": 3,"BetonVolume":beton_volume,"EndTime":datetime.now()}) def insert_artifact_task(self,model:ArtifactInfoModel) -> bool: """插入管片任务""" - if self.dal.exists_by_id(model.ArtifactID): - return False - return self.dal.insert_artifact({ - "ArtifactID": model.ArtifactID, - "ArtifactActionID": model.ArtifactActionID, - "ArtifactIDVice1": model.ArtifactIDVice1, - "ProduceRingNumber": model.ProduceRingNumber, - "MouldCode": model.MouldCode, - "SkeletonID": model.SkeletonID, - "RingTypeCode": model.RingTypeCode, - "SizeSpecification": model.SizeSpecification, - "BuriedDepth": model.BuriedDepth, - "BlockNumber": model.BlockNumber, - "BetonVolume": model.BetonVolume, - "BetonTaskID": model.BetonTaskID, - "HoleRingMarking": model.HoleRingMarking, - "GroutingPipeMarking": model.GroutingPipeMarking, - "PolypropyleneFiberMarking": model.PolypropyleneFiberMarking, - "PStatus":1, - "Status": 2, - "Source": model.Source, - "BeginTime": model.BeginTime, - "OptTime": datetime.now(), - }) + if self.dal.get_artifactid(model.MouldCode): + return False + return self.dal.insert_artifact(model) + + def save_artifact_task(self,model:ArtifactInfoModel) -> int: + + """保存管片任务(存在ModuleCode更新,不存在插入,存在ArtifactID不处理)""" + _artifactid=self.dal.get_artifactid(model.MouldCode) + if _artifactid is None: + return self.dal.insert_artifact(model) + else: + if not _artifactid and model.ArtifactID: + update_dict={ + 'ArtifactID':model.ArtifactID, + 'ArtifactActionID':model.ArtifactActionID, + 'ArtifactIDVice1':model.ArtifactIDVice1, + 'ProduceRingNumber':model.ProduceRingNumber, + 'SkeletonID':model.SkeletonID, + 'RingTypeCode':model.RingTypeCode, + 'SizeSpecification':model.SizeSpecification, + 'BuriedDepth':model.BuriedDepth, + 'BlockNumber':model.BlockNumber, + 'HoleRingMarking':model.HoleRingMarking, + 'GroutingPipeMarking':model.GroutingPipeMarking, + 'PolypropyleneFiberMarking':model.PolypropyleneFiberMarking, + 'BetonVolume':model.BetonVolume, + 'BetonTaskID':model.BetonTaskID, + } + return self.dal.update_by_modulecode(model.MouldCode, update_dict) + + return 1 + def insert_artifact_bycode(self,model: dict) -> bool: @@ -93,24 +109,77 @@ class PDRecordBll: """获取PD官片任务数据""" return self.dal.get_top_pd(5, "ID desc") + def get_last_pds(self,mould_code:str) -> List[PDRecordModel]: + """获取当前浇筑的后三片的派据(有可能其中一项为空,但需要保证第一条有记录)""" + return self.dal.get_last_pds(1,mould_code) + + def insert_PD_record(self,model:PDRecordModel) -> bool: + """保存PD官片任务(存在更新,不存在插入)""" + if self.dal.exists_artifactid(model.ArtifactActionID): + return False + # return self.update_PD_record(model.ArtifactActionID,{"Status": model.Status,"OptTime": datetime.now()}) + else: + return self.dal.insert_PD_record(model) + + def finish_pd(self, code: str,status:int) -> bool: + """完成管片生产""" + return self.dal.update_by_modulecode(code, {'Status': status,'EndTime':datetime.now()}) + + def start_pd(self, code: str,volumn:float) -> bool: + """开始管片生产""" + return self.dal.update_by_modulecode(code, {'Status': 2,'OptTime':datetime.now(),'FBetonVolume':volumn}) + + def update_PD_record(self,artifact_action_id: int, update_fields: dict) -> bool: + """更新PD官片任务状态""" + return self.dal.update_pd(artifact_action_id, update_fields) + + def save_PD_record(self,model:PDRecordModel) -> int: + + """保存PD官片任务(存在ModuleCode更新,不存在插入,存在ArtifactID不处理)""" + _artifactid=self.dal.get_artifactid(model.MouldCode) + if _artifactid is None: + return self.dal.insert_PD_record(model) + else: + if not _artifactid and model.ArtifactID: + update_dict={ + 'ArtifactID':model.ArtifactID, + 'ArtifactActionID':model.ArtifactActionID, + 'TaskID':model.TaskID, + 'ProjectName':model.ProjectName, + 'ProduceMixID':model.ProduceMixID, + 'BetonVolume':model.BetonVolume, + 'BetonGrade':model.BetonGrade, + 'BuriedDepth':model.BuriedDepth, + 'SkeletonID':model.SkeletonID, + 'RingTypeCode':model.RingTypeCode, + 'SizeSpecification':model.SizeSpecification, + 'BlockNumber':model.BlockNumber, + 'BetonVolume':model.BetonVolume, + 'PlannedVolume':model.PlannedVolume + } + return self.dal.update_by_modulecode(model.MouldCode, update_dict) + + return 1 + + if __name__ == "__main__": - artifact_dal = ArtifactBll() + # artifact_dal = ArtifactBll() - artifacts = artifact_dal.get_artifact_task() + # artifacts = artifact_dal.get_artifact_task() - print("\n打印artifacts数据:") - for i, artifact in enumerate(artifacts): - artifact_id = artifact.ArtifactID - # 如果是数据类对象,转换为字典输出 - print(artifact.MouldCode) - if hasattr(artifact, "__dataclass_fields__"): - print(f"第{i+1}条: {artifact.__dict__}") - else: - print(f"第{i+1}条: {artifact}") + # print("\n打印artifacts数据:") + # for i, artifact in enumerate(artifacts): + # artifact_id = artifact.ArtifactID + # # 如果是数据类对象,转换为字典输出 + # print(artifact.MouldCode) + # if hasattr(artifact, "__dataclass_fields__"): + # print(f"第{i+1}条: {artifact.__dict__}") + # else: + # print(f"第{i+1}条: {artifact}") pdrecord_dal = PDRecordBll() - pdrecords = pdrecord_dal.get_PD_record() + pdrecords = pdrecord_dal.get_last_pds("SHR2B2-11") # print(pdrecords[0].MouldCode) print("\n打印pdrecords数据:") for i, record in enumerate(pdrecords): diff --git a/busisness/dals.py b/busisness/dals.py index 1cc1a2a..f796f35 100644 --- a/busisness/dals.py +++ b/busisness/dals.py @@ -1,7 +1,10 @@ +import os +import sys +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from dataclasses import fields from typing import List, Optional, Dict, Any -from datetime import datetime -from .models import ArtifactInfoModel,PDRecordModel +from datetime import datetime, timedelta +from busisness.models import ArtifactInfoModel,PDRecordModel from common.sqlite_handler import SQLiteHandler @@ -33,8 +36,8 @@ class ArtifactDal(BaseDal): artifacts = [] for row in results: # 过滤字典,只保留模型中定义的字段 - filtered_data = filter_dict_for_model(dict(row), ArtifactInfoModel) - artifact = ArtifactInfoModel(**filtered_data) + # filtered_data = filter_dict_for_model(dict(row), ArtifactInfoModel) + artifact = ArtifactInfoModel(**row) artifacts.append(artifact) return artifacts @@ -57,26 +60,9 @@ class ArtifactDal(BaseDal): artifacts = [] for row in results: # 保证row的变量和模板变量一致 - artifact = ArtifactInfoModel() - artifact.ArtifactID=row["ArtifactID"] - artifact.ArtifactActionID=row["ArtifactActionID"] - artifact.ArtifactIDVice1=row["ArtifactIDVice1"] - artifact.ProduceRingNumber=row["ProduceRingNumber"] - artifact.MouldCode=row["MouldCode"] - artifact.SkeletonID=row["SkeletonID"] - artifact.RingTypeCode=row["RingTypeCode"] - artifact.SizeSpecification=row["SizeSpecification"] - artifact.BuriedDepth=row["BuriedDepth"] - artifact.BlockNumber=row["BlockNumber"] - artifact.BetonVolume=row["BetonVolume"] - artifact.BetonTaskID=row["BetonTaskID"] - artifact.HoleRingMarking=row["HoleRingMarking"] - artifact.GroutingPipeMarking=row["GroutingPipeMarking"] - artifact.PolypropyleneFiberMarking=row["PolypropyleneFiberMarking"] - artifact.Status=row["Status"] - artifact.BeginTime=row["BeginTime"] + artifact = ArtifactInfoModel(**row) artifacts.append(artifact) - + return artifacts except Exception as e: print(f"获取top构件任务失败: {e}") @@ -103,7 +89,6 @@ class ArtifactDal(BaseDal): try: sql = "SELECT count(1) FROM ArtifactTask WHERE MouldCode = ? and OptTime>?" results = self.db_dao.execute_read(sql, (module_code,datetime.now().replace(hour=0, minute=0, second=0, microsecond=0))) - rows = list(results) if rows[0][0] == 1: return True @@ -112,6 +97,20 @@ class ArtifactDal(BaseDal): except Exception as e: print(f"根据ID获取构件任务失败: {e}") return False + + def get_artifactid(self, module_code: str) -> Optional[str]: + """根据模具编号查找ID,根据ID确认是否扫码""" + try: + sql = "SELECT ArtifactID FROM ArtifactTask WHERE MouldCode = ? and OptTime>?" + _target_time = datetime.now() - timedelta(hours=4) + results = self.db_dao.fetch_one(sql, (module_code,_target_time)) + if results is None: + return None + else: + return results['ArtifactID'] + except Exception as e: + print(f"根据ID获取构件任务失败: {e}") + return None def get_by_id(self, artifact_id: int) -> Optional[ArtifactInfoModel]: """根据构件ID获取构件任务""" @@ -129,27 +128,43 @@ class ArtifactDal(BaseDal): return None - def insert_artifact(self, artifact_data: dict) -> Optional[int]: + def insert_artifact(self, model: ArtifactInfoModel) -> int: """插入一条构件任务记录""" try: # 使用insert方法插入数据 - row_id = self.db_dao.insert("ArtifactTask", artifact_data) + model.OptTime=datetime.now() + model.__dict__.pop("ProductionProcessCode", None) + model.__dict__.pop("ID", None) + row_id = self.db_dao.insert("ArtifactTask", model.__dict__) return row_id except Exception as e: print(f"插入构件任务失败: {e}") - return None + return 0 - def update_artifact(self, artifact_id: int, update_data: dict) -> bool: + def update_artifact(self, artifact_id: int, update_data: dict) -> int: """更新构件任务记录""" try: # 构建WHERE条件 where_condition = f"ArtifactID='{artifact_id}'" # 使用update方法更新数据 affected_rows = self.db_dao.update("ArtifactTask", update_data, where_condition) - return affected_rows > 0 + return affected_rows except Exception as e: print(f"更新构件任务失败: {e}") - return False + return 0 + + def update_by_modulecode(self, module_code: str, update_data: dict) -> int: + """更新构件任务记录""" + try: + # 构建WHERE条件 + _target_time = datetime.now() - timedelta(hours=4) + where_condition = f"MouldCode='{module_code}' and OptTime>='{_target_time}'" + # 使用update方法更新数据 + affected_rows = self.db_dao.update("ArtifactTask", update_data, where_condition) + return affected_rows + except Exception as e: + print(f"更新构件任务失败: {e}") + return 0 def validate_artifact(self, artifact_info: ArtifactInfoModel) -> bool: """验证构件信息是否符合业务规则""" @@ -186,11 +201,11 @@ class PDRecordDal(BaseDal): # pdrecord = PDRecordModel() pdrecord.ID=row["ID"] - pdrecord.PDCode=row["PDCode"] + pdrecord.ArtifactID=row["ArtifactID"] pdrecord.TaskID=row["TaskID"] pdrecord.ProjectName=row["ProjectName"] pdrecord.ProduceMixID=row["ProduceMixID"] - pdrecord.VinNo=row["VinNo"] + pdrecord.FBetonVolume=row["FBetonVolume"] pdrecord.BetonVolume=row["BetonVolume"] pdrecord.MouldCode=row["MouldCode"] pdrecord.SkeletonID=row["SkeletonID"] @@ -210,14 +225,115 @@ class PDRecordDal(BaseDal): print(f"获取top PD官片任务失败: {e}") return [] - - + def get_last_pds(self, top: int,mould_code:str) -> List[PDRecordModel]: + """获取派单的记录进行派单""" + try: + # 确保top为正整数 + if not isinstance(top, int) or top <= 0: + raise ValueError("top参数必须是正整数") + _target_time = datetime.now() - timedelta(hours=4) + # 查询指定数量的记录,按ID降序排列 + sql = f"""SELECT * FROM PDRecord WHERE CreateTime>? and ID>( +select ID FROM PDRecord WHERE MouldCode=? and CreateTime>? +) +order by ID asc LIMIT ?""" + results = self.db_dao.execute_read(sql, (_target_time,mould_code,_target_time,top)) + + pdrecords = [] + for row in results: + pdrecord = PDRecordModel(**row) + pdrecords.append(pdrecord) + return pdrecords + except Exception as e: + print(f"获取top PD官片任务失败: {e}") + return [] + def get_artifactid(self, module_code: str) -> Optional[str]: + """根据模具编号查找ID,根据ID确认是否扫码""" + try: + sql = "SELECT ArtifactID FROM PDRecord WHERE MouldCode = ? and CreateTime>?" + _target_time = datetime.now() - timedelta(hours=4) + results = self.db_dao.fetch_one(sql, (module_code,_target_time)) + if results is None: + return None + else: + return results['ArtifactID'] + except Exception as e: + print(f"根据ID获取构件任务失败: {e}") + return None + + def exists_by_module_code(self, module_code: str) -> bool: + """根据模具编号获取PD官片任务""" + try: + sql = "SELECT count(1) FROM PDRecord WHERE MouldCode = ? and OptTime>?" + results = self.db_dao.execute_read(sql, (module_code,datetime.now().replace(hour=0, minute=0, second=0, microsecond=0))) + + rows = list(results) + if rows[0][0] == 1: + return True + + return False + except Exception as e: + print(f"PDRecord(exists_by_module_code)失败: {e}") + return False + + def exists_artifactid(self, artifact_action_id: str) -> bool: + """根据构件动作编号获取PD官片任务""" + try: + sql = "SELECT count(1) FROM PDRecord WHERE ArtifactActionID = ?" + results = self.db_dao.execute_read(sql, (artifact_action_id,)) + + rows = list(results) + if rows[0][0] == 1: + return True + + return False + except Exception as e: + print(f"PDRecord(exists_by_module_code)失败: {e}") + return False + + def insert_PD_record(self, model: PDRecordModel) -> int: + """插入一条PD官片任务记录""" + try: + # 使用insert方法插入数据 + model.CreateTime=datetime.now() + model.__dict__.pop("ID", None) + row_id = self.db_dao.insert("PDRecord", model.__dict__) + return row_id + except Exception as e: + print(f"插入PD官片任务失败: {e}") + return 0 + + def update_pd(self, artifact_action_id: int, update_data: dict) -> bool: + """更新PD官片任务记录""" + try: + # 构建WHERE条件 + where_condition = f"ArtifactActionID='{artifact_action_id}'" + # 使用update方法更新数据 + affected_rows = self.db_dao.update("PDRecord", update_data, where_condition) + return affected_rows > 0 + except Exception as e: + print(f"更新PD管片任务失败: {e}") + return False + + def update_by_modulecode(self, module_code: str, update_data: dict) -> int: + """更新构件派单记录""" + try: + # 构建WHERE条件 + _target_time = datetime.now() - timedelta(hours=4) + where_condition = f"MouldCode='{module_code}' and CreateTime>='{_target_time}'" + # 使用update方法更新数据 + affected_rows = self.db_dao.update("PDRecord", update_data, where_condition) + return affected_rows + except Exception as e: + print(f"更新PD官片任务失败: {e}") + return 0 + if __name__ == "__main__": artifact_dal = ArtifactDal() - artifacts = artifact_dal.get_artifact_task() + artifacts = artifact_dal.get_top_artifact(5, "ID desc") # 显示获取到的数据 print(f"获取到 {len(artifacts)} 条构件任务数据:") diff --git a/busisness/models.py b/busisness/models.py index b4289a2..20edaf7 100644 --- a/busisness/models.py +++ b/busisness/models.py @@ -41,90 +41,62 @@ class LoginResponse: @dataclass class ArtifactInfo: - """管片信息模型""" + """管片信息基础模型""" #管片编号 - ArtifactID: str + ArtifactID: str = "" #管片ID - ArtifactActionID: int + ArtifactActionID: int = 0 #管片副标识1 - ArtifactIDVice1: str + ArtifactIDVice1: str = "" #产品环号 - ProduceRingNumber: int + ProduceRingNumber: int = 0 #模具编号 - MouldCode: str + MouldCode: str = "" #骨架ID - SkeletonID: str + SkeletonID: str = "" #环类型编码 - RingTypeCode: str + RingTypeCode: str = "" #尺寸规格 - SizeSpecification: str + SizeSpecification: str = "" #埋深 - BuriedDepth: str + BuriedDepth: str = "" #块号 - BlockNumber: str + BlockNumber: str = "" #环号标记 - HoleRingMarking: str + HoleRingMarking: str = "" #出管标记 - GroutingPipeMarking: str + GroutingPipeMarking: str = "" #聚丙烯纤维标记 - PolypropyleneFiberMarking: str + PolypropyleneFiberMarking: str = "" # 浇筑方量 - BetonVolume: float + BetonVolume: float = 0.0 # 任务单号(混凝土) - BetonTaskID: str + BetonTaskID: str = "" + #接口中的字段 + ProductionProcessCode: int = 0 + @dataclass -class ArtifactInfoModel: - def __init__(self): - pass +class ArtifactInfoModel(ArtifactInfo): """管片表模型""" - ID: int - #管片编号 - ArtifactID: str - #管片ID - ArtifactActionID: int - #管片副标识1 - ArtifactIDVice1: str - #产品环号 - ProduceRingNumber: int - #模具编号 - MouldCode: str - #骨架ID - SkeletonID: str - #环类型编码 - RingTypeCode: str - #尺寸规格 - SizeSpecification: str - #埋深 - BuriedDepth: str - #块号 - BlockNumber: str - #环号标记 - HoleRingMarking: str - #出管标记 - GroutingPipeMarking: str - #聚丙烯纤维标记 - PolypropyleneFiberMarking: str - # 浇筑方量 - BetonVolume: float - # 任务单号 - BetonTaskID: str + # 继承基础模型的所有字段 + ID: int = 0 #FK_PDID - FK_PDID: int=0 + FK_PDID: int = 0 #状态 - Status: int=1 + Status: int = 1 #开始时间 - BeginTime: str="" + BeginTime: str = "" #结束时间 - EndTime: str="" - #PStatus - PStatus: int=0 - #Source - Source: int=1 + EndTime: str = "" + #生产中状态(1正常、2警告、3异常) + PStatus: int = 1 + #数据来源(1 api 2离线RFID) + Source: int = 1 #FBetonVolume - FBetonVolume: float=0.0 + FBetonVolume: float = 0.0 #OptTime - OptTime: str="" + OptTime: str = "" @dataclass @@ -161,49 +133,49 @@ class ArtifactResponse: class TaskInfo: """任务单信息模型""" #任务单ID - TaskID: str + TaskID: str="" #任务单状态 - TaskStatus: int + TaskStatus: int=0 #任务单状态文本 - TaskStatusText: str - #计划生产数量 - TaskCount: int - #已生产数量 - AlreadyProduceCount: int + TaskStatusText: str="" + # #计划生产数量 + # TaskCount: int=0 + # #已生产数量 + # AlreadyProduceCount: int=0 #生产进度 - Progress: str + # Progress: str="" #工程名称 - ProjectName: str + ProjectName: str="" #计划生产日期 - TaskPlanDateText: str + # TaskPlanDateText: str="" #强度等级 - BetonGrade: str + BetonGrade: str="" #设计配合比编号 - MixID: str + MixID: str="" #生产配合比编号 - ProduceMixID: str - #计划方量 - PlannedVolume: float - #已供方量 - ProducedVolume: float + ProduceMixID: str="" + # #计划方量 + PlannedVolume: float=0.0 + # #已供方量 + # ProducedVolume: float=0.0 #出洞环标记 - HoleRingMarking: str - #注浆管标记 - GroutingPipeMarking: str - #聚丙烯纤维标记 - PolypropyleneFiberMarking: str + # HoleRingMarking: str="" + # #注浆管标记 + # GroutingPipeMarking: str="" + # #聚丙烯纤维标记 + # PolypropyleneFiberMarking: str="" #生产日期 - TaskDateText: str + # TaskDateText: str="" #盘数 - PlateCount: int - #任务单下发状态 - SendStatus: int - #任务单下发状态 - SendStatusText: str - #配合比下发状态 - MixSendStatus: int - #配合比下发状态 - MixSendStatusText: str + # PlateCount: int=0 + # #任务单下发状态 + # SendStatus: int=0 + # #任务单下发状态 + # SendStatusText: str="" + # #配合比下发状态 + # MixSendStatus: int=0 + # #配合比下发状态 + # MixSendStatusText: str="" @dataclass @@ -268,34 +240,40 @@ class NotPourArtifactResponse: @dataclass class PDRecordModel: - def __init__(self): - pass + # def __init__(self): + # pass """管片表模型""" - ID: int + ID: int=0 #派单编号 - PDCode: str + ArtifactID: str="" + + ArtifactActionID: int=0 #任务单号 - TaskID: int + TaskID: str="" #工程名称 - ProjectName: str + ProjectName: str="" #生产配合比编号 - ProduceMixID: str - #车架号 - VinNo: str + ProduceMixID: str="" #派单方量 - BetonVolume: float + BetonVolume: float=0.0 + #实际派单方量 + FBetonVolume: float=0.0 + #实际派单方量 + PlannedVolume: float=0.0 + #强度等级 + BetonGrade: str="" #模具编号 - MouldCode: str + MouldCode: str="" #骨架编号 - SkeletonID: str + SkeletonID: str="" #环类型编码 - RingTypeCode: str + RingTypeCode: str="" #尺寸规格 - SizeSpecification: str + SizeSpecification: str="" #埋深 - BuriedDepth: str + BuriedDepth: str="" #块号 - BlockNumber: str + BlockNumber: str="" # 派单模式(1自动派单 2手动派单0未知 ) Mode: int=0 # 派单状态(1计划中2已下发0未知),默认1 @@ -306,6 +284,8 @@ class PDRecordModel: Source: int=1 #创建时间 CreateTime: str="" + #创建时间 + EndTime: str="" #派单时间(下发) OptTime: str="" diff --git a/config/ini_manager.py b/config/ini_manager.py index 4f62c01..2eba754 100644 --- a/config/ini_manager.py +++ b/config/ini_manager.py @@ -166,6 +166,27 @@ class IniManager: def lower_transmitter_port(self): """获取下料斗变送器端口""" return self._read_config_value('app', 'LOWER_TRANSMITTER_PORT', 8234, int) + + @property + def relay_host(self): + """获取网络继电器IP""" + return self._read_config_value('hardware', 'relay_host', '192.168.250.62', str) + @property + def relay_port(self): + """获取网络继电器端口""" + return self._read_config_value('hardware', 'relay_port', 50000, int) + + @property + def upper_plc_ip(self): + """获取上料斗PLC IP""" + return self._read_config_value('hardware', 'upper_plc_ip', '192.168.250.233', str) + @property + def upper_plc_port(self): + """获取上料斗PLC端口""" + return self._read_config_value('hardware', 'upper_plc_port', 9600, int) + + + ini_manager = IniManager() diff --git a/config/opc_config.ini b/config/opc_config.ini index 50613ec..78af530 100644 --- a/config/opc_config.ini +++ b/config/opc_config.ini @@ -14,6 +14,7 @@ sub_interval = 500 [OPC_NODE_LIST] upper_weight = 2:upper,2:upper_weight lower_weight = 2:lower,2:lower_weight +pd_data = 2:pd,2:pd_data upper_hopper_position = 2:upper,2:upper_hopper_position upper_clamp_status = 2:upper,2:upper_clamp_status vibration_frequency=2:vibration_frequency diff --git a/core/pd_system.py b/core/pd_system.py new file mode 100644 index 0000000..0c5d53d --- /dev/null +++ b/core/pd_system.py @@ -0,0 +1,97 @@ +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import threading +import time +import queue +from core.system_state import SystemState,FeedStatus +from hardware.relay import RelayController +from hardware.inverter import InverterController +from hardware.transmitter import TransmitterController +from config.ini_manager import ini_manager +from hardware.upper_plc import OmronFinsPollingService +from vision.visual_callback_dq import VisualCallback +from opc.opcua_client_feed import OpcuaClientFeed +from busisness.blls import ArtifactBll,PDRecordBll +from busisness.models import ArtifactInfoModel,PDRecordModel + + +class FeedingControlSystem: + def __init__(self): + print('FeedingControlSystem初始化') + self.pd_record_bll=PDRecordBll() + + + def send_pd_data(self): + """ + 发送PD数据到OPC队列 + """ + # 构建PD数据 + _cur_mould='SHR2B1-13' + 'F块L1块需要设置重量' + _weight=0 + _pdrecords = self.pd_record_bll.get_last_pds(_cur_mould) + if _pdrecords: + _pdrecord=_pdrecords[0] + if _pdrecord.TaskID: + if _pdrecord.BlockNumber=='F': + print(f'{_pdrecord.MouldCode} F块,不发送派单数据') + print(f'{_pdrecord.MouldCode} F块,不发送派单数据') + print(f'{_pdrecord.MouldCode} F块,不发送派单数据') + return True + _fact_volumn=self.get_fact_volumn(_pdrecord.MouldCode,_pdrecord.BlockNumber,_weight) + if _fact_volumn>0: + _pdrecord.FBetonVolume=_fact_volumn + print(f'{_pdrecord.MouldCode}-{_pdrecord.BlockNumber} 实际派单方量:{_fact_volumn},{_fact_volumn},{_fact_volumn}') + print(f'{_pdrecord.MouldCode}-{_pdrecord.BlockNumber} 实际派单方量:{_fact_volumn},{_fact_volumn},{_fact_volumn}') + print(f'{_pdrecord.MouldCode}-{_pdrecord.BlockNumber} 实际派单方量:{_fact_volumn},{_fact_volumn},{_fact_volumn}') + # self.state._pd_data=_pdrecord + return True + else: + return False + else: + print(f'{_pdrecord.MouldCode} 未获取到数据-(等待扫码)') + return False + else: + print(f'接口数据异常') + return False + + def get_fact_volumn(self,mould_code:str,block_number:str='',_weight:float=0) -> float: + """获取实际派单发量""" + _now_volumn=0 + _pd_volumn=0 + + print(f'get_fact_volumn当前重量:{_weight}') + _now_volumn=_weight/2500 + if not block_number and '-' in mould_code: + block_number = mould_code.split('-')[0][-2:] + if block_number=='B1': + _pd_volumn=1.9 + if _weight>750: + #留0.3 + _pd_volumn=_pd_volumn-_now_volumn+0.3 + if _pd_volumn<1: + _pd_volumn=1 + + if block_number in ['B2','B3']: + _pd_volumn=1.9 + elif block_number=='L1': + _pd_volumn=2.0 + elif block_number=='L2': + #多F块后面剩下的,大约500,那下完F后多的就可以减去 + _pd_volumn=2 + # if _weight>1300: + _pd_volumn=_pd_volumn-_now_volumn+0.5 + if _pd_volumn>2.1: + _pd_volumn=2.1 + elif _pd_volumn<0.8: + _pd_volumn=0.8 + + return round(_pd_volumn,1) + + + +if __name__ == "__main__": + system = FeedingControlSystem() + + system.send_pd_data() diff --git a/core/state.py b/core/state.py index 5966d86..d520eba 100644 --- a/core/state.py +++ b/core/state.py @@ -3,7 +3,7 @@ import threading from enum import IntEnum class SystemState(QObject): - """状态中以_开头的属性会发送信号通知,不需要的不要加_开头""" + """状态中以_开头的属性会发送到OPC通知,不需要的不要加_开头""" state_updated=Signal(str,object) def __init__(self): super().__init__() diff --git a/core/system copy.py b/core/system copy.py new file mode 100644 index 0000000..f28e608 --- /dev/null +++ b/core/system copy.py @@ -0,0 +1,288 @@ +# core/system.py +import threading +import time +import cv2 +from core.state import SystemState +from hardware.relay import RelayController +from hardware.inverter import InverterController +from hardware.transmitter import TransmitterController +from hardware.RFID.rfid_service import rfid_service +from vision.camera import DualCameraController +from vision.detector import VisionDetector +from feeding.controller import FeedingController +from service.mould_service import app_web_service +from config.settings import app_set_config + + +class FeedingControlSystem: + def __init__(self): + self.state = SystemState() + + # 初始化硬件控制器 + self.relay_controller = RelayController( + host=app_set_config.relay_host, + port=app_set_config.relay_port + ) + + self.inverter_controller = InverterController(self.relay_controller) + self.transmitter_controller = TransmitterController(self.relay_controller) + + # 初始化视觉系统 + self.camera_controller = DualCameraController(app_set_config.camera_configs) + + self.vision_detector = VisionDetector() + + # 初始化RFID控制器 + self.rfid_controller = rfid_service( + host=app_set_config.rfid_host, + port=app_set_config.rfid_port + ) + # 初始化下料控制器 + self.feeding_controller = FeedingController( + self.relay_controller, + self.inverter_controller, + self.transmitter_controller, + self.vision_detector, + self.camera_controller, + self.rfid_controller, + self.state + ) + + # 线程管理 + self.monitor_thread = None + self.visual_control_thread = None + self.alignment_check_thread = None + self.lower_feeding_thread = None + self.led_thread = None + + def initialize(self): + """初始化系统""" + print("初始化控制系统...") + + # self.check_device_connectivity() + + # self.camera_controller.start_cameras() + # if not app_set_config.debug_feeding: + # 启动系统监控(要料,破拱)线程 + self.start_monitoring() + + # 启动视觉控制(角度、溢出)线程 + # self.start_visual_control() + + # 启动对齐检查线程 + self.start_alignment_check() + + # 启动下料线程 + self.start_lower_feeding() + #LED屏 + # self.start_led() + + print("控制系统初始化完成") + + def start_monitoring(self): + """启动系统监控""" + print('振动和要料监控线程启动') + self.monitor_thread = threading.Thread( + target=self._monitor_loop, + daemon=True, + name='monitor' + ) + self.monitor_thread.start() + + def _monitor_loop(self): + """监控循环""" + while self.state.running: + try: + # self.feeding_controller.check_upper_material_request() + self.feeding_controller.check_arch_blocking() + time.sleep(1) + except Exception as e: + print(f"监控线程错误: {e}") + + def start_visual_control(self): + """启动视觉控制""" + print('视觉控制线程启动') + self.visual_control_thread = threading.Thread( + target=self._visual_control_loop, + daemon=True, + name='visual_control' + ) + self.visual_control_thread.start() + + def _visual_control_loop(self): + """视觉控制循环""" + while self.state.running: + try: + # print('visual_control') + current_frame = self.camera_controller.get_single_latest_frame() + if current_frame is not None: + # 执行视觉控制逻辑 + self.feeding_controller.visual_control(current_frame) + time.sleep(app_set_config.visual_check_interval) + except Exception as e: + print(f"视觉控制循环错误: {e}") + time.sleep(app_set_config.visual_check_interval) + + def start_alignment_check(self): + """启动对齐检查""" + print('对齐检查线程启动') + self.alignment_check_thread = threading.Thread( + target=self._alignment_check_loop, + daemon=True, + name='align_check' + ) + self.alignment_check_thread.start() + + def _alignment_check_loop(self): + """对齐检查循环""" + loc_align_status=False + loc_before_status=None + while self.state.running: + try: + if self.state.lower_feeding_stage == 4: # 等待对齐阶段 + current_frame = self.camera_controller.get_single_latest_frame() + if current_frame is not None: + self.state.vehicle_aligned = self.alignment_check_status() + if self.state.vehicle_aligned: + # loc_count+=1 + print("检测到模具车对齐") + else: + print("模具车未对齐") + # time.sleep(app_set_config.alignment_check_interval) + # loc_align_status=self.alignment_check_status() + # if loc_align_status and not loc_before_status: + # print("模具车由未对齐到对齐") + # self.state.vehicle_aligned=True + # elif not loc_align_status and loc_before_status: + # print("模具车由对齐到未对齐") + # self.state.vehicle_aligned=False + + # if loc_before_status!=loc_align_status: + # loc_before_status=loc_align_status + + except Exception as e: + print(f"对齐检查循环错误: {e}") + finally: + time.sleep(app_set_config.alignment_check_interval) + + + def alignment_check_status(self)->bool: + """对齐检查循环""" + loc_aligned=False + loc_count=0 + for i in range(4): + try: + current_frame = self.camera_controller.get_single_latest_frame() + if current_frame is not None: + loc_aligned = self.vision_detector.detect_vehicle_alignment(current_frame) + if loc_aligned: + loc_count+=1 + print("检测到模具车对齐") + else: + loc_count=0 + print("模具车未对齐") + time.sleep(app_set_config.alignment_check_interval) + except Exception as e: + print(f"对齐检查循环错误: {e}") + time.sleep(app_set_config.alignment_check_interval) + + if loc_count>=3: + loc_aligned=True + else: + loc_aligned=False + return loc_aligned + + def start_lower_feeding(self): + """启动下料流程""" + self.lower_feeding_thread = threading.Thread( + target=self._start_lower_feeding, + name="Feeding", + daemon=True + ) + self.lower_feeding_thread.start() + + def _start_lower_feeding(self): + """启动下料流程""" + while self.state.running: + self.feeding_controller.start_feeding() + time.sleep(app_set_config.lower_feeding_interval) + + def start_led(self): + """启动LED流程""" + self.led_thread = threading.Thread( + target=self._start_led, + name="LED", + daemon=True + ) + self.led_thread.start() + + def _start_led(self): + """启动LED流程""" + while self.state.running: + led_info = app_web_service.get_pouring_led() + if led_info: + if self.state.current_artifact.MouldCode==led_info.MouldCode: + led_info.RingTypeCode=self.state.current_artifact.RingTypeCode + led_info.UpperWeight=self.state._upper_weight + led_info.LowerWeight=self.state._lower_weight + led_info.VibrationFrequency=self.state._mould_frequency + + #发送到LED屏 + + time.sleep(app_set_config.led_interval) + + def check_device_connectivity(self) -> bool: + """检查关键设备连接状态""" + try: + # 检查网络继电器连接 + test_response = self.relay_controller.send_command(self.relay_controller.read_status_command) + if not test_response: + print("网络继电器连接失败") + return False + + # 检查变频器连接 + if not self.relay_controller.modbus_client.connect(): + print("无法连接到网络继电器Modbus服务") + return False + + # 尝试读取变频器一个寄存器(测试连接) + # test_result = self.relay_controller.modbus_client.read_holding_registers( + # address=0x00, + # count=1, + # slave=self.inverter_controller.config['slave_id'] + # ) + + # if isinstance(test_result, Exception): + # print("变频器连接测试失败") + # return False + + # 检查下料斗变送器连接 + test_weight = self.transmitter_controller.read_data(2) + if test_weight is None: + print("下料斗变送器连接失败") + return False + + self.relay_controller.modbus_client.close() + return True + except Exception as e: + print(f"设备连接检查失败: {e}") + return False + + def stop(self): + """停止系统""" + print("停止控制系统...") + self.state.running = False + + # 等待线程结束 + if self.monitor_thread: + self.monitor_thread.join() + if self.visual_control_thread: + self.visual_control_thread.join() + if self.alignment_check_thread: + self.alignment_check_thread.join() + if self.lower_feeding_thread: + self.lower_feeding_thread.join() + + # 释放摄像头资源 + self.camera_controller.release() + print("控制系统已停止") diff --git a/core/system.py b/core/system.py index e94e9f3..0e70505 100644 --- a/core/system.py +++ b/core/system.py @@ -1,54 +1,53 @@ -# core/system.py +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import threading import time -import cv2 -from core.state import SystemState +import queue +from core.system_state import SystemState,FeedStatus from hardware.relay import RelayController from hardware.inverter import InverterController from hardware.transmitter import TransmitterController -from hardware.RFID.rfid_service import rfid_service -from vision.camera import DualCameraController -from vision.detector import VisionDetector -from feeding.controller import FeedingController -from service.mould_service import app_web_service -from config.settings import app_set_config +from config.ini_manager import ini_manager +from hardware.upper_plc import OmronFinsPollingService +from vision.visual_callback_dq import VisualCallback +from opc.opcua_client_feed import OpcuaClientFeed +from busisness.blls import ArtifactBll,PDRecordBll +from busisness.models import ArtifactInfoModel,PDRecordModel class FeedingControlSystem: def __init__(self): + print('FeedingControlSystem初始化') self.state = SystemState() # 初始化硬件控制器 self.relay_controller = RelayController( - host=app_set_config.relay_host, - port=app_set_config.relay_port + host=ini_manager.relay_host, + port=ini_manager.relay_port ) - self.inverter_controller = InverterController(self.relay_controller) + + self.inverter_controller = InverterController() self.transmitter_controller = TransmitterController(self.relay_controller) + #小屏修改过屏幕 + self.vf_auto_mode=True + # 初始化 OPC UA 客户端 + self.opcua_client_feed = OpcuaClientFeed() + + # 初始化 RFID 控制器 + # self.rfid_controller = rfid_service( + # host=app_set_config.rfid_host, + # port=app_set_config.rfid_port + # ) - # 初始化视觉系统 - self.camera_controller = DualCameraController(app_set_config.camera_configs) - - self.vision_detector = VisionDetector() - - # 初始化RFID控制器 - self.rfid_controller = rfid_service( - host=app_set_config.rfid_host, - port=app_set_config.rfid_port - ) + # self.plc_service = OmronFinsPollingService(ini_manager.upper_plc_ip, ini_manager.upper_plc_port) + # 初始化下料控制器 - self.feeding_controller = FeedingController( - self.relay_controller, - self.inverter_controller, - self.transmitter_controller, - self.vision_detector, - self.camera_controller, - self.rfid_controller, - self.state - ) - + self.feeding_controller = VisualCallback(self.state) + # 初始化 OPC 队列监听线程 + self.opc_queue_thread = None # 线程管理 self.monitor_thread = None @@ -60,26 +59,243 @@ class FeedingControlSystem: def initialize(self): """初始化系统""" print("初始化控制系统...") - + # 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_monitoring() # 启动下料线程 - self.start_lower_feeding() - #LED屏 - # self.start_led() + # self.start_lower_feeding() + # 启动OPC队列处理线程 + # self.opcua_client_feed.start() + # self.start_opc_queue_thread() + + #启用API线程 + self.start_api_thread() + + self.start_vf_thread() + # self.feeding_controller.get_current_mould() + # self.feeding_controller._cur_mould_model.MouldCode='SHR2L1-5' + # self.feeding_controller.send_pd_data() + #启用派单线程 + # self.start_pd_thread() print("控制系统初始化完成") + + def start_opc_queue_thread(self): + """启动OPC队列处理线程""" + print('启动OPC队列处理线程') + self.opc_queue_thread = threading.Thread( + target=self._process_opc_queue, + daemon=True, + name='opc_queue_processor' + ) + self.opc_queue_thread.start() + + def start_api_thread(self): + """启动PD线程""" + # print('启动API处理线程,从API获取未浇筑数据') + self.api_thread = threading.Thread( + target=self._process_api_db, + daemon=True, + name='api_thread' + ) + self.api_thread.start() + + def start_vf_thread(self): + """启动变频器控制线程""" + # print('启动API处理线程,从API获取未浇筑数据') + self.vf_thread = threading.Thread( + target=self._process_vf, + daemon=True, + name='vf_thread' + ) + self.vf_thread.start() + + def _process_vf(self): + _begin_time=None + _wait_times=300 + _start_wait_seconds=None + while self.state.running: + try: + # if self.feeding_controller._is_finish_ratio>=0.6: + # self.inverter_controller.set_frequency(230) + # else: + # self.inverter_controller.set_frequency(220) + + if self.state.vf_status in [1,2]: + if _begin_time is None : + print("----浇筑即将启动-----") + if _start_wait_seconds is None: + #记录盖板对齐时间 + _start_wait_seconds=time.time() + if self.feeding_controller._is_finish_ratio>=0.02: + _elasped_time=time.time()-_start_wait_seconds + if _elasped_time<10: + time.sleep(10-_elasped_time) + self.inverter_controller.control('start',230) + print("----振捣已经启动-----") + _begin_time=time.time() + self.state._mould_frequency=230 + self.state._mould_vibrate_status=True + if self.state.vf_status==2: + print("----振捣270s-----") + _wait_time=270 + else: + print("----振捣300秒-----") + _wait_time=300 + else: + print("----下料重量小于46KG,暂时不振捣-----") + # else: + elif self.state.vf_status==3 and _begin_time is not None: + if time.time()-_begin_time>=_wait_time: + if self.vf_auto_mode: + self.inverter_controller.control('stop') + self.state._mould_vibrate_status=False + _begin_time=None + _start_wait_seconds=None + except Exception as e: + print(f"处理变频器数据时发生错误: {e}") + time.sleep(2) + + + def _process_api_db(self): + from service.mould_service import app_web_service + """处理API队列中的数据""" + # 初始化三个列表用于跟踪ArtifactActionID + processed_artifact_actions = [] # 已处理的ArtifactActionID列表 + processed_artifact_ids = [] # 已处理的ArtifactActionID列表 + processed_pd_records = [] # 已插入PDRecord表的ArtifactActionID列表 + processed_pd_ids=[] + _model_task=None + artifact_bll=ArtifactBll() + pdrecord_bll=PDRecordBll() + print('启动API处理线程,从API获取未浇筑数据') + while self.state.running: + try: + not_poured = app_web_service.get_not_pour_artifacts() + if not_poured: + for item in reversed(not_poured): + + if item.MouldCode is None or item.MouldCode == '': + continue + + _is_artifactid=True + # 检查MouldCode是否已处理 + if item.MouldCode in processed_artifact_actions: + #print(f"待浇筑:MouldCode {item.MouldCode} 已处理,跳过") + #处理过了。判断是否更新 + if item.ArtifactID is None or item.ArtifactID == '': + _is_artifactid=False + if item.ArtifactID in processed_artifact_ids: + # print(f"待浇筑:ArtifactID {item.ArtifactID} 已处理,跳过") + _is_artifactid=False + if _is_artifactid: + _model_data = ArtifactInfoModel(**item.__dict__) + _ret=artifact_bll.save_artifact_task(_model_data) + if _ret > 0: + # 标记为已处理 + processed_artifact_actions.append(item.MouldCode) + if len(processed_artifact_actions) > 4: + processed_artifact_actions.pop(0) + if item.ArtifactID: + processed_artifact_ids.append(item.ArtifactID) + if len(processed_artifact_ids) > 4: + processed_artifact_ids.pop(0) + # 限制最多保存3条记录,删除最旧的 + + #print(f"待浇筑:已处理MouldCode {item.MouldCode} ArtifactID {item.ArtifactID}") + + if item.MouldCode in processed_pd_records: + #print(f"派单:MouldCode {item.MouldCode} 已处理,跳过") + if item.ArtifactID is None or item.ArtifactID == '': + continue + if item.ArtifactID in processed_pd_ids: + #print(f"待浇筑:ArtifactID {item.ArtifactID} 已处理,跳过") + continue + + _pd_record_data=None + if item.ArtifactID: + if item.BetonTaskID is not None and item.BetonTaskID != '': + #获取taskid + if _model_task is None or item.BetonTaskID != _model_task.TaskID: + _model_task = app_web_service.get_task_info(item.BetonTaskID) + if _model_task is None: + print(f"异常:BetonTaskID {item.BetonTaskID} 不存在,跳过") + continue + + _pd_record_data = PDRecordModel( + ArtifactID=item.ArtifactID, + ArtifactActionID=item.ArtifactActionID, + TaskID=_model_task.TaskID, + ProjectName=_model_task.ProjectName, + ProduceMixID=_model_task.ProduceMixID, + BetonGrade=_model_task.BetonGrade, + BetonVolume=item.BetonVolume, + MouldCode=item.MouldCode, + SkeletonID=item.SkeletonID, + RingTypeCode=item.RingTypeCode, + SizeSpecification=item.SizeSpecification, + BuriedDepth=item.BuriedDepth, + BlockNumber=item.BlockNumber, + PlannedVolume=_model_task.PlannedVolume + ) + else: + _pd_record_data = PDRecordModel( + MouldCode=item.MouldCode + ) + + if _pd_record_data is None: + continue + _ret=pdrecord_bll.save_PD_record(_pd_record_data) + if _ret > 0: + # 标记为已处理 + processed_pd_records.append(item.MouldCode) + # 限制最多保存3条记录,删除最旧的 + if len(processed_pd_records) > 4: + processed_pd_records.pop(0) + if item.ArtifactID: + processed_pd_ids.append(item.ArtifactID) + if len(processed_pd_ids) > 4: + processed_pd_ids.pop(0) + + #print(f"派单:已处理MouldCode {item.MouldCode} ArtifactID {item.ArtifactID}") + except Exception as e: + print(f"处理MouldCode {item.MouldCode} 时发生错误: {e}") + + time.sleep(5) + + def _process_opc_queue(self): + """处理OPC队列中的数据""" + while self.state.running: + try: + # 从队列中获取数据,设置超时以允许线程退出 + item = self.state.opc_queue.get(timeout=1) + if item: + public_name, value = item + # 这里可以添加实际的OPC处理逻辑 + print(f"Processing OPC update: {public_name} = {value}") + self.opcua_client_feed.write_value_by_name(public_name, value) + # 标记任务完成 + self.state.opc_queue.task_done() + except queue.Empty: + # 队列为空,继续循环 + continue + except Exception as e: + print(f"OPC队列处理错误: {e}") + + def angle_visual_callback(self, current_angle, overflow_detected, mould_aligned): + """角度视觉回调""" + self.feeding_controller.angle_visual_callback(current_angle, overflow_detected, mould_aligned) + + def diff_visual_callback(self, current_diff,current_area): + """差异视觉回调""" + self.feeding_controller.diff_visual_callback(current_diff,current_area) + + def shutdown(self): + """关闭系统""" + self.feeding_controller.shutdown() + self.stop() + def start_monitoring(self): """启动系统监控""" @@ -101,99 +317,6 @@ class FeedingControlSystem: except Exception as e: print(f"监控线程错误: {e}") - def start_visual_control(self): - """启动视觉控制""" - print('视觉控制线程启动') - self.visual_control_thread = threading.Thread( - target=self._visual_control_loop, - daemon=True, - name='visual_control' - ) - self.visual_control_thread.start() - - def _visual_control_loop(self): - """视觉控制循环""" - while self.state.running: - try: - # print('visual_control') - current_frame = self.camera_controller.get_single_latest_frame() - if current_frame is not None: - # 执行视觉控制逻辑 - self.feeding_controller.visual_control(current_frame) - time.sleep(app_set_config.visual_check_interval) - except Exception as e: - print(f"视觉控制循环错误: {e}") - time.sleep(app_set_config.visual_check_interval) - - def start_alignment_check(self): - """启动对齐检查""" - print('对齐检查线程启动') - self.alignment_check_thread = threading.Thread( - target=self._alignment_check_loop, - daemon=True, - name='align_check' - ) - self.alignment_check_thread.start() - - def _alignment_check_loop(self): - """对齐检查循环""" - loc_align_status=False - loc_before_status=None - while self.state.running: - try: - if self.state.lower_feeding_stage == 4: # 等待对齐阶段 - current_frame = self.camera_controller.get_single_latest_frame() - if current_frame is not None: - self.state.vehicle_aligned = self.alignment_check_status() - if self.state.vehicle_aligned: - # loc_count+=1 - print("检测到模具车对齐") - else: - print("模具车未对齐") - # time.sleep(app_set_config.alignment_check_interval) - # loc_align_status=self.alignment_check_status() - # if loc_align_status and not loc_before_status: - # print("模具车由未对齐到对齐") - # self.state.vehicle_aligned=True - # elif not loc_align_status and loc_before_status: - # print("模具车由对齐到未对齐") - # self.state.vehicle_aligned=False - - # if loc_before_status!=loc_align_status: - # loc_before_status=loc_align_status - - except Exception as e: - print(f"对齐检查循环错误: {e}") - finally: - time.sleep(app_set_config.alignment_check_interval) - - - def alignment_check_status(self)->bool: - """对齐检查循环""" - loc_aligned=False - loc_count=0 - for i in range(4): - try: - current_frame = self.camera_controller.get_single_latest_frame() - if current_frame is not None: - loc_aligned = self.vision_detector.detect_vehicle_alignment(current_frame) - if loc_aligned: - loc_count+=1 - print("检测到模具车对齐") - else: - loc_count=0 - print("模具车未对齐") - time.sleep(app_set_config.alignment_check_interval) - except Exception as e: - print(f"对齐检查循环错误: {e}") - time.sleep(app_set_config.alignment_check_interval) - - if loc_count>=3: - loc_aligned=True - else: - loc_aligned=False - return loc_aligned - def start_lower_feeding(self): """启动下料流程""" self.lower_feeding_thread = threading.Thread( @@ -209,32 +332,6 @@ class FeedingControlSystem: self.feeding_controller.start_feeding() time.sleep(app_set_config.lower_feeding_interval) - - - def start_led(self): - """启动LED流程""" - self.led_thread = threading.Thread( - target=self._start_led, - name="LED", - daemon=True - ) - self.led_thread.start() - - def _start_led(self): - """启动LED流程""" - while self.state.running: - led_info = app_web_service.get_pouring_led() - if led_info: - if self.state.current_artifact.MouldCode==led_info.MouldCode: - led_info.RingTypeCode=self.state.current_artifact.RingTypeCode - led_info.UpperWeight=self.state._upper_weight - led_info.LowerWeight=self.state._lower_weight - led_info.VibrationFrequency=self.state._mould_frequency - - #发送到LED屏 - - time.sleep(app_set_config.led_interval) - def check_device_connectivity(self) -> bool: """检查关键设备连接状态""" try: @@ -272,6 +369,59 @@ class FeedingControlSystem: print(f"设备连接检查失败: {e}") return False + + def start_pd_thread(self): + """启动PD线程""" + print('启动派单处理线程,从API获取未浇筑数据') + self.pd_jbl_thread = threading.Thread( + target=self._process_pd_jbl, + daemon=True, + name='pd_jbl_thread' + ) + self.pd_jbl_thread.start() + + def _process_pd_jbl(self): + # pass + #根据当前浇筑块进行最近一块的派单 + _isFinish=False + _start_time=None + while self.state.running: + #增加生产阶段检测, + if self.state._feed_status==FeedStatus.FCheckGB: + if not _isFinish: + if _start_time is None: + _start_time=time.time() + _isSuccess=self.feeding_controller.send_pd_data() + if _isSuccess: + _isFinish=True + if time.time()-_start_time>60: + print('派单超时,人工介入') + _isFinish=True + elif self.state._feed_status==FeedStatus.FFinished: + _start_time=None + _isFinish=False + time.sleep(5) + + @property + def _is_finish(self): + """检查系统是否运行""" + return self.feeding_controller._is_finish + + @property + def _is_finish_ratio(self): + """检查系统是否运行""" + return self.feeding_controller._is_finish_ratio + + @property + def vibrate_status(self): + """检查系统是否运行""" + return self.state._mould_vibrate_status + + def set_vf_mode(self,is_auto=False): + """设置变频器为自动模式""" + self.vf_auto_mode=is_auto + + def stop(self): """停止系统""" print("停止控制系统...") @@ -286,7 +436,22 @@ class FeedingControlSystem: self.alignment_check_thread.join() if self.lower_feeding_thread: self.lower_feeding_thread.join() - + if self.opc_queue_thread: + self.opc_queue_thread.join() + if self.vf_thread: + self.vf_thread.join() + if self.api_thread: + self.api_thread.join() # 释放摄像头资源 - self.camera_controller.release() + # self.camera_controller.release() print("控制系统已停止") + + +if __name__ == "__main__": + system = FeedingControlSystem() + system.initialize() + time.sleep(2) + system.state._upper_weight=1000 + + while True: + time.sleep(1) diff --git a/core/system_state.py b/core/system_state.py new file mode 100644 index 0000000..96a50aa --- /dev/null +++ b/core/system_state.py @@ -0,0 +1,178 @@ +import threading +from enum import IntEnum +import queue +import json +from dataclasses import asdict +from busisness.blls import ArtifactBll,PDRecordBll + +class SystemState: + """系统状态类,存在变化的参数及标志""" + """系统状态中以_开头的属性会发送到OPC通知,不需要的不要加_开头""" + # state_updated=Signal(str,object) + def __init__(self): + super().__init__() + # + self.watched_props = [] + self.lock = threading.RLock() + self.opc_queue = queue.Queue() + # 系统运行状态 + self.running = True + + # 上料斗控制相关 + self._upper_door_position = Upper_Door_Position.Default # default(在搅拌楼下接料), over_lower(在下料斗上方), returning(返回中) + # 是否破拱 + self._upper_is_arch_=False + self._upper_door_closed=True + self._upper_weight=0 + self._upper_volume=0.0 + #下料比例变频 + self.vf_frequencys=[{'radio':0,'fre':230},{'radio':0.3,'fre':230},{'radio':0.6,'fre':230}] + #使用 + self._mould_frequency=230 + self._mould_vibrate_status=False #True振动中False未振动 + #记录模具开始振动的时间 + self.mould_vibrate_time=0 + + #下料斗状态想着 + self.lower_feeding_stage = 0 # 0:未下料, 1:第一阶段, 2:第二阶段, 3:第三阶段, 4:等待模具车对齐 + self._lower_is_arch_=False + self._lower_weight=0 + self._lower_angle=0.0 + + #模具车状态 + self._mould_weight=0 + + + self.lower_feeding_cycle = 0 # 下料斗下料循环次数 + self.upper_feeding_count = 0 # 上料斗已下料次数 + self.upper_feeding_max = 2 #上料斗最大下料次数 + + # 重量相关 + self.last_upper_weight = 0 + self.last_lower_weight = 0 + self.last_weight_time = 0 + #需要下料的总重量 + self._mould_need_weight=0 + #完成下料的总重量 + self._mould_finish_weight=0 + + self.initial_upper_weight=0 + self.initial_lower_weight=0 + + + # 错误计数 + self.upper_weight_error_count = 0 + self.lower_weight_error_count = 0 + + # 视觉系统状态 + self.angle_control_mode = "normal" # 角度控制模式: normal, reducing, maintaining, recovery + self.overflow_detected = "0" # 堆料检测 + self.current_finish_status=False # 当前是否完成浇筑满 + self.door_opening_large = False # 夹角 + self.vehicle_aligned = False # 模具车是否对齐 + self.last_angle = None # 上次检测角度 + + #当前RFID的内容格式为 模块编号,分块号,尺寸规格,方量 + self.rfid_current=None + + #当前生产的管片 + self.current_artifact=None + #当前生产状态 + self._feed_status=FeedStatus.FNone + #每方重量 + self.density=2416.4 + self.bll_artifact=ArtifactBll() + self.bll_pdrecord=PDRecordBll() + #记录正在生产code模具编号,status:2正生产3完成生成,weight:完成重量 + self._db_status={'code':'','status':1,'weight':0} + #派单数据发送到OPC + self._pd_data='' + + #变频器相关 + #是否启动变频器0未1普通块启动2F块启动 3结束 + self.vf_status=0 + self.watched_props = [k for k in self.__dict__ if k.startswith('_')] + + def __setattr__(self, name, value): + super().__setattr__(name, value) + if name in self.watched_props: + with self.lock: + public_name = name.lstrip('_') + try: + if public_name.startswith('db'): + self.save_db(public_name,value) #有影响的话改成异步 + else: + if public_name=="pd_data": + #更新派单表 + if hasattr(value,'MouldCode'): + self.bll_pdrecord.start_pd(value.MouldCode,value.FBetonVolume) + _opc_pd={ + "ArtifactID":value.ArtifactID, + "TaskID":value.TaskID, + "ProduceMixID":value.ProduceMixID, + "BetonVolume":value.FBetonVolume, + "PlannedVolume":value.PlannedVolume + } + self.opc_queue.put_nowait((public_name, json.dumps(_opc_pd))) + else: + self.opc_queue.put_nowait((public_name, value)) + except queue.Full: + # 队列已满,记录异常但不中断程序 + print(f"OPC queue is full, skipping update for {public_name}") + except Exception as e: + # 捕获其他异常 + print(f"Unexpected error putting to OPC queue: {e}") + + # self.state_updated.emit(public_name, value) + + def save_db(self,public_name,val): + if not val: + return + if public_name=="db_status": + _code=val['code'] + if _code: + _status=val['status'] + if _status==3: + #完成生产 + self.bll_artifact.finish_produce(_code,val['weight']) + elif _status==2: + #开始生产 + self.bll_artifact.start_produce(_code) + + +class FeedStatus(IntEnum): + #初始值 + FNone = 0 + # 检查模车(模车到位) + FCheckGB = 1 + #RFID检测或匹配 + FRFID=2, + # 下料1(下料斗到模具车) + FFeed1=5 + #下料2(上--》下) + FFeed2 = 6 + # 下料3(下--》模) + FFeed3 = 7 + # 下料4(上--》下) + FFeed4 = 8 + #下料5(下-》模) + FFeed5 = 9 + #完成(管片生产完成) + FFinished = 11 + +class Upper_Door_Position(IntEnum): + # default(在搅拌楼下接料), over_lower(在下料斗上方), returning(返回中) + Default = 0 + Over_Lower = 1 + Returning = 2 + + +class Upper_PLC_Status(IntEnum): + # 即将振捣室 + PLC_ZDS_Ready = 4 + #到达振捣室 + PLC_ZDS_Finish = 5 + #即将搅拌楼 + PLC_JBL_Ready = 64 + #到达搅拌楼 + PLC_JBL_Finish = 66 diff --git a/db/messages.db b/db/messages.db index a4553b1c98df077e1173b38c4ec13f723250645e..0cf6ae724e13f9ab05f14c0e788ae6d38271ee53 100644 GIT binary patch delta 89 zcmZojXh@hK&1f=F#+lJ%W5N=CW@Cnxli3B*7*|d#v}W=)p8QZmL)A>zz);t~RKd`| f%GAQj$jr#V2wlkAXp*e<5dcc( B4b}hv diff --git a/db/three.db b/db/three.db index 448d15b19d8c1bcfd288757c02c6300fa79b28f9..5177849cdbacc0ccff45416cc9548bbb45eb26b5 100644 GIT binary patch literal 151552 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 delta 758 zcmaKoO=uHA6vt;jbdz>Jj5(x1mMmzkh?~yL&hEx|2)ib!V1%&dBqGL|f+z?v;6-RR z9)yZ*VnMuFLA93MKNepz z#N&mOsxphWTlFh98r7oLn7!G&I<)&vdnKn<(rS)dDQQD*C_nhdhKcGIrQ1nNNQwMmyaFdMFY+akH&$epyTi0buz25adsL?1aRb-=4;#6Z) z<5VJT^;wZ6`4Zs79uRxVw6qlXFT@o+^D$gH*E|?%)eYsJSGV)_;bne|lZ!w(D?gHp zfp5w4RXI&-1E2zs0rV?lafYFt9qKxN#=|bwbCw0+ab!a1;=<1I#be*!FS9I$*whi$ zv85q14`CkJS;MplwtOt5!PI`0Ri~1fbV4{xOU7dkaFWU7jE5`8fH`bIyA0T8Wcu#f zMrj5@a?&baV78-{6{M3v9)uX7vWJZk!ivA1oHr4|NQYsO diff --git a/db/three.db-shm b/db/three.db-shm deleted file mode 100644 index c735afc0d1e0d104bd79806cf09316d3ecfea122..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI*Aqv7^6bEpP2En*2tHUDpMKO5^Z?Gk^VYk>BR&8EEu-m-Cirbqa)-}Q3A07`r zKHh`h1zvK`CXwl@7SZQ@opxz^F7nm>a-FS@+f8*Vn`QktBqRU#qlU!X=U3j(pB6iX z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF u5FkK+009C7eoY{*&jN jBV8ji1w&IS0|P5Vb3J2IGZP~-1_s6~>~@WcQa1zuO$Z{H diff --git a/doc/table表设计.doc b/doc/table表设计.doc index f19a6bb76d63c854d613fdd5ec67834d33e0df55..9690e214588330d6d1172e808e46ff826b59bc2d 100644 GIT binary patch literal 78336 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 E{}C4XEdT%j delta 11544 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 kdYOMQ^JPh{W%p^PE_XHErZEbB= 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 delta 13224 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 Smy#%&XeUhq`BVI#zW)WPJRw*B diff --git a/feeding/__pycache__/process.cpython-39.pyc b/feeding/__pycache__/process.cpython-39.pyc index 4f5bdf1eef8764e2a827daf905ed3e4592813c3a..a48cb0582f787e47d82f3870d2f14574e92ce616 100644 GIT binary patch literal 6892 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 literal 4232 zcma)9+iw%u8J~+i9@}H*3Za0it#;e4yT$FIt-8D2R?_9s{ zJD1=0JLlw5sf2`2KJ&fej-8V9Un+EeDhhk>cYg=LBqlpjSvF;v@`|IBRZ|st)rpj& zW>n-Oj#iGDF_DitdO2>!%Ly|f%9@ijlhBPhM%ge8QKvhpa@tIbeB9|N_nN)3^h=2) zSn{mIl4lgNk7-AwjM2mxq>LQI4phAT6~ByZV%WCX5ijTYE=tD1s_(EPHa}j>+bAc0 z&b?wGm-l|*I2k4AYZnhx3e|&EW;=n=T{Qv~o#i~+E0#&!;DsWy+>W_)ZgLVu&&pRT zh2mJAhSU8iDD1)CeFwspOqofh!epkLl}weXEP^z`qD(^?Wih5B)mWS*kj7Y&8Ax@O z!c@~inkH7PjE(YY-ge!53?|zj<;mAx{M~mUYI04oBu_pgAyqt8q>-9}QnaQ*YPASt ztQLjTYZ_$SOO%o|sT8*iM%pPR!LKs%9(?z5+sU1zS$e$cI5y`|I5CY}Yc;K7j>98V zl`7MMx0fgwC{~W!+_QO?Zm`wkxr$pZdYkHdju+ixwPG>bv-4h+2YvZm+2*-!-KdML zwK<_Mhz^fbD|TR51+0uk4Zdrpimp{Ca@Vs8PHrp{3nH%V6u1Eeo+6PZ(LWy>TZacQzldfl%kG6K;_1Aa5b(DN!H;zt*+mW3-8R(W( ztQ0-VB4_R(grsV+j-S@~CDcGI(fVjNX_8_`|AP#6Mw-UHl;i~@vy41t8Z&1D$sF1BbSXGhsBPCk{HECh6hVf9ac6@GuwSU7}OjwIEol=Z;>%!2Y z6!sM8ijOpD(oLGJ|3ipf8^8L@t@_=I^|`B!D~}tOmNqt0srVSE^Iv^YbJx=+7aLbU zdQpRuL!Wl2G54r(>Ers+Tw`Y5-+_wyxlfwc9yVrAH*U_Zt$wh!d~d&;*f`J(Lxy)85Cx|6|{ zR!v(m42@ur&{@nm8^_$K?H+oNYT=v0YRnGe!^2Q^$89DeDTwEts%u+0Vo^>=58_TW zFJjOZu@s7})RKY;j&KYE24{-o(pF1|iL{(n4LKqkB1ZI9E`hS9=!z~UG_)Z_PCm7B zQ`~GYyCHaJi#v2kqw{c1g%Dhj1ppOLo9b^vQ+@tX{ldM*-1+*_^$;`74?k}#eHJ#Z zoF@Fu{J!zaQ$DhiAH(tR)c;E=csxl}|N5J{&HfcJF zb0CUT^VFJ#g;A!#|E0)6l$J*1M7b1W%Ax``&=SYX*ve0$$6RU8#JP% zff{2W#k56vTCeFW=1cshmkwD#SVM~}O_?`pE4P|SP3!K0h%ZJ_Yovm z3{dFze~dN5W{lx1?~dqaE7#XoAFSP7uFqet&)x?H)~=s@`s4zrrE&FI^Zcde`S%5* z1d-~bU1<_!4+l{Z9h~4b;_rfNTw7>-a%KI|a{c|Qo3x*;%n{2hKde8#wf^WzHkSdt-n{ndOFj*B?d~7m^XcI0I14czQetK>YaA$%+uA&&C`E*wlXV@`nurpuu+6-lfkSA&?envi%*gW)r=9MjE*P)l3=iX zw2H&81`6hKD9P^(*1Vilr2$4r*fLmiFFCfeJpo?jAAF!Fob5xoGtqW_n7c&?c%WtM>urJ&F2Ih3% w2zmg1Jb$xz341`3D-I4Z;I8S3*@_FH{!X}DKcjn^OIKq9$fX}}8s?P$0~ks7;Q#;t diff --git a/feeding/controller.py b/feeding/controller.py index e1bd80a..d89ee3d 100644 --- a/feeding/controller.py +++ b/feeding/controller.py @@ -1,246 +1,1195 @@ -# feeding/controller.py -import time -from core.state import FeedStatus -from feeding.process import FeedingProcess -from busisness.blls import ArtifactBll -from config.settings import app_set_config +from pickle import FALSE +from cv2.gapi import ov +from core.system_state import FeedStatus,Upper_Door_Position,SystemState,Upper_PLC_Status +from hardware.relay import RelayController +from hardware.transmitter import TransmitterController +import time +import threading +from datetime import datetime +import logging +import queue +from hardware.upper_plc import OmronFinsPollingService +from vision.muju_cls.muju_utils import run_stable_classification_loop +from vision.camera_picture import save_camera_picture class FeedingController: + # 下料控制 + _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, relay_controller, inverter_controller, - transmitter_controller, vision_detector, - camera_controller, rfid_controller,state): + transmitter_controller,plc_service, state): + """初始化视觉回调处理器""" + # 避免重复初始化 + if hasattr(self, '_initialized') and self._initialized: + return + self.relay_controller = relay_controller self.inverter_controller = inverter_controller self.transmitter_controller = transmitter_controller - self.vision_detector = vision_detector - self.camera_controller = camera_controller - self.rfid_controller = rfid_controller - self.state = state - self.artifact_bll = ArtifactBll() + self.plc_service = plc_service + self.state=state + + # 线程安全的参数传递 + self._new_data_available = threading.Event() + self._is_processing = threading.Lock() - # 初始化下料流程 - self.process = FeedingProcess( - relay_controller, inverter_controller, - transmitter_controller, vision_detector, - camera_controller, state + #diff参数 + self._is_processing_diff = threading.Lock() + self._new_data_diff = threading.Event() + self._current_diff=0 + self._current_diff_area=[] + self._is_diff_save=False + self._stop_event = threading.Event() + + # 添加下料斗门控制锁,防止两个线程同时控制 + self._door_control_lock = threading.Lock() + # 记录当前控制门的线程名称,用于调试 + self._current_controlling_thread = None + #是否启动后的第一个模具 + self._is_first_module=True + self.init_val() + # self._setup_logging_2() + #F块完成重量的70%,控制夹脚,F块多于这个比例就没有记录了(注意) + self._max_f_angle_ratio=0.7 + #完成多少,调整角度比例 ,多于0.8就没记录了(注意) + self._max_angle_radio=0.8 + + #重量大于95%,停留时间2秒,其他的1秒 + self._weight_ratio_955=0.955 + #完成多少,忽略未浇筑满 + self._max_ignore_radio=0.5 + + self._mould_accept_aligned=None + self._mould_before_aligned=False + #模具开始浇筑时间 + self._time_mould_begin='' + #模具结束浇筑时间 + self._time_mould_end='' + #重量判断为空次数 + self.max_weight_none=5 + #当前为空的次数 + self.cur_weight_none=0 + + self.state._mould_need_weight=1 + + + def start_feed_thread(self): + + self.plc_service.register_data_callback(self.on_plc_update) + # self.plc_service.register_status_callback(self.on_status_change) + self.plc_service.start_polling(interval=2.0) + + # 创建并启动单个持续运行的线程 + self.callback_thread = threading.Thread( + target=self._run_thread_loop, + daemon=True ) + self.callback_thread.start() + self.feed_thread = threading.Thread( + target=self._run_feed, + daemon=True + ) + self.feed_thread.start() + + """启动系统监控""" + self.monitor_thread = threading.Thread( + target=self._monitor_loop, + daemon=True, + name='monitor' + ) + self.monitor_thread.start() + + + self.diff_thread = threading.Thread( + target=self._diff_temp, + daemon=True + ) + self.diff_thread.start() + + def init_val(self): + #初始化值 + """初始化视觉回调处理器""" + self.angle_mode = "normal" + self.overflow = False + self.is_start_visual=False + # 线程安全的参数传递 + self._current_angle = None + self._overflow_detected = None + # 新增标志位:指示safe_control_lower_close是否正在执行 + self._is_safe_closing = False - def start_feeding(self): - #获取当前的管片任务 - #API读取生产任务 --》RFID读取模具配对检测--数据库入库》生产-》同步到数据库 - #API读取生产任务--》未读到--》RFID读取模具-数据库入库》生产 --》同步到数据库 - #从数据库获取当前的管片任务 - """启动下料流程""" - self.process.start_feeding() - - def check_upper_material_request(self): - """检查是否需要要料""" - current_weight = self.transmitter_controller.read_data(1) - if current_weight is None: - self.state.upper_weight_error_count += 1 - print(f"上料斗重量读取失败,错误计数: {self.state.upper_weight_error_count}") - if self.state.upper_weight_error_count >= app_set_config.max_error_count: - print("警告:上料斗传感器连续读取失败,请检查连接") - return False - #需要搅拌楼通知下完料后移到上料斗上方 + self._is_feed_start=True + #未浇筑满时间,用于确定是否进入未浇筑满 + self._before_finish_time=None + #进入未浇筑满状态标志位 + self._is_before_finish=False + #是否浇筑满标志位 + self._is_finish=False - self.state.upper_weight_error_count = 0 - # 判断是否需要要料:当前重量 < 目标重量 + 缓冲重量 - if self.state._feed_status != FeedStatus.FUpperToLower: - if current_weight < (app_set_config.min_required_weight): - print("上料斗重量不足,通知搅拌楼要料") - self.request_material_from_mixing_building() # 请求搅拌楼下料 - return True - else: - if self.state._upper_door_position != 'over_lower': - self.state._upper_door_position = 'over_lower' - return False + #用于保存diff标志位 + # self._is_diff_save=False + #用于判断当前判断是否对齐(diff) + self._is_diff_unaligned=False - def request_material_from_mixing_building(self): + #浇筑完成比例(重量) + self._is_finish_ratio=0 + + #下料阶段,用于控制下料斗的振动阶段 + self._is_feed_stage=0 + self._feed_status=FeedStatus.FNone + #振动相关参数 + self._last_arch_one_weight=0 + self._last_arch_two_weight=0 + self._last_arch_three_weight=0 + self._last_arch_four_weight=0 + self._last_arch_five_weight=0 + self._last_arch_time=0 + #是否为F块 + self._is_small_f=None + #采集数据用,下料重量=之前下的重量+最后一阶段(下-->模具车)重量差 + #记录最后一次下料斗到模具车前的重量 + self._mould_finish_weight=0 + + #记录最后一次下料斗到车初始重量 + self._inital_finish_lweight=0 + #记录视觉停止下料时的重量(计算后面加了多少) + self._last_lower_weight=0 + + # 初始化控制间隔和堆料状态跟踪属性 + self._last_overflow_state = False + self._last_control_time = 0 + self._is_running=True + self._initialized = True + self.plc_data=None + + #用于下料阶段初始值 + self._first_lower_weight_one=0 + self._is_first_lower_one=True + self._first_lower_weight_two=0 + self._is_first_lower_two=True + self._first_lower_weight_three=0 + self._is_first_lower_three=True + + def angle_visual_callback(self, current_angle, overflow_detected, mould_aligned): """ - 请求搅拌楼下料 + 视觉控制主逻辑,供外部推送数据 + 使用单个持续运行的线程,通过参数设置传递数据 + 如果线程正在处理数据,则丢弃此次推送 """ - print("发送要料请求至搅拌楼...") - # - - - self.process.return_upper_door_to_default() - # 这里需要与同事对接具体的通信方式 - # 可能是Modbus写寄存器、TCP通信、HTTP请求等 - pass - - def check_arch_blocking(self): - """检查是否需要破拱""" - 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) < 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 - - # 检查上料斗破拱(在上料斗向下料斗下料时检查) - if self.state._feed_status == FeedStatus.FUpperToLower: # 在任何阶段都可能需要上料斗破拱 - print('上料斗振动线程启用中...') - upper_weight = self.transmitter_controller.read_data(1) - if upper_weight is not None: - # 检查重量变化是否过慢(小于0.1kg变化且时间超过10秒),觉得有点小。改成 - if (abs(upper_weight - self.state.last_upper_weight) < 25) and \ - (current_time - self.state.last_weight_time) > 1: - print("上料斗可能堵塞,启动破拱") - self.state._upper_is_arch_=True - self.relay_controller.control(self.relay_controller.BREAK_ARCH_UPPER, 'open') - time.sleep(5) - self.relay_controller.control(self.relay_controller.BREAK_ARCH_UPPER, 'close') - self.state._upper_is_arch_=False - - self.state.last_upper_weight = upper_weight - - # 更新最后读取时间 - if (self.transmitter_controller.read_data(1) is not None or - self.transmitter_controller.read_data(2) is not None): - self.state.last_weight_time = current_time - - def visual_control(self, current_frame): - """ - 视觉控制主逻辑 - """ - # 检测是否溢料 - # print('视觉控制线程启用中...') - # self.state.overflow_detected = self.vision_detector.detect_overflow(current_frame) - overflow=self.state.overflow_detected in ["大堆料", "小堆料"] - current_angle =self.vision_detector.detect_angle(image=current_frame) - if current_angle is None: - print("无法获取当前角度,跳过本次调整") + #print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 收到推送数据") + # 尝试获取处理锁,若失败则说明正在处理,丢弃数据 + if not self._is_processing.acquire(blocking=False): + print("回调线程仍在执行,丢弃此次推送数据") return - self.state.last_angle = current_angle - self.state._lower_angle=current_angle - print(f"当前角度: {current_angle:.2f}°") - # return - if self.state.overflow_detected!="浇筑满": - if current_angle is None: - print("无法获取当前角度,跳过本次调整") - return + + try: + # 更新参数 + + if overflow_detected is not None: + # print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 收到溢料:{overflow_detected}") + self._overflow_detected = overflow_detected + if current_angle is not None: + #print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 收到角度:{current_angle}") + self._current_angle = current_angle + if mould_aligned is not None: + #print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 收到对齐:{mould_aligned}") + self._mould_accept_aligned=mould_aligned + # 通知线程有新数据可用 + self._new_data_available.set() + finally: + # 释放处理锁 + self._is_processing.release() - # print(f"当前角度: {current_angle:.2f}°, 溢料状态: {overflow}, 控制模式: {self.state.angle_control_mode}") + def diff_visual_callback(self, current_diff,current_area): + """ + 视觉模型diff回调 + """ + #print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 收到推送数据") + # 尝试获取处理锁,若失败则说明正在处理,丢弃数据 + if not self._is_processing_diff.acquire(blocking=False): + print("222回调线程仍在执行,丢弃此次推送数据") + return + + try: + # 更新参数 + if current_diff is not None: + # print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 收到diff:{current_diff}") + self._current_diff = current_diff + if current_area is not None: + # print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 收到area:{current_area}") + self._current_diff_area = current_area + # 通知线程有新数据可用 + self._new_data_diff.set() + finally: + # 释放处理锁 + self._is_processing_diff.release() - # 状态机控制逻辑 - if self.state.angle_control_mode == "normal": - # 正常模式大于app_set_config.angle_threshold=60度 - if overflow: - # if current_angle > app_set_config.angle_threshold: - # # 检测到堆料且角度过大,进入角度减小模式 - # print("检测到堆料且角度过大,关闭出砼门开始减小角度") - # self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') - # self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') - self.state.angle_control_mode = "reducing" - else: - # 保持正常开门 30 - # self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') - print(f'当前重量:{self.state._mould_finish_weight:.2f}kg, 目标重量:{self.state._mould_need_weight:.2f}kg') - if self.state._mould_need_weight>0: + def _diff_temp(self): + """ + 接受视觉回调数据 + 线程主循环,持续运行 + 等待新数据,然后调用处理方法 + """ + _temp_diff_count=0 + _temp_area_count=0 + _temp_diff_str2='' + _temp_area_str2='' + while not self._stop_event.is_set(): + # print('-----等待diff 数据------') + # 等待新数据可用 + self._new_data_diff.wait() + # 重置事件 + self._new_data_diff.clear() + #_is_diff_save是否完成此片 + if self._is_diff_save: + # print('-----进入diff 数据------') + #完成了此片,然后是对齐状态 + if not self._is_diff_unaligned: + # 处理数据 + # print('-----进入对齐数据------') + if self._current_diff is not None and self._current_diff_area is not None: + _timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if _temp_diff_count<=10 and self._current_diff!=0: + _temp_diff_str=f"diff , {_timestamp} , {self._current_diff}\n" + _temp_diff_count+=1 + with open('weight.txt', 'a') as f: + f.write(_temp_diff_str+'\n') + # print('-----保存成功(diff 数据)------') + + if _temp_area_count<=10 and self._current_diff_area!=[]: + _temp_area_str=f"area , {_timestamp} , {str(self._current_diff_area)}\n" + _temp_area_count+=1 + with open('weight.txt', 'a') as f: + f.write(_temp_area_str+'\n') + # print('-----保存成功(area 数据)------') + if _temp_diff_count>=10 and _temp_area_count>=10: + self._is_diff_save=False + time.sleep(1) + continue + # else: + #变成了未对齐,拉起盖板后,重新计数 + # if _temp_diff_count>=10 and _temp_area_count>=10: + # _temp_diff_count=0 + # _temp_area_count=0 + # self._is_diff_save=False + # _temp_diff_str='' + # _temp_area_str='' + + self._current_diff=0 + self._current_diff_area=[] + _temp_diff_count=0 + _temp_area_count=0 + _temp_diff_str='' + _temp_area_str='' + time.sleep(1) + + def _monitor_loop(self): + """监控循环""" + while self._is_running: + try: + current_time = time.time() + # 检查下料斗破拱(只有在下料过程中才检查) + if self._is_feed_stage==1: # 下料斗--》模具车 + _arch_weight = self.transmitter_controller.read_data(2) + if _arch_weight is not None and _arch_weight>0: + # 检查重量变化是否过慢 + _weight_changed=abs(_arch_weight - self._last_arch_one_weight) + #_last_arch_one_weight默认为0,一开始就进入振动 + print(f'---------------第一阶段,重量变化:{_weight_changed}------------------') + if (_weight_changed< 200) and \ + (current_time - self._last_arch_time) >= 2: + self._last_arch_time = current_time + print('---------------------第一阶段振动5秒(小于200KG)-----------------') + self.relay_controller.control_arch_lower_open_sync(5) + self._last_arch_one_weight = _arch_weight + continue + self._last_arch_one_weight = _arch_weight + + elif self._is_feed_stage==2: #上料斗到下料斗,料多,谨慎振动 + _arch_weight = self.transmitter_controller.read_data(1) + if _arch_weight is not None and _arch_weight>0: + # 检查重量变化是否过慢 + _weight_changed=abs(_arch_weight - self._last_arch_two_weight) + print(f'---------------第二阶段,重量变化:{_weight_changed}------------------') + if (_weight_changed < 100) and \ + (current_time - self._last_arch_time) >= 2: + self._last_arch_time = current_time + print('---------------------第二阶段振动3秒-----------------') + self.relay_controller.control_arch_upper_open_sync(3) + self._last_arch_two_weight = _arch_weight + continue + self._last_arch_two_weight = _arch_weight + elif self._is_feed_stage==3: #第二次下料斗-》模具车 + _arch_weight = self.transmitter_controller.read_data(2) + if _arch_weight is not None and _arch_weight>0: + #刚开始不需要振动,料太多 + if self._last_arch_three_weight>0: + _weight_changed=abs(_arch_weight - self._last_arch_three_weight) + # 检查重量变化是否过慢 + print(f'---------------第三阶段,重量变化:{_weight_changed}------------------') + if (_weight_changed < 100) and \ + (current_time - self._last_arch_time) >= 2: + self._last_arch_time = current_time + print('---------------------第三阶段振动5秒(小于100KG)-----------------') + self.relay_controller.control_arch_lower_open_sync(5) + self._last_arch_three_weight = _arch_weight + continue + self._last_arch_three_weight = _arch_weight + + elif self._is_feed_stage==4: #上料斗--》下料斗 + _arch_weight = self.transmitter_controller.read_data(1) + if _arch_weight is not None and _arch_weight>0: + # 检查重量变化是否过慢 + _weight_changed=abs(_arch_weight - self._last_arch_four_weight) + print(f'---------------第二阶段,重量变化:{_weight_changed}------------------') + if (_weight_changed < 200) and \ + (current_time - self._last_arch_time) > 2: + self._last_arch_time = current_time + print('---------------------第四阶段振动5秒-----------------') + self.relay_controller.control_arch_upper_open_sync(5) + self._last_arch_four_weight = _arch_weight + continue + self._last_arch_four_weight = _arch_weight + elif self._is_feed_stage==5: #下料斗->模具车 + _arch_weight = self.transmitter_controller.read_data(2) + if _arch_weight is not None and _arch_weight>0: + if self._last_arch_five_weight>0: + _weight_changed=abs(_arch_weight - self._last_arch_five_weight) + print(f'---------------第五阶段,重量变化:{_weight_changed}------------------') + _min_arch_weight=20 + if self._is_finish_ratio= 2: + self._last_arch_time = current_time + print(f'---------------------第五阶段振动3秒(小于{_min_arch_weight}kg))-----------------') + self.relay_controller.control_arch_lower_open_sync(3) + self._last_arch_five_weight = _arch_weight + continue + self._last_arch_five_weight = _arch_weight - 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') - + # 更新最后读取时间 + self._last_arch_time = current_time + time.sleep(2) + except Exception as e: + print(f"监控线程错误: {e}") - elif self.state.angle_control_mode == "reducing": - # 角度减小模式 - if overflow: - if current_angle <= app_set_config.target_angle: - # 角度已达到目标范围 - - # 仍有堆料,进入维持模式 - print(f"角度已降至{current_angle:.2f}°,仍有堆料,进入维持模式") - if current_angle<=app_set_config.min_angle: - self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') - self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') - time.sleep(0.1) - self.state.angle_control_mode = "maintaining" - # self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') - # self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') + def _aligned_get_times(self,flag): + """ + 获取对齐,1为对齐,0为未对齐 + """ + _current_times=time.time() + _temp_aligned_count=0 + if flag==1: + while time.time()-_current_times<=2: + print(f'-------------{self._mould_accept_aligned}-----------------') + if self._mould_accept_aligned=='盖板对齐': + _temp_aligned_count=_temp_aligned_count+1 + else: + _temp_aligned_count=0 + print(f'-------------{datetime.now().strftime("%H:%M:%S")} 盖板对齐,次数:{_temp_aligned_count}-----------------') + time.sleep(0.2) + self._mould_accept_aligned='' + if _temp_aligned_count>=8: + return True + else: + return False + elif flag==2: + while time.time()-_current_times<=5: + + if self._mould_accept_aligned=='盖板未对齐': + _temp_aligned_count=_temp_aligned_count+1 + else: + _temp_aligned_count=0 + print(f'-------------{datetime.now().strftime("%H:%M:%S")} 盖板未对齐,次数:{_temp_aligned_count}-----------------') + + time.sleep(0.2) + + self._mould_accept_aligned='' + if _temp_aligned_count>=20: + self._is_diff_unaligned=True + return True + else: + self._is_diff_unaligned=False + return False + + def _no_aligned_diff(self): + """ + diff 未对齐检测 + """ + _current_times=time.time() + _temp_aligned_count=0 + while time.time()-_current_times<=1: + # print(f'-------------{self._mould_accept_aligned}-----------------') + if self._mould_accept_aligned=='盖板未对齐': + _temp_aligned_count=_temp_aligned_count+1 + else: + _temp_aligned_count=0 + # print(f'-------------{datetime.now().strftime("%H:%M:%S")} 盖板对齐,次数:{_temp_aligned_count}-----------------') + time.sleep(0.2) + if _temp_aligned_count>=3: + return True + else: + return False + + def _run_thread_loop(self): + """ + 接受视觉回调数据 + 线程主循环,持续运行 + 等待新数据,然后调用处理方法 + """ + while not self._stop_event.is_set(): + # 等待新数据可用 + self._new_data_available.wait() + + # 重置事件 + self._new_data_available.clear() + + # 获取当前参数(使用临时变量避免被其他线程修改) + current_angle = self._current_angle + overflow_detected = self._overflow_detected + self._is_feed_start=True + + if self.is_start_visual: + # 处理数据 + self._process_angle_callback(current_angle, overflow_detected) + time.sleep(0.1) + + def _run_feed(self): + + while True: + # print("------------已启动----------------") + if self._is_feed_start: + + # if self.plc_data==5: + #_is_finish_ratio完成 比例,根据重量过滤一下 + + if self._is_first_module and self._overflow_detected=='未堆料': + #第一次打开 ,未堆料,检测对齐 + _is_aligned=self._aligned_get_times(1) + if _is_aligned: + print('------------进入第一块111111-------------') + self._is_first_module=False + self._mould_before_aligned=True + # self.is_start_visual=True + self.run_feed_all() + elif self._is_finish and self._is_finish_ratio>=0.7: + #后续流程--》检查到未对齐,--》后又对齐+未堆料 + print('------------------进入连续块检测------------------') + if self._mould_before_aligned: + #未对齐,检测对齐 + _is_not_aligned=self._aligned_get_times(2) + if _is_not_aligned: + #标志位 + self._mould_before_aligned=False + print('------------连续盖板未对齐-------------') else: + _is_aligned=self._aligned_get_times(1) + if _is_aligned and self._overflow_detected=='未堆料': + print('------------连续盖板已对齐-------------') + self._mould_before_aligned=True + print('-----------进入连续块111111-----------') + # self.is_start_visual=True + if self._last_lower_weight>0: + _current_weight=self.transmitter_controller.read_data(2) + if _current_weight is not None: + with open('weight.txt', 'a') as f: + f.write(f"补料:{self._last_lower_weight-_current_weight}\n\n"+"="*32) + self.init_val() + self.run_feed_all() + + # else: + # print("-----------上料斗未就位----------------") + # print("---------3--上料斗未就位----------------") + + time.sleep(0.2) + + def safe_control_lower_close(self,duration=3): + """线程安全的下料斗关闭方法""" + thread_name = threading.current_thread().name + # print(f"[{thread_name}] 尝试关闭下料斗...") + # 设置标志位,指示正在执行安全关闭操作 + self._is_safe_closing = True + try: + with self._door_control_lock: + self._current_controlling_thread = thread_name + #print(f"[{thread_name}] 获得下料斗控制权,执行关闭操作") self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') - # time.sleep(0.05) - # 先打开门 - else: - # 无堆料,恢复正常模式 - print(f"角度已降至{current_angle:.2f}°,无堆料,恢复正常模式") - # self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') - # self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') - self.state.angle_control_mode = "normal" + time.sleep(duration) + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self._current_controlling_thread = None + #print(f"[{thread_name}] 释放下料斗控制权") + finally: + # 无论成功失败,都要重置标志位 + self._is_safe_closing = False + + def close_lower_door_visual(self): + """关闭下料斗门""" + self.is_start_visual=False + time.sleep(0.5) + self.safe_control_lower_close() - elif self.state.angle_control_mode == "maintaining": - # 维持模式 - 使用脉冲控制 - if not overflow: - # 堆料已消除,恢复正常模式 - print("堆料已消除,恢复正常模式") - # self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') - # self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') - self.state.angle_control_mode = "normal" + def _visual_close(self): + self.is_start_visual=False + self._is_finish=True + self._is_feed_stage=0 + print(f'--------进入关闭(浇筑满)-----------') + self.safe_control_lower_close(3) + print(f'--------关闭完成-----------') + # try: + # self.db_queue.put_nowait({ + # "f":self._is_small_f, + # "Status": 3 + # }) + # except queue.Full: + # print("数据库队列已满,无法添加数据") + #记录重量 + _current_weight=self.transmitter_controller.read_data(2) + if _current_weight is not None: + self._last_lower_weight=_current_weight + self.state._mould_finish_weight= self.state._mould_finish_weight+(self._inital_finish_lweight-_current_weight) + with open('weight.txt', 'a') as f: + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if self._is_small_f: + f.write(f"{self._time_mould_begin},{timestamp},F,{self.state._mould_finish_weight}\n") else: - # 继续维持角度控制 - self.pulse_control_door_for_maintaining() + f.write(f"{self._time_mould_begin},{timestamp},B,{self.state._mould_finish_weight}\n") + #开启保存diff + self._is_diff_save=True + + #保存图片 + save_camera_picture() - elif self.state.angle_control_mode == "recovery": - # 恢复模式 - 逐步打开门 - if overflow: - # 又出现堆料,回到角度减小模式 - print("恢复过程中又检测到堆料,回到角度减小模式") - self.state.angle_control_mode = "maintaining" + def run_feed_all(self): + """ + 全流程下料:包括判断模具类型 + """ + _is_f= run_stable_classification_loop() + print(f'------------已判断出模具类型: {_is_f}-------------') + if _is_f is not None: + if _is_f=='模具车1': + self._is_small_f=True + print('-------------F块模具--------------') + print('-------------F块模具--------------') + print('-------------F块模具--------------') + self._time_mould_begin=datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self.state._mould_need_weight=0.54*2416 + self.state._mould_finish_weight=0 + self.run_feed_f() + elif _is_f=='模具车2': + self._is_small_f=False + self._time_mould_begin=datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self.state._mould_need_weight=1.91*2416 + self.state._mould_finish_weight=0 + self.run_feed() + print('-------------其他模具---------------') + + if self._is_small_f is None: + print('-----------未判断出模具类型--------------') + return + + def run_feed_f(self): + """第一阶段下料:下料斗向模具车下料(低速)""" + print("--------------------开始下料(F块)--------------------") + # loc_relay=self.relay_controller + loc_mitter=self.transmitter_controller + + initial_lower_weight=loc_mitter.read_data(2) + if initial_lower_weight is None: + print("-----f上料斗重量异常-----") + return + #初始下料斗重量 + self._inital_finish_lweight=initial_lower_weight + if initial_lower_weight>100: + if not self._is_finish: + self.is_start_visual=True + initial_lower_weight=loc_mitter.read_data(2) + if initial_lower_weight is None: + print("-----f上料斗重量异常2-----") + return + self._is_feed_stage=5 + while not self._is_finish: + current_weight = loc_mitter.read_data(2) + if current_weight is None: + self.cur_weight_none+=1 + if self.cur_weight_none>self.max_weight_none: + #如果重量连续5次为None,认为下料斗未就位,跳出循环 + print('------------f下到模具车,下料斗重量异常----------------') + print('------------f下到模具车,下料斗重量异常----------------') + self.close_lower_door_visual() + return + #视觉处理关闭,异常的话重量没有生效 + continue + self.cur_weight_none=0 + self.state._mould_finish_weight=initial_lower_weight-current_weight + self._is_finish_ratio=(self.state._mould_finish_weight)/self.state._mould_need_weight + print(f'------------已下料比例: {self._is_finish_ratio}-------------') + if self._is_finish_ratio>self._max_f_angle_ratio: + #关5秒 + #大于0.7后不再检测了,直接交给视觉控制夹脚 + # print(f'------------已下料比例: {self._is_finish_ratio}-------------') + break + + time.sleep(1) + # initial_lower_weight=_current_lower_weight + print(f'------------已下料(F): {self.state._mould_finish_weight}kg-------------') + print(f'------------已下料(F): {self.state._mould_finish_weight}kg-------------') + + print(f'------------已完成-------------') + + def run_feed(self): + """第一阶段下料:下料斗向模具车下料(低速)""" + print("--------------------开始下料(普通块)--------------------") + loc_relay=self.relay_controller + loc_mitter=self.transmitter_controller + + if self._feed_status==FeedStatus.FFeed1: + self.is_start_visual=True + if self._is_first_lower_one: + initial_lower_weight=loc_mitter.read_data(2) + if initial_lower_weight is None: + print("---------------下料斗重量异常----------------") + return + self._first_lower_weight_one=initial_lower_weight + self._is_first_lower_one=False + + if self._first_lower_weight_one>100: + #下料斗的料全部下完 + self._is_feed_stage=1 + + current_weight = loc_mitter.read_data(2) + if current_weight is None: + self.cur_weight_none+=1 + if self.cur_weight_none>self.max_weight_none: + print("-----------下料斗重量异常(第一次下到模具车)--------------") + self.close_lower_door_visual() + return + #continue + self.cur_weight_none=0 + if current_weight<250 and current_weight>0: + self.close_lower_door_visual() + self._feed_status=FeedStatus.FFeed2 + time.sleep(1) + elif self._feed_status==FeedStatus.FFeed2: + # if self._is + _current_lower_weight=loc_mitter.read_data(2) + if _current_lower_weight is None: + print("-------下料斗重量异常---------") + return + self.state._mould_finish_weight=initial_lower_weight-_current_lower_weight + # initial_lower_weight=_current_lower_weight + print(f'------------已下料(第一次): {self.state._mould_finish_weight}kg-------------') + print(f'------------已下料(第一次): {self.state._mould_finish_weight}kg-------------') + + + self._is_feed_stage=0 + + while self.plc_data!=Upper_PLC_Status.PLC_ZDS_Finish: + print('------------上料斗未就位----------------') + print('------------上料斗未就位----------------') + time.sleep(1) + + if self.plc_data==Upper_PLC_Status.PLC_ZDS_Finish: + print(f'------------上料斗向下料斗转移(留3000KG)-------------') + #打开上料斗出砼门,开5就,开三分之一下 + + loc_relay.control_upper_open_sync(6) + self._is_feed_stage=2 + loc_time_count=1 + upper_open_time=time.time() + + while not self._is_finish: + current_upper_weight = loc_mitter.read_data(1) + if current_upper_weight is None: + self.cur_weight_none+=1 + if self.cur_weight_none>self.max_weight_none: + #如果重量连续5次为None,认为上料斗未就位,跳出循环 + print('------------第一次上到下,上料斗重量异常----------------') + print('------------第一次上到下,上料斗重量异常----------------') + loc_relay.control_upper_close_sync(5+loc_time_count) + return + continue + self.cur_weight_none=0 + _two_lower_weight=loc_mitter.read_data(2) + if _two_lower_weight is None: + _two_lower_weight=0 + if (current_upper_weight<3000 and current_upper_weight>0) or _two_lower_weight>3200: + #关5秒,loc_time_count多关一秒 + loc_relay.control_upper_close_sync(5+loc_time_count) + break else: - # 堆料已消除,恢复正常模式 - print("堆料已消除,恢复正常模式") - self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') - self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') - self.state.angle_control_mode = "normal" + if time.time()-upper_open_time>5: + if loc_time_count<6: + upper_open_time=time.time() + loc_relay.control_upper_open_sync(0.8) + loc_time_count=loc_time_count+0.8 + else: + time.sleep(0.5) + else: + loc_relay.control_upper_close_sync(6+loc_time_count) + + self.is_start_visual=True + initial_lower_weight=loc_mitter.read_data(2) + if initial_lower_weight is None: + print("-------下料斗重量异常(第二次下料到模具车)---------") + return + self._is_feed_stage=3 + while not self._is_finish: + current_weight = loc_mitter.read_data(2) + if current_weight is None: + self.cur_weight_none+=1 + if self.cur_weight_none>self.max_weight_none: + print("-------下料斗重量异常(第二次下料到模具车)---------") + self.close_lower_door_visual() + return + continue + self.cur_weight_none=0 + # second_finish_weight=initial_lower_weight-current_weight + if current_weight<250: + self.close_lower_door_visual() + break + time.sleep(1) + _current_lower_weight=loc_mitter.read_data(2) + if _current_lower_weight is None: + print("-------下料斗重量异常(第二次下到模)---------") + return + self.state._mould_finish_weight+=initial_lower_weight-_current_lower_weight + print(f'------------已下料(第二次): {self.state._mould_finish_weight}kg-------------') + print(f'------------已下料(第二次): {self.state._mould_finish_weight}kg-------------') + + self._is_feed_stage=0 + if self.plc_data==Upper_PLC_Status.PLC_ZDS_Finish: + #第二次上料斗向下料斗转移 + loc_relay.control_upper_open_sync(12) + loc_time_count=1 + upper_open_time=time.time() + upper_open_time_2=None + #第二次到下料斗还需要的量 + # initial_upper_weight=loc_mitter.read_data(1) + # start_time=None + self._is_feed_stage=4 + while not self._is_finish: + # print(f'------------上料斗向下料斗转移22222-------------') + current_upper_weight = loc_mitter.read_data(1) + if current_upper_weight is None: + self.cur_weight_none+=1 + if self.cur_weight_none>self.max_weight_none: + #如果重量连续5次为None,认为上料斗未就位,跳出循环 + print('------------第二次上到下,上料斗重量异常----------------') + print('------------第二次上到下,上料斗重量异常----------------') + loc_relay.control_upper_close_sync(15) + break + continue + self.cur_weight_none=0 + if (current_upper_weight<600 and current_upper_weight>0) or upper_open_time_2 is not None: + if upper_open_time_2 is None: + upper_open_time_2=time.time() + if current_upper_weight<400 or time.time()-upper_open_time_2>5: + loc_relay.control_arch_upper_open_async(5) + # loc_relay.control_arch_upper_open() + loc_relay.control_upper_open_sync(5) + # start_time=None + #5秒后关闭 + loc_relay.control_upper_close_after()#control_upper_close_sync(8+loc_time_count) + break + time.sleep(1) + else: + if time.time()-upper_open_time>2: + # if loc_time_count<6: + upper_open_time=time.time() + loc_relay.control_upper_open_sync(1.2) + loc_time_count=loc_time_count+1 + else: + time.sleep(0.5) + else: + loc_relay.control_upper_close_sync(15) + # time.sleep(0.4) + + #第三次下料斗转移到模具车 + if not self._is_finish: + self.is_start_visual=True + initial_lower_weight=loc_mitter.read_data(2) + self._inital_finish_lweight=initial_lower_weight + if initial_lower_weight is None: + print("-------下料斗重量异常(第三次下到模具车)---------") + return + self._is_feed_stage=5 + while not self._is_finish: + current_weight = loc_mitter.read_data(2) + if current_weight is None: + self.cur_weight_none+=1 + if self.cur_weight_none>self.max_weight_none: + #重量异常退出 + print('------------第三次下到模具车,下料斗重量异常----------------') + self.close_lower_door_visual() + return + continue + self.cur_weight_none=0 + second_finish_weight=initial_lower_weight-current_weight + self._is_finish_ratio=(second_finish_weight+self.state._mould_finish_weight)/self.state._mould_need_weight + print(f'------------已下料比例: {self._is_finish_ratio}-------------') + if self._is_finish_ratio>=1: + #关5秒 + # print(f'------------已下料比例: {self._is_finish_ratio}-------------') + break + + time.sleep(1) + + # _current_lower_weight=loc_mitter.read_data(2) + + + print(f'------------已完成-------------') + + def _process_angle_callback(self, current_angle, overflow_detected): + """ + 实时精细控制 - 基于PID思想,无固定间隔 + """ + try: + # 记录控制时间戳(用于微分计算,而非限制) + current_time = time.time() + # 确保所有PID相关属性都被正确初始化 + if not hasattr(self, '_last_control_time'): + self._last_control_time = current_time + if not hasattr(self, '_last_error'): + self._last_error = 0 + if not hasattr(self, '_error_integral'): + self._error_integral = 0 + + # print(f"{self.angle_mode}") + self.overflow = overflow_detected in ["大堆料", "小堆料"] + + if current_angle is None: + return + + print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 角度11: {current_angle:.2f}°,{overflow_detected}") + + if overflow_detected == "未浇筑满" or self._is_before_finish: + if self._before_finish_time is None: + self._before_finish_time=current_time + self.safe_control_lower_close(3) + print('-----------------关闭(未浇筑满)--------------------') + # time.sleep(3) + else: + if overflow_detected=='浇筑满': + self._visual_close() + return + # print(f'--------已关闭已关闭-----------') + elif overflow_detected=="大堆料": + print(f'--------未浇筑满,大堆料-----------') + self._pulse_control('open',0.3) + time.sleep(0.3) + self._pulse_control('close',0.4) + print(f'--------比例:{self._is_finish_ratio}-----------') + if self._is_finish_ratio>= self._weight_ratio_955: + time.sleep(2) + else: + time.sleep(1) + self._is_before_finish=True + else: + # self._pulse_control('open',0.5) + # time.sleep(0.3) + # self._pulse_control('close',0.6) + # print(f'--------比例:{self._is_finish_ratio}-----------') + self._pulse_control('open',0.6) + time.sleep(0.3) + self._pulse_control('close',0.7) + if self._is_finish_ratio>= self._weight_ratio_955: + time.sleep(2) + else: + time.sleep(1) + self._is_before_finish=True + if self._is_finish_ratio<=self._max_ignore_radio: + #如果重量未达到最大忽略角度,需要跳出 + self._is_before_finish=False + return + elif overflow_detected == "浇筑满": + self._visual_close() + return + else: + self._before_finish_time=None + if self._is_finish_ratio>=self._max_angle_radio or (self._is_finish_ratio>self._max_f_angle_ratio and self._is_small_f): + if overflow_detected == "大堆料": + TARGET_ANGLE = 5.0 # 大堆料时控制在15度左右 + elif overflow_detected == "小堆料": + TARGET_ANGLE = 15.0 # 小堆料时控制在35度左右 + else: + TARGET_ANGLE = 35.0 # 12.25由25--》35 + else: + if self._is_feed_stage==1 or self._is_feed_stage==3: + #根据溢料状态动态调整目标角度 + if overflow_detected == "大堆料": + TARGET_ANGLE = 15.0 # 大堆料时控制在15度左右 + elif overflow_detected == "小堆料": + TARGET_ANGLE = 45.0 # 小堆料时控制在35度左右 + else: + TARGET_ANGLE = 55.0 # 未溢料时开到最大56度 + else: + #根据溢料状态动态调整目标角度 + if overflow_detected == "大堆料": + TARGET_ANGLE = 15.0 # 大堆料时控制在15度左右 + elif overflow_detected == "小堆料": + TARGET_ANGLE = 25.0 # 小堆料时控制在35度左右 + else: + TARGET_ANGLE = 45.0 # 未溢料时开到最大56度 + + # 确保目标角度在硬件范围内(5-56度) + TARGET_ANGLE = max(5.0, min(56.0, TARGET_ANGLE)) + + # PID控制参数 + KP = 0.2 # 比例系数 + KI = 0 # 积分系数 + KD = 0 # 微分系数 + # KP = 0.15 # 比例系数 + # KI = 0.008 # 积分系数 + # KD = 0.08 # 微分系数 + # if TARGET_ANGLE <= 25.0: + # KP, KI, KD = 0.18, 0.008, 0.08 # 小角度,强控制 + # elif TARGET_ANGLE <= 40.0: + # KP, KI, KD = 0.15, 0.01, 0.06 # 中角度 + # else: + # KP, KI, KD = 0.12, 0.012, 0.04 # 大角度,温和控制 + # 计算误差 + error = current_angle - TARGET_ANGLE + dt = current_time - self._last_control_time + + # 积分项(抗饱和) + self._error_integral += error * dt + self._error_integral = max(min(self._error_integral, 50), -50) # 积分限幅 + + # 微分项 + error_derivative = (error - self._last_error) / dt if dt > 0 else 0 + + # PID输出 + pid_output = (KP * error + KI * self._error_integral + KD * error_derivative) + print(f"📊 PID计算: 误差={error:.2f}°, 积分={self._error_integral:.2f}, " + f"微分={error_derivative:.2f}, 输出={pid_output:.2f}") + # 更新历史值 + self._last_error = error + self._last_control_time = current_time + + # 状态机 + PID控制 + + if self.angle_mode == "normal": + self._normal_mode_advanced(current_angle, pid_output,TARGET_ANGLE) + + elif self.angle_mode == "reducing": + self._reducing_mode_advanced(current_angle, pid_output, TARGET_ANGLE) + + elif self.angle_mode == "maintaining": + self._maintaining_mode_advanced(current_angle, pid_output, TARGET_ANGLE) + + except Exception as e: + print(f"处理视觉回调时发生异常: {e}") + + def _normal_mode_advanced(self, current_angle, pid_output,target_angle): + """高级正常模式控制""" + if self.overflow: + self.angle_mode = "reducing" + print("检测到溢料,切换到减小模式") + return + + # 🎯 修复1: 添加强制控制机制 + + + # 基于PID输出的智能控制 + control_threshold = 2 # 从2.0减小到0.5,提高灵敏度 + + if abs(pid_output) > control_threshold: + if pid_output > 0: + # 需要减小角度(关门) + pulse_time = min(0.3, pid_output * 0.1) + self._pulse_control("close", pulse_time) + print(f"正常模式: 角度偏高{pid_output:.1f},关门{pulse_time:.2f}秒") + else: + # 需要增大角度(开门) + pulse_time = min(0.3, abs(pid_output) * 0.1) + self._pulse_control("open", pulse_time) + print(f"正常模式: 角度偏低{abs(pid_output):.1f},开门{pulse_time:.2f}秒") else: - # self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') - self.relay_controller.control_lower_close() - + # 在死区内,保持静止 + 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 pulse_control_door_for_maintaining(self): - """ - 用于维持模式的脉冲控制 - 保持角度在目标范围内 - """ - 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') + 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 + self.state.upper_door_position=data + + @classmethod + def instance_exists(cls): + """检测实例是否存在""" + return cls._instance is not None + + def shutdown(self): + """关闭线程,清理资源""" + # 设置停止事件 + self._stop_event.set() + # 唤醒线程以便它能检测到停止事件 + self._new_data_available.set() + + self._is_running=False + self._is_finish=True + self.is_start_visual=False + # #关闭下料斗 + # self.safe_control_lower_close() + if self.plc_service: + self.plc_service.stop_polling() + # 等待线程结束 + if self.callback_thread.is_alive(): + self.callback_thread.join(timeout=1.0) + + if self.feed_thread.is_alive(): + self.feed_thread.join(timeout=1.0) + + if self.monitor_thread.is_alive(): + self.monitor_thread.join(timeout=1.0) + + # self.relay_controller._close_lower_5s + + def __del__(self): + """析构函数,确保线程安全关闭""" + self.shutdown() + +# 创建默认实例 +# visual_callback_instance = VisualCallback() + +# 兼容层,保持原来的函数调用方式可用 +# def angle_visual_callback(current_angle, overflow_detected): +# """ +# 兼容旧版本的函数调用方式 +# 将调用转发到默认实例的angle_visual_callback方法 +# """ +# visual_callback_instance.angle_visual_callback(current_angle, overflow_detected) \ No newline at end of file diff --git a/feeding/process copy.py b/feeding/process copy.py new file mode 100644 index 0000000..e33dd97 --- /dev/null +++ b/feeding/process copy.py @@ -0,0 +1,167 @@ +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 FeedingProcess: + 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.FNone + + #标志位用,是否是第一次运行 + self.is_first_flag=True + + def start_feeding(self): + loc_state=self.state + loc_state._upper_weight=self.transmitter_controller.read_data(1) + loc_state._lower_weight=self.transmitter_controller.read_data(2) + # loc_state._upper_volume=round(loc_state._upper_weight/self.state.density,1) + """开始生产管片""" + if loc_state._feed_status == FeedStatus.FNone: + return + elif loc_state._feed_status == FeedStatus.FCheckM: + print("---------------初始化数据------------------") + loc_state._mould_need_weight= 1200 + loc_state._feed_status = FeedStatus.FFeed + return + elif loc_state._feed_status == FeedStatus.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 + self.feeding_stage(loc_state) + # if loc_state._mould_need_weight>loc_state.initial_lower_weight: + # self.transfer_material_from_upper_to_lower(loc_state,loc_state.initial_upper_weight,loc_state.initial_lower_weight) + # loc_state._feed_status = FeedStatus.FFeed1 + # else: + # loc_state._feed_status = FeedStatus.FFeed1 + return + elif loc_state._feed_status == FeedStatus.FFinished: + """完成当前批次下料""" + print("振捣完成") + print("关闭所有网络继电器") + self.relay_controller.close_all() + return + + def _start_feeding_stage(self): + """启动指定下料阶段""" + """开始分步下料""" + 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) + # 确保下料斗出砼门关闭,同步关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 + # 等待物料流入下料斗,基于上料斗重量变化控制 + + 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 + # 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 feeding_stage(self,loc_state): + """第一阶段下料:下料斗向模具车下料(低速)""" + print("开始下料") + # self.relay_controller.control + first_finish_weight=0 + while True: + current_weight = loc_state._lower_weight + loc_state._mould_finish_weight=loc_state.initial_lower_weight-current_weight + first_finish_weight=loc_state._mould_finish_weight + if current_weight<100: + #关5秒 + self.relay_controller.control_lower_close() + break + + time.sleep(1) + + #打开上料斗出砼门 + self.relay_controller.control_upper_open_sync(5) + while True: + + 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 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/feeding/process.py b/feeding/process.py index e33dd97..5eeff0d 100644 --- a/feeding/process.py +++ b/feeding/process.py @@ -1,167 +1,359 @@ -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 pickle import FALSE +from cv2.gapi import ov from config.settings import app_set_config +from hardware.relay import RelayController +from hardware.transmitter import TransmitterController +import time +import threading +from datetime import datetime +import logging +import queue +from hardware.upper_plc import OmronFinsPollingService +from vision.muju_cls.muju_utils import run_stable_classification_loop +from vision.camera_picture import save_camera_picture class FeedingProcess: + # 类变量,用于存储实例引用,实现单例检测 + _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, relay_controller, inverter_controller, - transmitter_controller, vision_detector, - camera_controller, state): + transmitter_controller, state): + """初始化视觉回调处理器""" + # 避免重复初始化 + if hasattr(self, '_initialized') and self._initialized: + return + self.relay_controller = relay_controller - self.artifact_bll = ArtifactBll() - self.mould_service = MouldService() self.inverter_controller = inverter_controller self.transmitter_controller = transmitter_controller - self.vision_detector = vision_detector - self.camera_controller = camera_controller - self.state = state - self.state._feed_status = FeedStatus.FNone - - #标志位用,是否是第一次运行 - self.is_first_flag=True - - def start_feeding(self): - loc_state=self.state - loc_state._upper_weight=self.transmitter_controller.read_data(1) - loc_state._lower_weight=self.transmitter_controller.read_data(2) - # loc_state._upper_volume=round(loc_state._upper_weight/self.state.density,1) - """开始生产管片""" - if loc_state._feed_status == FeedStatus.FNone: - return - elif loc_state._feed_status == FeedStatus.FCheckM: - print("---------------初始化数据------------------") - loc_state._mould_need_weight= 1200 - loc_state._feed_status = FeedStatus.FFeed - return - elif loc_state._feed_status == FeedStatus.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 - self.feeding_stage(loc_state) - # if loc_state._mould_need_weight>loc_state.initial_lower_weight: - # self.transfer_material_from_upper_to_lower(loc_state,loc_state.initial_upper_weight,loc_state.initial_lower_weight) - # loc_state._feed_status = FeedStatus.FFeed1 - # else: - # loc_state._feed_status = FeedStatus.FFeed1 - return - elif loc_state._feed_status == FeedStatus.FFinished: - """完成当前批次下料""" - print("振捣完成") - print("关闭所有网络继电器") - self.relay_controller.close_all() - return - - def _start_feeding_stage(self): - """启动指定下料阶段""" - """开始分步下料""" - 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 + # 线程安全的参数传递 + self._new_data_available = threading.Event() + self._is_processing = threading.Lock() + + #diff参数 + self._is_processing_diff = threading.Lock() + self._new_data_diff = threading.Event() + self._current_diff=0 + self._current_diff_area=[] + self._is_diff_save=False + + + self._stop_event = threading.Event() - target_upper_weight=initial_upper_weight-feed_weight - target_upper_weight = max(target_upper_weight, 0) - # 确保下料斗出砼门关闭,同步关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 - # 等待物料流入下料斗,基于上料斗重量变化控制 - - start_time = time.time() - # timeout = 30 # 30秒超时 + # 添加下料斗门控制锁,防止两个线程同时控制 + self._door_control_lock = threading.Lock() + # 记录当前控制门的线程名称,用于调试 + self._current_controlling_thread = None + #是否启动后的第一个模具 + self._is_first_module=True + self.init_val() + # self._setup_logging_2() + #F块完成重量的70%,控制夹脚,F块多于这个比例就没有记录了(注意) + self._max_f_angle_ratio=0.7 + #完成多少,调整角度比例 ,多于0.8就没记录了(注意) + self._max_angle_radio=0.8 - 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") + #重量大于95%,停留时间2秒,其他的1秒 + self._weight_ratio_955=0.955 + #完成多少,忽略未浇筑满 + self._max_ignore_radio=0.5 - # 如果达到目标重量,则关闭上料斗出砼门 - if current_upper_weight <= target_upper_weight + 50: # 允许50kg的误差范围 - print(f"达到目标重量,当前重量: {current_upper_weight:.2f}kg") - print(f"花费时间 {time.time() - start_time:.2f}秒") - break - # time.sleep(1) - # self.relay_controller.control(self.relay_controller.DOOR_UPPER_OPEN, 'open') - # time.sleep(0.2) + self._mould_accept_aligned=None + self._mould_before_aligned=False + #模具开始浇筑时间 + self._time_mould_begin='' + #模具结束浇筑时间 + self._time_mould_end='' - loc_state._upper_door_closed=True - # 关闭上料斗出砼门d - self.relay_controller.control_upper_close() - #测试用 - print("上料斗下料完成") - - def feeding_stage(self,loc_state): + def run_feed_f(self): """第一阶段下料:下料斗向模具车下料(低速)""" - print("开始下料") - # self.relay_controller.control + print("--------------------开始下料(F块)--------------------") + self._time_mould_begin=datetime.now().strftime("%Y-%m-%d %H:%M:%S") + # loc_relay=self.relay_controller + loc_mitter=self.transmitter_controller + max_weight_none=5 + cur_weight_none=0 + initial_lower_weight=loc_mitter.read_data(2) + if initial_lower_weight is None: + print("-----f上料斗重量异常-----") + return first_finish_weight=0 - 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._finish_weight=first_finish_weight + self._inital_finish_lweight=initial_lower_weight + need_total_weight=0.54*2416 + if initial_lower_weight>100: + if not self._is_finish: + self.is_start_visual=True + initial_lower_weight=loc_mitter.read_data(2) + if initial_lower_weight is None: + print("-----f上料斗重量异常2-----") + return + self._is_feed_stage=5 + while not self._is_finish: + current_weight = loc_mitter.read_data(2) + if current_weight is None: + cur_weight_none+=1 + if cur_weight_none>max_weight_none: + #如果重量连续5次为None,认为下料斗未就位,跳出循环 + print('------------f下到模具车,下料斗重量异常----------------') + print('------------f下到模具车,下料斗重量异常----------------') + self.close_lower_door_visual() + return + #视觉处理关闭,异常的话重量没有生效 + continue + cur_weight_none=0 + first_finish_weight=initial_lower_weight-current_weight + self._is_finish_ratio=(first_finish_weight)/need_total_weight + print(f'------------已下料比例: {self._is_finish_ratio}-------------') + if self._is_finish_ratio>self._max_f_angle_ratio: + #关5秒 + #大于0.7后不再检测了,直接交给视觉控制夹脚 + # print(f'------------已下料比例: {self._is_finish_ratio}-------------') + break + + # print(f'------------已下料: {first_finish_weight+second_finish_weight}kg-------------') + time.sleep(1) + # initial_lower_weight=_current_lower_weight + print(f'------------已下料(F): {first_finish_weight}kg-------------') + print(f'------------已下料(F): {first_finish_weight}kg-------------') + + print(f'------------已完成-------------') + def run_feed(self): + """第一阶段下料:下料斗向模具车下料(低速)""" + print("--------------------开始下料(普通块)--------------------") + self._time_mould_begin=datetime.now().strftime("%Y-%m-%d %H:%M:%S") + loc_relay=self.relay_controller + loc_mitter=self.transmitter_controller + max_weight_none=5 + cur_weight_none=0 + + initial_lower_weight=loc_mitter.read_data(2) + # initial_upper_weight=loc_mitter.read_data(1) + if initial_lower_weight is None: + print("---------------下料斗重量异常----------------") + return + first_finish_weight=0 + need_total_weight=1.91*2416 + # start_time=None + self.is_start_visual=True + if initial_lower_weight>100: + #下料斗的料全部下完 + self._is_feed_stage=1 + while not self._is_finish: + current_weight = loc_mitter.read_data(2) + if current_weight is None: + cur_weight_none+=1 + if cur_weight_none>max_weight_none: + print("-----------下料斗重量异常(第一次下到模具车)--------------") + self.close_lower_door_visual() + return + continue + cur_weight_none=0 + if current_weight<250 and current_weight>0: + self.close_lower_door_visual() + break + time.sleep(1) + _current_lower_weight=loc_mitter.read_data(2) + if _current_lower_weight is None: + print("-------下料斗重量异常---------") + return + first_finish_weight=initial_lower_weight-_current_lower_weight + # initial_lower_weight=_current_lower_weight + print(f'------------已下料(第一次): {first_finish_weight}kg-------------') + print(f'------------已下料(第一次): {first_finish_weight}kg-------------') + self._is_feed_stage=0 + + while self.plc_data!=5: + print('------------上料斗未就位----------------') + print('------------上料斗未就位----------------') time.sleep(1) - #打开上料斗出砼门 - self.relay_controller.control_upper_open_sync(5) - while True: - - if loc_state._upper_weight<3000: - #关5秒 - self.relay_controller.control_upper_close() + if self.plc_data==5: + print(f'------------上料斗向下料斗转移(留3000KG)-------------') + #打开上料斗出砼门,开5就,开三分之一下 + + loc_relay.control_upper_open_sync(6) + self._is_feed_stage=2 + loc_time_count=1 + upper_open_time=time.time() + + while not self._is_finish: + current_upper_weight = loc_mitter.read_data(1) + if current_upper_weight is None: + cur_weight_none+=1 + if cur_weight_none>max_weight_none: + #如果重量连续5次为None,认为上料斗未就位,跳出循环 + print('------------第一次上到下,上料斗重量异常----------------') + print('------------第一次上到下,上料斗重量异常----------------') + loc_relay.control_upper_close_sync(5+loc_time_count) + return + continue + cur_weight_none=0 + _two_lower_weight=loc_mitter.read_data(2) + if _two_lower_weight is None: + _two_lower_weight=0 + if (current_upper_weight<3000 and current_upper_weight>0) or _two_lower_weight>3200: + #关5秒,loc_time_count多关一秒 + loc_relay.control_upper_close_sync(5+loc_time_count) + break + else: + if time.time()-upper_open_time>5: + if loc_time_count<6: + upper_open_time=time.time() + loc_relay.control_upper_open_sync(0.8) + loc_time_count=loc_time_count+0.8 + else: + time.sleep(0.5) + else: + loc_relay.control_upper_close_sync(6+loc_time_count) + + self.is_start_visual=True + initial_lower_weight=loc_mitter.read_data(2) + if initial_lower_weight is None: + print("-------下料斗重量异常(第二次下料到模具车)---------") + return + self._is_feed_stage=3 + while not self._is_finish: + current_weight = loc_mitter.read_data(2) + if current_weight is None: + cur_weight_none+=1 + if cur_weight_none>max_weight_none: + print("-------下料斗重量异常(第二次下料到模具车)---------") + self.close_lower_door_visual() + return + continue + cur_weight_none=0 + # second_finish_weight=initial_lower_weight-current_weight + if current_weight<250: + self.close_lower_door_visual() break - 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 - + # print(f'------------已下料: {first_finish_weight+second_finish_weight}kg-------------') time.sleep(1) - - - + _current_lower_weight=loc_mitter.read_data(2) + if _current_lower_weight is None: + print("-------下料斗重量异常(第二次下到模)---------") + return + first_finish_weight=first_finish_weight+initial_lower_weight-_current_lower_weight + print(f'------------已下料(第二次): {first_finish_weight}kg-------------') + print(f'------------已下料(第二次): {first_finish_weight}kg-------------') + + self._is_feed_stage=0 + if self.plc_data==5: + #第二次上料斗向下料斗转移 + loc_relay.control_upper_open_sync(12) + loc_time_count=1 + upper_open_time=time.time() + upper_open_time_2=None + #第二次到下料斗还需要的量 + #loc_left_need_weight=need_total_weight-first_finish_weight + # initial_upper_weight=loc_mitter.read_data(1) + # start_time=None + self._is_feed_stage=4 + while not self._is_finish: + # print(f'------------上料斗向下料斗转移22222-------------') + current_upper_weight = loc_mitter.read_data(1) + if current_upper_weight is None: + cur_weight_none+=1 + if cur_weight_none>max_weight_none: + #如果重量连续5次为None,认为上料斗未就位,跳出循环 + print('------------第二次上到下,上料斗重量异常----------------') + print('------------第二次上到下,上料斗重量异常----------------') + loc_relay.control_upper_close_sync(15) + break + continue + cur_weight_none=0 + if (current_upper_weight<600 and current_upper_weight>0) or upper_open_time_2 is not None: + if upper_open_time_2 is None: + upper_open_time_2=time.time() + if current_upper_weight<400 or time.time()-upper_open_time_2>5: + loc_relay.control_arch_upper_open_async(5) + # loc_relay.control_arch_upper_open() + loc_relay.control_upper_open_sync(5) + # start_time=None + #5秒后关闭 + loc_relay.control_upper_close_after()#control_upper_close_sync(8+loc_time_count) + break + time.sleep(1) + else: + if time.time()-upper_open_time>2: + # if loc_time_count<6: + upper_open_time=time.time() + loc_relay.control_upper_open_sync(1.2) + loc_time_count=loc_time_count+1 + else: + time.sleep(0.5) + else: + loc_relay.control_upper_close_sync(15) + # time.sleep(0.4) + + #第三次下料斗转移到模具车 + if not self._is_finish: + self.is_start_visual=True + initial_lower_weight=loc_mitter.read_data(2) + self._finish_weight=first_finish_weight + self._inital_finish_lweight=initial_lower_weight + if initial_lower_weight is None: + print("-------下料斗重量异常(第三次下到模具车)---------") + return + self._is_feed_stage=5 + while not self._is_finish: + current_weight = loc_mitter.read_data(2) + if current_weight is None: + cur_weight_none+=1 + if cur_weight_none>max_weight_none: + #重量异常退出 + print('------------第三次下到模具车,下料斗重量异常----------------') + self.close_lower_door_visual() + return + continue + cur_weight_none=0 + second_finish_weight=initial_lower_weight-current_weight + self._is_finish_ratio=(second_finish_weight+first_finish_weight)/need_total_weight + print(f'------------已下料比例: {self._is_finish_ratio}-------------') + if self._is_finish_ratio>=1: + #关5秒 + # print(f'------------已下料比例: {self._is_finish_ratio}-------------') + break + + # print(f'------------已下料: {first_finish_weight+second_finish_weight}kg-------------') + time.sleep(1) + # _current_lower_weight=loc_mitter.read_data(2) + # first_finish_weight=first_finish_weight+initial_lower_weight-_current_lower_weight + # print(f'------------已下料: {first_finish_weight}kg-------------') + # print(f'------------已下料: {first_finish_weight}kg-------------') + + print(f'------------已完成-------------') + + + + @classmethod + def instance_exists(cls): + """检测实例是否存在""" + return cls._instance is not None - 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' + + +# 创建默认实例 +# visual_callback_instance = VisualCallback() + +# 兼容层,保持原来的函数调用方式可用 +# def angle_visual_callback(current_angle, overflow_detected): +# """ +# 兼容旧版本的函数调用方式 +# 将调用转发到默认实例的angle_visual_callback方法 +# """ +# visual_callback_instance.angle_visual_callback(current_angle, overflow_detected) \ No newline at end of file diff --git a/hardware/inverter.py b/hardware/inverter.py index 76db96d..5ee50e7 100644 --- a/hardware/inverter.py +++ b/hardware/inverter.py @@ -1,88 +1,111 @@ # hardware/inverter.py -from pymodbus.exceptions import ModbusException +from math import e +import minimalmodbus +import time +from serial import SerialException class InverterController: - def __init__(self, relay_controller): - self.relay_controller = relay_controller - self.max_frequency = 400.0 # 频率最大值 + def __init__(self,port='/dev/ttyUSB0'): + self.inverter=None + self.port=port - # 变频器配置 - self.config = { - 'slave_id': 1, - 'frequency_register': 0x01, # 2001H - 'start_register': 0x00, # 2000H - 'stop_register': 0x00, # 2000H(用于停机) - 'start_command': 0x0013, # 正转点动运行 - 'stop_command': 0x0001 # 停机 - } + def _connect(self)->bool: + """连接变频器""" + try: + self.inverter = minimalmodbus.Instrument(self.port, 1) + # 2. 配置串口参数 + self.inverter.serial.baudrate = 9600 # 波特率 + self.inverter.serial.bytesize = 8 # 数据位 + self.inverter.serial.parity = 'N' # 无校验 + self.inverter.serial.stopbits = 1 # 停止位 + self.inverter.serial.timeout = 1.0 # 超时时间 + self.inverter.mode = minimalmodbus.MODE_RTU # RTU模式 + return True + except SerialException as e: + print(f"串口占用或无法连接变频器串口:{e}") + return False + except Exception as e: + print(f"连接变频器异常:{e}") + return False def set_frequency(self, frequency): """设置变频器频率""" + _ret=False try: - if not self.relay_controller.modbus_client.connect(): - print("无法连接网络继电器Modbus服务") - return False - - # 使用最大频率变量计算百分比 - percentage = frequency / self.max_frequency # 得到 0~1 的比例 - value = int(percentage * 10000) # 转换为 -10000 ~ 10000 的整数 - - # 限制范围 - value = max(-10000, min(10000, value)) - - result = self.relay_controller.modbus_client.write_register( - self.config['frequency_register'], - value, - slave=self.config['slave_id'] - ) - - if isinstance(result, Exception): - print(f"设置频率失败: {result}") - return False - - print(f"设置变频器频率为 {frequency}Hz") - return True - except ModbusException as e: - print(f"变频器Modbus通信错误: {e}") - return False + if(self._connect()): + frequency_value = int(frequency * 100) + self.inverter.write_register(0x7310, frequency_value) + _ret=True + else: + print(f'设置频率{frequency}失败') + except Exception as e: + print(f"设置频率{frequency}异常:{e}") finally: - self.relay_controller.modbus_client.close() + if self.inverter: + self.inverter.serial.close() + return _ret - def control(self, action): + def read_frequency(self): + """读取变频器频率""" + _ret=None + try: + if(self._connect()): + frequency_value = self.inverter.read_register(0x7310) + _ret=frequency_value / 100 + else: + print(f"读取频率{frequency}失败") + except Exception as e: + print(f"读取频率{frequency}异常:{e}") + finally: + if self.inverter: + self.inverter.serial.close() + return _ret + + def read_status(self): + """读取变频器启动状态""" + _ret=None + try: + if(self._connect()): + status_value = self.inverter.read_register(0x3000) + _ret=status_value + else: + print(f"读取启动状态失败") + except Exception as e: + print(f"读取启动状态异常:{e}") + finally: + if self.inverter: + self.inverter.serial.close() + return _ret + + def control(self, action,frequency=230): """控制变频器启停""" - # 先检查动作是否有效 if action not in ['start', 'stop']: print(f"无效操作: {action}") return False - + _ret=False + # 先检查动作是否有 try: - if not self.relay_controller.modbus_client.connect(): - print("无法连接网络继电器Modbus服务") - return False - - if action == 'start': - result = self.relay_controller.modbus_client.write_register( - address=self.config['start_register'], - value=self.config['start_command'], - slave=self.config['slave_id'] - ) - print("启动变频器") - elif action == 'stop': - result = self.relay_controller.modbus_client.write_register( - address=self.config['start_register'], - value=self.config['stop_command'], - slave=self.config['slave_id'] - ) - print("停止变频器") - - if isinstance(result, Exception): - print(f"控制失败: {result}") - return False - - return True - except ModbusException as e: + if(self._connect()): + if action == 'start': + status_value = self.inverter.read_register(0x3000) + ##读取3000H可直接读取变频器的当前状态(0001:正转运行;0002:反转运行;0003:停机;0004:电机参数辨识;0005:故障)。 + if status_value==3: + frequency_value = int(frequency * 100) + self.inverter.write_register(0x7310, frequency_value) + time.sleep(1) + self.inverter.write_register(0x2000, 1) # 1=正转运行 + print("启动变频器") + elif action == 'stop': + self.inverter.write_register(0x2000, 5) # 6=减速停机,5自由停机 + print("停止变频器") + _ret=True + else: + print("连接变频器失败") + except Exception as e: print(f"变频器控制错误: {e}") - return False finally: - self.relay_controller.modbus_client.close() + if self.inverter: + self.inverter.serial.close() + return _ret + diff --git a/hardware/relay.py b/hardware/relay.py index 4ce7b71..1deda92 100644 --- a/hardware/relay.py +++ b/hardware/relay.py @@ -29,7 +29,7 @@ class RelayController: 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关 +#遥1 DO 7 左 DO8 右 角度 摇2:DO 15下 13上 12 启动振捣 14停止振捣 下料斗DO7开 D09关 # 继电器命令(原始Socket) self.relay_commands = { self.RING: {'open': '00000000000601050000FF00', 'close': '000000000006010500000000'}, @@ -299,6 +299,22 @@ class RelayController: print(f"上料斗振动关闭完成,延迟{delay_seconds}秒") self.control(self.BREAK_ARCH_UPPER, 'close') + def control_arch_lower_open_async(self,delay_seconds: float = 15): + """异步控制上料斗振动 + + Args: + delay_seconds: 延迟关闭时间(秒),默认15秒 + """ + # 关闭下料斗出砼门 + self.control(self.BREAK_ARCH_LOWER, 'open') + # 异步5秒后关闭 + threading.Thread(target=lambda d: self._close_break_arch_lower(delay_seconds),args=(delay_seconds,), daemon=True, name="_close_break_arch_lower").start() + + def _close_break_arch_lower(self, delay_seconds: float = 15): + time.sleep(delay_seconds) + print(f"下料斗振动关闭完成,延迟{delay_seconds}秒") + self.control(self.BREAK_ARCH_LOWER, 'close') + def close_all(self): diff --git a/hardware/transmitter copy.py b/hardware/transmitter copy.py deleted file mode 100644 index 0070070..0000000 --- a/hardware/transmitter copy.py +++ /dev/null @@ -1,184 +0,0 @@ -# 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 d50cb94..6f759bb 100644 --- a/hardware/transmitter.py +++ b/hardware/transmitter.py @@ -16,6 +16,10 @@ class TransmitterController: self.is_start_lower=False self.start_time_upper=None self.start_time_lower=None + self.upper_ip=ini_manager.upper_transmitter_ip + self.upper_port=ini_manager.upper_transmitter_port + self.lower_ip=ini_manager.lower_transmitter_ip + self.lower_port=ini_manager.lower_transmitter_port # 变送器配置 self.config = { 1: { # 上料斗 @@ -93,12 +97,12 @@ class TransmitterController: weight = None if transmitter_id == 1: # 上料斗变送器的信息: - IP = ini_manager.upper_transmitter_ip - PORT = ini_manager.upper_transmitter_port + IP = self.upper_ip + PORT = self.upper_port elif transmitter_id == 2: # 下料斗变送器的信息: - IP = ini_manager.lower_transmitter_ip - PORT = ini_manager.lower_transmitter_port + IP = self.lower_ip + PORT = self.lower_port if not IP or not PORT: print(f"未配置变送器 {transmitter_id} 的IP或PORT") diff --git a/hardware/transmitter_bak.py b/hardware/transmitter_bak.py new file mode 100644 index 0000000..7b63f1c --- /dev/null +++ b/hardware/transmitter_bak.py @@ -0,0 +1,200 @@ +# hardware/transmitter.py +import socket +import threading +from config.ini_manager import ini_manager +from config.settings import app_set_config +import time + +class TransmitterController: + def __init__(self): + self.upper_ip = ini_manager.upper_transmitter_ip + self.upper_port = ini_manager.upper_transmitter_port + self.lower_ip = ini_manager.lower_transmitter_ip + self.lower_port = ini_manager.lower_transmitter_port + + # 存储最新重量值 + self.latest_weights = {1: None, 2: None} + # 存储连接状态 + self.connection_status = {1: False, 2: False} + # 线程控制 + self.running = True + self.threads = {} + # 连接配置 + self.TIMEOUT = 5 # 连接超时时间 + self.BUFFER_SIZE = 1024 + + # 启动后台接收线程 + self._start_receiver_threads() + + def _start_receiver_threads(self): + """启动后台接收线程""" + for transmitter_id in [1, 2]: + if (transmitter_id == 1 and self.upper_ip and self.upper_port) or \ + (transmitter_id == 2 and self.lower_ip and self.lower_port): + thread = threading.Thread( + target=self._continuous_receiver, + args=(transmitter_id,), + daemon=True, + name=f'transmitter_receiver_{transmitter_id}' + ) + thread.start() + self.threads[transmitter_id] = thread + print(f"启动变送器 {transmitter_id} 后台接收线程") + + def _continuous_receiver(self, transmitter_id): + """后台持续接收数据的线程函数""" + while self.running: + IP = None + PORT = None + + if transmitter_id == 1: + IP = self.upper_ip + PORT = self.upper_port + elif transmitter_id == 2: + IP = self.lower_ip + PORT = self.lower_port + + if not IP or not PORT: + time.sleep(5) + continue + + sock = None + try: + # 创建连接 + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(self.TIMEOUT) + sock.connect((IP, PORT)) + self.connection_status[transmitter_id] = True + print(f"变送器 {transmitter_id} 连接成功: {IP}:{PORT}") + + # 持续接收数据 + while self.running: + try: + data = sock.recv(self.BUFFER_SIZE) + if data: + # 提取有效数据包 + packet = self.get_latest_valid_packet(data) + if packet: + # 解析重量 + weight = self.parse_weight(packet) + if weight is not None: + self.latest_weights[transmitter_id] = weight + # 可选:打印接收到的重量 + # print(f"变送器 {transmitter_id} 重量: {weight}") + else: + # 连接关闭 + print(f"变送器 {transmitter_id} 连接关闭") + break + except socket.timeout: + # 超时是正常的,继续接收 + continue + except Exception as e: + print(f"接收数据异常: {e}") + break + + except ConnectionRefusedError: + print(f"变送器 {transmitter_id} 连接失败:{IP}:{PORT} 拒绝连接") + except Exception as e: + print(f"变送器 {transmitter_id} 异常:{e}") + finally: + self.connection_status[transmitter_id] = False + if sock: + try: + sock.close() + except: + pass + # 重试间隔 + time.sleep(3) + + # 直接读取 变送器返回的数据(从缓存中获取) + def read_data_sub(self, transmitter_id): + + """ + Args: transmitter_id 为1 表示上料斗, 为2 表示下料斗 + return: 读取成功返回重量 weight: int, 失败返回 None + """ + # 直接返回缓存的最新重量值 + return self.latest_weights.get(transmitter_id) + + def get_connection_status(self, transmitter_id): + """ + 获取变送器连接状态 + Args: transmitter_id 为1 表示上料斗, 为2 表示下料斗 + return: 连接状态 bool + """ + return self.connection_status.get(transmitter_id, False) + + def stop(self): + """停止后台线程""" + self.running = False + # 等待线程结束 + for thread in self.threads.values(): + if thread.is_alive(): + thread.join(timeout=2) + print("变送器后台接收线程已停止") + + 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 + + def read_data(self,transmitter_id): + """获取重量函数:根据变送器ID获取当前重量,三次""" + max_try_times=5 + try_times=0 + while try_times {node_id}") + except Exception as e: + print(f" {i+1}. (获取名称失败: {e})") + except Exception as e: + print(f"浏览子节点失败: {e}") + + # 尝试不同的路径格式 + print("\n[3] 尝试不同的路径格式访问节点...") + + path_formats = [ + # 格式1: 使用命名空间前缀 + "2:upper/2:upper_weight", + "2:lower/2:lower_weight", + + # 格式2: 原始字符串(避免转义问题) + r"2:upper/2:upper_weight", + r"2:lower/2:lower_weight", + + # 格式3: 使用列表格式 + ["2:upper", "2:upper_weight"], + ["2:lower", "2:lower_weight"], + + # 格式4: 不带命名空间前缀 + "upper/upper_weight", + "lower/lower_weight", + ["upper", "upper_weight"], + ["lower", "lower_weight"], + ] + + for path in path_formats: + try: + if isinstance(path, list): + node = objects.get_child(path) + else: + node = objects.get_child(path) + + browse_name = node.get_browse_name() + value = node.get_value() + print(f" ✓ 成功: {path}") + print(f" 节点: {node}") + print(f" 名称: {browse_name}") + print(f" 值: {value}") + print() + + # 找到一个有效的就继续尝试其他格式 + break + + except Exception as e: + print(f" ✗ 失败: {path}") + print(f" 错误: {e}") + print() + + # 尝试方法1:先获取设备对象,再获取变量 + print("\n[4] 方法1: 先获取设备对象,再获取变量...") + try: + upper_device = objects.get_child("2:upper") + print(f" 上料斗设备: {upper_device}") + + upper_weight = upper_device.get_child("2:upper_weight") + value = upper_weight.get_value() + print(f" ✓ 成功获取上料斗重量: {value}") + except Exception as e: + print(f" ✗ 失败: {e}") + + # 尝试方法2:直接使用完整路径 + print("\n[5] 方法2: 直接使用完整路径...") + try: + # 注意:这里使用原始字符串避免转义问题 + upper_weight = objects.get_child(r"2:upper/2:upper_weight") + value = upper_weight.get_value() + print(f" ✓ 成功获取上料斗重量: {value}") + except Exception as e: + print(f" ✗ 失败: {e}") + + # 尝试方法3:使用get_children遍历 + print("\n[6] 方法3: 使用get_children遍历查找...") + try: + for child in objects.get_children(): + try: + browse_name = str(child.get_browse_name()) + if "upper" in browse_name.lower(): + print(f" 发现上料斗相关节点: {browse_name} -> {child.nodeid}") + + # 尝试获取该节点的子节点 + for sub_child in child.get_children(): + try: + sub_browse_name = str(sub_child.get_browse_name()) + if "weight" in sub_browse_name.lower(): + value = sub_child.get_value() + print(f" └─ {sub_browse_name}: {value}") + except Exception as e: + print(f" └─ 获取{sub_browse_name}失败: {e}") + except Exception as e: + print(f" 处理节点失败: {e}") + except Exception as e: + print(f" ✗ 遍历失败: {e}") + + # 尝试写入操作 + print("\n[7] 尝试写入操作...") + try: + # 找到有效的节点路径 + upper_device = objects.get_child("2:upper") + upper_weight = upper_device.get_child("2:upper_weight") + + # 写入测试值 + test_value = 123.45 + upper_weight.set_value(test_value) + print(f" ✓ 成功写入: {test_value}") + + # 读取验证 + read_value = upper_weight.get_value() + print(f" ✓ 读取验证: {read_value}") + + except Exception as e: + print(f" ✗ 写入失败: {e}") + import traceback + traceback.print_exc() + + except Exception as e: + print(f"\n诊断过程出错: {e}") + import traceback + traceback.print_exc() + + finally: + try: + client.disconnect() + print("\n[8] 已断开连接") + except: + pass + + print("\n" + "=" * 60) + print("诊断完成") + print("=" * 60) + + +if __name__ == "__main__": + diagnose_node_access() diff --git a/opc/opcua_client_feed.py b/opc/opcua_client_feed.py new file mode 100644 index 0000000..ef3e2ec --- /dev/null +++ b/opc/opcua_client_feed.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 +from opcua import Client, ua +import time +from datetime import datetime +import configparser +from threading import Thread + +# class OpcuaUiSignal: + # value_changed = Signal(str, str, object) + # opc_disconnected = Signal(str) # OPC服务断开信号,参数:断开原因 + # opc_reconnected = Signal() # OPC重连成功信号 + # opc_log = Signal(str) # OPC运行日志信号,参数:日志信息 + +# Opcua回调处理器 +class SubscriptionHandler: + def __init__(self): + self.node_id_to_name = {} + # self.opc_signal = opc_signal + + def datachange_notification(self, node, val, data): + try: + node_id = node.nodeid.to_string() + var_name = self.node_id_to_name.get(node_id) + # self.opc_signal.value_changed.emit(node_id, var_name, val) + except Exception as e: + err_msg = f"opcua解析值变化事件失败: {e}" + # self.opc_signal.opc_log.emit(err_msg) + +class OpcuaClientFeed(Thread): + def __init__(self, parent=None): + super().__init__(parent) + self.server_url = "" + self.client = None + + self.connected = False + self.subscription = None + self.monitored_items = [] + self.is_running = True # 线程运行标志位 + self.node_id_mapping = {} # node_id 和 可读变量名的映射表 + self.is_reconnect_tip_sent = False # 重连失败提示是否已发送 + + # self.opc_signal = OpcuaUiSignal() + # self.handler = SubscriptionHandler(self.opc_signal) + self.handler = SubscriptionHandler() + + self.target_var_paths = [] + + # 参数 + self.heartbeat_interval = None # 心跳检测间隔 + self.reconnect_interval = None # 首次/掉线重连间隔 + self.sub_interval = None # 订阅间隔 (单位:ms) + + + def stop_run(self): + """停止线程+断开连接""" + self.is_running = False + self.disconnect() + self.wait() + print("opcua客户端线程已退出") + + def connect(self): + """连接到OPC服务器""" + try: + self.client.connect() + self.connected = True + msg = f"成功连接到OPCUA服务器: {self.server_url}" + print(msg) + # self.opc_signal.opc_log.emit(msg) + self.is_reconnect_tip_sent = False + return True + except Exception as e: + self.connected = False + err_msg = f"连接OPCUA服务器失败: {e}" + print(err_msg) + if not self.is_reconnect_tip_sent: + # self.opc_signal.opc_log.emit(err_msg) + # 标记为已发送,后续不重复在UI上显示 + self.is_reconnect_tip_sent = True + return False + + def disconnect(self): + """断开连接""" + self.connected = False + try: + if self.monitored_items: + for item in self.monitored_items: + try: + self.subscription.unsubscribe(item) + except Exception: + pass + self.monitored_items.clear() + if self.subscription: + try: + self.subscription.delete() + except Exception: + pass + self.subscription = None + if self.client: + try: + self.client.disconnect() + except Exception: + pass + self.node_id_mapping.clear() + if hasattr(self, 'handler') and self.handler: + self.handler.node_id_to_name = {} + except Exception as e: + print(f"opcua断开连接异常: {e}") + + def build_node_id_mapping(self): + """根据object_name+var_name路径获取nodeid,建立映射表""" + if not self.connected: + return False + try: + # self.opc_signal.opc_log.emit("开始构建nodeid映射表...") + objects_node = self.client.get_objects_node() + self.handler.node_id_to_name = self.node_id_mapping + for var_name, path_list in self.target_var_paths: + target_node = objects_node.get_child(path_list) + node_id = target_node.nodeid.to_string() + self.node_id_mapping[node_id] = var_name + # self.opc_signal.opc_log.emit("nodeid映射表构建成功") + return True + except Exception as e: + err_msg = f"构建{var_name}映射表失败: {e}" + print(err_msg) + # self.opc_signal.opc_log.emit(err_msg) + return False + + def create_multi_subscription(self, interval=None): + """订阅多个变量(基于映射表的nodeid)""" + if not self.connected: + return + if not self.node_id_mapping and not self.build_node_id_mapping(): + return + try: + interval = int(interval) if interval else self.sub_interval + self.subscription = self.client.create_subscription(interval, self.handler) + # self.opc_signal.opc_log.emit(f"opcua订阅创建成功(间隔:{interval}ms)") + for node_id, var_name in self.node_id_mapping.items(): + var_node = self.client.get_node(node_id) + monitored_item = self.subscription.subscribe_data_change(var_node) + self.monitored_items.append(monitored_item) + # self.opc_signal.opc_log.emit(f"已订阅变量: {var_name} (nodeid: {node_id})") + print(f"已订阅变量: {var_name} (nodeid: {node_id})") + except Exception as e: + err_msg = f"创建批量订阅失败: {e}" + print(err_msg) + # self.opc_signal.opc_log.emit(err_msg) + + def read_opc_config(self, cfg_path = "config/opc_config.ini"): + """读取OPC配置文件, 初始化所有参数和节点列表""" + try: + cfg = configparser.ConfigParser() + cfg.read(cfg_path, encoding="utf-8") + # 1. 读取服务器基础配置 + self.server_url = cfg.get("OPC_SERVER_CONFIG", "server_url") + self.heartbeat_interval = cfg.getint("OPC_SERVER_CONFIG", "heartbeat_interval") + self.reconnect_interval = cfg.getint("OPC_SERVER_CONFIG", "reconnect_interval") + self.sub_interval = cfg.getint("OPC_SERVER_CONFIG", "sub_interval") + + # 2. 读取OPC节点配置 + node_section = cfg["OPC_NODE_LIST"] + for readable_name, node_path_str in node_section.items(): + node_path_list = node_path_str.split(",") + self.target_var_paths.append( (readable_name, node_path_list) ) + # print("target_var_paths", self.target_var_paths) + except Exception as e: + print(f"读取配置文件失败: {e},使用默认配置启动!") + self.server_url = "opc.tcp://localhost:4840/zjsh_feed/server/" + self.heartbeat_interval = 4 + self.reconnect_interval = 2 + self.sub_interval = 500 + self.target_var_paths = [ + ("upper_weight", ["2:upper", "2:upper_weight"]), + ("lower_weight", ["2:lower", "2:lower_weight"]) + ] + # 参数合法性检验 + self.heartbeat_interval = self.heartbeat_interval if isinstance(self.heartbeat_interval, int) and self.heartbeat_interval >=1 else 4 + self.reconnect_interval = self.reconnect_interval if isinstance(self.reconnect_interval, int) and self.reconnect_interval >=1 else 2 + self.sub_interval = self.sub_interval if isinstance(self.sub_interval, int) and self.sub_interval >=100 else 500 + + def write_value_by_name(self, var_readable_name, value): + """ + 根据变量可读名称写入值(主要用于修改方量, 方量的类型为 Double类型) + :param var_readable_name: 变量可读名称(如"upper_weight") + :param value: 要写入的值 + """ + if not self.connected: + # self.opc_signal.opc_log.emit(f"{var_readable_name}写入失败: OPC服务未连接") + return + target_node_id = None + for node_id, name in self.node_id_mapping.items(): + if name == var_readable_name: + target_node_id = node_id + break + if not target_node_id: + # self.opc_signal.opc_log.emit(f"写入失败:未找到变量名 {var_readable_name} 对应的nodeid") + return + try: + target_node = self.client.get_node(target_node_id) + # variant = ua.Variant(float(value), ua.VariantType.Double) + target_node.set_value(value) + # self.opc_signal.opc_log.emit(f"写入成功:{var_readable_name} = {value}") + except Exception as e: + err_msg = f"opcua写入值失败: {e}" + print(err_msg) + # self.opc_signal.opc_log.emit(err_msg) + + # ===== 心跳检测函数 ===== + def _heartbeat_check(self): + """心跳检测: 判断opc服务是否存活""" + try: + self.client.get_node("i=2258").get_value() + return True + except Exception as e: + err_msg = f"心跳检测失败, OPCUA服务已断开 {e}" + print(err_msg) + # self.opc_signal.opc_log.emit(err_msg) + return False + + # ===== 掉线重连函数 ===== + def _auto_reconnect(self): + """掉线后自动重连+重建映射+恢复订阅""" + # self.opc_signal.opc_disconnected.emit("OPC服务掉线, 开始自动重连...") + try: + self.disconnect() + except Exception as e: + print(f"_auto_reconnect: 断开旧连接时出现异常: {e}") + while self.is_running: + # self.opc_signal.opc_log.emit(f"重试连接OPC服务器: {self.server_url}") + if self.connect(): + self.build_node_id_mapping() + self.create_multi_subscription() + # self.opc_signal.opc_reconnected.emit() + # self.opc_signal.opc_log.emit("OPCUA服务器重连成功, 所有订阅已恢复正常") + print("OPCUA服务器重连成功, 所有订阅已恢复正常") + break + time.sleep(self.reconnect_interval) + + def _init_connect_with_retry(self): + """连接opc服务器""" + # self.opc_signal.opc_log.emit("OPC客户端初始化, 开始连接服务器...") + print("OPC客户端初始化, 开始连接服务器...") + while self.is_running: + if self.connect(): + self.build_node_id_mapping() + self.create_multi_subscription() + break + # self.opc_signal.opc_log.emit(f"连接OPCUA服务器失败, {self.reconnect_interval}秒后重试...") + time.sleep(self.reconnect_interval) + + def run(self) -> None: + """opcua客户端线程主函数""" + self.read_opc_config() # 读取配置文件 + self.client = Client(self.server_url) # 初始化opc客户端 + + # 连接opc服务器 + self._init_connect_with_retry() + + while self.is_running: + if self.connected: + if not self._heartbeat_check(): + self.connected = False + self._auto_reconnect() + else: + self._auto_reconnect() + time.sleep(self.heartbeat_interval) + + +if __name__ == "__main__": + opcua_client = OpcuaClientFeed() + opcua_client.run() + opcua_client.write_value_by_name("upper_weight", 100.0) + diff --git a/opc/opcua_client_subscription.py b/opc/opcua_client_subscription.py index ff896ed..a37add4 100644 --- a/opc/opcua_client_subscription.py +++ b/opc/opcua_client_subscription.py @@ -17,24 +17,36 @@ class SubHandler: def __init__(self): self.data_changes = {} self.change_count = 0 + # 缓存节点名称,避免在回调中频繁查询 + self.node_names = {} def datachange_notification(self, node, val, data): """ 数据变化时的回调函数 + + 注意:此函数在订阅线程中调用,必须快速返回,避免耗时操作 """ self.change_count += 1 - node_name = node.get_display_name().Text + + # 从缓存获取节点名称,避免发起网络请求 + node_id = str(node) + if node_id in self.node_names: + node_name = self.node_names[node_id] + else: + # 如果缓存中没有,尝试从节点ID中提取名称(备用方案) + node_name = node_id + + # 存储数据变化 self.data_changes[node_name] = { 'value': val, 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'), - 'node_id': str(node) + 'node_id': node_id } print(f"🔔 数据变化 #{self.change_count}") print(f" 节点: {node_name}") print(f" 数值: {val}") print(f" 时间: {self.data_changes[node_name]['timestamp']}") - print(f" 节点ID: {node}") print("-" * 50) class OPCUAClientSubscription: @@ -95,6 +107,23 @@ class OPCUAClientSubscription: upper_weight_node = upper_device.get_child("2:upper_weight") lower_weight_node = lower_device.get_child("2:lower_weight") + # 【关键优化】在订阅前预获取并缓存节点名称,避免回调中发起网络请求 + try: + upper_name = upper_weight_node.get_display_name().Text + except Exception: + upper_name = "upper_weight" + + try: + lower_name = lower_weight_node.get_display_name().Text + except Exception: + lower_name = "lower_weight" + + # 缓存节点名称 + self.handler.node_names[str(upper_weight_node)] = upper_name + self.handler.node_names[str(lower_weight_node)] = lower_name + + print(f"📋 已缓存节点名称: {upper_name}, {lower_name}") + # 开始监控 upper_handle = self.subscription.subscribe_data_change(upper_weight_node) lower_handle = self.subscription.subscribe_data_change(lower_weight_node) @@ -109,6 +138,8 @@ class OPCUAClientSubscription: except Exception as e: print(f"❌ 设置订阅失败: {e}") + import traceback + traceback.print_exc() return False def get_current_values(self): @@ -149,7 +180,7 @@ class OPCUAClientSubscription: # 每5秒显示一次统计信息 if current_time - last_stats_time >= 5: elapsed = current_time - start_time - changes_per_minute = (self.handler.change_count / elapsed) * 60 + changes_per_minute = (self.handler.change_count / elapsed) * 60 if elapsed > 0 else 0 print(f"\n📈 统计信息 (运行时间: {elapsed:.1f}s)") print(f" 总变化次数: {self.handler.change_count}") @@ -186,6 +217,8 @@ def main(): except Exception as e: print(f"❌ 客户端运行错误: {e}") + import traceback + traceback.print_exc() finally: client.disconnect() @@ -198,6 +231,6 @@ if __name__ == "__main__": try: main() - except KeyboardInterrupt: - print("\n👋 用户中断程序") - sys.exit(0) \ No newline at end of file + except Exception as e: + print(f"❌ 客户端运行错误: {e}") + sys.exit(1) diff --git a/opc/opcua_client_test.py b/opc/opcua_client_test.py index 852ef84..b27307a 100644 --- a/opc/opcua_client_test.py +++ b/opc/opcua_client_test.py @@ -52,20 +52,235 @@ class OPCUAClientTest: objects = self.client.get_objects_node() print(f"对象节点: {objects}") - upper_device = objects.get_child("2:upper") - print(f"\n上料斗对象: {upper_device}") - - lower_device = objects.get_child("2:lower") - print(f"下料斗对象: {lower_device}") - - print("\n=== 当前对象属性===") - self.read_object_properties(upper_device, lower_device) + # 浏览Objects下的所有子节点 + print("\n=== Objects 节点下的子节点 ===") + for child in objects.get_children(): + browse_name = child.get_browse_name() + print(f" 节点: {browse_name} (nodeId: {child.nodeid})") + + # 如果是上料斗或下料斗对象,继续浏览它们的子节点 + if "upper" in str(browse_name).lower() or "lower" in str(browse_name).lower(): + try: + for sub_child in child.get_children(): + sub_browse_name = sub_child.get_browse_name() + print(f" └─ {sub_browse_name} (nodeId: {sub_child.nodeid})") + except: + pass + # 尝试获取设备对象 + print("\n=== 尝试获取设备对象 ===") + try: + upper_device = objects.get_child("2:upper") + print(f"上料斗对象: {upper_device}") + except Exception as e: + print(f"获取上料斗对象失败: {e}") + # 尝试其他可能的路径 + try: + upper_device = objects.get_child(["2:upper"]) + print(f"上料斗对象(列表方式): {upper_device}") + except Exception as e2: + print(f" 也无法通过列表方式获取: {e2}") + + try: + lower_device = objects.get_child("2:lower") + print(f"下料斗对象: {lower_device}") + except Exception as e: + print(f"获取下料斗对象失败: {e}") + except Exception as e: print(f"浏览节点时出错: {e}") + def get_node_path(self, obj_path: str) -> str: + """ + 获取节点路径 - 尝试多种格式 + + Args: + obj_path: 对象名称(如 "upper", "upper_weight") + + Returns: + str: 节点路径,如果找不到返回 None + """ + if not self.connected: + return None + + try: + objects = self.client.get_objects_node() + + # 尝试多种节点路径格式 + path_formats = [ + f"2:{obj_path}", + f"2:upper/2:{obj_path}", + f"2:lower/2:{obj_path}", + f"ns=2;{obj_path}", + obj_path + ] + + for path in path_formats: + try: + node = objects.get_child(path) + print(f" 找到节点: {path} -> {node}") + return path + except: + continue + + return None + + except Exception as e: + print(f"查找节点路径时出错: {e}") + return None + + def write_data(self, node_path: str, value, data_type: str = "auto") -> bool: + """ + 向OPC UA节点写入数据 + + Args: + node_path: 节点路径(如 "2:upper/2:upper_weight") + value: 要写入的值 + data_type: 数据类型("int", "float", "bool", "string", "auto") + + Returns: + bool: 写入成功返回True,失败返回False + """ + if not self.connected: + print("请先连接到服务器") + return False + + try: + # 获取对象节点 + objects = self.client.get_objects_node() + + # 尝试多种方式获取节点 + node = None + error_msg = None + + # 方式1: 直接使用路径 + try: + node = objects.get_child(node_path) + except Exception as e: + error_msg = e + + # 方式2: 分解路径 + if node is None and "/" in node_path: + try: + parts = node_path.split("/") + node = objects + for part in parts: + node = node.get_child(part) + except: + pass + + # 方式3: 尝试用数字索引 + if node is None: + try: + node = objects.get_child([node_path]) + except: + pass + + if node is None: + print(f"写入数据失败 {node_path}: 找不到节点") + print(f" 详细错误: {error_msg}") + print(f" 提示: 请先运行 browse_nodes() 方法查看可用的节点路径") + return False + + # 根据数据类型转换值 + if data_type == "int": + value = int(value) + elif data_type == "float": + value = float(value) + elif data_type == "bool": + value = bool(value) + elif data_type == "string": + value = str(value) + # "auto" 模式下自动推断类型 + + # 写入数据 + node.set_value(value) + + # 获取节点名称用于显示 + try: + node_name = node.get_browse_name() + except: + node_name = node_path + + print(f"✓ 成功写入 {node_name}: {value}") + return True + + except Exception as e: + print(f"✗ 写入数据失败 {node_path}: {e}") + return False + + def write_weights_directly(self, upper_value, lower_value) -> bool: + """ + 直接写入上下料斗重量(自动检测节点路径) + + Args: + upper_value: 上料斗重量值 + lower_value: 下料斗重量值 + + Returns: + bool: 写入成功返回True,失败返回False + """ + if not self.connected: + print("请先连接到服务器") + return False + + success = True + + try: + objects = self.client.get_objects_node() + + # 查找上料斗重量节点 + upper_weight_node = None + lower_weight_node = None + + # 遍历Objects下的所有节点 + for child in objects.get_children(): + browse_name = str(child.get_browse_name()) + print(browse_name) + if "upper" in browse_name.lower(): + upper_weight_node = child + print(f"找到上料斗重量节点: {browse_name}") + break + + for child in objects.get_children(): + browse_name = str(child.get_browse_name()) + if "lower" in browse_name.lower(): + lower_weight_node = child + print(f"找到下料斗重量节点: {browse_name}") + break + + # 写入上料斗重量 + if upper_weight_node: + try: + upper_weight_node.set_value(upper_value) + print(f"✓ 成功写入上料斗重量: {upper_value}") + except Exception as e: + print(f"✗ 写入上料斗重量失败: {e}") + success = False + else: + print("✗ 未找到上料斗重量节点") + success = False + + # 写入下料斗重量 + if lower_weight_node: + try: + lower_weight_node.set_value(lower_value) + print(f"✓ 成功写入下料斗重量: {lower_value}") + except Exception as e: + print(f"✗ 写入下料斗重量失败: {e}") + success = False + else: + print("✗ 未找到下料斗重量节点") + success = False + + return success + + except Exception as e: + print(f"写入重量数据时出错: {e}") + return False + def read_object_properties(self, upper_device, lower_device): - """读取重量数值""" + """读取重量数值(需要外部传入device对象)""" try: # 读取重量 upper_weight = upper_device.get_child("2:upper_weight").get_value() @@ -76,7 +291,79 @@ class OPCUAClientTest: except Exception as e: print(f"读取数据时出错: {e}") + + def read_weights(self) -> tuple: + """ + 直接读取上料斗和下料斗重量(无需先获取device对象) + + Returns: + tuple: (上料斗重量, 下料斗重量),读取失败返回 (None, None) + """ + if not self.connected: + print("请先连接到服务器") + return None, None + try: + # 直接获取节点并读取数据 + objects = self.client.get_objects_node() + + # 使用列表格式访问节点(freeopcua推荐的方式) + upper_weight = objects.get_child(["2:upper", "2:upper_weight"]).get_value() + lower_weight = objects.get_child(["2:lower", "2:lower_weight"]).get_value() + + print(f"上料斗重量: {upper_weight}") + print(f"下料斗重量: {lower_weight}") + + return upper_weight, lower_weight + + except Exception as e: + print(f"读取重量数据时出错: {e}") + return None, None + + def write_multiple_values(self, values_dict: dict) -> dict: + """ + 批量写入多个节点 + + Args: + values_dict: 字典,key为节点路径,value为要写入的值 + + Returns: + dict: 写入结果,key为节点路径,value为成功/失败状态 + """ + results = {} + for node_path, value in values_dict.items(): + results[node_path] = self.write_data(node_path, value) + return results + + def write_test_data(self): + """测试写入各种类型的数据""" + if not self.connected: + print("请先连接到服务器") + return + + print("\n=== 测试写入数据 ===") + + # 测试写入目标重量 + self.write_data("2:upper/2:target_weight", 150.5, "float") + + # 测试写入开关量 + self.write_data("2:upper/2:valve_on", True, "bool") + + # 测试写入整数 + self.write_data("2:upper/2:cycle_count", 10, "int") + + # 测试批量写入 + values = { + "2:upper/2:target_weight": 200.0, + "2:lower/2:target_weight": 100.0, + } + results = self.write_multiple_values(values) + + print("\n批量写入结果:") + for path, success in results.items(): + status = "✓ 成功" if success else "✗ 失败" + print(f" {path}: {status}") + def monitor_data(self, duration=30): """监控数据变化""" if not self.connected: @@ -97,7 +384,7 @@ class OPCUAClientTest: start_time = time.time() while time.time() - start_time < duration: print(f"\n--- {time.strftime('%H:%M:%S')} ---") - self.read_sensor_values(upper_device, lower_device) + self.read_object_properties(upper_device, lower_device) time.sleep(5) # 每5秒读取一次 except KeyboardInterrupt: @@ -117,21 +404,61 @@ def main(): if not client.connect(): return - # 浏览节点结构 + # 浏览节点结构(首先发现实际节点结构) + print("\n" + "="*60) + print("步骤1: 浏览服务器节点结构") + print("="*60) client.browse_nodes() - # 监控数据变化 - client.monitor_data(duration=30) + # 尝试使用新方法写入数据 + print("\n" + "="*60) + print("步骤2: 使用动态节点查找方法写入数据") + print("="*60) - # 测试写入数据 - # client.write_test_data() + # 方法1: 使用write_weights_directly自动查找节点 + print("\n尝试方法1: write_weights_directly (自动查找节点)") + # client.write_data("2:upper/2:upper_weight", 180, "int") + # client.write_data("2:lower/2:lower_weight", 120, "int") + + values = { + "2:upper/2:upper_weight": 200, + "2:lower/2:lower_weight": 100, + } + client.write_multiple_values(values) + # success1 = client.write_weights_directly(150, 120) + time.sleep(2) - # 继续监控 - print("\n继续监控数据...") - client.monitor_data(duration=15) + # if not success1: + # # 方法2: 尝试可能的替代路径 + # print("\n尝试方法2: 尝试其他节点路径格式") + + # # 列出可能的节点路径格式 + # possible_paths = [ + # "2:upper_weight", + # "2:lower_weight", + # "2:upper/upper_weight", + # "2:lower/lower_weight", + # "ns=2;upper_weight", + # "ns=2;lower_weight" + # ] + + # for path in possible_paths: + # print(f" 尝试写入: {path}") + # client.write_data(path, 150.5, "float") + # time.sleep(0.5) + + print("\n" + "="*60) + print("步骤3: 读取验证数据") + print("="*60) + upper, lower = client.read_weights() + print(f"读取结果 - 上料斗: {upper}, 下料斗: {lower}") except KeyboardInterrupt: print("\n客户端被用户中断") + except Exception as e: + print(f"客户端运行错误: {e}") + import traceback + traceback.print_exc() finally: # 断开连接 client.disconnect() diff --git a/opc/opcua_server_test.py b/opc/opcua_server_test.py new file mode 100644 index 0000000..bb3ea46 --- /dev/null +++ b/opc/opcua_server_test.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +""" +简单的OPC UA服务器示例 +用于工业自动化数据通信 +""" + +from opcua import Server, ua +import time +import random +import threading +from datetime import datetime +# from core.system import SystemState +# from config.ini_manager import ini_manager + +class SimpleOPCUAServer: + # def __init__(self, state, endpoint=ini_manager.opcua_endpoint, name="Feed_Server"): + def __init__(self, endpoint='', name="Feed_Server"): + """ + 初始化OPC UA服务器 + Args: + endpoint: 服务器端点地址 + name: 服务器名称 + """ + self.server = Server() + self.server.set_endpoint(endpoint) + self.server.set_server_name(name) + # self.state = state + + # 设置服务器命名空间 + self.namespace = self.server.register_namespace("Feed_Control_System") + + # 获取对象节点 + self.objects = self.server.get_objects_node() + + # 创建自定义对象 + self.create_object_structure() + + # 运行标志 + self.running = False + + # 订阅和监控项 + self.subscription = None + self.monitored_items = [] + + # 记录上次值用于检测变化 + self._last_values = {} + + def create_object_structure(self): + """创建OPC UA对象结构""" + # 创建上料斗对象 + self.upper = self.objects.add_object(self.namespace, "upper") + self.lower=self.objects.add_object(self.namespace, "lower") + self.sys=self.objects.add_object(self.namespace, "sys") + self.mould=self.objects.add_object(self.namespace, "mould") + self.pd=self.objects.add_object(self.namespace, "pd") + + # 创建变量 + self.create_variables() + + def create_variables(self): + """创建OPC UA变量""" + # 创建变量时显式指定数据类型和初始值 + #上料斗 + self.upper_weight = self.upper.add_variable(self.namespace, "upper_weight", ua.Variant(0.0, ua.VariantType.Float)) + self.upper_is_arch = self.upper.add_variable(self.namespace, "upper_is_arch", ua.Variant(False, ua.VariantType.Boolean)) + self.upper_door_closed = self.upper.add_variable(self.namespace, "upper_door_closed", ua.Variant(False, ua.VariantType.Boolean)) + self.upper_volume = self.upper.add_variable(self.namespace, "upper_volume", ua.Variant(0.0, ua.VariantType.Float)) + self.upper_door_position = self.upper.add_variable(self.namespace, "upper_door_position", ua.Variant(0, ua.VariantType.Int16)) + + #下料斗 + self.lower_weight = self.lower.add_variable(self.namespace, "lower_weight", ua.Variant(0.0, ua.VariantType.Float)) + self.lower_is_arch = self.lower.add_variable(self.namespace, "lower_is_arch", ua.Variant(False, ua.VariantType.Boolean)) + + #模具车 + self.mould_finish_weight = self.mould.add_variable(self.namespace, "mould_finish_weight", ua.Variant(0.0, ua.VariantType.Float)) + self.mould_need_weight = self.mould.add_variable(self.namespace, "mould_need_weight", ua.Variant(0.0, ua.VariantType.Float)) + self.mould_frequency = self.mould.add_variable(self.namespace, "mould_frequency", ua.Variant(230, ua.VariantType.Int32)) + self.mould_vibrate_status = self.mould.add_variable(self.namespace, "mould_vibrate_status", ua.Variant(False, ua.VariantType.Boolean)) + self.feed_status = self.mould.add_variable(self.namespace, "feed_status", ua.Variant(0, ua.VariantType.Int16)) + self.pd_data=self.pd.add_variable(self.namespace, "pd_data", ua.Variant("", ua.VariantType.String)) + + # 在创建变量后立即设置可写权限(不需要等待服务器启动) + self.upper_weight.set_writable(True) + self.lower_weight.set_writable(True) + self.upper_is_arch.set_writable(True) + self.upper_door_closed.set_writable(True) + self.upper_volume.set_writable(True) + self.upper_door_position.set_writable(True) + self.lower_is_arch.set_writable(True) + self.mould_finish_weight.set_writable(True) + self.mould_need_weight.set_writable(True) + self.mould_frequency.set_writable(True) + self.mould_vibrate_status.set_writable(True) + self.feed_status.set_writable(True) + self.pd_data.set_writable(True) + + print("[变量创建] 变量创建完成,AccessLevel权限已设置") + + # 验证并打印当前的AccessLevel属性 + # try: + # al = self.upper_weight.get_attribute(ua.AttributeIds.AccessLevel) + # ual = self.upper_weight.get_attribute(ua.AttributeIds.UserAccessLevel) + # print(f"[变量创建] upper_weight AccessLevel: {al.Value.Value}, UserAccessLevel: {ual.Value.Value}") + + # al2 = self.lower_weight.get_attribute(ua.AttributeIds.AccessLevel) + # ual2 = self.lower_weight.get_attribute(ua.AttributeIds.UserAccessLevel) + # print(f"[变量创建] lower_weight AccessLevel: {al2.Value.Value}, UserAccessLevel: {ual2.Value.Value}") + + # except Exception as e: + # print(f"[变量创建] 获取权限属性失败: {e}") + + def setup_variable_permissions(self): + """设置变量权限 - 在服务器启动后调用""" + try: + # 重新设置变量为可写,确保权限生效 + self.upper_weight.set_writable(True) + self.lower_weight.set_writable(True) + print("[权限设置] 变量权限已重新设置") + + # 验证权限 + try: + al = self.upper_weight.get_attribute(ua.AttributeIds.AccessLevel) + ual = self.upper_weight.get_attribute(ua.AttributeIds.UserAccessLevel) + print(f"[权限设置] upper_weight AccessLevel: {al.Value.Value}, UserAccessLevel: {ual.Value.Value}") + except Exception as e: + print(f"[权限设置] 验证失败: {e}") + + except Exception as e: + print(f"[权限设置] 设置权限失败: {e}") + print("[权限设置] 尝试强制设置...") + + + # def setup_state_listeners(self): + # """设置状态监听器 - 事件驱动更新""" + # if hasattr(self.state, 'state_updated'): + # self.state.state_updated.connect(self.on_state_changed) + # print("状态监听器已设置 - 事件驱动模式") + + # def on_state_changed(self, property_name, value): + # """状态变化时的回调函数""" + # try: + # # 根据属性名更新对应的OPC UA变量 + # if property_name == "upper_weight": + # self.upper_weight.set_value(value) + # elif property_name == "lower_weight": + # self.lower_weight.set_value(value) + + # # 可以在这里添加更多状态映射 + # print(f"状态更新: {property_name} = {value}") + + # except Exception as e: + # print(f"状态更新错误: {e}") + + def start(self): + """启动服务器""" + try: + self.server.start() + self.running = True + print(f"服务器端点: opc.tcp://0.0.0.0:4840/freeopcua/server/") + print("=" * 60) + + # 【关键修复】在设置监听器之前,先设置变量权限 + # 这确保 AccessLevel 属性在客户端写入前已正确设置 + # self.setup_variable_permissions() + + print("=" * 60) + + # 设置客户端写入监听器 + # self.setup_write_listeners() + + print("=" * 60) + + # 初始化当前值 + # if self.state: + # self.upper_weight.set_value(self.state._upper_weight) + # self.lower_weight.set_value(self.state._lower_weight) + # print("已同步初始状态值") + + # 设置状态监听器 - 关键步骤! + # self.setup_state_listeners() + + # # 只有在没有状态系统时才使用模拟线程 + # if not self.state: + # print("使用模拟数据模式") + # self.simulation_thread = threading.Thread(target=self.simulate_data) + # self.simulation_thread.daemon = True + # self.simulation_thread.start() + + except Exception as e: + print(f"启动服务器失败: {e}") + + def stop(self): + """停止服务器""" + # 移除监听器 + # self.remove_write_listeners() + + self.running = False + self.server.stop() + print("OPC UA服务器已停止") + + # 断开状态监听器 + # if hasattr(self.state, 'state_updated'): + # try: + # self.state.state_updated.disconnect(self.on_state_changed) + # except: + # pass + + +def main(): + """主函数""" + # 创建系统状态实例 + # state = SystemState() + + # 创建并启动服务器 + server = SimpleOPCUAServer( + endpoint="opc.tcp://0.0.0.0:4840/freeopcua/server/", + name="工业自动化 OPC UA 服务器" + ) + + try: + server.start() + print("服务器正在运行,按 Ctrl+C 停止...") + # 保持服务器运行 + while True: + time.sleep(1) + + except KeyboardInterrupt: + print("\n正在停止服务器...") + server.stop() + + except Exception as e: + print(f"服务器运行错误: {e}") + server.stop() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/service/api_http_client.py b/service/api_http_client.py index 150cd1a..7339711 100644 --- a/service/api_http_client.py +++ b/service/api_http_client.py @@ -58,7 +58,7 @@ class BaseHttpClient: # 解析JSON响应 return response.json() - except exception as e: + except Exception as e: # 如果是最后一次尝试,直接抛出异常 if attempt == retries: print(f"请求失败(第{attempt + 1}次尝试): {e}") diff --git a/service/mould_service.py b/service/mould_service.py index 5d52ed2..d05258d 100644 --- a/service/mould_service.py +++ b/service/mould_service.py @@ -1,3 +1,7 @@ +import os +import sys +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + from datetime import datetime, timedelta from common.sqlite_handler import SQLiteHandler from typing import Optional, List @@ -42,7 +46,16 @@ class MouldService: return None # 转换为任务单信息对象 - task_info = TaskInfo(**data) + task_info = TaskInfo( + TaskID=data.get('TaskID', ''), + TaskStatus=data.get('TaskStatus', 0), + TaskStatusText=data.get('TaskStatusText', ''), + ProjectName=data.get('ProjectName', ''), + BetonGrade=data.get('BetonGrade', ''), + MixID=data.get('MixID', ''), + ProduceMixID=data.get('ProduceMixID', ''), + PlannedVolume=data.get('PlannedVolume', 0.0) + ) return task_info except Exception as e: @@ -51,12 +64,12 @@ class MouldService: def get_not_pour_artifacts(self) -> Optional[List[ArtifactInfo]]: """ - 获取已入模绑定未浇筑的管片信息 + 获取已入待浇筑的管片信息 Returns: 未浇筑管片列表,如果失败返回None """ - url = f"{self._host}/api/ext/mould/not_pour_hidden" + url = f"{self._host}/api/ext/mould/not_pour_rfid" try: # 调用API获取数据 @@ -74,7 +87,8 @@ class MouldService: return [] # 转换为管片信息对象列表 - artifacts = [ArtifactInfo(**item) for item in data_list] + # artifacts = [ArtifactInfo(**item) for item in data_list] + artifacts = [ArtifactInfo(**item) for item in data_list if item.get('MouldCode') is not None and item.get('MouldCode') != ''] return artifacts except Exception as e: @@ -136,60 +150,62 @@ class MouldService: app_web_service = MouldService() -# if __name__ == "__main__": -# # 创建模具服务实例 -# mould_service = MouldService() -# led_info = mould_service.get_pouring_led() -# if led_info: -# print(led_info) +if __name__ == "__main__": + # 创建模具服务实例 + mould_service = MouldService() + # led_info = mould_service.get_pouring_led() + # if led_info: + # print(led_info) - # db = SQLiteHandler.get_instance("db/three.db", max_readers=50, busy_timeout=4000) + db = SQLiteHandler.get_instance("db/three.db", max_readers=50, busy_timeout=4000) # 测试获取未浇筑管片信息 - # not_poured = mould_service.get_not_pour_artifacts() - # if not_poured: - # for item in not_poured: - # artifact = db.fetch_one("SELECT * FROM ArtifactTask WHERE ArtifactID = ?", (item.ArtifactID,)) - # if not artifact: - # dict={ - # "ArtifactID": item.ArtifactID, - # "ArtifactActionID": item.ArtifactActionID, - # "ArtifactIDVice1": item.ArtifactIDVice1, - # "ProduceRingNumber": item.ProduceRingNumber, - # "MouldCode": item.MouldCode, - # "SkeletonID": item.SkeletonID, - # "RingTypeCode": item.RingTypeCode, - # "SizeSpecification": item.SizeSpecification, - # "BuriedDepth": item.BuriedDepth, - # "BlockNumber": item.BlockNumber, - # "BetonVolume": item.BetonVolume, - # "BetonTaskID": item.BetonTaskID, - # "HoleRingMarking": item.HoleRingMarking, - # "GroutingPipeMarking": item.GroutingPipeMarking, - # "PolypropyleneFiberMarking": item.PolypropyleneFiberMarking, - # "Status": 1, - # "Source": 1 - # } - # db.insert("ArtifactTask", dict) + not_poured = mould_service.get_not_pour_artifacts() + if not_poured: + for item in not_poured: + artifact = db.fetch_one("SELECT * FROM ArtifactTask WHERE ArtifactID = ?", (item.ArtifactID,)) + if not artifact: + dict={ + "ArtifactID": item.ArtifactID, + "ArtifactActionID": item.ArtifactActionID, + "ArtifactIDVice1": item.ArtifactIDVice1, + "ProduceRingNumber": item.ProduceRingNumber, + "MouldCode": item.MouldCode, + "SkeletonID": item.SkeletonID, + "RingTypeCode": item.RingTypeCode, + "SizeSpecification": item.SizeSpecification, + "BuriedDepth": item.BuriedDepth, + "BlockNumber": item.BlockNumber, + "BetonVolume": item.BetonVolume, + "BetonTaskID": item.BetonTaskID, + "HoleRingMarking": item.HoleRingMarking, + "GroutingPipeMarking": item.GroutingPipeMarking, + "PolypropyleneFiberMarking": item.PolypropyleneFiberMarking, + "Status": 1, + "Source": 1 + } + db.insert("ArtifactTask", dict) - # dict={ - # "TaskID": item.BetonTaskID, - # "ProjectName": "上海市轨道交通19号线工程盾构区间管片生产2标", - # "ProduceMixID": "20251030-02", - # "VinNo": "", - # "BetonVolume": item.BetonVolume, - # "MouldCode": item.MouldCode, - # "SkeletonID": item.SkeletonID, - # "RingTypeCode": item.RingTypeCode, - # "SizeSpecification": item.SizeSpecification, - # "BuriedDepth": item.BuriedDepth, - # "BlockNumber": item.BlockNumber, - # "Mode": 1, - # "Status": 1, - # "Source": 1, - # "OptTime": str(datetime.now() - timedelta(minutes=5)), - # "CreateTime": str(datetime.now()) - # } - # db.insert("PDRecord", dict) + dict={ + "TaskID": item.BetonTaskID, + "ProjectName": "上海市轨道交通19号线工程盾构区间管片生产2标", + "ProduceMixID": "20251030-02", + "VinNo": "", + "BetonVolume": item.BetonVolume, + "MouldCode": item.MouldCode, + "SkeletonID": item.SkeletonID, + "RingTypeCode": item.RingTypeCode, + "SizeSpecification": item.SizeSpecification, + "BuriedDepth": item.BuriedDepth, + "BlockNumber": item.BlockNumber, + "Mode": 1, + "Status": 1, + "Source": 1, + "OptTime": str(datetime.now() - timedelta(minutes=5)), + "CreateTime": str(datetime.now()) + } + db.insert("PDRecord", dict) + + # for i in range(2, 5): # row = db.fetch_one("SELECT * FROM ArtifactTask WHERE ID = ?", (i,)) # if row: diff --git a/settings.ini b/settings.ini index d7010b7..1244fd8 100644 --- a/settings.ini +++ b/settings.ini @@ -14,4 +14,10 @@ upper_transmitter_ip = 192.168.250.63 upper_transmitter_port = 502 lower_transmitter_ip = 192.168.250.66 lower_transmitter_port = 8234 +[hardware] +relay_host = 192.168.250.62 +relay_port = 50000 +upper_plc_ip = 192.168.250.233 +upper_plc_port = 9600 + diff --git a/test copy.py b/test copy.py index de26e5b..8e03229 100644 --- a/test copy.py +++ b/test copy.py @@ -1,32 +1,62 @@ -import socket +# 读取原始数据文件 +import datetime -# 设备信息 -IP = "192.168.250.63" -PORT = 502 -TIMEOUT = 5 # 超时时间(秒) +input_file = r"C:\Users\fujin\Desktop\fsdownload\weight.txt" # 原始数据文件路径 +output_file = r"C:\Users\fujin\Desktop\fsdownload\filtered_B_records.txt" # 输出结果文件路径 (使用原始字符串避免Unicode转义错误) -# 创建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("❌ 未收到任何数据(设备未主动发送)") +# 存储包含"B"标记的记录 +filtered_records = [] + +# 读取并过滤数据 +with open(input_file, "r", encoding="utf-8") as f: + lines = f.readlines() + for line in lines: + # 筛选包含"B,"的行(避免误匹配其他含B的字符) + if "B," in line or "F," in line: + # 移除等号 + cleaned_line = line.strip().replace('=', '') - 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 + # 分割行数据 + parts = cleaned_line.split(',') + + if len(parts) >= 2: + try: + # 解析前两个日期(假设格式为YYYY-MM-DD或YYYY/MM/DD) + date1_str = parts[0].strip() + date2_str = parts[1].strip() + + # 尝试不同的日期格式 + date_formats = ['%Y-%m-%d %H:%M:%S', '%Y-%m-%d', '%Y/%m/%d'] + date1 = None + date2 = None + + for fmt in date_formats: + try: + if date1 is None: + date1 = datetime.datetime.strptime(date1_str, fmt) + if date2 is None: + date2 = datetime.datetime.strptime(date2_str, fmt) + except ValueError: + continue + + if date1 and date2: + # 计算时间差(秒数) + total_seconds = (date2 - date1).total_seconds() + # 转换为分:秒格式 + minutes = int(total_seconds // 60) + seconds = int(total_seconds % 60) + time_diff = f"{minutes:02d}:{seconds:02d}" + # 在行末尾添加时间差 + cleaned_line = f"{cleaned_line},{time_diff}" + except Exception as e: + # 如果日期解析失败,保持原始行不变 + pass + + filtered_records.append(cleaned_line) + +# 保存过滤后的数据 +with open(output_file, "w", encoding="utf-8") as f: + f.write("\n".join(filtered_records)) + +print(f"过滤完成!共提取到 {len(filtered_records)} 条含'B'标记的记录") +print(f"结果已保存至:{output_file}") \ No newline at end of file diff --git a/test_weight.py b/test_weight.py index 880ad21..9765dd9 100644 --- a/test_weight.py +++ b/test_weight.py @@ -11,7 +11,8 @@ 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))) + # data = '上料斗:' + str(int(transmitter_c.read_data(1))) + ',' +"下料斗:" + str(int(transmitter_c.read_data(2))) + data = str(int(transmitter_c.read_data(2))) timestamp = datetime.now().strftime('%H:%M:%S.%f')[:-3] output = f"[{timestamp}] {data}" print(output) diff --git a/tests/485test.py b/tests/485test.py index c362c3d..94a6ffc 100644 --- a/tests/485test.py +++ b/tests/485test.py @@ -1,339 +1,79 @@ -import serial +import minimalmodbus import time -import struct +from serial import SerialException -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 - +# 设置频率为55Hz并启动变频器 +def start_inverter_55hz(): try: - while True: - print("\n" + "=" * 50) - print("汇川MD520变频器频率查询") - print("=" * 50) + # 1. 连接到变频器 + # COM3: 串口地址 + # 1: 从站地址 + inverter = minimalmodbus.Instrument('COM3', 1) + + # 2. 配置串口参数 + inverter.serial.baudrate = 9600 # 波特率 + inverter.serial.bytesize = 8 # 数据位 + inverter.serial.parity = 'N' # 无校验 + inverter.serial.stopbits = 1 # 停止位 + inverter.serial.timeout = 1.0 # 超时时间 + inverter.mode = minimalmodbus.MODE_RTU # RTU模式 + + print("✅ 已连接到变频器") + + # 3. 设置频率为55Hz + # 假设最大频率为50Hz,55Hz对应110% = 11000 + # # 如果您的最大频率不同,请调整下面的计算 + # frequency_value = int(70 * 100) + # inverter.write_register(0x1000, frequency_value) + # print(f"✅ 频率设置为200Hz") - # 设置允许频率 - frequency = inverter.set_frequency(slave_addr=0x01, frequency=210.0) - if frequency is not None: - print(f"✅ 设置成功") - else: - print("❌ 频率设置失败") + frequency_value = int(210 * 100) + inverter.write_register(0x7310, frequency_value) - # 查询运行频率 - frequency = inverter.query_frequency(slave_addr=0x01) + # freq = inverter.read_register(0x7310) + # print(f" 读取到的频率值: {freq}") - if frequency is not None: - print(f"✅ 当前运行频率: {frequency:.2f} Hz") - else: - print("❌ 频率查询失败") + # max_freq = 300 + # target_freq = 230 + # frequency_value = int(target_freq / max_freq * 100 * 100) # = 2333 + # inverter.write_register(0x1000, frequency_value) + + # time.sleep(0.5) # 等待一下 + + # 4. 启动变频器(正转运行) + inverter.write_register(0x2000, 1) # 1=正转运行 + print("✅ 变频器已启动(正转运行)") + # time.sleep() - - # 可选:读取其他监控参数 - 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用户中断查询") + # inverter.write_register(0x2000, 6) # 1=正转运行 + # print("✅ 变频器已停止(正转运行)") + + # 5. 检查状态 + time.sleep(1) + #读取3000H可直接读取变频器的当前状态(0001:正转运行;0002:反转运行;0003:停机;0004:电机参数辨识;0005:故障)。 + status = inverter.read_register(0x3000) + + if status == 1: + print("✅ 变频器正在正转运行") + elif status == 5: + print("⚠️ 变频器故障") + else: + print(f"📊 变频器状态码: {status}") + + except SerialException: + print('无法连接或打开串口') + except Exception as e: + print(f"❌ 错误: {e}") + print("请检查:") + print("1. COM3端口是否正确?") + print("2. 变频器电源和通信线是否连接正常?") + print("3. 变频器地址和波特率设置是否正确?") + finally: - # 断开连接 - inverter.disconnect() - + print("脚本执行完成") +# 运行 if __name__ == "__main__": - main() \ No newline at end of file + start_inverter_55hz() \ No newline at end of file diff --git a/tests/api_service_test.py b/tests/api_service_test.py new file mode 100644 index 0000000..b689482 --- /dev/null +++ b/tests/api_service_test.py @@ -0,0 +1,93 @@ +""" +API接口 +""" +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import time +from busisness.blls import ArtifactBll,PDRecordBll +from busisness.models import ArtifactInfoModel,PDRecordModel +from service.mould_service import app_web_service + + +_model_task=None +artifact_bll=ArtifactBll() +pdrecord_bll=PDRecordBll() + + +if __name__ == "__main__": + try: + # 初始化三个列表用于跟踪ArtifactActionID + processed_artifact_actions = [] # 已处理的ArtifactActionID列表 + processed_pd_records = [] # 已插入PDRecord表的ArtifactActionID列表 + + # db = SQLiteHandler.get_instance("db/three.db", max_readers=50, busy_timeout=4000) + while True: + # 测试获取未浇筑管片信息 + try: + not_poured = app_web_service.get_not_pour_artifacts() + if not_poured: + for item in reversed(not_poured): + # 检查MouldCode是否已处理 + if item.MouldCode in processed_artifact_actions: + print(f"待浇筑:MouldCode {item.MouldCode} 已处理,跳过") + continue + + _model_data = ArtifactInfoModel(**item.__dict__) + artifact_bll.insert_artifact_task(_model_data) + # 标记为已处理 + processed_artifact_actions.append(item.MouldCode) + # 限制最多保存3条记录,删除最旧的 + if len(processed_artifact_actions) > 3: + processed_artifact_actions.pop(0) + print(f"待浇筑:已处理MouldCode {item.MouldCode}") + + if item.MouldCode in processed_pd_records: + print(f"派单:MouldCode {item.MouldCode} 已处理,跳过") + continue + + if item.BetonTaskID is not None and item.BetonTaskID != '': + #获取taskid + if _model_task is None or item.BetonTaskID != _model_task.TaskID: + _model_task = app_web_service.get_task_info(item.BetonTaskID) + if _model_task is None: + print(f"异常:BetonTaskID {item.BetonTaskID} 不存在,跳过") + continue + + _pd_record_data = PDRecordModel( + ArtifactID=item.ArtifactID, + TaskID=_model_task.TaskID, + ProjectName=_model_task.ProjectName, + ProduceMixID=_model_task.ProduceMixID, + BetonGrade=_model_task.BetonGrade, + BetonVolume=item.BetonVolume, + MouldCode=item.MouldCode, + SkeletonID=item.SkeletonID, + RingTypeCode=item.RingTypeCode, + SizeSpecification=item.SizeSpecification, + BuriedDepth=item.BuriedDepth, + BlockNumber=item.BlockNumber + ) + + pdrecord_bll.insert_PD_record(_pd_record_data) + # 标记为已处理 + processed_pd_records.append(item.MouldCode) + # 限制最多保存3条记录,删除最旧的 + if len(processed_pd_records) > 3: + processed_pd_records.pop(0) + print(f"派单:已处理MouldCode {item.MouldCode}") + else: + print(f"异常:MouldCode {item.MouldCode} 无BetonTaskID,跳过") + + + + except Exception as e: + print(f"处理MouldCode {item.MouldCode} 时发生错误: {e}") + + time.sleep(5) + except KeyboardInterrupt: + + print("\n测试被用户中断") + except Exception as e: + + print(f"测试过程中发生错误: {e}") \ No newline at end of file diff --git a/tests/test_rfid.py b/tests/test_rfid.py index c00ac0a..9c4519b 100644 --- a/tests/test_rfid.py +++ b/tests/test_rfid.py @@ -54,7 +54,7 @@ def test_rfid_functions(): """ global rfid # 初始化RFID控制器 - rfid = rfid_service(host='192.168.250.67', port=6000) + rfid = rfid_service(host='192.168.250.77', port=6000) # print("=== RFID硬件测试开始 ===") diff --git a/vision/camera_picture.py b/vision/camera_picture.py new file mode 100644 index 0000000..2325ab6 --- /dev/null +++ b/vision/camera_picture.py @@ -0,0 +1,87 @@ +import os +import threading +from datetime import datetime +import cv2 +import time + +def capture_camera_images(rtsp_url, save_dir, camera_name, img_count=10, interval=0.5, reverse=False): + """ + 从RTSP摄像头抓取指定数量图片并保存 + :param rtsp_url: 摄像头RTSP地址 + :param save_dir: 保存目录 + :param camera_name: 摄像头名称(用于文件名区分) + :param img_count: 抓取图片数量 + :param interval: 抓取间隔(秒) + :param reverse: 是否翻转180 + """ + # 创建摄像头专属保存目录 + camera_save_dir = os.path.join(save_dir, camera_name) + if not os.path.exists(camera_save_dir): + os.makedirs(camera_save_dir) + + # 抓取并保存图片 + for i in range(img_count): + cap = None + try: + # 打开RTSP流 + cap = cv2.VideoCapture(rtsp_url, cv2.CAP_FFMPEG) + if not cap.isOpened(): + continue + cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) + ret, frame = cap.read() + if reverse: # 镜像翻转 + frame = cv2.flip(frame, -1) + if ret and frame is not None: + img_filename = f"{camera_name}_{datetime.now().strftime('%Y%m%d%H%M%S')}_{i+1}.jpg" + img_path = os.path.join(camera_save_dir, img_filename) + cv2.imwrite(img_path, frame) + time.sleep(interval) + except Exception as e: + print(f"⚠ 摄像头 {camera_name} 第 {i+1} 次截图异常:{e},跳过") + finally: + if cap is not None and cap.isOpened(): + cap.release() + + print(f"✅ 摄像头 {camera_name} 完成,共保存 {img_count} 张浇筑满的图片") + + +def save_camera_picture(save_dir="full_images", img_count=15, interval=1.5, camera_60_rtsp=None, camera_61_rtsp=None): + """ + 执行摄像头截图保存: + 1. 创建以时间戳命名的目录,统一存放日志和图片 + 2. 抓取60和61摄像头各img_count张图片 + :param save_dir: 保存浇筑满图片的顶级目录 + :param img_count: 每次浇筑满, 保存的图片的总数量 + :param interval: 保存两张浇筑满图片之间的时间间隔 + """ + # 0. 先创建上级目录save_dir(若不存在) + if not os.path.exists(save_dir): + os.makedirs(save_dir) + + # 1. 创建保存浇筑满图片的根目录 + root_dir = f"{save_dir}/PourFull_{datetime.now().strftime('%Y%m%d%H%M%S')}" + if not os.path.exists(root_dir): + os.makedirs(root_dir) + + # 2. 抓取两个摄像头的图片 + if camera_60_rtsp is None: + camera_60_rtsp = "rtsp://admin:XJ123456@192.168.250.60:554/streaming/channels/101" + + if camera_61_rtsp is None: + camera_61_rtsp = "rtsp://admin:XJ123456@192.168.250.61:554/streaming/channels/101" + + # 并行抓取 + t1 = threading.Thread( + target=capture_camera_images, + args=(camera_60_rtsp, root_dir, "camera60", img_count, interval, False), + daemon=True + ) + t2 = threading.Thread( + target=capture_camera_images, + args=(camera_61_rtsp, root_dir, "camera61", img_count, interval, True), + daemon=True + ) + t1.start() + t2.start() + + return root_dir \ No newline at end of file diff --git a/vision/muju_cls/muju_utils.py b/vision/muju_cls/muju_utils.py index 70d0190..61b7d7f 100644 --- a/vision/muju_cls/muju_utils.py +++ b/vision/muju_cls/muju_utils.py @@ -54,28 +54,29 @@ def run_stable_classification_loop(): # --------------------------- result = classify_single_image(frame, model_path, roi_file) + class_id = result["class_id"] class_name = result["class"] score = result["score"] print(f"[FRAME] {class_name} | conf={score:.3f}") + if score>0.8: + # --------------------------- + # 稳定判断 + # --------------------------- + stable_class_id = judge.update(class_id) - # --------------------------- - # 稳定判断 - # --------------------------- - stable_class_id = judge.update(class_id) - - if stable_class_id is not None: - _ret=CLASS_NAMES[stable_class_id] - if _ret is None: - print("-------当前类别为:为空,继续等待稳定------") - continue - if _ret=="模具车1" or _ret=="模具车2": - break - print(f"当前类别为:{_ret},继续等待稳定...") - else: - print("当前类别为:为空,继续等待稳定...") - + if stable_class_id is not None: + _ret=CLASS_NAMES[stable_class_id] + if _ret is None: + print("-------当前类别为:为空,继续等待稳定------") + continue + if _ret=="模具车1" or _ret=="模具车2": + break + print(f"当前类别为:{_ret},继续等待稳定...") + else: + print("当前类别为:为空,继续等待稳定...") + time.sleep(0.1) finally: if cap is not None: diff --git a/vision/visual_callback.py b/vision/visual_callback.py index 403bff2..4ebbb1f 100644 --- a/vision/visual_callback.py +++ b/vision/visual_callback.py @@ -124,7 +124,6 @@ class VisualCallback: self._initialized = True self.plc_data=None - def angle_visual_callback(self, current_angle, overflow_detected): """ 视觉控制主逻辑,供外部推送数据 @@ -689,8 +688,8 @@ class VisualCallback: # 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}") + # 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 @@ -743,11 +742,11 @@ class VisualCallback: if error > 0: # 当前角度 > 目标角度,需要关门 pulse_time=0.1 # 根据误差计算脉冲时间 self._pulse_control("close", pulse_time) - print(f"🚨 强制关门: 误差{abs_error:.1f}°过大,脉冲{pulse_time:.3f}s") + 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") + print(f"强制开门: 误差{abs_error:.1f}°过大,脉冲{pulse_time:.3f}s") return else: self._stop_door() diff --git a/vision/visual_callback_dq copy.py b/vision/visual_callback_dq copy.py new file mode 100644 index 0000000..8129c7e --- /dev/null +++ b/vision/visual_callback_dq copy.py @@ -0,0 +1,1192 @@ + +from pickle import FALSE +from cv2.gapi import ov +from config.settings import app_set_config +from hardware.relay import RelayController +from hardware.transmitter import TransmitterController +import time +import threading +from datetime import datetime +import logging +import queue +from hardware.upper_plc import OmronFinsPollingService +from vision.muju_cls.muju_utils import run_stable_classification_loop +from vision.camera_picture import save_camera_picture + +class VisualCallback: + # 类变量,用于存储实例引用,实现单例检测 + _instance = None + _lock = threading.Lock() + + def __new__(cls): + """检测实例是否存在,实现单例模式""" + with cls._lock: + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + """初始化视觉回调处理器""" + # 避免重复初始化 + if hasattr(self, '_initialized') and self._initialized: + return + + self.relay_controller = RelayController() + self.transmitter_controller = TransmitterController(self.relay_controller) + + # 线程安全的参数传递 + self._new_data_available = threading.Event() + self._is_processing = threading.Lock() + + #diff参数 + self._is_processing_diff = threading.Lock() + self._new_data_diff = threading.Event() + self._current_diff=0 + self._current_diff_area=[] + self._is_diff_save=False + + + self._stop_event = threading.Event() + + # 添加下料斗门控制锁,防止两个线程同时控制 + self._door_control_lock = threading.Lock() + # 记录当前控制门的线程名称,用于调试 + self._current_controlling_thread = None + #是否启动后的第一个模具 + self._is_first_module=True + self.init_val() + # self._setup_logging_2() + #F块完成重量的70%,控制夹脚,F块多于这个比例就没有记录了(注意) + self._max_f_angle_ratio=0.7 + #完成多少,调整角度比例 ,多于0.8就没记录了(注意) + self._max_angle_radio=0.8 + + #重量大于95%,停留时间2秒,其他的1秒 + self._weight_ratio_955=0.955 + #完成多少,忽略未浇筑满 + self._max_ignore_radio=0.5 + + self._mould_accept_aligned=None + self._mould_before_aligned=False + #模具开始浇筑时间 + self._time_mould_begin='' + #模具结束浇筑时间 + self._time_mould_end='' + + # self.db_queue=queue.Queue() + + # self.plc_data=5 + self.plc_service = OmronFinsPollingService("192.168.250.233") + self.plc_service.register_data_callback(self.on_plc_update) + # self.plc_service.register_status_callback(self.on_status_change) + self.plc_service.start_polling(interval=2.0) + + # 创建并启动单个持续运行的线程 + self.callback_thread = threading.Thread( + target=self._run_thread_loop, + daemon=True + ) + self.callback_thread.start() + + self.feed_thread = threading.Thread( + target=self._run_feed, + daemon=True + ) + self.feed_thread.start() + + """启动系统监控""" + self.monitor_thread = threading.Thread( + target=self._monitor_loop, + daemon=True, + name='monitor' + ) + self.monitor_thread.start() + + + self.diff_thread = threading.Thread( + target=self._diff_temp, + daemon=True + ) + self.diff_thread.start() + + """启动数据库监控""" + # self.db_thread = threading.Thread( + # target=self._monitor_db_loop, + # daemon=True, + # name='db_monitor' + # ) + # self.db_thread.start() + + def init_val(self): + #初始化值 + """初始化视觉回调处理器""" + self.angle_mode = "normal" + self.overflow = False + self.is_start_visual=False + + # 线程安全的参数传递 + self._current_angle = None + self._overflow_detected = None + # 新增标志位:指示safe_control_lower_close是否正在执行 + self._is_safe_closing = False + + self._is_feed_start=True + #未浇筑满时间,用于确定是否进入未浇筑满 + self._before_finish_time=None + #进入未浇筑满状态标志位 + self._is_before_finish=False + #是否浇筑满标志位 + self._is_finish=False + + #用于保存diff标志位 + # self._is_diff_save=False + #用于判断当前判断是否对齐(diff) + self._is_diff_unaligned=False + + #浇筑完成比例(重量) + self._is_finish_ratio=0 + + #下料阶段,汇总时用枚举 + self._is_feed_stage=0 + #振动相关参数 + self._last_arch_one_weight=0 + self._last_arch_two_weight=0 + self._last_arch_three_weight=0 + self._last_arch_four_weight=0 + self._last_arch_five_weight=0 + self._last_arch_time=0 + #是否为F块 + self._is_small_f=None + #采集数据用,下料重量=之前下的重量+最后一阶段(下-->模具车)重量差 + #记录最后一次下料斗到模具车前的重量 + self._finish_weight=0 + + #记录最后一次下料斗到车初始重量 + self._inital_finish_lweight=0 + #记录视觉停止下料时的重量(计算后面加了多少) + self._last_lower_weight=0 + + # 初始化控制间隔和堆料状态跟踪属性 + self._last_overflow_state = False + self._last_control_time = 0 + self._is_running=True + self._initialized = True + self.plc_data=None + + def angle_visual_callback(self, current_angle, overflow_detected, mould_aligned): + """ + 视觉控制主逻辑,供外部推送数据 + 使用单个持续运行的线程,通过参数设置传递数据 + 如果线程正在处理数据,则丢弃此次推送 + """ + #print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 收到推送数据") + # 尝试获取处理锁,若失败则说明正在处理,丢弃数据 + if not self._is_processing.acquire(blocking=False): + print("回调线程仍在执行,丢弃此次推送数据") + return + + try: + # 更新参数 + + if overflow_detected is not None: + # print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 收到溢料:{overflow_detected}") + self._overflow_detected = overflow_detected + if current_angle is not None: + #print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 收到角度:{current_angle}") + self._current_angle = current_angle + if mould_aligned is not None: + #print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 收到对齐:{mould_aligned}") + self._mould_accept_aligned=mould_aligned + # 通知线程有新数据可用 + self._new_data_available.set() + finally: + # 释放处理锁 + self._is_processing.release() + + def diff_visual_callback(self, current_diff,current_area): + """ + 视觉模型diff回调 + """ + #print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 收到推送数据") + # 尝试获取处理锁,若失败则说明正在处理,丢弃数据 + if not self._is_processing_diff.acquire(blocking=False): + print("222回调线程仍在执行,丢弃此次推送数据") + return + + try: + # 更新参数 + if current_diff is not None: + # print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 收到diff:{current_diff}") + self._current_diff = current_diff + if current_area is not None: + # print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 收到area:{current_area}") + self._current_diff_area = current_area + # 通知线程有新数据可用 + self._new_data_diff.set() + finally: + # 释放处理锁 + self._is_processing_diff.release() + + def _diff_temp(self): + """ + 接受视觉回调数据 + 线程主循环,持续运行 + 等待新数据,然后调用处理方法 + """ + _temp_diff_count=0 + _temp_area_count=0 + _temp_diff_str2='' + _temp_area_str2='' + while not self._stop_event.is_set(): + # print('-----等待diff 数据------') + # 等待新数据可用 + self._new_data_diff.wait() + # 重置事件 + self._new_data_diff.clear() + #_is_diff_save是否完成此片 + if self._is_diff_save: + # print('-----进入diff 数据------') + #完成了此片,然后是对齐状态 + if not self._is_diff_unaligned: + # 处理数据 + # print('-----进入对齐数据------') + if self._current_diff is not None and self._current_diff_area is not None: + _timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if _temp_diff_count<=10 and self._current_diff!=0: + _temp_diff_str=f"diff , {_timestamp} , {self._current_diff}\n" + _temp_diff_count+=1 + with open('weight.txt', 'a') as f: + f.write(_temp_diff_str+'\n') + # print('-----保存成功(diff 数据)------') + + if _temp_area_count<=10 and self._current_diff_area!=[]: + _temp_area_str=f"area , {_timestamp} , {str(self._current_diff_area)}\n" + _temp_area_count+=1 + with open('weight.txt', 'a') as f: + f.write(_temp_area_str+'\n') + # print('-----保存成功(area 数据)------') + if _temp_diff_count>=10 and _temp_area_count>=10: + self._is_diff_save=False + time.sleep(1) + continue + # else: + #变成了未对齐,拉起盖板后,重新计数 + # if _temp_diff_count>=10 and _temp_area_count>=10: + # _temp_diff_count=0 + # _temp_area_count=0 + # self._is_diff_save=False + # _temp_diff_str='' + # _temp_area_str='' + + self._current_diff=0 + self._current_diff_area=[] + _temp_diff_count=0 + _temp_area_count=0 + _temp_diff_str='' + _temp_area_str='' + time.sleep(1) + + def _monitor_loop(self): + """监控循环""" + while self._is_running: + try: + current_time = time.time() + # 检查下料斗破拱(只有在下料过程中才检查) + if self._is_feed_stage==1: # 下料斗--》模具车 + _arch_weight = self.transmitter_controller.read_data(2) + if _arch_weight is not None and _arch_weight>0: + # 检查重量变化是否过慢 + _weight_changed=abs(_arch_weight - self._last_arch_one_weight) + #_last_arch_one_weight默认为0,一开始就进入振动 + print(f'---------------第一阶段,重量变化:{_weight_changed}------------------') + if (_weight_changed< 200) and \ + (current_time - self._last_arch_time) >= 2: + self._last_arch_time = current_time + print('---------------------第一阶段振动5秒(小于200KG)-----------------') + self.relay_controller.control_arch_lower_open_sync(5) + self._last_arch_one_weight = _arch_weight + continue + self._last_arch_one_weight = _arch_weight + + elif self._is_feed_stage==2: #上料斗到下料斗,料多,谨慎振动 + _arch_weight = self.transmitter_controller.read_data(1) + if _arch_weight is not None and _arch_weight>0: + # 检查重量变化是否过慢 + _weight_changed=abs(_arch_weight - self._last_arch_two_weight) + print(f'---------------第二阶段,重量变化:{_weight_changed}------------------') + if (_weight_changed < 100) and \ + (current_time - self._last_arch_time) >= 2: + self._last_arch_time = current_time + print('---------------------第二阶段振动3秒-----------------') + self.relay_controller.control_arch_upper_open_sync(3) + self._last_arch_two_weight = _arch_weight + continue + self._last_arch_two_weight = _arch_weight + elif self._is_feed_stage==3: #第二次下料斗-》模具车 + _arch_weight = self.transmitter_controller.read_data(2) + if _arch_weight is not None and _arch_weight>0: + #刚开始不需要振动,料太多 + if self._last_arch_three_weight>0: + _weight_changed=abs(_arch_weight - self._last_arch_three_weight) + # 检查重量变化是否过慢 + print(f'---------------第三阶段,重量变化:{_weight_changed}------------------') + if (_weight_changed < 100) and \ + (current_time - self._last_arch_time) >= 2: + self._last_arch_time = current_time + print('---------------------第三阶段振动5秒(小于100KG)-----------------') + self.relay_controller.control_arch_lower_open_sync(5) + self._last_arch_three_weight = _arch_weight + continue + self._last_arch_three_weight = _arch_weight + + elif self._is_feed_stage==4: #上料斗--》下料斗 + _arch_weight = self.transmitter_controller.read_data(1) + if _arch_weight is not None and _arch_weight>0: + # 检查重量变化是否过慢 + _weight_changed=abs(_arch_weight - self._last_arch_four_weight) + print(f'---------------第二阶段,重量变化:{_weight_changed}------------------') + if (_weight_changed < 200) and \ + (current_time - self._last_arch_time) > 2: + self._last_arch_time = current_time + print('---------------------第四阶段振动5秒-----------------') + self.relay_controller.control_arch_upper_open_sync(5) + self._last_arch_four_weight = _arch_weight + continue + self._last_arch_four_weight = _arch_weight + elif self._is_feed_stage==5: #下料斗->模具车 + _arch_weight = self.transmitter_controller.read_data(2) + if _arch_weight is not None and _arch_weight>0: + if self._last_arch_five_weight>0: + _weight_changed=abs(_arch_weight - self._last_arch_five_weight) + print(f'---------------第五阶段,重量变化:{_weight_changed}------------------') + _min_arch_weight=20 + if self._is_finish_ratio= 2: + self._last_arch_time = current_time + print(f'---------------------第五阶段振动3秒(小于{_min_arch_weight}kg))-----------------') + self.relay_controller.control_arch_lower_open_sync(3) + self._last_arch_five_weight = _arch_weight + continue + self._last_arch_five_weight = _arch_weight + + # 更新最后读取时间 + self._last_arch_time = current_time + time.sleep(2) + except Exception as e: + print(f"监控线程错误: {e}") + + def _aligned_get_times(self,flag): + """ + 获取对齐,1为对齐,0为未对齐 + """ + _current_times=time.time() + _temp_aligned_count=0 + if flag==1: + while time.time()-_current_times<=2: + print(f'-------------{self._mould_accept_aligned}-----------------') + if self._mould_accept_aligned=='盖板对齐': + _temp_aligned_count=_temp_aligned_count+1 + else: + _temp_aligned_count=0 + print(f'-------------{datetime.now().strftime("%H:%M:%S")} 盖板对齐,次数:{_temp_aligned_count}-----------------') + time.sleep(0.2) + self._mould_accept_aligned='' + if _temp_aligned_count>=8: + return True + else: + return False + elif flag==2: + while time.time()-_current_times<=5: + + if self._mould_accept_aligned=='盖板未对齐': + _temp_aligned_count=_temp_aligned_count+1 + else: + _temp_aligned_count=0 + print(f'-------------{datetime.now().strftime("%H:%M:%S")} 盖板未对齐,次数:{_temp_aligned_count}-----------------') + + time.sleep(0.2) + + self._mould_accept_aligned='' + if _temp_aligned_count>=20: + self._is_diff_unaligned=True + return True + else: + self._is_diff_unaligned=False + return False + + def _no_aligned_diff(self): + """ + diff 未对齐检测 + """ + _current_times=time.time() + _temp_aligned_count=0 + while time.time()-_current_times<=1: + # print(f'-------------{self._mould_accept_aligned}-----------------') + if self._mould_accept_aligned=='盖板未对齐': + _temp_aligned_count=_temp_aligned_count+1 + else: + _temp_aligned_count=0 + # print(f'-------------{datetime.now().strftime("%H:%M:%S")} 盖板对齐,次数:{_temp_aligned_count}-----------------') + time.sleep(0.2) + if _temp_aligned_count>=3: + return True + else: + return False + + + def _run_thread_loop(self): + """ + 接受视觉回调数据 + 线程主循环,持续运行 + 等待新数据,然后调用处理方法 + """ + while not self._stop_event.is_set(): + # 等待新数据可用 + self._new_data_available.wait() + + # 重置事件 + self._new_data_available.clear() + + # 获取当前参数(使用临时变量避免被其他线程修改) + current_angle = self._current_angle + overflow_detected = self._overflow_detected + self._is_feed_start=True + + if self.is_start_visual: + # 处理数据 + self._process_angle_callback(current_angle, overflow_detected) + time.sleep(0.1) + + def _run_feed(self): + + while True: + # print("------------已启动----------------") + if self._is_feed_start: + + # if self.plc_data==5: + #_is_finish_ratio完成 比例,根据重量过滤一下 + + if self._is_first_module and self._overflow_detected=='未堆料': + #第一次打开 ,未堆料,检测对齐 + _is_aligned=self._aligned_get_times(1) + if _is_aligned: + print('------------进入第一块111111-------------') + self._is_first_module=False + self._mould_before_aligned=True + # self.is_start_visual=True + self.run_feed_all() + elif self._is_finish and self._is_finish_ratio>=0.7: + #后续流程--》检查到未对齐,--》后又对齐+未堆料 + print('------------------进入连续块检测------------------') + if self._mould_before_aligned: + #未对齐,检测对齐 + _is_not_aligned=self._aligned_get_times(2) + if _is_not_aligned: + #标志位 + self._mould_before_aligned=False + print('------------连续盖板未对齐-------------') + else: + _is_aligned=self._aligned_get_times(1) + if _is_aligned and self._overflow_detected=='未堆料': + print('------------连续盖板已对齐-------------') + self._mould_before_aligned=True + print('-----------进入连续块111111-----------') + # self.is_start_visual=True + if self._last_lower_weight>0: + _current_weight=self.transmitter_controller.read_data(2) + if _current_weight is not None: + with open('weight.txt', 'a') as f: + f.write(f"补料:{self._last_lower_weight-_current_weight}\n\n"+"="*32) + self.init_val() + self.run_feed_all() + + # else: + # print("-----------上料斗未就位----------------") + # print("---------3--上料斗未就位----------------") + + time.sleep(0.2) + + def safe_control_lower_close(self,duration=3): + """线程安全的下料斗关闭方法""" + thread_name = threading.current_thread().name + # print(f"[{thread_name}] 尝试关闭下料斗...") + # 设置标志位,指示正在执行安全关闭操作 + self._is_safe_closing = True + try: + with self._door_control_lock: + self._current_controlling_thread = thread_name + #print(f"[{thread_name}] 获得下料斗控制权,执行关闭操作") + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') + time.sleep(duration) + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self._current_controlling_thread = None + #print(f"[{thread_name}] 释放下料斗控制权") + finally: + # 无论成功失败,都要重置标志位 + self._is_safe_closing = False + + def close_lower_door_visual(self): + """关闭下料斗门""" + self.is_start_visual=False + #休眠让视觉先结束 + time.sleep(0.5) + self.safe_control_lower_close() + + def _visual_close(self): + self.is_start_visual=False + self._is_finish=True + self._is_feed_stage=0 + print(f'--------进入关闭(浇筑满)-----------') + self.safe_control_lower_close(3) + print(f'--------关闭完成-----------') + # try: + # self.db_queue.put_nowait({ + # "f":self._is_small_f, + # "Status": 3 + # }) + # except queue.Full: + # print("数据库队列已满,无法添加数据") + #记录重量 + _current_weight=self.transmitter_controller.read_data(2) + if _current_weight is not None: + self._last_lower_weight=_current_weight + self._finish_weight= self._finish_weight+(self._inital_finish_lweight-_current_weight) + with open('weight.txt', 'a') as f: + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if self._is_small_f: + f.write(f"{self._time_mould_begin},{timestamp},F,{self._finish_weight}\n") + else: + f.write(f"{self._time_mould_begin},{timestamp},B,{self._finish_weight}\n") + #开启保存diff + self._is_diff_save=True + + #保存图片 + save_camera_picture() + + def run_feed_all(self): + """ + 全流程下料:包括判断模具类型 + """ + _is_f= run_stable_classification_loop() + print(f'------------已判断出模具类型: {_is_f}-------------') + if _is_f is not None: + if _is_f=='模具车1': + self._is_small_f=True + print('-------------F块模具--------------') + print('-------------F块模具--------------') + print('-------------F块模具--------------') + self.run_feed_f() + elif _is_f=='模具车2': + self._is_small_f=False + self.run_feed() + print('-------------其他模具---------------') + + if self._is_small_f is None: + print('-----------未判断出模具类型--------------') + return + + def run_feed_f(self): + """第一阶段下料:下料斗向模具车下料(低速)""" + print("--------------------开始下料(F块)--------------------") + self._time_mould_begin=datetime.now().strftime("%Y-%m-%d %H:%M:%S") + # loc_relay=self.relay_controller + loc_mitter=self.transmitter_controller + max_weight_none=5 + cur_weight_none=0 + initial_lower_weight=loc_mitter.read_data(2) + if initial_lower_weight is None: + print("-----f上料斗重量异常-----") + return + first_finish_weight=0 + self._finish_weight=first_finish_weight + self._inital_finish_lweight=initial_lower_weight + need_total_weight=0.54*2416 + if initial_lower_weight>100: + if not self._is_finish: + self.is_start_visual=True + initial_lower_weight=loc_mitter.read_data(2) + if initial_lower_weight is None: + print("-----f上料斗重量异常2-----") + return + self._is_feed_stage=5 + while not self._is_finish: + current_weight = loc_mitter.read_data(2) + if current_weight is None: + cur_weight_none+=1 + if cur_weight_none>max_weight_none: + #如果重量连续5次为None,认为下料斗未就位,跳出循环 + print('------------f下到模具车,下料斗重量异常----------------') + print('------------f下到模具车,下料斗重量异常----------------') + self.close_lower_door_visual() + return + #视觉处理关闭,异常的话重量没有生效 + continue + cur_weight_none=0 + first_finish_weight=initial_lower_weight-current_weight + self._is_finish_ratio=(first_finish_weight)/need_total_weight + print(f'------------已下料比例: {self._is_finish_ratio}-------------') + if self._is_finish_ratio>self._max_f_angle_ratio: + #关5秒 + #大于0.7后不再检测了,直接交给视觉控制夹脚 + # print(f'------------已下料比例: {self._is_finish_ratio}-------------') + break + + # print(f'------------已下料: {first_finish_weight+second_finish_weight}kg-------------') + time.sleep(1) + # initial_lower_weight=_current_lower_weight + print(f'------------已下料(F): {first_finish_weight}kg-------------') + print(f'------------已下料(F): {first_finish_weight}kg-------------') + + print(f'------------已完成-------------') + + def run_feed(self): + """第一阶段下料:下料斗向模具车下料(低速)""" + print("--------------------开始下料(普通块)--------------------") + self._time_mould_begin=datetime.now().strftime("%Y-%m-%d %H:%M:%S") + loc_relay=self.relay_controller + loc_mitter=self.transmitter_controller + max_weight_none=5 + cur_weight_none=0 + + initial_lower_weight=loc_mitter.read_data(2) + # initial_upper_weight=loc_mitter.read_data(1) + if initial_lower_weight is None: + print("---------------下料斗重量异常----------------") + return + first_finish_weight=0 + need_total_weight=1.91*2416 + # start_time=None + self.is_start_visual=True + if initial_lower_weight>100: + #下料斗的料全部下完 + self._is_feed_stage=1 + while not self._is_finish: + current_weight = loc_mitter.read_data(2) + if current_weight is None: + cur_weight_none+=1 + if cur_weight_none>max_weight_none: + print("-----------下料斗重量异常(第一次下到模具车)--------------") + self.close_lower_door_visual() + return + continue + cur_weight_none=0 + if current_weight<250 and current_weight>0: + self.close_lower_door_visual() + break + time.sleep(1) + _current_lower_weight=loc_mitter.read_data(2) + if _current_lower_weight is None: + print("-------下料斗重量异常---------") + return + first_finish_weight=initial_lower_weight-_current_lower_weight + # initial_lower_weight=_current_lower_weight + print(f'------------已下料(第一次): {first_finish_weight}kg-------------') + print(f'------------已下料(第一次): {first_finish_weight}kg-------------') + self._is_feed_stage=0 + + while self.plc_data!=5: + print('------------上料斗未就位----------------') + print('------------上料斗未就位----------------') + time.sleep(1) + + if self.plc_data==5: + print(f'------------上料斗向下料斗转移(留3000KG)-------------') + #打开上料斗出砼门,开5就,开三分之一下 + + loc_relay.control_upper_open_sync(6) + self._is_feed_stage=2 + loc_time_count=1 + upper_open_time=time.time() + + while not self._is_finish: + current_upper_weight = loc_mitter.read_data(1) + if current_upper_weight is None: + cur_weight_none+=1 + if cur_weight_none>max_weight_none: + #如果重量连续5次为None,认为上料斗未就位,跳出循环 + print('------------第一次上到下,上料斗重量异常----------------') + print('------------第一次上到下,上料斗重量异常----------------') + loc_relay.control_upper_close_sync(5+loc_time_count) + return + continue + cur_weight_none=0 + _two_lower_weight=loc_mitter.read_data(2) + if _two_lower_weight is None: + _two_lower_weight=0 + if (current_upper_weight<3000 and current_upper_weight>0) or _two_lower_weight>3200: + #关5秒,loc_time_count多关一秒 + loc_relay.control_upper_close_sync(5+loc_time_count) + break + else: + if time.time()-upper_open_time>5: + if loc_time_count<6: + upper_open_time=time.time() + loc_relay.control_upper_open_sync(0.8) + loc_time_count=loc_time_count+0.8 + else: + time.sleep(0.5) + else: + loc_relay.control_upper_close_sync(6+loc_time_count) + + self.is_start_visual=True + initial_lower_weight=loc_mitter.read_data(2) + if initial_lower_weight is None: + print("-------下料斗重量异常(第二次下料到模具车)---------") + return + self._is_feed_stage=3 + while not self._is_finish: + current_weight = loc_mitter.read_data(2) + if current_weight is None: + cur_weight_none+=1 + if cur_weight_none>max_weight_none: + print("-------下料斗重量异常(第二次下料到模具车)---------") + self.close_lower_door_visual() + return + continue + cur_weight_none=0 + # second_finish_weight=initial_lower_weight-current_weight + if current_weight<250: + self.close_lower_door_visual() + break + # print(f'------------已下料: {first_finish_weight+second_finish_weight}kg-------------') + time.sleep(1) + _current_lower_weight=loc_mitter.read_data(2) + if _current_lower_weight is None: + print("-------下料斗重量异常(第二次下到模)---------") + return + first_finish_weight=first_finish_weight+initial_lower_weight-_current_lower_weight + print(f'------------已下料(第二次): {first_finish_weight}kg-------------') + print(f'------------已下料(第二次): {first_finish_weight}kg-------------') + + self._is_feed_stage=0 + if self.plc_data==5: + #第二次上料斗向下料斗转移 + loc_relay.control_upper_open_sync(12) + loc_time_count=1 + upper_open_time=time.time() + upper_open_time_2=None + #第二次到下料斗还需要的量 + #loc_left_need_weight=need_total_weight-first_finish_weight + # initial_upper_weight=loc_mitter.read_data(1) + # start_time=None + self._is_feed_stage=4 + while not self._is_finish: + # print(f'------------上料斗向下料斗转移22222-------------') + current_upper_weight = loc_mitter.read_data(1) + if current_upper_weight is None: + cur_weight_none+=1 + if cur_weight_none>max_weight_none: + #如果重量连续5次为None,认为上料斗未就位,跳出循环 + print('------------第二次上到下,上料斗重量异常----------------') + print('------------第二次上到下,上料斗重量异常----------------') + loc_relay.control_upper_close_sync(15) + break + continue + cur_weight_none=0 + if (current_upper_weight<600 and current_upper_weight>0) or upper_open_time_2 is not None: + if upper_open_time_2 is None: + upper_open_time_2=time.time() + if current_upper_weight<400 or time.time()-upper_open_time_2>5: + loc_relay.control_arch_upper_open_async(5) + # loc_relay.control_arch_upper_open() + loc_relay.control_upper_open_sync(5) + # start_time=None + #5秒后关闭 + loc_relay.control_upper_close_after()#control_upper_close_sync(8+loc_time_count) + break + time.sleep(1) + else: + if time.time()-upper_open_time>2: + # if loc_time_count<6: + upper_open_time=time.time() + loc_relay.control_upper_open_sync(1.2) + loc_time_count=loc_time_count+1 + else: + time.sleep(0.5) + else: + loc_relay.control_upper_close_sync(15) + # time.sleep(0.4) + + #第三次下料斗转移到模具车 + if not self._is_finish: + self.is_start_visual=True + initial_lower_weight=loc_mitter.read_data(2) + self._finish_weight=first_finish_weight + self._inital_finish_lweight=initial_lower_weight + if initial_lower_weight is None: + print("-------下料斗重量异常(第三次下到模具车)---------") + return + self._is_feed_stage=5 + while not self._is_finish: + current_weight = loc_mitter.read_data(2) + if current_weight is None: + cur_weight_none+=1 + if cur_weight_none>max_weight_none: + #重量异常退出 + print('------------第三次下到模具车,下料斗重量异常----------------') + self.close_lower_door_visual() + return + continue + cur_weight_none=0 + second_finish_weight=initial_lower_weight-current_weight + self._is_finish_ratio=(second_finish_weight+first_finish_weight)/need_total_weight + print(f'------------已下料比例: {self._is_finish_ratio}-------------') + if self._is_finish_ratio>=1: + #关5秒 + # print(f'------------已下料比例: {self._is_finish_ratio}-------------') + break + + # print(f'------------已下料: {first_finish_weight+second_finish_weight}kg-------------') + time.sleep(1) + + # _current_lower_weight=loc_mitter.read_data(2) + # first_finish_weight=first_finish_weight+initial_lower_weight-_current_lower_weight + # print(f'------------已下料: {first_finish_weight}kg-------------') + # print(f'------------已下料: {first_finish_weight}kg-------------') + + + print(f'------------已完成-------------') + + def _process_angle_callback(self, current_angle, overflow_detected): + """ + 实时精细控制 - 基于PID思想,无固定间隔 + """ + try: + # 记录控制时间戳(用于微分计算,而非限制) + current_time = time.time() + # 确保所有PID相关属性都被正确初始化 + if not hasattr(self, '_last_control_time'): + self._last_control_time = current_time + if not hasattr(self, '_last_error'): + self._last_error = 0 + if not hasattr(self, '_error_integral'): + self._error_integral = 0 + + # print(f"{self.angle_mode}") + self.overflow = overflow_detected in ["大堆料", "小堆料"] + + if current_angle is None: + return + + print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 角度11: {current_angle:.2f}°,{overflow_detected}") + + if overflow_detected == "未浇筑满" or self._is_before_finish: + if self._before_finish_time is None: + self._before_finish_time=current_time + self.safe_control_lower_close(3) + print('-----------------关闭(未浇筑满)--------------------') + # time.sleep(3) + else: + if overflow_detected=='浇筑满': + self._visual_close() + return + # print(f'--------已关闭已关闭-----------') + elif overflow_detected=="大堆料": + print(f'--------未浇筑满,大堆料-----------') + self._pulse_control('open',0.3) + time.sleep(0.3) + self._pulse_control('close',0.4) + print(f'--------比例:{self._is_finish_ratio}-----------') + if self._is_finish_ratio>= self._weight_ratio_955: + time.sleep(2) + else: + time.sleep(1) + self._is_before_finish=True + else: + # self._pulse_control('open',0.5) + # time.sleep(0.3) + # self._pulse_control('close',0.6) + # print(f'--------比例:{self._is_finish_ratio}-----------') + self._pulse_control('open',0.6) + time.sleep(0.3) + self._pulse_control('close',0.7) + if self._is_finish_ratio>= self._weight_ratio_955: + time.sleep(2) + else: + time.sleep(1) + self._is_before_finish=True + if self._is_finish_ratio<=self._max_ignore_radio: + #如果重量未达到最大忽略角度,需要跳出 + self._is_before_finish=False + return + elif overflow_detected == "浇筑满": + self._visual_close() + return + else: + self._before_finish_time=None + if self._is_finish_ratio>=self._max_angle_radio or (self._is_finish_ratio>self._max_f_angle_ratio and self._is_small_f): + if overflow_detected == "大堆料": + TARGET_ANGLE = 5.0 # 大堆料时控制在15度左右 + elif overflow_detected == "小堆料": + TARGET_ANGLE = 15.0 # 小堆料时控制在35度左右 + else: + TARGET_ANGLE = 35.0 # 12.25由25--》35 + else: + if self._is_feed_stage==1 or self._is_feed_stage==3: + #根据溢料状态动态调整目标角度 + if overflow_detected == "大堆料": + TARGET_ANGLE = 15.0 # 大堆料时控制在15度左右 + elif overflow_detected == "小堆料": + TARGET_ANGLE = 45.0 # 小堆料时控制在35度左右 + else: + TARGET_ANGLE = 55.0 # 未溢料时开到最大56度 + else: + #根据溢料状态动态调整目标角度 + if overflow_detected == "大堆料": + TARGET_ANGLE = 15.0 # 大堆料时控制在15度左右 + elif overflow_detected == "小堆料": + TARGET_ANGLE = 25.0 # 小堆料时控制在35度左右 + else: + TARGET_ANGLE = 45.0 # 未溢料时开到最大56度 + + # 确保目标角度在硬件范围内(5-56度) + TARGET_ANGLE = max(5.0, min(56.0, TARGET_ANGLE)) + + # PID控制参数 + KP = 0.2 # 比例系数 + KI = 0 # 积分系数 + KD = 0 # 微分系数 + # KP = 0.15 # 比例系数 + # KI = 0.008 # 积分系数 + # KD = 0.08 # 微分系数 + # if TARGET_ANGLE <= 25.0: + # KP, KI, KD = 0.18, 0.008, 0.08 # 小角度,强控制 + # elif TARGET_ANGLE <= 40.0: + # KP, KI, KD = 0.15, 0.01, 0.06 # 中角度 + # else: + # KP, KI, KD = 0.12, 0.012, 0.04 # 大角度,温和控制 + # 计算误差 + error = current_angle - TARGET_ANGLE + dt = current_time - self._last_control_time + + # 积分项(抗饱和) + self._error_integral += error * dt + self._error_integral = max(min(self._error_integral, 50), -50) # 积分限幅 + + # 微分项 + error_derivative = (error - self._last_error) / dt if dt > 0 else 0 + + # PID输出 + pid_output = (KP * error + KI * self._error_integral + KD * error_derivative) + print(f"📊 PID计算: 误差={error:.2f}°, 积分={self._error_integral:.2f}, " + f"微分={error_derivative:.2f}, 输出={pid_output:.2f}") + # 更新历史值 + self._last_error = error + self._last_control_time = current_time + + # 状态机 + PID控制 + + if self.angle_mode == "normal": + self._normal_mode_advanced(current_angle, pid_output,TARGET_ANGLE) + + elif self.angle_mode == "reducing": + self._reducing_mode_advanced(current_angle, pid_output, TARGET_ANGLE) + + elif self.angle_mode == "maintaining": + self._maintaining_mode_advanced(current_angle, pid_output, TARGET_ANGLE) + + except Exception as e: + print(f"处理视觉回调时发生异常: {e}") + + def _normal_mode_advanced(self, current_angle, pid_output,target_angle): + """高级正常模式控制""" + if self.overflow: + self.angle_mode = "reducing" + print("检测到溢料,切换到减小模式") + return + + # 🎯 修复1: 添加强制控制机制 + + + # 基于PID输出的智能控制 + control_threshold = 2 # 从2.0减小到0.5,提高灵敏度 + + if abs(pid_output) > control_threshold: + if pid_output > 0: + # 需要减小角度(关门) + pulse_time = min(0.3, pid_output * 0.1) + self._pulse_control("close", pulse_time) + print(f"正常模式: 角度偏高{pid_output:.1f},关门{pulse_time:.2f}秒") + else: + # 需要增大角度(开门) + pulse_time = min(0.3, abs(pid_output) * 0.1) + self._pulse_control("open", pulse_time) + print(f"正常模式: 角度偏低{abs(pid_output):.1f},开门{pulse_time:.2f}秒") + else: + # 在死区内,保持静止 + error = current_angle - target_angle + abs_error = abs(error) + + # 强制控制:如果误差超过5度,强制控制 + if abs_error > 5: + if error > 0: # 当前角度 > 目标角度,需要关门 + pulse_time=0.1 # 根据误差计算脉冲时间 + self._pulse_control("close", pulse_time) + print(f"🚨 强制关门: 误差{abs_error:.1f}°过大,脉冲{pulse_time:.3f}s") + else: # 当前角度 < 目标角度,需要开门 + pulse_time =0.1 + self._pulse_control("open", pulse_time) + print(f"🚨 强制开门: 误差{abs_error:.1f}°过大,脉冲{pulse_time:.3f}s") + return + else: + self._stop_door() + print(f"正常模式: 角度在目标范围内,保持静止") + + def _reducing_mode_advanced(self, current_angle, pid_output, target_angle): + """高级减小模式控制""" + if not self.overflow: + if current_angle <= target_angle + 5.0: + self.angle_mode = "normal" + print("溢料消除且角度合适,返回正常模式") + else: + # 缓慢恢复 + self._pulse_control("close", 0.1) + return + + # 有溢料,积极减小角度 + if current_angle > target_angle: + # 使用PID输出计算控制量 + pulse_time = min(0.5, max(0.1, pid_output * 0.15)) + self._pulse_control("close", pulse_time) + print(f"减小模式: 积极关门{pulse_time:.2f}秒,PID输出:{pid_output:.1f}") + else: + self.angle_mode = "maintaining" + print("角度已达标,进入维持模式") + + def _maintaining_mode_advanced(self, current_angle, pid_output, target_angle): + """高级维持模式控制""" + if not self.overflow: + self.angle_mode = "normal" + print("溢料消除,返回正常模式") + return + + # 精确维持控制 + dead_zone = 1.5 # 更小的死区 + + if abs(pid_output) > dead_zone: + pulse_time = min(0.2, abs(pid_output) * 0.05) # 更精细的控制 + + if pid_output > 0: + self._pulse_control("close", pulse_time) + print(f"维持模式: 微调关门{pulse_time:.2f}秒") + else: + self._pulse_control("open", pulse_time) + print(f"维持模式: 微调开门{pulse_time:.2f}秒") + else: + self._stop_door() + print("维持模式: 角度精确控制中") + + def _pulse_control(self, action, duration): + """统一的脉冲控制方法""" + # 检查是否正在执行safe_control_lower_close,如果是则跳过relay操作 + if self._is_safe_closing: + thread_name = threading.current_thread().name + print(f"[{thread_name}] safe_control_lower_close正在执行,跳过脉冲控制 {action}") + return + + if duration <= 0: + return + + thread_name = threading.current_thread().name + print(f"[{thread_name}] 尝试脉冲控制 {action},时长 {duration:.2f}秒...") + + with self._door_control_lock: + self._current_controlling_thread = thread_name + print(f"[{thread_name}] 获得下料斗控制权,执行脉冲控制") + + if action == "open": + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'open') + time.sleep(duration) + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + print(f"[{thread_name}] 开门脉冲: {duration:.2f}秒") + else: # close + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') + time.sleep(duration) + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + print(f"[{thread_name}] 关门脉冲: {duration:.2f}秒") + + self._current_controlling_thread = None + print(f"[{thread_name}] 释放下料斗控制权") + + def _stop_door(self): + """停止门运动""" + # 检查是否正在执行safe_control_lower_close,如果是则跳过relay操作 + if self._is_safe_closing: + thread_name = threading.current_thread().name + print(f"[{thread_name}] safe_control_lower_close正在执行,跳过停止门运动操作") + return + + thread_name = threading.current_thread().name + print(f"[{thread_name}] 尝试停止门运动...") + + with self._door_control_lock: + self._current_controlling_thread = thread_name + print(f"[{thread_name}] 获得下料斗控制权,执行停止操作") + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self._current_controlling_thread = None + print(f"[{thread_name}] 释放下料斗控制权") + + def _open_door(self, duration=0.5): + """打开门""" + self._pulse_control("open", 0.3) + + def _close_door(self, duration=0.5): + """关闭门""" + self._pulse_control("close", 1) + + def on_plc_update(self,data: int, binary: str): + #4即将振捣室5振捣室 64即将搅拌楼 66到达搅拌楼 + # print(f"[数据回调] 数值: 0x{data:02X} | 十进制: {data:3d} | 二进制: {binary}") + self.plc_data=data + + @classmethod + def instance_exists(cls): + """检测实例是否存在""" + return cls._instance is not None + + def shutdown(self): + """关闭线程,清理资源""" + # 设置停止事件 + self._stop_event.set() + # 唤醒线程以便它能检测到停止事件 + self._new_data_available.set() + + self._is_running=False + self._is_finish=True + self.is_start_visual=False + # #关闭下料斗 + # self.safe_control_lower_close() + if self.plc_service: + self.plc_service.stop_polling() + # 等待线程结束 + if self.callback_thread.is_alive(): + self.callback_thread.join(timeout=1.0) + + if self.feed_thread.is_alive(): + self.feed_thread.join(timeout=1.0) + + if self.monitor_thread.is_alive(): + self.monitor_thread.join(timeout=1.0) + + # self.relay_controller._close_lower_5s + + def __del__(self): + """析构函数,确保线程安全关闭""" + self.shutdown() + +# 创建默认实例 +# visual_callback_instance = VisualCallback() + +# 兼容层,保持原来的函数调用方式可用 +# def angle_visual_callback(current_angle, overflow_detected): +# """ +# 兼容旧版本的函数调用方式 +# 将调用转发到默认实例的angle_visual_callback方法 +# """ +# visual_callback_instance.angle_visual_callback(current_angle, overflow_detected) \ No newline at end of file diff --git a/vision/visual_callback_dq.py b/vision/visual_callback_dq.py index 32d7276..299ecf4 100644 --- a/vision/visual_callback_dq.py +++ b/vision/visual_callback_dq.py @@ -1,5 +1,6 @@ from pickle import FALSE +from re import S from cv2.gapi import ov from config.settings import app_set_config from hardware.relay import RelayController @@ -11,20 +12,28 @@ import logging import queue from hardware.upper_plc import OmronFinsPollingService from vision.muju_cls.muju_utils import run_stable_classification_loop +from vision.camera_picture import save_camera_picture +from busisness.blls import ArtifactBll,PDRecordBll +from busisness.models import ArtifactInfoModel,PDRecordModel +from service.mould_service import app_web_service +from core.system_state import SystemState,FeedStatus +from dataclasses import asdict +import json +import math 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 __new__(cls,*args, **kwargs): + # """检测实例是否存在,实现单例模式""" + # with cls._lock: + # if cls._instance is None: + # cls._instance = super().__new__(cls) + # return cls._instance - def __init__(self): + def __init__(self,state:SystemState=None): """初始化视觉回调处理器""" # 避免重复初始化 if hasattr(self, '_initialized') and self._initialized: @@ -32,10 +41,21 @@ class VisualCallback: self.relay_controller = RelayController() self.transmitter_controller = TransmitterController(self.relay_controller) + self.pd_record_bll=PDRecordBll() + self.state=state # 线程安全的参数传递 self._new_data_available = threading.Event() self._is_processing = threading.Lock() + + #diff参数 + self._is_processing_diff = threading.Lock() + self._new_data_diff = threading.Event() + self._current_diff=0 + self._current_diff_area=[] + self._is_diff_save=False + + self._stop_event = threading.Event() # 添加下料斗门控制锁,防止两个线程同时控制 @@ -44,6 +64,7 @@ class VisualCallback: self._current_controlling_thread = None #是否启动后的第一个模具 self._is_first_module=True + self.init_val() # self._setup_logging_2() #F块完成重量的70%,控制夹脚,F块多于这个比例就没有记录了(注意) @@ -54,7 +75,7 @@ class VisualCallback: #重量大于95%,停留时间2秒,其他的1秒 self._weight_ratio_955=0.955 #完成多少,忽略未浇筑满 - self._max_ignore_radio=0.5 + self._max_ignore_radio=0.8 self._mould_accept_aligned=None self._mould_before_aligned=False @@ -62,7 +83,8 @@ class VisualCallback: self._time_mould_begin='' #模具结束浇筑时间 self._time_mould_end='' - + #记录当前模具信息model + self._cur_mould_model=None # self.db_queue=queue.Queue() # self.plc_data=5 @@ -71,20 +93,21 @@ class VisualCallback: # self.plc_service.register_status_callback(self.on_status_change) self.plc_service.start_polling(interval=2.0) - # 创建并启动单个持续运行的线程 + # 获取视觉数据线程(angle_visual_callback推送的数据),并进行处理,注意处理视觉进行夹角控制 self.callback_thread = threading.Thread( target=self._run_thread_loop, daemon=True ) self.callback_thread.start() + # 启动下料线程检测 self.feed_thread = threading.Thread( target=self._run_feed, daemon=True ) self.feed_thread.start() - """启动系统监控""" + #启动振动线程 self.monitor_thread = threading.Thread( target=self._monitor_loop, daemon=True, @@ -92,6 +115,13 @@ class VisualCallback: ) self.monitor_thread.start() + #获取diff(diff_visual_callback推送的数据)数据线程 + self.diff_thread = threading.Thread( + target=self._diff_temp, + daemon=True + ) + self.diff_thread.start() + """启动数据库监控""" # self.db_thread = threading.Thread( # target=self._monitor_db_loop, @@ -99,9 +129,7 @@ class VisualCallback: # name='db_monitor' # ) # self.db_thread.start() - - - + def init_val(self): #初始化值 """初始化视觉回调处理器""" @@ -122,6 +150,15 @@ class VisualCallback: self._is_before_finish=False #是否浇筑满标志位 self._is_finish=False + + #用于保存diff标志位 + # self._is_diff_save=False + #用于判断当前判断是否对齐(diff) + self._is_diff_unaligned=False + + self._diff_f_val=0 + self._diff_f_area=[] + #浇筑完成比例(重量) self._is_finish_ratio=0 @@ -144,15 +181,23 @@ class VisualCallback: self._inital_finish_lweight=0 #记录视觉停止下料时的重量(计算后面加了多少) self._last_lower_weight=0 + #每片开始下料斗的重量 + self._init_lower_weight=0 # 初始化控制间隔和堆料状态跟踪属性 self._last_overflow_state = False self._last_control_time = 0 self._is_running=True + self._is_stop_one_seconds=False self._initialized = True self.plc_data=None - + self._mould_need_weight=0 + #点动等级 + self._point_speed_grade=0 + self._point_weight=0 + + def angle_visual_callback(self, current_angle, overflow_detected, mould_aligned): """ 视觉控制主逻辑,供外部推送数据 @@ -183,8 +228,93 @@ class VisualCallback: # 释放处理锁 self._is_processing.release() + def diff_visual_callback(self, current_diff,current_area): + """ + 视觉模型diff回调 + """ + #print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 收到推送数据") + # 尝试获取处理锁,若失败则说明正在处理,丢弃数据 + if not self._is_processing_diff.acquire(blocking=False): + print("222回调线程仍在执行,丢弃此次推送数据") + return + + try: + # 更新参数 + if current_diff is not None: + # print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 收到diff:{current_diff}") + self._current_diff = current_diff + self._diff_f_val=current_diff + if current_area is not None: + # print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 收到area:{current_area}") + self._current_diff_area = current_area + self._diff_f_area = current_area + # 通知线程有新数据可用 + self._new_data_diff.set() + finally: + # 释放处理锁 + self._is_processing_diff.release() + + def _diff_temp(self): + """ + 接受视觉回调数据 + 线程主循环,持续运行 + 等待新数据,然后调用处理方法 + """ + _temp_diff_count=0 + _temp_area_count=0 + _temp_diff_str2='' + _temp_area_str2='' + while not self._stop_event.is_set(): + # print('-----等待diff 数据------') + # 等待新数据可用 + self._new_data_diff.wait() + # 重置事件 + self._new_data_diff.clear() + #_is_diff_save是否完成此片 + if self._is_diff_save: + # print('-----进入diff 数据------') + #完成了此片,然后是对齐状态 + if not self._is_diff_unaligned: + # 处理数据 + # print('-----进入对齐数据------') + if self._current_diff is not None and self._current_diff_area is not None: + _timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if _temp_diff_count<=10 and self._current_diff!=0: + _temp_diff_str=f"diff , {_timestamp} , {self._current_diff}\n" + _temp_diff_count+=1 + with open('weight.txt', 'a') as f: + f.write(_temp_diff_str+'\n') + # print('-----保存成功(diff 数据)------') + + if _temp_area_count<=10 and self._current_diff_area!=[]: + _temp_area_str=f"area , {_timestamp} , {str(self._current_diff_area)}\n" + _temp_area_count+=1 + with open('weight.txt', 'a') as f: + f.write(_temp_area_str+'\n') + # print('-----保存成功(area 数据)------') + if _temp_diff_count>=10 and _temp_area_count>=10: + self._is_diff_save=False + time.sleep(1) + continue + # else: + #变成了未对齐,拉起盖板后,重新计数 + # if _temp_diff_count>=10 and _temp_area_count>=10: + # _temp_diff_count=0 + # _temp_area_count=0 + # self._is_diff_save=False + # _temp_diff_str='' + # _temp_area_str='' + + self._current_diff=0 + self._current_diff_area=[] + _temp_diff_count=0 + _temp_area_count=0 + _temp_diff_str='' + _temp_area_str='' + time.sleep(1) + def _monitor_loop(self): - """监控循环""" + """破拱线程""" while self._is_running: try: current_time = time.time() @@ -246,7 +376,8 @@ class VisualCallback: (current_time - self._last_arch_time) > 2: self._last_arch_time = current_time print('---------------------第四阶段振动5秒-----------------') - self.relay_controller.control_arch_upper_open_sync(5) + #重量不准,暂时不振动 + # self.relay_controller.control_arch_upper_open_sync(5) self._last_arch_four_weight = _arch_weight continue self._last_arch_four_weight = _arch_weight @@ -282,12 +413,13 @@ class VisualCallback: _temp_aligned_count=0 if flag==1: while time.time()-_current_times<=2: - print(f'-------------{self._mould_accept_aligned}-----------------') + # print(f'-------------{self._mould_accept_aligned}-----------------') if self._mould_accept_aligned=='盖板对齐': _temp_aligned_count=_temp_aligned_count+1 else: _temp_aligned_count=0 - print(f'-------------{datetime.now().strftime("%H:%M:%S")} 盖板对齐,次数:{_temp_aligned_count}-----------------') + if _temp_aligned_count>0: + print(f'-------------{datetime.now().strftime("%H:%M:%S")} 盖板对齐,次数:{_temp_aligned_count}-----------------') time.sleep(0.2) self._mould_accept_aligned='' if _temp_aligned_count>=8: @@ -301,17 +433,38 @@ class VisualCallback: _temp_aligned_count=_temp_aligned_count+1 else: _temp_aligned_count=0 - print(f'-------------{datetime.now().strftime("%H:%M:%S")} 盖板未对齐,次数:{_temp_aligned_count}-----------------') - + if _temp_aligned_count>0: + print(f'-------------{datetime.now().strftime("%H:%M:%S")} 盖板未对齐,次数:{_temp_aligned_count}-----------------') time.sleep(0.2) self._mould_accept_aligned='' if _temp_aligned_count>=20: + self._is_diff_unaligned=True return True else: + self._is_diff_unaligned=False return False - + def _no_aligned_diff(self): + """ + diff 未对齐检测 + """ + _current_times=time.time() + _temp_aligned_count=0 + while time.time()-_current_times<=1: + # print(f'-------------{self._mould_accept_aligned}-----------------') + if self._mould_accept_aligned=='盖板未对齐': + _temp_aligned_count=_temp_aligned_count+1 + else: + _temp_aligned_count=0 + # print(f'-------------{datetime.now().strftime("%H:%M:%S")} 盖板对齐,次数:{_temp_aligned_count}-----------------') + time.sleep(0.2) + if _temp_aligned_count>=3: + return True + else: + return False + + def _run_thread_loop(self): """ 接受视觉回调数据 @@ -336,7 +489,7 @@ class VisualCallback: time.sleep(0.1) def _run_feed(self): - + _is_api_request=True while True: # print("------------已启动----------------") if self._is_feed_start: @@ -346,38 +499,62 @@ class VisualCallback: if self._is_first_module and self._overflow_detected=='未堆料': #第一次打开 ,未堆料,检测对齐 + if _is_api_request: + self.get_current_mould() + _is_api_request=False _is_aligned=self._aligned_get_times(1) if _is_aligned: - print('------------进入第一块111111-------------') + _is_api_request=True + print('------------启动程序后,进入第一块-------------') self._is_first_module=False self._mould_before_aligned=True + _current_weight=self.transmitter_controller.read_data(2) + if _current_weight: + self._init_lower_weight=_current_weight + else: + print('------------获取上料斗重量失败-------------') + return + + self.state._feed_status=FeedStatus.FCheckGB # self.is_start_visual=True self.run_feed_all() elif self._is_finish and self._is_finish_ratio>=0.7: #后续流程--》检查到未对齐,--》后又对齐+未堆料 - print('------------------进入连续块检测------------------') + #print('------------------进入连续块检测------------------') if self._mould_before_aligned: #未对齐,检测对齐 _is_not_aligned=self._aligned_get_times(2) if _is_not_aligned: #标志位 self._mould_before_aligned=False - print('------------连续盖板未对齐-------------') + #print('------------连续盖板未对齐-------------') else: - _is_aligned=self._aligned_get_times(1) - if _is_aligned and self._overflow_detected=='未堆料': - print('------------连续盖板已对齐-------------') - self._mould_before_aligned=True - print('-----------进入连续块111111-----------') - # self.is_start_visual=True - if self._last_lower_weight>0: - _current_weight=self.transmitter_controller.read_data(2) - if _current_weight is not None: - with open('weight.txt', 'a') as f: - f.write(f"{self._last_lower_weight-_current_weight}\n") - self.init_val() - self.run_feed_all() - + if _is_api_request: + self.get_current_mould() + _is_api_request=False + _is_aligned=self._aligned_get_times(1) + if _is_aligned and self._overflow_detected=='未堆料': + print('------------进入连续生产-------------') + self._mould_before_aligned=True + _is_api_request=True + + _current_weight=self.transmitter_controller.read_data(2) + if not _current_weight: + print('------------获取上料斗重量失败-------------') + return + + + # print('-----------进入连续块111111-----------') + # self.is_start_visual=True + if self._last_lower_weight>0: + with open('weight.txt', 'a') as f: + f.write(f"补料:{self._last_lower_weight-_current_weight}\n\n"+"="*32) + + self.init_val() + self._init_lower_weight=_current_weight + self.state._feed_status=FeedStatus.FCheckGB + self.run_feed_all() + # else: # print("-----------上料斗未就位----------------") # print("---------3--上料斗未就位----------------") @@ -385,38 +562,41 @@ class VisualCallback: time.sleep(0.2) def safe_control_lower_close(self,duration=3): - """线程安全的下料斗关闭方法""" - thread_name = threading.current_thread().name - # print(f"[{thread_name}] 尝试关闭下料斗...") - # 设置标志位,指示正在执行安全关闭操作 - self._is_safe_closing = True - try: - with self._door_control_lock: - self._current_controlling_thread = thread_name - #print(f"[{thread_name}] 获得下料斗控制权,执行关闭操作") - self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') - self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') - time.sleep(duration) - self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') - self._current_controlling_thread = None - #print(f"[{thread_name}] 释放下料斗控制权") - finally: - # 无论成功失败,都要重置标志位 - self._is_safe_closing = False + """线程安全的下料斗关闭方法""" + 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"关闭下料斗{duration}秒") + self.relay_controller.control(self.relay_controller.DOOR_LOWER_OPEN, 'close') + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'open') + time.sleep(duration) + self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') + self._current_controlling_thread = None + #print(f"[{thread_name}] 释放下料斗控制权") + finally: + # 无论成功失败,都要重置标志位 + self._is_safe_closing = False def close_lower_door_visual(self): """关闭下料斗门""" self.is_start_visual=False + #休眠让视觉先结束 time.sleep(0.5) self.safe_control_lower_close() def _visual_close(self): self.is_start_visual=False self._is_finish=True + self.state.vf_status=3 + self.state._feed_status=FeedStatus.FFinished self._is_feed_stage=0 print(f'--------进入关闭(浇筑满)-----------') self.safe_control_lower_close(3) - print(f'--------关闭完成-----------') + print(f'--------浇筑完成-----------') # try: # self.db_queue.put_nowait({ # "f":self._is_small_f, @@ -432,10 +612,20 @@ class VisualCallback: with open('weight.txt', 'a') as f: timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") if self._is_small_f: - f.write(f"{self._time_mould_begin},{timestamp},F,{self._finish_weight}\n") + if self._cur_mould_model: + f.write(f"{self._time_mould_begin},{timestamp},{self._cur_mould_model.MouldCode},F,{self._finish_weight}\n") + else: + f.write(f"{self._time_mould_begin},{timestamp},F,{self._finish_weight}\n") else: - f.write(f"{self._time_mould_begin},{timestamp},B,{self._finish_weight}\n") - + if self._cur_mould_model: + f.write(f"{self._time_mould_begin},{timestamp},{self._cur_mould_model.MouldCode},B,{self._finish_weight}\n") + else: + f.write(f"{self._time_mould_begin},{timestamp},B,{self._finish_weight}\n") + #开启保存diff + self._is_diff_save=True + + #保存图片 + save_camera_picture() def run_feed_all(self): """ @@ -447,13 +637,15 @@ class VisualCallback: if _is_f=='模具车1': self._is_small_f=True print('-------------F块模具--------------') - print('-------------F块模具--------------') - print('-------------F块模具--------------') + # print('-------------F块模具--------------') + # print('-------------F块模具--------------') + # self.send_pd_data() self.run_feed_f() elif _is_f=='模具车2': self._is_small_f=False + print('-------------B-L模具---------------') + # self.send_pd_data() self.run_feed() - print('-------------其他模具---------------') if self._is_small_f is None: print('-----------未判断出模具类型--------------') @@ -467,21 +659,17 @@ class VisualCallback: loc_mitter=self.transmitter_controller max_weight_none=5 cur_weight_none=0 - initial_lower_weight=loc_mitter.read_data(2) - if initial_lower_weight is None: - print("-----f上料斗重量异常-----") - return + initial_lower_weight=self._init_lower_weight first_finish_weight=0 self._finish_weight=first_finish_weight self._inital_finish_lweight=initial_lower_weight - need_total_weight=0.54*2416 + self._mould_need_weight=0.54*2416 + need_total_weight=self._mould_need_weight if initial_lower_weight>100: + self.state._feed_status=FeedStatus.FFeed5 + self.state.vf_status=2 if not self._is_finish: self.is_start_visual=True - initial_lower_weight=loc_mitter.read_data(2) - if initial_lower_weight is None: - print("-----f上料斗重量异常2-----") - return self._is_feed_stage=5 while not self._is_finish: current_weight = loc_mitter.read_data(2) @@ -499,20 +687,19 @@ class VisualCallback: first_finish_weight=initial_lower_weight-current_weight self._is_finish_ratio=(first_finish_weight)/need_total_weight print(f'------------已下料比例: {self._is_finish_ratio}-------------') - if self._is_finish_ratio>self._max_f_angle_ratio: + # if self._is_finish_ratio>=1: #关5秒 #大于0.7后不再检测了,直接交给视觉控制夹脚 # print(f'------------已下料比例: {self._is_finish_ratio}-------------') - break + # break # print(f'------------已下料: {first_finish_weight+second_finish_weight}kg-------------') - time.sleep(1) + time.sleep(0.5) # initial_lower_weight=_current_lower_weight print(f'------------已下料(F): {first_finish_weight}kg-------------') print(f'------------已下料(F): {first_finish_weight}kg-------------') - print(f'------------已完成-------------') - + # print(f'------------已完成-------------') def run_feed(self): """第一阶段下料:下料斗向模具车下料(低速)""" @@ -523,15 +710,15 @@ class VisualCallback: max_weight_none=5 cur_weight_none=0 - initial_lower_weight=loc_mitter.read_data(2) + initial_lower_weight=self._init_lower_weight # initial_upper_weight=loc_mitter.read_data(1) - if initial_lower_weight is None: - print("---------------下料斗重量异常----------------") - return first_finish_weight=0 - need_total_weight=1.91*2416 + self._mould_need_weight=1.91*2416 + need_total_weight=self._mould_need_weight # start_time=None self.is_start_visual=True + self.state._feed_status=FeedStatus.FFeed1 + self.state.vf_status=1 if initial_lower_weight>100: #下料斗的料全部下完 self._is_feed_stage=1 @@ -545,7 +732,12 @@ class VisualCallback: return continue cur_weight_none=0 + self._is_finish_ratio=(initial_lower_weight-current_weight)/need_total_weight + print(f'------------已下料比例: {self._is_finish_ratio}-------------') if current_weight<250 and current_weight>0: + # if current_weight>100: + #100,上面粘贴的,振动一下 + # self.relay_controller.control_arch_lower_open_async(5) self.close_lower_door_visual() break time.sleep(1) @@ -559,13 +751,13 @@ class VisualCallback: print(f'------------已下料(第一次): {first_finish_weight}kg-------------') self._is_feed_stage=0 - while self.plc_data!=5: - print('------------上料斗未就位----------------') - print('------------上料斗未就位----------------') + while self.plc_data not in [5,37]: + #print('------------上料斗未就位----------------') + # print('------------上料斗未就位----------------') time.sleep(1) - if self.plc_data==5: - print(f'------------上料斗向下料斗转移(留3000KG)-------------') + if self.plc_data==5 or self.plc_data==37: + print(f'------------上料斗就位(上料斗往下料斗阶段)-------------') #打开上料斗出砼门,开5就,开三分之一下 loc_relay.control_upper_open_sync(6) @@ -588,12 +780,12 @@ class VisualCallback: _two_lower_weight=loc_mitter.read_data(2) if _two_lower_weight is None: _two_lower_weight=0 - if (current_upper_weight<3000 and current_upper_weight>0) or _two_lower_weight>3200: + if (current_upper_weight<3200 and current_upper_weight>0) or _two_lower_weight>3200: #关5秒,loc_time_count多关一秒 loc_relay.control_upper_close_sync(5+loc_time_count) break else: - if time.time()-upper_open_time>5: + if time.time()-upper_open_time>=4: if loc_time_count<6: upper_open_time=time.time() loc_relay.control_upper_open_sync(0.8) @@ -619,12 +811,15 @@ class VisualCallback: return continue cur_weight_none=0 - # second_finish_weight=initial_lower_weight-current_weight + self._is_finish_ratio=(first_finish_weight+initial_lower_weight-current_weight)/need_total_weight if current_weight<250: + # if current_weight>100: + #100,上面粘贴的,振动一下 + # self.relay_controller.control_arch_lower_open_async(5) self.close_lower_door_visual() break # print(f'------------已下料: {first_finish_weight+second_finish_weight}kg-------------') - time.sleep(1) + time.sleep(0.5) _current_lower_weight=loc_mitter.read_data(2) if _current_lower_weight is None: print("-------下料斗重量异常(第二次下到模)---------") @@ -634,7 +829,7 @@ class VisualCallback: print(f'------------已下料(第二次): {first_finish_weight}kg-------------') self._is_feed_stage=0 - if self.plc_data==5: + if self.plc_data==5 or self.plc_data==37: #第二次上料斗向下料斗转移 loc_relay.control_upper_open_sync(12) loc_time_count=1 @@ -642,8 +837,8 @@ class VisualCallback: upper_open_time_2=None #第二次到下料斗还需要的量 #loc_left_need_weight=need_total_weight-first_finish_weight - # initial_upper_weight=loc_mitter.read_data(1) - # start_time=None + #initial_upper_weight=loc_mitter.read_data(1) + #start_time=None self._is_feed_stage=4 while not self._is_finish: # print(f'------------上料斗向下料斗转移22222-------------') @@ -669,9 +864,9 @@ class VisualCallback: #5秒后关闭 loc_relay.control_upper_close_after()#control_upper_close_sync(8+loc_time_count) break - time.sleep(1) + time.sleep(0.5) else: - if time.time()-upper_open_time>2: + if time.time()-upper_open_time>=1: # if loc_time_count<6: upper_open_time=time.time() loc_relay.control_upper_open_sync(1.2) @@ -706,13 +901,13 @@ class VisualCallback: second_finish_weight=initial_lower_weight-current_weight self._is_finish_ratio=(second_finish_weight+first_finish_weight)/need_total_weight print(f'------------已下料比例: {self._is_finish_ratio}-------------') - if self._is_finish_ratio>=1: + # if self._is_finish_ratio>=1: #关5秒 # print(f'------------已下料比例: {self._is_finish_ratio}-------------') - break + # break # print(f'------------已下料: {first_finish_weight+second_finish_weight}kg-------------') - time.sleep(1) + time.sleep(0.5) # _current_lower_weight=loc_mitter.read_data(2) # first_finish_weight=first_finish_weight+initial_lower_weight-_current_lower_weight @@ -720,7 +915,7 @@ class VisualCallback: # print(f'------------已下料: {first_finish_weight}kg-------------') - print(f'------------已完成-------------') + # print(f'------------已完成-------------') def _process_angle_callback(self, current_angle, overflow_detected): """ @@ -743,43 +938,171 @@ class VisualCallback: if current_angle is None: return - print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 角度11: {current_angle:.2f}°,{overflow_detected}") + print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} 角度11: {current_angle:.2f}°,{overflow_detected},diff_f_val:{self._diff_f_val},diff_f_area:{self._diff_f_area}") + if self._is_small_f: + if self._is_finish_ratio>=1.02: + print('重量达到最大比例,浇筑满关闭') + self._visual_close() + return + elif self._is_finish_ratio>=0.9: + if (self._diff_f_val>=427 and self._diff_f_val<=450): + print('------------diff到达浇筑满-------------') + self._visual_close() + return + elif (len(self._diff_f_area)>0 and self._diff_f_area[-1]>=33400 and self._diff_f_area[-1]<=34500): + print('------------area到达浇筑满-------------') + self._visual_close() + return + else: + if self._is_finish_ratio>=1.01: + print('重量达到最大比例,浇筑满关闭') + self._visual_close() + return + elif self._is_finish_ratio>=0.93: + if (self._diff_f_val>=460 and self._diff_f_val<=510): + print('------------diff到达浇筑满-------------') + self._visual_close() + return + if (len(self._diff_f_area)>0 and self._diff_f_area[-1]>=38200 and self._diff_f_area[-1]<=41000): + print('------------area到达浇筑满-------------') + self._visual_close() + return + + 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) + self.safe_control_lower_close(1) print('-----------------关闭(未浇筑满)--------------------') # time.sleep(3) else: + if not self._is_stop_one_seconds: + #根据角度来计算还需要多久完全关闭 + if current_angle>=20: + self.safe_control_lower_close(2) + elif current_angle>=10 and current_angle<20: + self.safe_control_lower_close(1) + elif current_angle>=6 and current_angle<10: + self.safe_control_lower_close(0.5) + self._is_stop_one_seconds=True + elif current_angle>7: + #点动状态下,如果关闭后角度大于7度,关紧 + self.safe_control_lower_close(0.2) + _open_time=0.3 + _sleep_time=0.3 + _close_time=0.5 if overflow_detected=='浇筑满': - self._visual_close() - return + if self._is_small_f: + self._visual_close() + return + else: + if self._diff_f_val>=410 and self._diff_f_val<450: + #排除这个范围的关闭 + print(f'浇筑满状态,diff_f_val:{self._diff_f_val},不关闭') + _open_time=0.5 + _sleep_time=0.3 + _close_time=0.7 + else: + self._visual_close() + return + # print(f'--------已关闭已关闭-----------') elif overflow_detected=="大堆料": print(f'--------未浇筑满,大堆料-----------') - self._pulse_control('open',0.3) - time.sleep(0.3) - self._pulse_control('close',0.4) - print(f'--------比例:{self._is_finish_ratio}-----------') - if self._is_finish_ratio>= self._weight_ratio_955: - time.sleep(2) - else: - time.sleep(1) - self._is_before_finish=True + elif overflow_detected=="小堆料": + print(f'--------未浇筑满,小堆料-----------') + _open_time=0.5 + _sleep_time=0.3 + _close_time=0.7 else: - # self._pulse_control('open',0.5) - # time.sleep(0.3) - # self._pulse_control('close',0.6) - # print(f'--------比例:{self._is_finish_ratio}-----------') - self._pulse_control('open',0.6) - time.sleep(0.3) - self._pulse_control('close',0.7) - if self._is_finish_ratio>= self._weight_ratio_955: - time.sleep(2) + if self._is_small_f: + _open_time=0.6 + _sleep_time=0.3 + _close_time=0.8 else: + if self._is_finish_ratio<0.9: + _open_time=1 + _sleep_time=0.3 + _close_time=1.2 + #之前慢的参数 + # _open_time=0.8 + # _sleep_time=0.3 + # _close_time=1 + elif self._is_finish_ratio<0.95 and self._is_finish_ratio>=0.9: + if self._point_weight>=10: + _open_time=0.7 + _sleep_time=0.3 + _close_time=0.9 + else: + #之前慢的参数 + # _open_time=0.8 + # _sleep_time=0.3 + # _close_time=1 + _open_time=0.9 + _sleep_time=0.3 + _close_time=1.1 + else: + _open_time=0.6 + _sleep_time=0.3 + _close_time=0.8 + + if self._point_speed_grade==1: + if _open_time>0.6: + _open_time=0.6 + _sleep_time=0.3 + _close_time=0.8 + elif self._point_speed_grade==2: + if _open_time>0.5: + _open_time=0.5 + _sleep_time=0.3 + _close_time=0.7 + elif self._point_speed_grade==3: + if _open_time>0.4: + _open_time=0.4 + _sleep_time=0.3 + _close_time=0.6 + elif self._point_speed_grade==4: + if _open_time>0.3: + _open_time=0.3 + _sleep_time=0.3 + _close_time=0.5 + _last_finish_ratio=self._is_finish_ratio + print(f'--------比例开始:{_last_finish_ratio}-----------') + self._pulse_control('open',_open_time) + time.sleep(_sleep_time) + self._pulse_control('close',_close_time) + print(f'--------比例结束:{self._is_finish_ratio}-----------') + + self._point_weight=(self._is_finish_ratio-_last_finish_ratio)*self._mould_need_weight + print(f'--------流速:{self._point_weight}-----------') + if self._is_small_f: + time.sleep(2.5) + else: + # if self._is_finish_ratio>= 0.93: + # time.sleep(2) + # print('--------重量已到95.5%,需要2秒休息-----------') + # else: time.sleep(1) - self._is_before_finish=True + #下得过快,需要2秒休息 + if self._point_weight>=65: + time.sleep(5) + self._point_speed_grade=4 + elif self._point_weight>=50 and self._point_weight<65: + time.sleep(4) + self._point_speed_grade=3 + elif self._point_weight>=35 and self._point_weight<50: + time.sleep(3) + self._point_speed_grade=2 + elif self._point_weight>=25 and self._point_weight<35: + time.sleep(2) + self._point_speed_grade=1 + elif self._point_weight>=15 and self._point_weight<25: + time.sleep(1) + self._point_speed_grade=0 + else: + self._point_speed_grade=0 + self._is_before_finish=True if self._is_finish_ratio<=self._max_ignore_radio: #如果重量未达到最大忽略角度,需要跳出 self._is_before_finish=False @@ -789,7 +1112,15 @@ class VisualCallback: return else: self._before_finish_time=None - if self._is_finish_ratio>=self._max_angle_radio or (self._is_finish_ratio>self._max_f_angle_ratio and self._is_small_f): + #2160KG + if self._is_finish_ratio>=0.85 and not self._is_small_f: + if overflow_detected == "大堆料": + TARGET_ANGLE = 5.0 # 大堆料时控制在15度左右 + elif overflow_detected == "小堆料": + TARGET_ANGLE = 15.0 # 小堆料时控制在35度左右 + else: + TARGET_ANGLE = 35.0 # 12.25由25--》35 + elif (self._is_finish_ratio>0.7 and self._is_small_f): if overflow_detected == "大堆料": TARGET_ANGLE = 5.0 # 大堆料时控制在15度左右 elif overflow_detected == "小堆料": @@ -800,19 +1131,31 @@ class VisualCallback: if self._is_feed_stage==1 or self._is_feed_stage==3: #根据溢料状态动态调整目标角度 if overflow_detected == "大堆料": - TARGET_ANGLE = 15.0 # 大堆料时控制在15度左右 + if not self.state.mould_vibrate_status: + TARGET_ANGLE = 15.0 # 临时控制变频器堆料时很小 + else: + TARGET_ANGLE = 35.0 # 大堆料时控制在15度左右 elif overflow_detected == "小堆料": - TARGET_ANGLE = 45.0 # 小堆料时控制在35度左右 + TARGET_ANGLE = 55.0 # 小堆料时控制在35度左右 else: TARGET_ANGLE = 55.0 # 未溢料时开到最大56度 else: + if self._is_small_f: #根据溢料状态动态调整目标角度 - if overflow_detected == "大堆料": - TARGET_ANGLE = 15.0 # 大堆料时控制在15度左右 - elif overflow_detected == "小堆料": - TARGET_ANGLE = 25.0 # 小堆料时控制在35度左右 + if overflow_detected == "大堆料": + TARGET_ANGLE = 15.0 # 大堆料时控制在15度左右 + elif overflow_detected == "小堆料": + TARGET_ANGLE = 25.0 # 小堆料时控制在35度左右 + else: + TARGET_ANGLE = 45.0 # 未溢料时开到最大56度 else: - TARGET_ANGLE = 45.0 # 未溢料时开到最大56度 + #根据溢料状态动态调整目标角度 + if overflow_detected == "大堆料": + TARGET_ANGLE = 15.0 # 大堆料时控制在15度左右 + elif overflow_detected == "小堆料": + TARGET_ANGLE = 55.0 # 小堆料时控制在35度左右 + else: + TARGET_ANGLE = 55.0 # 未溢料时开到最大56度 # 确保目标角度在硬件范围内(5-56度) TARGET_ANGLE = max(5.0, min(56.0, TARGET_ANGLE)) @@ -843,8 +1186,8 @@ class VisualCallback: # 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}") + #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 @@ -861,6 +1204,9 @@ class VisualCallback: self._maintaining_mode_advanced(current_angle, pid_output, TARGET_ANGLE) except Exception as e: + print("处理视觉回调时发生异常: ") + print("处理视觉回调时发生异常: ") + print("处理视觉回调时发生异常: ") print(f"处理视觉回调时发生异常: {e}") def _normal_mode_advanced(self, current_angle, pid_output,target_angle): @@ -897,11 +1243,11 @@ class VisualCallback: if error > 0: # 当前角度 > 目标角度,需要关门 pulse_time=0.1 # 根据误差计算脉冲时间 self._pulse_control("close", pulse_time) - print(f"🚨 强制关门: 误差{abs_error:.1f}°过大,脉冲{pulse_time:.3f}s") + #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") + #print(f"🚨 强制开门: 误差{abs_error:.1f}°过大,脉冲{pulse_time:.3f}s") return else: self._stop_door() @@ -923,7 +1269,7 @@ class VisualCallback: # 使用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}") + # print(f"减小模式: 积极关门{pulse_time:.2f}秒,PID输出:{pid_output:.1f}") else: self.angle_mode = "maintaining" print("角度已达标,进入维持模式") @@ -963,11 +1309,11 @@ class VisualCallback: return thread_name = threading.current_thread().name - print(f"[{thread_name}] 尝试脉冲控制 {action},时长 {duration:.2f}秒...") + #print(f"[{thread_name}] 尝试脉冲控制 {action},时长 {duration:.2f}秒...") with self._door_control_lock: self._current_controlling_thread = thread_name - print(f"[{thread_name}] 获得下料斗控制权,执行脉冲控制") + # print(f"[{thread_name}] 获得下料斗控制权,执行脉冲控制") if action == "open": self.relay_controller.control(self.relay_controller.DOOR_LOWER_CLOSE, 'close') @@ -983,7 +1329,7 @@ class VisualCallback: print(f"[{thread_name}] 关门脉冲: {duration:.2f}秒") self._current_controlling_thread = None - print(f"[{thread_name}] 释放下料斗控制权") + #print(f"[{thread_name}] 释放下料斗控制权") def _stop_door(self): """停止门运动""" @@ -994,15 +1340,15 @@ class VisualCallback: return thread_name = threading.current_thread().name - print(f"[{thread_name}] 尝试停止门运动...") + #print(f"[{thread_name}] 尝试停止门运动...") with self._door_control_lock: self._current_controlling_thread = thread_name - print(f"[{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}] 释放下料斗控制权") + #print(f"[{thread_name}] 释放下料斗控制权") def _open_door(self, duration=0.5): """打开门""" @@ -1017,10 +1363,10 @@ class VisualCallback: # print(f"[数据回调] 数值: 0x{data:02X} | 十进制: {data:3d} | 二进制: {binary}") self.plc_data=data - @classmethod - def instance_exists(cls): + # @classmethod + # def instance_exists(cls): """检测实例是否存在""" - return cls._instance is not None + # return cls._instance is not None def shutdown(self): """关闭线程,清理资源""" @@ -1048,6 +1394,86 @@ class VisualCallback: # self.relay_controller._close_lower_5s + def send_pd_data(self): + """ + 发送PD数据到OPC队列 + """ + # 构建PD数据 + _cur_mould=self._cur_mould_model + if _cur_mould is not None: + if _cur_mould.MouldCode: + _pdrecords = self.pd_record_bll.get_last_pds(_cur_mould.MouldCode) + if _pdrecords: + _pdrecord=_pdrecords[0] + if _pdrecord.TaskID: + if _pdrecord.BlockNumber=='F': + print(f'{_pdrecord.MouldCode} F块,不发送派单数据') + print(f'{_pdrecord.MouldCode} F块,不发送派单数据') + print(f'{_pdrecord.MouldCode} F块,不发送派单数据') + return True + _fact_volumn=self.get_fact_volumn(_pdrecord.MouldCode,_pdrecord.BlockNumber) + if _fact_volumn>0: + _pdrecord.FBetonVolume=_fact_volumn + print(f'{_pdrecord.MouldCode}-{_pdrecord.BlockNumber} 实际派单方量:{_fact_volumn},{_fact_volumn},{_fact_volumn}') + print(f'{_pdrecord.MouldCode}-{_pdrecord.BlockNumber} 实际派单方量:{_fact_volumn},{_fact_volumn},{_fact_volumn}') + print(f'{_pdrecord.MouldCode}-{_pdrecord.BlockNumber} 实际派单方量:{_fact_volumn},{_fact_volumn},{_fact_volumn}') + self.state._pd_data=_pdrecord + return True + else: + return False + else: + print(f'{_pdrecord.MouldCode} 未获取到数据-(等待扫码)') + return False + else: + print(f'接口数据异常') + return False + else: + return None + def get_fact_volumn(self,mould_code:str,block_number:str='') -> float: + """获取实际派单发量""" + _now_volumn=0 + _pd_volumn=0 + print(f'get_fact_volumn当前重量:{self._init_lower_weight}') + _now_volumn=self._init_lower_weight/2500 + if not block_number and '-' in mould_code: + block_number = mould_code.split('-')[0][-2:] + if block_number in ['B1','B2','B3']: + _pd_volumn=1.9 + elif block_number=='L1': + _pd_volumn=2.0 + if _now_volumn>0.5: + #保证至少0.5方 + _pd_volumn=1.9-_now_volumn+0.5 + _pd_volumn=math.ceil(_pd_volumn*10)/10 + + if _pd_volumn<0.8: + _pd_volumn=0.8 + #调整 + elif block_number=='L2': + #2.4方,大约L2和F的量 + _pd_volumn=2.4 + # if _weight>1300: + #留0.15 math.floor(_now_volumn*10)/10 保留一位小数,丢掉其他的 + _pd_volumn=_pd_volumn-math.floor(_now_volumn*10)/10+0.1 + _pd_volumn=math.ceil(_pd_volumn*10)/10 + if _pd_volumn>2.1: + _pd_volumn=2.1 + elif _pd_volumn<0.8: + _pd_volumn=0.8 + + return _pd_volumn + + def get_current_mould(self): + """获取当前要浇筑的管片""" + _not_poured=app_web_service.get_not_pour_artifacts() + if _not_poured is not None and len(_not_poured)>=1: + _cur_poured_model=_not_poured[-1] + if _cur_poured_model.MouldCode: + self._cur_mould_model=_cur_poured_model + print(f'当前要浇筑的管片 {json.dumps(asdict(_cur_poured_model), ensure_ascii=False)}') + else: + print('当前没有未浇筑的管片') + def __del__(self): """析构函数,确保线程安全关闭""" self.shutdown() diff --git a/vision/weight.txt b/vision/weight.txt new file mode 100644 index 0000000..1a54c9c --- /dev/null +++ b/vision/weight.txt @@ -0,0 +1,654 @@ +2026-01-06 07:00:53,2026-01-06 07:06:04,B,3154 +diff , 2026-01-06 07:06:04 , 632 + +area , 2026-01-06 07:06:04 , [292.2259399848001, 223.00896842952304, 36723] + +diff , 2026-01-06 07:06:05 , 454 + +area , 2026-01-06 07:06:05 , [291.8715470887836, 223.00896842952304, 36503] + +diff , 2026-01-06 07:06:06 , 453 + +area , 2026-01-06 07:06:06 , [225.656819085974, 226.92069099136816, 36164] + +diff , 2026-01-06 07:06:07 , 456 + +area , 2026-01-06 07:06:07 , [229.40139493908924, 223.00896842952304, 36277] + +diff , 2026-01-06 07:06:08 , 457 + +area , 2026-01-06 07:06:08 , [226.947130407062, 223.00896842952304, 36278] + +diff , 2026-01-06 07:06:09 , 635 + +area , 2026-01-06 07:06:09 , [230.10649708341572, 223.8861317723811, 36710] + +diff , 2026-01-06 07:06:10 , 456 + +area , 2026-01-06 07:06:10 , [288.2568299277573, 227.343352662883, 37054] + +diff , 2026-01-06 07:06:11 , 453 + +area , 2026-01-06 07:06:11 , [227.30596120647607, 223.8861317723811, 36971] + +diff , 2026-01-06 07:06:12 , 453 + +area , 2026-01-06 07:06:12 , [230.10649708341572, 223.4390297150433, 37326] + +diff , 2026-01-06 07:06:13 , 455 + +area , 2026-01-06 07:06:13 , [233.15445524372893, 223.4390297150433, 36853] + +2026-01-06 07:24:40,2026-01-06 07:29:52,B,4623 +diff , 2026-01-06 07:29:53 , 458 + +area , 2026-01-06 07:29:53 , [288.1336495447902, 223.8861317723811, 36642] + +diff , 2026-01-06 07:29:54 , 459 + +area , 2026-01-06 07:29:54 , [226.59214461229675, 226.80828909014767, 36083] + +diff , 2026-01-06 07:29:55 , 461 + +area , 2026-01-06 07:29:55 , [284.6928169097352, 226.80828909014767, 36640] + +diff , 2026-01-06 07:29:56 , 461 + +area , 2026-01-06 07:29:56 , [226.59214461229675, 230.70760715676457, 36218] + +diff , 2026-01-06 07:29:57 , 464 + +area , 2026-01-06 07:29:57 , [281.18677067031444, 230.27375013231534, 36645] + +diff , 2026-01-06 07:29:58 , 463 + +area , 2026-01-06 07:29:58 , [280.71693928225994, 230.27375013231534, 36969] + +diff , 2026-01-06 07:29:59 , 462 + +area , 2026-01-06 07:29:59 , [277.2183255125822, 234.18368858654523, 36761] + +diff , 2026-01-06 07:30:00 , 463 + +area , 2026-01-06 07:30:00 , [219.1255347968374, 234.61031520374377, 36405] + +diff , 2026-01-06 07:30:01 , 467 + +area , 2026-01-06 07:30:01 , [222.8542124349459, 234.61031520374377, 36479] + +area , 2026-01-06 07:30:02 , [223.5799633240868, 231.1579546543878, 36852] + +diff , 2026-01-06 07:30:03 , 465 + +area , 2026-01-06 07:30:03 , [219.1255347968374, 234.61031520374377, 36812] + +补料:2 +2026-01-06 07:32:05,2026-01-06 07:36:39,B,4602 +补料:4 +2026-01-06 07:39:31,2026-01-06 07:41:51,F,1316 +补料:2 +2026-01-06 07:46:40,2026-01-06 07:51:03,B,4471 +补料:8 +2026-01-06 08:00:43,2026-01-06 08:05:26,B,4430 +补料:24 +2026-01-06 08:08:22,2026-01-06 08:13:52,B,4526 +补料:20 +2026-01-06 08:17:08,2026-01-06 08:21:25,B,4593 +补料:14 +2026-01-06 08:24:34,2026-01-06 08:29:49,B,4609 +补料:11 +2026-01-06 08:38:57,2026-01-06 08:44:04,B,4554 +diff , 2026-01-06 08:44:04 , 456 + +area , 2026-01-06 08:44:04 , [232.14219780126146, 218.8880992653552, 35193] + +diff , 2026-01-06 08:44:05 , 448 + +area , 2026-01-06 08:44:05 , [221.79495034828903, 222.80035906613796, 35124] + +diff , 2026-01-06 08:44:06 , 633 + +area , 2026-01-06 08:44:06 , [224.61077445216202, 222.80035906613796, 35255] + +diff , 2026-01-06 08:44:07 , 650 + +area , 2026-01-06 08:44:07 , [286.8449058289165, 212.9530464679949, 36314] + +diff , 2026-01-06 08:44:08 , 456 + +area , 2026-01-06 08:44:08 , [283.70583356709466, 228.70505022845472, 35764] + +diff , 2026-01-06 08:44:09 , 446 + +area , 2026-01-06 08:44:09 , [215.60380330597138, 225.94247055390008, 35407] + +diff , 2026-01-06 08:44:10 , 456 + +area , 2026-01-06 08:44:10 , [284.48198536990003, 229.65408770583642, 36252] + +diff , 2026-01-06 08:44:11 , 634 + +area , 2026-01-06 08:44:11 , [222.14409737825582, 225.94247055390008, 35237] + +diff , 2026-01-06 08:44:12 , 457 + +area , 2026-01-06 08:44:12 , [229.40139493908924, 223.4390297150433, 35324] + +diff , 2026-01-06 08:44:13 , 630 + +area , 2026-01-06 08:44:13 , [284.48198536990003, 225.94247055390008, 36088] + +补料:13 +2026-01-06 08:45:55,2026-01-06 08:49:57,B,4557 +补料:34 +2026-01-06 09:00:18,2026-01-06 09:04:15,B,4571 +diff , 2026-01-06 09:04:16 , 636 + +area , 2026-01-06 09:04:16 , [225.656819085974, 227.56098083810414, 36387] + +diff , 2026-01-06 09:04:17 , 637 + +area , 2026-01-06 09:04:17 , [222.8542124349459, 230.70760715676457, 36409] + +diff , 2026-01-06 09:04:18 , 632 + +area , 2026-01-06 09:04:18 , [229.40139493908924, 227.56098083810414, 36588] + +diff , 2026-01-06 09:04:19 , 624 + +area , 2026-01-06 09:04:19 , [219.1255347968374, 234.39496581624786, 36588] + +diff , 2026-01-06 09:04:20 , 450 + +area , 2026-01-06 09:04:20 , [226.947130407062, 230.70760715676457, 36692] + +diff , 2026-01-06 09:04:21 , 450 + +area , 2026-01-06 09:04:21 , [226.01327394646538, 227.7827912727386, 36614] + +diff , 2026-01-06 09:04:22 , 638 + +area , 2026-01-06 09:04:22 , [226.01327394646538, 227.343352662883, 36655] + +diff , 2026-01-06 09:04:23 , 627 + +area , 2026-01-06 09:04:23 , [222.8542124349459, 232.13142829009604, 36405] + +diff , 2026-01-06 09:04:24 , 642 + +area , 2026-01-06 09:04:24 , [222.8542124349459, 234.61031520374377, 36148] + +diff , 2026-01-06 09:04:25 , 451 + +area , 2026-01-06 09:04:25 , [291.7601754866486, 232.13142829009604, 36663] + +补料:10 +2026-01-06 09:07:13,2026-01-06 09:11:16,B,4589 +补料:6 +2026-01-06 09:22:40,2026-01-06 09:27:26,B,4489 +diff , 2026-01-06 09:27:27 , 630 + +area , 2026-01-06 09:27:27 , [290.6217472936256, 227.1299187689724, 35628] + +diff , 2026-01-06 09:27:28 , 625 + +area , 2026-01-06 09:27:28 , [291.7601754866486, 226.71568097509268, 35404] + +area , 2026-01-06 09:27:29 , [286.6304240655552, 227.56098083810414, 35718] + +diff , 2026-01-06 09:27:30 , 630 + +area , 2026-01-06 09:27:30 , [241.6112580158466, 223.6604569431083, 35214] + +diff , 2026-01-06 09:27:31 , 623 + +area , 2026-01-06 09:27:31 , [291.3485884640597, 223.6604569431083, 35546] + +diff , 2026-01-06 09:27:32 , 630 + +area , 2026-01-06 09:27:32 , [225.55043781824057, 226.71568097509268, 35207] + +area , 2026-01-06 09:27:33 , [224.61077445216202, 227.56098083810414, 35130] + +diff , 2026-01-06 09:27:34 , 630 + +area , 2026-01-06 09:27:34 , [237.80664414603726, 230.63390904201404, 35448] + +diff , 2026-01-06 09:27:35 , 627 + +area , 2026-01-06 09:27:35 , [295.7363690857112, 219.76350925483513, 35487] + +diff , 2026-01-06 09:27:36 , 621 + +area , 2026-01-06 09:27:36 , [287.7846417027844, 227.56098083810414, 35986] + +diff , 2026-01-06 09:27:37 , 629 + +area , 2026-01-06 09:27:37 , [224.61077445216202, 227.56098083810414, 35270] + +diff , 2026-01-06 09:27:38 , 618 + +补料:23 +2026-01-06 09:29:56,2026-01-06 09:34:04,B,4500 +补料:39 +2026-01-06 09:37:15,2026-01-06 09:41:23,B,4513 +补料:29 +2026-01-06 09:44:22,2026-01-06 09:48:35,B,4555 +补料:2 +2026-01-06 09:51:26,2026-01-06 09:55:19,B,4538 +补料:8 +2026-01-06 09:57:42,2026-01-06 09:59:52,F,1326 +补料:3 +2026-01-06 10:04:27,2026-01-06 10:10:54,B,4515 +补料:11 +2026-01-06 10:13:40,2026-01-06 10:19:42,B,4528 +补料:35 +2026-01-06 10:22:11,2026-01-06 10:26:25,B,4526 +补料:3 +2026-01-06 10:28:55,2026-01-06 10:32:54,B,4619 +补料:8 +2026-01-06 10:35:56,2026-01-06 10:40:40,B,4575 +补料:7 +2026-01-06 10:42:46,2026-01-06 10:45:18,F,1281 +补料:4 +2026-01-06 10:49:14,2026-01-06 10:53:45,B,4091 +补料:30 +2026-01-06 10:55:44,2026-01-06 10:59:57,B,4368 +补料:16 +2026-01-06 11:02:16,2026-01-06 11:07:24,B,4518 +补料:22 +2026-01-06 11:09:53,2026-01-06 11:15:08,B,4295 +补料:15 +2026-01-06 12:41:11,2026-01-06 12:47:05,B,4593 +diff , 2026-01-06 12:47:05 , 445 + +area , 2026-01-06 12:47:05 , [291.8715470887836, 218.47654336335515, 35671] + +diff , 2026-01-06 12:47:06 , 444 + +area , 2026-01-06 12:47:06 , [291.8715470887836, 218.47654336335515, 36236] + +diff , 2026-01-06 12:47:07 , 444 + +area , 2026-01-06 12:47:07 , [287.4665197896966, 222.39604313026794, 36312] + +diff , 2026-01-06 12:47:08 , 445 + +area , 2026-01-06 12:47:08 , [291.8715470887836, 218.47654336335515, 35942] + +diff , 2026-01-06 12:47:09 , 445 + +area , 2026-01-06 12:47:09 , [287.4665197896966, 222.39604313026794, 35242] + +diff , 2026-01-06 12:47:10 , 444 + +area , 2026-01-06 12:47:10 , [222.8542124349459, 220.0772591614136, 35374] + +diff , 2026-01-06 12:47:11 , 441 + +area , 2026-01-06 12:47:11 , [222.8542124349459, 220.0772591614136, 35156] + +diff , 2026-01-06 12:47:12 , 450 + +area , 2026-01-06 12:47:12 , [287.8975512226528, 219.86586820150143, 35537] + +diff , 2026-01-06 12:47:13 , 447 + +area , 2026-01-06 12:47:13 , [287.8975512226528, 222.39604313026794, 35955] + +diff , 2026-01-06 12:47:14 , 447 + +area , 2026-01-06 12:47:14 , [228.7116088002531, 219.86586820150143, 34972] + +补料:31 + +================================2026-01-06 12:57:50,2026-01-06 13:04:33,B,4616 +diff , 2026-01-06 13:04:33 , 443 + +area , 2026-01-06 13:04:33 , [295.8462438497403, 223.8861317723811, 36251] + +diff , 2026-01-06 13:04:34 , 445 + +area , 2026-01-06 13:04:34 , [295.8462438497403, 226.92069099136816, 36584] + +diff , 2026-01-06 13:04:35 , 444 + +area , 2026-01-06 13:04:35 , [295.4268098869837, 223.8861317723811, 36546] + +diff , 2026-01-06 13:04:36 , 444 + +area , 2026-01-06 13:04:36 , [295.8462438497403, 223.4390297150433, 36709] + +diff , 2026-01-06 13:04:38 , 439 + +area , 2026-01-06 13:04:38 , [291.8715470887836, 227.7827912727386, 36495] + +diff , 2026-01-06 13:04:39 , 439 + +area , 2026-01-06 13:04:39 , [295.8462438497403, 223.4390297150433, 36476] + +diff , 2026-01-06 13:04:40 , 443 + +area , 2026-01-06 13:04:40 , [295.8462438497403, 226.92069099136816, 37179] + +diff , 2026-01-06 13:04:41 , 445 + +area , 2026-01-06 13:04:41 , [291.44639301250584, 227.7827912727386, 36543] + +diff , 2026-01-06 13:04:42 , 445 + +area , 2026-01-06 13:04:42 , [291.8715470887836, 227.343352662883, 36556] + +diff , 2026-01-06 13:04:43 , 445 + +area , 2026-01-06 13:04:43 , [291.8715470887836, 227.7827912727386, 36611] + +补料:23 + +================================2026-01-06 13:06:34,2026-01-06 13:11:47,B,4620 +diff , 2026-01-06 13:11:47 , 446 + +area , 2026-01-06 13:11:47 , [295.7363690857112, 234.39496581624786, 37311] + +diff , 2026-01-06 13:11:48 , 445 + +area , 2026-01-06 13:11:48 , [219.1255347968374, 234.39496581624786, 36681] + +diff , 2026-01-06 13:11:49 , 447 + +area , 2026-01-06 13:11:49 , [222.28360263411244, 231.68297304722245, 36683] + +diff , 2026-01-06 13:11:50 , 446 + +area , 2026-01-06 13:11:50 , [222.8542124349459, 234.39496581624786, 36658] + +diff , 2026-01-06 13:11:51 , 448 + +area , 2026-01-06 13:11:51 , [226.01327394646538, 231.68297304722245, 36662] + +diff , 2026-01-06 13:11:53 , 451 + +area , 2026-01-06 13:11:53 , [222.8542124349459, 234.39496581624786, 36783] + +diff , 2026-01-06 13:11:54 , 447 + +area , 2026-01-06 13:11:54 , [300.38974682901545, 231.68297304722245, 37410] + +diff , 2026-01-06 13:11:55 , 450 + +area , 2026-01-06 13:11:55 , [295.8462438497403, 234.61031520374377, 37612] + +diff , 2026-01-06 13:11:56 , 448 + +area , 2026-01-06 13:11:56 , [299.1738624947039, 231.68297304722245, 37302] + +diff , 2026-01-06 13:11:57 , 447 + +area , 2026-01-06 13:11:57 , [299.5496619927988, 228.00877176108818, 37456] + +补料:7 + +================================2026-01-06 13:13:42,2026-01-06 13:16:06,F,1309 +diff , 2026-01-06 13:16:07 , 440 + +area , 2026-01-06 13:16:07 , [294.89828755013144, 219.10043359153812, 36251] + +diff , 2026-01-06 13:16:08 , 440 + +area , 2026-01-06 13:16:08 , [295.4268098869837, 219.10043359153812, 36394] + +diff , 2026-01-06 13:16:09 , 440 + +area , 2026-01-06 13:16:09 , [291.44639301250584, 223.00896842952304, 36546] + +diff , 2026-01-06 13:16:10 , 440 + +area , 2026-01-06 13:16:10 , [297.2742841215836, 219.53815158190613, 36711] + +diff , 2026-01-06 13:16:11 , 440 + +area , 2026-01-06 13:16:11 , [296.0760037557924, 219.53815158190613, 37139] + +diff , 2026-01-06 13:16:12 , 440 + +area , 2026-01-06 13:16:12 , [292.1044333795706, 223.4390297150433, 36799] + +diff , 2026-01-06 13:16:13 , 440 + +area , 2026-01-06 13:16:13 , [292.4790590794493, 223.4390297150433, 36876] + +diff , 2026-01-06 13:16:14 , 438 + +area , 2026-01-06 13:16:14 , [292.1044333795706, 223.4390297150433, 36701] + +diff , 2026-01-06 13:16:15 , 436 + +area , 2026-01-06 13:16:15 , [295.62983611266304, 219.53815158190613, 37240] + +diff , 2026-01-06 13:16:16 , 436 + +area , 2026-01-06 13:16:16 , [296.0760037557924, 219.53815158190613, 36894] + +补料:20 + +================================2026-01-06 13:20:23,2026-01-06 13:24:22,B,3787 +area , 2026-01-06 13:24:22 , [222.8542124349459, 219.9931817125249, 35708] + +diff , 2026-01-06 13:24:23 , 628 + +area , 2026-01-06 13:24:23 , [226.59214461229675, 223.00896842952304, 35809] + +diff , 2026-01-06 13:24:24 , 628 + +area , 2026-01-06 13:24:24 , [222.49719099350446, 226.71568097509268, 35928] + +diff , 2026-01-06 13:24:25 , 628 + +area , 2026-01-06 13:24:25 , [291.44639301250584, 219.9931817125249, 36442] + +diff , 2026-01-06 13:24:26 , 445 + +area , 2026-01-06 13:24:26 , [226.59214461229675, 223.00896842952304, 36080] + +diff , 2026-01-06 13:24:27 , 451 + +area , 2026-01-06 13:24:27 , [291.8715470887836, 223.00896842952304, 36566] + +diff , 2026-01-06 13:24:28 , 450 + +area , 2026-01-06 13:24:28 , [232.14219780126146, 226.71568097509268, 35976] + +diff , 2026-01-06 13:24:29 , 449 + +area , 2026-01-06 13:24:29 , [236.24775131204953, 223.00896842952304, 36068] + +diff , 2026-01-06 13:24:30 , 447 + +area , 2026-01-06 13:24:30 , [226.59214461229675, 223.00896842952304, 36061] + +diff , 2026-01-06 13:24:31 , 451 + +area , 2026-01-06 13:24:31 , [226.59214461229675, 223.00896842952304, 36082] + +diff , 2026-01-06 13:24:32 , 448 + +area , 2026-01-06 13:24:32 , [228.7116088002531, 226.92069099136816, 36133] + +补料:14 + +================================2026-01-06 13:27:18,2026-01-06 13:31:49,B,4587 +area , 2026-01-06 13:31:50 , [225.656819085974, 226.71568097509268, 36039] + +diff , 2026-01-06 13:31:51 , 446 + +area , 2026-01-06 13:31:51 , [288.1336495447902, 227.1299187689724, 36865] + +diff , 2026-01-06 13:31:52 , 446 + +area , 2026-01-06 13:31:52 , [229.7520402520944, 223.00896842952304, 36074] + +diff , 2026-01-06 13:31:53 , 447 + +area , 2026-01-06 13:31:53 , [222.8542124349459, 226.71568097509268, 36004] + +diff , 2026-01-06 13:31:54 , 447 + +area , 2026-01-06 13:31:54 , [288.1336495447902, 226.71568097509268, 36669] + +diff , 2026-01-06 13:31:55 , 449 + +area , 2026-01-06 13:31:55 , [298.51130631853795, 215.5388596054085, 36684] + +diff , 2026-01-06 13:31:56 , 449 + +area , 2026-01-06 13:31:56 , [287.8975512226528, 226.92069099136816, 36957] + +diff , 2026-01-06 13:31:57 , 449 + +area , 2026-01-06 13:31:57 , [292.2259399848001, 223.00896842952304, 36961] + +diff , 2026-01-06 13:31:59 , 450 + +area , 2026-01-06 13:31:59 , [288.1336495447902, 223.6604569431083, 36748] + +diff , 2026-01-06 13:32:00 , 450 + +area , 2026-01-06 13:32:00 , [291.2541845192958, 218.27734651126764, 36470] + +diff , 2026-01-06 13:32:01 , 451 + +area , 2026-01-06 13:32:01 , [291.44639301250584, 218.68013169924697, 36329] + +补料:23 + +================================2026-01-06 13:34:34,2026-01-06 13:38:21,B,4445 +area , 2026-01-06 13:38:22 , [226.59214461229675, 223.00896842952304, 36074] + +diff , 2026-01-06 13:38:23 , 446 + +area , 2026-01-06 13:38:23 , [226.59214461229675, 223.00896842952304, 36009] + +diff , 2026-01-06 13:38:24 , 446 + +area , 2026-01-06 13:38:24 , [255.93163149560078, 223.8861317723811, 36109] + +diff , 2026-01-06 13:38:25 , 445 + +area , 2026-01-06 13:38:25 , [259.763353843455, 226.92069099136816, 36393] + +diff , 2026-01-06 13:38:26 , 447 + +area , 2026-01-06 13:38:26 , [226.59214461229675, 223.4390297150433, 36062] + +diff , 2026-01-06 13:38:27 , 447 + +area , 2026-01-06 13:38:27 , [219.49259668608414, 223.8861317723811, 36108] + +diff , 2026-01-06 13:38:28 , 447 + +area , 2026-01-06 13:38:28 , [222.8542124349459, 223.8861317723811, 36100] + +diff , 2026-01-06 13:38:29 , 447 + +area , 2026-01-06 13:38:29 , [222.8542124349459, 223.8861317723811, 36432] + +diff , 2026-01-06 13:38:30 , 447 + +area , 2026-01-06 13:38:30 , [227.30596120647607, 220.4654167891191, 36374] + +diff , 2026-01-06 13:38:31 , 449 + +area , 2026-01-06 13:38:31 , [226.59214461229675, 223.00896842952304, 36393] + +diff , 2026-01-06 13:38:32 , 449 + +area , 2026-01-06 13:38:32 , [222.49719099350446, 223.8861317723811, 36231] + +补料:13 + +================================2026-01-06 13:41:18,2026-01-06 13:46:19,B,4585 +diff , 2026-01-06 13:46:19 , 445 + +area , 2026-01-06 13:46:19 , [299.81661061388843, 217.55459085020476, 36444] + +diff , 2026-01-06 13:46:20 , 446 + +area , 2026-01-06 13:46:20 , [296.98484809834997, 223.4390297150433, 36393] + +diff , 2026-01-06 13:46:21 , 569 + +area , 2026-01-06 13:46:21 , [222.8542124349459, 223.8861317723811, 36042] + +diff , 2026-01-06 13:46:22 , 569 + +area , 2026-01-06 13:46:22 , [293.025596151599, 227.343352662883, 36691] + +diff , 2026-01-06 13:46:23 , 569 + +area , 2026-01-06 13:46:23 , [288.51343122981297, 231.0411218809327, 36527] + +diff , 2026-01-06 13:46:24 , 446 + +area , 2026-01-06 13:46:24 , [292.6106628269038, 223.8861317723811, 36411] + +diff , 2026-01-06 13:46:25 , 446 + +area , 2026-01-06 13:46:25 , [255.93163149560078, 227.343352662883, 36103] + +diff , 2026-01-06 13:46:26 , 449 + +area , 2026-01-06 13:46:26 , [241.6112580158466, 227.56098083810414, 35767] + +diff , 2026-01-06 13:46:27 , 450 + +area , 2026-01-06 13:46:27 , [293.025596151599, 227.343352662883, 36360] + +diff , 2026-01-06 13:46:28 , 570 + +area , 2026-01-06 13:46:28 , [252.10513679812237, 231.25094594401122, 35970] + +补料:5 + +================================2026-01-06 13:48:52,2026-01-06 13:53:47,B,3040 +area , 2026-01-06 13:53:48 , [253.3081127796739, 234.61031520374377, 39025] + +area , 2026-01-06 13:53:49 , [226.01327394646538, 239.40342520523802, 39706] + +area , 2026-01-06 13:53:50 , [278.4977558257876, 239.17566765873153, 39501] + +area , 2026-01-06 13:53:51 , [226.37358503146962, 242.42524621004307, 39117] + +area , 2026-01-06 13:53:52 , [277.09384691833196, 239.17566765873153, 39129] + +area , 2026-01-06 13:53:53 , [230.10649708341572, 239.17566765873153, 39036] + +area , 2026-01-06 13:53:54 , [233.8482413874434, 235.74774654278247, 38873] + +diff , 2026-01-06 13:53:55 , 484 + +area , 2026-01-06 13:53:55 , [237.59840066801797, 235.74774654278247, 40196] + +diff , 2026-01-06 13:53:56 , 484 + +area , 2026-01-06 13:53:56 , [227.1057022621845, 243.0740627874558, 39249] + +diff , 2026-01-06 13:53:57 , 479 + +area , 2026-01-06 13:53:57 , [227.1057022621845, 243.0740627874558, 39206] + +diff , 2026-01-06 13:53:58 , 480 + +area , 2026-01-06 13:53:58 , [227.1057022621845, 243.0740627874558, 39102] + +diff , 2026-01-06 13:53:59 , 485 + +diff , 2026-01-06 13:54:00 , 484 + +diff , 2026-01-06 13:54:01 , 484 + +diff , 2026-01-06 13:54:02 , 484 + +diff , 2026-01-06 13:54:03 , 483 + +diff , 2026-01-06 13:54:04 , 481 +