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,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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user