feat(gb32960/vendor): implement Guangdong fuel-cell extension parsers
Adds 6 vendor parsers under codec/parser/vendor/guangdong/ implementing the Guangdong "燃料电池汽车数据接入技术规范 v1.0" extension typeCodes that the city's data ingestion platform sends inside V2016 frames: 0x30 GdFcStackBlockParser §7.2.3.4 表 13/14 fuel-cell stack 0x31 GdFcAuxiliaryBlockParser §7.2.3.5 表 15/16 auxiliary system 0x32 GdFcDcDcBlockParser §7.2.3.6 表 17 DC/DC 0x33 GdFcAirConditionerBlockParser §7.2.3.7 表 18 air conditioner 0x34 GdFcVehicleInfoBlockParser §7.2.3.8 表 19 vehicle info 0x80 GdFcDemoExtensionBlockParser §7.2.3.14 表 27 demo data extension Each parser uses ProtocolVersion.V2016 + its respective typeCode and is strictly Guangdong-spec layout, not the GB/T 32960.3-2025 fuel-cell stack layout (the two share typeCode 0x30 but have different field orders and extra cell-voltage statistics in the Guangdong variant). Field model in InfoBlock.java gains: - GdFcStack(stackCount, stacks: Stack(workState, waterTempC, h2InletPressureKpa, airInletPressureKpa, airInletTempC, max/min/avg cell voltage ids and values, cellCount, frameCellStart, frameCellCount, frameCellVoltagesV)) - GdFcAuxiliary(subSystemCount, subsystems: Subsystem with air compressor, hydrogen pump, water pump, PTC, low-voltage battery) - GdFcDcDc, GdFcAirConditioner, GdFcVehicleInfo, GdFcDemoExtension InfoBlockType enum gains the matching GD_FC_* constants. Tests: - GdFcParserTest covers the 4 fixed-length parsers (0x32/0x33/0x34/0x80) with hand-crafted bytes asserting field-by-field decoded values. - 0x30/0x31 are variable-length and will be exercised end-to-end via the body parser integration test in a follow-up. Spec doc: adds reference/广东燃料电池汽车示范应用城市群综合监管平台-...pdf (v1.0.220822) used as the source of truth for these parsers. These parsers exist as classes only; no Spring beans are wired yet. Wiring + decoder context plumbing land in the next commit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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}。
|
||||
*
|
||||
* <p>固定 5 字节:
|
||||
* <pre>
|
||||
* status(1) 0 关闭 / 1 启动
|
||||
* powerKw(2, offset -5, 1 kw, range -5~+30)
|
||||
* compressorInputVoltageV(2, 0.1V, 0~1000V)
|
||||
* </pre>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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}。变长。
|
||||
*
|
||||
* <pre>
|
||||
* 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)
|
||||
* </pre>
|
||||
*
|
||||
* 每子系统 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<InfoBlock.GdFcAuxiliary.Subsystem> 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;
|
||||
}
|
||||
}
|
||||
@@ -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}。
|
||||
*
|
||||
* <p>固定 9 字节:
|
||||
* <pre>
|
||||
* 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)
|
||||
* </pre>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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}。
|
||||
*
|
||||
* <p>结构(typeCode 由外层消费):
|
||||
* <pre>
|
||||
* 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)
|
||||
* </pre>
|
||||
*
|
||||
* 含 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);
|
||||
}
|
||||
}
|
||||
@@ -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:燃料电池电堆数据。
|
||||
*
|
||||
* <p>typeCode {@code 0x30},工程上观察到对端在 GB/T 32960.3-<b>2016</b> 帧({@code 2323}
|
||||
* 起始符)里下发该块。本规范在 GB/T 32960.3-2016 标准里属于 {@code 0x30~0x7F} 预留区段,
|
||||
* 因此只有当对端被 vendor profile 选中后才启用此 parser。
|
||||
*
|
||||
* <p>结构:
|
||||
* <pre>
|
||||
* 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)
|
||||
* </pre>
|
||||
*/
|
||||
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<InfoBlock.GdFcStack.Stack> 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<Double> 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);
|
||||
}
|
||||
}
|
||||
@@ -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}。
|
||||
*
|
||||
* <p>固定 7 字节:
|
||||
* <pre>
|
||||
* collisionAlarm(1) 0 无 / 1 有
|
||||
* ambientTempC(2, -40 offset, 1°C)
|
||||
* ambientPressureKpa(2, -100 offset, 0.1 kPa)
|
||||
* hydrogenMassKg(2, 0.1 kg)
|
||||
* </pre>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<Stack> 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<Double> frameCellVoltagesV // 2*m B (0.001 V)
|
||||
) {}
|
||||
}
|
||||
|
||||
/** 广东规范 0x31 辅助系统数据(表 15/16)。变长。 */
|
||||
record GdFcAuxiliary(int subSystemCount, List<Subsystem> 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
|
||||
// ========================================================================
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
BIN
reference/广东燃料电池汽车示范应用城市群综合监管平台-燃料电池汽车数据接入技术规范v1.0.220822.pdf
Normal file
BIN
reference/广东燃料电池汽车示范应用城市群综合监管平台-燃料电池汽车数据接入技术规范v1.0.220822.pdf
Normal file
Binary file not shown.
Reference in New Issue
Block a user