refactor(gb32960/model): nest InfoBlock by <protocol+version>.<dataType>

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.<protocolVersion>.<dataType>
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) <noreply@anthropic.com>
This commit is contained in:
lingniu-dev
2026-04-15 16:52:57 +08:00
parent 982272460a
commit dd8f75e0da
32 changed files with 534 additions and 541 deletions

View File

@@ -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.codec.ValueDecoder;
import com.lingniu.ingest.protocol.gb32960.model.InfoBlock; import com.lingniu.ingest.protocol.gb32960.model.InfoBlock;
import com.lingniu.ingest.protocol.gb32960.model.ProtocolVersion; import com.lingniu.ingest.protocol.gb32960.model.ProtocolVersion;
// ProtocolVersion still used by version() override below
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
@@ -29,9 +30,8 @@ public final class AlarmV2016BlockParser implements InfoBlockParser {
List<Long> motor = readFaultList(buf); List<Long> motor = readFaultList(buf);
List<Long> engine = readFaultList(buf); List<Long> engine = readFaultList(buf);
List<Long> other = readFaultList(buf); List<Long> other = readFaultList(buf);
return new InfoBlock.Alarm( return new InfoBlock.Gb32960V2016.Alarm(
ProtocolVersion.V2016, maxLevel, generalFlag, maxLevel, generalFlag, battery, motor, engine, other);
battery, motor, engine, other, null);
} }
static List<Long> readFaultList(ByteBuffer buf) { static List<Long> readFaultList(ByteBuffer buf) {

View File

@@ -25,7 +25,7 @@ public final class DriveMotorV2016BlockParser implements InfoBlockParser {
@Override @Override
public InfoBlock parse(ByteBuffer buf) { public InfoBlock parse(ByteBuffer buf) {
int count = buf.get() & 0xFF; int count = buf.get() & 0xFF;
List<InfoBlock.DriveMotor.Motor> motors = new ArrayList<>(count); List<InfoBlock.Gb32960V2016.DriveMotor.Motor> motors = new ArrayList<>(count);
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
Integer serialNo = ValueDecoder.u8(buf); Integer serialNo = ValueDecoder.u8(buf);
Integer state = ValueDecoder.u8(buf); Integer state = ValueDecoder.u8(buf);
@@ -37,9 +37,9 @@ public final class DriveMotorV2016BlockParser implements InfoBlockParser {
Integer motorTempC = ValueDecoder.tempOffset40(buf); Integer motorTempC = ValueDecoder.tempOffset40(buf);
Double ctrlVoltage = ValueDecoder.scaledU16(buf, 0.1); Double ctrlVoltage = ValueDecoder.scaledU16(buf, 0.1);
Double ctrlCurrent = ValueDecoder.scaledU16WithOffset(buf, 0.1, -1000.0); 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)); serialNo, state, ctrlTempC, rpm, torque, motorTempC, ctrlVoltage, ctrlCurrent));
} }
return new InfoBlock.DriveMotor(motors); return new InfoBlock.Gb32960V2016.DriveMotor(motors);
} }
} }

View File

@@ -23,6 +23,6 @@ public final class EngineV2016BlockParser implements InfoBlockParser {
Integer engineState = ValueDecoder.u8(buf); Integer engineState = ValueDecoder.u8(buf);
Integer crankshaftRpm = ValueDecoder.u16(buf); Integer crankshaftRpm = ValueDecoder.u16(buf);
Double fuelRate = ValueDecoder.scaledU16(buf, 0.01); Double fuelRate = ValueDecoder.scaledU16(buf, 0.01);
return new InfoBlock.Engine(engineState, crankshaftRpm, fuelRate); return new InfoBlock.Gb32960V2016.Engine(engineState, crankshaftRpm, fuelRate);
} }
} }

View File

@@ -31,7 +31,7 @@ public final class ExtremeValueV2016BlockParser implements InfoBlockParser {
Integer minTSub = ValueDecoder.u8(buf); Integer minTSub = ValueDecoder.u8(buf);
Integer minTProbe = ValueDecoder.u8(buf); Integer minTProbe = ValueDecoder.u8(buf);
Integer minT = ValueDecoder.tempOffset40(buf); Integer minT = ValueDecoder.tempOffset40(buf);
return new InfoBlock.Extreme( return new InfoBlock.Gb32960V2016.Extreme(
maxVSub, maxVCell, maxV, maxVSub, maxVCell, maxV,
minVSub, minVCell, minV, minVSub, minVCell, minV,
maxTSub, maxTProbe, maxT, maxTSub, maxTProbe, maxT,

View File

@@ -44,7 +44,7 @@ public final class FuelCellV2016BlockParser implements InfoBlockParser {
Integer maxHydrogenPressureProbeId = ValueDecoder.u8(buf); Integer maxHydrogenPressureProbeId = ValueDecoder.u8(buf);
Integer hvDcDcStatus = ValueDecoder.u8(buf); Integer hvDcDcStatus = ValueDecoder.u8(buf);
return new InfoBlock.FuelCellV2016( return new InfoBlock.Gb32960V2016.FuelCell(
fcVoltage, fcCurrent, consumption, probes, fcVoltage, fcCurrent, consumption, probes,
maxHydrogenTempC, maxHydrogenTempProbeId, maxHydrogenTempC, maxHydrogenTempProbeId,
maxHydrogenConcentration, maxHydrogenConcentrationProbeId, maxHydrogenConcentration, maxHydrogenConcentrationProbeId,

View File

@@ -27,6 +27,6 @@ public final class PositionV2016BlockParser implements InfoBlockParser {
double latitude = latRaw / 1_000_000.0; double latitude = latRaw / 1_000_000.0;
if ((statusFlag & 0b100) != 0) longitude = -longitude; if ((statusFlag & 0b100) != 0) longitude = -longitude;
if ((statusFlag & 0b010) != 0) latitude = -latitude; if ((statusFlag & 0b010) != 0) latitude = -latitude;
return new InfoBlock.Position(statusFlag, null, longitude, latitude); return new InfoBlock.Gb32960V2016.Position(statusFlag, longitude, latitude);
} }
} }

View File

@@ -34,6 +34,6 @@ public final class TemperatureV2016BlockParser implements InfoBlockParser {
} }
if (maxT == Integer.MIN_VALUE) maxT = 0; if (maxT == Integer.MIN_VALUE) maxT = 0;
if (minT == Integer.MAX_VALUE) minT = 0; if (minT == Integer.MAX_VALUE) minT = 0;
return new InfoBlock.Temperature(subCount, maxT, minT); return new InfoBlock.Gb32960V2016.Temperature(subCount, maxT, minT);
} }
} }

View File

@@ -35,8 +35,8 @@ public final class VehicleV2016BlockParser implements InfoBlockParser {
Integer insulation = ValueDecoder.u16(buf); Integer insulation = ValueDecoder.u16(buf);
Integer accelPedal = ValueDecoder.u8(buf); Integer accelPedal = ValueDecoder.u8(buf);
Integer brakePedal = ValueDecoder.u8(buf); Integer brakePedal = ValueDecoder.u8(buf);
return new InfoBlock.Vehicle(vehicleState, chargingState, runningMode, speedKmh, mileageKm, return new InfoBlock.Gb32960V2016.Vehicle(vehicleState, chargingState, runningMode,
totalVoltageV, totalCurrentA, soc, dcDcStatus, gearRaw, insulation, speedKmh, mileageKm, totalVoltageV, totalCurrentA, soc, dcDcStatus, gearRaw,
accelPedal, brakePedal); insulation, accelPedal, brakePedal);
} }
} }

View File

@@ -44,6 +44,6 @@ public final class VoltageV2016BlockParser implements InfoBlockParser {
} }
if (maxCell == Double.MIN_VALUE) maxCell = 0; if (maxCell == Double.MIN_VALUE) maxCell = 0;
if (minCell == Double.MAX_VALUE) minCell = 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);
} }
} }

View File

@@ -29,16 +29,15 @@ public final class AlarmV2025BlockParser implements InfoBlockParser {
List<Long> other = readFaultList(buf); List<Long> other = readFaultList(buf);
int generalAlarmCount = buf.hasRemaining() ? (buf.get() & 0xFF) : 0; int generalAlarmCount = buf.hasRemaining() ? (buf.get() & 0xFF) : 0;
List<InfoBlock.Alarm.GeneralAlarmEntry> levels = new ArrayList<>(generalAlarmCount); List<InfoBlock.Gb32960V2025.Alarm.GeneralAlarmEntry> levels = new ArrayList<>(generalAlarmCount);
for (int i = 0; i < generalAlarmCount && buf.remaining() >= 2; i++) { for (int i = 0; i < generalAlarmCount && buf.remaining() >= 2; i++) {
int bit = buf.get() & 0xFF; int bit = buf.get() & 0xFF;
int level = 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( return new InfoBlock.Gb32960V2025.Alarm(
ProtocolVersion.V2025, maxLevel, generalFlag, maxLevel, generalFlag, battery, motor, engine, other, levels);
battery, motor, engine, other, levels);
} }
private static List<Long> readFaultList(ByteBuffer buf) { private static List<Long> readFaultList(ByteBuffer buf) {

View File

@@ -21,7 +21,7 @@ public final class BatteryTemperatureV2025BlockParser implements InfoBlockParser
@Override @Override
public InfoBlock parse(ByteBuffer buf) { public InfoBlock parse(ByteBuffer buf) {
int subCount = buf.get() & 0xFF; int subCount = buf.get() & 0xFF;
List<InfoBlock.BatteryTemperature.Pack> packs = new ArrayList<>(subCount); List<InfoBlock.Gb32960V2025.BatteryTemperature.Pack> packs = new ArrayList<>(subCount);
for (int i = 0; i < subCount; i++) { for (int i = 0; i < subCount; i++) {
int packNo = buf.get() & 0xFF; int packNo = buf.get() & 0xFF;
int probeCount = buf.getShort() & 0xFFFF; int probeCount = buf.getShort() & 0xFFFF;
@@ -30,8 +30,8 @@ public final class BatteryTemperatureV2025BlockParser implements InfoBlockParser
for (int k = 0; k < safeCount; k++) { for (int k = 0; k < safeCount; k++) {
temps.add((buf.get() & 0xFF) - 40); 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);
} }
} }

View File

@@ -27,7 +27,7 @@ public final class DriveMotorV2025BlockParser implements InfoBlockParser {
@Override @Override
public InfoBlock parse(ByteBuffer buf) { public InfoBlock parse(ByteBuffer buf) {
int count = buf.get() & 0xFF; int count = buf.get() & 0xFF;
List<InfoBlock.DriveMotor.Motor> motors = new ArrayList<>(count); List<InfoBlock.Gb32960V2025.DriveMotor.Motor> motors = new ArrayList<>(count);
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
Integer serialNo = ValueDecoder.u8(buf); Integer serialNo = ValueDecoder.u8(buf);
Integer state = ValueDecoder.u8(buf); Integer state = ValueDecoder.u8(buf);
@@ -39,9 +39,9 @@ public final class DriveMotorV2025BlockParser implements InfoBlockParser {
Integer motorTempC = ValueDecoder.tempOffset40(buf); Integer motorTempC = ValueDecoder.tempOffset40(buf);
Double ctrlVoltage = ValueDecoder.scaledU16(buf, 0.1); Double ctrlVoltage = ValueDecoder.scaledU16(buf, 0.1);
Double ctrlCurrent = ValueDecoder.scaledU16WithOffset(buf, 0.1, -1000.0); 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)); serialNo, state, ctrlTempC, rpm, torque, motorTempC, ctrlVoltage, ctrlCurrent));
} }
return new InfoBlock.DriveMotor(motors); return new InfoBlock.Gb32960V2025.DriveMotor(motors);
} }
} }

View File

@@ -22,6 +22,6 @@ public final class EngineV2025BlockParser implements InfoBlockParser {
Integer engineState = ValueDecoder.u8(buf); Integer engineState = ValueDecoder.u8(buf);
Integer crankshaftRpm = ValueDecoder.u16(buf); Integer crankshaftRpm = ValueDecoder.u16(buf);
Double fuelRate = ValueDecoder.scaledU16(buf, 0.01); Double fuelRate = ValueDecoder.scaledU16(buf, 0.01);
return new InfoBlock.Engine(engineState, crankshaftRpm, fuelRate); return new InfoBlock.Gb32960V2025.Engine(engineState, crankshaftRpm, fuelRate);
} }
} }

View File

@@ -23,7 +23,7 @@ public final class FuelCellStackV2025BlockParser implements InfoBlockParser {
@Override @Override
public InfoBlock parse(ByteBuffer buf) { public InfoBlock parse(ByteBuffer buf) {
int stackCount = buf.get() & 0xFF; int stackCount = buf.get() & 0xFF;
List<InfoBlock.FuelCellStack.Stack> stacks = new ArrayList<>(stackCount); List<InfoBlock.Gb32960V2025.FuelCellStack.Stack> stacks = new ArrayList<>(stackCount);
for (int i = 0; i < stackCount; i++) { for (int i = 0; i < stackCount; i++) {
int stackNo = buf.get() & 0xFF; int stackNo = buf.get() & 0xFF;
Double voltage = ValueDecoder.scaledU16(buf, 0.1); Double voltage = ValueDecoder.scaledU16(buf, 0.1);
@@ -37,9 +37,9 @@ public final class FuelCellStackV2025BlockParser implements InfoBlockParser {
for (int k = 0; k < safeCount; k++) { for (int k = 0; k < safeCount; k++) {
temps.add((buf.get() & 0xFF) - 40); 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)); stackNo, voltage, current, h2InletPressure, airInletPressure, airInletTemp, temps));
} }
return new InfoBlock.FuelCellStack(stackCount, stacks); return new InfoBlock.Gb32960V2025.FuelCellStack(stackCount, stacks);
} }
} }

View File

@@ -27,7 +27,7 @@ public final class FuelCellV2025BlockParser implements InfoBlockParser {
Integer hvDcDcStatus = ValueDecoder.u8(buf); Integer hvDcDcStatus = ValueDecoder.u8(buf);
Integer remainingH2Percent = ValueDecoder.u8(buf); Integer remainingH2Percent = ValueDecoder.u8(buf);
Integer hvDcDcControllerTempC = ValueDecoder.tempOffset40(buf); Integer hvDcDcControllerTempC = ValueDecoder.tempOffset40(buf);
return new InfoBlock.FuelCellV2025( return new InfoBlock.Gb32960V2025.FuelCell(
maxTempC, maxTempProbeId, maxTempC, maxTempProbeId,
maxConcentration, maxConcentrationProbeId, maxConcentration, maxConcentrationProbeId,
maxPressure, maxPressureProbeId, maxPressure, maxPressureProbeId,

View File

@@ -22,7 +22,7 @@ public final class MinParallelVoltageV2025BlockParser implements InfoBlockParser
@Override @Override
public InfoBlock parse(ByteBuffer buf) { public InfoBlock parse(ByteBuffer buf) {
int subCount = buf.get() & 0xFF; int subCount = buf.get() & 0xFF;
List<InfoBlock.MinParallelVoltage.Pack> packs = new ArrayList<>(subCount); List<InfoBlock.Gb32960V2025.MinParallelVoltage.Pack> packs = new ArrayList<>(subCount);
for (int i = 0; i < subCount; i++) { for (int i = 0; i < subCount; i++) {
int packNo = buf.get() & 0xFF; int packNo = buf.get() & 0xFF;
Double voltage = ValueDecoder.scaledU16(buf, 0.1); Double voltage = ValueDecoder.scaledU16(buf, 0.1);
@@ -33,9 +33,9 @@ public final class MinParallelVoltageV2025BlockParser implements InfoBlockParser
for (int k = 0; k < safeCount; k++) { for (int k = 0; k < safeCount; k++) {
frameVoltages.add((buf.getShort() & 0xFFFF) * 0.001); 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)); packNo, voltage, current, totalUnits, frameVoltages));
} }
return new InfoBlock.MinParallelVoltage(subCount, packs); return new InfoBlock.Gb32960V2025.MinParallelVoltage(subCount, packs);
} }
} }

View File

@@ -29,6 +29,6 @@ public final class PositionV2025BlockParser implements InfoBlockParser {
double latitude = latRaw / 1_000_000.0; double latitude = latRaw / 1_000_000.0;
if ((statusFlag & 0b100) != 0) longitude = -longitude; if ((statusFlag & 0b100) != 0) longitude = -longitude;
if ((statusFlag & 0b010) != 0) latitude = -latitude; if ((statusFlag & 0b010) != 0) latitude = -latitude;
return new InfoBlock.Position(statusFlag, coordSystem, longitude, latitude); return new InfoBlock.Gb32960V2025.Position(statusFlag, coordSystem, longitude, latitude);
} }
} }

View File

@@ -30,7 +30,7 @@ public final class SuperCapacitorExtremeV2025BlockParser implements InfoBlockPar
Integer minTSys = ValueDecoder.u8(buf); Integer minTSys = ValueDecoder.u8(buf);
Integer minTProbe = ValueDecoder.u16(buf); Integer minTProbe = ValueDecoder.u16(buf);
Integer minT = ValueDecoder.tempOffset40(buf); Integer minT = ValueDecoder.tempOffset40(buf);
return new InfoBlock.SuperCapacitorExtreme( return new InfoBlock.Gb32960V2025.SuperCapacitorExtreme(
maxVSys, maxVCell, maxV, maxVSys, maxVCell, maxV,
minVSys, minVCell, minV, minVSys, minVCell, minV,
maxTSys, maxTProbe, maxT, maxTSys, maxTProbe, maxT,

View File

@@ -35,6 +35,6 @@ public final class SuperCapacitorV2025BlockParser implements InfoBlockParser {
for (int i = 0; i < safeProbeCount; i++) { for (int i = 0; i < safeProbeCount; i++) {
temps.add((buf.get() & 0xFF) - 40); 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);
} }
} }

View File

@@ -30,8 +30,8 @@ public final class VehicleV2025BlockParser implements InfoBlockParser {
Integer dcDcStatus = ValueDecoder.u8(buf); Integer dcDcStatus = ValueDecoder.u8(buf);
Integer gearRaw = ValueDecoder.u8raw(buf); Integer gearRaw = ValueDecoder.u8raw(buf);
Integer insulation = ValueDecoder.u16(buf); Integer insulation = ValueDecoder.u16(buf);
return new InfoBlock.Vehicle(vehicleState, chargingState, runningMode, speedKmh, mileageKm, return new InfoBlock.Gb32960V2025.Vehicle(vehicleState, chargingState, runningMode,
totalVoltageV, totalCurrentA, soc, dcDcStatus, gearRaw, insulation, speedKmh, mileageKm, totalVoltageV, totalCurrentA, soc, dcDcStatus, gearRaw,
null, null); insulation);
} }
} }

View File

@@ -29,6 +29,6 @@ public final class GdFcAirConditionerBlockParser implements InfoBlockParser {
int rawPower = buf.getShort() & 0xFFFF; int rawPower = buf.getShort() & 0xFFFF;
Integer power = (rawPower == 0xFFFE || rawPower == 0xFFFF) ? null : rawPower - 5; Integer power = (rawPower == 0xFFFE || rawPower == 0xFFFF) ? null : rawPower - 5;
Double inputV = ValueDecoder.scaledU16(buf, 0.1); Double inputV = ValueDecoder.scaledU16(buf, 0.1);
return new InfoBlock.GdFcAirConditioner(status, power, inputV); return new InfoBlock.GuangdongFc.AirConditioner(status, power, inputV);
} }
} }

View File

@@ -36,7 +36,7 @@ public final class GdFcAuxiliaryBlockParser implements InfoBlockParser {
@Override @Override
public InfoBlock parse(ByteBuffer buf) { public InfoBlock parse(ByteBuffer buf) {
int count = buf.get() & 0xFF; int count = buf.get() & 0xFF;
List<InfoBlock.GdFcAuxiliary.Subsystem> subs = new ArrayList<>(count); List<InfoBlock.GuangdongFc.Auxiliary.Subsystem> subs = new ArrayList<>(count);
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
Double airCompV = ValueDecoder.scaledU16(buf, 0.1); Double airCompV = ValueDecoder.scaledU16(buf, 0.1);
Integer airCompP = readPowerOffsetMinus5(buf); Integer airCompP = readPowerOffsetMinus5(buf);
@@ -46,10 +46,10 @@ public final class GdFcAuxiliaryBlockParser implements InfoBlockParser {
Double ptcV = ValueDecoder.scaledU16(buf, 0.1); Double ptcV = ValueDecoder.scaledU16(buf, 0.1);
Integer ptcP = readPowerOffsetMinus5(buf); Integer ptcP = readPowerOffsetMinus5(buf);
Double lvBattV = ValueDecoder.scaledU16(buf, 0.1); 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)); airCompV, airCompP, h2PumpV, h2PumpP, waterPumpV, ptcV, ptcP, lvBattV));
} }
return new InfoBlock.GdFcAuxiliary(count, subs); return new InfoBlock.GuangdongFc.Auxiliary(count, subs);
} }
/** 功率字段raw(2B) - 50xFFFE/0xFFFF 异常/无效。 */ /** 功率字段raw(2B) - 50xFFFE/0xFFFF 异常/无效。 */

View File

@@ -32,6 +32,6 @@ public final class GdFcDcDcBlockParser implements InfoBlockParser {
Double outV = ValueDecoder.scaledU16(buf, 0.1); Double outV = ValueDecoder.scaledU16(buf, 0.1);
Double outC = ValueDecoder.scaledU16(buf, 0.1); Double outC = ValueDecoder.scaledU16(buf, 0.1);
Integer ctrlTemp = ValueDecoder.tempOffset40(buf); Integer ctrlTemp = ValueDecoder.tempOffset40(buf);
return new InfoBlock.GdFcDcDc(inV, inC, outV, outC, ctrlTemp); return new InfoBlock.GuangdongFc.DcDc(inV, inC, outV, outC, ctrlTemp);
} }
} }

View File

@@ -42,6 +42,6 @@ public final class GdFcDemoExtensionBlockParser implements InfoBlockParser {
org.slf4j.LoggerFactory.getLogger(GdFcDemoExtensionBlockParser.class) org.slf4j.LoggerFactory.getLogger(GdFcDemoExtensionBlockParser.class)
.debug("[gb32960/gd-fc/0x80] declaredLen={} (expected 9), parsed by fixed layout", declaredLen); .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);
} }
} }

View File

@@ -45,7 +45,7 @@ public final class GdFcStackBlockParser implements InfoBlockParser {
@Override @Override
public InfoBlock parse(ByteBuffer buf) { public InfoBlock parse(ByteBuffer buf) {
int stackCount = buf.get() & 0xFF; int stackCount = buf.get() & 0xFF;
List<InfoBlock.GdFcStack.Stack> stacks = new ArrayList<>(stackCount); List<InfoBlock.GuangdongFc.Stack.Item> stacks = new ArrayList<>(stackCount);
for (int i = 0; i < stackCount; i++) { for (int i = 0; i < stackCount; i++) {
Integer workState = ValueDecoder.u8raw(buf); Integer workState = ValueDecoder.u8raw(buf);
Integer waterTemp = ValueDecoder.tempOffset40(buf); Integer waterTemp = ValueDecoder.tempOffset40(buf);
@@ -65,11 +65,11 @@ public final class GdFcStackBlockParser implements InfoBlockParser {
for (int k = 0; k < safe; k++) { for (int k = 0; k < safe; k++) {
frameVoltages.add((buf.getShort() & 0xFFFF) * 0.001); 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, workState, waterTemp, h2InletP, airInletP, airInletTemp,
maxId, minId, maxV, minV, avgV, maxId, minId, maxV, minV, avgV,
cellCount, frameStart, frameCells, frameVoltages)); cellCount, frameStart, frameCells, frameVoltages));
} }
return new InfoBlock.GdFcStack(stackCount, stacks); return new InfoBlock.GuangdongFc.Stack(stackCount, stacks);
} }
} }

View File

@@ -31,6 +31,6 @@ public final class GdFcVehicleInfoBlockParser implements InfoBlockParser {
Integer tempC = (rawTemp == 0xFFFE || rawTemp == 0xFFFF) ? null : rawTemp - 40; Integer tempC = (rawTemp == 0xFFFE || rawTemp == 0xFFFF) ? null : rawTemp - 40;
Double pressure = ValueDecoder.scaledU16WithOffset(buf, 0.1, -100.0); Double pressure = ValueDecoder.scaledU16WithOffset(buf, 0.1, -100.0);
Double h2Mass = ValueDecoder.scaledU16(buf, 0.1); Double h2Mass = ValueDecoder.scaledU16(buf, 0.1);
return new InfoBlock.GdFcVehicleInfo(collision, tempC, pressure, h2Mass); return new InfoBlock.GuangdongFc.VehicleInfo(collision, tempC, pressure, h2Mass);
} }
} }

View File

@@ -102,11 +102,18 @@ public final class Gb32960EventMapper implements EventMapper<Gb32960Message> {
Map<String, String> meta, Map<String, String> meta,
Instant eventTime, Instant ingestTime, Instant eventTime, Instant ingestTime,
List<VehicleEvent> out) { List<VehicleEvent> out) {
InfoBlock.Vehicle v = message.findBlock(InfoBlock.Vehicle.class).orElse(null); // 整车数据:先查 V2016再 V2025每帧只会命中其一extracted 出来变成统一
InfoBlock.Position p = message.findBlock(InfoBlock.Position.class).orElse(null); // 视图VehicleView供后面 payload 构造使用
InfoBlock.FuelCellV2016 fc16 = message.findBlock(InfoBlock.FuelCellV2016.class).orElse(null); VehicleView v = extractVehicleView(message);
InfoBlock.FuelCellV2025 fc25 = message.findBlock(InfoBlock.FuelCellV2025.class).orElse(null); // 位置数据:同样两版本都查
InfoBlock.Alarm alarm = message.findBlock(InfoBlock.Alarm.class).orElse(null); 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) { if (v != null) {
Double fcVoltageV = fc16 != null ? fc16.fcVoltageV() : null; Double fcVoltageV = fc16 != null ? fc16.fcVoltageV() : null;
@@ -119,25 +126,25 @@ public final class Gb32960EventMapper implements EventMapper<Gb32960Message> {
? fc25.remainingHydrogenPercent().doubleValue() : null; ? fc25.remainingHydrogenPercent().doubleValue() : null;
RealtimePayload payload = new RealtimePayload( RealtimePayload payload = new RealtimePayload(
v.speedKmh(), v.speedKmh,
v.totalMileageKm(), v.totalMileageKm,
v.socPercent() != null ? v.socPercent().doubleValue() : null, v.socPercent != null ? v.socPercent.doubleValue() : null,
v.totalVoltageV(), v.totalVoltageV,
v.totalCurrentA(), v.totalCurrentA,
fcVoltageV, fcVoltageV,
fcCurrentA, fcCurrentA,
fcTempC, fcTempC,
hydrogenRemaining, hydrogenRemaining,
hydrogenHighPressure, hydrogenHighPressure,
null, null,
mapVehicleState(v.vehicleState()), mapVehicleState(v.vehicleState),
mapChargingState(v.chargingState()), mapChargingState(v.chargingState),
mapRunningMode(v.runningMode()), mapRunningMode(v.runningMode),
extractGearLevel(v.gearRaw()), extractGearLevel(v.gearRaw),
v.acceleratorPedal() != null ? v.acceleratorPedal().doubleValue() : null, v.acceleratorPedal != null ? v.acceleratorPedal.doubleValue() : null,
v.brakePedal() != null ? v.brakePedal().doubleValue() : null, v.brakePedal != null ? v.brakePedal.doubleValue() : null,
p != null ? p.longitude() : null, p != null ? p.longitude : null,
p != null ? p.latitude() : null, p != null ? p.latitude : null,
null, null,
null, null,
null, null,
@@ -149,31 +156,31 @@ public final class Gb32960EventMapper implements EventMapper<Gb32960Message> {
} }
if (p != null) { if (p != null) {
LocationPayload loc = new LocationPayload( LocationPayload loc = new LocationPayload(
p.longitude(), p.latitude(), 0.0, p.longitude, p.latitude, 0.0,
v != null && v.speedKmh() != null ? v.speedKmh() : 0.0, v != null && v.speedKmh != null ? v.speedKmh : 0.0,
0.0, 0, p.statusFlag()); 0.0, 0, p.statusFlag);
out.add(new VehicleEvent.Location( out.add(new VehicleEvent.Location(
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
header.vin(), ProtocolId.GB32960, eventTime, ingestTime, header.vin(), ProtocolId.GB32960, eventTime, ingestTime,
null, meta, loc)); null, meta, loc));
} }
if (alarm != null && alarm.maxLevel() != null && alarm.maxLevel() > 0) { if (alarm != null && alarm.maxLevel != null && alarm.maxLevel > 0) {
List<String> faultCodes = new ArrayList<>(); List<String> faultCodes = new ArrayList<>();
addFaultCodes(faultCodes, "BAT", alarm.batteryFaults()); addFaultCodes(faultCodes, "BAT", alarm.batteryFaults);
addFaultCodes(faultCodes, "MOT", alarm.motorFaults()); addFaultCodes(faultCodes, "MOT", alarm.motorFaults);
addFaultCodes(faultCodes, "ENG", alarm.engineFaults()); addFaultCodes(faultCodes, "ENG", alarm.engineFaults);
addFaultCodes(faultCodes, "OTH", alarm.otherFaults()); addFaultCodes(faultCodes, "OTH", alarm.otherFaults);
Set<String> activeBits = GeneralAlarmFlagBit.parse(alarm.generalAlarmFlag()).stream() Set<String> activeBits = GeneralAlarmFlagBit.parse(alarm.generalAlarmFlag).stream()
.map(Enum::name) .map(Enum::name)
.collect(Collectors.toCollection(java.util.LinkedHashSet::new)); .collect(Collectors.toCollection(java.util.LinkedHashSet::new));
AlarmPayload ap = new AlarmPayload( AlarmPayload ap = new AlarmPayload(
mapAlarmLevel(alarm.maxLevel()), mapAlarmLevel(alarm.maxLevel),
(int) (alarm.generalAlarmFlag() & 0xFFFF), (int) (alarm.generalAlarmFlag & 0xFFFF),
"GB32960_ALARM", "GB32960_ALARM",
faultCodes, faultCodes,
activeBits, activeBits,
p != null ? p.longitude() : null, p != null ? p.longitude : null,
p != null ? p.latitude() : null); p != null ? p.latitude : null);
out.add(new VehicleEvent.Alarm( out.add(new VehicleEvent.Alarm(
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
header.vin(), ProtocolId.GB32960, eventTime, ingestTime, header.vin(), ProtocolId.GB32960, eventTime, ingestTime,
@@ -181,6 +188,61 @@ public final class Gb32960EventMapper implements EventMapper<Gb32960Message> {
} }
} }
/** 跨版本的整车数据视图,把 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<Long> batteryFaults, List<Long> motorFaults,
List<Long> engineFaults, List<Long> 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。 */ /** 挡位字节按附录 A.1 解析:低 4 位为挡位值 0000-1111。 */
private static Integer extractGearLevel(Integer gearRaw) { private static Integer extractGearLevel(Integer gearRaw) {
if (gearRaw == null) return null; if (gearRaw == null) return null;

View File

@@ -3,66 +3,53 @@ package com.lingniu.ingest.protocol.gb32960.model;
import java.util.List; import java.util.List;
/** /**
* 已解析的信息体基接口。每种语义对应一种 record 实现。 * 已解析的信息体基接口。按 {@code <协议+版本>.<数据类型>} 严格分组:
* *
* <p>规范来源GB/T 32960.3-2016 附录 B 表 B.10-B.17GB/T 32960.3-2025 §7.2.4 表 10-27。 * <ul>
* 字段命名使用规范中的中文直译以便对照查阅。 * <li>{@link Gb32960V2016} —— GB/T 32960.3-2016 附录 B 表 B.10~B.20
* <li>{@link Gb32960V2025} —— GB/T 32960.3-2025 §7.2.4 表 10~27
* <li>{@link GuangdongFc} —— 广东燃料电池汽车示范应用城市群综合监管平台规范 v1.0
* <li>{@link Raw} —— 未实现 parser 的原始字节兜底
* </ul>
*
* <p>哪怕同一个数据类型在两个版本字段一致(如 Engine 5 字节固定布局),也按规则
* 复制成两份 record绝不跨版本共用 record 类型,以便消费方在 instanceof / switch
* 时能严格区分数据来源。
* *
* <p>异常值约定: * <p>异常值约定:
* <ul> * <ul>
* <li>单字节:{@code 0xFE} 异常{@code 0xFF} 无效 * <li>单字节:{@code 0xFE} 异常 / {@code 0xFF} 无效
* <li>双字节:{@code 0xFFFE} 异常{@code 0xFFFF} 无效 * <li>双字节:{@code 0xFFFE} 异常 / {@code 0xFFFF} 无效
* <li>四字节:{@code 0xFFFFFFFE} 异常{@code 0xFFFFFFFF} 无效 * <li>四字节:{@code 0xFFFFFFFE} 异常 / {@code 0xFFFFFFFF} 无效
* </ul> * </ul>
* 对外 record 使用 {@link Double}/{@link Integer} 等装箱类型,异常或无效时设为 {@code null}。 * 对外 record 使用 {@link Double} / {@link Integer} 等装箱类型,异常或无效时设为
* {@code null}。
*/ */
public sealed interface InfoBlock public sealed interface InfoBlock
permits InfoBlock.Vehicle, permits InfoBlock.Gb32960V2016,
InfoBlock.DriveMotor, InfoBlock.Gb32960V2025,
InfoBlock.FuelCellV2016, InfoBlock.GuangdongFc,
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,
InfoBlock.Raw { InfoBlock.Raw {
InfoBlockType type(); InfoBlockType type();
// ======================================================================== // ========================================================================
// 0x01 整车数据 —— 2016/2025 共享(表 10 // GB/T 32960.3-2016
// ======================================================================== // ========================================================================
/** /** GB/T 32960.3-2016 附录 B 定义的全部信息体类型。 */
* 整车数据。固定 21 字节2025 版)/ 20 字节2016 版,无 brake sealed interface Gb32960V2016 extends InfoBlock
* permits Gb32960V2016.Vehicle,
* @param vehicleState 车辆状态0x01 启动 / 0x02 熄火 / 0x03 其他 Gb32960V2016.DriveMotor,
* @param chargingState 充电状态0x01 停车充电 / 0x02 行驶充电 / 0x03 未充电 / 0x04 充电完成 Gb32960V2016.FuelCell,
* @param runningMode 运行模式0x01 纯电 / 0x02 混动 / 0x03 燃油 Gb32960V2016.Engine,
* @param speedKmh 车速 km/h0~500最小计量 0.1 km/h Gb32960V2016.Position,
* @param totalMileageKm 累计里程 km0~999999.9,最小计量 0.1 km Gb32960V2016.Extreme,
* @param totalVoltageV 总电压 V0~6000最小计量 0.1 V Gb32960V2016.Alarm,
* @param totalCurrentA 总电流 A-3000~+3000原始值偏移 3000 A最小计量 0.1 A Gb32960V2016.Voltage,
* @param socPercent SOC 百分比0~100 Gb32960V2016.Temperature {
* @param dcDcStatus DC-DC 状态0x01 工作 / 0x02 断开
* @param gearRaw 挡位原始字节,按附录 A.1 解析 /** 0x01 整车数据。固定 20 字节(含加速/制动踏板)。 */
* @param insulationResistanceKohm 高压对地绝缘电阻 kΩ0~60000
* @param acceleratorPedal 加速踏板行程值 0~100仅 2016 版)
* @param brakePedal 制动踏板状态 0~100仅 2016 版)
*/
record Vehicle( record Vehicle(
Integer vehicleState, Integer vehicleState,
Integer chargingState, Integer chargingState,
@@ -77,33 +64,14 @@ public sealed interface InfoBlock
Integer insulationResistanceKohm, Integer insulationResistanceKohm,
Integer acceleratorPedal, Integer acceleratorPedal,
Integer brakePedal Integer brakePedal
) implements InfoBlock { ) implements Gb32960V2016 {
@Override public InfoBlockType type() { return InfoBlockType.VEHICLE; } @Override public InfoBlockType type() { return InfoBlockType.VEHICLE; }
} }
// ======================================================================== /** 0x02 驱动电机数据。变长。每电机 12 字节rpm 偏移 20000torque 2 字节偏移 20000。 */
// 0x02 驱动电机数据 —— 表 15/16 record DriveMotor(List<Motor> motors) implements Gb32960V2016 {
// ========================================================================
/** 驱动电机数据。变长1 字节电机个数 + N × 电机信息。 */
record DriveMotor(List<Motor> motors) implements InfoBlock {
@Override public InfoBlockType type() { return InfoBlockType.DRIVE_MOTOR; } @Override public InfoBlockType type() { return InfoBlockType.DRIVE_MOTOR; }
/**
* 单个驱动电机数据。
*
* <p>2016 版字段大小(表 B.16):转速 2 字节偏移 20000转矩 2 字节偏移 20000 单位 0.1 N·m共 12 字节。
* <p>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 电机控制器输入电压 V0~6000最小计量 0.1 V
* @param controllerDcCurrentA 电机控制器直流母线电流 A-1000~+5000最小计量 0.1 A
*/
public record Motor( public record Motor(
Integer serialNo, Integer serialNo,
Integer state, Integer state,
@@ -116,18 +84,8 @@ public sealed interface InfoBlock
) {} ) {}
} }
// ======================================================================== /** 0x03 燃料电池数据2016 版字段顺序,含 N×探针温度 1B。变长。 */
// 0x03 燃料电池数据2016 版) record FuelCell(
// ========================================================================
/**
* 燃料电池数据2016 版)。变长。
*
* <p>2016 版字段顺序:燃料电池电压(2) + 电流(2) + 氢气消耗率(2) + 探针数(2)
* + N×探针温度(1) + 最高温度(2) + 最高温度探针代号(1) + 最高浓度(2) + 最高浓度代号(1)
* + 最高压力(2) + 最高压力代号(1) + DCDC 状态(1)。
*/
record FuelCellV2016(
Double fcVoltageV, Double fcVoltageV,
Double fcCurrentA, Double fcCurrentA,
Double hydrogenConsumptionKgPer100km, Double hydrogenConsumptionKgPer100km,
@@ -139,88 +97,29 @@ public sealed interface InfoBlock
Double maxHydrogenPressureMpa, Double maxHydrogenPressureMpa,
Integer maxHydrogenPressureProbeId, Integer maxHydrogenPressureProbeId,
Integer hvDcDcStatus Integer hvDcDcStatus
) implements InfoBlock { ) implements Gb32960V2016 {
@Override public InfoBlockType type() { return InfoBlockType.FUEL_CELL_V2016; } @Override public InfoBlockType type() { return InfoBlockType.FUEL_CELL_V2016; }
} }
// ======================================================================== /** 0x04 发动机数据。固定 5 字节。 */
// 0x03 燃料电池发动机及车载氢系统数据2025 版,表 17
// ========================================================================
/**
* 燃料电池发动机及车载氢系统数据2025 版)。固定 13 字节。
*
* <p>较 2016 版的差异(见 2025 规范前言 o
* <ul>
* <li>删除:燃料电池电压、燃料电池电流、氢气消耗率、温度探针列表
* <li>新增:剩余氢量百分比、高压 DC/DC 控制器温度
* <li>温度探针数据迁移至 0x30 燃料电池电堆
* </ul>
*/
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; }
}
// ========================================================================
// 0x04 发动机数据 —— 表 20
// ========================================================================
/**
* 发动机数据。固定 5 字节。
*
* @param engineState 发动机状态0x01 启动 / 0x02 关闭
* @param crankshaftRpm 曲轴转速 r/min0~60000
* @param fuelConsumptionRate 燃料消耗率 L/100km0~600最小计量 0.01
*/
record Engine( record Engine(
Integer engineState, Integer engineState,
Integer crankshaftRpm, Integer crankshaftRpm,
Double fuelConsumptionRate Double fuelConsumptionRate
) implements InfoBlock { ) implements Gb32960V2016 {
@Override public InfoBlockType type() { return InfoBlockType.ENGINE; } @Override public InfoBlockType type() { return InfoBlockType.ENGINE; }
} }
// ======================================================================== /** 0x05 车辆位置数据。固定 9 字节。 */
// 0x05 车辆位置数据 —— 表 21
// ========================================================================
/**
* 车辆位置数据。
*
* <p>2016 版固定 9 字节:{@code 状态位(1) + 经度(4) + 纬度(4)}。
* <p>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( record Position(
int statusFlag, int statusFlag,
Integer coordSystem,
double longitude, double longitude,
double latitude double latitude
) implements InfoBlock { ) implements Gb32960V2016 {
@Override public InfoBlockType type() { @Override public InfoBlockType type() { return InfoBlockType.POSITION_V2016; }
return coordSystem == null ? InfoBlockType.POSITION_V2016 : InfoBlockType.POSITION_V2025;
}
} }
// ======================================================================== /** 0x06 动力蓄电池极值数据。固定 14 字节。 */
// 0x06 动力蓄电池极值数据 —— 仅 2016 版
// ========================================================================
/** 动力蓄电池极值数据2016 版)。固定 14 字节。2025 版已删除。 */
record Extreme( record Extreme(
Integer maxCellVoltageSubSysNo, Integer maxCellVoltageSubSysNo,
Integer maxCellVoltageCellId, Integer maxCellVoltageCellId,
@@ -234,31 +133,129 @@ public sealed interface InfoBlock
Integer minTempSubSysNo, Integer minTempSubSysNo,
Integer minTempProbeId, Integer minTempProbeId,
Integer minTempC Integer minTempC
) implements InfoBlock { ) implements Gb32960V2016 {
@Override public InfoBlockType type() { return InfoBlockType.EXTREME_V2016; } @Override public InfoBlockType type() { return InfoBlockType.EXTREME_V2016; }
} }
/** 0x07 报警数据。变长4 个故障代码列表)。 */
record Alarm(
Integer maxLevel,
long generalAlarmFlag,
List<Long> batteryFaults,
List<Long> motorFaults,
List<Long> engineFaults,
List<Long> otherFaults
) implements Gb32960V2016 {
@Override public InfoBlockType type() { return InfoBlockType.ALARM_V2016; }
}
/** 0x08 可充电储能装置电压数据。变长。PoC 仅保留汇总。 */
record Voltage(
int subSystemCount,
double maxCellVoltageV,
double minCellVoltageV,
double totalVoltageV
) implements Gb32960V2016 {
@Override public InfoBlockType type() { return InfoBlockType.VOLTAGE_V2016; }
}
/** 0x09 可充电储能装置温度数据。变长。 */
record Temperature(
int subSystemCount,
int maxTempC,
int minTempC
) implements Gb32960V2016 {
@Override public InfoBlockType type() { return InfoBlockType.TEMPERATURE_V2016; }
}
}
// ======================================================================== // ========================================================================
// 报警数据 —— 2016 的 0x07 / 2025 的 0x06表 23 // GB/T 32960.3-2025
// ======================================================================== // ========================================================================
/** /** GB/T 32960.3-2025 §7.2.4 定义的全部信息体类型。 */
* 报警数据。 sealed interface Gb32960V2025 extends InfoBlock
* permits Gb32960V2025.Vehicle,
* <p>2016 版 typeCode=0x072025 版 typeCode=0x06。前半部分maxLevel + generalAlarmFlag Gb32960V2025.DriveMotor,
* + 4 类故障代码列表一致2025 版在尾部额外增加"通用报警故障等级列表"。 Gb32960V2025.FuelCell,
* Gb32960V2025.Engine,
* @param protocolVersion 协议版本,决定 typeCode 映射 Gb32960V2025.Position,
* @param maxLevel 最高报警等级 0~40 无故障4 热事件为最高级) Gb32960V2025.Alarm,
* @param generalAlarmFlag 通用报警标志(表 24 的 28 个位) Gb32960V2025.MinParallelVoltage,
* @param batteryFaults 可充电储能装置故障代码列表 Gb32960V2025.BatteryTemperature,
* @param motorFaults 驱动电机故障代码列表 Gb32960V2025.FuelCellStack,
* @param engineFaults 发动机故障代码列表 Gb32960V2025.SuperCapacitor,
* @param otherFaults 其他故障代码列表 Gb32960V2025.SuperCapacitorExtreme {
* @param generalAlarmLevels 2025 新增:{bit, level} 列表2016 版为 null 或空列表
*/ /** 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; }
}
/** 0x02 驱动电机数据。变长。每电机 14 字节rpm 偏移 32000torque 4 字节偏移 20000。 */
record DriveMotor(List<Motor> 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( record Alarm(
ProtocolVersion protocolVersion,
Integer maxLevel, Integer maxLevel,
long generalAlarmFlag, long generalAlarmFlag,
List<Long> batteryFaults, List<Long> batteryFaults,
@@ -266,58 +263,17 @@ public sealed interface InfoBlock
List<Long> engineFaults, List<Long> engineFaults,
List<Long> otherFaults, List<Long> otherFaults,
List<GeneralAlarmEntry> generalAlarmLevels List<GeneralAlarmEntry> generalAlarmLevels
) implements InfoBlock { ) implements Gb32960V2025 {
@Override public InfoBlockType type() { @Override public InfoBlockType type() { return InfoBlockType.ALARM_V2025; }
return protocolVersion == ProtocolVersion.V2025
? InfoBlockType.ALARM_V2025 : InfoBlockType.ALARM_V2016;
}
/** 2025 版通用报警故障等级列表项2 字节bit 对应表 24 位序号level 为故障等级。 */ /** 通用报警故障等级列表项2 字节bit 对应表 24 位序号level 为故障等级。 */
public record GeneralAlarmEntry(int bit, int level) {} public record GeneralAlarmEntry(int bit, int level) {}
} }
// ======================================================================== /** 0x07 动力蓄电池最小并联单元电压数据。变长。 */
// 2016 版 0x08 可充电储能装置电压数据 record MinParallelVoltage(int subSystemCount, List<Pack> packs) implements Gb32960V2025 {
// ========================================================================
/** 可充电储能装置电压数据2016 版。变长。PoC 仅保留汇总。 */
record Voltage(
int subSystemCount,
double maxCellVoltageV,
double minCellVoltageV,
double totalVoltageV
) implements InfoBlock {
@Override public InfoBlockType type() { return InfoBlockType.VOLTAGE_V2016; }
}
// ========================================================================
// 2016 版 0x09 可充电储能装置温度数据
// ========================================================================
/** 可充电储能装置温度数据2016 版)。变长。 */
record Temperature(
int subSystemCount,
int maxTempC,
int minTempC
) implements InfoBlock {
@Override public InfoBlockType type() { return InfoBlockType.TEMPERATURE_V2016; }
}
// ========================================================================
// 2025 版 0x07 动力蓄电池最小并联单元电压数据(表 11/12
// ========================================================================
/** 动力蓄电池最小并联单元电压数据2025 版)。变长。 */
record MinParallelVoltage(int subSystemCount, List<Pack> packs) implements InfoBlock {
@Override public InfoBlockType type() { return InfoBlockType.MIN_PARALLEL_VOLTAGE_V2025; } @Override public InfoBlockType type() { return InfoBlockType.MIN_PARALLEL_VOLTAGE_V2025; }
/**
* @param packNo 电池包号 1~50
* @param packVoltageV 动力蓄电池包电压 V0~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( public record Pack(
int packNo, int packNo,
Double packVoltageV, Double packVoltageV,
@@ -327,39 +283,17 @@ public sealed interface InfoBlock
) {} ) {}
} }
// ======================================================================== /** 0x08 动力蓄电池温度数据。变长。 */
// 2025 版 0x08 动力蓄电池温度数据(表 13/14 record BatteryTemperature(int subSystemCount, List<Pack> packs) implements Gb32960V2025 {
// ========================================================================
/** 动力蓄电池温度数据2025 版)。变长。 */
record BatteryTemperature(int subSystemCount, List<Pack> packs) implements InfoBlock {
@Override public InfoBlockType type() { return InfoBlockType.BATTERY_TEMPERATURE_V2025; } @Override public InfoBlockType type() { return InfoBlockType.BATTERY_TEMPERATURE_V2025; }
/**
* @param packNo 电池包号 1~50
* @param probeCount 温度探针个数 1~65531
* @param probeTempsC 各探针温度 ℃,-40~+210
*/
public record Pack(int packNo, int probeCount, List<Integer> probeTempsC) {} public record Pack(int packNo, int probeCount, List<Integer> probeTempsC) {}
} }
// ======================================================================== /** 0x30 燃料电池电堆数据。变长2025 标准)。 */
// 2025 版 0x30 燃料电池电堆数据(表 18/19 record FuelCellStack(int stackCount, List<Stack> stacks) implements Gb32960V2025 {
// ========================================================================
/** 燃料电池电堆数据2025 版。变长1 字节电堆个数 + N × 电堆信息。 */
record FuelCellStack(int stackCount, List<Stack> stacks) implements InfoBlock {
@Override public InfoBlockType type() { return InfoBlockType.FUEL_CELL_STACK; } @Override public InfoBlockType type() { return InfoBlockType.FUEL_CELL_STACK; }
/**
* @param stackNo 电堆数据序号 1~253
* @param voltageV 燃料电池电堆电压 V0~2000最小 0.1 V
* @param currentA 燃料电池电堆电流 A0~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( public record Stack(
int stackNo, int stackNo,
Double voltageV, Double voltageV,
@@ -371,26 +305,18 @@ public sealed interface InfoBlock
) {} ) {}
} }
// ======================================================================== /** 0x31 超级电容器数据。变长。 */
// 2025 版 0x31 超级电容器数据(表 25
// ========================================================================
/** 超级电容器数据2025 版)。变长。 */
record SuperCapacitor( record SuperCapacitor(
int systemNo, int systemNo,
Double totalVoltageV, Double totalVoltageV,
Double totalCurrentA, Double totalCurrentA,
List<Double> cellVoltages, List<Double> cellVoltages,
List<Integer> probeTempsC List<Integer> probeTempsC
) implements InfoBlock { ) implements Gb32960V2025 {
@Override public InfoBlockType type() { return InfoBlockType.SUPER_CAPACITOR; } @Override public InfoBlockType type() { return InfoBlockType.SUPER_CAPACITOR; }
} }
// ======================================================================== /** 0x32 超级电容器极值数据。固定 18 字节。 */
// 2025 版 0x32 超级电容器极值数据(表 26
// ========================================================================
/** 超级电容器极值数据2025 版)。固定 14 字节。 */
record SuperCapacitorExtreme( record SuperCapacitorExtreme(
Integer maxVoltageSystemNo, Integer maxVoltageSystemNo,
Integer maxVoltageCellId, Integer maxVoltageCellId,
@@ -404,98 +330,103 @@ public sealed interface InfoBlock
Integer minTempSystemNo, Integer minTempSystemNo,
Integer minTempProbeId, Integer minTempProbeId,
Integer minTempC Integer minTempC
) implements InfoBlock { ) implements Gb32960V2025 {
@Override public InfoBlockType type() { return InfoBlockType.SUPER_CAPACITOR_EXTREME; } @Override public InfoBlockType type() { return InfoBlockType.SUPER_CAPACITOR_EXTREME; }
} }
}
// ======================================================================== // ========================================================================
// 广东燃料电池汽车示范规范扩展块typeCode 0x30~0x34、0x80 // 广东燃料电池汽车示范应用城市群综合监管平台规范 v1.0(基于 GB/T 32960.3-2016 帧头
// ======================================================================== // ========================================================================
/** /** 广东燃料电池汽车示范应用规范定义的扩展信息体typeCode 0x30~0x34、0x80。 */
* 广东规范 0x30 燃料电池电堆数据(表 13/14。变长。 sealed interface GuangdongFc extends InfoBlock
* 一个 stackCount + 多个 stack每个 stack 包含定长字段后跟 frameCellCount 个单体电压2B/个)。 permits GuangdongFc.Stack,
*/ GuangdongFc.Auxiliary,
record GdFcStack(int stackCount, List<Stack> stacks) implements InfoBlock { GuangdongFc.DcDc,
GuangdongFc.AirConditioner,
GuangdongFc.VehicleInfo,
GuangdongFc.DemoExtension {
/** 0x30 燃料电池电堆数据§7.2.3.4 表 13/14。变长。 */
record Stack(int stackCount, List<Item> stacks) implements GuangdongFc {
@Override public InfoBlockType type() { return InfoBlockType.GD_FC_STACK; } @Override public InfoBlockType type() { return InfoBlockType.GD_FC_STACK; }
public record Stack( public record Item(
Integer engineWorkState, // 1B Integer engineWorkState,
Integer stackWaterOutletTempC, // 1B (-40 offset) Integer stackWaterOutletTempC,
Double hydrogenInletPressureKpa, // 2B (-100 offset, 0.1 kPa) Double hydrogenInletPressureKpa,
Double airInletPressureKpa, // 2B (-100 offset, 0.1 kPa) Double airInletPressureKpa,
Integer airInletTempC, // 1B (-40 offset) Integer airInletTempC,
Integer maxCellVoltageId, // 2B Integer maxCellVoltageId,
Integer minCellVoltageId, // 2B Integer minCellVoltageId,
Double maxCellVoltageV, // 2B (0.001 V) Double maxCellVoltageV,
Double minCellVoltageV, // 2B (0.001 V) Double minCellVoltageV,
Double avgCellVoltageV, // 2B (0.001 V) Double avgCellVoltageV,
Integer cellCount, // 2B Integer cellCount,
Integer frameCellStart, // 2B Integer frameCellStart,
Integer frameCellCount, // 1B Integer frameCellCount,
List<Double> frameCellVoltagesV // 2*m B (0.001 V) List<Double> frameCellVoltagesV
) {} ) {}
} }
/** 广东规范 0x31 辅助系统数据(表 15/16。变长。 */ /** 0x31 辅助系统数据(§7.2.3.5 表 15/16。变长。 */
record GdFcAuxiliary(int subSystemCount, List<Subsystem> subsystems) implements InfoBlock { record Auxiliary(int subSystemCount, List<Subsystem> subsystems) implements GuangdongFc {
@Override public InfoBlockType type() { return InfoBlockType.GD_FC_AUXILIARY; } @Override public InfoBlockType type() { return InfoBlockType.GD_FC_AUXILIARY; }
public record Subsystem( public record Subsystem(
Double airCompressorMotorVoltageV, // 2B 0.1V Double airCompressorMotorVoltageV,
Integer airCompressorPowerKw, // 2B (-5 offset, 1 kw) Integer airCompressorPowerKw,
Double hydrogenPumpMotorVoltageV, // 2B 0.2V Double hydrogenPumpMotorVoltageV,
Integer hydrogenPumpPowerKw, // 2B (-5 offset, 1 kw) Integer hydrogenPumpPowerKw,
Double waterPumpVoltageV, // 2B 0.1V Double waterPumpVoltageV,
Double ptcVoltageV, // 2B 0.1V Double ptcVoltageV,
Integer ptcPowerKw, // 2B (-5 offset, 1 kw) Integer ptcPowerKw,
Double lowVoltageBatteryVoltageV // 2B 0.1V Double lowVoltageBatteryVoltageV
) {} ) {}
} }
/** 广东规范 0x32 DC/DC燃料电池系统数据表 17。固定 9 字节。 */ /** 0x32 DC/DC燃料电池系统数据§7.2.3.6 表 17。固定 9 字节。 */
record GdFcDcDc( record DcDc(
Double inputVoltageV, // 2B 0.1V (0~400V) Double inputVoltageV,
Double inputCurrentA, // 2B 0.1A (0~600A) Double inputCurrentA,
Double outputVoltageV, // 2B 0.1V (0~700V) Double outputVoltageV,
Double outputCurrentA, // 2B 0.1A (0~600A) Double outputCurrentA,
Integer controllerTempC // 1B -40 offset Integer controllerTempC
) implements InfoBlock { ) implements GuangdongFc {
@Override public InfoBlockType type() { return InfoBlockType.GD_FC_DCDC; } @Override public InfoBlockType type() { return InfoBlockType.GD_FC_DCDC; }
} }
/** 广东规范 0x33 空调数据(表 18。固定 5 字节。 */ /** 0x33 空调数据(§7.2.3.7 表 18。固定 5 字节。 */
record GdFcAirConditioner( record AirConditioner(
Integer status, // 1B 0 关闭 / 1 启动 Integer status,
Integer powerKw, // 2B (-5 offset, 1 kw, range -5~+30) Integer powerKw,
Double compressorInputVoltageV // 2B 0.1V (0~1000V) Double compressorInputVoltageV
) implements InfoBlock { ) implements GuangdongFc {
@Override public InfoBlockType type() { return InfoBlockType.GD_FC_AIR_CONDITIONER; } @Override public InfoBlockType type() { return InfoBlockType.GD_FC_AIR_CONDITIONER; }
} }
/** 广东规范 0x34 车辆信息数据(表 19。固定 7 字节。 */ /** 0x34 车辆信息数据(§7.2.3.8 表 19。固定 7 字节。 */
record GdFcVehicleInfo( record VehicleInfo(
Integer collisionAlarm, // 1B 0 无 / 1 有 Integer collisionAlarm,
Integer ambientTempC, // 2B (-40 offset, 1°C) Integer ambientTempC,
Double ambientPressureKpa, // 2B (-100 offset, 0.1 kPa) Double ambientPressureKpa,
Double hydrogenMassKg // 2B 0.1 kg Double hydrogenMassKg
) implements InfoBlock { ) implements GuangdongFc {
@Override public InfoBlockType type() { return InfoBlockType.GD_FC_VEHICLE_INFO; } @Override public InfoBlockType type() { return InfoBlockType.GD_FC_VEHICLE_INFO; }
} }
/** /** 0x80 燃料电池汽车示范应用数据扩展§7.2.3.14 表 27。 */
* 广东规范 0x80 燃料电池汽车示范应用数据扩展(表 27。固定 12 字节 record DemoExtension(
* (含 2 字节内层 length=9 + 9 字节数据)。 Integer stackTempC,
*/ Double airCompressorVoltageV,
record GdFcDemoExtension( Double airCompressorCurrentA,
Integer stackTempC, // 1B -40 offset Double hydrogenPumpVoltageV,
Double airCompressorVoltageV, // 2B 0.1V (0~2000V) Double hydrogenPumpCurrentA
Double airCompressorCurrentA, // 2B 0.1A (-1000 offset) ) implements GuangdongFc {
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; } @Override public InfoBlockType type() { return InfoBlockType.GD_FC_DEMO_EXTENSION; }
} }
}
// ======================================================================== // ========================================================================
// Raw / Unknown // Raw / Unknown

View File

@@ -78,7 +78,7 @@ class Gb32960DecoderGoldenTest {
.isPresent(); .isPresent();
} }
// 总电流必须落在协议规定的 -1000~+1000 A 区间,防回归到 -3000 偏移 // 总电流必须落在协议规定的 -1000~+1000 A 区间,防回归到 -3000 偏移
msg.findBlock(InfoBlock.Vehicle.class).ifPresent(v -> { msg.findBlock(InfoBlock.Gb32960V2016.Vehicle.class).ifPresent(v -> {
if (v.totalCurrentA() != null) { if (v.totalCurrentA() != null) {
assertThat(v.totalCurrentA()) assertThat(v.totalCurrentA())
.as("[%s] vehicle.totalCurrentA 越界,疑似偏移常量回归", name) .as("[%s] vehicle.totalCurrentA 越界,疑似偏移常量回归", name)

View File

@@ -37,12 +37,12 @@ public class Gb32960DecoderTest {
assertThat(msg.header().vin()).isEqualTo("LTEST000000000001"); assertThat(msg.header().vin()).isEqualTo("LTEST000000000001");
assertThat(msg.header().eventTime()).isNotNull(); 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.socPercent()).isEqualTo(70);
assertThat(v.speedKmh()).isEqualTo(52.3, org.assertj.core.data.Offset.offset(0.01)); assertThat(v.speedKmh()).isEqualTo(52.3, org.assertj.core.data.Offset.offset(0.01));
assertThat(v.gearRaw()).isEqualTo(0x0F); 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.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)); assertThat(p.latitude()).isEqualTo(39.916527, org.assertj.core.data.Offset.offset(0.000001));
} }

View File

@@ -48,7 +48,7 @@ class Gb32960FullBlocksTest {
writeU16(os, 11000); // current raw → 100 A writeU16(os, 11000); // current raw → 100 A
}); });
Gb32960Message msg = decoder.decode(ByteBuffer.wrap(frame)); 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); assertThat(dm.motors()).hasSize(1);
var m = dm.motors().get(0); var m = dm.motors().get(0);
assertThat(m.rpm()).isEqualTo(3000); assertThat(m.rpm()).isEqualTo(3000);
@@ -68,7 +68,7 @@ class Gb32960FullBlocksTest {
os.write(1); writeU32(os, 0xCAFE_BABEL); // other faults 1 os.write(1); writeU32(os, 0xCAFE_BABEL); // other faults 1
}); });
Gb32960Message msg = decoder.decode(ByteBuffer.wrap(frame)); 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.maxLevel()).isEqualTo(2);
assertThat(a.batteryFaults()).hasSize(1); assertThat(a.batteryFaults()).hasSize(1);
assertThat(a.otherFaults()).hasSize(1); assertThat(a.otherFaults()).hasSize(1);
@@ -90,7 +90,7 @@ class Gb32960FullBlocksTest {
writeU16(os, 3300); // 3.3V writeU16(os, 3300); // 3.3V
}); });
Gb32960Message msg = decoder.decode(ByteBuffer.wrap(frame)); 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.subSystemCount()).isEqualTo(1);
assertThat(v.maxCellVoltageV()).isEqualTo(3.6, org.assertj.core.data.Offset.offset(0.001)); 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)); 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 os.write(55); // actual 15
}); });
Gb32960Message msg = decoder.decode(ByteBuffer.wrap(frame)); 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.maxTempC()).isEqualTo(30);
assertThat(t.minTempC()).isEqualTo(15); assertThat(t.minTempC()).isEqualTo(15);
} }

View File

@@ -24,7 +24,8 @@ class GdFcParserTest {
0x04, 0x52, // 0x0452 = 1106 → 110.6 A 0x04, 0x52, // 0x0452 = 1106 → 110.6 A
0x69 // 0x69 = 105 - 40 = 65 °C 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.inputVoltageV()).isCloseTo(300.0, within(1e-6));
assertThat(b.inputCurrentA()).isCloseTo(120.0, within(1e-6)); assertThat(b.inputCurrentA()).isCloseTo(120.0, within(1e-6));
assertThat(b.outputVoltageV()).isCloseTo(560.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 0x00, 0x14, // 20 - 5 = 15 kw
0x15, (byte) 0x18 // 0x1518 = 5400 → 540.0 V 0x15, (byte) 0x18 // 0x1518 = 5400 → 540.0 V
}); });
InfoBlock.GdFcAirConditioner b = InfoBlock.GuangdongFc.AirConditioner b =
(InfoBlock.GdFcAirConditioner) new GdFcAirConditionerBlockParser().parse(buf); (InfoBlock.GuangdongFc.AirConditioner) new GdFcAirConditionerBlockParser().parse(buf);
assertThat(b.status()).isEqualTo(1); assertThat(b.status()).isEqualTo(1);
assertThat(b.powerKw()).isEqualTo(15); assertThat(b.powerKw()).isEqualTo(15);
assertThat(b.compressorInputVoltageV()).isCloseTo(540.0, within(1e-6)); 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 0x04, 0x4C, // 0x044C = 1100 → 110*0.1 - 100 = 10.0 kPa
0x00, 0x50 // 0x0050 = 80 → 8.0 kg 0x00, 0x50 // 0x0050 = 80 → 8.0 kg
}); });
InfoBlock.GdFcVehicleInfo b = InfoBlock.GuangdongFc.VehicleInfo b =
(InfoBlock.GdFcVehicleInfo) new GdFcVehicleInfoBlockParser().parse(buf); (InfoBlock.GuangdongFc.VehicleInfo) new GdFcVehicleInfoBlockParser().parse(buf);
assertThat(b.collisionAlarm()).isEqualTo(0); assertThat(b.collisionAlarm()).isEqualTo(0);
assertThat(b.ambientTempC()).isEqualTo(30); assertThat(b.ambientTempC()).isEqualTo(30);
assertThat(b.ambientPressureKpa()).isCloseTo(10.0, within(1e-6)); assertThat(b.ambientPressureKpa()).isCloseTo(10.0, within(1e-6));
@@ -75,8 +76,8 @@ class GdFcParserTest {
0x00, 0x0D, // 0x000D = 13 → 1.3V 0x00, 0x0D, // 0x000D = 13 → 1.3V
0x27, 0x10 // 0 A 0x27, 0x10 // 0 A
}); });
InfoBlock.GdFcDemoExtension b = InfoBlock.GuangdongFc.DemoExtension b =
(InfoBlock.GdFcDemoExtension) new GdFcDemoExtensionBlockParser().parse(buf); (InfoBlock.GuangdongFc.DemoExtension) new GdFcDemoExtensionBlockParser().parse(buf);
assertThat(b.stackTempC()).isEqualTo(50); assertThat(b.stackTempC()).isEqualTo(50);
assertThat(b.airCompressorVoltageV()).isCloseTo(372.0, within(1e-6)); assertThat(b.airCompressorVoltageV()).isCloseTo(372.0, within(1e-6));
assertThat(b.airCompressorCurrentA()).isCloseTo(0.0, within(1e-6)); assertThat(b.airCompressorCurrentA()).isCloseTo(0.0, within(1e-6));