diff --git a/README.md b/README.md index d63686b..528e230 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,10 @@ ------------------------------ #### 当前支持的充电桩协议 -| 协议名 | 版本号 | -|---|------------| -| 云快充 | 1.5.0、1.6.0 | +| 协议名 | 版本号 | +|-----|-------------| +| 云快充 | 1.5.0、1.6.0 | +| 绿能 | 3.4 | ------------------------------ #### 充电桩协议文档 diff --git a/jcpp-app-bootstrap/pom.xml b/jcpp-app-bootstrap/pom.xml index 52e9b2b..23150f1 100644 --- a/jcpp-app-bootstrap/pom.xml +++ b/jcpp-app-bootstrap/pom.xml @@ -37,6 +37,10 @@ sanbing jcpp-protocol-yunkuaichong + + sanbing + jcpp-protocol-lvneng + org.testcontainers junit-jupiter diff --git a/jcpp-app-bootstrap/src/main/resources/app-service.yml b/jcpp-app-bootstrap/src/main/resources/app-service.yml index 72a6265..6db7cbd 100644 --- a/jcpp-app-bootstrap/src/main/resources/app-service.yml +++ b/jcpp-app-bootstrap/src/main/resources/app-service.yml @@ -228,7 +228,7 @@ service: buffer-memory: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_BUFFER_MEMORY:33554432}" other-properties: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_QUEUE_KAFKA_OTHER_PROPERTIES:}" yunkuaichongV160: - enabled: "${PROTOCOLS_YUNKUAICHONGV150_ENABLED:true}" + enabled: "${PROTOCOLS_YUNKUAICHONGV160_ENABLED:true}" listener: tcp: bind-address: "${PROTOCOLS_YUNKUAICHONGV160_LISTENER_TCP_BIND_ADDRESS:0.0.0.0}" @@ -266,4 +266,42 @@ service: linger-ms: "${PROTOCOLS_YUNKUAICHONGV160_FORWARD_KAFKA_LINGER_MS:0}" buffer-memory: "${PROTOCOLS_YUNKUAICHONGV160_FORWARD_BUFFER_MEMORY:33554432}" other-properties: "${PROTOCOLS_YUNKUAICHONGV160_FORWARD_QUEUE_KAFKA_OTHER_PROPERTIES:}" - + lvnengV340: + enabled: "${PROTOCOLS_LVNENG340_ENABLED:true}" + listener: + tcp: + bind-address: "${PROTOCOLS_LVNENG340_LISTENER_TCP_BIND_ADDRESS:0.0.0.0}" + bind-port: "${PROTOCOLS_LVNENG340_LISTENER_TCP_BIND_PORT:38011}" + boss-group-thread_count: "${PROTOCOLS_LVNENG340_LISTENER_TCP_BOSS_GROUP_THREADS:4}" + worker-group-thread-count: "${PROTOCOLS_LVNENG340_LISTENER_TCP_WORKER_GROUP_THREADS:16}" + so-keep-alive: "${PROTOCOLS_LVNENG340_LISTENER_TCP_SO_KEEPALIVE:true}" + so-backlog: "${PROTOCOLS_LVNENG340_LISTENER_TCP_SO_BACKLOG:128}" + so-rcvbuf: "${PROTOCOLS_LVNENG340_LISTENER_TCP_SO_RCVBUF:65536}" + so-sndbuf: "${PROTOCOLS_LVNENG340_LISTENER_TCP_SO_SNDBUF:65536}" + nodelay: "${PROTOCOLS_LVNENG340_LISTENER_TCP_NODELAY:true}" + handler: + idle-timeout-seconds: "${PROTOCOLS_LVNENG340_LISTENER_TCP_HANDLER_IDLE_TIMEOUT_SECONDS:600}" + max_connections: "${PROTOCOLS_LVNENG340_LISTENER_TCP_HANDLER_MAX_CONNECTIONS:100000}" + # 默认为二进制类型的拆包器 + # 可选JSON类型的拆包器 "${PROTOCOLS_LVNENG340_NETTY_HANDLER_BINARY_CONFIGURATION:type:JSON}" + # 可选纯文本类型的拆包器 "${PROTOCOLS_LVNENG340_NETTY_HANDLER_BINARY_CONFIGURATION:type:TEXT;maxFrameLength:128;stripDelimiter:true;messageSeparator:null;charsetName:UTF-8}" + configuration: "${PROTOCOLS_LVNENG340_NETTY_HANDLER_BINARY_CONFIGURATION:type:BINARY;decoder:sanbing.jcpp.protocol.listener.tcp.decoder.JCPPLengthFieldBasedFrameDecoder;byteOrder:LITTLE_ENDIAN;head:AAF5;lengthFieldOffset:2;lengthFieldLength:2;lengthAdjustment:-4;initialBytesToStrip:0}" + forwarder: + # 如果是单体服务,可选kafka、memory,未来计划扩展RocketMQ, GRpc、REST + type: "${PROTOCOLS_LVNENG340_FORWARD_TYPE:memory}" + memory: + topic: "${PROTOCOLS_LVNENG340_FORWARD_MEMORY_TOPIC:protocol_uplink}" + kafka: + topic: "${PROTOCOLS_LVNENG340_FORWARD_KAFKA_TOPIC:protocol_uplink}" + jcpp-partition: "${PROTOCOLS_LVNENG340_FORWARD_KAFKA_JCPP_PARTITION:true}" # 是否利用JCPP的分片框架 + # 以下配置只有在service.type为protocol时且jcpp-partition为false时才生效 + bootstrap-servers: "${PROTOCOLS_LVNENG340_FORWARD_KAFKA_SERVERS:kafka:9092}" + acks: "${PROTOCOLS_LVNENG340_FORWARD_KAFKA_ACKS:1}" + # 可选 protobuf(推荐)、json + encoder: "${PROTOCOLS_LVNENG340_FORWARD_KAFKA_ENCODER:protobuf}" + retries: "${PROTOCOLS_LVNENG340_FORWARD_KAFKA_RETRIES:1}" + compression-type: "${PROTOCOLS_LVNENG340_FORWARD_KAFKA_COMPRESSION_TYPE:none}" # none, gzip, snappy, lz4, zstd + batch-size: "${PROTOCOLS_LVNENG340_FORWARD_KAFKA_BATCH_SIZE:16384}" + linger-ms: "${PROTOCOLS_LVNENG340_FORWARD_KAFKA_LINGER_MS:0}" + buffer-memory: "${PROTOCOLS_LVNENG340_FORWARD_BUFFER_MEMORY:33554432}" + other-properties: "${PROTOCOLS_LVNENG340_FORWARD_QUEUE_KAFKA_OTHER_PROPERTIES:}" diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/config/DownlinkRestTemplateConfiguration.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/config/DownlinkRestTemplateConfiguration.java index 52acd9e..1d35c3f 100644 --- a/jcpp-app/src/main/java/sanbing/jcpp/app/service/config/DownlinkRestTemplateConfiguration.java +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/config/DownlinkRestTemplateConfiguration.java @@ -25,8 +25,8 @@ public class DownlinkRestTemplateConfiguration { @Bean("downlinkRestTemplate") public RestTemplate downlinkRestTemplate() { RestTemplate restTemplate = new RestTemplateBuilder() - .setConnectTimeout(Duration.of(3, ChronoUnit.SECONDS)) - .setReadTimeout(Duration.of(3, ChronoUnit.SECONDS)) + .connectTimeout(Duration.of(3, ChronoUnit.SECONDS)) + .readTimeout(Duration.of(3, ChronoUnit.SECONDS)) .build(); restTemplate.setMessageConverters(Collections.singletonList(new ProtobufHttpMessageConverter())); return restTemplate; diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPRedisCacheConfiguration.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPRedisCacheConfiguration.java index 4a14bf7..0c13395 100644 --- a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPRedisCacheConfiguration.java +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPRedisCacheConfiguration.java @@ -6,7 +6,7 @@ */ package sanbing.jcpp.infrastructure.cache; -import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.StatefulConnection; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; @@ -135,8 +135,8 @@ public abstract class JCPPRedisCacheConfiguration { registry.addConverter(UUID.class, String.class, UUID::toString); } - protected GenericObjectPoolConfig> buildPoolConfig() { - GenericObjectPoolConfig> poolConfig = new GenericObjectPoolConfig<>(); + protected GenericObjectPoolConfig> buildPoolConfig() { + GenericObjectPoolConfig> poolConfig = new GenericObjectPoolConfig<>(); poolConfig.setMaxTotal(maxTotal); poolConfig.setMaxIdle(maxIdle); poolConfig.setMinIdle(minIdle); diff --git a/jcpp-infrastructure-queue/pom.xml b/jcpp-infrastructure-queue/pom.xml index 2d33b24..928f2d3 100644 --- a/jcpp-infrastructure-queue/pom.xml +++ b/jcpp-infrastructure-queue/pom.xml @@ -48,6 +48,10 @@ org.apache.curator curator-recipes + + org.apache.zookeeper + zookeeper + diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/DefaultServiceInfoProvider.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/DefaultServiceInfoProvider.java index 8176062..2dc602b 100644 --- a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/DefaultServiceInfoProvider.java +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/DefaultServiceInfoProvider.java @@ -59,7 +59,7 @@ public class DefaultServiceInfoProvider implements ServiceInfoProvider { try { this.serviceId = InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { - this.serviceId = RandomStringUtils.randomAlphabetic(10); + this.serviceId = RandomStringUtils.secure().nextAlphabetic(10); } } log.info("Current Service ID: {}", serviceId); diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/codec/BCDUtil.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/codec/BCDUtil.java index 6d29e31..834e4be 100644 --- a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/codec/BCDUtil.java +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/codec/BCDUtil.java @@ -6,6 +6,8 @@ */ package sanbing.jcpp.infrastructure.util.codec; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; public class BCDUtil { private static final String HEX = "0123456789ABCDEF"; @@ -171,4 +173,124 @@ public class BCDUtil { return sb.toString().toUpperCase(); } + private static final int HOUR_24 = 0x24; + private static final int ZERO_TIME = 0x00; + private static final int BCD_DATE_LENGTH = 8; + private static final char[] DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; + private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + + /** + * BCD编码的时间转换为LocalDateTime对象 + * 格式: YYYYMMDDHHmmss (8字节BCD) + * 示例: 20240315235959 + * + * @param bcdBytes BCD编码的时间字节数组,必须是8字节 + * @return 转换后的LocalDateTime对象,如果输入无效则返回null + * @throws IllegalArgumentException 如果输入字节数组长度不是8 + */ + public static LocalDateTime bcdToDate(byte[] bcdBytes) { + if (bcdBytes == null || bcdBytes.length != BCD_DATE_LENGTH) { + throw new IllegalArgumentException("BCD date bytes must be 8 bytes long"); + } + + // 检查是否全为0 + boolean allZero = true; + for (int i = 0; i < 7; i++) { + if (bcdBytes[i] != ZERO_TIME) { + allZero = false; + break; + } + } + if (allZero) { + return null; + } + + // 使用StringBuilder预分配容量,避免扩容 + StringBuilder timeStr = new StringBuilder(14); + + // 年月日 + for (int i = 0; i < 4; i++) { + appendBcdByte(timeStr, bcdBytes[i]); + } + + // 小时特殊处理 + byte hour = bcdBytes[4]; + if ((hour & 0xff) == HOUR_24) { + timeStr.append("00"); + } else { + appendBcdByte(timeStr, hour); + } + + // 分秒 + appendBcdByte(timeStr, bcdBytes[5]); + appendBcdByte(timeStr, bcdBytes[6]); + + try { + return LocalDateTime.parse(timeStr.toString(), DATETIME_FORMATTER); + } catch (Exception e) { + return null; + } + } + + /** + * 将单个BCD字节追加到StringBuilder + * 性能优化: 直接计算BCD值并追加,避免字符串转换 + */ + private static void appendBcdByte(StringBuilder sb, byte bcd) { + // 高4位 + sb.append(DIGITS[(bcd >> 4) & 0x0F]); + // 低4位 + sb.append(DIGITS[bcd & 0x0F]); + } + /** + * LocalDateTime转换为8字节BCD编码 + * 格式: YYYYMMDDHHmmss + 0xFF (8字节BCD) + * 示例: 20240315235959FF + * + * @param dateTime 要转换的LocalDateTime对象 + * @return 8字节BCD编码的字节数组,最后一个字节固定为0xFF;如果输入为null则返回null + */ + public static byte[] dateToBcd8(LocalDateTime dateTime) { + if (dateTime == null) { + return null; + } + + byte[] bytes = new byte[8]; + + // 年 (2字节) + int year = dateTime.getYear(); + bytes[0] = (byte) ((year / 100) << 4 | (year / 10 % 10)); + bytes[1] = (byte) ((year % 10) << 4); + + // 月 (与年的最后4位共用一个字节) + int month = dateTime.getMonthValue(); + bytes[1] |= (byte) (month / 10); + bytes[2] = (byte) ((month % 10) << 4); + + // 日 (与月的最后4位共用一个字节) + int day = dateTime.getDayOfMonth(); + bytes[2] |= (byte) (day / 10); + bytes[3] = (byte) ((day % 10) << 4); + + // 时 (与日的最后4位共用一个字节) + int hour = dateTime.getHour(); + bytes[3] |= (byte) (hour / 10); + bytes[4] = (byte) ((hour % 10) << 4); + + // 分 (与时的最后4位共用一个字节) + int minute = dateTime.getMinute(); + bytes[4] |= (byte) (minute / 10); + bytes[5] = (byte) ((minute % 10) << 4); + + // 秒 (与分的最后4位共用一个字节) + int second = dateTime.getSecond(); + bytes[5] |= (byte) (second / 10); + bytes[6] = (byte) ((second % 10) << 4); + + // 最后一个字节固定为0xFF + bytes[7] = (byte) 0xFF; + + return bytes; + } + } diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/codec/ByteUtil.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/codec/ByteUtil.java index b0add1f..b16029c 100644 --- a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/codec/ByteUtil.java +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/codec/ByteUtil.java @@ -71,8 +71,8 @@ public class ByteUtil { /** * ByteBuf转byte数组 * - * @param byteBuf - * @return + * @param byteBuf ByteBuf对象 + * @return 转换后的字节数组 */ public static byte[] toBytes(ByteBuf byteBuf) { int msgLength = byteBuf.readableBytes(); @@ -80,4 +80,48 @@ public class ByteUtil { byteBuf.readBytes(bytes); return bytes; } + + /** + * 计算字节数组的累加和,如果累加结果超过1字节,则只取低8位 + * + * 示例: + * byte[] data = {0x01, 0x02, 0x03}; + * byte sum = calculateSum(data); // sum = 0x06 + * + * byte[] data2 = {(byte)0xFF, (byte)0xFF}; + * byte sum2 = calculateSum(data2); // sum2 = (byte)0xFE (254 + 255 = 509, 取低8位为254) + * + * @param data 要计算累加和的字节数组 + * @return 累加和的低8位 + * @throws IllegalArgumentException 如果输入数组为null + */ + public static byte calculateSum(byte[] data) { + if (data == null) { + throw new IllegalArgumentException("输入数组不能为null"); + } + + int sum = 0; + for (byte b : data) { + sum += b & 0xFF; + } + + return (byte) (sum & 0xFF); + } + + /** + * 验证数据的累加和是否与期望值相等 + * + * 示例: + * byte[] data = {0x01, 0x02, 0x03}; + * boolean valid = verifySum(data, (byte)0x06); // valid = true + * + * @param data 要验证的数据 + * @param expectedSum 期望的累加和 + * @return 包含验证结果和实际计算出的累加和的键值对 + * @throws IllegalArgumentException 如果输入数组为null + */ + public static JCPPPair verifySum(byte[] data, byte expectedSum) { + byte actualSum = calculateSum(data); + return JCPPPair.of(actualSum == expectedSum, actualSum); + } } \ No newline at end of file diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/JacksonUtil.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/JacksonUtil.java index f32c8ef..9e6833b 100644 --- a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/JacksonUtil.java +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/JacksonUtil.java @@ -190,28 +190,4 @@ public class JacksonUtil { return node; } - /** - * 合并两个ObjectNode. - * 如果存在相同的字段,优先保留第二个ObjectNode中的值。 - * - * @param node1 the first ObjectNode - * @param node2 the second ObjectNode - * @return 合并后的结果 - */ - public static ObjectNode merge(ObjectNode node1, ObjectNode node2) { - ObjectNode mergedNode = OBJECT_MAPPER.createObjectNode(); - - // 把第一个节点的所有字段添加到mergedNode中 - node1.fields().forEachRemaining(entry -> { - mergedNode.set(entry.getKey(), entry.getValue()); - }); - - // 把第二个节点的所有字段添加到mergedNode中,覆盖相同字段 - node2.fields().forEachRemaining(entry -> { - mergedNode.set(entry.getKey(), entry.getValue()); - }); - - return mergedNode; - } - } diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/TcpSession.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/TcpSession.java index 727bbf5..24faa70 100644 --- a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/TcpSession.java +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/TcpSession.java @@ -30,6 +30,8 @@ import java.util.function.Consumer; @Setter public class TcpSession extends ProtocolSession { + public static final String SCHEDULE_KEY_AUTO_SYNC_TIME = "auto-sync-time"; + private SocketAddress address; private ChannelHandlerContext ctx; diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/decoder/JCPPLengthFieldBasedFrameDecoder.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/decoder/JCPPLengthFieldBasedFrameDecoder.java index 3bdb8a6..acba4f6 100644 --- a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/decoder/JCPPLengthFieldBasedFrameDecoder.java +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/decoder/JCPPLengthFieldBasedFrameDecoder.java @@ -82,7 +82,7 @@ public class JCPPLengthFieldBasedFrameDecoder extends ByteToMessageDecoder { protected Object decode(ChannelHandlerContext ctx, ByteBuf in) { if (log.isDebugEnabled()) { String hexDump = ByteBufUtil.hexDump(in); - log.debug("{} 开始解析16进制报文:{}", ctx.channel(), hexDump); + log.debug("{} 开始拆解16进制报文:{}", ctx.channel(), hexDump); } // 帧长 long frameLength = 0; diff --git a/jcpp-protocol-bootstrap/pom.xml b/jcpp-protocol-bootstrap/pom.xml index 1d72e50..696e97f 100644 --- a/jcpp-protocol-bootstrap/pom.xml +++ b/jcpp-protocol-bootstrap/pom.xml @@ -33,6 +33,10 @@ sanbing jcpp-protocol-yunkuaichong + + sanbing + jcpp-protocol-lvneng + diff --git a/jcpp-protocol-bootstrap/src/main/resources/protocol-service.yml b/jcpp-protocol-bootstrap/src/main/resources/protocol-service.yml index 68c8b49..f3f451e 100644 --- a/jcpp-protocol-bootstrap/src/main/resources/protocol-service.yml +++ b/jcpp-protocol-bootstrap/src/main/resources/protocol-service.yml @@ -134,6 +134,45 @@ service: linger-ms: "${PROTOCOLS_YUNKUAICHONGV160_FORWARD_KAFKA_LINGER_MS:0}" buffer-memory: "${PROTOCOLS_YUNKUAICHONGV160_FORWARD_BUFFER_MEMORY:33554432}" other-properties: "${PROTOCOLS_YUNKUAICHONGV160_FORWARD_QUEUE_KAFKA_OTHER_PROPERTIES:}" + lvnengV340: + enabled: "${PROTOCOLS_LVNENG340_ENABLED:true}" + listener: + tcp: + bind-address: "${PROTOCOLS_LVNENG340_LISTENER_TCP_BIND_ADDRESS:0.0.0.0}" + bind-port: "${PROTOCOLS_LVNENG340_LISTENER_TCP_BIND_PORT:38011}" + boss-group-thread_count: "${PROTOCOLS_LVNENG340_LISTENER_TCP_BOSS_GROUP_THREADS:4}" + worker-group-thread-count: "${PROTOCOLS_LVNENG340_LISTENER_TCP_WORKER_GROUP_THREADS:16}" + so-keep-alive: "${PROTOCOLS_LVNENG340_LISTENER_TCP_SO_KEEPALIVE:true}" + so-backlog: "${PROTOCOLS_LVNENG340_LISTENER_TCP_SO_BACKLOG:128}" + so-rcvbuf: "${PROTOCOLS_LVNENG340_LISTENER_TCP_SO_RCVBUF:65536}" + so-sndbuf: "${PROTOCOLS_LVNENG340_LISTENER_TCP_SO_SNDBUF:65536}" + nodelay: "${PROTOCOLS_LVNENG340_LISTENER_TCP_NODELAY:true}" + handler: + idle-timeout-seconds: "${PROTOCOLS_LVNENG340_LISTENER_TCP_HANDLER_IDLE_TIMEOUT_SECONDS:600}" + max_connections: "${PROTOCOLS_LVNENG340_LISTENER_TCP_HANDLER_MAX_CONNECTIONS:100000}" + # 默认为二进制类型的拆包器 + # 可选JSON类型的拆包器 "${PROTOCOLS_LVNENG340_NETTY_HANDLER_BINARY_CONFIGURATION:type:JSON}" + # 可选纯文本类型的拆包器 "${PROTOCOLS_LVNENG340_NETTY_HANDLER_BINARY_CONFIGURATION:type:TEXT;maxFrameLength:128;stripDelimiter:true;messageSeparator:null;charsetName:UTF-8}" + configuration: "${PROTOCOLS_LVNENG340_NETTY_HANDLER_BINARY_CONFIGURATION:type:BINARY;decoder:sanbing.jcpp.protocol.listener.tcp.decoder.JCPPLengthFieldBasedFrameDecoder;byteOrder:LITTLE_ENDIAN;head:AAF5;lengthFieldOffset:2;lengthFieldLength:2;lengthAdjustment:-4;initialBytesToStrip:0}" + forwarder: + # 如果是单体服务,可选kafka、memory,未来计划扩展RocketMQ, GRpc、REST + type: "${PROTOCOLS_LVNENG340_FORWARD_TYPE:memory}" + memory: + topic: "${PROTOCOLS_LVNENG340_FORWARD_MEMORY_TOPIC:protocol_uplink}" + kafka: + topic: "${PROTOCOLS_LVNENG340_FORWARD_KAFKA_TOPIC:protocol_uplink}" + jcpp-partition: "${PROTOCOLS_LVNENG340_FORWARD_KAFKA_JCPP_PARTITION:true}" # 是否利用JCPP的分片框架 + # 以下配置只有在service.type为protocol时且jcpp-partition为false时才生效 + bootstrap-servers: "${PROTOCOLS_LVNENG340_FORWARD_KAFKA_SERVERS:kafka:9092}" + acks: "${PROTOCOLS_LVNENG340_FORWARD_KAFKA_ACKS:1}" + # 可选 protobuf(推荐)、json + encoder: "${PROTOCOLS_LVNENG340_FORWARD_KAFKA_ENCODER:protobuf}" + retries: "${PROTOCOLS_LVNENG340_FORWARD_KAFKA_RETRIES:1}" + compression-type: "${PROTOCOLS_LVNENG340_FORWARD_KAFKA_COMPRESSION_TYPE:none}" # none, gzip, snappy, lz4, zstd + batch-size: "${PROTOCOLS_LVNENG340_FORWARD_KAFKA_BATCH_SIZE:16384}" + linger-ms: "${PROTOCOLS_LVNENG340_FORWARD_KAFKA_LINGER_MS:0}" + buffer-memory: "${PROTOCOLS_LVNENG340_FORWARD_BUFFER_MEMORY:33554432}" + other-properties: "${PROTOCOLS_LVNENG340_FORWARD_QUEUE_KAFKA_OTHER_PROPERTIES:}" # 应用程序服务注册中心配置 zk: diff --git a/jcpp-protocol-lvneng/READMD.md b/jcpp-protocol-lvneng/READMD.md new file mode 100644 index 0000000..54b4bf1 --- /dev/null +++ b/jcpp-protocol-lvneng/READMD.md @@ -0,0 +1,20 @@ +### 绿能直流3.4模拟报文 + +--- + +> 示例统一桩编号:20231212000010 (HEX:20231212000010) +> 示例统一枪编号:01 + +#### 106 上行登录 +`AAF56D0010016A000000000032303233313231323030303031300000000000000000000000000000000000000000010100000000000000000000000200000000000020250808120101FF00000000000000000000000000000000000000000000000001E24000220003E80000B4` +#### 0x02 下行登录应答 +`AAF51200100169000000000001E24000008C` +#### 0x56 下行登录后对时 +`AAF530001001030032303233313231323030303031300000000000000000000000000000000000004F353512090819A6` + +--- + +#### 109 充电桩状态信息包上报 +`AAF5D00010C46D00000000003230323331323132303030303130000000000000000000000000000000000000010101000000000000000000000020250808105527FF00000000000000000000000000000000000000000000000000000000000000F567720000000000F5677200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003232460000000000000000000000000000000000000044` + +--- diff --git a/jcpp-protocol-lvneng/pom.xml b/jcpp-protocol-lvneng/pom.xml new file mode 100644 index 0000000..88b41e1 --- /dev/null +++ b/jcpp-protocol-lvneng/pom.xml @@ -0,0 +1,37 @@ + + + + + sanbing + jcpp-parent + 1.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + jcpp-protocol-lvneng + jar + JChargePointProtocol LvNeng Protocol Module + 绿能全版本协议模块 + + + ${basedir}/.. + + + + + sanbing + jcpp-protocol-api + + + + diff --git a/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/AbstractLvnengCmdExe.java b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/AbstractLvnengCmdExe.java new file mode 100644 index 0000000..a8d8939 --- /dev/null +++ b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/AbstractLvnengCmdExe.java @@ -0,0 +1,85 @@ +/** + * 开源代码,仅供学习和交流研究使用,商用请联系三丙 + * 微信:mohan_88888 + * 抖音:程序员三丙 + * 付费课程知识星球:https://t.zsxq.com/aKtXo + */ +package sanbing.jcpp.protocol.lvneng; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import sanbing.jcpp.infrastructure.util.codec.ByteUtil; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.listener.tcp.enums.SequenceNumberLength; +import sanbing.jcpp.protocol.lvneng.enums.LvnengDownlinkCmdEnum; + +/** + * 绿能协议基础类 + */ +public class AbstractLvnengCmdExe { + + protected static final int LVNENG_HEAD = 0xAAF5; + protected static final int LVNENG_ENCRYPTION_FLAG = 0x10; + + /** + * 编码下行消息 + * 格式:帧头(2) + 长度(2) + 加密标识(1) + 序号(1) + 命令字(2) + 数据域(n) + 校验和(1) + */ + protected byte[] encode(LvnengDownlinkCmdEnum downlinkCmd, + int seqNo, + int encryptionFlag, + ByteBuf msgBody) { + // 1. 计算长度 + int msgBodyLength = msgBody.readableBytes(); + int totalLength = msgBodyLength + 9; // 总长度 = 数据域长度 + 9字节固定头尾 + + // 2. 构建消息头和数据域 + ByteBuf response = Unpooled.buffer(totalLength); + response.writeShort(LVNENG_HEAD); // 帧头 + response.writeShortLE(totalLength); // 长度 + response.writeByte(encryptionFlag); // 加密标识 + response.writeByte(seqNo); // 序号 + response.writeShortLE(downlinkCmd.getCmd()); // 命令字 + response.writeBytes(msgBody); // 数据域 + + // 3. 准备校验和计算的数据(命令字 + 数据域) + byte[] sumData = new byte[2 + msgBodyLength]; // 2字节命令字 + 数据域 + sumData[0] = (byte) (downlinkCmd.getCmd() & 0xFF); + sumData[1] = (byte) ((downlinkCmd.getCmd() >> 8) & 0xFF); + if (msgBodyLength > 0) { + System.arraycopy(response.array(), 8, sumData, 2, msgBodyLength); + } + + // 4. 计算并写入校验和 + response.writeByte(ByteUtil.calculateSum(sumData)); + + // 5. 转换为字节数组 + return ByteUtil.toBytes(response); + } + + /** + * 编码并发送消息(完整参数版本) + */ + protected void encodeAndWriteFlush(LvnengDownlinkCmdEnum downlinkCmd, + int seqNo, + int encryptionFlag, + ByteBuf msgBody, + TcpSession tcpSession) { + byte[] encode = encode(downlinkCmd, seqNo, encryptionFlag, msgBody); + tcpSession.writeAndFlush(Unpooled.copiedBuffer(encode)); + } + + /** + * 编码并发送消息(简化参数版本) + * 使用默认的加密标识和自增序号 + */ + protected void encodeAndWriteFlush(LvnengDownlinkCmdEnum downlinkCmd, + ByteBuf msgBody, + TcpSession tcpSession) { + byte[] encode = encode(downlinkCmd, + tcpSession.nextSeqNo(SequenceNumberLength.SHORT), + LVNENG_ENCRYPTION_FLAG, + msgBody); + tcpSession.writeAndFlush(Unpooled.copiedBuffer(encode)); + } +} \ No newline at end of file diff --git a/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/LvnengDownlinkCmdExe.java b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/LvnengDownlinkCmdExe.java new file mode 100644 index 0000000..a02c3ba --- /dev/null +++ b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/LvnengDownlinkCmdExe.java @@ -0,0 +1,19 @@ +/** + * 开源代码,仅供学习和交流研究使用,商用请联系三丙 + * 微信:mohan_88888 + * 抖音:程序员三丙 + * 付费课程知识星球:https://t.zsxq.com/aKtXo + */ +package sanbing.jcpp.protocol.lvneng; + +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; + +/** + * @author baigod + */ +public abstract class LvnengDownlinkCmdExe extends AbstractLvnengCmdExe { + + public abstract void execute(TcpSession tcpSession, LvnengDwonlinkMessage lvnengDwonlinkMessage, ProtocolContext ctx); + +} \ No newline at end of file diff --git a/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/LvnengDwonlinkMessage.java b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/LvnengDwonlinkMessage.java new file mode 100644 index 0000000..5e3c007 --- /dev/null +++ b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/LvnengDwonlinkMessage.java @@ -0,0 +1,42 @@ +/** + * 开源代码,仅供学习和交流研究使用,商用请联系三丙 + * 微信:mohan_88888 + * 抖音:程序员三丙 + * 付费课程知识星球:https://t.zsxq.com/aKtXo + */ +package sanbing.jcpp.protocol.lvneng; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import sanbing.jcpp.proto.gen.ProtocolProto.DownlinkRequestMessage; + +import java.io.Serializable; +import java.util.UUID; + +/** + * @author baigod + */ +@Data +@Accessors(chain = true) +@NoArgsConstructor +public class LvnengDwonlinkMessage implements Serializable { + public static final byte SUCCESS_BYTE = 0x00; + public static final byte FAILURE_BYTE = 0x01; + + // 消息ID + private UUID id; + + // 请求ID(如有) + private UUID requestId; + + // 指令 + private int cmd; + + // 消息体 + private DownlinkRequestMessage msg; + + // 上行消息 + private LvnengUplinkMessage requestData; + +} \ No newline at end of file diff --git a/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/LvnengProtocolMessageProcessor.java b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/LvnengProtocolMessageProcessor.java new file mode 100644 index 0000000..ae59403 --- /dev/null +++ b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/LvnengProtocolMessageProcessor.java @@ -0,0 +1,191 @@ +/** + * 开源代码,仅供学习和交流研究使用,商用请联系三丙 + * 微信:mohan_88888 + * 抖音:程序员三丙 + * 付费课程知识星球:https://t.zsxq.com/aKtXo + */ +package sanbing.jcpp.protocol.lvneng; + +import cn.hutool.core.util.ClassUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.util.JCPPPair; +import sanbing.jcpp.infrastructure.util.codec.ByteUtil; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.proto.gen.ProtocolProto; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.ProtocolMessageProcessor; +import sanbing.jcpp.protocol.domain.ListenerToHandlerMsg; +import sanbing.jcpp.protocol.domain.SessionToHandlerMsg; +import sanbing.jcpp.protocol.forwarder.Forwarder; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.lvneng.annotation.LvnengCmd; +import sanbing.jcpp.protocol.lvneng.enums.LvnengDownlinkCmdEnum; + +import java.lang.reflect.InvocationTargetException; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +public class LvnengProtocolMessageProcessor extends ProtocolMessageProcessor { + // 协议常量定义 + private static final int HEADER_SIZE = 9; // 帧头(2) + 长度(2) + 加密标识(1) + 序号(1) + 命令字(2) + 校验和(1) + private static final int FRAME_MIN_LENGTH = 9; // 最小帧长度(无数据域的情况) + + private final Map uplinkCmdExeMap = new ConcurrentHashMap<>(); + private final Map downlinkCmdExeMap = new ConcurrentHashMap<>(); + + public LvnengProtocolMessageProcessor(Forwarder forwarder, ProtocolContext protocolContext) { + super(forwarder, protocolContext); + + Set> cmdClasses = ClassUtil.scanPackageByAnnotation(ClassUtil.getPackage(this.getClass()), LvnengCmd.class); + cmdClasses.stream().filter(LvnengUplinkCmdExe.class::isAssignableFrom) + .forEach(clazz -> { + int cmd = clazz.getAnnotation(LvnengCmd.class).value(); + try { + LvnengUplinkCmdExe lvnengUplinkCmdExe = (LvnengUplinkCmdExe) clazz.getDeclaredConstructor().newInstance(); + uplinkCmdExeMap.put(cmd, lvnengUplinkCmdExe); + } catch (InstantiationException | + IllegalAccessException | + InvocationTargetException | + NoSuchMethodException e) { + throw new RuntimeException(e); + } + }); + + cmdClasses.stream().filter(LvnengDownlinkCmdExe.class::isAssignableFrom) + .forEach(clazz -> { + int cmd = clazz.getAnnotation(LvnengCmd.class).value(); + try { + LvnengDownlinkCmdExe lvnengDownlinkCmdExe = (LvnengDownlinkCmdExe) clazz.getDeclaredConstructor().newInstance(); + downlinkCmdExeMap.put(cmd, lvnengDownlinkCmdExe); + } catch (InstantiationException | + IllegalAccessException | + InvocationTargetException | + NoSuchMethodException e) { + throw new RuntimeException(e); + } + }); + } + + @Override + protected void uplinkHandle(ListenerToHandlerMsg listenerToHandlerMsg) { + final UUID msgId = listenerToHandlerMsg.id(); + final byte[] msg = listenerToHandlerMsg.msg(); + final TcpSession session = (TcpSession) listenerToHandlerMsg.session(); + + ByteBuf in = Unpooled.wrappedBuffer(msg); + try { + // 1. 解析帧头信息 + final int startFlag = in.readUnsignedShort(); + final int dataLength = in.readUnsignedShortLE(); + final int encryptFlag = in.readUnsignedByte(); + final int seqNo = in.readUnsignedByte(); + final int frameType = in.readUnsignedShortLE(); + + // 2. 计算并检查消息体长度 + if (dataLength < FRAME_MIN_LENGTH) { + log.warn("{} 绿能协议帧长度异常,期望最小长度:{} 实际长度:{}", + session, FRAME_MIN_LENGTH, dataLength); + return; + } + final int msgBodyLength = dataLength - HEADER_SIZE; + + // 3. 读取消息体数据 + byte[] msgBody = new byte[msgBodyLength]; + in.readBytes(msgBody); + + // 4. 校验和验证 + byte receivedCheckSum = in.readByte(); + + // 准备校验和数据(命令字 + 数据域) + byte[] sumData = new byte[2 + msgBody.length]; // 2字节命令字 + 数据域 + sumData[0] = (byte) (frameType & 0xFF); + sumData[1] = (byte) ((frameType >> 8) & 0xFF); + System.arraycopy(msgBody, 0, sumData, 2, msgBody.length); + + // 验证校验和 + JCPPPair checkResult = ByteUtil.verifySum(sumData, receivedCheckSum); + if (!checkResult.getFirst()) { + log.warn("{} 绿能校验和验证失败 CMD:0x{} 接收校验和:0x{} 期望校验和:0x{}", + session, Integer.toHexString(frameType), + String.format("%02x", receivedCheckSum & 0xFF), + String.format("%02x", checkResult.getSecond() & 0xFF)); + return; + } + + // 5. 构建上行消息对象并执行 + LvnengUplinkMessage uplinkMessage = new LvnengUplinkMessage(msgId) + .setHead(startFlag) + .setDataLength(dataLength) + .setSequenceNumber(seqNo) + .setEncryptionFlag(encryptFlag) + .setCmd(frameType) + .setMsgBody(msgBody) + .setCheckSum(checkResult.getSecond()) + .setRawFrame(msg); + + exeCmd(uplinkMessage, session); + + } catch (Exception e) { + log.error("{} 处理绿能协议上行消息时发生异常", session, e); + } finally { + in.release(); + } + } + + @Override + protected void downlinkHandle(SessionToHandlerMsg sessionToHandlerMsg) { + TcpSession session = (TcpSession) sessionToHandlerMsg.session(); + + ProtocolProto.DownlinkRequestMessage protocolDownlinkMsg = sessionToHandlerMsg.downlinkMsg(); + + int cmd = LvnengDownlinkCmdEnum.valueOf(protocolDownlinkMsg.getDownlinkCmd()).getCmd(); + + LvnengDwonlinkMessage message = new LvnengDwonlinkMessage(); + message.setId(new UUID(protocolDownlinkMsg.getMessageIdMSB(), protocolDownlinkMsg.getMessageIdLSB())); + message.setCmd(cmd); + message.setMsg(protocolDownlinkMsg); + + if (protocolDownlinkMsg.hasRequestIdMSB() && protocolDownlinkMsg.hasRequestIdLSB()) { + message.setRequestId(new UUID(protocolDownlinkMsg.getRequestIdMSB(), protocolDownlinkMsg.getRequestIdLSB())); + } + + if (protocolDownlinkMsg.hasRequestData()) { + message.setRequestData(JacksonUtil.fromBytes(protocolDownlinkMsg.getRequestData().toByteArray(), LvnengUplinkMessage.class)); + } + + exeCmd(message, session); + } + + + private void exeCmd(LvnengUplinkMessage message, TcpSession session) { + LvnengUplinkCmdExe uplinkCmdExe = uplinkCmdExeMap.get(message.getCmd()); + + if (uplinkCmdExe == null) { + + log.info("{} 绿能协议接收到未知的上行指令 0x{}", session, Integer.toHexString(message.getCmd())); + + return; + } + + uplinkCmdExe.execute(session, message, protocolContext); + } + + private void exeCmd(LvnengDwonlinkMessage message, TcpSession session) { + LvnengDownlinkCmdExe downlinkCmdExe = downlinkCmdExeMap.get(message.getCmd()); + + if (downlinkCmdExe == null) { + + log.info("{} 绿能协议接收到未知的下行指令 0x{}", session, Integer.toHexString(message.getCmd())); + + return; + } + + downlinkCmdExe.execute(session, message, protocolContext); + } + +} diff --git a/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/LvnengUplinkCmdExe.java b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/LvnengUplinkCmdExe.java new file mode 100644 index 0000000..53a9203 --- /dev/null +++ b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/LvnengUplinkCmdExe.java @@ -0,0 +1,35 @@ +/** + * 开源代码,仅供学习和交流研究使用,商用请联系三丙 + * 微信:mohan_88888 + * 抖音:程序员三丙 + * 付费课程知识星球:https://t.zsxq.com/aKtXo + */ +package sanbing.jcpp.protocol.lvneng; + +import com.google.protobuf.ByteString; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; + +/** + * @author baigod + */ +@Slf4j +public abstract class LvnengUplinkCmdExe extends AbstractLvnengCmdExe { + + public abstract void execute(TcpSession tcpSession, LvnengUplinkMessage lvnengUplinkMessage, ProtocolContext ctx); + + protected UplinkQueueMessage.Builder uplinkMessageBuilder(String messageKey, TcpSession tcpSession, LvnengUplinkMessage lvnengUplinkMessage) { + return UplinkQueueMessage.newBuilder() + .setMessageIdMSB(lvnengUplinkMessage.getId().getMostSignificantBits()) + .setMessageIdLSB(lvnengUplinkMessage.getId().getLeastSignificantBits()) + .setSessionIdMSB(tcpSession.getId().getMostSignificantBits()) + .setSessionIdLSB(tcpSession.getId().getLeastSignificantBits()) + .setRequestData(ByteString.copyFrom(JacksonUtil.writeValueAsBytes(lvnengUplinkMessage))) + .setMessageKey(messageKey) + .setProtocolName(tcpSession.getProtocolName()); + } + +} \ No newline at end of file diff --git a/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/LvnengUplinkMessage.java b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/LvnengUplinkMessage.java new file mode 100644 index 0000000..d12f6dc --- /dev/null +++ b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/LvnengUplinkMessage.java @@ -0,0 +1,53 @@ +/** + * 开源代码,仅供学习和交流研究使用,商用请联系三丙 + * 微信:mohan_88888 + * 抖音:程序员三丙 + * 付费课程知识星球:https://t.zsxq.com/aKtXo + */ +package sanbing.jcpp.protocol.lvneng; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.UUID; + +@Data +@Accessors(chain = true) +public class LvnengUplinkMessage implements Serializable { + // 消息ID + private final UUID id; + + // 起始域 + private int head; + + // 数据长度 + private int dataLength; + + // 序列号 + private int sequenceNumber; + + // 加密标识 + private int encryptionFlag; + + // 指令 + private int cmd; + + // 消息体 + private byte[] msgBody; + + // 校验和 + private int checkSum; + + // 真实报文 + private byte[] rawFrame; + + public LvnengUplinkMessage(UUID id) { + this.id = id; + } + + public LvnengUplinkMessage() { + this(UUID.randomUUID()); + } + +} diff --git a/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/annotation/LvnengCmd.java b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/annotation/LvnengCmd.java new file mode 100644 index 0000000..b7a9ba0 --- /dev/null +++ b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/annotation/LvnengCmd.java @@ -0,0 +1,21 @@ +/** + * 开源代码,仅供学习和交流研究使用,商用请联系三丙 + * 微信:mohan_88888 + * 抖音:程序员三丙 + * 付费课程知识星球:https://t.zsxq.com/aKtXo + */ +package sanbing.jcpp.protocol.lvneng.annotation; + +import java.lang.annotation.*; + +/** + * @author baigod + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface LvnengCmd { + + int value(); + +} \ No newline at end of file diff --git a/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/enums/LvnengDownlinkCmdEnum.java b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/enums/LvnengDownlinkCmdEnum.java new file mode 100644 index 0000000..925b022 --- /dev/null +++ b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/enums/LvnengDownlinkCmdEnum.java @@ -0,0 +1,26 @@ +/** + * 开源代码,仅供学习和交流研究使用,商用请联系三丙 + * 微信:mohan_88888 + * 抖音:程序员三丙 + * 付费课程知识星球:https://t.zsxq.com/aKtXo + */ +package sanbing.jcpp.protocol.lvneng.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author baigod + */ +@AllArgsConstructor +@Getter +public enum LvnengDownlinkCmdEnum { + + LOGIN_ACK((short) 105), + + SYNC_TIME((short) 3) + ; + + private final short cmd; + +} \ No newline at end of file diff --git a/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/v340/LvnengV340ProtocolBootstrap.java b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/v340/LvnengV340ProtocolBootstrap.java new file mode 100644 index 0000000..cd464f3 --- /dev/null +++ b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/v340/LvnengV340ProtocolBootstrap.java @@ -0,0 +1,41 @@ +/** + * 开源代码,仅供学习和交流研究使用,商用请联系三丙 + * 微信:mohan_88888 + * 抖音:程序员三丙 + * 付费课程知识星球:https://t.zsxq.com/aKtXo + */ +package sanbing.jcpp.protocol.lvneng.v340; + +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.util.annotation.ProtocolComponent; +import sanbing.jcpp.protocol.ProtocolBootstrap; +import sanbing.jcpp.protocol.ProtocolMessageProcessor; +import sanbing.jcpp.protocol.lvneng.LvnengProtocolMessageProcessor; + +import static sanbing.jcpp.protocol.lvneng.v340.LvnengV340ProtocolBootstrap.PROTOCOL_NAME; + +@ProtocolComponent(PROTOCOL_NAME) +@Slf4j +public class LvnengV340ProtocolBootstrap extends ProtocolBootstrap { + + public static final String PROTOCOL_NAME = "lvnengV340"; + @Override + protected String getProtocolName() { + return PROTOCOL_NAME; + } + + @Override + protected void _init() { + // do nothing + } + + @Override + protected void _destroy() { + // do nothing + } + + @Override + protected ProtocolMessageProcessor messageProcessor() { + return new LvnengProtocolMessageProcessor(forwarder, protocolContext); + } +} diff --git a/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/v340/cmd/LvnengV340LoginAckDLCmd.java b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/v340/cmd/LvnengV340LoginAckDLCmd.java new file mode 100644 index 0000000..399882b --- /dev/null +++ b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/v340/cmd/LvnengV340LoginAckDLCmd.java @@ -0,0 +1,135 @@ +/** + * 开源代码,仅供学习和交流研究使用,商用请联系三丙 + * 微信:mohan_88888 + * 抖音:程序员三丙 + * 付费课程知识星球:https://t.zsxq.com/aKtXo + */ +package sanbing.jcpp.protocol.lvneng.v340.cmd; + +import cn.hutool.core.util.RandomUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.util.codec.BCDUtil; +import sanbing.jcpp.infrastructure.util.codec.CP56Time2aUtil; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.infrastructure.util.mdc.MDCUtils; +import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.LoginResponse; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.listener.tcp.enums.SequenceNumberLength; +import sanbing.jcpp.protocol.lvneng.LvnengDownlinkCmdExe; +import sanbing.jcpp.protocol.lvneng.LvnengDwonlinkMessage; +import sanbing.jcpp.protocol.lvneng.LvnengUplinkMessage; +import sanbing.jcpp.protocol.lvneng.annotation.LvnengCmd; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import static sanbing.jcpp.infrastructure.util.config.ThreadPoolConfiguration.PROTOCOL_SESSION_SCHEDULED; +import static sanbing.jcpp.protocol.domain.SessionCloseReason.MANUALLY; +import static sanbing.jcpp.protocol.listener.tcp.TcpSession.SCHEDULE_KEY_AUTO_SYNC_TIME; +import static sanbing.jcpp.protocol.lvneng.enums.LvnengDownlinkCmdEnum.LOGIN_ACK; +import static sanbing.jcpp.protocol.lvneng.enums.LvnengDownlinkCmdEnum.SYNC_TIME; + +/** + * 绿能3.4 服务器应答充电桩签到命令 + */ +@Slf4j +@LvnengCmd(105) +public class LvnengV340LoginAckDLCmd extends LvnengDownlinkCmdExe { + @Override + public void execute(TcpSession tcpSession, LvnengDwonlinkMessage lvnengDwonlinkMessage, ProtocolContext ctx) { + log.debug("{} 绿能3.4登录认证应答", tcpSession); + + if (!lvnengDwonlinkMessage.getMsg().hasLoginResponse()) { + return; + } + + LoginResponse loginResponse = lvnengDwonlinkMessage.getMsg().getLoginResponse(); + + LvnengUplinkMessage requestData = JacksonUtil.fromBytes(lvnengDwonlinkMessage.getMsg().getRequestData().toByteArray(), LvnengUplinkMessage.class); + + // 获取上行报文 + byte[] uplinkRawFrame = requestData.getRawFrame(); + // 从上行报文中取出桩编号字节数组 + byte[] pileCodeBytes = Arrays.copyOfRange(uplinkRawFrame, 12, 44); + byte[] randomNumBytes = Arrays.copyOfRange(uplinkRawFrame, 98, 102); + + if (loginResponse.getSuccess()) { + + // 构造并下发登录ACK + loginAck(tcpSession, requestData, randomNumBytes); + + // 构造定时对时 + registerSyncTimeTask(tcpSession, pileCodeBytes, requestData); + + } else { + + log.info("绿能3.4登录认证失败,服务端断开连接。 pileCode:{}", loginResponse.getPileCode()); + + // 构造并下发登录ACK + loginAck(tcpSession, requestData, new byte[]{0x00, 0x00, 0x00, 0x00}); + + // 断开连接 + tcpSession.close(MANUALLY); + } + } + + + private void loginAck(TcpSession tcpSession, LvnengUplinkMessage requestData, byte[] randomNumBytes) { + // 创建ACK消息体7字节桩编号+1字节登录结果 + ByteBuf loginAckMsgBody = Unpooled.buffer(18); + loginAckMsgBody.writeShortLE(0x00); + loginAckMsgBody.writeShortLE(0x00); + loginAckMsgBody.writeBytes(randomNumBytes); + loginAckMsgBody.writeByte(0x00); + + encodeAndWriteFlush(LOGIN_ACK, + requestData.getSequenceNumber(), + requestData.getEncryptionFlag(), + loginAckMsgBody, + tcpSession); + } + + private void registerSyncTimeTask(TcpSession tcpSession, byte[] pileCodeBytes, LvnengUplinkMessage requestData) { + tcpSession.addSchedule(SCHEDULE_KEY_AUTO_SYNC_TIME, k -> { + log.info("{} 云快充3.4开始注册定时对时任务", tcpSession); + return PROTOCOL_SESSION_SCHEDULED.scheduleAtFixedRate(() -> + syncTime(tcpSession, pileCodeBytes, requestData), + 0, RandomUtil.randomInt(420, 480), TimeUnit.MINUTES); + } + ); + } + + private void syncTime(TcpSession tcpSession, byte[] pileCodeBytes, LvnengUplinkMessage requestData) { + TracerContextUtil.newTracer(); + MDCUtils.recordTracer(); + log.info("{} 绿能3.4开始下发对时报文", tcpSession); + ByteBuf syncTimeMsgBody = Unpooled.buffer(14); + syncTimeMsgBody.writeBytes(pileCodeBytes); + syncTimeMsgBody.writeBytes(CP56Time2aUtil.encode(LocalDateTime.now())); + + ByteBuf msgBodyBuf = Unpooled.buffer(); + // 预留1 + msgBodyBuf.writeShortLE(0); + // 预留1 + msgBodyBuf.writeShortLE(0); + msgBodyBuf.writeByte(1); + // 4 参数起始地址,子命令 + msgBodyBuf.writeIntLE(2); + //6 参数字节长度 + msgBodyBuf.writeShortLE(8); + //7 命令参数数据 + msgBodyBuf.writeBytes(BCDUtil.dateToBcd8(LocalDateTime.now())); + + encodeAndWriteFlush(SYNC_TIME, + tcpSession.nextSeqNo(SequenceNumberLength.SHORT), + requestData.getEncryptionFlag(), + syncTimeMsgBody, + tcpSession); + } + +} diff --git a/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/v340/cmd/LvnengV340LoginULCmd.java b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/v340/cmd/LvnengV340LoginULCmd.java new file mode 100644 index 0000000..833a5c6 --- /dev/null +++ b/jcpp-protocol-lvneng/src/main/java/sanbing/jcpp/protocol/lvneng/v340/cmd/LvnengV340LoginULCmd.java @@ -0,0 +1,118 @@ +/** + * 开源代码,仅供学习和交流研究使用,商用请联系三丙 + * 微信:mohan_88888 + * 抖音:程序员三丙 + * 付费课程知识星球:https://t.zsxq.com/aKtXo + */ +package sanbing.jcpp.protocol.lvneng.v340.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.apache.commons.lang3.StringUtils; +import sanbing.jcpp.infrastructure.util.codec.BCDUtil; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.LoginRequest; +import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.lvneng.LvnengUplinkCmdExe; +import sanbing.jcpp.protocol.lvneng.LvnengUplinkMessage; +import sanbing.jcpp.protocol.lvneng.annotation.LvnengCmd; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.ZoneId; + +/** + * 绿能3.4 充电桩签到信息上报 + */ +@Slf4j +@LvnengCmd(106) +public class LvnengV340LoginULCmd extends LvnengUplinkCmdExe { + @Override + public void execute(TcpSession tcpSession, LvnengUplinkMessage lvnengUplinkMessage, ProtocolContext ctx) { + log.debug("{} 绿能3.4登录认证请求", tcpSession); + ByteBuf byteBuf = Unpooled.wrappedBuffer(lvnengUplinkMessage.getMsgBody()); + + ObjectNode additionalInfo = JacksonUtil.newObjectNode(); + + // 预留 + byteBuf.readShortLE(); + // 预留 + byteBuf.readShortLE(); + + // 充电桩编码 + byte[] pileCodeBytes = new byte[32]; + byteBuf.readBytes(pileCodeBytes); + String pileCode = StringUtils.trim(new String(pileCodeBytes, StandardCharsets.US_ASCII)); + + //预留 + int flag = byteBuf.readByte(); + additionalInfo.put("标识", flag); + + // 充电桩软件版本 (4字节) + // 格式: 0x00 0x01 0x0100 表示 V0.1.256 + int major = byteBuf.readUnsignedByte(); // 主版本号 + int minor = byteBuf.readUnsignedByte(); // 次版本号 + int patch = byteBuf.readUnsignedShort(); // 修订版本号 + String version = String.format("V%d.%d.%d", major, minor, patch); + additionalInfo.put("版本", version); + + // 预留 + byteBuf.skipBytes(10); + + // 充电枪个数 + int gunsNum = byteBuf.readByte(); + additionalInfo.put("充电枪个数", gunsNum); + + // 预留 + byteBuf.skipBytes(6); + + // 当前充电桩时间 + byte[] pileSystemTimeBytes = new byte[8]; + byteBuf.readBytes(pileSystemTimeBytes); + LocalDateTime pileSystemTime = BCDUtil.bcdToDate(pileSystemTimeBytes); + additionalInfo.put("当前充电桩时间", pileSystemTime == null ? null : pileSystemTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); + // 预留 + byteBuf.readLong(); + byteBuf.readLong(); + byteBuf.readLong(); + + long randomNum = byteBuf.readUnsignedIntLE(); + additionalInfo.put("桩生成的随机数", randomNum); + + // 充电桩与服务器通信协议版本 (2字节) + // 十进制30表示V3.0 + int softVersion = byteBuf.readUnsignedShortLE(); + int softMajor = softVersion / 10; // 主版本号 + int softMinor = softVersion % 10; // 次版本号 + additionalInfo.put("桩后台通信协议版本", String.format("V%d.%d", softMajor, softMinor)); + + int whiteVersion = byteBuf.readIntLE(); + additionalInfo.put("白名单版本号", whiteVersion); + + tcpSession.addPileCode(pileCode); + + // 注册前置会话 + ctx.getProtocolSessionRegistryProvider().register(tcpSession); + + // 转发到后端 + LoginRequest loginRequest = LoginRequest.newBuilder() + .setPileCode(pileCode) + .setCredential(pileCode) + .setRemoteAddress(tcpSession.getAddress().toString()) + .setNodeId(ctx.getServiceInfoProvider().getServiceId()) + .setNodeHostAddress(ctx.getServiceInfoProvider().getHostAddress()) + .setNodeRestPort(ctx.getServiceInfoProvider().getRestPort()) + .setNodeGrpcPort(ctx.getServiceInfoProvider().getGrpcPort()) + .setAdditionalInfo(additionalInfo.toString()) + .build(); + + UplinkQueueMessage uplinkQueueMessage = uplinkMessageBuilder(loginRequest.getPileCode(), tcpSession, lvnengUplinkMessage) + .setLoginRequest(loginRequest) + .build(); + tcpSession.getForwarder().sendMessage(uplinkQueueMessage); + } +} diff --git a/jcpp-protocol-yunkuaichong/READMD.md b/jcpp-protocol-yunkuaichong/READMD.md index d050784..583b54b 100644 --- a/jcpp-protocol-yunkuaichong/READMD.md +++ b/jcpp-protocol-yunkuaichong/READMD.md @@ -10,7 +10,7 @@ #### 0x02 下行登录应答 `68 0C 00 19 00 02 20 23 12 12 00 00 10 00 A1 55` #### 0x56 下行登录后对时 -`68 12 01 00 00 56 20 23 12 12 00 00 10 30 75 0F 11 12 0C 18 04 7D ` +`68 12 01 00 00 56 20 23 12 12 00 00 10 30 75 0F 11 12 0C 18 04 7D` --- diff --git a/jcpp-protocol-yunkuaichong/pom.xml b/jcpp-protocol-yunkuaichong/pom.xml index 4caba18..de542b0 100644 --- a/jcpp-protocol-yunkuaichong/pom.xml +++ b/jcpp-protocol-yunkuaichong/pom.xml @@ -21,7 +21,7 @@ jcpp-protocol-yunkuaichong jar JChargePointProtocol Yunkuaichong Protocol Module - 云快充1.5 + 云快充全版本协议模块 ${basedir}/.. diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/AbstractYunKuaiChongCmdExe.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/AbstractYunKuaiChongCmdExe.java index 8091242..4a3497b 100644 --- a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/AbstractYunKuaiChongCmdExe.java +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/AbstractYunKuaiChongCmdExe.java @@ -71,7 +71,7 @@ public class AbstractYunKuaiChongCmdExe { protected static byte[] encodePileCode(String pileCode) { if (StringUtils.length(pileCode) > 32) { - throw new IllegalArgumentException("云快充1.5可接受最大桩编号为14位"); + throw new IllegalArgumentException("云快充可接受最大桩编号为14位"); } String pileCodeStr = StringUtils.leftPad(pileCode, 14, '0'); @@ -81,7 +81,7 @@ public class AbstractYunKuaiChongCmdExe { protected static byte[] encodeGunCode(String gunCode) { if (StringUtils.length(gunCode) > 2) { - throw new IllegalArgumentException("云快充1.5可接受最大枪编号为2位"); + throw new IllegalArgumentException("云快充可接受最大枪编号为2位"); } String gunCodeStr = StringUtils.leftPad(gunCode, 2, '0'); diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/YunKuaiChongProtocolMessageProcessor.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/YunKuaiChongProtocolMessageProcessor.java index 7808c29..87bb97b 100644 --- a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/YunKuaiChongProtocolMessageProcessor.java +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/YunKuaiChongProtocolMessageProcessor.java @@ -8,6 +8,7 @@ package sanbing.jcpp.protocol.yunkuaichong; import cn.hutool.core.util.ClassUtil; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import lombok.extern.slf4j.Slf4j; import sanbing.jcpp.infrastructure.util.JCPPPair; @@ -100,6 +101,8 @@ public class YunKuaiChongProtocolMessageProcessor extends ProtocolMessageProcess // ================== 校验和双模式处理 ================== final int checkSumLE = in.getUnsignedShortLE(checksumPos); final int checkSumBE = in.getUnsignedShort(checksumPos); + byte[] checkSumBytes = new byte[2]; + in.getBytes(checksumPos, checkSumBytes); // ================== 校验数据智能拷贝 ================== final byte[] checkData = Arrays.copyOfRange(msg, 2, 2 + dataLength); @@ -108,16 +111,16 @@ public class YunKuaiChongProtocolMessageProcessor extends ProtocolMessageProcess JCPPPair checkResult = checkCrcSum(checkData, checkSumLE); if (!checkResult.getFirst()) { if (log.isDebugEnabled()) { // 日志惰性计算 - log.debug("{} 云快充校验域一次校验失败 CMD:{} 校验和:0x{} 期望校验和:0x{}", - session, frameType, Integer.toHexString(checkSumLE), Integer.toHexString(checkResult.getSecond())); + log.debug("{} 云快充校验域一次校验失败 CMD:{} 校验和:0x{} 期望校验和:{}", + session, Integer.toHexString(frameType), ByteBufUtil.hexDump(checkSumBytes), checkResult.getSecond()); } checkResult = checkCrcSum(checkData, checkSumBE); } // ================== 最终校验失败处理 ================== if (!checkResult.getFirst()) { - log.info("{} 云快充校验域二次校验失败 CMD:{} 校验和:0x{} 期望校验和:0x{}", - session, frameType, Integer.toHexString(checkSumBE), Integer.toHexString(checkResult.getSecond())); + log.info("{} 云快充校验域二次校验失败 CMD:{} 校验和:0x{} 期望校验和:{}", + session, Integer.toHexString(frameType), ByteBufUtil.hexDump(checkSumBytes), checkResult.getSecond()); return; } diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150LoginAckDLCmd.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150LoginAckDLCmd.java index deadec3..9aea855 100644 --- a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150LoginAckDLCmd.java +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150LoginAckDLCmd.java @@ -29,6 +29,7 @@ import java.util.concurrent.TimeUnit; import static sanbing.jcpp.infrastructure.util.config.ThreadPoolConfiguration.PROTOCOL_SESSION_SCHEDULED; import static sanbing.jcpp.protocol.domain.SessionCloseReason.MANUALLY; +import static sanbing.jcpp.protocol.listener.tcp.TcpSession.SCHEDULE_KEY_AUTO_SYNC_TIME; import static sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDwonlinkMessage.FAILURE_BYTE; import static sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDwonlinkMessage.SUCCESS_BYTE; import static sanbing.jcpp.protocol.yunkuaichong.enums.YunKuaiChongDownlinkCmdEnum.LOGIN_ACK; @@ -94,7 +95,7 @@ public class YunKuaiChongV150LoginAckDLCmd extends YunKuaiChongDownlinkCmdExe { } private void registerSyncTimeTask(TcpSession tcpSession, byte[] pileCodeBytes, YunKuaiChongUplinkMessage requestData) { - tcpSession.addSchedule("auto-sync-time", k -> { + tcpSession.addSchedule(SCHEDULE_KEY_AUTO_SYNC_TIME, k -> { log.info("{} 云快充1.5.0开始注册定时对时任务", tcpSession); return PROTOCOL_SESSION_SCHEDULED.scheduleAtFixedRate(() -> syncTime(tcpSession, pileCodeBytes, requestData), diff --git a/jcpp-testing/pom.xml b/jcpp-testing/pom.xml index 8235cdd..685ff04 100644 --- a/jcpp-testing/pom.xml +++ b/jcpp-testing/pom.xml @@ -60,6 +60,10 @@ sanbing jcpp-protocol-yunkuaichong + + sanbing + jcpp-protocol-lvneng + diff --git a/pom.xml b/pom.xml index ed4fb46..1bbc3ea 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ org.springframework.boot spring-boot-starter-parent - 3.3.5 + 3.5.4 sanbing @@ -49,11 +49,12 @@ 3.13.0 5.8.32 3.5.7 - 5.7.0 + 5.9.0 6.6.2 - 3.9.2 + 3.9.3 3.8.16.Final 1.3.2 + 3.18.0 @@ -106,6 +107,7 @@ jcpp-protocol-api jcpp-testing jcpp-protocol-yunkuaichong + jcpp-protocol-lvneng @@ -148,6 +150,11 @@ jcpp-protocol-yunkuaichong ${project.version} + + sanbing + jcpp-protocol-lvneng + ${project.version} + sanbing jcpp-infrastructure-cache