简介:面向欧标充电桩开发、联调与协议学习者的 OCPP1.6 消息事件 JSON 定义合集,重点解决充电桩与后台之间请求、响应报文不清晰、字段约束难查找的问题,也适合在 Linux 环境下进行桩端或云端模拟联调时查阅。压缩包共 78 个文件,全部为 JSON 格式,整体约 28KB,按消息事件分类组织,覆盖注册启动、授权、开始与停止交易、计量值上报、状态通知、远程启停、配置查询等典型交互,并给出成对的请求与响应结构。内容逐项提示必要字段和可选字段,便于按一问一答的通信模式拼装报文,模拟充电桩业务流程,快速核对字段含义、响应格式与消息配对关系。已有 5474 人学习,适合需要梳理 OCPP1.6 报文结构、排查字段缺失、验证消息顺序或搭建模拟通信流程的开发者作为速查与对照材料。
1. 从一条 BootNotification 的 JSON 报错说起:OCPP 1.6J 到底长什么样
欧标充电桩的 OCPP1.6 消息事件 JSON 格式,是我在联调现场被问得最多的一类问题。桩侧开发觉得字段都填了,CSMS 侧却只回一个FormationViolation;现场运维抓到的日志里,一串[2,"...","BootNotification",{...}]看不出哪里错。Linux 下用wscat连上去,心跳正常,一到交易就断,最后发现是connectorId写成了字符串。这类问题不玄学,根子在 OCPP 1.6J 的消息帧和 payload 约束没有被逐字段对过。
这份资料围绕欧标充电桩 OCPP1.6 的消息事件 JSON 格式展开,适合做桩侧协议对接、CSMS 侧消息解析、现场日志排查的人。它不替代规范原文,但能把 CALL、CALLRESULT、CALLERROR 的数组结构、常用 Action 的字段、Linux 下的联调命令和典型翻车点串成一条可复现的线。新手可以照着发消息,熟手可以拿字段表和避坑清单去卡边界。
2. 拆开 OCPP 1.6J 消息帧:四元组、Action 与 CALLERROR 的 JSON 格式
OCPP 1.6J 跑在 WebSocket 上,JSON 消息不是随便一个对象,而是一个固定长度的数组。数组第一个元素是消息类型编号,第二个是uniqueId,后面跟着 Action 或错误信息。很多人第一次对接时把 payload 直接发成对象,CSMS 返回ProtocolError,日志里只看到连接被关闭,其实根子在消息帧第一层就没对上。下面把三类消息拆开看,字段含义和常见误用一起过。
2.1 消息数组四元组:2、3、4 分别代表什么
CALL 是桩侧或 CSMS 侧发起的请求,格式为[2, uniqueId, action, payload]。CALLRESULT 是对请求的响应,格式为[3, uniqueId, payload]。CALLERROR 是错误响应,格式为[4, uniqueId, errorCode, errorDescription, errorDetails]。这四个位置不能多也不能少,uniqueId必须是字符串,action必须是 PascalCase 的字符串,payload必须是 JSON 对象,不能是数组或裸字符串。
[ 2, "8f3c1a2e-4b7d-4c9a-9f10-2d6e8a1b5c70", "BootNotification", { "chargePointVendor": "VendorA", "chargePointModel": "ModelX", "chargePointSerialNumber": "CP-SN-0001", "firmwareVersion": "1.0.0" } ]这段是桩侧上线的 CALL。2表示请求;uniqueId用 UUID 或全局递增字符串,CSMS 靠它把响应配回来;BootNotification是 Action 名,大小写敏感;payload 里chargePointVendor和chargePointModel是必填,序列号和固件版本按需填。常见错误是把uniqueId写成数字,或者在同一连接里复用同一个 ID,导致响应回来时分不清是哪条请求。
CALLRESULT 只带三个元素,uniqueId必须和请求一致,payload 里放业务响应。比如 BootNotification 的响应会带status、currentTime和interval。status取Accepted、Pending、Rejected,currentTime是 CSMS 当前时间,interval是心跳间隔秒数。桩侧拿到Accepted后才算注册成功,拿到Pending要等 CSMS 再下发,拿到Rejected一般要检查白名单或桩身份配置。
[ 3, "8f3c1a2e-4b7d-4c9a-9f10-2d6e8a1b5c70", { "status": "Accepted", "currentTime": "2025-03-18T08:12:30Z", "interval": 300 } ]这段响应的currentTime用 UTC ISO8601 带Z,不要用本地时间;interval单位是秒,桩侧要拿它覆盖本地心跳周期。有人把interval当成毫秒,结果 300 秒变成 300 毫秒,心跳风暴把 CSMS 打限流。字段名大小写也要对,currentTime不是currenttime,status不是Status。
2.2 Action 与 payload:常用消息的字段边界
OCPP 1.6 的 Action 名是固定的,桩侧上线、授权、启停交易、上报电表、状态通知各有一条。字段名同样大小写敏感,类型不能漂移。下面这张表是联调时最常对照的几组,列出请求必填字段和容易写错的类型。
| Action | 请求关键字段 | 响应关键字段 | 常见类型错误 |
|---|---|---|---|
| BootNotification | chargePointVendor, chargePointModel | status, currentTime, interval | interval 写成字符串 |
| Heartbeat | 无 | currentTime | 发成空数组而不是空对象 |
| Authorize | idTag | idTagInfo.status | idTag 大小写被改 |
| StartTransaction | connectorId, idTag, meterStart, timestamp | transactionId, idTagInfo | connectorId 写成 "1" |
| MeterValues | connectorId, meterValue | 空对象 | sampledValue 不是数组 |
| StopTransaction | transactionId, meterStop, timestamp | idTagInfo | meterStop 小于 meterStart |
| StatusNotification | connectorId, errorCode, status | 空对象 | status 用了自定义词 |
以 Authorize 为例,请求 payload 只有一个idTag,响应里idTagInfo.status取Accepted、Blocked、Expired、Invalid、ConcurrentTx。桩侧拿到Accepted才允许启动,拿到ConcurrentTx要检查同一个 idTag 是否已经在别的枪上交易。有人把idTag全转小写再发,CSMS 侧按大小写敏感匹配白名单,直接返回Invalid,现场看起来像卡失效,其实是字符串被改了。
[ 2, "0b6d9e2a-7c41-4f8e-b3a2-1d5c9e7f2a11", "Authorize", { "idTag": "RFID-A1B2C3" } ]这段请求里idTag保持原始大小写,不要做 trim 之外的处理。uniqueId换一个新的,不要和上一条 BootNotification 复用。响应回来先看idTagInfo.status,再看有没有expiryDate。如果 CSMS 返回ConcurrentTx,不要立刻重试,先查同 idTag 的活跃 transactionId,否则会触发更严格的限流。
2.3 CALLERROR:错误码不是装饰,排查先看这四个字段
CALLERROR 的数组是[4, uniqueId, errorCode, errorDescription, errorDetails]。errorCode是规范枚举,常见有NotImplemented、NotSupported、InternalError、ProtocolError、SecurityError、FormationViolation、PropertyConstraintViolation、OccurenceConstraintViolation、TypeConstraintViolation、GenericError。errorDescription是给人看的短句,errorDetails是对象,可以放字段名和原因。排查时先看errorCode,再看errorDescription有没有指到具体字段。
[ 4, "0b6d9e2a-7c41-4f8e-b3a2-1d5c9e7f2a11", "FormationViolation", "connectorId must be an integer", { "action": "StartTransaction", "field": "connectorId", "receivedType": "string" } ]这段错误响应说明请求帧结构不对,connectorId期望整数却收到字符串。桩侧不要只把errorDescription打到日志就完事,要把errorDetails.field取出来做校验分支。ProtocolError多半是消息数组长度不对或uniqueId类型错;NotImplemented是 Action 没实现;NotSupported是 Action 实现了但当前状态不允许。把这几类分开,现场排查能少走很多弯路。
注意:CALLERROR 的
uniqueId必须对应原请求,不能新生成。有人错误响应里换了 ID,CSMS 侧挂起的请求永远等不到结果,最后超时断开。
3. 充电桩上线与保活:BootNotification、Heartbeat 的字段配置与 Linux 联调
桩侧第一件事是连上 CSMS 并完成 BootNotification,第二件事是按 CSMS 给的心跳间隔保活。很多“桩在线但不下发命令”的问题,根子在 BootNotification 响应里的interval没被正确应用,或者 Heartbeat 发成了空数组。Linux 下用 WebSocket 客户端手动发一遍,比在桩上改代码再烧录快得多。下面按上线顺序拆字段和命令。
3.1 BootNotification 的 JSON 字段与 CSMS 响应处理
BootNotification 的请求 payload 里chargePointVendor和chargePointModel是必填,chargePointSerialNumber、chargeBoxSerialNumber、firmwareVersion、iccid、imsi、meterType、meterSerialNumber按桩的能力填。字段名不要自创,比如把chargePointVendor写成vendor,CSMS 会直接回FormationViolation。chargePointSerialNumber建议和桩身铭牌一致,现场换板后如果序列号没改,CSMS 侧会认为是同一台桩,诊断和固件升级都会串。
[ 2, "b1e7c3a4-5d2f-4a8b-9c6e-7f1d2a3b4c5d", "BootNotification", { "chargePointVendor": "VendorA", "chargePointModel": "ModelX", "chargePointSerialNumber": "CP-SN-0001", "chargeBoxSerialNumber": "CB-SN-0001", "firmwareVersion": "1.0.0", "iccid": "89860000000000000000", "imsi": "460000000000000", "meterType": "AC", "meterSerialNumber": "METER-SN-0001" } ]这段请求里iccid和imsi是可选字段,没有就省略,不要填空字符串。meterType用AC或DC,不要写ac。响应回来先判断status:Accepted表示注册成功,Pending表示 CSMS 还在处理,Rejected表示身份不被接受。拿到Accepted后把currentTime和本地时间做差,超过阈值就触发对时;把interval写入心跳配置,单位秒。
[ 3, "b1e7c3a4-5d2f-4a8b-9c6e-7f1d2a3b4c5d", { "status": "Accepted", "currentTime": "2025-03-18T08:15:00Z", "interval": 300 } ]响应里的interval是 CSMS 建议的心跳周期,桩侧一般按它覆盖本地HeartbeatInterval。如果本地配置是 60 秒,CSMS 回 300,就改成 300;不要两边各按各的,否则 CSMS 侧会看到心跳过密或过疏。currentTime是 UTC,桩侧如果只有本地时区,要做一次偏移换算再存。status为Pending时不要立刻重发 BootNotification,按 CSMS 下发的interval等下一轮或等TriggerMessage。
3.2 Heartbeat 与配置键:空对象不是空数组
Heartbeat 的请求 payload 是空对象{},不是空数组[],也不是null。响应 payload 只有currentTime。这条消息看起来简单,但联调时经常因为发成[]被回FormationViolation。心跳周期由 CSMS 的HeartbeatInterval配置键控制,桩侧也可以用ChangeConfiguration改,但要看 CSMS 是否允许。GetConfiguration可以读当前值,请求里带key数组,响应里带configurationKey和unknownKey。
[ 2, "c2f8d4b5-6e3a-4b9c-8d7f-9a2e3b4c5d6e", "Heartbeat", {} ]这段请求的 payload 是空对象,uniqueId每次换新。响应里的currentTime可以用来校正本地时钟,但不要每次心跳都强行写 RTC,频繁写有寿命风险。心跳间隔一般 300 秒,现场网络差可以降到 120 秒,但不要低于 CSMS 限制。HeartbeatInterval的单位是秒,MeterValueSampleInterval也是秒,两个配置别混。
[ 2, "d3a9e5c6-7f4b-4c0d-9e8a-1b3f4c5d6e7f", "GetConfiguration", { "key": [ "HeartbeatInterval", "MeterValueSampleInterval", "ConnectionTimeOut" ] } ]这段是读配置的 CALL。key是字符串数组,不传就返回全部。响应里configurationKey是对象数组,每项有key、readonly、value;unknownKey是 CSMS 不认识的键。桩侧拿到value后要做类型转换,HeartbeatInterval是字符串形式的数字,不要直接当整数用。readonly为 true 的键不能通过ChangeConfiguration改,改了会回NotSupported。
3.3 Linux 下用 websocat 手动跑一条 CALL
Linux 环境联调,最省事的是websocat或wscat。连接 URL 一般是ws://<csms-host>/ocpp/<chargePointIdentity>,WebSocket 子协议要带ocpp1.6。下面用websocat连本地 CSMS,手动发一条 BootNotification,看响应回来是什么。命令里的地址和桩身份按实际替换,不要直接抄到生产。
# 连接 CSMS,带 OCPP 1.6 子协议;-H 指定 Sec-WebSocket-Protocol websocat -H="Sec-WebSocket-Protocol: ocpp1.6" \ ws://csms.local/ocpp/CP-001 # 连接成功后,在交互输入里粘贴下面这行 JSON(单行) [2,"e4b0f6d7-8a5c-4d1e-af9b-2c4a5d6e7f80","BootNotification",{"chargePointVendor":"VendorA","chargePointModel":"ModelX","chargePointSerialNumber":"CP-SN-0001"}]第一条命令建立 WebSocket 连接,Sec-WebSocket-Protocol: ocpp1.6必须带,不带有的 CSMS 会直接握手失败。ws://csms.local/ocpp/CP-001里的CP-001是桩身份,要和 CSMS 侧配置一致。第二条是单行 JSON,粘贴后回车发送。响应会以[3,"e4b0...",{...}]回来,如果回[4,...]就看errorCode和errorDetails。
# 用 wscat 的等价命令;-s 指定子协议,-c 指定连接地址 wscat -s ocpp1.6 -c ws://csms.local/ocpp/CP-001 # 发送 Heartbeat;注意 payload 是空对象,不是空数组 [2,"f5c1a7e8-9b6d-4e2f-b0ac-3d5b6e7f8091","Heartbeat",{}]wscat的-s参数指定子协议,-c指定地址。Heartbeat 的 payload 写{},不要写[]。如果连接被 CSMS 主动关闭,先看握手阶段是否返回 404 或 400:404 多半是 URL 里的桩身份不对,400 多半是子协议没带或不被接受。把这两步跑通,再回到桩侧代码里对字段,效率会高很多。
4. 交易链路 JSON:Authorize、StartTransaction、MeterValues、StopTransaction 的状态衔接
交易链路是 OCPP 1.6J 里最容易出顺序问题的一段。Authorize 过了才能 StartTransaction,StartTransaction 的响应拿到transactionId才能发 MeterValues,StopTransaction 的meterStop不能小于meterStart。每一步的 JSON 字段和上一步的响应字段咬合,错一个位置,CSMS 侧就会把交易记成异常。下面按一次完整充电的顺序拆。
4.1 Authorize 与 StartTransaction:idTag 和 transactionId 的传递
Authorize 的请求只有idTag,响应idTagInfo.status为Accepted才继续。StartTransaction 的请求要带connectorId、idTag、meterStart、timestamp,reservationId可选。connectorId是整数,从 1 开始;meterStart是电表起始读数,单位 Wh,整数;timestamp是 UTC ISO8601 带Z。响应里transactionId是 CSMS 分配的整数,后续 MeterValues 和 StopTransaction 都要带它。
[ 2, "a6d2b8f9-0c7e-4f3a-91bd-4e6c7f809a12", "StartTransaction", { "connectorId": 1, "idTag": "RFID-A1B2C3", "meterStart": 12000, "timestamp": "2025-03-18T08:20:00Z" } ]这段请求里connectorId是整数 1,不要加引号;meterStart是 12000 Wh,不是 12.0 kWh;timestamp用 UTC,现场如果只有本地时间,先换算再发。响应里transactionId可能是 1001 这样的整数,idTagInfo.status再次确认授权状态。拿到transactionId后存到会话上下文,后面每条 MeterValues 都带同一个值。
[ 3, "a6d2b8f9-0c7e-4f3a-91bd-4e6c7f809a12", { "transactionId": 1001, "idTagInfo": { "status": "Accepted" } } ]响应里的transactionId不要自己生成,必须用 CSMS 返回的。有人桩侧本地先生成一个 ID,CSMS 又返回另一个,MeterValues 带本地 ID 被回FormationViolation或PropertyConstraintViolation。idTagInfo.status如果不是Accepted,要按状态处理:Blocked拒绝启动,Expired提示卡过期,ConcurrentTx查并发交易。
4.2 MeterValues:sampledValue 的数组结构和采样上下文
MeterValues 的请求带connectorId、可选transactionId、meterValue数组。meterValue每一项有timestamp和sampledValue数组;sampledValue每一项有value、context、format、measurand、phase、location、unit。value是字符串形式的数字,measurand常用Energy.Active.Import.Register、Power.Active.Import、Current.Import、Voltage。context常用Sample.Periodic、Transaction.Begin、Transaction.End。
[ 2, "b7e3c9a0-1d8f-4a4b-82ce-5f7d809a1b23", "MeterValues", { "connectorId": 1, "transactionId": 1001, "meterValue": [ { "timestamp": "2025-03-18T08:25:00Z", "sampledValue": [ { "value": "12345", "context": "Sample.Periodic", "format": "Raw", "measurand": "Energy.Active.Import.Register", "unit": "Wh" }, { "value": "7.2", "context": "Sample.Periodic", "measurand": "Power.Active.Import", "unit": "kW" } ] } ] } ]这段请求里transactionId用 StartTransaction 响应里的 1001;meterValue是数组,里面再套sampledValue数组;value是字符串,不是数字。unit和measurand要配对,能量用Wh或kWh,功率用W或kW。如果 CSMS 侧按measurand建曲线,measurand拼错不会报错,但数据会落到未知序列,现场看就是“有数据但图表空”。
提示:MeterValues 的
transactionId在交易开始前可以省略,但交易开始后建议每条都带。省略后 CSMS 可能按connectorId猜,遇到多枪并发就会串。
4.3 StopTransaction 与 StatusNotification:结束原因和枪状态
StopTransaction 的请求带transactionId、meterStop、timestamp,可选reason和transactionData。meterStop是结束读数,单位 Wh,不能小于meterStart。reason常用Local、Remote、EVDisconnected、PowerLoss、Reboot、EmergencyStop。响应里idTagInfo可选,主要看 CSMS 是否接受这次结束。结束后发 StatusNotification,把枪状态改回Available或Faulted。
[ 2, "c8f4d0b1-2e9a-4b5c-93df-6a8e9a1b2c34", "StopTransaction", { "transactionId": 1001, "meterStop": 12890, "timestamp": "2025-03-18T09:10:00Z", "reason": "Local" } ]这段请求里meterStop是 12890 Wh,比meterStart的 12000 大,差值是本次充电量。reason用Local表示本地主动结束,远程下发停止用Remote。如果meterStop小于meterStart,CSMS 可能回PropertyConstraintViolation,也可能把交易记成负电量。结束响应回来后不要立刻断电,先发 StatusNotification。
[ 2, "d9a5e1c2-3f0b-4c6d-84e0-7b9f0a2c3d45", "StatusNotification", { "connectorId": 1, "errorCode": "NoError", "status": "Available", "timestamp": "2025-03-18T09:10:05Z" } ]errorCode用NoError表示无故障,status用Available表示枪可用。如果枪口有故障,errorCode换成ConnectorLockFailure、GroundFailure、OverCurrentFailure等,status用Faulted。timestamp可选但建议带,现场排查时能对上事件顺序。状态通知发完再断开继电器,避免 CSMS 侧看到“交易结束但枪仍占用”的中间态。
5. 联调避坑:五类 OCPP 1.6 JSON 报错的现象、原因与排查
OCPP 1.6J 的坑大多不在协议本身,而在“字段看起来对、类型和顺序不对”。下面五条是联调里反复出现的翻车记录,每条按现象、原因、解决拆开。现场遇到问题时,先按消息类型归类,再按字段逐项对,比盯着日志猜要快。
5.1 连接与握手类:404、400 和子协议不匹配
现象是 WebSocket 客户端连不上,返回 404 或 400,桩侧日志只写“连接失败”。原因常见三种:URL 里少了桩身份,比如只写ws://csms.local/ocpp;子协议没带ocpp1.6;CSMS 侧桩身份大小写和 URL 不一致。解决是先确认完整 URL 形如ws://csms.local/ocpp/CP-001,再确认握手头带Sec-WebSocket-Protocol: ocpp1.6,最后对 CSMS 的桩注册表核大小写。
# 排查握手:-v 打印握手细节,看返回码和子协议 websocat -v -H="Sec-WebSocket-Protocol: ocpp1.6" \ ws://csms.local/ocpp/CP-001-v会打印请求头和响应头,返回 404 看 URL 路径,返回 400 看Sec-WebSocket-Protocol。如果 CSMS 返回101 Switching Protocols但没带子协议,有的客户端会继续,有的会断开,桩侧要按客户端行为统一。
5.2 时间戳与类型类:UTC、整数和字符串的边界
现象是 BootNotification 或 StartTransaction 被回FormationViolation,errorDescription指到timestamp或connectorId。原因是时间戳用了本地时间没带时区,或者connectorId写成"1",或者meterStart写成12.0。解决是统一 UTC ISO8601 带Z,connectorId用整数,meterStart、meterStop用整数 Wh,value用字符串。
{ "connectorId": 1, "meterStart": 12000, "timestamp": "2025-03-18T08:20:00Z" }这段是正确类型示例。connectorId不加引号,meterStart不带小数,timestamp带Z。如果现场只有本地时间,用date -u或代码里的 UTC 转换,不要手写偏移。idTag保持原始大小写,不要做小写化。
5.3 uniqueId 与响应配对类:重复 ID 导致请求挂起
现象是桩侧发了请求,CSMS 回了响应,但桩侧业务层没收到,最后超时重发。原因是uniqueId重复或为空,响应回来被配到旧请求上;或者 CALLERROR 里换了新 ID。解决是每个 CALL 生成唯一字符串,推荐 UUID;响应处理按uniqueId建映射,收到 CALLRESULT 或 CALLERROR 后删除映射;错误响应必须复用原请求 ID。
import uuid def next_unique_id(): # 每次 CALL 都生成新的 UUID 字符串,不要复用 return str(uuid.uuid4()) pending = {} uid = next_unique_id() pending[uid] = {"action": "Heartbeat", "sent_at": None}这段 Python 片段用 UUID 生成uniqueId,pending字典按 ID 存上下文。发送前写入,收到响应后按 ID 取出并删除。不要用全局自增整数不加前缀,多线程下容易撞;也不要把uniqueId设成空字符串。
5.4 交易顺序类:transactionId 缺失和 meterStop 倒挂
现象是 MeterValues 被回PropertyConstraintViolation,或者 StopTransaction 后 CSMS 记的电量是负数。原因是 StartTransaction 响应还没回来就发 MeterValues,transactionId为空;或者meterStop小于meterStart。解决是把交易状态机写成“授权通过 → 启动请求 → 收到 transactionId → 允许采样 → 结束请求 → 收到响应 → 状态通知”,每一步卡住就不往下走。
| 阶段 | 允许发送 | 必须拿到 |
|---|---|---|
| 授权后 | StartTransaction | idTagInfo.status=Accepted |
| 启动响应后 | MeterValues | transactionId 整数 |
| 结束请求后 | StatusNotification | StopTransaction 响应 |
这张表可以直接贴到联调 notes 里。transactionId没拿到之前,MeterValues 最多发Transaction.Begin的采样,并且不要带transactionId。meterStop发送前做一次比较,小于meterStart就本地报警,不要发出去。
5.5 日志定位类:只打 errorDescription 不够
现象是现场日志只有一行FormationViolation,不知道哪个字段错。原因是桩侧只打了errorDescription,没打errorDetails和原始请求。解决是日志里同时记录uniqueId、Action、请求 payload、响应errorCode、errorDetails,并且对 payload 做脱敏。Linux 下可以用jq过滤,把每条 CALL 和响应按uniqueId配对。
# 从日志里按 uniqueId 找一条请求和它的错误响应 grep 'a6d2b8f9-0c7e-4f3a-91bd-4e6c7f809a12' ocpp.log | jq -c .grep先按 ID 缩小范围,jq -c压缩输出方便看数组结构。日志里不要打完整 idTag,做掩码。把errorDetails.field单独提取出来,能直接定位到字段名。
6. 日志回放与模拟器验证:把 OCPP 1.6J 消息流跑通的一个笨办法
现场日志拿回来,最怕只看单条消息。OCPP 1.6J 的问题经常是“请求发了、响应回了、下一条没发”,单条看不出断点。我一般会把日志里的 CALL 和 CALLRESULT 按uniqueId配对,算每条请求的响应耗时,再把未配对的 CALL 拎出来。这个笨办法不依赖 CSMS 后台,Linux 上一个 Python 脚本就能跑。
import json import sys def replay(path): pending = {} for line in open(path, encoding="utf-8"): line = line.strip() if not line: continue try: msg = json.loads(line) except json.JSONDecodeError: continue if not isinstance(msg, list) or len(msg) < 3: continue msg_type, uid = msg[0], msg[1] if msg_type == 2: pending[uid] = {"action": msg[2], "payload": msg[3]} elif msg_type in (3, 4): req = pending.pop(uid, None) if req: print(f"{uid} {req['action']} -> type={msg_type}") for uid, req in pending.items(): print(f"UNMATCHED {uid} {req['action']}") if __name__ == "__main__": replay(sys.argv[1])这段脚本读一行一条 JSON 的日志,msg_type == 2时把uniqueId、Action、payload 存进pending;收到 3 或 4 时按 ID 弹出并打印配对结果;最后剩下的就是没等到响应的 CALL。参数path是日志文件路径,日志里每行必须是完整 JSON 数组,不能有前缀时间戳。如果日志带时间戳,先用sed或jq剥掉再喂给脚本。跑完看UNMATCHED,一般能直接定位到“哪条消息发出去后连接断了”。
模拟器验证放在回放之后。用websocat或wscat连 CSMS,按 BootNotification → Heartbeat → Authorize → StartTransaction → MeterValues → StopTransaction 的顺序手动发一遍,每一步只看响应类型和关键字段。不要一上来就发交易,先把上线和心跳跑通。CSMS 侧如果支持TriggerMessage,可以让它主动要 Heartbeat 或 MeterValues,验证桩侧被动响应路径。回放脚本和模拟器命令放在同一个目录,现场换桩时先跑回放,再跑模拟器,最后才上真桩。从那以后我每次上线新桩,都先拿这份 JSON 样例跑一遍回放,再上现场。希望帮到你。
本文还有配套的精品资源,点击获取