!47 新增 充电桩主动申请启动充电(0x31)2.运营平台确认启动充电(0x32)

* fix(ProtocolUplinkConsumerService):指标初始化代码恢复
* update:启动方式枚举类调整
* update:增加 0x31、0x32 的枚举类
* update:添加下行日志打印
* add:1.充电桩主动申请启动充电(0x31)2.运营平台确认启动充电(0x32)
This commit is contained in:
白板
2025-09-12 05:44:33 +00:00
committed by 三丙
parent 4eebd3d1b0
commit bc5411eb4b
12 changed files with 708 additions and 4 deletions

View File

@@ -45,6 +45,13 @@
---
#### 0x31 充电桩主动申请充电
`68 37 00 04 00 31 20 23 12 12 00 00 10 01 01 00 00 00 00 00 D1 4B 0A 54 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FA E8`
#### 0x32 运营平台确认启动充电
`68 28 04 00 00 32 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 20 23 12 12 00 00 10 01 00 00 00 00 00 00 00 00 00 00 01 06 29 42`
---
#### 0x34 下发启动充电
`68 30 01 00 00 34 00 00 00 00 00 00 12 34 56 78 90 12 34 56 78 90 20 23 12 12 00 00 10 01 56 78 90 12 34 56 78 90 56 78 90 12 34 56 78 90 10 27 00 00 5f d9`
#### 0x33 上行启动应答

View File

@@ -0,0 +1,85 @@
/**
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
* 微信mohan_88888
* 抖音:程序员三丙
* 付费课程知识星球https://t.zsxq.com/aKtXo
*/
package sanbing.jcpp.protocol.yunkuaichong.enums;
import lombok.Getter;
/**
* 云快充协议密码验证标志枚举
*
* @author baiban
* @since 2024-12-16
*/
@Getter
public enum YunKuaiChongPasswordRequiredEnum {
/** 不需要密码 */
NOT_REQUIRED(0x00, false, "不需要密码"),
/** 需要密码 */
REQUIRED(0x01, true,"需要密码");
/** 密码验证标志代码 */
private final int code;
/** 是否需要密码的布尔值 */
private final boolean value;
/** 密码验证标志描述 */
private final String description;
YunKuaiChongPasswordRequiredEnum(int code, boolean value, String description) {
this.code = code;
this.value = value;
this.description = description;
}
/**
* 根据代码获取密码验证标志枚举
*
* @param code 密码验证标志代码
* @return 密码验证标志枚举未找到时返回NOT_REQUIRED
*/
public static YunKuaiChongPasswordRequiredEnum fromCode(int code) {
for (YunKuaiChongPasswordRequiredEnum passwordRequired : values()) {
if (passwordRequired.code == code) {
return passwordRequired;
}
}
return NOT_REQUIRED;
}
/**
* 根据代码获取密码验证标志描述
*
* @param code 密码验证标志代码
* @return 密码验证标志描述
*/
public static String getDescription(int code) {
return fromCode(code).getDescription();
}
/**
* 根据代码检查是否需要密码
*
* @param code 密码验证标志代码
* @return true表示需要密码false表示不需要密码
*/
public static boolean isPasswordRequired(int code) {
return fromCode(code).isValue();
}
/**
* 根据布尔值获取对应的枚举
*
* @param required 是否需要密码
* @return 对应的枚举值
*/
public static YunKuaiChongPasswordRequiredEnum fromBoolean(boolean required) {
return required ? REQUIRED : NOT_REQUIRED;
}
}

View File

@@ -0,0 +1,194 @@
/**
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
* 微信mohan_88888
* 抖音:程序员三丙
* 付费课程知识星球https://t.zsxq.com/aKtXo
*/
package sanbing.jcpp.protocol.yunkuaichong.enums;
import lombok.Getter;
/**
* 云快充协议启动充电失败原因枚举
*
* @author baiban
* @since 2024-12-16
*/
@Getter
public enum YunKuaiChongStartChargeFailureReasonEnum {
/** 成功(无失败原因) */
SUCCESS(0x00, "SUCCESS", "成功"),
/** 账户不存在 */
ACCOUNT_NOT_EXISTS(0x01, "ACCOUNT_NOT_EXISTS", "账户不存在"),
/** 账户冻结 */
ACCOUNT_FROZEN(0x02, "ACCOUNT_FROZEN", "账户冻结"),
/** 账户余额不足 */
INSUFFICIENT_BALANCE(0x03, "INSUFFICIENT_BALANCE", "账户余额不足"),
/** 该卡存在未结账记录 */
CARD_HAS_UNPAID_RECORD(0x04, "CARD_HAS_UNPAID_RECORD", "该卡存在未结账记录"),
/** 桩停用 */
PILE_DISABLED(0x05, "PILE_DISABLED", "桩停用"),
/** 该账户不能在此桩上充电 */
ACCOUNT_NOT_ALLOWED_ON_PILE(0x06, "ACCOUNT_NOT_ALLOWED_ON_PILE", "该账户不能在此桩上充电"),
/** 密码错误 */
PASSWORD_ERROR(0x07, "PASSWORD_ERROR", "密码错误"),
/** 电站电容不足 */
INSUFFICIENT_STATION_CAPACITY(0x08, "INSUFFICIENT_STATION_CAPACITY", "电站电容不足"),
/** 系统中vin码不存在 */
VIN_CODE_NOT_EXISTS(0x09, "VIN_CODE_NOT_EXISTS", "系统中vin码不存在"),
/** 该桩存在未结账记录 */
PILE_HAS_UNPAID_RECORD(0x0A, "PILE_HAS_UNPAID_RECORD", "该桩存在未结账记录"),
/** 该桩不支持刷卡 */
PILE_NOT_SUPPORT_CARD(0x0B, "PILE_NOT_SUPPORT_CARD", "该桩不支持刷卡"),
/** 未知错误 */
UNKNOWN(0xFF, "UNKNOWN", "未知错误");
/** 失败原因代码 */
private final int code;
/** 失败原因值 */
private final String value;
/** 失败原因描述 */
private final String description;
YunKuaiChongStartChargeFailureReasonEnum(int code, String value, String description) {
this.code = code;
this.value = value;
this.description = description;
}
/**
* 根据代码获取失败原因枚举
*
* @param code 失败原因代码
* @return 失败原因枚举未找到时返回UNKNOWN
*/
public static YunKuaiChongStartChargeFailureReasonEnum fromCode(int code) {
for (YunKuaiChongStartChargeFailureReasonEnum reason : values()) {
if (reason.code == code) {
return reason;
}
}
return UNKNOWN;
}
/**
* 根据值获取失败原因枚举
*
* @param value 失败原因值
* @return 失败原因枚举未找到时返回UNKNOWN
*/
public static YunKuaiChongStartChargeFailureReasonEnum fromValue(String value) {
if (value == null || value.trim().isEmpty()) {
return SUCCESS;
}
for (YunKuaiChongStartChargeFailureReasonEnum reason : values()) {
if (reason.value.equals(value)) {
return reason;
}
}
return UNKNOWN;
}
/**
* 根据描述获取失败原因枚举
*
* @param description 失败原因描述
* @return 失败原因枚举未找到时返回UNKNOWN
*/
public static YunKuaiChongStartChargeFailureReasonEnum fromDescription(String description) {
if (description == null || description.trim().isEmpty()) {
return SUCCESS;
}
for (YunKuaiChongStartChargeFailureReasonEnum reason : values()) {
if (reason.description.equals(description)) {
return reason;
}
}
return UNKNOWN;
}
/**
* 根据代码获取失败原因值
*
* @param code 失败原因代码
* @return 失败原因值
*/
public static String getValue(int code) {
return fromCode(code).getValue();
}
/**
* 根据值获取失败原因代码
*
* @param value 失败原因值
* @return 失败原因代码
*/
public static int getCode(String value) {
return fromValue(value).getCode();
}
/**
* 根据值获取失败原因描述
*
* @param value 失败原因值
* @return 失败原因描述
*/
public static String getDescription(String value) {
return fromValue(value).getDescription();
}
/**
* 根据代码获取失败原因描述
*
* @param code 失败原因代码
* @return 失败原因描述
*/
public static String getDescription(int code) {
return fromCode(code).getDescription();
}
/**
* 检查是否为成功状态
*
* @param code 失败原因代码
* @return true表示成功false表示失败
*/
public static boolean isSuccess(int code) {
return code == SUCCESS.code;
}
/**
* 检查是否为成功状态
*
* @return true表示成功false表示失败
*/
public boolean isSuccess() {
return this == SUCCESS;
}
/**
* 获取字节形式的代码
*
* @return 字节形式的失败原因代码
*/
public byte getByteCode() {
return (byte) code;
}
}

View File

@@ -0,0 +1,126 @@
/**
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
* 微信mohan_88888
* 抖音:程序员三丙
* 付费课程知识星球https://t.zsxq.com/aKtXo
*/
package sanbing.jcpp.protocol.yunkuaichong.enums;
import lombok.Getter;
/**
* 云快充协议启动方式枚举
*
* @author baiban
* @since 2024-12-16
*/
@Getter
public enum YunKuaiChongStartTypeEnum {
/** 通过刷卡启动充电 */
CARD_START(0x01, "CARD_START", "刷卡启动"),
/** 通过账号启动充电(暂不支持) */
ACCOUNT_START(0x02, "ACCOUNT_START", "账号启动"),
/** VIN码启动充电 */
VIN_START(0x03, "VIN_START", "VIN码启动"),
/** 未知启动方式 */
UNKNOWN(0xFF, "UNKNOWN", "未知启动方式");
/** 启动方式代码 */
private final int code;
/** 启动方式值 */
private final String value;
/** 启动方式描述 */
private final String description;
YunKuaiChongStartTypeEnum(int code, String value, String description) {
this.code = code;
this.value = value;
this.description = description;
}
/**
* 根据代码获取启动方式枚举
*
* @param code 启动方式代码
* @return 启动方式枚举未找到时返回UNKNOWN
*/
public static YunKuaiChongStartTypeEnum fromCode(int code) {
for (YunKuaiChongStartTypeEnum startType : values()) {
if (startType.code == code) {
return startType;
}
}
return UNKNOWN;
}
/**
* 根据值获取启动方式枚举
*
* @param value 启动方式值
* @return 启动方式枚举未找到时返回UNKNOWN
*/
public static YunKuaiChongStartTypeEnum fromValue(String value) {
for (YunKuaiChongStartTypeEnum startType : values()) {
if (startType.value.equals(value)) {
return startType;
}
}
return UNKNOWN;
}
/**
* 根据代码获取启动方式值
*
* @param code 启动方式代码
* @return 启动方式值
*/
public static String getValue(int code) {
return fromCode(code).getValue();
}
/**
* 根据值获取启动方式代码
*
* @param value 启动方式值
* @return 启动方式代码
*/
public static int getCode(String value) {
return fromValue(value).getCode();
}
/**
* 根据值获取启动方式描述
*
* @param value 启动方式值
* @return 启动方式描述
*/
public static String getDescription(String value) {
return fromValue(value).getDescription();
}
/**
* 根据代码获取启动方式描述
*
* @param code 启动方式代码
* @return 启动方式描述
*/
public static String getDescription(int code) {
return fromCode(code).getDescription();
}
/**
* 检查是否为有效的启动方式代码
*
* @param code 启动方式代码
* @return true表示有效false表示无效
*/
public static boolean isValid(int code) {
return fromCode(code) != UNKNOWN;
}
}

View File

@@ -42,6 +42,7 @@ public class YunKuaiChongDownlinkCmdConverter implements DownlinkCmdConverter {
COMMAND_MAP.put(DownlinkCmdEnum.VERIFY_PRICING_ACK, 0x06);
COMMAND_MAP.put(DownlinkCmdEnum.QUERY_PRICING_ACK, 0X0A);
COMMAND_MAP.put(DownlinkCmdEnum.SET_PRICING, 0x58);
COMMAND_MAP.put(DownlinkCmdEnum.START_CHARGE_ACK, 0x32);
COMMAND_MAP.put(DownlinkCmdEnum.REMOTE_START_CHARGING, 0x34);
COMMAND_MAP.put(DownlinkCmdEnum.REMOTE_STOP_CHARGING, 0x36);
COMMAND_MAP.put(DownlinkCmdEnum.TRANSACTION_RECORD_ACK, 0x40);

View File

@@ -0,0 +1,84 @@
/**
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
* 微信mohan_88888
* 抖音:程序员三丙
* 付费课程知识星球https://t.zsxq.com/aKtXo
*/
package sanbing.jcpp.protocol.yunkuaichong.v150.cmd;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import lombok.extern.slf4j.Slf4j;
import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil;
import sanbing.jcpp.proto.gen.ProtocolProto;
import sanbing.jcpp.protocol.ProtocolContext;
import sanbing.jcpp.protocol.annotation.ProtocolCmd;
import sanbing.jcpp.protocol.listener.tcp.TcpSession;
import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDownlinkCmdExe;
import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDwonlinkMessage;
import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkMessage;
import sanbing.jcpp.protocol.yunkuaichong.enums.YunKuaiChongStartChargeFailureReasonEnum;
import java.math.BigDecimal;
import static sanbing.jcpp.protocol.domain.DownlinkCmdEnum.START_CHARGE_ACK;
import static sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDwonlinkMessage.FAILURE_BYTE;
import static sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDwonlinkMessage.SUCCESS_BYTE;
import static sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongProtocolConstants.ProtocolNames.*;
/**
* 云快充1.5.0 充电桩主动申请启动充电
*
* @author baiban
*/
@Slf4j
@ProtocolCmd(value = 0x32, protocolNames = {V150, V160, V170})
public class YunKuaiChongV150StartChargeAckDLCmd extends YunKuaiChongDownlinkCmdExe {
@Override
public void execute(TcpSession tcpSession, YunKuaiChongDwonlinkMessage yunKuaiChongDwonlinkMessage, ProtocolContext ctx) {
log.info("{} 云快充1.5.0运营平台确认启动充电", tcpSession);
if (!yunKuaiChongDwonlinkMessage.getMsg().hasStartChargeResponse()) {
return;
}
ProtocolProto.StartChargeResponse startChargeResponse = yunKuaiChongDwonlinkMessage.getMsg().getStartChargeResponse();
String tradeNo = startChargeResponse.getTradeNo();
String pileCode = startChargeResponse.getPileCode();
String gunCode = startChargeResponse.getGunCode();
String logicalCardNo = startChargeResponse.getLogicalCardNo();
String limitYuan = startChargeResponse.getLimitYuan();
String failReasonValue = startChargeResponse.getFailReason();
boolean authSuccess = startChargeResponse.getAuthSuccess();
int failReasonCode = YunKuaiChongStartChargeFailureReasonEnum.getCode(failReasonValue);
ByteBuf msgBody = Unpooled.buffer(44);
// 交易流水号
msgBody.writeBytes(encodeTradeNo(tradeNo));
// 桩编码
msgBody.writeBytes(encodePileCode(pileCode));
// 枪号
msgBody.writeBytes(encodeGunCode(gunCode));
// 逻辑卡号
msgBody.writeBytes(encodeCardNo(logicalCardNo));
// 账户余额
msgBody.writeIntLE(new BigDecimal(limitYuan).multiply(new BigDecimal("100")).intValue());
// 鉴权成功标志
msgBody.writeByte(authSuccess ? SUCCESS_BYTE : FAILURE_BYTE);
// 失败原因
msgBody.writeByte(failReasonCode);
YunKuaiChongUplinkMessage requestData = JacksonUtil.fromBytes(yunKuaiChongDwonlinkMessage.getMsg().getRequestData().toByteArray(), YunKuaiChongUplinkMessage.class);
encodeAndWriteFlush(START_CHARGE_ACK,
requestData.getSequenceNumber(),
requestData.getEncryptionFlag(),
msgBody,
tcpSession);
if (!authSuccess) {
log.info("业务[云快充1.5.0 充电桩主动申请启动充电失败] 失败原因:{}", YunKuaiChongStartChargeFailureReasonEnum.getDescription(failReasonCode));
}
}
}

View File

@@ -0,0 +1,127 @@
/**
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
* 微信mohan_88888
* 抖音:程序员三丙
* 付费课程知识星球https://t.zsxq.com/aKtXo
*/
package sanbing.jcpp.protocol.yunkuaichong.v150.cmd;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.DigestUtils;
import sanbing.jcpp.infrastructure.util.codec.BCDUtil;
import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil;
import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil;
import sanbing.jcpp.proto.gen.ProtocolProto;
import sanbing.jcpp.protocol.ProtocolContext;
import sanbing.jcpp.protocol.annotation.ProtocolCmd;
import sanbing.jcpp.protocol.listener.tcp.TcpSession;
import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkCmdExe;
import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkMessage;
import sanbing.jcpp.protocol.yunkuaichong.enums.YunKuaiChongStartTypeEnum;
import sanbing.jcpp.protocol.yunkuaichong.enums.YunKuaiChongPasswordRequiredEnum;
import java.nio.charset.StandardCharsets;
import static sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongProtocolConstants.ProtocolNames.*;
/**
* 云快充1.5.0 充电桩主动申请启动充电
*
* @author baiban
*/
@Slf4j
@ProtocolCmd(value = 0x31, protocolNames = {V150, V160, V170})
public class YunKuaiChongV150StartChargeULCmd extends YunKuaiChongUplinkCmdExe {
@Override
public void execute(TcpSession tcpSession, YunKuaiChongUplinkMessage yunKuaiChongUplinkMessage, ProtocolContext ctx) {
log.debug("{} 云快充1.5.0充电桩主动申请启动充电", tcpSession);
ByteBuf byteBuf = Unpooled.wrappedBuffer(yunKuaiChongUplinkMessage.getMsgBody());
ObjectNode additionalInfo = JacksonUtil.newObjectNode();
// 从Tracer中获取当前时间
long ts = TracerContextUtil.getCurrentTracer().getTracerTs();
// 桩编号
byte[] pileCodeBytes = new byte[7];
byteBuf.readBytes(pileCodeBytes);
String pileCode = BCDUtil.toString(pileCodeBytes);
// 枪号
byte gunCodeByte = byteBuf.readByte();
String gunCode = BCDUtil.toString(gunCodeByte);
// 启动方式
int startTypeCode = byteBuf.readUnsignedByte();
String startType = YunKuaiChongStartTypeEnum.getValue(startTypeCode);
// 是否需要密码
int needPasswordCode = byteBuf.readUnsignedByte();
boolean needPassword = YunKuaiChongPasswordRequiredEnum.isPasswordRequired(needPasswordCode);
// 物理卡号
byte[] cardNoBytes = new byte[8];
byteBuf.readBytes(cardNoBytes);
String cardNo = BCDUtil.toString(cardNoBytes);
// 密码
byte[] passwordBytes = new byte[16];
byteBuf.readBytes(passwordBytes);
String password = DigestUtils.md5DigestAsHex(passwordBytes).substring(8, 24).toLowerCase();
// VIN码
byte[] carVinCodeBytes = new byte[17];
byteBuf.readBytes(carVinCodeBytes);
// VIN码反序处理
String carVinCode = reverseVinCode(new String(carVinCodeBytes, StandardCharsets.US_ASCII));
// 转发到后端
ProtocolProto.StartChargeRequest startChargingRequest = ProtocolProto.StartChargeRequest.newBuilder()
.setTs(ts)
.setPileCode(pileCode)
.setGunCode(gunCode)
.setStartType(startType)
.setNeedPassword(needPassword)
.setCardNo(cardNo)
.setPassword(password)
.setCarVinCode(carVinCode)
.setAdditionalInfo(additionalInfo.toString())
.build();
ProtocolProto.UplinkQueueMessage uplinkQueueMessage = uplinkMessageBuilder(startChargingRequest.getPileCode(), tcpSession, yunKuaiChongUplinkMessage)
.setStartChargeRequest(startChargingRequest)
.build();
tcpSession.getForwarder().sendMessage(uplinkQueueMessage);
}
/**
* VIN码反序处理
*
* @param originalVinCode 原始VIN码
* @return 反序后的VIN码
*/
private String reverseVinCode(String originalVinCode) {
if (originalVinCode == null || originalVinCode.trim().isEmpty()) {
return "";
}
// 移除末尾的null字符和空格
String trimmedVin = originalVinCode.trim().replaceAll("\0", "");
if (trimmedVin.isEmpty()) {
return "";
}
// 反序VIN码
return new StringBuilder(trimmedVin).reverse().toString();
}
}