diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcVendorTlvBlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcVendorTlvBlockParser.java new file mode 100644 index 00000000..2ca9628e --- /dev/null +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcVendorTlvBlockParser.java @@ -0,0 +1,56 @@ +package com.lingniu.ingest.protocol.gb32960.codec.parser.vendor.guangdong; + +import com.lingniu.ingest.protocol.gb32960.codec.InfoBlockParser; +import com.lingniu.ingest.protocol.gb32960.model.InfoBlock; +import com.lingniu.ingest.protocol.gb32960.model.ProtocolVersion; + +import java.nio.ByteBuffer; + +/** + * 广东燃料电池 peer 的厂商私有 TLV 块兜底 parser。 + * + *
同一 peer 在广东规范 v1.0 表 8 列出的 0x30~0x34 / 0x80 之外,**还会下发**一些 + * 在该规范里完全未定义的扩展 typeCode(线上首先观察到 {@code 0x83},未来不排除有 + * 0x84 等)。这些块经验上是 **TLV 格式**: + * + *
+ * typeCode (1B, 由 BodyParser 外层消费) + * length (2B WORD, 大端) + * payload (length 字节) + *+ * + *
本 parser 严格按 length 消费字节,不贪心读到 buffer 末尾,保证 BodyParser + * 主循环在该块之后仍能继续解析后面的标准/扩展块。 + * + *
typeCode 通过构造器注入,未来若发现新的 vendor typeCode,只需在 catalog 里多注册 + * 一个 {@code new GdFcVendorTlvBlockParser(0x84)} 即可,无需新写 parser 类。 + * + *
length 字段宽度为 2 字节是从 {@code 0x83} 实际字节序列推断而来(0x0024=36 与
+ * payload 实际长度匹配);如未来对端的某个 typeCode 用 1B length 而非 2B,将出现
+ * 字节漂移并被 BodyParser 的 hex dump 立刻暴露,可针对性新增独立 parser。
+ */
+public final class GdFcVendorTlvBlockParser implements InfoBlockParser {
+
+ private final int typeCode;
+
+ public GdFcVendorTlvBlockParser(int typeCode) {
+ if (typeCode < 0 || typeCode > 0xFF) {
+ throw new IllegalArgumentException("typeCode out of range: " + typeCode);
+ }
+ this.typeCode = typeCode;
+ }
+
+ @Override public ProtocolVersion version() { return ProtocolVersion.V2016; }
+ @Override public int typeCode() { return typeCode; }
+ @Override public int fixedLength() { return -1; }
+
+ @Override
+ public InfoBlock parse(ByteBuffer buf) {
+ int len = buf.getShort() & 0xFFFF;
+ // 防御:若 length 大于剩余可读字节,截短到剩余长度(避免 BufferUnderflowException)
+ int safe = Math.min(len, buf.remaining());
+ byte[] payload = new byte[safe];
+ buf.get(payload);
+ return new InfoBlock.GuangdongFc.VendorTlv(typeCode, len, payload);
+ }
+}
diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/config/Gb32960AutoConfiguration.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/config/Gb32960AutoConfiguration.java
index c3068854..055c7d59 100644
--- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/config/Gb32960AutoConfiguration.java
+++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/config/Gb32960AutoConfiguration.java
@@ -34,6 +34,7 @@ import com.lingniu.ingest.protocol.gb32960.codec.parser.vendor.guangdong.GdFcDcD
import com.lingniu.ingest.protocol.gb32960.codec.parser.vendor.guangdong.GdFcDemoExtensionBlockParser;
import com.lingniu.ingest.protocol.gb32960.codec.parser.vendor.guangdong.GdFcStackBlockParser;
import com.lingniu.ingest.protocol.gb32960.codec.parser.vendor.guangdong.GdFcVehicleInfoBlockParser;
+import com.lingniu.ingest.protocol.gb32960.codec.parser.vendor.guangdong.GdFcVendorTlvBlockParser;
import com.lingniu.ingest.protocol.gb32960.codec.profile.Gb32960ProfileRegistry;
import com.lingniu.ingest.protocol.gb32960.codec.profile.RuleBasedVendorExtensionSelector;
import com.lingniu.ingest.protocol.gb32960.codec.profile.VendorExtensionCatalog;
@@ -108,7 +109,9 @@ public class Gb32960AutoConfiguration {
new GdFcDcDcBlockParser(),
new GdFcAirConditionerBlockParser(),
new GdFcVehicleInfoBlockParser(),
- new GdFcDemoExtensionBlockParser())));
+ new GdFcDemoExtensionBlockParser(),
+ // 0x83 厂商私有 TLV 兜底(同 peer 在广东规范之外又下发的未公开扩展)
+ new GdFcVendorTlvBlockParser(0x83))));
}
// ================= Core components =================
diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/model/InfoBlock.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/model/InfoBlock.java
index 138e02bc..cc241d1a 100644
--- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/model/InfoBlock.java
+++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/model/InfoBlock.java
@@ -339,14 +339,15 @@ public sealed interface InfoBlock
// 广东燃料电池汽车示范应用城市群综合监管平台规范 v1.0(基于 GB/T 32960.3-2016 帧头)
// ========================================================================
- /** 广东燃料电池汽车示范应用规范定义的扩展信息体(typeCode 0x30~0x34、0x80)。 */
+ /** 广东燃料电池汽车示范应用规范定义的扩展信息体(typeCode 0x30~0x34、0x80)+ 同 peer 的厂商私有 TLV。 */
sealed interface GuangdongFc extends InfoBlock
permits GuangdongFc.Stack,
GuangdongFc.Auxiliary,
GuangdongFc.DcDc,
GuangdongFc.AirConditioner,
GuangdongFc.VehicleInfo,
- GuangdongFc.DemoExtension {
+ GuangdongFc.DemoExtension,
+ GuangdongFc.VendorTlv {
/** 0x30 燃料电池电堆数据(§7.2.3.4 表 13/14)。变长。 */
record Stack(int stackCount, List 关键在于对应的 {@code GdFcVendorTlvBlockParser} 严格按 length 字段消费字节,
+ * 因此 BodyParser 主循环在该块之后**仍能继续解析其它信息体**,不会被吞光。
+ *
+ * @param typeCode 触发的 typeCode(保留以便诊断/分类)
+ * @param declaredLength TLV 的 length 字段值(payload 字节数)
+ * @param payload 原始 payload 字节,长度 = declaredLength
+ */
+ record VendorTlv(int typeCode, int declaredLength, byte[] payload) implements GuangdongFc {
+ @Override public InfoBlockType type() { return InfoBlockType.GD_FC_VENDOR_TLV; }
+ }
}
// ========================================================================
diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/model/InfoBlockType.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/model/InfoBlockType.java
index 0f7bc1dc..1fef0255 100644
--- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/model/InfoBlockType.java
+++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/model/InfoBlockType.java
@@ -56,6 +56,8 @@ public enum InfoBlockType {
GD_FC_VEHICLE_INFO,
/** 广东燃料电池规范 0x80 燃料电池汽车示范应用数据扩展(表 27)。 */
GD_FC_DEMO_EXTENSION,
+ /** 同 peer 的厂商私有 TLV 兜底(如 0x83),等待对端文档后升级为正式 parser。 */
+ GD_FC_VENDOR_TLV,
/** 原始/未解析。 */
RAW
}
diff --git a/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcVendorTlvBlockParserTest.java b/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcVendorTlvBlockParserTest.java
new file mode 100644
index 00000000..e8d93891
--- /dev/null
+++ b/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcVendorTlvBlockParserTest.java
@@ -0,0 +1,118 @@
+package com.lingniu.ingest.protocol.gb32960.codec.parser.vendor.guangdong;
+
+import com.lingniu.ingest.protocol.gb32960.codec.Gb32960BodyParser;
+import com.lingniu.ingest.protocol.gb32960.codec.Gb32960MessageDecoder;
+import com.lingniu.ingest.protocol.gb32960.codec.InfoBlockParser;
+import com.lingniu.ingest.protocol.gb32960.codec.InfoBlockParserRegistry;
+import com.lingniu.ingest.protocol.gb32960.codec.parser.v2016.VehicleV2016BlockParser;
+import com.lingniu.ingest.protocol.gb32960.model.InfoBlock;
+import com.lingniu.ingest.protocol.gb32960.model.ProtocolVersion;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * 验证 {@link GdFcVendorTlvBlockParser} 严格按 length 消费字节,不会贪心吞光后续信息体。
+ *
+ * 这是关键回归 case:用户问"加 0x83 兜底会不会把后面其它块也吃掉?"——通过构造
+ * 一个 {@code 0x83 TLV + 0x01 Vehicle} 串联的 body,断言 BodyParser 解出**两个块**而非一个。
+ */
+class GdFcVendorTlvBlockParserTest {
+
+ @Test
+ void parsesPayloadOfDeclaredLengthExactly() {
+ // 0x83 typeCode 已被外层消费,buffer 从 length 字段开始
+ byte[] data = bytes(
+ 0x00, 0x05, // length = 5
+ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE); // 5 bytes payload
+ ByteBuffer buf = ByteBuffer.wrap(data);
+
+ InfoBlock.GuangdongFc.VendorTlv tlv =
+ (InfoBlock.GuangdongFc.VendorTlv) new GdFcVendorTlvBlockParser(0x83).parse(buf);
+
+ assertThat(tlv.typeCode()).isEqualTo(0x83);
+ assertThat(tlv.declaredLength()).isEqualTo(5);
+ assertThat(tlv.payload()).containsExactly(0xAA, 0xBB, 0xCC, 0xDD, 0xEE);
+ assertThat(buf.remaining()).as("应正好消费 2+5=7 字节").isZero();
+ }
+
+ @Test
+ void doesNotSwallowSubsequentBlocksWhenChainedInBodyParser() {
+ // 构造 body:0x83 TLV(length=4) + 0x01 Vehicle(20B 标准布局)
+ ByteArrayOutputStream body = new ByteArrayOutputStream();
+ // 0x83 TLV:3+4 = 7 字节
+ body.write(0x83);
+ body.write(0x00); body.write(0x04); // length = 4
+ body.write(0x11); body.write(0x22); body.write(0x33); body.write(0x44);
+ // 0x01 Vehicle: typeCode + 20 字节 body
+ body.write(0x01);
+ body.write(0x01); // vehicleState
+ body.write(0x03); // chargingState
+ body.write(0x02); // runningMode
+ write16(body, 0); // speed
+ write32(body, 100_000L); // mileage
+ write16(body, 5605); // totalVoltage
+ write16(body, 10000); // totalCurrent
+ body.write(85); // SOC
+ body.write(0x01); // dcDcStatus
+ body.write(0x00); // gear
+ write16(body, 10000); // insulation
+ body.write(0x00); // accelPedal
+ body.write(0x00); // brakePedal
+
+ // 装配 body parser:注册 0x83 vendor TLV + 0x01 标准 Vehicle
+ InfoBlockParserRegistry registry = new InfoBlockParserRegistry(List.of(
+ new GdFcVendorTlvBlockParser(0x83),
+ new VehicleV2016BlockParser()));
+ Gb32960BodyParser parser = new Gb32960BodyParser(registry);
+
+ ByteBuffer buf = ByteBuffer.wrap(body.toByteArray());
+ Gb32960MessageDecoder.BodyParseResult result = parser.parse(ProtocolVersion.V2016, buf);
+
+ // 关键断言:两个块都解析出来了,0x01 没被 0x83 吞掉
+ assertThat(result.blocks()).hasSize(2);
+ assertThat(result.blocks().get(0)).isInstanceOf(InfoBlock.GuangdongFc.VendorTlv.class);
+ assertThat(result.blocks().get(1)).isInstanceOf(InfoBlock.Gb32960V2016.Vehicle.class);
+
+ InfoBlock.GuangdongFc.VendorTlv tlv = (InfoBlock.GuangdongFc.VendorTlv) result.blocks().get(0);
+ assertThat(tlv.declaredLength()).isEqualTo(4);
+ assertThat(tlv.payload()).containsExactly(0x11, 0x22, 0x33, 0x44);
+
+ InfoBlock.Gb32960V2016.Vehicle v = (InfoBlock.Gb32960V2016.Vehicle) result.blocks().get(1);
+ assertThat(v.socPercent()).isEqualTo(85);
+ assertThat(v.totalVoltageV()).isEqualTo(560.5);
+ }
+
+ @Test
+ void truncatesPayloadIfDeclaredLengthExceedsRemaining() {
+ // 防御场景:length 字段声称 100 字节但 buffer 只剩 3 字节
+ byte[] data = bytes(0x00, 0x64, 0x01, 0x02, 0x03);
+ ByteBuffer buf = ByteBuffer.wrap(data);
+ InfoBlock.GuangdongFc.VendorTlv tlv =
+ (InfoBlock.GuangdongFc.VendorTlv) new GdFcVendorTlvBlockParser(0x83).parse(buf);
+ assertThat(tlv.declaredLength()).isEqualTo(100);
+ assertThat(tlv.payload()).hasSize(3).containsExactly(0x01, 0x02, 0x03);
+ }
+
+ private static byte[] bytes(int... values) {
+ byte[] out = new byte[values.length];
+ for (int i = 0; i < values.length; i++) out[i] = (byte) values[i];
+ return out;
+ }
+
+ private static void write16(ByteArrayOutputStream os, int v) {
+ os.write((v >> 8) & 0xFF);
+ os.write(v & 0xFF);
+ }
+
+ private static void write32(ByteArrayOutputStream os, long v) {
+ os.write((int) ((v >> 24) & 0xFF));
+ os.write((int) ((v >> 16) & 0xFF));
+ os.write((int) ((v >> 8) & 0xFF));
+ os.write((int) (v & 0xFF));
+ }
+}