diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcAirConditionerBlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcAirConditionerBlockParser.java
new file mode 100644
index 00000000..68a1c18b
--- /dev/null
+++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcAirConditionerBlockParser.java
@@ -0,0 +1,34 @@
+package com.lingniu.ingest.protocol.gb32960.codec.parser.vendor.guangdong;
+
+import com.lingniu.ingest.protocol.gb32960.codec.InfoBlockParser;
+import com.lingniu.ingest.protocol.gb32960.codec.ValueDecoder;
+import com.lingniu.ingest.protocol.gb32960.model.InfoBlock;
+import com.lingniu.ingest.protocol.gb32960.model.ProtocolVersion;
+
+import java.nio.ByteBuffer;
+
+/**
+ * 广东燃料电池规范 §7.2.3.7 表 18:空调数据。typeCode {@code 0x33}。
+ *
+ *
固定 5 字节:
+ *
+ * status(1) 0 关闭 / 1 启动
+ * powerKw(2, offset -5, 1 kw, range -5~+30)
+ * compressorInputVoltageV(2, 0.1V, 0~1000V)
+ *
+ */
+public final class GdFcAirConditionerBlockParser implements InfoBlockParser {
+
+ @Override public ProtocolVersion version() { return ProtocolVersion.V2016; }
+ @Override public int typeCode() { return 0x33; }
+ @Override public int fixedLength() { return 5; }
+
+ @Override
+ public InfoBlock parse(ByteBuffer buf) {
+ Integer status = ValueDecoder.u8raw(buf);
+ int rawPower = buf.getShort() & 0xFFFF;
+ Integer power = (rawPower == 0xFFFE || rawPower == 0xFFFF) ? null : rawPower - 5;
+ Double inputV = ValueDecoder.scaledU16(buf, 0.1);
+ return new InfoBlock.GdFcAirConditioner(status, power, inputV);
+ }
+}
diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcAuxiliaryBlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcAuxiliaryBlockParser.java
new file mode 100644
index 00000000..09dd0fd8
--- /dev/null
+++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcAuxiliaryBlockParser.java
@@ -0,0 +1,61 @@
+package com.lingniu.ingest.protocol.gb32960.codec.parser.vendor.guangdong;
+
+import com.lingniu.ingest.protocol.gb32960.codec.InfoBlockParser;
+import com.lingniu.ingest.protocol.gb32960.codec.ValueDecoder;
+import com.lingniu.ingest.protocol.gb32960.model.InfoBlock;
+import com.lingniu.ingest.protocol.gb32960.model.ProtocolVersion;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 广东燃料电池规范 §7.2.3.5 表 15/16:辅助系统数据。typeCode {@code 0x31}。变长。
+ *
+ *
+ * subSystemCount(1)
+ * For each:
+ * airCompressorMotorVoltageV(2, 0.1V, 0~1000V)
+ * airCompressorPowerKw(2, offset -5, 1 kw, range -5~+30)
+ * hydrogenPumpMotorVoltageV(2, 0.2V, 0~50V)
+ * hydrogenPumpPowerKw(2, offset -5, 1 kw)
+ * waterPumpVoltageV(2, 0.1V)
+ * ptcVoltageV(2, 0.1V)
+ * ptcPowerKw(2, offset -5, 1 kw)
+ * lowVoltageBatteryVoltageV(2, 0.1V, 0~32V)
+ *
+ *
+ * 每子系统 16 字节。
+ */
+public final class GdFcAuxiliaryBlockParser implements InfoBlockParser {
+
+ @Override public ProtocolVersion version() { return ProtocolVersion.V2016; }
+ @Override public int typeCode() { return 0x31; }
+ @Override public int fixedLength() { return -1; }
+
+ @Override
+ public InfoBlock parse(ByteBuffer buf) {
+ int count = buf.get() & 0xFF;
+ List subs = new ArrayList<>(count);
+ for (int i = 0; i < count; i++) {
+ Double airCompV = ValueDecoder.scaledU16(buf, 0.1);
+ Integer airCompP = readPowerOffsetMinus5(buf);
+ Double h2PumpV = ValueDecoder.scaledU16(buf, 0.2);
+ Integer h2PumpP = readPowerOffsetMinus5(buf);
+ Double waterPumpV = ValueDecoder.scaledU16(buf, 0.1);
+ Double ptcV = ValueDecoder.scaledU16(buf, 0.1);
+ Integer ptcP = readPowerOffsetMinus5(buf);
+ Double lvBattV = ValueDecoder.scaledU16(buf, 0.1);
+ subs.add(new InfoBlock.GdFcAuxiliary.Subsystem(
+ airCompV, airCompP, h2PumpV, h2PumpP, waterPumpV, ptcV, ptcP, lvBattV));
+ }
+ return new InfoBlock.GdFcAuxiliary(count, subs);
+ }
+
+ /** 功率字段:raw(2B) - 5,0xFFFE/0xFFFF 异常/无效。 */
+ private static Integer readPowerOffsetMinus5(ByteBuffer buf) {
+ int raw = buf.getShort() & 0xFFFF;
+ if (raw == 0xFFFE || raw == 0xFFFF) return null;
+ return raw - 5;
+ }
+}
diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcDcDcBlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcDcDcBlockParser.java
new file mode 100644
index 00000000..3d841392
--- /dev/null
+++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcDcDcBlockParser.java
@@ -0,0 +1,37 @@
+package com.lingniu.ingest.protocol.gb32960.codec.parser.vendor.guangdong;
+
+import com.lingniu.ingest.protocol.gb32960.codec.InfoBlockParser;
+import com.lingniu.ingest.protocol.gb32960.codec.ValueDecoder;
+import com.lingniu.ingest.protocol.gb32960.model.InfoBlock;
+import com.lingniu.ingest.protocol.gb32960.model.ProtocolVersion;
+
+import java.nio.ByteBuffer;
+
+/**
+ * 广东燃料电池规范 §7.2.3.6 表 17:DC/DC(燃料电池系统)数据。typeCode {@code 0x32}。
+ *
+ * 固定 9 字节:
+ *
+ * inputVoltageV(2, 0.1V, 0~400V)
+ * inputCurrentA(2, 0.1A, 0~600A)
+ * outputVoltageV(2, 0.1V, 0~700V)
+ * outputCurrentA(2, 0.1A, 0~600A)
+ * controllerTempC(1, -40 offset)
+ *
+ */
+public final class GdFcDcDcBlockParser implements InfoBlockParser {
+
+ @Override public ProtocolVersion version() { return ProtocolVersion.V2016; }
+ @Override public int typeCode() { return 0x32; }
+ @Override public int fixedLength() { return 9; }
+
+ @Override
+ public InfoBlock parse(ByteBuffer buf) {
+ Double inV = ValueDecoder.scaledU16(buf, 0.1);
+ Double inC = ValueDecoder.scaledU16(buf, 0.1);
+ Double outV = ValueDecoder.scaledU16(buf, 0.1);
+ Double outC = ValueDecoder.scaledU16(buf, 0.1);
+ Integer ctrlTemp = ValueDecoder.tempOffset40(buf);
+ return new InfoBlock.GdFcDcDc(inV, inC, outV, outC, ctrlTemp);
+ }
+}
diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcDemoExtensionBlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcDemoExtensionBlockParser.java
new file mode 100644
index 00000000..bb5a7655
--- /dev/null
+++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcDemoExtensionBlockParser.java
@@ -0,0 +1,47 @@
+package com.lingniu.ingest.protocol.gb32960.codec.parser.vendor.guangdong;
+
+import com.lingniu.ingest.protocol.gb32960.codec.InfoBlockParser;
+import com.lingniu.ingest.protocol.gb32960.codec.ValueDecoder;
+import com.lingniu.ingest.protocol.gb32960.model.InfoBlock;
+import com.lingniu.ingest.protocol.gb32960.model.ProtocolVersion;
+
+import java.nio.ByteBuffer;
+
+/**
+ * 广东燃料电池规范 §7.2.3.14 表 27:燃料电池汽车示范应用数据扩展。typeCode {@code 0x80}。
+ *
+ * 结构(typeCode 由外层消费):
+ *
+ * dataLength (2B WORD) = 9 表示后续扩展数据长度
+ * stackTempC (1B, -40 offset, -40~+210°C)
+ * airCompressorVoltageV (2B, 0.1V, 0~2000V)
+ * airCompressorCurrentA (2B, 0.1A, -1000 offset, -1000~+1000A)
+ * hydrogenPumpVoltageV (2B, 0.1V, 0~2000V)
+ * hydrogenPumpCurrentA (2B, 0.1A, -1000 offset, -1000~+1000A)
+ *
+ *
+ * 含 length 字段共 11 字节 body(不含外层 1B typeCode)。
+ */
+public final class GdFcDemoExtensionBlockParser implements InfoBlockParser {
+
+ @Override public ProtocolVersion version() { return ProtocolVersion.V2016; }
+ @Override public int typeCode() { return 0x80; }
+ @Override public int fixedLength() { return 11; }
+
+ @Override
+ public InfoBlock parse(ByteBuffer buf) {
+ int declaredLen = buf.getShort() & 0xFFFF;
+ // declaredLen 通常 = 9,但即便不是也按 9 字节固定布局解,避免无谓的失败。
+ Integer stackTemp = ValueDecoder.tempOffset40(buf);
+ Double acV = ValueDecoder.scaledU16(buf, 0.1);
+ Double acC = ValueDecoder.scaledU16WithOffset(buf, 0.1, -1000.0);
+ Double h2V = ValueDecoder.scaledU16(buf, 0.1);
+ Double h2C = ValueDecoder.scaledU16WithOffset(buf, 0.1, -1000.0);
+ // 防御:若 declaredLen 与实际不一致,仅在 DEBUG 留痕。
+ if (declaredLen != 9 && org.slf4j.LoggerFactory.getLogger(GdFcDemoExtensionBlockParser.class).isDebugEnabled()) {
+ org.slf4j.LoggerFactory.getLogger(GdFcDemoExtensionBlockParser.class)
+ .debug("[gb32960/gd-fc/0x80] declaredLen={} (expected 9), parsed by fixed layout", declaredLen);
+ }
+ return new InfoBlock.GdFcDemoExtension(stackTemp, acV, acC, h2V, h2C);
+ }
+}
diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcStackBlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcStackBlockParser.java
new file mode 100644
index 00000000..7c8caaf5
--- /dev/null
+++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcStackBlockParser.java
@@ -0,0 +1,75 @@
+package com.lingniu.ingest.protocol.gb32960.codec.parser.vendor.guangdong;
+
+import com.lingniu.ingest.protocol.gb32960.codec.InfoBlockParser;
+import com.lingniu.ingest.protocol.gb32960.codec.ValueDecoder;
+import com.lingniu.ingest.protocol.gb32960.model.InfoBlock;
+import com.lingniu.ingest.protocol.gb32960.model.ProtocolVersion;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 广东燃料电池汽车示范应用城市群综合监管平台 §7.2.3.4 表 13/14:燃料电池电堆数据。
+ *
+ * typeCode {@code 0x30},工程上观察到对端在 GB/T 32960.3-2016 帧({@code 2323}
+ * 起始符)里下发该块。本规范在 GB/T 32960.3-2016 标准里属于 {@code 0x30~0x7F} 预留区段,
+ * 因此只有当对端被 vendor profile 选中后才启用此 parser。
+ *
+ *
结构:
+ *
+ * stackCount (1B)
+ * For each stack:
+ * engineWorkState (1B) 00 关闭 / 01 打开 / FE 异常 / FF 无效
+ * stackWaterOutletTempC (1B, -40 offset)
+ * hydrogenInletPressureKpa (2B WORD, -100 offset, 0.1 kPa)
+ * airInletPressureKpa (2B WORD, -100 offset, 0.1 kPa)
+ * airInletTempC (1B, -40 offset)
+ * maxCellVoltageId (2B WORD)
+ * minCellVoltageId (2B WORD)
+ * maxCellVoltageV (2B WORD, 0.001 V)
+ * minCellVoltageV (2B WORD, 0.001 V)
+ * avgCellVoltageV (2B WORD, 0.001 V)
+ * cellCount (2B WORD)
+ * frameCellStart (2B WORD)
+ * frameCellCount (1B)
+ * frameCellVoltagesV (2 * frameCellCount, 0.001 V)
+ *
+ */
+public final class GdFcStackBlockParser implements InfoBlockParser {
+
+ @Override public ProtocolVersion version() { return ProtocolVersion.V2016; }
+ @Override public int typeCode() { return 0x30; }
+ @Override public int fixedLength() { return -1; }
+
+ @Override
+ public InfoBlock parse(ByteBuffer buf) {
+ int stackCount = buf.get() & 0xFF;
+ List stacks = new ArrayList<>(stackCount);
+ for (int i = 0; i < stackCount; i++) {
+ Integer workState = ValueDecoder.u8raw(buf);
+ Integer waterTemp = ValueDecoder.tempOffset40(buf);
+ Double h2InletP = ValueDecoder.scaledU16WithOffset(buf, 0.1, -100.0);
+ Double airInletP = ValueDecoder.scaledU16WithOffset(buf, 0.1, -100.0);
+ Integer airInletTemp = ValueDecoder.tempOffset40(buf);
+ Integer maxId = ValueDecoder.u16(buf);
+ Integer minId = ValueDecoder.u16(buf);
+ Double maxV = ValueDecoder.scaledU16(buf, 0.001);
+ Double minV = ValueDecoder.scaledU16(buf, 0.001);
+ Double avgV = ValueDecoder.scaledU16(buf, 0.001);
+ Integer cellCount = ValueDecoder.u16(buf);
+ Integer frameStart = ValueDecoder.u16(buf);
+ int frameCells = buf.get() & 0xFF;
+ List frameVoltages = new ArrayList<>(frameCells);
+ int safe = Math.min(frameCells, buf.remaining() / 2);
+ for (int k = 0; k < safe; k++) {
+ frameVoltages.add((buf.getShort() & 0xFFFF) * 0.001);
+ }
+ stacks.add(new InfoBlock.GdFcStack.Stack(
+ workState, waterTemp, h2InletP, airInletP, airInletTemp,
+ maxId, minId, maxV, minV, avgV,
+ cellCount, frameStart, frameCells, frameVoltages));
+ }
+ return new InfoBlock.GdFcStack(stackCount, stacks);
+ }
+}
diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcVehicleInfoBlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcVehicleInfoBlockParser.java
new file mode 100644
index 00000000..955bed01
--- /dev/null
+++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcVehicleInfoBlockParser.java
@@ -0,0 +1,36 @@
+package com.lingniu.ingest.protocol.gb32960.codec.parser.vendor.guangdong;
+
+import com.lingniu.ingest.protocol.gb32960.codec.InfoBlockParser;
+import com.lingniu.ingest.protocol.gb32960.codec.ValueDecoder;
+import com.lingniu.ingest.protocol.gb32960.model.InfoBlock;
+import com.lingniu.ingest.protocol.gb32960.model.ProtocolVersion;
+
+import java.nio.ByteBuffer;
+
+/**
+ * 广东燃料电池规范 §7.2.3.8 表 19:车辆信息(补充)。typeCode {@code 0x34}。
+ *
+ * 固定 7 字节:
+ *
+ * collisionAlarm(1) 0 无 / 1 有
+ * ambientTempC(2, -40 offset, 1°C)
+ * ambientPressureKpa(2, -100 offset, 0.1 kPa)
+ * hydrogenMassKg(2, 0.1 kg)
+ *
+ */
+public final class GdFcVehicleInfoBlockParser implements InfoBlockParser {
+
+ @Override public ProtocolVersion version() { return ProtocolVersion.V2016; }
+ @Override public int typeCode() { return 0x34; }
+ @Override public int fixedLength() { return 7; }
+
+ @Override
+ public InfoBlock parse(ByteBuffer buf) {
+ Integer collision = ValueDecoder.u8raw(buf);
+ int rawTemp = buf.getShort() & 0xFFFF;
+ Integer tempC = (rawTemp == 0xFFFE || rawTemp == 0xFFFF) ? null : rawTemp - 40;
+ Double pressure = ValueDecoder.scaledU16WithOffset(buf, 0.1, -100.0);
+ Double h2Mass = ValueDecoder.scaledU16(buf, 0.1);
+ return new InfoBlock.GdFcVehicleInfo(collision, tempC, pressure, h2Mass);
+ }
+}
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 0660f48b..a36ee03a 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
@@ -32,6 +32,12 @@ public sealed interface InfoBlock
InfoBlock.FuelCellStack,
InfoBlock.SuperCapacitor,
InfoBlock.SuperCapacitorExtreme,
+ InfoBlock.GdFcStack,
+ InfoBlock.GdFcAuxiliary,
+ InfoBlock.GdFcDcDc,
+ InfoBlock.GdFcAirConditioner,
+ InfoBlock.GdFcVehicleInfo,
+ InfoBlock.GdFcDemoExtension,
InfoBlock.Raw {
InfoBlockType type();
@@ -402,6 +408,95 @@ public sealed interface InfoBlock
@Override public InfoBlockType type() { return InfoBlockType.SUPER_CAPACITOR_EXTREME; }
}
+ // ========================================================================
+ // 广东燃料电池汽车示范规范扩展块(typeCode 0x30~0x34、0x80)
+ // ========================================================================
+
+ /**
+ * 广东规范 0x30 燃料电池电堆数据(表 13/14)。变长。
+ * 一个 stackCount + 多个 stack;每个 stack 包含定长字段后跟 frameCellCount 个单体电压(2B/个)。
+ */
+ record GdFcStack(int stackCount, List stacks) implements InfoBlock {
+ @Override public InfoBlockType type() { return InfoBlockType.GD_FC_STACK; }
+
+ public record Stack(
+ Integer engineWorkState, // 1B
+ Integer stackWaterOutletTempC, // 1B (-40 offset)
+ Double hydrogenInletPressureKpa, // 2B (-100 offset, 0.1 kPa)
+ Double airInletPressureKpa, // 2B (-100 offset, 0.1 kPa)
+ Integer airInletTempC, // 1B (-40 offset)
+ Integer maxCellVoltageId, // 2B
+ Integer minCellVoltageId, // 2B
+ Double maxCellVoltageV, // 2B (0.001 V)
+ Double minCellVoltageV, // 2B (0.001 V)
+ Double avgCellVoltageV, // 2B (0.001 V)
+ Integer cellCount, // 2B
+ Integer frameCellStart, // 2B
+ Integer frameCellCount, // 1B
+ List frameCellVoltagesV // 2*m B (0.001 V)
+ ) {}
+ }
+
+ /** 广东规范 0x31 辅助系统数据(表 15/16)。变长。 */
+ record GdFcAuxiliary(int subSystemCount, List subsystems) implements InfoBlock {
+ @Override public InfoBlockType type() { return InfoBlockType.GD_FC_AUXILIARY; }
+
+ public record Subsystem(
+ Double airCompressorMotorVoltageV, // 2B 0.1V
+ Integer airCompressorPowerKw, // 2B (-5 offset, 1 kw)
+ Double hydrogenPumpMotorVoltageV, // 2B 0.2V
+ Integer hydrogenPumpPowerKw, // 2B (-5 offset, 1 kw)
+ Double waterPumpVoltageV, // 2B 0.1V
+ Double ptcVoltageV, // 2B 0.1V
+ Integer ptcPowerKw, // 2B (-5 offset, 1 kw)
+ Double lowVoltageBatteryVoltageV // 2B 0.1V
+ ) {}
+ }
+
+ /** 广东规范 0x32 DC/DC(燃料电池系统)数据(表 17)。固定 9 字节。 */
+ record GdFcDcDc(
+ Double inputVoltageV, // 2B 0.1V (0~400V)
+ Double inputCurrentA, // 2B 0.1A (0~600A)
+ Double outputVoltageV, // 2B 0.1V (0~700V)
+ Double outputCurrentA, // 2B 0.1A (0~600A)
+ Integer controllerTempC // 1B -40 offset
+ ) implements InfoBlock {
+ @Override public InfoBlockType type() { return InfoBlockType.GD_FC_DCDC; }
+ }
+
+ /** 广东规范 0x33 空调数据(表 18)。固定 5 字节。 */
+ record GdFcAirConditioner(
+ Integer status, // 1B 0 关闭 / 1 启动
+ Integer powerKw, // 2B (-5 offset, 1 kw, range -5~+30)
+ Double compressorInputVoltageV // 2B 0.1V (0~1000V)
+ ) implements InfoBlock {
+ @Override public InfoBlockType type() { return InfoBlockType.GD_FC_AIR_CONDITIONER; }
+ }
+
+ /** 广东规范 0x34 车辆信息数据(表 19)。固定 7 字节。 */
+ record GdFcVehicleInfo(
+ Integer collisionAlarm, // 1B 0 无 / 1 有
+ Integer ambientTempC, // 2B (-40 offset, 1°C)
+ Double ambientPressureKpa, // 2B (-100 offset, 0.1 kPa)
+ Double hydrogenMassKg // 2B 0.1 kg
+ ) implements InfoBlock {
+ @Override public InfoBlockType type() { return InfoBlockType.GD_FC_VEHICLE_INFO; }
+ }
+
+ /**
+ * 广东规范 0x80 燃料电池汽车示范应用数据扩展(表 27)。固定 12 字节
+ * (含 2 字节内层 length=9 + 9 字节数据)。
+ */
+ record GdFcDemoExtension(
+ Integer stackTempC, // 1B -40 offset
+ Double airCompressorVoltageV, // 2B 0.1V (0~2000V)
+ Double airCompressorCurrentA, // 2B 0.1A (-1000 offset)
+ Double hydrogenPumpVoltageV, // 2B 0.1V (0~2000V)
+ Double hydrogenPumpCurrentA // 2B 0.1A (-1000 offset)
+ ) implements InfoBlock {
+ @Override public InfoBlockType type() { return InfoBlockType.GD_FC_DEMO_EXTENSION; }
+ }
+
// ========================================================================
// Raw / Unknown
// ========================================================================
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 d9d1f0b8..0f7bc1dc 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
@@ -44,6 +44,18 @@ public enum InfoBlockType {
SUPER_CAPACITOR_EXTREME,
/** 自定义数据 0x80~0xFE(表 27)。 */
USER_DEFINED,
+ /** 广东燃料电池规范 0x30 燃料电池电堆数据(表 14)。 */
+ GD_FC_STACK,
+ /** 广东燃料电池规范 0x31 辅助系统数据(表 16)。 */
+ GD_FC_AUXILIARY,
+ /** 广东燃料电池规范 0x32 DC/DC 数据(表 17)。 */
+ GD_FC_DCDC,
+ /** 广东燃料电池规范 0x33 空调数据(表 18)。 */
+ GD_FC_AIR_CONDITIONER,
+ /** 广东燃料电池规范 0x34 车辆信息数据(表 19)。 */
+ GD_FC_VEHICLE_INFO,
+ /** 广东燃料电池规范 0x80 燃料电池汽车示范应用数据扩展(表 27)。 */
+ GD_FC_DEMO_EXTENSION,
/** 原始/未解析。 */
RAW
}
diff --git a/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcParserTest.java b/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcParserTest.java
new file mode 100644
index 00000000..f9d65acf
--- /dev/null
+++ b/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/codec/parser/vendor/guangdong/GdFcParserTest.java
@@ -0,0 +1,86 @@
+package com.lingniu.ingest.protocol.gb32960.codec.parser.vendor.guangdong;
+
+import com.lingniu.ingest.protocol.gb32960.model.InfoBlock;
+import org.junit.jupiter.api.Test;
+
+import java.nio.ByteBuffer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.within;
+
+/**
+ * 单元测试:广东燃料电池规范 fixed-length parser(0x32 / 0x33 / 0x34 / 0x80)逐字段还原。
+ * 0x30/0x31 是变长,集成层会在 BodyParser 全链路测试里覆盖。
+ */
+class GdFcParserTest {
+
+ @Test
+ void dcDcBlockParsesAllFields() {
+ // inV=300.0 inC=120.0 outV=560.0 outC=110.5 ctrlTemp=65°C
+ ByteBuffer buf = ByteBuffer.wrap(new byte[]{
+ 0x0B, (byte) 0xB8, // 0x0BB8 = 3000 → 300.0 V
+ 0x04, (byte) 0xB0, // 0x04B0 = 1200 → 120.0 A
+ 0x15, (byte) 0xE0, // 0x15E0 = 5600 → 560.0 V
+ 0x04, 0x52, // 0x0452 = 1106 → 110.6 A
+ 0x69 // 0x69 = 105 - 40 = 65 °C
+ });
+ InfoBlock.GdFcDcDc b = (InfoBlock.GdFcDcDc) new GdFcDcDcBlockParser().parse(buf);
+ assertThat(b.inputVoltageV()).isCloseTo(300.0, within(1e-6));
+ assertThat(b.inputCurrentA()).isCloseTo(120.0, within(1e-6));
+ assertThat(b.outputVoltageV()).isCloseTo(560.0, within(1e-6));
+ assertThat(b.outputCurrentA()).isCloseTo(110.6, within(1e-6));
+ assertThat(b.controllerTempC()).isEqualTo(65);
+ }
+
+ @Test
+ void airConditionerParsesPowerOffsetAndStatus() {
+ // status=01 power raw=20 (= 15 kw after -5 offset) inputV=540.0
+ ByteBuffer buf = ByteBuffer.wrap(new byte[]{
+ 0x01,
+ 0x00, 0x14, // 20 - 5 = 15 kw
+ 0x15, (byte) 0x18 // 0x1518 = 5400 → 540.0 V
+ });
+ InfoBlock.GdFcAirConditioner b =
+ (InfoBlock.GdFcAirConditioner) new GdFcAirConditionerBlockParser().parse(buf);
+ assertThat(b.status()).isEqualTo(1);
+ assertThat(b.powerKw()).isEqualTo(15);
+ assertThat(b.compressorInputVoltageV()).isCloseTo(540.0, within(1e-6));
+ }
+
+ @Test
+ void vehicleInfoParsesAmbientFields() {
+ // collision=00 ambientTemp raw=70 (=30°C) ambientPressure raw=1100 (10kPa) h2Mass raw=80 (8.0 kg)
+ ByteBuffer buf = ByteBuffer.wrap(new byte[]{
+ 0x00,
+ 0x00, 0x46, // 70 - 40 = 30°C
+ 0x04, 0x4C, // 0x044C = 1100 → 110*0.1 - 100 = 10.0 kPa
+ 0x00, 0x50 // 0x0050 = 80 → 8.0 kg
+ });
+ InfoBlock.GdFcVehicleInfo b =
+ (InfoBlock.GdFcVehicleInfo) new GdFcVehicleInfoBlockParser().parse(buf);
+ assertThat(b.collisionAlarm()).isEqualTo(0);
+ assertThat(b.ambientTempC()).isEqualTo(30);
+ assertThat(b.ambientPressureKpa()).isCloseTo(10.0, within(1e-6));
+ assertThat(b.hydrogenMassKg()).isCloseTo(8.0, within(1e-6));
+ }
+
+ @Test
+ void demoExtensionParsesInnerLengthAndFields() {
+ // length=9, stackTemp=50, acV=372.0, acC=0, h2V=1.3V, h2C=0
+ ByteBuffer buf = ByteBuffer.wrap(new byte[]{
+ 0x00, 0x09, // declared length
+ 0x5A, // 90 - 40 = 50°C
+ 0x0E, (byte) 0x88, // 0x0E88 = 3720 → 372.0V
+ 0x27, 0x10, // 0x2710 = 10000 → 0 A (after -1000)
+ 0x00, 0x0D, // 0x000D = 13 → 1.3V
+ 0x27, 0x10 // 0 A
+ });
+ InfoBlock.GdFcDemoExtension b =
+ (InfoBlock.GdFcDemoExtension) new GdFcDemoExtensionBlockParser().parse(buf);
+ assertThat(b.stackTempC()).isEqualTo(50);
+ assertThat(b.airCompressorVoltageV()).isCloseTo(372.0, within(1e-6));
+ assertThat(b.airCompressorCurrentA()).isCloseTo(0.0, within(1e-6));
+ assertThat(b.hydrogenPumpVoltageV()).isCloseTo(1.3, within(1e-6));
+ assertThat(b.hydrogenPumpCurrentA()).isCloseTo(0.0, within(1e-6));
+ }
+}
diff --git a/reference/广东燃料电池汽车示范应用城市群综合监管平台-燃料电池汽车数据接入技术规范v1.0.220822.pdf b/reference/广东燃料电池汽车示范应用城市群综合监管平台-燃料电池汽车数据接入技术规范v1.0.220822.pdf
new file mode 100644
index 00000000..ab50e811
Binary files /dev/null and b/reference/广东燃料电池汽车示范应用城市群综合监管平台-燃料电池汽车数据接入技术规范v1.0.220822.pdf differ