""" L1报文解析服务 — UDP协议 假设报文格式(固定帧结构,收到实际协议文档后对应调整): ┌─────────────────────────────────────────────────────┐ │ Offset Size 说明 │ │ 0 2B 魔数 0xAA 0xBB │ │ 2 4B 报文类型 ASCII,如 "PC01" │ │ 6 2B 序列号 uint16 大端 │ │ 8 2B Body长度 uint16 大端 │ │ 10 2B 校验和 所有Body字节累加低16位 │ │ 12 N B Body GBK编码固定列宽文本 │ └─────────────────────────────────────────────────────┘ UDP最大包:65507字节,单帧不分片。 回执帧:Header相同结构,Body = "ACK" + 原序列号(2B) """ import asyncio import struct import time import uuid import json from datetime import datetime from typing import Optional, Dict, Any, Tuple from loguru import logger from app.config import settings # ─────────────────────────── 报文类型注册表 ─────────────────────────── MSG_TYPES: Dict[str, str] = { "PC01": "卷材入口报文", "PC02": "卷材出口报文", "PC03": "过程数据报文", "PC04": "质量数据报文", "PC05": "设备状态报文", "PC10": "计划下发报文", "PC11": "计划确认报文", "PC20": "心跳报文", } HEADER_SIZE = 12 # 报文头固定长度 MAGIC = b'\xAA\xBB' # ─────────────────────────── 校验和 ─────────────────────────── def _checksum(body: bytes) -> int: return sum(body) & 0xFFFF # ─────────────────────────── 报文头解析 ─────────────────────────── def parse_header(raw: bytes) -> Optional[Dict[str, Any]]: if len(raw) < HEADER_SIZE: logger.warning(f"报文过短: {len(raw)}B,丢弃") return None magic = raw[0:2] if magic != MAGIC: logger.warning(f"魔数错误: {magic.hex()},丢弃") return None msg_type = raw[2:6].decode("ascii", errors="replace").strip() seq = struct.unpack(">H", raw[6:8])[0] body_len = struct.unpack(">H", raw[8:10])[0] checksum = struct.unpack(">H", raw[10:12])[0] body = raw[HEADER_SIZE: HEADER_SIZE + body_len] if len(body) < body_len: logger.warning(f"Body不完整: 期望{body_len}B 实际{len(body)}B") return None if _checksum(body) != checksum: logger.warning(f"校验和错误 [{msg_type}] seq={seq},丢弃") return None return {"msg_type": msg_type, "seq": seq, "body": body, "body_str": body.decode("gbk", errors="replace")} # ─────────────────────────── 构建回执帧 ─────────────────────────── def build_ack(seq: int) -> bytes: body = b"ACK" + struct.pack(">H", seq) hdr = MAGIC hdr += b"ACK ".ljust(4)[:4] hdr += struct.pack(">H", seq) hdr += struct.pack(">H", len(body)) hdr += struct.pack(">H", _checksum(body)) return hdr + body # ─────────────────────────── 构建发送帧 ─────────────────────────── def build_frame(msg_type: str, body: bytes, seq: int = 0) -> bytes: hdr = MAGIC hdr += msg_type.encode("ascii").ljust(4)[:4] hdr += struct.pack(">H", seq) hdr += struct.pack(">H", len(body)) hdr += struct.pack(">H", _checksum(body)) return hdr + body # ─────────────────────────── Body解析器 ─────────────────────────── class PC01Parser: """卷材入口报文 Body固定列宽(GBK): 卷号(20) 钢种(10) 厚度(6) 宽度(6) 重量(8) 班次(2) """ def parse(self, body: str) -> Dict[str, Any]: return { "coil_no": body[0:20].strip(), "steel_grade": body[20:30].strip(), "thickness": _safe_float(body[30:36]), "width": _safe_float(body[36:42]), "weight": _safe_float(body[42:50]), "shift": body[50:52].strip(), "event_time": datetime.now(), } class PC02Parser: """卷材出口报文 Body: 卷号(20) 实测厚度(6) 实测宽度(6) 处理长度(8) 平均速度(6) 质量等级(2) """ def parse(self, body: str) -> Dict[str, Any]: return { "coil_no": body[0:20].strip(), "actual_thickness": _safe_float(body[20:26]), "actual_width": _safe_float(body[26:32]), "process_length": _safe_float(body[32:40]), "avg_speed": _safe_float(body[40:46]), "quality_grade": body[46:48].strip(), "event_time": datetime.now(), } class PC03Parser: """过程数据报文(周期推送) Body: 卷号(20) 位置(10) 速度(6) 入口张力(8) 出口张力(8) 酸液温度(6) """ def parse(self, body: str) -> Dict[str, Any]: return { "coil_no": body[0:20].strip(), "position": body[20:30].strip(), "speed": _safe_float(body[30:36]), "tension_inlet": _safe_float(body[36:44]), "tension_outlet": _safe_float(body[44:52]), "acid_temp": _safe_float(body[52:58]), "event_time": datetime.now(), } class PC04Parser: """质量数据报文 Body: 卷号(20) 缺陷类型(10) 缺陷位置(8) 严重程度(2) """ def parse(self, body: str) -> Dict[str, Any]: return { "coil_no": body[0:20].strip(), "defect_type": body[20:30].strip(), "defect_pos": _safe_float(body[30:38]), "severity": body[38:40].strip(), "event_time": datetime.now(), } class PC05Parser: """设备状态报文 Body: 设备编号(10) 状态码(4) 故障码(6) 时间戳(14 yyyyMMddHHmmss) """ def parse(self, body: str) -> Dict[str, Any]: ts_str = body[20:34].strip() try: ts = datetime.strptime(ts_str, "%Y%m%d%H%M%S") except Exception: ts = datetime.now() return { "equipment_code": body[0:10].strip(), "status_code": body[10:14].strip(), "fault_code": body[14:20].strip(), "event_time": ts, } class PC20Parser: """心跳报文 Body: 时间戳(14)""" def parse(self, body: str) -> Dict[str, Any]: return {"event_time": datetime.now(), "raw_ts": body[0:14].strip()} def _safe_float(s: str) -> float: try: return float(s.strip()) except (ValueError, AttributeError): return 0.0 BODY_PARSERS: Dict[str, Any] = { "PC01": PC01Parser(), "PC02": PC02Parser(), "PC03": PC03Parser(), "PC04": PC04Parser(), "PC05": PC05Parser(), "PC20": PC20Parser(), } # ─────────────────────────── 分发器 ─────────────────────────── class MessageDispatcher: def __init__(self): self._handlers: Dict[str, list] = {} def register(self, msg_type: str): def decorator(func): self._handlers.setdefault(msg_type, []).append(func) return func return decorator async def dispatch(self, msg_type: str, data: Dict[str, Any]): for handler in self._handlers.get(msg_type, []): try: await handler(data) except Exception as e: logger.error(f"报文处理器异常 [{msg_type}]: {e}") dispatcher = MessageDispatcher() # ─────────────────────────── UDP 服务端 ─────────────────────────── class L1UdpProtocol(asyncio.DatagramProtocol): """asyncio UDP DatagramProtocol 实现""" def __init__(self, server: "L1UdpServer"): self._server = server self.transport: Optional[asyncio.DatagramTransport] = None def connection_made(self, transport: asyncio.DatagramTransport): self.transport = transport host, port = transport.get_extra_info("sockname") logger.info(f"UDP监听启动: {host}:{port}") def datagram_received(self, data: bytes, addr: Tuple[str, int]): asyncio.create_task(self._server.handle(data, addr, self.transport)) def error_received(self, exc: Exception): logger.error(f"UDP错误: {exc}") def connection_lost(self, exc): logger.warning(f"UDP连接丢失: {exc}") class L1UdpServer: """UDP报文接收服务""" def __init__(self): self._transport: Optional[asyncio.DatagramTransport] = None self._running = False # 统计 self.recv_count = 0 self.error_count = 0 async def start(self): self._running = True loop = asyncio.get_running_loop() self._transport, _ = await loop.create_datagram_endpoint( lambda: L1UdpProtocol(self), local_addr=(settings.L1_HOST, settings.L1_PORT), ) logger.info(f"L1 UDP服务已启动,监听 {settings.L1_HOST}:{settings.L1_PORT}") async def handle(self, raw: bytes, addr: Tuple[str, int], transport: asyncio.DatagramTransport): t0 = time.time() self.recv_count += 1 logger.debug(f"收到UDP包 from {addr[0]}:{addr[1]} {len(raw)}B") header = parse_header(raw) if not header: self.error_count += 1 await self._save_log(raw, addr, None, "error", "报文头解析失败", t0) return msg_type = header["msg_type"] seq = header["seq"] # 发送ACK回执 if msg_type != "ACK": ack = build_ack(seq) transport.sendto(ack, addr) # 心跳不做业务处理 if msg_type == "PC20": logger.debug(f"心跳 seq={seq}") return # Body解析 body_parser = BODY_PARSERS.get(msg_type) data: Dict[str, Any] = {} if body_parser: try: data = body_parser.parse(header["body_str"]) except Exception as e: logger.error(f"Body解析异常 [{msg_type}]: {e}") self.error_count += 1 await self._save_log(raw, addr, header, "error", str(e), t0) return else: logger.warning(f"未知报文类型: {msg_type}") elapsed_ms = (time.time() - t0) * 1000 logger.info(f"[{msg_type}] seq={seq} from {addr[0]} 耗时{elapsed_ms:.1f}ms") await self._save_log(raw, addr, header, "success", None, t0, data) await dispatcher.dispatch(msg_type, data) async def _save_log(self, raw: bytes, addr: Tuple[str, int], header: Optional[Dict], status: str, error_msg: Optional[str], t0: float, parsed_data: Optional[Dict] = None): try: from app.database import AsyncSessionLocal from app.models.message import MessageLog elapsed_ms = (time.time() - t0) * 1000 async with AsyncSessionLocal() as db: log = MessageLog( msg_id=str(uuid.uuid4())[:16], msg_type=header["msg_type"] if header else "UNKNOWN", direction="recv", source=f"{addr[0]}:{addr[1]}", raw_data=raw.hex(), parsed_data=json.dumps(parsed_data, default=str) if parsed_data else None, status=status, error_msg=error_msg, process_time=round(elapsed_ms, 2), received_at=datetime.now(), ) db.add(log) await db.commit() except Exception as e: logger.error(f"保存报文日志失败: {e}") def send(self, data: bytes, addr: Tuple[str, int]): """主动向L1发送报文""" if self._transport: self._transport.sendto(data, addr) else: raise RuntimeError("UDP服务未启动") def stop(self): self._running = False if self._transport: self._transport.close() # 全局单例 l1_server = L1UdpServer()