feat: parse gb32960 guangdong fuel cell blocks
This commit is contained in:
@@ -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]),
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user