简介《云快充平台与设备通信协议1.6》是一份面向充电桩厂商、平台对接开发者和物联网通信工程师的协议规范文档主要解决充电桩与云快充平台之间基于TCP/IP的应用层数据交换、报文解析与联调对接问题适合从事充电桩嵌入式开发、平台后端接入及协议测试的人员查阅。资源包为单个PDF文件大小约1.39MB内容围绕通信接口、接入流程、应用层报文帧格式、数据格式定义、帧类型码及注册心跳等模块展开并覆盖登录认证、App充电、刷卡充电、离线充电和双枪并充等典型业务流程版本记录可追溯至V1.6。目前已有6188人学习可作为协议字段核对、联调排错和业务流程梳理时的案头参考。1. 0x68 那一帧一份充电桩网络协议的成败其实压在计费链路上拆开任意一条充电桩发往运营平台的上行帧第一个字节永远是0x68。刚开始接触这类桩云网络协议的人大多以为难点在充电控制——电流怎么爬坡、BMS 怎么握手、绝缘检测什么时候做。真正把联调周期拖长一个月的几乎都发生在后半段电量怎么换算、流水号怎么拼、交易记录丢了一条怎么补。原因不难理解。充电控制做错了现场顶多是充不上电用户换个桩就行计费和对账做错了是钱算错、单丢失、事后无法追溯属于必须回滚重来的事故。所以这份 V1.6 协议里一半的字段设计都在服务同一件事让每一度电都有出处让每一笔交易都能对上账。搞懂这条主线再看那些看似繁琐的帧类型码和校验规则就都不是拍脑袋定的了。2. 帧头解剖0x68 之后每个字节各自在防什么故障2.1 数据长度字节到底统计了哪一段先看整帧的固定结构这是后面所有讨论的地基。字段长度说明起始标志1 字节固定0x68数据长度1 字节数据域字节数上限 200序列号域2 字节发送顺序号从 0 递增加密标志1 字节0x00不加密0x01为 3DES帧类型标志1 字节决定这一帧是什么业务消息体N 字节业务数据帧校验域2 字节CRC16低字节在前关键在“数据长度”这个词的边界。协议写的是“其值为序列号域 加密标志 帧类型标志 消息体字节数之和”也就是N 4。它不包含起始符0x68不包含长度字节自身也不包含尾部 2 字节 CRC。换句话说这里的“数据域”是协议自定义的一个区间从序列号开始、到消息体结束而不是 TCP 语义里的“payload”。这个定义直接决定了收包逻辑怎么写。正确的做法是读到0x68后再读 1 字节拿到len然后继续读len 2字节数据域 CRC才算一帧完整。如果误把长度理解成“整帧长度”就会少读或多读 3 个字节表现是永远卡在等下一帧、或者把下一帧的帧头吃掉形成粘包假象。顺带一个边界不加密时数据域上限 200 字节意味着消息体最多 196 字节。加密时长度字段填的是加密后的长度而 3DES 是 8 字节分组填充会把长度顶上去这时候 200 的上限要在加密前就留出余量否则一条本来合规的报文加密后就越界了。2.2 序列号与加密标志只在一条连接内有效的编号序列号域 2 字节规则写得很克制从 0 开始顺序增加如果是应答包序号与对应的询问包保持一致桩与平台断开重连、或者溢出之后归 0。这三句话合起来说明一件事——序列号不是事务 ID它只在一条 TCP 长连接的生命周期内有效。它的作用是把请求和应答配成对平台下发一条读取实时数据桩回的那一条必须带同样的序号平台才知道这是对哪次询问的回答。一旦断线重连两边各自归零历史序号全部作废。由此推出一个实操约束同一时刻同方向不要并发多条等待应答的请求。因为 2 字节序号虽然空间够大但“应答沿用询问序号”的配对方式本质上是靠顺序隐式对应的。如果平台连续下发对时设置和工作参数设置两条命令桩如果乱序回复平台侧就可能把参数设置的应答当成对时的应答处理。稳妥的实现是给每条待应答请求维护一个 pending 表按序号索引而不是按到达顺序。加密标志只作用于消息体帧头是明文的。0x01表示 3DES这就带来一个绕不开的问题密钥怎么下发、怎么轮换。协议本身没有定义密钥协商流程实践中绝大多数部署都填0x00把安全性交给传输层和专网。这不是偷懒而是当设备侧算力和运维通道都受限时强行上对称加密反而会引入“密钥不同步导致整批桩失联”的更大风险。2.3 CRC16 从哪算到哪0x180D 与低字节在前的代价校验域的计算范围协议写的是“从序列号域到数据域”多项式0x180D低字节在前、高字节在后。翻译成代码就是不含0x68、不含长度字节。def crc16(data: bytes, init: int 0x0000, reflected: bool False) - int: 按多项式 0x180D 计算 CRC16。 data: 待校验字节范围是 序列号域 - 消息体末尾 init: 初值协议未写死需要用平台给的样例报文反推 reflected: 是否为反射(低位在前)算法同样需要反推 crc init if reflected: # 0x180D 的 16 位位反转结果为 0xB018 for byte in data: crc ^ byte for _ in range(8): crc (crc 1) ^ 0xB018 if crc 1 else crc 1 else: for byte in data: crc ^ byte 8 for _ in range(8): if crc 0x8000: crc ((crc 1) ^ 0x180D) 0xFFFF else: crc (crc 1) 0xFFFF return crc 0xFFFF def pack_frame(seq: int, frame_type: int, body: bytes, init0x0000, reflectedFalse) - bytes: # 数据域 序列号(2) 加密标志(1) 帧类型(1) 消息体(N) payload seq.to_bytes(2, little) b\x00 bytes([frame_type]) body crc crc16(payload, init, reflected) # 长度只统计数据域不含 0x68、不含长度字节自身、不含 CRC return b\x68 bytes([len(payload)]) payload crc.to_bytes(2, little)上面这段代码里有两个参数刻意留成了可配置init和reflected。因为协议只规定了多项式和字节序没有把初值和反射方式写死在文本里而这两项任意一个不对算出来的 CRC 就完全不同。工程上的做法不是猜而是拿平台给的一条已知样例报文把消息体固定、枚举四种组合初值 0x0000 / 0xFFFF × 是否反射哪种能对上就用哪种。这一步不做后面的联调基本寸步难行。pack_frame里还有一个细节值得留意seq.to_bytes(2, little)。协议规定 BIN 码一律低位在前序列号属于 BIN 码所以要小端。而 CRC 结果同样是低字节在前两处都用了little但因为语义不同很容易在某一处写错。3. 帧类型码的分段法为什么奇数向上、偶数向下是一条不能破的约定3.1 把方向写进编号里省掉一张表协议开篇就定了一条充电桩定义的帧类型码为奇数运营平台定义的为偶数。这条规则的价值在于解析器拿到帧类型标志的第一时间不需要查任何映射表只看最低位就知道这一帧该由谁处理。这条规则还顺带成了一个自检工具。文档里如果出现“偶数帧号但方向写的是桩→平台”那基本可以断定是笔误而不是什么特殊约定。第 5 章会专门讲一处真实的笔误这里先记住这个判断法。按这个规则梳理下来方向其实是清晰的0x01/0x02登录认证及其应答0x03/0x04心跳及其应答0x05/0x06计费模型验证请求及其应答0x09/0x0A计费模型请求及其应答0x31/0x32桩主动申请启动、平台确认启动0x34/0x33平台远程控制启机、桩回复注意这一对的奇偶与上一对相反0x36/0x35平台远程停机、桩回复0x3B/0x40交易记录上行、确认下行可以看到0x31/0x32与0x34/0x33这两组发起方一个是桩、一个是平台编号顺序就跟着反过来了。这不是设计失误而是“谁发起谁用自己的奇偶段”这条规则的直接结果。3.2 分段预留与“已定义不可重定义”协议里有一句容易被略过但分量很重的话帧类型码分段定义已定义的帧类型码不可重定义为其他定义。把用到的号段摊开看分段意图很明显0x01~0x0A是注册与费率元数据0x12~0x25是实时数据与充电过程0x31~0x48是运营交互启停、交易、余额、离线卡0x51~0x58是平台下发的设置类0x61~0x63是车位锁0x91~0x94是远程维护0xA1~0xA4是并充。每段都留了空号。比如0x07/0x08空着0x0B~0x11空着0x26~0x30空着。这些空号不是浪费是给未来版本留的插槽。而“不可重定义”意味着帧号是一种先到先得的全局资源——一旦某个号被某家桩企的私有实现占用后续版本就不能再把它分配给标准语义。这条约束解释了一个看起来很奇怪的现象并充是运营交互的一部分逻辑上完全应该塞进0x3x段但 V1.6 把它放到了0xA0段。因为在 2018 年到 2020 年这两年多里0x3x段已经被启停、交易、余额、离线卡占满了连续的空号不够放下并充需要的四条帧只能另开一段。3.3 交易记录的三次改号0x39 到 0x3F 再到 0x3B版本记录里有一条很值得玩味的线索。V1.2 写的是“更新交易记录帧为 0x39”V1.4 在描述电量精度时把交易记录称作0x3FV1.5 又改成“修改交易记录为 0x3B并修改电表起止值字节长度”。一个帧类型码在三个版本里换了三次。同一时期电量精度也从原来的小数位改成了 4 位小数。这两件事放在一起看说明交易记录这条帧当时还在剧烈变动中字段长度都没稳定下来。改帧号为什么代价最大因为桩端固件里这个号是写死的常量。平台侧一旦只认新号现场存量桩的交易记录全部上不来——桩照常充电、照常扣费但平台收不到结算数据等于整批订单丢失。而交易记录又不像心跳丢了就丢了它是计费的唯一凭证。所以真实升级路径通常是双号兼容平台侧同时监听新旧两个帧号解析时按长度和字段布局区分等存量桩通过远程升级0x94逐步换完再关掉旧号。这套“先兼容、后收口”的做法几乎是所有长周期硬件协议的标配只不过文档里通常不会写。顺带说说为什么改号往往伴随着改长度。交易记录里的电表起止值是典型的 BCD 码定长字段精度从两位小数改到四位小数同样的数值范围就需要更多字节。字段一变长整帧长度就变而帧长度在桩端往往是硬编码的缓冲区大小——这时候换个帧号反而是最省事的做法新号对应新布局老号保持老布局两套代码并行互不干扰。3.4 并充帧插进 0xA0 段V1.6 唯一的新增功能是双枪并充对应四条帧0xA1桩主动申请并充充电、0xA2平台确认并充启动、0xA3远程并充启机命令回复、0xA4平台远程控制并充启机。这四条帧的结构几乎是0x31~0x34的镜像只是多了“另一把枪”的上下文。这个设计选择说明并充没有被当成一种新的充电模式而是被当成“两把枪绑定后走一遍熟悉的启动流程”。桩端实现时绝大部分启动逻辑可以复用只需要在参数里多传一个伴生枪号。4. 一次充电的完整时序从登录认证到交易记录谁在等谁4.1 上电与心跳为什么每次复位都要重新登录协议对登录的要求很硬充电桩每次复位或通信离线都必须重新登录并且登录成功后才能进行后续交互。这条规则背后是状态一致性。平台侧为每台桩维护一份会话状态在线与否、当前费率版本、是否有进行中的订单、离线卡清单版本号。桩一旦断电重启本地这些状态可能全部丢失或回退如果平台还按老状态继续下发指令就会出现“平台以为在充电、桩其实刚上电”的错位。强制重新登录本质是让双方在同一个时间点上重建共识。登录成功之后是心跳0x03/0x04。心跳不只是保活它还是平台判定“离线”的唯一依据——TCP 连接断了平台未必立刻知道但心跳超时是确定信号。紧接着是计费模型验证0x05/0x06。桩本地存着一份费率上电后把版本号报给平台平台比对如果不一致桩再发0x09请求完整费率平台用0x0A下发。这套“先验版本、后拉全量”的两段式设计很实用绝大多数情况下桩的费率是对的一次短报文就能确认不必每次上电都传几 KB 的费率表。对于 GPRS 链路的桩这个差别在流量成本上是实打实的。费率本身在 V1.3 做过一次拆分把服务费和电费分开。这个改动看着小实际影响很大在此之前费率是一个混合单价之后变成“电费按时段分尖峰平谷 服务费单独计价”桩端计算金额的逻辑要跟着改交易记录里的金额字段也要拆。这也是为什么 V1.3 之后紧接着 V1.4 就动了电量精度——计费精度要求提高整个金额链路都得跟着提精度。4.2 三路启动充电刷卡、App、远程启机的差异启动充电有三条路径帧的走法完全不同这是最容易搞混的地方。刷卡充电走的是桩主动申请桩读卡拿到物理卡号M1 卡第 0 扇区 0 块卡面印的是逻辑卡号只用于屏幕显示核对发0x31带上卡号和枪号平台鉴权后回0x32里面带交易流水号和账户余额桩拿到流水号才开始充电。V1.1 在这一步加了“刷卡失败原因提示”就是为了让用户知道是余额不足、卡未激活还是卡被锁定。App 充电走的是平台主动下发平台发0x34远程控制启机桩回0x33确认。注意这里平台的指令先到桩是被动方。远程停机统一走0x36下发、0x35回复。三条路径最后都会收敛到同一个东西上交易流水号。协议规定流水号由平台产生格式是桩号 7 字节 枪号 1 字节 年月日时分秒 6 字节 自增序号 2 字节共 16 字节全部按 BCD 编码。def build_trade_no(pile_no: str, gun_no: int, when: str, seq: int) - bytes: 拼装 16 字节交易流水号全部 BCD 编码。 pile_no: 桩编号14 位数字字符串7 字节 BCD gun_no: 枪号 1~991 字节 BCD when: YYMMDDhhmmss 12 位6 字节 BCD seq: 自增序号 0~99992 字节 BCD raw f{pile_no:014}{gun_no:02d}{when}{seq:04d} assert len(raw) 32, 流水号必须是 32 位 BCD 字符 return bytes(int(raw[i:i 2], 16) for i in range(0, 32, 2))这里int(raw[i:i2], 16)是把两位十进制字符当作十六进制解析得到的字节高半字节和低半字节正好各存一位十进制数这就是 BCD 的本质。用int(..., 10)会直接算错这是新手最常见的翻车点之一。桩号本身也由平台生成不支持桩企自行编号。原因和流水号一致桩号是全局唯一标识进了流水号就是账务主键的一部分不能有任何歧义。4.3 充电中继0x23 与 0x25 如何映射 BMS 报文充电过程这一段的帧本质是把 GB/T 27930 里桩与 BMS 之间的 CAN 报文翻译成 TCP 帧上报给平台。0x15充电握手、0x17参数配置、0x19充电结束、0x1B错误报文、0x1D充电阶段 BMS 中止、0x21充电阶段充电机中止、0x23充电过程 BMS 需求与充电机输出、0x25充电过程 BMS 信息。其中0x23的设计最能说明问题它把“BMS 要什么”和“充电机给了什么”放在同一帧里一起上报。这个组合不是为了省报文而是为了可审计——事后判断一次充电是否异常最直接的证据就是需求电流和实际输出电流的偏差曲线。如果分成两帧上报时间戳对不齐追溯就失去了基准。0x1D和0x21也是一对前者是 BMS 侧主动中止后者是充电机侧主动中止。分开定义的意义在于责任划分——同一笔异常订单到底是车的问题还是桩的问题看这两个帧号就够了。4.4 交易记录与离线模式断网那一刻谁记账0x3B上行、0x40确认下行。这个“上报 确认”的配对决定了交易记录的语义是至少一次投递桩发出去后没等到0x40就必须重发。这就带来一个必须处理的副作用平台可能收到重复的交易记录。去重的依据只能是交易流水号因为它是这次充电的唯一标记——从远程启动或卡鉴权回复时产生一直到桩结束充电全程用同一个号。平台侧如果按“收到就入库”处理重复补传就会造成重复计费。离线模式解决的是另一个问题断网时还能不能充电。答案是可以但需要提前准备。离线卡数据同步0x44、清除0x46、查询0x48作用是把卡片的鉴权和余额信息提前下发到桩本地。断网时桩凭本地缓存判断这张卡能不能充、能充多少充完之后把交易记录存在本地等网络恢复再补传。这条链路里有一个隐含的顺序要求离线卡的同步必须在断网之前完成而桩无法预知什么时候断网。所以实际部署中平台会定期比如每次心跳检查桩端离线卡清单的版本号发现有更新就推一次。这和 4.1 里费率的两段式校验是同一套思路用版本号做增量同步的触发条件而不是每次全量推。余额更新0x42/0x41也是同理。离线充电时桩本地扣减余额恢复联网后平台下发的余额是权威值桩必须无条件覆盖本地值——否则会出现桩本地余额比平台高的“凭空多钱”问题。5. 联调避坑五个让报文“看起来对、平台却不认”的细节5.1 长度字段把起始符算进去了现象桩端日志显示报文已发出平台侧完全没反应抓包看到平台在收到第一帧后就一直等后续帧全部错位。原因发送方在填长度字节时按“整帧长度”计算把0x68、长度字节自身、CRC 全算了进去导致平台侧按len 2读完之后还剩几个字节被当成下一帧的开头从此彻底错位。解决长度字段严格等于数据域长度即2序列号 1加密标志 1帧类型 N消息体。用一条已知报文验证如果消息体是 20 字节长度字节必须是 24。收包侧则按0x68→ 读长度 → 读len 2字节的顺序解析不要试图“扫到下一个 0x68”来找帧因为消息体里完全可能出现0x68这个字节值。5.2 CRC 对不上但看起来只差一位现象平台返回校验错误桩端用自己算的 CRC 回算发现和目标值“差不多但不一样”。原因多项式0x180D是对的但初值或者反射方式选错了。协议文档里写了多项式和“低字节在前”却没写初值也没写算法是 MSB-first 还是 LSB-first。这两项任意一个不同结果就完全不同。解决不要靠猜。拿平台给的一条完整样例报文取其中的数据域用第 2 章那段代码枚举init为0x0000和0xFFFF、reflected为True和False四种组合比对 CRC 结果。四选一一次就能确定。确定之后把这两个参数写进配置常量不要让每个开发者各自实现一遍。5.3 0x13 这一帧在两张表里叫两个名字现象桩端按“上传实时监测数据”实现了0x13平台侧解析正常但对接文档评审时被质疑帧名不符双方各执一词。原因协议正文 7.2 节的标题是“上传实时监测数据”而第 4 章的帧类型定义一览表里0x13的备注写的是“离线监测数据”。同一个帧号两处命名不一致。解决以帧号和字段布局为准名称以正文的数据定义章节为准。这类不一致在长周期维护的协议里很常见——改字段的时候顺手改了正文标题忘了一览表。实操建议是联调时以帧号 字节布局为唯一契约名称只作为检索辅助不要拿名称去反推语义。5.4 并充那四条帧方向栏写反了两条现象按文档表格实现后桩端把0xA3当成下行帧处理平台把0xA4当成上行帧处理双方都在等对方先发并充流程永远启动不了。原因帧类型定义表里0xA3远程并充启机命令回复标注的方向是“运营平台→充电桩”0xA4运营平台远程控制并充启机标注的是“充电桩→运营平台”。但按协议自己定的规则——桩定义为奇数、平台定义为偶数——0xA3是奇数必然是桩发往平台0xA4是偶数必然是平台发往桩。表格里的方向栏填反了。解决永远用奇偶规则校验方向。发现文档矛盾时以奇偶规则和帧名语义“回复”必然是应答方发的双重确认。这一条不只适用于并充是整个协议里最可靠的自检手段。5.5 电量 4 位小数与 BIN 小端叠在一起算错现象平台收到的电量数值比实际小了一万倍或者高字节低字节颠倒出现明显不合理的巨大数字。原因两个独立问题叠在一起。V1.4 把实时数据和交易记录里的电量精度改成了 4 位小数也就是上送值要乘 10000而 BIN 码又是低位在前。开发者如果只记得改倍率忘了字节序或者反过来的情况都有。解决把“倍率”和“字节序”拆成两个独立的转换步骤分别写单元测试。先用一条已知报文验证字节序比如电压 225.1 上送为 2251即0x08CB小端发送时字节序列是CB 08再叠加倍率验证数值。两件事一起调出错时根本分不清是哪一步的问题。顺带提醒一句协议里小数一律乘倍率上送这个约定是全局的不只是电量。电压一位小数乘 10、电量四位小数乘 10000每个字段的倍率必须逐字段对照文档确认不能想当然按“统一两位小数”处理。6. 设计取舍为什么这套协议宁可把复杂度压到桩端6.1 私有二进制帧与文本协议的取舍如果把这份协议和同时期的物联网方案对比最明显的差异是它没有选 JSON 或 MQTT而是用了自定义的二进制帧1 字节起始符、1 字节长度、定长字段、CRC16。这个选择在当时的环境下几乎没有悬念。设备侧走的是 GPRS流量按 KB 计费一条心跳如果用 JSON 序列化光字段名就要几十字节而二进制帧里一次心跳可能只有十几个字节。再加上桩端多是资源受限的嵌入式环境定长结构体直接内存映射解析开销几乎为零。代价也很清楚文档即契约。JSON 可以靠“字段缺失就当作没传”实现向前兼容二进制帧不行——字节位置是写死的加一个字段就要么占用预留字节要么升版本换帧号。这也解释了为什么这份协议里到处是预留字段以及为什么交易记录改一次布局要换一个帧号。取舍点选择换来什么付出什么帧格式私有二进制定长帧流量省、解析快、内存可控兼容只能靠预留字段或换帧号校验CRC16 而非哈希单片机算得动无完整性以外的安全保证加密可选 3DES默认关闭不引入密钥分发难题安全依赖传输层与专网记账平台产生流水号 记录确认账务主键唯一、可补传平台必须做幂等去重离线卡片数据提前下发到桩断网仍可充电桩要维护本地余额与卡清单6.2 复杂度压到桩端之后靠什么补救整套协议的分工很明确平台负责产生权威数据桩号、流水号、费率、余额桩负责执行和本地兜底CRC、流水号拼装、离线记账、断网缓存。平台轻桩重。这个分工在部署规模小的时候很划算——平台侧改动一次影响全网桩端逻辑一旦稳定就很少动。但它有个致命弱点桩端逻辑一旦有 bug修复成本极高因为要一台一台升级。所以协议里那几个维护类帧号分量比看起来重得多。0x92远程重启、0x94远程更新是这套架构唯一的后悔药。它们的存在某种程度上是对“把复杂度压到桩端”这个决策的补偿——既然错误会长期留在现场就必须留一条能远程纠错的通道。从那以后我每次评审这类桩云协议都会先翻一遍远程维护段有几个帧号、升级失败有没有回滚机制再看业务帧。业务帧写错了改平台就行维护通道没设计好一个 CRC 计算的 bug 就能让一批桩变成砖头只能派人到现场。这个顺序希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?