锂电池通信(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增加锂电池链路配置;归档锂电池协议文档
100 lines
3.8 KiB
Python
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()) |