From dd8f75e0da8394cd3c007d12f3128cbbcc0ca070 Mon Sep 17 00:00:00 2001 From: lingniu-dev Date: Wed, 15 Apr 2026 16:52:57 +0800 Subject: [PATCH] refactor(gb32960/model): nest InfoBlock by . MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure InfoBlock from a flat sealed interface into three nested sealed interfaces grouped by protocol and version, so every record's fully-qualified name follows the rule InfoBlock.. and the type system enforces strict separation between version variants. New shape: InfoBlock (sealed) ├── Gb32960V2016 (sealed) │ ├── Vehicle (20 B, has accel/brake pedal) │ ├── DriveMotor (rpm offset 20000, torque 2 B) │ ├── FuelCell (with N x 1B probe temps) │ ├── Engine (5 B fixed) │ ├── Position (9 B, no coord system) │ ├── Extreme (14 B, 2016-only) │ ├── Alarm (typeCode 0x07, no general alarm levels) │ ├── Voltage (0x08 storage voltage) │ └── Temperature (0x09 storage temperature) ├── Gb32960V2025 (sealed) │ ├── Vehicle (18 B, no pedal fields) │ ├── DriveMotor (rpm offset 32000, torque 4 B) │ ├── FuelCell (no current/voltage, no probe list) │ ├── Engine (5 B fixed, identical layout to V2016 but │ duplicated to keep version separation strict) │ ├── Position (10 B, with coordSystem byte) │ ├── Alarm (typeCode 0x06, with generalAlarmLevels) │ ├── MinParallelVoltage (0x07 in V2025) │ ├── BatteryTemperature (0x08 in V2025) │ ├── FuelCellStack (0x30) │ ├── SuperCapacitor (0x31) │ └── SuperCapacitorExtreme (0x32) ├── GuangdongFc (sealed) │ ├── Stack (0x30, vendor variant — different field order │ and adds max/min/avg single-cell voltage stats) │ ├── Auxiliary (0x31, air compressor / hydrogen pump / PTC) │ ├── DcDc (0x32 fixed 9 B) │ ├── AirConditioner (0x33 fixed 5 B) │ ├── VehicleInfo (0x34 fixed 7 B) │ └── DemoExtension (0x80 demo extension) └── Raw Even types that share an identical field list (Engine, the two DriveMotor.Motor inner records, Vehicle when ignoring pedals) are deliberately duplicated rather than shared, per the rule "数据类型严格按 版本拆分". This lets consumers branch on instanceof / pattern-match without losing track of which protocol version produced the data. Knock-on changes (mechanical for the most part): - All 16 standard parsers (v2016/, v2025/) updated to instantiate the matching nested record; e.g. new InfoBlock.Vehicle(...) becomes new InfoBlock.Gb32960V2016.Vehicle(...). - The 6 Guangdong vendor parsers now return InfoBlock.GuangdongFc.* records (previously InfoBlock.GdFcXxx flat records). - Position/Alarm record pairs split: Position no longer carries a nullable coordSystem field (V2025 has its own record with coordSystem; V2016 has neither), and Alarm no longer carries the protocolVersion tag — the nested type already encodes the version. - Gb32960EventMapper grew three small private "view" records (VehicleView, PositionView, AlarmView) plus extract* helpers that try V2016 first then fall back to V2025, so downstream payload assembly still sees a single uniform view. - Test files updated to reference the new fully-qualified names. InfoBlockType enum is left untouched: it is the logical type tag, and both DriveMotor variants still report InfoBlockType.DRIVE_MOTOR even though they live under different sealed parents — the *structural* separation is what matters. All 30 tests pass (18 existing + 8 selector + 4 vendor parser). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../parser/v2016/AlarmV2016BlockParser.java | 6 +- .../v2016/DriveMotorV2016BlockParser.java | 6 +- .../parser/v2016/EngineV2016BlockParser.java | 2 +- .../v2016/ExtremeValueV2016BlockParser.java | 2 +- .../v2016/FuelCellV2016BlockParser.java | 2 +- .../v2016/PositionV2016BlockParser.java | 2 +- .../v2016/TemperatureV2016BlockParser.java | 2 +- .../parser/v2016/VehicleV2016BlockParser.java | 6 +- .../parser/v2016/VoltageV2016BlockParser.java | 2 +- .../parser/v2025/AlarmV2025BlockParser.java | 9 +- .../BatteryTemperatureV2025BlockParser.java | 6 +- .../v2025/DriveMotorV2025BlockParser.java | 6 +- .../parser/v2025/EngineV2025BlockParser.java | 2 +- .../v2025/FuelCellStackV2025BlockParser.java | 6 +- .../v2025/FuelCellV2025BlockParser.java | 2 +- .../MinParallelVoltageV2025BlockParser.java | 6 +- .../v2025/PositionV2025BlockParser.java | 2 +- ...SuperCapacitorExtremeV2025BlockParser.java | 2 +- .../v2025/SuperCapacitorV2025BlockParser.java | 2 +- .../parser/v2025/VehicleV2025BlockParser.java | 6 +- .../GdFcAirConditionerBlockParser.java | 2 +- .../guangdong/GdFcAuxiliaryBlockParser.java | 6 +- .../vendor/guangdong/GdFcDcDcBlockParser.java | 2 +- .../GdFcDemoExtensionBlockParser.java | 2 +- .../guangdong/GdFcStackBlockParser.java | 6 +- .../guangdong/GdFcVehicleInfoBlockParser.java | 2 +- .../gb32960/mapper/Gb32960EventMapper.java | 124 ++- .../protocol/gb32960/model/InfoBlock.java | 823 ++++++++---------- .../codec/Gb32960DecoderGoldenTest.java | 2 +- .../gb32960/codec/Gb32960DecoderTest.java | 4 +- .../gb32960/codec/Gb32960FullBlocksTest.java | 8 +- .../vendor/guangdong/GdFcParserTest.java | 15 +- 32 files changed, 534 insertions(+), 541 deletions(-) diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/AlarmV2016BlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/AlarmV2016BlockParser.java index ca64e866..9b8d06ae 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/AlarmV2016BlockParser.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/AlarmV2016BlockParser.java @@ -4,6 +4,7 @@ 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; +// ProtocolVersion still used by version() override below import java.nio.ByteBuffer; import java.util.ArrayList; @@ -29,9 +30,8 @@ public final class AlarmV2016BlockParser implements InfoBlockParser { List motor = readFaultList(buf); List engine = readFaultList(buf); List other = readFaultList(buf); - return new InfoBlock.Alarm( - ProtocolVersion.V2016, maxLevel, generalFlag, - battery, motor, engine, other, null); + return new InfoBlock.Gb32960V2016.Alarm( + maxLevel, generalFlag, battery, motor, engine, other); } static List readFaultList(ByteBuffer buf) { diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/DriveMotorV2016BlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/DriveMotorV2016BlockParser.java index ffb8eab1..b6c0250c 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/DriveMotorV2016BlockParser.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/DriveMotorV2016BlockParser.java @@ -25,7 +25,7 @@ public final class DriveMotorV2016BlockParser implements InfoBlockParser { @Override public InfoBlock parse(ByteBuffer buf) { int count = buf.get() & 0xFF; - List motors = new ArrayList<>(count); + List motors = new ArrayList<>(count); for (int i = 0; i < count; i++) { Integer serialNo = ValueDecoder.u8(buf); Integer state = ValueDecoder.u8(buf); @@ -37,9 +37,9 @@ public final class DriveMotorV2016BlockParser implements InfoBlockParser { Integer motorTempC = ValueDecoder.tempOffset40(buf); Double ctrlVoltage = ValueDecoder.scaledU16(buf, 0.1); Double ctrlCurrent = ValueDecoder.scaledU16WithOffset(buf, 0.1, -1000.0); - motors.add(new InfoBlock.DriveMotor.Motor( + motors.add(new InfoBlock.Gb32960V2016.DriveMotor.Motor( serialNo, state, ctrlTempC, rpm, torque, motorTempC, ctrlVoltage, ctrlCurrent)); } - return new InfoBlock.DriveMotor(motors); + return new InfoBlock.Gb32960V2016.DriveMotor(motors); } } diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/EngineV2016BlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/EngineV2016BlockParser.java index 6f453b5f..580857ae 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/EngineV2016BlockParser.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/EngineV2016BlockParser.java @@ -23,6 +23,6 @@ public final class EngineV2016BlockParser implements InfoBlockParser { Integer engineState = ValueDecoder.u8(buf); Integer crankshaftRpm = ValueDecoder.u16(buf); Double fuelRate = ValueDecoder.scaledU16(buf, 0.01); - return new InfoBlock.Engine(engineState, crankshaftRpm, fuelRate); + return new InfoBlock.Gb32960V2016.Engine(engineState, crankshaftRpm, fuelRate); } } diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/ExtremeValueV2016BlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/ExtremeValueV2016BlockParser.java index f7a92f6b..c5361903 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/ExtremeValueV2016BlockParser.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/ExtremeValueV2016BlockParser.java @@ -31,7 +31,7 @@ public final class ExtremeValueV2016BlockParser implements InfoBlockParser { Integer minTSub = ValueDecoder.u8(buf); Integer minTProbe = ValueDecoder.u8(buf); Integer minT = ValueDecoder.tempOffset40(buf); - return new InfoBlock.Extreme( + return new InfoBlock.Gb32960V2016.Extreme( maxVSub, maxVCell, maxV, minVSub, minVCell, minV, maxTSub, maxTProbe, maxT, diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/FuelCellV2016BlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/FuelCellV2016BlockParser.java index 74bbdf2c..527657d0 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/FuelCellV2016BlockParser.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/FuelCellV2016BlockParser.java @@ -44,7 +44,7 @@ public final class FuelCellV2016BlockParser implements InfoBlockParser { Integer maxHydrogenPressureProbeId = ValueDecoder.u8(buf); Integer hvDcDcStatus = ValueDecoder.u8(buf); - return new InfoBlock.FuelCellV2016( + return new InfoBlock.Gb32960V2016.FuelCell( fcVoltage, fcCurrent, consumption, probes, maxHydrogenTempC, maxHydrogenTempProbeId, maxHydrogenConcentration, maxHydrogenConcentrationProbeId, diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/PositionV2016BlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/PositionV2016BlockParser.java index b0228618..a543f7fb 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/PositionV2016BlockParser.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/PositionV2016BlockParser.java @@ -27,6 +27,6 @@ public final class PositionV2016BlockParser implements InfoBlockParser { double latitude = latRaw / 1_000_000.0; if ((statusFlag & 0b100) != 0) longitude = -longitude; if ((statusFlag & 0b010) != 0) latitude = -latitude; - return new InfoBlock.Position(statusFlag, null, longitude, latitude); + return new InfoBlock.Gb32960V2016.Position(statusFlag, longitude, latitude); } } diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/TemperatureV2016BlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/TemperatureV2016BlockParser.java index 4a8d742a..375bc7b3 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/TemperatureV2016BlockParser.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/TemperatureV2016BlockParser.java @@ -34,6 +34,6 @@ public final class TemperatureV2016BlockParser implements InfoBlockParser { } if (maxT == Integer.MIN_VALUE) maxT = 0; if (minT == Integer.MAX_VALUE) minT = 0; - return new InfoBlock.Temperature(subCount, maxT, minT); + return new InfoBlock.Gb32960V2016.Temperature(subCount, maxT, minT); } } diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/VehicleV2016BlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/VehicleV2016BlockParser.java index 941c0140..df2558bc 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/VehicleV2016BlockParser.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/VehicleV2016BlockParser.java @@ -35,8 +35,8 @@ public final class VehicleV2016BlockParser implements InfoBlockParser { Integer insulation = ValueDecoder.u16(buf); Integer accelPedal = ValueDecoder.u8(buf); Integer brakePedal = ValueDecoder.u8(buf); - return new InfoBlock.Vehicle(vehicleState, chargingState, runningMode, speedKmh, mileageKm, - totalVoltageV, totalCurrentA, soc, dcDcStatus, gearRaw, insulation, - accelPedal, brakePedal); + return new InfoBlock.Gb32960V2016.Vehicle(vehicleState, chargingState, runningMode, + speedKmh, mileageKm, totalVoltageV, totalCurrentA, soc, dcDcStatus, gearRaw, + insulation, accelPedal, brakePedal); } } diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/VoltageV2016BlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/VoltageV2016BlockParser.java index 33733088..e6d5fdf8 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/VoltageV2016BlockParser.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2016/VoltageV2016BlockParser.java @@ -44,6 +44,6 @@ public final class VoltageV2016BlockParser implements InfoBlockParser { } if (maxCell == Double.MIN_VALUE) maxCell = 0; if (minCell == Double.MAX_VALUE) minCell = 0; - return new InfoBlock.Voltage(subCount, maxCell, minCell, totalV); + return new InfoBlock.Gb32960V2016.Voltage(subCount, maxCell, minCell, totalV); } } diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/AlarmV2025BlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/AlarmV2025BlockParser.java index 6fce0e27..8a3b0b78 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/AlarmV2025BlockParser.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/AlarmV2025BlockParser.java @@ -29,16 +29,15 @@ public final class AlarmV2025BlockParser implements InfoBlockParser { List other = readFaultList(buf); int generalAlarmCount = buf.hasRemaining() ? (buf.get() & 0xFF) : 0; - List levels = new ArrayList<>(generalAlarmCount); + List levels = new ArrayList<>(generalAlarmCount); for (int i = 0; i < generalAlarmCount && buf.remaining() >= 2; i++) { int bit = buf.get() & 0xFF; int level = buf.get() & 0xFF; - levels.add(new InfoBlock.Alarm.GeneralAlarmEntry(bit, level)); + levels.add(new InfoBlock.Gb32960V2025.Alarm.GeneralAlarmEntry(bit, level)); } - return new InfoBlock.Alarm( - ProtocolVersion.V2025, maxLevel, generalFlag, - battery, motor, engine, other, levels); + return new InfoBlock.Gb32960V2025.Alarm( + maxLevel, generalFlag, battery, motor, engine, other, levels); } private static List readFaultList(ByteBuffer buf) { diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/BatteryTemperatureV2025BlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/BatteryTemperatureV2025BlockParser.java index 6bf7ec40..002a64c8 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/BatteryTemperatureV2025BlockParser.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/BatteryTemperatureV2025BlockParser.java @@ -21,7 +21,7 @@ public final class BatteryTemperatureV2025BlockParser implements InfoBlockParser @Override public InfoBlock parse(ByteBuffer buf) { int subCount = buf.get() & 0xFF; - List packs = new ArrayList<>(subCount); + List packs = new ArrayList<>(subCount); for (int i = 0; i < subCount; i++) { int packNo = buf.get() & 0xFF; int probeCount = buf.getShort() & 0xFFFF; @@ -30,8 +30,8 @@ public final class BatteryTemperatureV2025BlockParser implements InfoBlockParser for (int k = 0; k < safeCount; k++) { temps.add((buf.get() & 0xFF) - 40); } - packs.add(new InfoBlock.BatteryTemperature.Pack(packNo, probeCount, temps)); + packs.add(new InfoBlock.Gb32960V2025.BatteryTemperature.Pack(packNo, probeCount, temps)); } - return new InfoBlock.BatteryTemperature(subCount, packs); + return new InfoBlock.Gb32960V2025.BatteryTemperature(subCount, packs); } } diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/DriveMotorV2025BlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/DriveMotorV2025BlockParser.java index 9a7abec9..bcf49e03 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/DriveMotorV2025BlockParser.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/DriveMotorV2025BlockParser.java @@ -27,7 +27,7 @@ public final class DriveMotorV2025BlockParser implements InfoBlockParser { @Override public InfoBlock parse(ByteBuffer buf) { int count = buf.get() & 0xFF; - List motors = new ArrayList<>(count); + List motors = new ArrayList<>(count); for (int i = 0; i < count; i++) { Integer serialNo = ValueDecoder.u8(buf); Integer state = ValueDecoder.u8(buf); @@ -39,9 +39,9 @@ public final class DriveMotorV2025BlockParser implements InfoBlockParser { Integer motorTempC = ValueDecoder.tempOffset40(buf); Double ctrlVoltage = ValueDecoder.scaledU16(buf, 0.1); Double ctrlCurrent = ValueDecoder.scaledU16WithOffset(buf, 0.1, -1000.0); - motors.add(new InfoBlock.DriveMotor.Motor( + motors.add(new InfoBlock.Gb32960V2025.DriveMotor.Motor( serialNo, state, ctrlTempC, rpm, torque, motorTempC, ctrlVoltage, ctrlCurrent)); } - return new InfoBlock.DriveMotor(motors); + return new InfoBlock.Gb32960V2025.DriveMotor(motors); } } diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/EngineV2025BlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/EngineV2025BlockParser.java index 8f17f8a2..48661477 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/EngineV2025BlockParser.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/EngineV2025BlockParser.java @@ -22,6 +22,6 @@ public final class EngineV2025BlockParser implements InfoBlockParser { Integer engineState = ValueDecoder.u8(buf); Integer crankshaftRpm = ValueDecoder.u16(buf); Double fuelRate = ValueDecoder.scaledU16(buf, 0.01); - return new InfoBlock.Engine(engineState, crankshaftRpm, fuelRate); + return new InfoBlock.Gb32960V2025.Engine(engineState, crankshaftRpm, fuelRate); } } diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/FuelCellStackV2025BlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/FuelCellStackV2025BlockParser.java index 32aecb90..64d42a21 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/FuelCellStackV2025BlockParser.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/FuelCellStackV2025BlockParser.java @@ -23,7 +23,7 @@ public final class FuelCellStackV2025BlockParser implements InfoBlockParser { @Override public InfoBlock parse(ByteBuffer buf) { int stackCount = buf.get() & 0xFF; - List stacks = new ArrayList<>(stackCount); + List stacks = new ArrayList<>(stackCount); for (int i = 0; i < stackCount; i++) { int stackNo = buf.get() & 0xFF; Double voltage = ValueDecoder.scaledU16(buf, 0.1); @@ -37,9 +37,9 @@ public final class FuelCellStackV2025BlockParser implements InfoBlockParser { for (int k = 0; k < safeCount; k++) { temps.add((buf.get() & 0xFF) - 40); } - stacks.add(new InfoBlock.FuelCellStack.Stack( + stacks.add(new InfoBlock.Gb32960V2025.FuelCellStack.Stack( stackNo, voltage, current, h2InletPressure, airInletPressure, airInletTemp, temps)); } - return new InfoBlock.FuelCellStack(stackCount, stacks); + return new InfoBlock.Gb32960V2025.FuelCellStack(stackCount, stacks); } } diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/FuelCellV2025BlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/FuelCellV2025BlockParser.java index 9adab4d8..72edbd76 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/FuelCellV2025BlockParser.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/FuelCellV2025BlockParser.java @@ -27,7 +27,7 @@ public final class FuelCellV2025BlockParser implements InfoBlockParser { Integer hvDcDcStatus = ValueDecoder.u8(buf); Integer remainingH2Percent = ValueDecoder.u8(buf); Integer hvDcDcControllerTempC = ValueDecoder.tempOffset40(buf); - return new InfoBlock.FuelCellV2025( + return new InfoBlock.Gb32960V2025.FuelCell( maxTempC, maxTempProbeId, maxConcentration, maxConcentrationProbeId, maxPressure, maxPressureProbeId, diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/MinParallelVoltageV2025BlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/MinParallelVoltageV2025BlockParser.java index 13db9676..665e67f1 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/MinParallelVoltageV2025BlockParser.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/MinParallelVoltageV2025BlockParser.java @@ -22,7 +22,7 @@ public final class MinParallelVoltageV2025BlockParser implements InfoBlockParser @Override public InfoBlock parse(ByteBuffer buf) { int subCount = buf.get() & 0xFF; - List packs = new ArrayList<>(subCount); + List packs = new ArrayList<>(subCount); for (int i = 0; i < subCount; i++) { int packNo = buf.get() & 0xFF; Double voltage = ValueDecoder.scaledU16(buf, 0.1); @@ -33,9 +33,9 @@ public final class MinParallelVoltageV2025BlockParser implements InfoBlockParser for (int k = 0; k < safeCount; k++) { frameVoltages.add((buf.getShort() & 0xFFFF) * 0.001); } - packs.add(new InfoBlock.MinParallelVoltage.Pack( + packs.add(new InfoBlock.Gb32960V2025.MinParallelVoltage.Pack( packNo, voltage, current, totalUnits, frameVoltages)); } - return new InfoBlock.MinParallelVoltage(subCount, packs); + return new InfoBlock.Gb32960V2025.MinParallelVoltage(subCount, packs); } } diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/PositionV2025BlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/PositionV2025BlockParser.java index 3611f85f..d6a65c84 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/PositionV2025BlockParser.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/PositionV2025BlockParser.java @@ -29,6 +29,6 @@ public final class PositionV2025BlockParser implements InfoBlockParser { double latitude = latRaw / 1_000_000.0; if ((statusFlag & 0b100) != 0) longitude = -longitude; if ((statusFlag & 0b010) != 0) latitude = -latitude; - return new InfoBlock.Position(statusFlag, coordSystem, longitude, latitude); + return new InfoBlock.Gb32960V2025.Position(statusFlag, coordSystem, longitude, latitude); } } diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/SuperCapacitorExtremeV2025BlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/SuperCapacitorExtremeV2025BlockParser.java index cd0f2b83..657a825c 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/SuperCapacitorExtremeV2025BlockParser.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/SuperCapacitorExtremeV2025BlockParser.java @@ -30,7 +30,7 @@ public final class SuperCapacitorExtremeV2025BlockParser implements InfoBlockPar Integer minTSys = ValueDecoder.u8(buf); Integer minTProbe = ValueDecoder.u16(buf); Integer minT = ValueDecoder.tempOffset40(buf); - return new InfoBlock.SuperCapacitorExtreme( + return new InfoBlock.Gb32960V2025.SuperCapacitorExtreme( maxVSys, maxVCell, maxV, minVSys, minVCell, minV, maxTSys, maxTProbe, maxT, diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/SuperCapacitorV2025BlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/SuperCapacitorV2025BlockParser.java index b6411cf1..b1e65c07 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/SuperCapacitorV2025BlockParser.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/SuperCapacitorV2025BlockParser.java @@ -35,6 +35,6 @@ public final class SuperCapacitorV2025BlockParser implements InfoBlockParser { for (int i = 0; i < safeProbeCount; i++) { temps.add((buf.get() & 0xFF) - 40); } - return new InfoBlock.SuperCapacitor(systemNo, totalVoltage, totalCurrent, cells, temps); + return new InfoBlock.Gb32960V2025.SuperCapacitor(systemNo, totalVoltage, totalCurrent, cells, temps); } } diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/VehicleV2025BlockParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/VehicleV2025BlockParser.java index 5721bb71..d36348e6 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/VehicleV2025BlockParser.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/parser/v2025/VehicleV2025BlockParser.java @@ -30,8 +30,8 @@ public final class VehicleV2025BlockParser implements InfoBlockParser { Integer dcDcStatus = ValueDecoder.u8(buf); Integer gearRaw = ValueDecoder.u8raw(buf); Integer insulation = ValueDecoder.u16(buf); - return new InfoBlock.Vehicle(vehicleState, chargingState, runningMode, speedKmh, mileageKm, - totalVoltageV, totalCurrentA, soc, dcDcStatus, gearRaw, insulation, - null, null); + return new InfoBlock.Gb32960V2025.Vehicle(vehicleState, chargingState, runningMode, + speedKmh, mileageKm, totalVoltageV, totalCurrentA, soc, dcDcStatus, gearRaw, + insulation); } } 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 index 68a1c18b..67a822d9 100644 --- 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 @@ -29,6 +29,6 @@ public final class GdFcAirConditionerBlockParser implements InfoBlockParser { 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); + return new InfoBlock.GuangdongFc.AirConditioner(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 index 09dd0fd8..919a6ee9 100644 --- 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 @@ -36,7 +36,7 @@ public final class GdFcAuxiliaryBlockParser implements InfoBlockParser { @Override public InfoBlock parse(ByteBuffer buf) { int count = buf.get() & 0xFF; - List subs = new ArrayList<>(count); + List subs = new ArrayList<>(count); for (int i = 0; i < count; i++) { Double airCompV = ValueDecoder.scaledU16(buf, 0.1); Integer airCompP = readPowerOffsetMinus5(buf); @@ -46,10 +46,10 @@ public final class GdFcAuxiliaryBlockParser implements InfoBlockParser { Double ptcV = ValueDecoder.scaledU16(buf, 0.1); Integer ptcP = readPowerOffsetMinus5(buf); Double lvBattV = ValueDecoder.scaledU16(buf, 0.1); - subs.add(new InfoBlock.GdFcAuxiliary.Subsystem( + subs.add(new InfoBlock.GuangdongFc.Auxiliary.Subsystem( airCompV, airCompP, h2PumpV, h2PumpP, waterPumpV, ptcV, ptcP, lvBattV)); } - return new InfoBlock.GdFcAuxiliary(count, subs); + return new InfoBlock.GuangdongFc.Auxiliary(count, subs); } /** 功率字段:raw(2B) - 5,0xFFFE/0xFFFF 异常/无效。 */ 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 index 3d841392..71f7c170 100644 --- 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 @@ -32,6 +32,6 @@ public final class GdFcDcDcBlockParser implements InfoBlockParser { 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); + return new InfoBlock.GuangdongFc.DcDc(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 index bb5a7655..8457d615 100644 --- 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 @@ -42,6 +42,6 @@ public final class GdFcDemoExtensionBlockParser implements InfoBlockParser { 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); + return new InfoBlock.GuangdongFc.DemoExtension(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 index 7c8caaf5..d597897e 100644 --- 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 @@ -45,7 +45,7 @@ public final class GdFcStackBlockParser implements InfoBlockParser { @Override public InfoBlock parse(ByteBuffer buf) { int stackCount = buf.get() & 0xFF; - List stacks = new ArrayList<>(stackCount); + List stacks = new ArrayList<>(stackCount); for (int i = 0; i < stackCount; i++) { Integer workState = ValueDecoder.u8raw(buf); Integer waterTemp = ValueDecoder.tempOffset40(buf); @@ -65,11 +65,11 @@ public final class GdFcStackBlockParser implements InfoBlockParser { for (int k = 0; k < safe; k++) { frameVoltages.add((buf.getShort() & 0xFFFF) * 0.001); } - stacks.add(new InfoBlock.GdFcStack.Stack( + stacks.add(new InfoBlock.GuangdongFc.Stack.Item( workState, waterTemp, h2InletP, airInletP, airInletTemp, maxId, minId, maxV, minV, avgV, cellCount, frameStart, frameCells, frameVoltages)); } - return new InfoBlock.GdFcStack(stackCount, stacks); + return new InfoBlock.GuangdongFc.Stack(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 index 955bed01..50ffec99 100644 --- 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 @@ -31,6 +31,6 @@ public final class GdFcVehicleInfoBlockParser implements InfoBlockParser { 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); + return new InfoBlock.GuangdongFc.VehicleInfo(collision, tempC, pressure, h2Mass); } } diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/mapper/Gb32960EventMapper.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/mapper/Gb32960EventMapper.java index ce1a0a65..9576af0f 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/mapper/Gb32960EventMapper.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/mapper/Gb32960EventMapper.java @@ -102,11 +102,18 @@ public final class Gb32960EventMapper implements EventMapper { Map meta, Instant eventTime, Instant ingestTime, List out) { - InfoBlock.Vehicle v = message.findBlock(InfoBlock.Vehicle.class).orElse(null); - InfoBlock.Position p = message.findBlock(InfoBlock.Position.class).orElse(null); - InfoBlock.FuelCellV2016 fc16 = message.findBlock(InfoBlock.FuelCellV2016.class).orElse(null); - InfoBlock.FuelCellV2025 fc25 = message.findBlock(InfoBlock.FuelCellV2025.class).orElse(null); - InfoBlock.Alarm alarm = message.findBlock(InfoBlock.Alarm.class).orElse(null); + // 整车数据:先查 V2016,再 V2025;每帧只会命中其一,extracted 出来变成统一 + // 视图(VehicleView)供后面 payload 构造使用 + VehicleView v = extractVehicleView(message); + // 位置数据:同样两版本都查 + PositionView p = extractPositionView(message); + // 燃料电池:V2016 字段更全(带电压/电流),V2025 字段更少 + InfoBlock.Gb32960V2016.FuelCell fc16 = + message.findBlock(InfoBlock.Gb32960V2016.FuelCell.class).orElse(null); + InfoBlock.Gb32960V2025.FuelCell fc25 = + message.findBlock(InfoBlock.Gb32960V2025.FuelCell.class).orElse(null); + // 报警 + AlarmView alarm = extractAlarmView(message); if (v != null) { Double fcVoltageV = fc16 != null ? fc16.fcVoltageV() : null; @@ -119,25 +126,25 @@ public final class Gb32960EventMapper implements EventMapper { ? fc25.remainingHydrogenPercent().doubleValue() : null; RealtimePayload payload = new RealtimePayload( - v.speedKmh(), - v.totalMileageKm(), - v.socPercent() != null ? v.socPercent().doubleValue() : null, - v.totalVoltageV(), - v.totalCurrentA(), + v.speedKmh, + v.totalMileageKm, + v.socPercent != null ? v.socPercent.doubleValue() : null, + v.totalVoltageV, + v.totalCurrentA, fcVoltageV, fcCurrentA, fcTempC, hydrogenRemaining, hydrogenHighPressure, null, - mapVehicleState(v.vehicleState()), - mapChargingState(v.chargingState()), - mapRunningMode(v.runningMode()), - extractGearLevel(v.gearRaw()), - v.acceleratorPedal() != null ? v.acceleratorPedal().doubleValue() : null, - v.brakePedal() != null ? v.brakePedal().doubleValue() : null, - p != null ? p.longitude() : null, - p != null ? p.latitude() : null, + mapVehicleState(v.vehicleState), + mapChargingState(v.chargingState), + mapRunningMode(v.runningMode), + extractGearLevel(v.gearRaw), + v.acceleratorPedal != null ? v.acceleratorPedal.doubleValue() : null, + v.brakePedal != null ? v.brakePedal.doubleValue() : null, + p != null ? p.longitude : null, + p != null ? p.latitude : null, null, null, null, @@ -149,31 +156,31 @@ public final class Gb32960EventMapper implements EventMapper { } if (p != null) { LocationPayload loc = new LocationPayload( - p.longitude(), p.latitude(), 0.0, - v != null && v.speedKmh() != null ? v.speedKmh() : 0.0, - 0.0, 0, p.statusFlag()); + p.longitude, p.latitude, 0.0, + v != null && v.speedKmh != null ? v.speedKmh : 0.0, + 0.0, 0, p.statusFlag); out.add(new VehicleEvent.Location( UUID.randomUUID().toString(), header.vin(), ProtocolId.GB32960, eventTime, ingestTime, null, meta, loc)); } - if (alarm != null && alarm.maxLevel() != null && alarm.maxLevel() > 0) { + if (alarm != null && alarm.maxLevel != null && alarm.maxLevel > 0) { List faultCodes = new ArrayList<>(); - addFaultCodes(faultCodes, "BAT", alarm.batteryFaults()); - addFaultCodes(faultCodes, "MOT", alarm.motorFaults()); - addFaultCodes(faultCodes, "ENG", alarm.engineFaults()); - addFaultCodes(faultCodes, "OTH", alarm.otherFaults()); - Set activeBits = GeneralAlarmFlagBit.parse(alarm.generalAlarmFlag()).stream() + addFaultCodes(faultCodes, "BAT", alarm.batteryFaults); + addFaultCodes(faultCodes, "MOT", alarm.motorFaults); + addFaultCodes(faultCodes, "ENG", alarm.engineFaults); + addFaultCodes(faultCodes, "OTH", alarm.otherFaults); + Set activeBits = GeneralAlarmFlagBit.parse(alarm.generalAlarmFlag).stream() .map(Enum::name) .collect(Collectors.toCollection(java.util.LinkedHashSet::new)); AlarmPayload ap = new AlarmPayload( - mapAlarmLevel(alarm.maxLevel()), - (int) (alarm.generalAlarmFlag() & 0xFFFF), + mapAlarmLevel(alarm.maxLevel), + (int) (alarm.generalAlarmFlag & 0xFFFF), "GB32960_ALARM", faultCodes, activeBits, - p != null ? p.longitude() : null, - p != null ? p.latitude() : null); + p != null ? p.longitude : null, + p != null ? p.latitude : null); out.add(new VehicleEvent.Alarm( UUID.randomUUID().toString(), header.vin(), ProtocolId.GB32960, eventTime, ingestTime, @@ -181,6 +188,61 @@ public final class Gb32960EventMapper implements EventMapper { } } + /** 跨版本的整车数据视图,把 V2016 的踏板字段(V2025 没有)放在一起。 */ + private record VehicleView( + Integer vehicleState, Integer chargingState, Integer runningMode, + Double speedKmh, Double totalMileageKm, Double totalVoltageV, Double totalCurrentA, + Integer socPercent, Integer gearRaw, + Integer acceleratorPedal, Integer brakePedal) {} + + private static VehicleView extractVehicleView(Gb32960Message m) { + var v16 = m.findBlock(InfoBlock.Gb32960V2016.Vehicle.class).orElse(null); + if (v16 != null) { + return new VehicleView(v16.vehicleState(), v16.chargingState(), v16.runningMode(), + v16.speedKmh(), v16.totalMileageKm(), v16.totalVoltageV(), v16.totalCurrentA(), + v16.socPercent(), v16.gearRaw(), + v16.acceleratorPedal(), v16.brakePedal()); + } + var v25 = m.findBlock(InfoBlock.Gb32960V2025.Vehicle.class).orElse(null); + if (v25 != null) { + return new VehicleView(v25.vehicleState(), v25.chargingState(), v25.runningMode(), + v25.speedKmh(), v25.totalMileageKm(), v25.totalVoltageV(), v25.totalCurrentA(), + v25.socPercent(), v25.gearRaw(), + null, null); + } + return null; + } + + /** 跨版本的位置数据视图。 */ + private record PositionView(int statusFlag, double longitude, double latitude) {} + + private static PositionView extractPositionView(Gb32960Message m) { + var p16 = m.findBlock(InfoBlock.Gb32960V2016.Position.class).orElse(null); + if (p16 != null) return new PositionView(p16.statusFlag(), p16.longitude(), p16.latitude()); + var p25 = m.findBlock(InfoBlock.Gb32960V2025.Position.class).orElse(null); + if (p25 != null) return new PositionView(p25.statusFlag(), p25.longitude(), p25.latitude()); + return null; + } + + /** 跨版本的报警视图(仅暴露公共字段,2025 的 generalAlarmLevels 暂未上送下游)。 */ + private record AlarmView(Integer maxLevel, long generalAlarmFlag, + List batteryFaults, List motorFaults, + List engineFaults, List otherFaults) {} + + private static AlarmView extractAlarmView(Gb32960Message m) { + var a16 = m.findBlock(InfoBlock.Gb32960V2016.Alarm.class).orElse(null); + if (a16 != null) { + return new AlarmView(a16.maxLevel(), a16.generalAlarmFlag(), + a16.batteryFaults(), a16.motorFaults(), a16.engineFaults(), a16.otherFaults()); + } + var a25 = m.findBlock(InfoBlock.Gb32960V2025.Alarm.class).orElse(null); + if (a25 != null) { + return new AlarmView(a25.maxLevel(), a25.generalAlarmFlag(), + a25.batteryFaults(), a25.motorFaults(), a25.engineFaults(), a25.otherFaults()); + } + return null; + } + /** 挡位字节按附录 A.1 解析:低 4 位为挡位值 0000-1111。 */ private static Integer extractGearLevel(Integer gearRaw) { if (gearRaw == null) return null; 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 a36ee03a..138e02bc 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 @@ -3,498 +3,429 @@ package com.lingniu.ingest.protocol.gb32960.model; import java.util.List; /** - * 已解析的信息体基接口。每种语义对应一种 record 实现。 + * 已解析的信息体基接口。按 {@code <协议+版本>.<数据类型>} 严格分组: * - *

规范来源:GB/T 32960.3-2016 附录 B 表 B.10-B.17,GB/T 32960.3-2025 §7.2.4 表 10-27。 - * 字段命名使用规范中的中文直译以便对照查阅。 + *

    + *
  • {@link Gb32960V2016} —— GB/T 32960.3-2016 附录 B 表 B.10~B.20 + *
  • {@link Gb32960V2025} —— GB/T 32960.3-2025 §7.2.4 表 10~27 + *
  • {@link GuangdongFc} —— 广东燃料电池汽车示范应用城市群综合监管平台规范 v1.0 + *
  • {@link Raw} —— 未实现 parser 的原始字节兜底 + *
+ * + *

哪怕同一个数据类型在两个版本字段一致(如 Engine 5 字节固定布局),也按规则 + * 复制成两份 record,绝不跨版本共用 record 类型,以便消费方在 instanceof / switch + * 时能严格区分数据来源。 * *

异常值约定: *

    - *
  • 单字节:{@code 0xFE} 异常、{@code 0xFF} 无效 - *
  • 双字节:{@code 0xFFFE} 异常、{@code 0xFFFF} 无效 - *
  • 四字节:{@code 0xFFFFFFFE} 异常、{@code 0xFFFFFFFF} 无效 + *
  • 单字节:{@code 0xFE} 异常 / {@code 0xFF} 无效 + *
  • 双字节:{@code 0xFFFE} 异常 / {@code 0xFFFF} 无效 + *
  • 四字节:{@code 0xFFFFFFFE} 异常 / {@code 0xFFFFFFFF} 无效 *
- * 对外 record 使用 {@link Double}/{@link Integer} 等装箱类型,异常或无效时设为 {@code null}。 + * 对外 record 使用 {@link Double} / {@link Integer} 等装箱类型,异常或无效时设为 + * {@code null}。 */ public sealed interface InfoBlock - permits InfoBlock.Vehicle, - InfoBlock.DriveMotor, - InfoBlock.FuelCellV2016, - InfoBlock.FuelCellV2025, - InfoBlock.Engine, - InfoBlock.Position, - InfoBlock.Extreme, - InfoBlock.Alarm, - InfoBlock.Voltage, - InfoBlock.Temperature, - InfoBlock.MinParallelVoltage, - InfoBlock.BatteryTemperature, - InfoBlock.FuelCellStack, - InfoBlock.SuperCapacitor, - InfoBlock.SuperCapacitorExtreme, - InfoBlock.GdFcStack, - InfoBlock.GdFcAuxiliary, - InfoBlock.GdFcDcDc, - InfoBlock.GdFcAirConditioner, - InfoBlock.GdFcVehicleInfo, - InfoBlock.GdFcDemoExtension, + permits InfoBlock.Gb32960V2016, + InfoBlock.Gb32960V2025, + InfoBlock.GuangdongFc, InfoBlock.Raw { InfoBlockType type(); // ======================================================================== - // 0x01 整车数据 —— 2016/2025 共享(表 10) + // GB/T 32960.3-2016 // ======================================================================== - /** - * 整车数据。固定 21 字节(2025 版)/ 20 字节(2016 版,无 brake)。 - * - * @param vehicleState 车辆状态:0x01 启动 / 0x02 熄火 / 0x03 其他 - * @param chargingState 充电状态:0x01 停车充电 / 0x02 行驶充电 / 0x03 未充电 / 0x04 充电完成 - * @param runningMode 运行模式:0x01 纯电 / 0x02 混动 / 0x03 燃油 - * @param speedKmh 车速 km/h,0~500,最小计量 0.1 km/h - * @param totalMileageKm 累计里程 km,0~999999.9,最小计量 0.1 km - * @param totalVoltageV 总电压 V,0~6000,最小计量 0.1 V - * @param totalCurrentA 总电流 A,-3000~+3000(原始值偏移 3000 A),最小计量 0.1 A - * @param socPercent SOC 百分比,0~100 - * @param dcDcStatus DC-DC 状态:0x01 工作 / 0x02 断开 - * @param gearRaw 挡位原始字节,按附录 A.1 解析 - * @param insulationResistanceKohm 高压对地绝缘电阻 kΩ,0~60000 - * @param acceleratorPedal 加速踏板行程值 0~100(仅 2016 版) - * @param brakePedal 制动踏板状态 0~100(仅 2016 版) - */ - record Vehicle( - Integer vehicleState, - Integer chargingState, - Integer runningMode, - Double speedKmh, - Double totalMileageKm, - Double totalVoltageV, - Double totalCurrentA, - Integer socPercent, - Integer dcDcStatus, - Integer gearRaw, - Integer insulationResistanceKohm, - Integer acceleratorPedal, - Integer brakePedal - ) implements InfoBlock { - @Override public InfoBlockType type() { return InfoBlockType.VEHICLE; } - } + /** GB/T 32960.3-2016 附录 B 定义的全部信息体类型。 */ + sealed interface Gb32960V2016 extends InfoBlock + permits Gb32960V2016.Vehicle, + Gb32960V2016.DriveMotor, + Gb32960V2016.FuelCell, + Gb32960V2016.Engine, + Gb32960V2016.Position, + Gb32960V2016.Extreme, + Gb32960V2016.Alarm, + Gb32960V2016.Voltage, + Gb32960V2016.Temperature { - // ======================================================================== - // 0x02 驱动电机数据 —— 表 15/16 - // ======================================================================== + /** 0x01 整车数据。固定 20 字节(含加速/制动踏板)。 */ + record Vehicle( + Integer vehicleState, + Integer chargingState, + Integer runningMode, + Double speedKmh, + Double totalMileageKm, + Double totalVoltageV, + Double totalCurrentA, + Integer socPercent, + Integer dcDcStatus, + Integer gearRaw, + Integer insulationResistanceKohm, + Integer acceleratorPedal, + Integer brakePedal + ) implements Gb32960V2016 { + @Override public InfoBlockType type() { return InfoBlockType.VEHICLE; } + } - /** 驱动电机数据。变长:1 字节电机个数 + N × 电机信息。 */ - record DriveMotor(List motors) implements InfoBlock { - @Override public InfoBlockType type() { return InfoBlockType.DRIVE_MOTOR; } + /** 0x02 驱动电机数据。变长。每电机 12 字节(rpm 偏移 20000,torque 2 字节偏移 20000)。 */ + record DriveMotor(List motors) implements Gb32960V2016 { + @Override public InfoBlockType type() { return InfoBlockType.DRIVE_MOTOR; } - /** - * 单个驱动电机数据。 - * - *

2016 版字段大小(表 B.16):转速 2 字节偏移 20000,转矩 2 字节偏移 20000 单位 0.1 N·m,共 12 字节。 - *

2025 版字段大小(表 16):转速 2 字节偏移 32000,转矩 4 字节偏移 20000 N·m 单位 0.1 N·m,共 14 字节。 - * - * @param serialNo 电机顺序号 1~253 - * @param state 电机状态:0x01 耗电 / 0x02 发电 / 0x03 关闭 / 0x04 准备 - * @param controllerTempC 电机控制器温度 ℃,-40~+210 - * @param rpm 电机转速 r/min - * @param torqueNm 电机转矩 N·m,最小计量 0.1 N·m - * @param motorTempC 电机温度 ℃,-40~+210 - * @param controllerInputVoltageV 电机控制器输入电压 V,0~6000,最小计量 0.1 V - * @param controllerDcCurrentA 电机控制器直流母线电流 A,-1000~+5000,最小计量 0.1 A - */ - public record Motor( - Integer serialNo, - Integer state, - Integer controllerTempC, - Integer rpm, - Double torqueNm, - Integer motorTempC, - Double controllerInputVoltageV, - Double controllerDcCurrentA - ) {} - } + public record Motor( + Integer serialNo, + Integer state, + Integer controllerTempC, + Integer rpm, + Double torqueNm, + Integer motorTempC, + Double controllerInputVoltageV, + Double controllerDcCurrentA + ) {} + } - // ======================================================================== - // 0x03 燃料电池数据(2016 版) - // ======================================================================== + /** 0x03 燃料电池数据(2016 版字段顺序,含 N×探针温度 1B)。变长。 */ + record FuelCell( + Double fcVoltageV, + Double fcCurrentA, + Double hydrogenConsumptionKgPer100km, + List probeTempC, + Double maxHydrogenSystemTempC, + Integer maxHydrogenSystemTempProbeId, + Double maxHydrogenConcentrationPercent, + Integer maxHydrogenConcentrationProbeId, + Double maxHydrogenPressureMpa, + Integer maxHydrogenPressureProbeId, + Integer hvDcDcStatus + ) implements Gb32960V2016 { + @Override public InfoBlockType type() { return InfoBlockType.FUEL_CELL_V2016; } + } - /** - * 燃料电池数据(2016 版)。变长。 - * - *

2016 版字段顺序:燃料电池电压(2) + 电流(2) + 氢气消耗率(2) + 探针数(2) - * + N×探针温度(1) + 最高温度(2) + 最高温度探针代号(1) + 最高浓度(2) + 最高浓度代号(1) - * + 最高压力(2) + 最高压力代号(1) + DCDC 状态(1)。 - */ - record FuelCellV2016( - Double fcVoltageV, - Double fcCurrentA, - Double hydrogenConsumptionKgPer100km, - List probeTempC, - Double maxHydrogenSystemTempC, - Integer maxHydrogenSystemTempProbeId, - Double maxHydrogenConcentrationPercent, - Integer maxHydrogenConcentrationProbeId, - Double maxHydrogenPressureMpa, - Integer maxHydrogenPressureProbeId, - Integer hvDcDcStatus - ) implements InfoBlock { - @Override public InfoBlockType type() { return InfoBlockType.FUEL_CELL_V2016; } - } + /** 0x04 发动机数据。固定 5 字节。 */ + record Engine( + Integer engineState, + Integer crankshaftRpm, + Double fuelConsumptionRate + ) implements Gb32960V2016 { + @Override public InfoBlockType type() { return InfoBlockType.ENGINE; } + } - // ======================================================================== - // 0x03 燃料电池发动机及车载氢系统数据(2025 版,表 17) - // ======================================================================== + /** 0x05 车辆位置数据。固定 9 字节。 */ + record Position( + int statusFlag, + double longitude, + double latitude + ) implements Gb32960V2016 { + @Override public InfoBlockType type() { return InfoBlockType.POSITION_V2016; } + } - /** - * 燃料电池发动机及车载氢系统数据(2025 版)。固定 13 字节。 - * - *

较 2016 版的差异(见 2025 规范前言 o): - *

    - *
  • 删除:燃料电池电压、燃料电池电流、氢气消耗率、温度探针列表 - *
  • 新增:剩余氢量百分比、高压 DC/DC 控制器温度 - *
  • 温度探针数据迁移至 0x30 燃料电池电堆 - *
- */ - record FuelCellV2025( - Double maxHydrogenSystemTempC, - Integer maxHydrogenSystemTempProbeId, - Double maxHydrogenConcentrationPercent, - Integer maxHydrogenConcentrationProbeId, - Double maxHydrogenPressureMpa, - Integer maxHydrogenPressureProbeId, - Integer hvDcDcStatus, - Integer remainingHydrogenPercent, - Integer hvDcDcControllerTempC - ) implements InfoBlock { - @Override public InfoBlockType type() { return InfoBlockType.FUEL_CELL_V2025; } - } + /** 0x06 动力蓄电池极值数据。固定 14 字节。 */ + record Extreme( + Integer maxCellVoltageSubSysNo, + Integer maxCellVoltageCellId, + Double maxCellVoltageV, + Integer minCellVoltageSubSysNo, + Integer minCellVoltageCellId, + Double minCellVoltageV, + Integer maxTempSubSysNo, + Integer maxTempProbeId, + Integer maxTempC, + Integer minTempSubSysNo, + Integer minTempProbeId, + Integer minTempC + ) implements Gb32960V2016 { + @Override public InfoBlockType type() { return InfoBlockType.EXTREME_V2016; } + } - // ======================================================================== - // 0x04 发动机数据 —— 表 20 - // ======================================================================== + /** 0x07 报警数据。变长(4 个故障代码列表)。 */ + record Alarm( + Integer maxLevel, + long generalAlarmFlag, + List batteryFaults, + List motorFaults, + List engineFaults, + List otherFaults + ) implements Gb32960V2016 { + @Override public InfoBlockType type() { return InfoBlockType.ALARM_V2016; } + } - /** - * 发动机数据。固定 5 字节。 - * - * @param engineState 发动机状态:0x01 启动 / 0x02 关闭 - * @param crankshaftRpm 曲轴转速 r/min,0~60000 - * @param fuelConsumptionRate 燃料消耗率 L/100km,0~600,最小计量 0.01 - */ - record Engine( - Integer engineState, - Integer crankshaftRpm, - Double fuelConsumptionRate - ) implements InfoBlock { - @Override public InfoBlockType type() { return InfoBlockType.ENGINE; } - } + /** 0x08 可充电储能装置电压数据。变长。PoC 仅保留汇总。 */ + record Voltage( + int subSystemCount, + double maxCellVoltageV, + double minCellVoltageV, + double totalVoltageV + ) implements Gb32960V2016 { + @Override public InfoBlockType type() { return InfoBlockType.VOLTAGE_V2016; } + } - // ======================================================================== - // 0x05 车辆位置数据 —— 表 21 - // ======================================================================== - - /** - * 车辆位置数据。 - * - *

2016 版固定 9 字节:{@code 状态位(1) + 经度(4) + 纬度(4)}。 - *

2025 版固定 10 字节:{@code 状态位(1) + 坐标系(1) + 经度(4) + 纬度(4)}。 - * - * @param statusFlag 状态位:bit0 0=有效/1=无效;bit1 0=北纬/1=南纬;bit2 0=东经/1=西经 - * @param coordSystem 坐标系(2025 新增):0x01 WGS84 / 0x02 GCJ-02 / 0x03 其他;2016 版为 null - * @param longitude 经度(十进制度) - * @param latitude 纬度(十进制度) - */ - record Position( - int statusFlag, - Integer coordSystem, - double longitude, - double latitude - ) implements InfoBlock { - @Override public InfoBlockType type() { - return coordSystem == null ? InfoBlockType.POSITION_V2016 : InfoBlockType.POSITION_V2025; + /** 0x09 可充电储能装置温度数据。变长。 */ + record Temperature( + int subSystemCount, + int maxTempC, + int minTempC + ) implements Gb32960V2016 { + @Override public InfoBlockType type() { return InfoBlockType.TEMPERATURE_V2016; } } } // ======================================================================== - // 0x06 动力蓄电池极值数据 —— 仅 2016 版 + // GB/T 32960.3-2025 // ======================================================================== - /** 动力蓄电池极值数据(2016 版)。固定 14 字节。2025 版已删除。 */ - record Extreme( - Integer maxCellVoltageSubSysNo, - Integer maxCellVoltageCellId, - Double maxCellVoltageV, - Integer minCellVoltageSubSysNo, - Integer minCellVoltageCellId, - Double minCellVoltageV, - Integer maxTempSubSysNo, - Integer maxTempProbeId, - Integer maxTempC, - Integer minTempSubSysNo, - Integer minTempProbeId, - Integer minTempC - ) implements InfoBlock { - @Override public InfoBlockType type() { return InfoBlockType.EXTREME_V2016; } - } + /** GB/T 32960.3-2025 §7.2.4 定义的全部信息体类型。 */ + sealed interface Gb32960V2025 extends InfoBlock + permits Gb32960V2025.Vehicle, + Gb32960V2025.DriveMotor, + Gb32960V2025.FuelCell, + Gb32960V2025.Engine, + Gb32960V2025.Position, + Gb32960V2025.Alarm, + Gb32960V2025.MinParallelVoltage, + Gb32960V2025.BatteryTemperature, + Gb32960V2025.FuelCellStack, + Gb32960V2025.SuperCapacitor, + Gb32960V2025.SuperCapacitorExtreme { - // ======================================================================== - // 报警数据 —— 2016 的 0x07 / 2025 的 0x06(表 23) - // ======================================================================== - - /** - * 报警数据。 - * - *

2016 版 typeCode=0x07,2025 版 typeCode=0x06。前半部分(maxLevel + generalAlarmFlag - * + 4 类故障代码列表)一致,2025 版在尾部额外增加"通用报警故障等级列表"。 - * - * @param protocolVersion 协议版本,决定 typeCode 映射 - * @param maxLevel 最高报警等级 0~4(0 无故障;4 热事件为最高级) - * @param generalAlarmFlag 通用报警标志(表 24 的 28 个位) - * @param batteryFaults 可充电储能装置故障代码列表 - * @param motorFaults 驱动电机故障代码列表 - * @param engineFaults 发动机故障代码列表 - * @param otherFaults 其他故障代码列表 - * @param generalAlarmLevels 2025 新增:{bit, level} 列表;2016 版为 null 或空列表 - */ - record Alarm( - ProtocolVersion protocolVersion, - Integer maxLevel, - long generalAlarmFlag, - List batteryFaults, - List motorFaults, - List engineFaults, - List otherFaults, - List generalAlarmLevels - ) implements InfoBlock { - @Override public InfoBlockType type() { - return protocolVersion == ProtocolVersion.V2025 - ? InfoBlockType.ALARM_V2025 : InfoBlockType.ALARM_V2016; + /** 0x01 整车数据。固定 18 字节(删除加速/制动踏板)。 */ + record Vehicle( + Integer vehicleState, + Integer chargingState, + Integer runningMode, + Double speedKmh, + Double totalMileageKm, + Double totalVoltageV, + Double totalCurrentA, + Integer socPercent, + Integer dcDcStatus, + Integer gearRaw, + Integer insulationResistanceKohm + ) implements Gb32960V2025 { + @Override public InfoBlockType type() { return InfoBlockType.VEHICLE; } } - /** 2025 版通用报警故障等级列表项(2 字节):bit 对应表 24 位序号,level 为故障等级。 */ - public record GeneralAlarmEntry(int bit, int level) {} + /** 0x02 驱动电机数据。变长。每电机 14 字节(rpm 偏移 32000,torque 4 字节偏移 20000)。 */ + record DriveMotor(List motors) implements Gb32960V2025 { + @Override public InfoBlockType type() { return InfoBlockType.DRIVE_MOTOR; } + + public record Motor( + Integer serialNo, + Integer state, + Integer controllerTempC, + Integer rpm, + Double torqueNm, + Integer motorTempC, + Double controllerInputVoltageV, + Double controllerDcCurrentA + ) {} + } + + /** 0x03 燃料电池发动机及车载氢系统数据。固定 13 字节。 */ + record FuelCell( + Double maxHydrogenSystemTempC, + Integer maxHydrogenSystemTempProbeId, + Double maxHydrogenConcentrationPercent, + Integer maxHydrogenConcentrationProbeId, + Double maxHydrogenPressureMpa, + Integer maxHydrogenPressureProbeId, + Integer hvDcDcStatus, + Integer remainingHydrogenPercent, + Integer hvDcDcControllerTempC + ) implements Gb32960V2025 { + @Override public InfoBlockType type() { return InfoBlockType.FUEL_CELL_V2025; } + } + + /** 0x04 发动机数据。固定 5 字节。字段同 2016 版。 */ + record Engine( + Integer engineState, + Integer crankshaftRpm, + Double fuelConsumptionRate + ) implements Gb32960V2025 { + @Override public InfoBlockType type() { return InfoBlockType.ENGINE; } + } + + /** 0x05 车辆位置数据。固定 10 字节(多坐标系字节)。 */ + record Position( + int statusFlag, + int coordSystem, + double longitude, + double latitude + ) implements Gb32960V2025 { + @Override public InfoBlockType type() { return InfoBlockType.POSITION_V2025; } + } + + /** 0x06 报警数据。变长(4 个故障代码列表 + 通用报警故障等级列表)。 */ + record Alarm( + Integer maxLevel, + long generalAlarmFlag, + List batteryFaults, + List motorFaults, + List engineFaults, + List otherFaults, + List generalAlarmLevels + ) implements Gb32960V2025 { + @Override public InfoBlockType type() { return InfoBlockType.ALARM_V2025; } + + /** 通用报警故障等级列表项(2 字节):bit 对应表 24 位序号,level 为故障等级。 */ + public record GeneralAlarmEntry(int bit, int level) {} + } + + /** 0x07 动力蓄电池最小并联单元电压数据。变长。 */ + record MinParallelVoltage(int subSystemCount, List packs) implements Gb32960V2025 { + @Override public InfoBlockType type() { return InfoBlockType.MIN_PARALLEL_VOLTAGE_V2025; } + + public record Pack( + int packNo, + Double packVoltageV, + Double packCurrentA, + int totalUnits, + List frameVoltages + ) {} + } + + /** 0x08 动力蓄电池温度数据。变长。 */ + record BatteryTemperature(int subSystemCount, List packs) implements Gb32960V2025 { + @Override public InfoBlockType type() { return InfoBlockType.BATTERY_TEMPERATURE_V2025; } + + public record Pack(int packNo, int probeCount, List probeTempsC) {} + } + + /** 0x30 燃料电池电堆数据。变长(2025 标准)。 */ + record FuelCellStack(int stackCount, List stacks) implements Gb32960V2025 { + @Override public InfoBlockType type() { return InfoBlockType.FUEL_CELL_STACK; } + + public record Stack( + int stackNo, + Double voltageV, + Double currentA, + Double hydrogenInletPressureKpa, + Double airInletPressureKpa, + Integer airInletTempC, + List coolantOutletProbeTempsC + ) {} + } + + /** 0x31 超级电容器数据。变长。 */ + record SuperCapacitor( + int systemNo, + Double totalVoltageV, + Double totalCurrentA, + List cellVoltages, + List probeTempsC + ) implements Gb32960V2025 { + @Override public InfoBlockType type() { return InfoBlockType.SUPER_CAPACITOR; } + } + + /** 0x32 超级电容器极值数据。固定 18 字节。 */ + record SuperCapacitorExtreme( + Integer maxVoltageSystemNo, + Integer maxVoltageCellId, + Double maxCellVoltageV, + Integer minVoltageSystemNo, + Integer minVoltageCellId, + Double minCellVoltageV, + Integer maxTempSystemNo, + Integer maxTempProbeId, + Integer maxTempC, + Integer minTempSystemNo, + Integer minTempProbeId, + Integer minTempC + ) implements Gb32960V2025 { + @Override public InfoBlockType type() { return InfoBlockType.SUPER_CAPACITOR_EXTREME; } + } } // ======================================================================== - // 2016 版 0x08 可充电储能装置电压数据 + // 广东燃料电池汽车示范应用城市群综合监管平台规范 v1.0(基于 GB/T 32960.3-2016 帧头) // ======================================================================== - /** 可充电储能装置电压数据(2016 版)。变长。PoC 仅保留汇总。 */ - record Voltage( - int subSystemCount, - double maxCellVoltageV, - double minCellVoltageV, - double totalVoltageV - ) implements InfoBlock { - @Override public InfoBlockType type() { return InfoBlockType.VOLTAGE_V2016; } - } + /** 广东燃料电池汽车示范应用规范定义的扩展信息体(typeCode 0x30~0x34、0x80)。 */ + sealed interface GuangdongFc extends InfoBlock + permits GuangdongFc.Stack, + GuangdongFc.Auxiliary, + GuangdongFc.DcDc, + GuangdongFc.AirConditioner, + GuangdongFc.VehicleInfo, + GuangdongFc.DemoExtension { - // ======================================================================== - // 2016 版 0x09 可充电储能装置温度数据 - // ======================================================================== + /** 0x30 燃料电池电堆数据(§7.2.3.4 表 13/14)。变长。 */ + record Stack(int stackCount, List stacks) implements GuangdongFc { + @Override public InfoBlockType type() { return InfoBlockType.GD_FC_STACK; } - /** 可充电储能装置温度数据(2016 版)。变长。 */ - record Temperature( - int subSystemCount, - int maxTempC, - int minTempC - ) implements InfoBlock { - @Override public InfoBlockType type() { return InfoBlockType.TEMPERATURE_V2016; } - } + public record Item( + Integer engineWorkState, + Integer stackWaterOutletTempC, + Double hydrogenInletPressureKpa, + Double airInletPressureKpa, + Integer airInletTempC, + Integer maxCellVoltageId, + Integer minCellVoltageId, + Double maxCellVoltageV, + Double minCellVoltageV, + Double avgCellVoltageV, + Integer cellCount, + Integer frameCellStart, + Integer frameCellCount, + List frameCellVoltagesV + ) {} + } - // ======================================================================== - // 2025 版 0x07 动力蓄电池最小并联单元电压数据(表 11/12) - // ======================================================================== + /** 0x31 辅助系统数据(§7.2.3.5 表 15/16)。变长。 */ + record Auxiliary(int subSystemCount, List subsystems) implements GuangdongFc { + @Override public InfoBlockType type() { return InfoBlockType.GD_FC_AUXILIARY; } - /** 动力蓄电池最小并联单元电压数据(2025 版)。变长。 */ - record MinParallelVoltage(int subSystemCount, List packs) implements InfoBlock { - @Override public InfoBlockType type() { return InfoBlockType.MIN_PARALLEL_VOLTAGE_V2025; } + public record Subsystem( + Double airCompressorMotorVoltageV, + Integer airCompressorPowerKw, + Double hydrogenPumpMotorVoltageV, + Integer hydrogenPumpPowerKw, + Double waterPumpVoltageV, + Double ptcVoltageV, + Integer ptcPowerKw, + Double lowVoltageBatteryVoltageV + ) {} + } - /** - * @param packNo 电池包号 1~50 - * @param packVoltageV 动力蓄电池包电压 V,0~6000,最小 0.1 V - * @param packCurrentA 动力蓄电池包电流 A,-3000~+3000,最小 0.1 A - * @param totalUnits 最小并联单元总数 1~65531 - * @param frameVoltages 本帧最小并联单元电压列表,0~60 V,最小 0.001 V - */ - public record Pack( - int packNo, - Double packVoltageV, - Double packCurrentA, - int totalUnits, - List frameVoltages - ) {} - } + /** 0x32 DC/DC(燃料电池系统)数据(§7.2.3.6 表 17)。固定 9 字节。 */ + record DcDc( + Double inputVoltageV, + Double inputCurrentA, + Double outputVoltageV, + Double outputCurrentA, + Integer controllerTempC + ) implements GuangdongFc { + @Override public InfoBlockType type() { return InfoBlockType.GD_FC_DCDC; } + } - // ======================================================================== - // 2025 版 0x08 动力蓄电池温度数据(表 13/14) - // ======================================================================== + /** 0x33 空调数据(§7.2.3.7 表 18)。固定 5 字节。 */ + record AirConditioner( + Integer status, + Integer powerKw, + Double compressorInputVoltageV + ) implements GuangdongFc { + @Override public InfoBlockType type() { return InfoBlockType.GD_FC_AIR_CONDITIONER; } + } - /** 动力蓄电池温度数据(2025 版)。变长。 */ - record BatteryTemperature(int subSystemCount, List packs) implements InfoBlock { - @Override public InfoBlockType type() { return InfoBlockType.BATTERY_TEMPERATURE_V2025; } + /** 0x34 车辆信息数据(§7.2.3.8 表 19)。固定 7 字节。 */ + record VehicleInfo( + Integer collisionAlarm, + Integer ambientTempC, + Double ambientPressureKpa, + Double hydrogenMassKg + ) implements GuangdongFc { + @Override public InfoBlockType type() { return InfoBlockType.GD_FC_VEHICLE_INFO; } + } - /** - * @param packNo 电池包号 1~50 - * @param probeCount 温度探针个数 1~65531 - * @param probeTempsC 各探针温度 ℃,-40~+210 - */ - public record Pack(int packNo, int probeCount, List probeTempsC) {} - } - - // ======================================================================== - // 2025 版 0x30 燃料电池电堆数据(表 18/19) - // ======================================================================== - - /** 燃料电池电堆数据(2025 版)。变长:1 字节电堆个数 + N × 电堆信息。 */ - record FuelCellStack(int stackCount, List stacks) implements InfoBlock { - @Override public InfoBlockType type() { return InfoBlockType.FUEL_CELL_STACK; } - - /** - * @param stackNo 电堆数据序号 1~253 - * @param voltageV 燃料电池电堆电压 V,0~2000,最小 0.1 V - * @param currentA 燃料电池电堆电流 A,0~2000,最小 0.1 A - * @param hydrogenInletPressureKpa 氢气入口压力 kPa,-100~+400(偏移 100 kPa),最小 0.1 kPa - * @param airInletPressureKpa 空气入口压力 kPa,-100~+400,最小 0.1 kPa - * @param airInletTempC 空气入口温度 ℃,-40~+210 - * @param coolantOutletProbeTempsC 冷却水出水口温度探针温度列表 ℃ - */ - public record Stack( - int stackNo, - Double voltageV, - Double currentA, - Double hydrogenInletPressureKpa, - Double airInletPressureKpa, - Integer airInletTempC, - List coolantOutletProbeTempsC - ) {} - } - - // ======================================================================== - // 2025 版 0x31 超级电容器数据(表 25) - // ======================================================================== - - /** 超级电容器数据(2025 版)。变长。 */ - record SuperCapacitor( - int systemNo, - Double totalVoltageV, - Double totalCurrentA, - List cellVoltages, - List probeTempsC - ) implements InfoBlock { - @Override public InfoBlockType type() { return InfoBlockType.SUPER_CAPACITOR; } - } - - // ======================================================================== - // 2025 版 0x32 超级电容器极值数据(表 26) - // ======================================================================== - - /** 超级电容器极值数据(2025 版)。固定 14 字节。 */ - record SuperCapacitorExtreme( - Integer maxVoltageSystemNo, - Integer maxVoltageCellId, - Double maxCellVoltageV, - Integer minVoltageSystemNo, - Integer minVoltageCellId, - Double minCellVoltageV, - Integer maxTempSystemNo, - Integer maxTempProbeId, - Integer maxTempC, - Integer minTempSystemNo, - Integer minTempProbeId, - Integer minTempC - ) implements 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; } + /** 0x80 燃料电池汽车示范应用数据扩展(§7.2.3.14 表 27)。 */ + record DemoExtension( + Integer stackTempC, + Double airCompressorVoltageV, + Double airCompressorCurrentA, + Double hydrogenPumpVoltageV, + Double hydrogenPumpCurrentA + ) implements GuangdongFc { + @Override public InfoBlockType type() { return InfoBlockType.GD_FC_DEMO_EXTENSION; } + } } // ======================================================================== diff --git a/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/codec/Gb32960DecoderGoldenTest.java b/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/codec/Gb32960DecoderGoldenTest.java index fcbe1a52..91b92f66 100644 --- a/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/codec/Gb32960DecoderGoldenTest.java +++ b/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/codec/Gb32960DecoderGoldenTest.java @@ -78,7 +78,7 @@ class Gb32960DecoderGoldenTest { .isPresent(); } // 总电流必须落在协议规定的 -1000~+1000 A 区间,防回归到 -3000 偏移 - msg.findBlock(InfoBlock.Vehicle.class).ifPresent(v -> { + msg.findBlock(InfoBlock.Gb32960V2016.Vehicle.class).ifPresent(v -> { if (v.totalCurrentA() != null) { assertThat(v.totalCurrentA()) .as("[%s] vehicle.totalCurrentA 越界,疑似偏移常量回归", name) diff --git a/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/codec/Gb32960DecoderTest.java b/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/codec/Gb32960DecoderTest.java index ca027018..da45e891 100644 --- a/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/codec/Gb32960DecoderTest.java +++ b/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/codec/Gb32960DecoderTest.java @@ -37,12 +37,12 @@ public class Gb32960DecoderTest { assertThat(msg.header().vin()).isEqualTo("LTEST000000000001"); assertThat(msg.header().eventTime()).isNotNull(); - InfoBlock.Vehicle v = msg.findBlock(InfoBlock.Vehicle.class).orElseThrow(); + InfoBlock.Gb32960V2016.Vehicle v = msg.findBlock(InfoBlock.Gb32960V2016.Vehicle.class).orElseThrow(); assertThat(v.socPercent()).isEqualTo(70); assertThat(v.speedKmh()).isEqualTo(52.3, org.assertj.core.data.Offset.offset(0.01)); assertThat(v.gearRaw()).isEqualTo(0x0F); - InfoBlock.Position p = msg.findBlock(InfoBlock.Position.class).orElseThrow(); + InfoBlock.Gb32960V2016.Position p = msg.findBlock(InfoBlock.Gb32960V2016.Position.class).orElseThrow(); assertThat(p.longitude()).isEqualTo(116.397128, org.assertj.core.data.Offset.offset(0.000001)); assertThat(p.latitude()).isEqualTo(39.916527, org.assertj.core.data.Offset.offset(0.000001)); } diff --git a/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/codec/Gb32960FullBlocksTest.java b/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/codec/Gb32960FullBlocksTest.java index 03e40c7c..1620957b 100644 --- a/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/codec/Gb32960FullBlocksTest.java +++ b/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/codec/Gb32960FullBlocksTest.java @@ -48,7 +48,7 @@ class Gb32960FullBlocksTest { writeU16(os, 11000); // current raw → 100 A }); Gb32960Message msg = decoder.decode(ByteBuffer.wrap(frame)); - InfoBlock.DriveMotor dm = msg.findBlock(InfoBlock.DriveMotor.class).orElseThrow(); + InfoBlock.Gb32960V2016.DriveMotor dm = msg.findBlock(InfoBlock.Gb32960V2016.DriveMotor.class).orElseThrow(); assertThat(dm.motors()).hasSize(1); var m = dm.motors().get(0); assertThat(m.rpm()).isEqualTo(3000); @@ -68,7 +68,7 @@ class Gb32960FullBlocksTest { os.write(1); writeU32(os, 0xCAFE_BABEL); // other faults 1 }); Gb32960Message msg = decoder.decode(ByteBuffer.wrap(frame)); - InfoBlock.Alarm a = msg.findBlock(InfoBlock.Alarm.class).orElseThrow(); + InfoBlock.Gb32960V2016.Alarm a = msg.findBlock(InfoBlock.Gb32960V2016.Alarm.class).orElseThrow(); assertThat(a.maxLevel()).isEqualTo(2); assertThat(a.batteryFaults()).hasSize(1); assertThat(a.otherFaults()).hasSize(1); @@ -90,7 +90,7 @@ class Gb32960FullBlocksTest { writeU16(os, 3300); // 3.3V }); Gb32960Message msg = decoder.decode(ByteBuffer.wrap(frame)); - InfoBlock.Voltage v = msg.findBlock(InfoBlock.Voltage.class).orElseThrow(); + InfoBlock.Gb32960V2016.Voltage v = msg.findBlock(InfoBlock.Gb32960V2016.Voltage.class).orElseThrow(); assertThat(v.subSystemCount()).isEqualTo(1); assertThat(v.maxCellVoltageV()).isEqualTo(3.6, org.assertj.core.data.Offset.offset(0.001)); assertThat(v.minCellVoltageV()).isEqualTo(3.3, org.assertj.core.data.Offset.offset(0.001)); @@ -109,7 +109,7 @@ class Gb32960FullBlocksTest { os.write(55); // actual 15 }); Gb32960Message msg = decoder.decode(ByteBuffer.wrap(frame)); - InfoBlock.Temperature t = msg.findBlock(InfoBlock.Temperature.class).orElseThrow(); + InfoBlock.Gb32960V2016.Temperature t = msg.findBlock(InfoBlock.Gb32960V2016.Temperature.class).orElseThrow(); assertThat(t.maxTempC()).isEqualTo(30); assertThat(t.minTempC()).isEqualTo(15); } 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 index f9d65acf..0aedda7f 100644 --- 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 @@ -24,7 +24,8 @@ class GdFcParserTest { 0x04, 0x52, // 0x0452 = 1106 → 110.6 A 0x69 // 0x69 = 105 - 40 = 65 °C }); - InfoBlock.GdFcDcDc b = (InfoBlock.GdFcDcDc) new GdFcDcDcBlockParser().parse(buf); + InfoBlock.GuangdongFc.DcDc b = + (InfoBlock.GuangdongFc.DcDc) 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)); @@ -40,8 +41,8 @@ class GdFcParserTest { 0x00, 0x14, // 20 - 5 = 15 kw 0x15, (byte) 0x18 // 0x1518 = 5400 → 540.0 V }); - InfoBlock.GdFcAirConditioner b = - (InfoBlock.GdFcAirConditioner) new GdFcAirConditionerBlockParser().parse(buf); + InfoBlock.GuangdongFc.AirConditioner b = + (InfoBlock.GuangdongFc.AirConditioner) new GdFcAirConditionerBlockParser().parse(buf); assertThat(b.status()).isEqualTo(1); assertThat(b.powerKw()).isEqualTo(15); assertThat(b.compressorInputVoltageV()).isCloseTo(540.0, within(1e-6)); @@ -56,8 +57,8 @@ class GdFcParserTest { 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); + InfoBlock.GuangdongFc.VehicleInfo b = + (InfoBlock.GuangdongFc.VehicleInfo) new GdFcVehicleInfoBlockParser().parse(buf); assertThat(b.collisionAlarm()).isEqualTo(0); assertThat(b.ambientTempC()).isEqualTo(30); assertThat(b.ambientPressureKpa()).isCloseTo(10.0, within(1e-6)); @@ -75,8 +76,8 @@ class GdFcParserTest { 0x00, 0x0D, // 0x000D = 13 → 1.3V 0x27, 0x10 // 0 A }); - InfoBlock.GdFcDemoExtension b = - (InfoBlock.GdFcDemoExtension) new GdFcDemoExtensionBlockParser().parse(buf); + InfoBlock.GuangdongFc.DemoExtension b = + (InfoBlock.GuangdongFc.DemoExtension) 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));