云快充 1.6 主要解决充电桩与运营平台之间的通信,二维码显示并没有统一命令。实际项目中,不同设备厂商会在基础协议上增加私有扩展:有的只下发 URL 前缀,由充电桩拼接桩号和枪号;有的按枪下发完整 URL;还有的使用固定长度字段保存二维码内容。
本文根据三组项目材料,对这些扩展进行统一设计。由于材料没有标注公司归属,文中以“扩展 A、B、C”代称。生产实施仍应以设备厂商确认的协议版本和真机联调结果为准。
一、三种扩展有什么区别
| 对比项 | 扩展 A:前缀拼接 | 扩展 B:完整 URL | 扩展 C:固定内容 |
|---|---|---|---|
| 下发/应答命令 | 0xF0 / 0xF1 |
0x9C / 0x9B |
0x5A / 0x59 |
| 下发范围 | 整桩 | 单枪 | 单枪 |
| 桩编号 | BCD 7 字节 | 下发数据域不携带 | BCD 7 字节 |
| 枪号 | 由格式决定是否拼接 | BIN 1 字节 | BCD 1 字节 |
| 二维码内容 | ASCII 变长前缀 | ASCII 变长 URL | ASCII 固定 150 字节 |
| 长度字段 | BIN 1 字节 | BIN 2 字节 | 无 |
| 最大长度 | 200 字节 | 200 字节 | 固定 150 字节 |
| 成功码 | 0x00 |
0x01 |
0x01 |
最容易出错的地方有三个:
0xF1的0x00表示成功,而0x9B、0x59的0x01才表示成功;- 扩展 B 的枪号是 BIN,扩展 C 的枪号是 BCD;
- 长度是编码后的字节数,不是 Java 字符串长度。
二、三种协议的数据域
扩展 A:0xF0/0xF1
0xF0 下发数据域:
| 字段 | 类型 | 长度 | 说明 |
|---|---|---|---|
| 桩编号 | BCD | 7 字节 | 14 位编号,不足补零 |
| 二维码格式 | BIN | 1 字节 | 0x00 或 0x01 |
| 前缀长度 | BIN | 1 字节 | 不超过 200 |
| 二维码前缀 | ASCII | 变长 | URL 前缀 |
两种格式的拼接规则为:
格式 0:前缀 + 14 位桩编号
格式 1:前缀 + 14 位桩编号 + 2 位枪编号
例如:
前缀:https://q.example.com/?No=
桩号:34220001000233
格式 0:https://q.example.com/?No=34220001000233
格式 1:https://q.example.com/?No=3422000100023301
0xF1 应答由 7 字节 BCD 桩编号和 1 字节结果组成:0x00 成功,0x01 失败。
扩展 B:0x9C/0x9B
0x9C 按枪下发完整 URL:
| 字段 | 类型 | 长度 | 说明 |
|---|---|---|---|
| 枪号 | BIN | 1 字节 | 充电枪号 |
| URL 长度 | BIN | 2 字节 | ASCII 字节数 |
| URL | ASCII | 变长 | 最大 200 字节 |
该命令不携带桩编号,目标设备由当前 TCP 会话确定。URL 长度字段的端序在材料中没有明确说明,必须通过厂商黄金报文或真机测试确认,不能根据 Java 默认端序猜测。
0x9B 应答由 7 字节 BCD 桩编号、1 字节 BIN 枪号和 1 字节结果组成:0x00 失败,0x01 成功。
扩展 C:0x5A/0x59
0x5A 下发固定长度二维码内容:
| 字段 | 类型 | 长度 | 说明 |
|---|---|---|---|
| 桩编号 | BCD | 7 字节 | 14 位编号,不足补零 |
| 枪号 | BCD | 1 字节 | 0x01 表示 1 号枪 |
| 二维码内容 | ASCII | 固定 150 字节 | 不足补 0x00 |
二维码内容可以采用:
https://q.example.com/qr?c=<设备编号><枪号>
0x59 应答由桩编号、BCD 枪号和设置结果组成:0x00 失败,0x01 成功。固定内容域必须补足 150 字节,不能省略尾部的 0x00。
三、兼容层设计
三种扩展应复用现有云快充 1.6 基础帧,只负责命令数据域:
业务配置
-> 二维码扩展适配器
-> 数据域 byte[]
-> 云快充基础帧编码器
-> TCP 会话
通过设备型号和固件版本选择适配器,不要只根据品牌名称判断。同一厂商的不同固件也可能使用不同命令。
public enum QrProtocol {
PREFIX_F0_F1,
URL_9C_9B,
FIXED_5A_59
}
public interface QrExtensionCodec {
QrProtocol protocol();
QrCommand encode(QrSetting setting, QrCapability capability);
QrAck decodeAck(byte frameType, ByteBuffer payload);
}
public record QrCommand(byte frameType, byte[] payload) {
}
public record QrAck(
QrProtocol protocol,
String deviceNo,
Integer connectorNo,
boolean success,
int rawResult
) {
}
能力配置至少包含协议类型、最大长度、长度字段端序、断电是否保存以及登录后是否需要重发。找不到明确配置时,不要轮流尝试三个命令,以免旧固件错误处理未知帧。
四、Java 编码实现
先把 14 位桩号转换为 7 字节 BCD:
static byte[] encodeDeviceNo(String value) {
if (value == null || !value.matches("\\d{1,14}")) {
throw new IllegalArgumentException("invalid deviceNo");
}
String digits = "0".repeat(14 - value.length()) + value;
byte[] result = new byte[7];
for (int i = 0; i < result.length; i++) {
int high = digits.charAt(i * 2) - '0';
int low = digits.charAt(i * 2 + 1) - '0';
result[i] = (byte) ((high << 4) | low);
}
return result;
}
static byte encodeBcd(int value) {
if (value < 0 || value > 99) {
throw new IllegalArgumentException("BCD value out of range");
}
return (byte) (((value / 10) << 4) | value % 10);
}
三种数据域的核心编码如下:
// 0xF0:桩号 + 格式 + 一字节长度 + 前缀
static byte[] encodeF0(String deviceNo, int format, byte[] prefix) {
if (prefix.length > 200) throw new IllegalArgumentException("prefix too long");
ByteBuffer out = ByteBuffer.allocate(9 + prefix.length);
return out.put(encodeDeviceNo(deviceNo))
.put((byte) format)
.put((byte) prefix.length)
.put(prefix)
.array();
}
// 0x9C:BIN 枪号 + 两字节长度 + 完整 URL
static byte[] encode9c(int connector, byte[] url, ByteOrder byteOrder) {
if (url.length > 200) throw new IllegalArgumentException("url too long");
ByteBuffer out = ByteBuffer.allocate(3 + url.length).order(byteOrder);
return out.put((byte) connector)
.putShort((short) url.length)
.put(url)
.array();
}
// 0x5A:桩号 + BCD 枪号 + 固定 150 字节内容
static byte[] encode5a(String deviceNo, int connector, byte[] content) {
if (content.length > 150) throw new IllegalArgumentException("content too long");
ByteBuffer out = ByteBuffer.allocate(158);
out.put(encodeDeviceNo(deviceNo)).put(encodeBcd(connector)).put(content);
while (out.hasRemaining()) out.put((byte) 0x00);
return out.array();
}
调用前还要严格校验 ASCII。不要直接用字符数作为长度,也不要让不可编码字符被静默替换为问号。若 URL 中包含中文,应先完成百分号编码,再检查最终 ASCII 字节数。
应答解析也必须归属具体适配器:
boolean success = switch (protocol) {
case PREFIX_F0_F1 -> rawResult == 0x00;
case URL_9C_9B, FIXED_5A_59 -> rawResult == 0x01;
};
五、下发与重试
二维码设置应作为异步任务处理:
PENDING -> SENT -> ACK_SUCCESS
-> ACK_FAILED
-> TIMEOUT
扩展 A 的应答不带枪号,可按设备会话和协议序列号关联;扩展 B、C 还应校验枪号。收到重复应答时保持幂等。
设备断电后是否保存二维码,各材料没有统一结论。平台可以维护“期望配置版本”和“设备最后确认版本”,在设备登录后按能力配置决定是否重发。扩展 A 下发一次,扩展 B、C 按枪下发。
超时不代表设备一定没有写入,因此重试必须可幂等。同一枪存在多条待处理任务时,只发送最新配置;格式错误、长度超限和能力不匹配不应重试。
六、二维码内容设计
二维码尽量使用 HTTPS 短链接:
https://q.example.com/c/4Yp7kN2m
不要在 URL 中放入手机号、用户令牌、价格或内部数据库主键。扫码后由服务端把公开 ID 映射到设备和枪口。
协议允许 150 或 200 字节,不代表应该用满。URL 越长,二维码越密,在小尺寸或低分辨率屏幕上的识别率越差。优先使用短域名、短路径和服务端映射。
协议日志记录设备号、枪号、命令码、序列号、内容哈希、长度和结果码即可,默认不要打印完整 URL。
七、测试重点
上线前至少覆盖以下用例:
| 用例 | 预期结果 |
|---|---|
| 14 位桩号 | 编码为 7 字节 BCD |
| 扩展 B 的 12 号枪 | BIN 0x0C |
| 扩展 C 的 12 号枪 | BCD 0x12 |
| URL 为 200 字节 | 允许 |
| URL 为 201 字节 | 拒绝 |
| 扩展 C 内容不足 150 字节 | 补 0x00 |
0xF1 返回 00 |
成功 |
0x9B、0x59 返回 01 |
成功 |
| 应答长度不正确 | 拒绝解析 |
每种扩展还需要一组厂商确认的完整十六进制报文,逐字节验证编码结果。真机测试不能只看应答成功,还要确认二维码是否显示在正确枪口、扫码内容是否完全一致,以及断电重启后是否需要重新下发。
总结
云快充二维码扩展的难点不在生成二维码,而在兼容协议差异:0xF0/0xF1 下发前缀,0x9C/0x9B 按枪下发变长 URL,0x5A/0x59 下发固定 150 字节内容。三者的枪号编码、长度字段和成功码并不统一。
实现时应保留统一的云快充基础帧,为每种扩展建立独立适配器,并根据设备型号和固件能力选择协议。端序、BCD 补零方向和断电保存策略等材料未明确的内容,必须通过厂商确认和真机测试后再上线。
本文中的域名、桩号和二维码内容均为演示数据,命令定义来自项目提供的三份扩展材料,不代表公开行业标准。