feat: parse gb32960 guangdong fuel cell blocks

This commit is contained in:
lingniu
2026-07-01 22:27:34 +08:00
parent 796d79dd30
commit a15de91912
2 changed files with 287 additions and 3 deletions

View File

@@ -222,6 +222,74 @@ func parseDataBody(version string, body []byte, fields map[string]any) (time.Tim
unit := parseTemperatureData(body[cursor : cursor+size]) unit := parseTemperatureData(body[cursor : cursor+size])
units = append(units, map[string]any{"type": "0x09", "name": "temperature", "value": unit}) units = append(units, map[string]any{"type": "0x09", "name": "temperature", "value": unit})
cursor += size cursor += size
case 0x30:
size, ok := gdFCStackDataSize(body[cursor:])
if !ok {
units = append(units, map[string]any{"type": "0x30", "error": "truncated"})
return eventTime, units
}
unit := parseGdFCStackData(body[cursor : cursor+size])
units = append(units, map[string]any{"type": "0x30", "name": "gd_fc_stack", "value": unit})
if summaries, ok := unit["summaries"].([]map[string]any); ok && len(summaries) > 0 {
fields["gd_fc_stack_hydrogen_inlet_pressure_kpa"] = summaries[0]["hydrogen_inlet_pressure_kpa"]
fields["gd_fc_stack_water_outlet_temp_c"] = summaries[0]["stack_water_outlet_temp_c"]
fields["gd_fc_stack_cell_count"] = summaries[0]["cell_count"]
fields["gd_fc_stack_frame_cell_count"] = summaries[0]["frame_cell_count"]
}
cursor += size
case 0x31:
size, ok := gdFCAuxiliaryDataSize(body[cursor:])
if !ok {
units = append(units, map[string]any{"type": "0x31", "error": "truncated"})
return eventTime, units
}
unit := parseGdFCAuxiliaryData(body[cursor : cursor+size])
units = append(units, map[string]any{"type": "0x31", "name": "gd_fc_auxiliary", "value": unit})
cursor += size
case 0x32:
if len(body[cursor:]) < 9 {
units = append(units, map[string]any{"type": "0x32", "error": "truncated"})
return eventTime, units
}
unit := parseGdFCDcDcData(body[cursor : cursor+9])
units = append(units, map[string]any{"type": "0x32", "name": "gd_fc_dcdc", "value": unit})
fields["gd_fc_dcdc_controller_temp_c"] = unit["controller_temp_c"]
cursor += 9
case 0x33:
if len(body[cursor:]) < 5 {
units = append(units, map[string]any{"type": "0x33", "error": "truncated"})
return eventTime, units
}
unit := parseGdFCAirConditionerData(body[cursor : cursor+5])
units = append(units, map[string]any{"type": "0x33", "name": "gd_fc_air_conditioner", "value": unit})
cursor += 5
case 0x34:
if len(body[cursor:]) < 7 {
units = append(units, map[string]any{"type": "0x34", "error": "truncated"})
return eventTime, units
}
unit := parseGdFCVehicleInfoData(body[cursor : cursor+7])
units = append(units, map[string]any{"type": "0x34", "name": "gd_fc_vehicle_info", "value": unit})
fields["gd_fc_vehicle_hydrogen_mass_kg"] = unit["hydrogen_mass_kg"]
cursor += 7
case 0x80:
if len(body[cursor:]) < 11 {
units = append(units, map[string]any{"type": "0x80", "error": "truncated"})
return eventTime, units
}
unit := parseGdFCDemoExtensionData(body[cursor : cursor+11])
units = append(units, map[string]any{"type": "0x80", "name": "gd_fc_demo_extension", "value": unit})
fields["gd_fc_demo_stack_temp_c"] = unit["stack_temp_c"]
cursor += 11
case 0x83:
size, ok := gdFCVendorTLVDataSize(body[cursor:])
if !ok {
units = append(units, map[string]any{"type": "0x83", "error": "truncated"})
return eventTime, units
}
unit := parseGdFCVendorTLVData(0x83, body[cursor:cursor+size])
units = append(units, map[string]any{"type": "0x83", "name": "gd_fc_vendor_tlv", "value": unit})
cursor += size
default: default:
units = append(units, map[string]any{ units = append(units, map[string]any{
"type": fmt.Sprintf("0x%02X", unitType), "type": fmt.Sprintf("0x%02X", unitType),
@@ -464,6 +532,197 @@ func parseTemperatureData(data []byte) map[string]any {
} }
} }
func gdFCStackDataSize(data []byte) (int, bool) {
if len(data) < 1 {
return 0, false
}
cursor := 1
for i := 0; i < int(data[0]); i++ {
if len(data[cursor:]) < 22 {
return 0, false
}
frameCellCount := int(data[cursor+21])
cursor += 22 + frameCellCount*2
if cursor > len(data) {
return 0, false
}
}
return cursor, true
}
func parseGdFCStackData(data []byte) map[string]any {
count := int(data[0])
cursor := 1
summaries := make([]map[string]any, 0, count)
for i := 0; i < count; i++ {
frameCellCount := int(data[cursor+21])
summary := map[string]any{
"engine_work_state": int(data[cursor]),
"stack_water_outlet_temp_c": nullableTempOffset40(data[cursor+1]),
"hydrogen_inlet_pressure_kpa": nullableScaledU16WithOffset(data[cursor+2:cursor+4], 0.1, -100),
"air_inlet_pressure_kpa": nullableScaledU16WithOffset(data[cursor+4:cursor+6], 0.1, -100),
"air_inlet_temp_c": nullableTempOffset40(data[cursor+6]),
"max_cell_voltage_id": nullableU16(data[cursor+7 : cursor+9]),
"min_cell_voltage_id": nullableU16(data[cursor+9 : cursor+11]),
"max_cell_voltage_v": nullableScaledU16(data[cursor+11:cursor+13], 0.001),
"min_cell_voltage_v": nullableScaledU16(data[cursor+13:cursor+15], 0.001),
"avg_cell_voltage_v": nullableScaledU16(data[cursor+15:cursor+17], 0.001),
"cell_count": nullableU16(data[cursor+17 : cursor+19]),
"frame_cell_start": nullableU16(data[cursor+19 : cursor+21]),
"frame_cell_count": frameCellCount,
}
cursor += 22
maxCell := 0.0
minCell := 0.0
seen := false
for c := 0; c < frameCellCount; c++ {
voltage := float64(binary.BigEndian.Uint16(data[cursor:cursor+2])) / 1000
if !seen || voltage > maxCell {
maxCell = voltage
}
if !seen || voltage < minCell {
minCell = voltage
}
seen = true
cursor += 2
}
if seen {
summary["frame_max_cell_voltage_v"] = maxCell
summary["frame_min_cell_voltage_v"] = minCell
}
summaries = append(summaries, summary)
}
return map[string]any{
"stack_count": count,
"summaries": summaries,
}
}
func gdFCAuxiliaryDataSize(data []byte) (int, bool) {
if len(data) < 1 {
return 0, false
}
size := 1 + int(data[0])*16
return size, len(data) >= size
}
func parseGdFCAuxiliaryData(data []byte) map[string]any {
count := int(data[0])
cursor := 1
subsystems := make([]map[string]any, 0, count)
for i := 0; i < count; i++ {
subsystems = append(subsystems, map[string]any{
"air_compressor_motor_voltage_v": nullableScaledU16(data[cursor:cursor+2], 0.1),
"air_compressor_power_kw": nullableU16WithOffset(data[cursor+2:cursor+4], -5),
"hydrogen_pump_motor_voltage_v": nullableScaledU16(data[cursor+4:cursor+6], 0.2),
"hydrogen_pump_power_kw": nullableU16WithOffset(data[cursor+6:cursor+8], -5),
"water_pump_voltage_v": nullableScaledU16(data[cursor+8:cursor+10], 0.1),
"ptc_voltage_v": nullableScaledU16(data[cursor+10:cursor+12], 0.1),
"ptc_power_kw": nullableU16WithOffset(data[cursor+12:cursor+14], -5),
"low_voltage_battery_voltage_v": nullableScaledU16(data[cursor+14:cursor+16], 0.1),
})
cursor += 16
}
return map[string]any{
"subsystem_count": count,
"subsystems": subsystems,
}
}
func parseGdFCDcDcData(data []byte) map[string]any {
return map[string]any{
"input_voltage_v": nullableScaledU16(data[0:2], 0.1),
"input_current_a": nullableScaledU16(data[2:4], 0.1),
"output_voltage_v": nullableScaledU16(data[4:6], 0.1),
"output_current_a": nullableScaledU16(data[6:8], 0.1),
"controller_temp_c": nullableTempOffset40(data[8]),
}
}
func parseGdFCAirConditionerData(data []byte) map[string]any {
return map[string]any{
"status": int(data[0]),
"power_kw": nullableU16WithOffset(data[1:3], -5),
"compressor_input_voltage_v": nullableScaledU16(data[3:5], 0.1),
}
}
func parseGdFCVehicleInfoData(data []byte) map[string]any {
return map[string]any{
"collision_alarm": int(data[0]),
"ambient_temp_c": nullableU16WithOffset(data[1:3], -40),
"ambient_pressure_kpa": nullableScaledU16WithOffset(data[3:5], 0.1, -100),
"hydrogen_mass_kg": nullableScaledU16(data[5:7], 0.1),
}
}
func parseGdFCDemoExtensionData(data []byte) map[string]any {
return map[string]any{
"declared_length": int(binary.BigEndian.Uint16(data[0:2])),
"stack_temp_c": nullableTempOffset40(data[2]),
"air_compressor_voltage_v": nullableScaledU16(data[3:5], 0.1),
"air_compressor_current_a": nullableScaledU16WithOffset(data[5:7], 0.1, -1000),
"hydrogen_pump_voltage_v": nullableScaledU16(data[7:9], 0.1),
"hydrogen_pump_current_a": nullableScaledU16WithOffset(data[9:11], 0.1, -1000),
}
}
func gdFCVendorTLVDataSize(data []byte) (int, bool) {
if len(data) < 2 {
return 0, false
}
size := 2 + int(binary.BigEndian.Uint16(data[0:2]))
return size, len(data) >= size
}
func parseGdFCVendorTLVData(typeCode byte, data []byte) map[string]any {
length := int(binary.BigEndian.Uint16(data[0:2]))
return map[string]any{
"type": fmt.Sprintf("0x%02X", typeCode),
"declared_length": length,
"payload_hex": strings.ToUpper(hex.EncodeToString(data[2:])),
}
}
func nullableU16(data []byte) any {
value := binary.BigEndian.Uint16(data)
if value == 0xfffe || value == 0xffff {
return nil
}
return int(value)
}
func nullableU16WithOffset(data []byte, offset int) any {
value := binary.BigEndian.Uint16(data)
if value == 0xfffe || value == 0xffff {
return nil
}
return int(value) + offset
}
func nullableScaledU16(data []byte, scale float64) any {
value := binary.BigEndian.Uint16(data)
if value == 0xfffe || value == 0xffff {
return nil
}
return float64(value) * scale
}
func nullableScaledU16WithOffset(data []byte, scale float64, offset float64) any {
value := binary.BigEndian.Uint16(data)
if value == 0xfffe || value == 0xffff {
return nil
}
return float64(value)*scale + offset
}
func nullableTempOffset40(value byte) any {
if value == 0xfe || value == 0xff {
return nil
}
return int(value) - 40
}
func parsePositionData(data []byte) map[string]any { func parsePositionData(data []byte) map[string]any {
return map[string]any{ return map[string]any{
"position_status": int(data[0]), "position_status": int(data[0]),

View File

@@ -3,6 +3,7 @@ package gb32960
import ( import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"math"
"testing" "testing"
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
@@ -102,6 +103,9 @@ func TestParseFrameKeepsParsingRealFuelCellReportUntilUnknownExtension(t *testin
assertFloatField(t, env, "fuel_cell_hydrogen_consumption_kg_per_100km", 1.8) assertFloatField(t, env, "fuel_cell_hydrogen_consumption_kg_per_100km", 1.8)
assertFloatField(t, env, envelope.FieldLongitude, 120.800326) assertFloatField(t, env, envelope.FieldLongitude, 120.800326)
assertFloatField(t, env, envelope.FieldLatitude, 31.634907) assertFloatField(t, env, envelope.FieldLatitude, 31.634907)
assertFloatField(t, env, "gd_fc_stack_hydrogen_inlet_pressure_kpa", 97.0)
assertFloatField(t, env, "gd_fc_vehicle_hydrogen_mass_kg", 4.3)
assertIntField(t, env, "gd_fc_demo_stack_temp_c", 65)
units, ok := env.Parsed["data_units"].([]map[string]any) units, ok := env.Parsed["data_units"].([]map[string]any)
if !ok { if !ok {
@@ -111,7 +115,7 @@ func TestParseFrameKeepsParsingRealFuelCellReportUntilUnknownExtension(t *testin
for _, unit := range units { for _, unit := range units {
gotTypes = append(gotTypes, fmt.Sprint(unit["type"])) gotTypes = append(gotTypes, fmt.Sprint(unit["type"]))
} }
wantTypes := []string{"0x01", "0x02", "0x03", "0x04", "0x05", "0x06", "0x07", "0x08", "0x09", "0x30"} wantTypes := []string{"0x01", "0x02", "0x03", "0x04", "0x05", "0x06", "0x07", "0x08", "0x09", "0x30", "0x31", "0x32", "0x33", "0x34", "0x80", "0x83"}
if fmt.Sprint(gotTypes) != fmt.Sprint(wantTypes) { if fmt.Sprint(gotTypes) != fmt.Sprint(wantTypes) {
t.Fatalf("unit types = %v, want %v", gotTypes, wantTypes) t.Fatalf("unit types = %v, want %v", gotTypes, wantTypes)
} }
@@ -121,8 +125,18 @@ func TestParseFrameKeepsParsingRealFuelCellReportUntilUnknownExtension(t *testin
if units[2]["name"] != "fuel_cell" { if units[2]["name"] != "fuel_cell" {
t.Fatalf("unexpected fuel cell unit: %#v", units[2]) t.Fatalf("unexpected fuel cell unit: %#v", units[2])
} }
if units[9]["parse"] != "unknown_unit" { if units[9]["name"] != "gd_fc_stack" {
t.Fatalf("unexpected unknown unit: %#v", units[9]) t.Fatalf("unexpected gd stack unit: %#v", units[9])
}
if units[10]["name"] != "gd_fc_auxiliary" {
t.Fatalf("unexpected gd auxiliary unit: %#v", units[10])
}
if units[15]["name"] != "gd_fc_vendor_tlv" {
t.Fatalf("unexpected gd vendor tlv unit: %#v", units[15])
}
tlv := units[15]["value"].(map[string]any)
if tlv["declared_length"] != 36 {
t.Fatalf("unexpected vendor tlv: %#v", tlv)
} }
} }
@@ -132,6 +146,17 @@ func assertFloatField(t *testing.T, env envelope.FrameEnvelope, key string, want
if !ok { if !ok {
t.Fatalf("field %s missing or not float64: %#v", key, env.Fields[key]) t.Fatalf("field %s missing or not float64: %#v", key, env.Fields[key])
} }
if math.Abs(got-want) > 0.000001 {
t.Fatalf("field %s = %v, want %v", key, got, want)
}
}
func assertIntField(t *testing.T, env envelope.FrameEnvelope, key string, want int) {
t.Helper()
got, ok := env.Fields[key].(int)
if !ok {
t.Fatalf("field %s missing or not int: %#v", key, env.Fields[key])
}
if got != want { if got != want {
t.Fatalf("field %s = %v, want %v", key, got, want) t.Fatalf("field %s = %v, want %v", key, got, want)
} }