Files
pickling-mes/backend/app/services/message_parser.py

349 lines
13 KiB
Python
Raw Normal View History

"""
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()