Files
H100PowerManger/test/pccu/pccu_ws_check.py
T
zjk 7e4d08808a pCCU新增锂电池通信/协调操作 + 电源协调状态机骨架(惰性)
锂电池通信(docs/锂电池协议20230324.docx):
- 协议层 BatProtocol:自检0x0000(2B)/设备控制0x0001(17B)/电池组状态0x0003(80B)/
  电池包报警0x0004(68B),uint32字节和校验、小端
- 链路层 BatLinkManager:动力/仪表锂电池独立UDP链路,本机监听与远端IP端口
  均由.moos配置(dyn_bat_*/ins_bat_*/bat_work_condition)
- SystemData/SnapshotBuilder/网页:电池数据按动力/仪表归类展示(运行状态/接触器
  电气量/电池包电压温度/报警),收发帧落库SQLite,网页新增电池标签页

操作->指令下发(PowerCoordinator):
- 操作接口:自检/上下电(母线接触器55H接通77H断开,含预充)/功率设定(0~500kW)/
  电池切换(低压->高压6步、高压->低压5步,按协议时序)
- 步骤引擎:每电池独立步骤队列,下发->状态反馈确认->超时兜底;周期1s心跳控制帧
- PM操控指令insBatCmd/dynBatCmd/dynBatPower自动映射为电池操作

电源协调状态机骨架(CoordFsm, TinyFSM):
- 事件Tick/PmControl/StepDone + 状态Normal/Switching/Fault,react均为TODO占位
- 当前未接线(无start/dispatch调用,运行行为与接入前一致),CoordFsm.hpp头部附接线指南

测试与部署:
- pccuTest新增电池协议编解码/帧长/链路分发用例(175项通过);集成测试覆盖电池
  心跳、启停映射(0x10->0x55/0x20->0x77)、状态落库;同步pccu_it.moos与ws_check
- h100.moos与pCCU.moos增加锂电池链路配置;归档锂电池协议文档
2026-08-30 14:40:27 +08:00

100 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""验证 pCCU WebSocket 推送的 JSON 快照格式与分组完整性。"""
import socket, base64, os, struct, time, json, sys
HOST, PORT = "127.0.0.1", 18081
class WsClient:
def __init__(self):
self.s = socket.create_connection((HOST, PORT), timeout=5)
key = base64.b64encode(os.urandom(16)).decode()
req = (f"GET /ws HTTP/1.1\r\nHost: {HOST}:{PORT}\r\n"
f"Upgrade: websocket\r\nConnection: Upgrade\r\n"
f"Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n")
self.s.sendall(req.encode())
self.buf = b""
# 读取 HTTP 101 响应头
while b"\r\n\r\n" not in self.buf:
chunk = self.s.recv(4096)
if not chunk:
raise RuntimeError("连接关闭")
self.buf += chunk
head, self.buf = self.buf.split(b"\r\n\r\n", 1)
if b"101" not in head.split(b"\r\n")[0]:
raise RuntimeError("WebSocket 升级失败: " + head.decode(errors="replace"))
def _read(self, n):
while len(self.buf) < n:
chunk = self.s.recv(4096)
if not chunk:
raise RuntimeError("连接关闭")
self.buf += chunk
out, self.buf = self.buf[:n], self.buf[n:]
return out
def recv_text(self, timeout=6):
self.s.settimeout(timeout)
end = time.time() + timeout
while time.time() < end:
if len(self.buf) < 2:
try:
chunk = self.s.recv(4096)
except socket.timeout:
return None
if not chunk:
return None
self.buf += chunk
continue
b1, b2 = self.buf[0], self.buf[1]
opcode = b1 & 0x0F
length = b2 & 0x7F
hdr_len = 2
self.buf = self.buf[2:] # 消费帧头前2字节
if length == 126:
length = struct.unpack(">H", self._read(2))[0]
hdr_len += 2
elif length == 127:
length = struct.unpack(">Q", self._read(8))[0]
hdr_len += 8
masked = (b2 & 0x80) != 0
mask = self._read(4) if masked else b""
payload = self._read(length)
if opcode == 1: # text
if masked:
payload = bytes(b ^ mask[i % 4] for i, b in enumerate(payload))
return payload.decode("utf-8", "replace")
# ping/pong/close 跳过,继续
return None
def main():
try:
ws = WsClient()
except Exception as e:
print("FAIL: WS 连接失败:", e)
return 1
text = ws.recv_text()
if not text:
print("FAIL: 未收到快照")
return 1
try:
data = json.loads(text)
except Exception as e:
print("FAIL: 快照非合法JSON:", text[:200])
return 1
ok = True
for key in ("fc", "pmCmd", "links", "fcStatusCount", "pmControlCount", "logs",
"batDyn", "batIns", "batDynAlarm", "batInsAlarm",
"batDynStatusCount", "batInsStatusCount"):
ok = ok and key in data
ok = ok and ("batDyn" in data.get("links", {})) and ("batIns" in data.get("links", {}))
print(f"PASS: 快照 JSON 有效, 分组keys={sorted(data.keys())}")
print(f" fc.fc_status={data['fc']['fc_status']}, fc.fc_mode={data['fc']['fc_mode']}, "
f"heartbeat={data['fc']['heartbeat']}")
print(f" batDyn.soc={data['batDyn'].get('soc')}, batIns.soc={data['batIns'].get('soc')}")
print(f" links={json.dumps(data['links'], ensure_ascii=False)}")
print(f" logs条数={len(data['logs'])}, fcStatusCount={data['fcStatusCount']}")
ws.s.close()
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())