update 电单车协议

This commit is contained in:
Guoqs
2024-08-16 10:03:16 +08:00
parent fc9bde387e
commit af02eab7ce
9 changed files with 211 additions and 63 deletions

View File

@@ -7,11 +7,7 @@ import com.jsowell.common.constant.Constants;
import com.jsowell.common.core.domain.ykc.RealTimeMonitorData; import com.jsowell.common.core.domain.ykc.RealTimeMonitorData;
import com.jsowell.common.core.page.PageResponse; import com.jsowell.common.core.page.PageResponse;
import com.jsowell.common.enums.DelFlagEnum; import com.jsowell.common.enums.DelFlagEnum;
import com.jsowell.common.enums.ykc.BusinessTypeEnum; import com.jsowell.common.enums.ykc.*;
import com.jsowell.common.enums.ykc.OrderPayModeEnum;
import com.jsowell.common.enums.ykc.OrderStatusEnum;
import com.jsowell.common.enums.ykc.PileStatusEnum;
import com.jsowell.common.enums.ykc.ReturnCodeEnum;
import com.jsowell.common.exception.BusinessException; import com.jsowell.common.exception.BusinessException;
import com.jsowell.common.util.*; import com.jsowell.common.util.*;
import com.jsowell.pile.domain.*; import com.jsowell.pile.domain.*;
@@ -235,7 +231,7 @@ public class PileService {
return null; return null;
} }
// 枪口状态不为2占用未充电 // 枪口状态不为2占用未充电
if (!StringUtils.equals("2", pileConnectorDetailVO.getConnectorStatus())) { if (!StringUtils.equals(PileConnectorDataBaseStatusEnum.OCCUPIED_NOT_CHARGED.getValue(), pileConnectorDetailVO.getConnectorStatus())) {
throw new BusinessException(ReturnCodeEnum.CODE_PILE_CONNECTOR_STATUS_ERROR); throw new BusinessException(ReturnCodeEnum.CODE_PILE_CONNECTOR_STATUS_ERROR);
} }
String pileSn = pileConnectorDetailVO.getPileSn(); String pileSn = pileConnectorDetailVO.getPileSn();

View File

@@ -1,6 +1,6 @@
package com.jsowell.netty.decoder; package com.jsowell.netty.decoder;
import com.jsowell.netty.domain.ebike.EBikeMessage; import com.jsowell.netty.domain.ebike.AbsEBikeMessage;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.codec.ByteToMessageDecoder;
@@ -22,7 +22,7 @@ public class MessageDecode extends ByteToMessageDecoder {
in.readBytes(bytes); in.readBytes(bytes);
// 解析字节数组 // 解析字节数组
EBikeMessage message = EBikeMessage.parseMessage(bytes); AbsEBikeMessage message = AbsEBikeMessage.parseMessage(bytes);
out.add(message); out.add(message);
} }
} }

View File

@@ -1,62 +1,64 @@
package com.jsowell.netty.domain.ebike; package com.jsowell.netty.domain.ebike;
import com.jsowell.common.YouDianUtils;
import com.jsowell.common.util.BytesUtil; import com.jsowell.common.util.BytesUtil;
import lombok.AllArgsConstructor; import com.jsowell.netty.domain.ebike.deviceupload.EBikeMessageCmd03;
import lombok.Builder; import com.jsowell.netty.domain.ebike.deviceupload.SettlementInfo;
import lombok.Data; import com.jsowell.netty.domain.ebike.serversend.EBikeMessageCmd82;
import lombok.NoArgsConstructor; import com.jsowell.netty.domain.ebike.serversend.SpecificDataCmd82;
import lombok.extern.slf4j.Slf4j; import lombok.Getter;
import org.apache.commons.lang3.builder.ToStringBuilder; import lombok.Setter;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
@Slf4j @Getter
@Data @Setter
@NoArgsConstructor public abstract class AbsEBikeMessage {
@AllArgsConstructor
@Builder
public class EBikeMessage {
private String header; // 包头 (3字节) private String header; // 包头 (3字节)
private int length; // 长度 (2字节) private int length; // 长度 (2字节)
private int physicalId; // 物理ID (4字节) private int physicalId; // 物理ID (4字节)
private int messageId; // 消息ID (2字节) private int messageId; // 消息ID (2字节)
private String command; // 命令 (1字节) private String command; // 命令 (1字节)
private byte[] data; // 数据 (n字节) private Object payload; // 数据 (n字节)
private int checksum; // 校验 (2字节) private int checksum; // 校验 (2字节)
@Override public AbsEBikeMessage(String header, int length, int physicalId, int messageId, String command, Object payload, int checksum) {
public String toString() { this.header = header;
return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) this.length = length;
.append("header", header) this.physicalId = physicalId;
.append("length", length) this.messageId = messageId;
.append("physicalId", physicalId) this.command = command;
.append("messageId", messageId) this.payload = payload;
.append("command", command) this.checksum = checksum;
.append("data", data)
.append("checksum", checksum)
.toString();
} }
public static EBikeMessage parseMessage(byte[] messageBytes) { public abstract void parsePayload(byte[] dataBytes);
private static AbsEBikeMessage createMessageInstance(String command, String header, int length, int physicalId, int messageId, int checksum, byte[] dataBytes) {
switch (command) {
case "82":
return new EBikeMessageCmd82(header, length, physicalId, messageId, command, null, checksum, new SpecificDataCmd82(dataBytes));
case "03":
return new EBikeMessageCmd03(header, length, physicalId, messageId, command, null, checksum, new SettlementInfo(dataBytes));
default:
throw new IllegalArgumentException("Unsupported command: " + command);
}
}
public static AbsEBikeMessage parseMessage(byte[] messageBytes) {
if (messageBytes == null || messageBytes.length < 14 || messageBytes.length > 256) { if (messageBytes == null || messageBytes.length < 14 || messageBytes.length > 256) {
throw new IllegalArgumentException("Invalid message bytes"); throw new IllegalArgumentException("Invalid message bytes");
} }
EBikeMessage message = new EBikeMessage();
try { try {
// 读取包头 // 读取包头
byte[] headerBytes = Arrays.copyOfRange(messageBytes, 0, 3); byte[] headerBytes = Arrays.copyOfRange(messageBytes, 0, 3);
String header = new String(headerBytes, StandardCharsets.UTF_8); String header = new String(headerBytes, StandardCharsets.UTF_8);
message.setHeader(header);
// 读取长度 // 读取长度
byte[] lengthBytes = Arrays.copyOfRange(messageBytes, 3, 5); byte[] lengthBytes = Arrays.copyOfRange(messageBytes, 3, 5);
int length = BytesUtil.bytesToIntLittle(lengthBytes); int length = BytesUtil.bytesToIntLittle(lengthBytes);
message.setLength(length);
// 验证长度 // 验证长度
if (length != (messageBytes.length - 5)) { if (length != (messageBytes.length - 5)) {
@@ -66,38 +68,29 @@ public class EBikeMessage {
// 读取物理ID // 读取物理ID
byte[] physicalIdBytes = Arrays.copyOfRange(messageBytes, 5, 9); byte[] physicalIdBytes = Arrays.copyOfRange(messageBytes, 5, 9);
int physicalId = BytesUtil.bytesToIntLittle(physicalIdBytes); int physicalId = BytesUtil.bytesToIntLittle(physicalIdBytes);
message.setPhysicalId(physicalId);
// 读取消息ID // 读取消息ID
byte[] messageIdBytes = Arrays.copyOfRange(messageBytes, 9, 11); byte[] messageIdBytes = Arrays.copyOfRange(messageBytes, 9, 11);
int messageId = BytesUtil.bytesToIntLittle(messageIdBytes); int messageId = BytesUtil.bytesToIntLittle(messageIdBytes);
message.setMessageId(messageId);
// 读取命令 // 读取命令
byte commandByte = messageBytes[11]; byte commandByte = messageBytes[11];
String command = BytesUtil.bcd2StrLittle(new byte[]{commandByte}); String command = BytesUtil.bcd2StrLittle(new byte[]{commandByte});
message.setCommand(command);
// 读取数据 // 读取数据
byte[] dataBytes = Arrays.copyOfRange(messageBytes, 12, messageBytes.length - 2); byte[] dataBytes = Arrays.copyOfRange(messageBytes, 12, messageBytes.length - 2);
message.setData(dataBytes);
String data = BytesUtil.bcd2StrLittle(dataBytes);
// 读取校验 // 读取校验
byte[] checksumBytes = Arrays.copyOfRange(messageBytes, messageBytes.length - 2, messageBytes.length); byte[] checksumBytes = Arrays.copyOfRange(messageBytes, messageBytes.length - 2, messageBytes.length);
int checksum = BytesUtil.bytesToIntLittle(checksumBytes); int checksum = BytesUtil.bytesToIntLittle(checksumBytes);
message.setChecksum(checksum);
// 校验码验证 // 根据命令创建相应的子类实例
if (!YouDianUtils.validateChecksum(messageBytes)) { AbsEBikeMessage parsedMessage = createMessageInstance(command, header, length, physicalId, messageId, checksum, dataBytes);
throw new IllegalArgumentException("Checksum validation failed"); parsedMessage.parsePayload(dataBytes);
}
log.info("报文:{}, parseMessage:{}", BytesUtil.binary(messageBytes, 16), message.toString()); return parsedMessage;
return message;
} catch (Exception e) { } catch (Exception e) {
log.error("Error parsing message", e); throw new IllegalArgumentException("Error parsing message", e);
throw e;
} }
} }

View File

@@ -0,0 +1,22 @@
package com.jsowell.netty.domain.ebike.deviceupload;
import com.jsowell.netty.domain.ebike.AbsEBikeMessage;
public class EBikeMessageCmd03 extends AbsEBikeMessage {
private SettlementInfo settlementInfo;
public EBikeMessageCmd03(String header, int length, int physicalId, int messageId, String command, Object payload, int checksum, SettlementInfo settlementInfo) {
super(header, length, physicalId, messageId, command, payload, checksum);
this.settlementInfo = settlementInfo;
}
@Override
public void parsePayload(byte[] dataBytes) {
this.settlementInfo = new SettlementInfo(dataBytes);
}
public SettlementInfo getSettlementInfo() {
return settlementInfo;
}
}

View File

@@ -0,0 +1,36 @@
package com.jsowell.netty.domain.ebike.deviceupload;
import com.jsowell.common.util.BytesUtil;
import lombok.Data;
import java.util.Arrays;
@Data
public class SettlementInfo {
private String chargingDuration; // 充电时长, 单位:"秒"
private String maxPower; // 最大功率, 单位:"0.1W"
private String consumedEnergy; // 耗电量, 单位:"0.01度"
private String portNumber; // 端口号
private String startMode; // 在线/离线启动/验证码
private String cardNumberOrVerificationCode; // 卡号/验证码
private String stopReason; // 停止原因
private String orderNumber; // 订单编号
private String secondMaxPower; // 第二最大功率
// private String timestamp; // 时间戳 上发指令当时的时间,有时候不准确,该字段属于调试使用,服务器无需关心此字段
// private String placeholderDuration; // 占位时长 充电柜专用,其他设备忽略此字段 表示充满后占用设备的时长,单位为分钟
public SettlementInfo(byte[] dataBytes) {
this.chargingDuration = BytesUtil.bytesToIntLittle(Arrays.copyOfRange(dataBytes, 0, 2)) + "";
this.maxPower = BytesUtil.bytesToIntLittle(Arrays.copyOfRange(dataBytes, 2, 4)) + "";
this.consumedEnergy = BytesUtil.bytesToIntLittle(Arrays.copyOfRange(dataBytes, 4, 6)) + "";
this.portNumber = BytesUtil.bcd2StrLittle(new byte[]{dataBytes[6]});
this.startMode = BytesUtil.bcd2StrLittle(new byte[]{dataBytes[7]});
this.cardNumberOrVerificationCode = BytesUtil.bcd2StrLittle(Arrays.copyOfRange(dataBytes, 8, 12));
this.stopReason = BytesUtil.bcd2StrLittle(new byte[]{dataBytes[12]});
this.orderNumber = BytesUtil.bcd2StrLittle(Arrays.copyOfRange(dataBytes, 13, 29));
this.secondMaxPower = BytesUtil.bytesToIntLittle(Arrays.copyOfRange(dataBytes, 29, 31)) + "";
// this.timestamp = BytesUtil.bytesToIntLittle(Arrays.copyOfRange(dataBytes, 31, 35)) + "";
// this.placeholderDuration = BytesUtil.bytesToIntLittle(Arrays.copyOfRange(dataBytes, 35, 37)) + "";
}
}

View File

@@ -0,0 +1,22 @@
package com.jsowell.netty.domain.ebike.serversend;
import com.jsowell.netty.domain.ebike.AbsEBikeMessage;
public class EBikeMessageCmd82 extends AbsEBikeMessage {
private SpecificDataCmd82 specificData;
public EBikeMessageCmd82(String header, int length, int physicalId, int messageId, String command, Object payload, int checksum, SpecificDataCmd82 specificData) {
super(header, length, physicalId, messageId, command, payload, checksum);
this.specificData = specificData;
}
@Override
public void parsePayload(byte[] dataBytes) {
this.specificData = new SpecificDataCmd82(dataBytes);
}
public SpecificDataCmd82 getSpecificData() {
return specificData;
}
}

View File

@@ -0,0 +1,78 @@
package com.jsowell.netty.domain.ebike.serversend;
import com.jsowell.common.util.BytesUtil;
import lombok.Getter;
import lombok.Setter;
import java.util.Arrays;
@Getter
@Setter
public class SpecificDataCmd82 {
private String rateMode; // 费率模式 (1字节)
private String balanceOrValidity; // 余额/有效期 (4字节)
private String portNumber; // 端口号 (1字节)
private String chargeCommand; // 充电命令 (1字节)
private String chargeDurationOrPower; // 充电时长/电量 (2字节)
private String orderNumber; // 订单编号 (16字节)
private String maxChargeDuration; // 最大充电时长 (2字节)
private String overloadPower; // 过载功率 (2字节)
private String qrCodeLight; // 二维码灯 (1字节)
private String longChargeMode; // 长充模式 (1字节)
private String extraFloatChargeTime; // 额外浮充时间 (2字节)
private String skipShortCircuitDetection; // 是否跳过短路检测 (1字节)
private String noUserPullOutCheck; // 不判断用户拔出 (1字节)
private String forceAutoStopWhenFull; // 强制带充满自停 (1字节)
private String fullChargePower; // 充满功率 (1字节)
private String maxFullChargePowerCheckTime; // 充满功率最长判断时间 (1字节)
public SpecificDataCmd82(byte[] dataBytes) {
byte rateModeBytes = dataBytes[0];
this.rateMode = BytesUtil.bcd2StrLittle(new byte[]{rateModeBytes});
byte[] balanceOrValidityBytes = Arrays.copyOfRange(dataBytes, 1, 5);
this.balanceOrValidity = BytesUtil.bcd2StrLittle(balanceOrValidityBytes);
byte portNumberBytes = dataBytes[5];
this.portNumber = BytesUtil.bcd2StrLittle(new byte[]{portNumberBytes});
byte chargeCommandBytes = dataBytes[6];
this.chargeCommand = BytesUtil.bcd2StrLittle(new byte[]{chargeCommandBytes});
byte[] chargeDurationOrPowerBytes = Arrays.copyOfRange(dataBytes, 7, 9);
this.chargeDurationOrPower = BytesUtil.bcd2StrLittle(chargeDurationOrPowerBytes);
byte[] orderNumberBytes = Arrays.copyOfRange(dataBytes, 9, 25);
this.orderNumber = BytesUtil.bcd2StrLittle(orderNumberBytes);
byte[] maxChargeDurationBytes = Arrays.copyOfRange(dataBytes, 25, 27);
this.maxChargeDuration = BytesUtil.bcd2StrLittle(maxChargeDurationBytes);
byte[] overloadPowerBytes = Arrays.copyOfRange(dataBytes, 27, 29);
this.overloadPower = BytesUtil.bcd2StrLittle(overloadPowerBytes);
byte qrCodeLightBytes = dataBytes[29];
this.qrCodeLight = BytesUtil.bcd2StrLittle(new byte[]{qrCodeLightBytes});
byte longChargeModeBytes = dataBytes[30];
this.longChargeMode = BytesUtil.bcd2StrLittle(new byte[]{longChargeModeBytes});
byte[] extraFloatChargeTimeBytes = Arrays.copyOfRange(dataBytes, 31, 33);
this.extraFloatChargeTime = BytesUtil.bcd2StrLittle(extraFloatChargeTimeBytes);
byte skipShortCircuitDetectionBytes = dataBytes[33];
this.skipShortCircuitDetection = BytesUtil.bcd2StrLittle(new byte[]{skipShortCircuitDetectionBytes});
byte noUserPullOutCheckBytes = dataBytes[34];
this.noUserPullOutCheck = BytesUtil.bcd2StrLittle(new byte[]{noUserPullOutCheckBytes});
byte forceAutoStopWhenFullByte = dataBytes[35];
this.forceAutoStopWhenFull = BytesUtil.bcd2StrLittle(new byte[]{forceAutoStopWhenFullByte});
byte fullChargePowerBytes = dataBytes[36];
this.fullChargePower = BytesUtil.bcd2StrLittle(new byte[]{fullChargePowerBytes});
byte maxFullChargePowerCheckTimeBytes = dataBytes[37];
this.maxFullChargePowerCheckTime = BytesUtil.bcd2StrLittle(new byte[]{maxFullChargePowerCheckTimeBytes});
}
}

View File

@@ -1,7 +1,7 @@
package com.jsowell.netty.server.electricbicycles; package com.jsowell.netty.server.electricbicycles;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import com.jsowell.netty.domain.ebike.EBikeMessage; import com.jsowell.netty.domain.ebike.AbsEBikeMessage;
import io.netty.channel.*; import io.netty.channel.*;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -9,10 +9,10 @@ import org.springframework.stereotype.Component;
@ChannelHandler.Sharable @ChannelHandler.Sharable
@Slf4j @Slf4j
@Component @Component
public class ChargingPileHandler extends SimpleChannelInboundHandler<EBikeMessage> { public class ChargingPileHandler extends SimpleChannelInboundHandler<AbsEBikeMessage> {
@Override @Override
protected void channelRead0(ChannelHandlerContext ctx, EBikeMessage msg) throws Exception { protected void channelRead0(ChannelHandlerContext ctx, AbsEBikeMessage msg) throws Exception {
log.info("收到消息, channelId:{}, msg:{}", ctx.channel().id().toString(), JSON.toJSONString(msg)); log.info("收到消息, channelId:{}, msg:{}", ctx.channel().id().toString(), JSON.toJSONString(msg));
} }

View File

@@ -11,7 +11,10 @@ import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode;
import com.jsowell.common.core.redis.RedisCache; import com.jsowell.common.core.redis.RedisCache;
import com.jsowell.common.enums.DelFlagEnum; import com.jsowell.common.enums.DelFlagEnum;
import com.jsowell.common.enums.lianlian.LianLianPileStatusEnum; import com.jsowell.common.enums.lianlian.LianLianPileStatusEnum;
import com.jsowell.common.enums.ykc.*; import com.jsowell.common.enums.ykc.PileConnectorDataBaseStatusEnum;
import com.jsowell.common.enums.ykc.PileConnectorStatusEnum;
import com.jsowell.common.enums.ykc.PileStatusEnum;
import com.jsowell.common.enums.ykc.ReturnCodeEnum;
import com.jsowell.common.exception.BusinessException; import com.jsowell.common.exception.BusinessException;
import com.jsowell.common.util.*; import com.jsowell.common.util.*;
import com.jsowell.pile.domain.*; import com.jsowell.pile.domain.*;
@@ -25,7 +28,6 @@ import com.jsowell.pile.thirdparty.EquipmentInfo;
import com.jsowell.pile.thirdparty.ZDLConnectorInfo; import com.jsowell.pile.thirdparty.ZDLConnectorInfo;
import com.jsowell.pile.thirdparty.ZDLEquipmentInfo; import com.jsowell.pile.thirdparty.ZDLEquipmentInfo;
import com.jsowell.pile.util.UserUtils; import com.jsowell.pile.util.UserUtils;
import com.jsowell.pile.vo.PileReservationInfoVO;
import com.jsowell.pile.vo.base.MerchantInfoVO; import com.jsowell.pile.vo.base.MerchantInfoVO;
import com.jsowell.pile.vo.base.PileInfoVO; import com.jsowell.pile.vo.base.PileInfoVO;
import com.jsowell.pile.vo.uniapp.customer.GroundLockInfoVO; import com.jsowell.pile.vo.uniapp.customer.GroundLockInfoVO;
@@ -43,7 +45,6 @@ import org.springframework.stereotype.Service;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.sql.Time;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -1212,8 +1213,9 @@ public class PileBasicInfoServiceImpl implements PileBasicInfoService {
public String startPersonalPileCharging(StartPersonPileDTO dto) { public String startPersonalPileCharging(StartPersonPileDTO dto) {
// 查询充电桩信息 // 查询充电桩信息
PileConnectorDetailVO pileConnectorDetailVO = queryPileConnectorDetail(dto.getPileConnectorCode()); PileConnectorDetailVO pileConnectorDetailVO = queryPileConnectorDetail(dto.getPileConnectorCode());
if (pileConnectorDetailVO == null) { if (pileConnectorDetailVO == null
throw new BusinessException("", ""); || !StringUtils.equals(pileConnectorDetailVO.getConnectorStatus(), PileConnectorDataBaseStatusEnum.OCCUPIED_NOT_CHARGED.getValue())) {
throw new BusinessException(ReturnCodeEnum.CODE_PILE_CONNECTOR_STATUS_ERROR);
} }
dto.setMerchantId(pileConnectorDetailVO.getMerchantId()); dto.setMerchantId(pileConnectorDetailVO.getMerchantId());
dto.setStationId(pileConnectorDetailVO.getStationId()); dto.setStationId(pileConnectorDetailVO.getStationId());
@@ -1221,8 +1223,7 @@ public class PileBasicInfoServiceImpl implements PileBasicInfoService {
// String mode = pileMerchantInfoService.getDelayModeByAppIdAndRequestSource(dto.getAppId(), dto.getRequestSource()); // String mode = pileMerchantInfoService.getDelayModeByAppIdAndRequestSource(dto.getAppId(), dto.getRequestSource());
String mode = pileMerchantInfoService.getDelayModeByMerchantId(pileConnectorDetailVO.getMerchantId()); String mode = pileMerchantInfoService.getDelayModeByMerchantId(pileConnectorDetailVO.getMerchantId());
AbstractProgramLogic orderLogic = ProgramLogicFactory.getProgramLogic(mode); AbstractProgramLogic orderLogic = ProgramLogicFactory.getProgramLogic(mode);
String orderCode = orderLogic.startPersonalPileCharging(dto); return orderLogic.startPersonalPileCharging(dto);
return orderCode;
} }
/** /**