简介:欧标充电桩OCPP1.6通信协议原文,覆盖所有消息事件的JSON格式定义,面向充电桩嵌入式开发、协议测试及运营平台对接人员,也适合从零接触OCPP的开发者快速上手。资源按一问一答的请求与响应配对组织,每个JSON文件都标注了必填字段与可选字段,并呈现字段层级结构,可直接用于模拟充电桩业务流程,辅助联调与排障,减少报文格式理解偏差。压缩包内共七十八个文件,全部为JSON格式,整体仅二十八KB,轻量易用;消息类型涵盖启动充电、停止充电、心跳、计量值上报、远程控制、固件升级及证书管理等常用场景,也覆盖安全事件通知等扩展消息。目前已有五千四百三十五人学习,适合在Linux等环境下快速查阅OCPP1.6报文格式、编写模拟桩或搭建测试环境的开发者,也可作为接口文档随身参考。 调试欧标充电桩的OCPP协议时,我最常看到的画面是:一把Type 2枪插上桩,后台WebSocket窗口里瞬间涌出一串串JSON数组。很多人第一次接触桩端后台联调,盯着这些[2,"uuid","Action",{...}]这样的结构一头雾水——这东西既不像REST API那么直观,也不像MQTT那样松耦合。但只要你在这个行业待上几个月就会明白,OCPP 1.6 JSON格式几乎就是欧洲充电桩与后台管理系统之间的"通用行话",做桩的要会讲,做后台的要会听。
这篇文章不是抄一遍规范文档,而是把我实际调过的几十台欧标桩、对接过的多个后台系统里,关于OCPP 1.6消息事件最核心的机制、最容易踩的坑、以及真正能提高效率的调试工具和思路,一次性讲透。适合刚入行的桩端固件开发者、充电运营平台后端开发,以及做现场集成的实施工程师。
1. 为什么欧洲充电桩都在说OCPP 1.6 JSON这门"行话"
OCPP(Open Charge Point Protocol)由Open Charge Alliance维护,是目前全球充电桩与充电管理后台之间最主流的通信协议。欧标充电桩尤其明显——欧洲的公共充电网络从政府补贴项目到私营运营商,几乎默认要求设备支持OCPP,而OCPP 1.6 JSON正是存量市场占有率最高的版本。
很多人会问:OCPP都出到2.0.1了,为什么还要学1.6?答案很现实:市面上跑着的几十万台欧标桩,绝大多数固件跑的还是1.6 JSON;大量运营平台、漫游结算网络(比如Hubject的OCPI对接)也都以1.6为基础。作为一个开发者,你可以研究2.0.1的智能充电新特性,但上门联调时,对面给你最多的文档一定写着OCPP 1.6 JSON。
1.1 1.6协议里JSON和SOAP的路线之争
OCPP 1.6同时定义了两种实现:基于SOAP/XML的版本和基于WebSocket + JSON的版本。SOAP版本早期在欧洲有一定装机量,但它继承了SOAP协议的沉重包袱——XML命名空间、WSDL生成客户端、HTTP短连接每次都要建立会话,对嵌入式设备的适配成本很高。
JSON版本走的是WebSocket长连接,消息体是轻量级数组JSON,天然适合资源有限的桩端处理器。官方在1.6版本里同时维护两套规范,但业界最终"用脚投票"选了JSON。现在如果你听到"欧标桩支持OCPP 1.6",绝大部分指的就是JSON实现。
1.2 JSON相比SOAP到底省了什么
传输层:SOAP走HTTPS短连接,每次请求都要重新建连;JSON走WebSocket,一次握手后持续双向推送,这对充电过程中高频上报电表读数非常关键。
消息体:SOAP的XML要包一堆<soap:Envelope>标签,一条BootNotification愣是能撑到几KB;JSON版本同样的内容几百字节就搞定,弱网环境下差别立竿见影。
状态保持:SOAP是无状态的,后台要知道桩在线不在线,得靠桩频繁轮询;WebSocket天然维护连接状态,心跳机制也更简单直接。
我用一个表格概括两版的差异,方便给团队做技术决策时参考:
| 对比维度 | OCPP 1.6 SOAP | OCPP 1.6 JSON |
|---|---|---|
| 传输方式 | HTTPS短连接 | WebSocket长连接 |
| 消息格式 | XML/SOAP | JSON数组 |
| 实时性 | 轮询+回调 | 全双工即时推送 |
| 嵌入式适配难度 | 高 | 低 |
| 当前欧洲主流 | 已基本退场 | 绝对主流 |
做桩基固件的朋友,如果还想着兼容SOAP,我建议直接砍掉;做后台的朋友,你对接的桩如果只支持SOAP,大概率是五六年前的老设备,尽早规划更换。
2. 消息帧的底层骨架:CALL、CALLRESULT、CALLERROR
OCPP 1.6 JSON里所有动作,不管是桩主动上报还是后台下发指令,最终都封装成三种消息类型之一:CALL、CALLRESULT、CALLERROR。整个协议就是这三类消息的排列组合。
消息体都是一个JSON数组,第一元素是消息类型编号,第二元素是唯一标识符(uniqueId),后面的元素因类型不同而不同。
CALL:请求消息,桩端和后台都可以发起。结构是[2, "<uniqueId>", "<Action>", {<Payload>}]。
CALLRESULT:对CALL的成功响应。结构是[3, "<uniqueId>", {<Payload>}]。
CALLERROR:对CALL的失败响应。结构是[4, "<uniqueId>", "<errorCode>", "<errorDescription>", {<errorDetails>}]。
看一个最经典的BootNotification例子。桩上电后发起:
[2, "192bc5d8", "BootNotification", { "chargePointVendor": "ChargeStar", "chargePointModel": "AC-22KW-T2", "chargePointSerialNumber": "CS20240001", "chargeBoxSerialNumber": "CS20240001", "firmwareVersion": "1.4.2", "iccid": "", "imsi": "", "meterType": "SmartMeter A", "meterSerialNumber": "SM20240001" }]后台处理完毕,回复:
[3, "192bc5d8", { "currentTime": "2024-06-15T08:30:00Z", "interval": 300, "status": "Accepted" }]如果后台发现桩不在白名单里,可能回CALLERROR或者BootNotification里status为Rejected的CALLRESULT。注意这里的区别:协议层面的错误用CALLERROR,业务层面的拒绝用Payload里的状态字段。这是新手最容易混淆的地方。
2.1 uniqueId是异步匹配的关键
OCPP 1.6 JSON是异步协议。发出一条CALL后,不会阻塞等回复,而是靠uniqueId把CALL和对应的CALLRESULT/CALLERROR对上。这个uniqueId没有固定格式要求,可以是UUID,也可以是自增数字,但必须保证一定时间窗口内唯一。
我在实际项目里发现,有些桩端用设备上电时间戳后四位做uniqueId,重启后容易重复;有些后台用Redis缓存所有pending请求时,压根不校验uniqueId长度,结果被一个过长字符串搞崩。稳妥的做法:桩端用递增计数加随机数的组合,后台用pending_requests字典存uniqueId -> 回调函数,收到消息先查这个字典。
2.2 消息类型编号一览
| 消息类型 | 编号 | 方向 | 说明 |
|---|---|---|---|
| CALL | 2 | 双向 | 请求消息 |
| CALLRESULT | 3 | 双向 | 成功响应 |
| CALLERROR | 4 | 双向 | 错误响应 |
为什么编号是2、3、4而不是1、2、3?因为规范里0和1留给了HTTP Upgrade阶段的基础帧类型定义(WebSocket本身有自己的帧类型),OCPP消息从2开始编号。这种设计细节不用刻意记,但排查时看到[2,...]别条件反射当成HTTP状态码就行。
3. 从插枪到起充:一次真实充电的完整消息事件链
协议规范里列了二十多种标准Action,但现场联调时,真正高频出现的就那么几个。我按一次完整的交流桩充电流程,把这些消息事件串起来讲。
3.1 上电与注册:BootNotification和Heartbeat
桩上电后第一件事:建立WebSocket连接,连接成功立即发BootNotification。后台收到后检查桩的供应商、型号、序列号是否在白名单,返回的interval字段告诉桩每隔多少秒发一次Heartbeat,currentTime用来校准桩的系统时间。
这段联调中最常见的坑是:桩端不按后台返回的interval调整心跳周期,固件里写死60秒。后台可能预期300秒一次心跳以降低负载,结果被添加的桩打个措手不及。如果你在写桩端,一定要把BootNotification返回的interval动态更新到心跳任务里。
3.2 插枪与鉴权:StatusNotification和Authorize
用户插枪后,桩端先上报插枪状态:
[2, "a1b2c3d4", "StatusNotification", { "connectorId": 1, "errorCode": "NoError", "status": "Occupied", "timestamp": "2024-06-15T09:00:00Z" }]如果是刷卡/扫码鉴权,桩端发Authorize请求,把idTag传给后台验证:
[2, "a1b2c3d4", "Authorize", { "idTag": "RFID-CARD-001", "chargePointId": "CS20240001" }]后台返回的idTagInfo里有一个status字段,取值范围为Accepted、Blocked、Expired、Invalid、ConcurrentTx。只有Accepted才能继续。这里有个设计细节:Authorize只是"验证凭据是否有效",并不代表"我要在这个插座上充电",真正宣告充电开始的是StartTransaction。
3.3 起充与计量:StartTransaction和MeterValues
满足充电条件后,桩端发StartTransaction:
[2, "b2c3d4e5", "StartTransaction", { "connectorId": 1, "idTag": "RFID-CARD-001", "meterStart": 16800.5, "reservationId": 0, "timestamp": "2024-06-15T09:01:00Z" }]注意meterStart是当前电表读数,单位是kWh,不是W,也不是Wh。很多第一次对接的人会在这里把单位搞混,导致后台统计的充电量出现几十倍偏差。
后台收到后回复CALLRESULT,里面有一个非常重要的字段transactionId:
[3, "b2c3d4e5", { "transactionId": 8800123, "idTagInfo": { "status": "Accepted", "expiryDate": "2025-06-15T00:00:00Z", "parentIdTag": "" } }]这个transactionId是整个订单的唯一标识,桩端必须保存好,StopTransaction时要原样带回来。
充电过程中,桩端按固定周期(常见1秒、10秒、15秒)上报MeterValues:
[2, "c3d4e5f6", "MeterValues", { "connectorId": 1, "transactionId": 8800123, "meterValue": [ { "timestamp": "2024-06-15T09:05:00Z", "sampledValue": [ { "value": "16900.8", "measurand": "Energy.Active.Import.Register", "unit": "kWh", "context": "Sample.Periodic" }, { "value": "230.1", "measurand": "Voltage", "unit": "V", "phase": "L1" } ] } ] }]measurand字段定义了采样值的物理量,最常用的是Energy.Active.Import.Register(累计有功电能)。后台做计费时,一般取订单开始和结束的电表读数差,或者把MeterValues里相邻上报的电量差值累加。两种方式都要注意丢包后的补偿。
3.4 结束充电:StopTransaction
用户拔枪或远程停止后,桩端发StopTransaction:
[2, "d4e5f6a7", "StopTransaction", { "transactionId": 8800123, "meterStop": 17320.6, "timestamp": "2024-06-15T10:25:00Z", "reason": "Local", "idTag": "RFID-CARD-001" }]reason字段常见的有Local(本地停止)、Remote(后台远程停止)、EmergencyStop(急停)、Other等,后台可以用来判断订单结束原因。
StopTransaction的CALLRESULT里同样可能带idTagInfo,这个字段很微妙——在个别充电网络的协议扩展里,后台会在充电结束后返回一条idTagInfo,触发桩端在同一个插座上立即开启下一笔订单,用于"连续充电"场景。第一次调试如果发现后台返回的不是空结构,别当成错误。
3.5 远程指令类事件
后台也可以主动下发指令,最常用的几个:
- RemoteStartTransaction:后台发起远程启动充电,payload里带connectorId和idTag。
- RemoteStopTransaction:远程停止指定transactionId的订单。
- Reset:远程重启桩,type字段区分Hard和Soft。
- UnlockConnector:远程解锁充电枪。
这些指令都是后台发CALL,桩端处理完回CALLRESULT,CALLRESULT里的payload只代表桩端是否"接受并执行",不代表最终执行成功。比如RemoteStartTransaction的CALLRESULT只返回{"status":"Accepted"},真正起充成功与否,你得等桩端后续发的StatusNotification或StartTransaction消息来确认。
4. 现场踩坑:消息事件最常翻车的五个细节
这一节是重点。以下问题我全部在实际项目中遇到过,有的差点造成批量事故。
4.1 transactionId凭空消失
有一次现场联调,后台一直报"StopTransaction缺少transactionId"。排查到最后才发现,桩端在StartTransaction的CALLRESULT还没收到时,用户就快速拔枪结束了充电,桩端本地拿不到transactionId,干脆发了个0上去。
这个时序问题在快速插拔场景下特别容易复现。规范要求桩端必须先保存transactionId再允许结束,但很多固件实现没做这个约束。解决思路有两个:一是桩端把"等待StartTransaction.CALLRESULT"作为一个中间状态,期间不允许StopTransaction;二是如果StopTransaction发上去时transactionId未知,至少把idTag和meterStop带上,后台做人工对账。
4.2 timestamp时区错乱
OCPP 1.6 JSON规范要求所有时间字段使用UTC时区的ISO 8601格式,也就是2024-06-15T09:00:00Z。但国内不少桩的RTC默认是北京时间(UTC+8),固件直接取本地时间往上报,后台不做时区偏移的话,充电记录全部差8小时,计费系统直接乱套。
我的建议是:桩端固件内部所有时间戳统一生成UTC字符串,只在本地人机交互界面显示时转换成当地时区。后台侧也不要依赖桩端时间做计费,压力测试时以MeterValues里的采样时间和StopTransaction的meterStop为准计算电量,时间字段只做展示用。
4.3 心跳僵死与NAT超时
WebSocket连接在公网环境下很容易被中间设备静默断开,尤其是运营商NAT的空闲超时。很多桩虽然实现了Heartbeat,但只是"定时发、不管回不回"。
规范上Heartbeat的CALLRESULT里带后台的currentTime,你应该把它用作双重确认:如果连续几个Heartbeat都没有CALLRESULT,基本可以判定连接已经断了,需要主动重连。同时建议桩端开启WebSocket层的Ping/Pong保活,间隔建议30秒,比Heartbeat更轻量。这个组合拳能明显改善设备掉线后长时间不恢复的问题。
4.4 MeterValues报文过大
有些桩把内部所有监测点都塞进一条MeterValues里,传感器数据、温度、湿度、继电器状态全带上,一条消息几十KB。后台解析慢,弱网下还要分包重传。
其实规范允许拆分:把电能数据放一条,设备健康状态放DataTransfer或扩展字段。计量用的MeterValues保持精简,只上报Energy.Active.Import.Register以及必要的电压电流。这能极大降低平台侧处理开销,也减少掉包概率。
4.5 本地授权白名单和后台状态不同步
部分桩支持本地白名单离线鉴权,但白名单更新依赖后台的SendLocalList指令。实际运营中经常出现:白名单里明明删掉了某张卡,桩端因为一直在线没收到推送更新,导致这张卡还能继续充电充电完成后账单到了平台侧被拒付。
我的建议是:桩端每次在线Authorize时,除了本地匹配,还要看后台返回的idTagInfo状态。后台返回Expired或Blocked的,桩端要把这个idTag从本地白名单剔除,并在日志里打一条告警。本地白名单只能作为离线降级方案,不能当主要鉴权手段。
5. 调试OCPP 1.6 JSON的实用工具箱
5.1 没有真桩也能联调
我最常用的调试路径分两条线:一是用模拟后台测桩,二是用模拟桩测后台。
测试桩端固件:找一个开源OCPP后台模拟器,推荐用Python写的ocpp库或者ocpp-j(Java版)。自己写一个简单WebSocket服务端,起在本地端口,把桩的WebSocket URL指过去,日志打印所有收到的消息,自动回复BootNotification、Heartbeat、Authorize等常见请求。这样开发桩端时不需要依赖某个云平台,问题定位快得多。
测试后台逻辑:反向用开源模拟桩,ocpp库里带了ChargePoint实现,启动后会自动连接你指定的后台地址,按脚本触发BootNotification和StartTransaction。我用这套流程验证后台的事务处理、计费逻辑和远程指令下发,效率很高。想模拟异常时,直接改脚本发一段格式错误的JSON,就能验证后台的容错能力。
5.2 快速验证消息格式和Schema
OCPP官方发布了JSON Schema校验文件(在Open Charge Alliance仓库的schemas目录下)。联调时最省事的方法是:把桩端日志里的每条OCPP消息抽取出来,用jsonschema库跑一遍校验,能快速发现字段类型不符合、缺少必填字段、多发了未定义字段等问题。
两条铁律:
发出去的字段必须都对得上Schema定义的名称和类型,订单字段和单位错误比缺少字段更隐蔽,也更致命。
任何修改桩端OCPP字段顺序、大小写、单位、时区的动作,都必须重新回归验证MeterValues计量和交易结束流程。
5.3 抓包与日志级别设置
如果桩端和后台都是自己维护的,日志级别建议这样设:联调阶段开DEBUG,打印完整消息帧;上线阶段开INFO,只打印连接状态、CALLERROR和关键动作。
线上环境不要盲目抓包,WebSocket流量可能包含用户idTag等敏感信息。如果需要抓包分析,在测试桩上开启wireshark抓取TCP 443端口或8443端口的流量,注意TLS解密要提前配置好私钥或使用代理方式。
6. 一个关于消息事件设计的小结性经验
调试OCPP 1.6 JSON这些年,我最大的体会有两点。
第一,所有看似复杂的联调问题,最后几乎都能归因到消息帧结构、时序、单位、时区这四个维度。你在排查一个"后台收到的电量不对"问题时,不要老盯着计费逻辑,先抓一条MeterValues看measurand是不是Energy.Active.Import.Register、unit是kWh还是Wh、timestamp是不是UTC——八成问题都出在这儿。
第二,规范只能保证你不出大错,真正靠谱的系统还得靠日志和测试覆盖。我建议每个项目在交付前,至少准备一份"OCPP消息链路检查清单",把上电注册、心跳、插枪、鉴权、起充、计量、结束、远程指令、异常断线重连这九条路径全部跑一遍,每条路径对应的消息时序都记录下来,固件迭代后回归比对。
这行当没有那么多玄学,就是把每个消息事件都管明白,桩和后台才能安然对话。
本文还有配套的精品资源,点击获取