diff --git a/go/vehicle-gateway/internal/protocol/gb32960/parser.go b/go/vehicle-gateway/internal/protocol/gb32960/parser.go index fb66299e..93917ff4 100644 --- a/go/vehicle-gateway/internal/protocol/gb32960/parser.go +++ b/go/vehicle-gateway/internal/protocol/gb32960/parser.go @@ -368,10 +368,10 @@ func parseVehicleData(data []byte) map[string]any { "vehicle_status": int(data[0]), "charge_status": int(data[1]), "running_mode": int(data[2]), - "speed_kmh": float64(binary.BigEndian.Uint16(data[3:5])) / 10, - "total_mileage_km": float64(binary.BigEndian.Uint32(data[5:9])) / 10, - "total_voltage_v": float64(binary.BigEndian.Uint16(data[9:11])) / 10, - "total_current_a": float64(binary.BigEndian.Uint16(data[11:13]))/10 - 1000, + "speed_kmh": scaledU16(data[3:5], 10), + "total_mileage_km": scaledU32(data[5:9], 10), + "total_voltage_v": scaledU16(data[9:11], 10), + "total_current_a": scaledU16WithOffset(data[11:13], 10, -1000), "soc_percent": int(data[13]), "dc_dc_status": int(data[14]), "gear": int(data[15]), @@ -391,10 +391,10 @@ func parseDriveMotorData(data []byte) map[string]any { "state": int(data[cursor+1]), "controller_temperature_c": int(data[cursor+2]) - 40, "speed_rpm": int(binary.BigEndian.Uint16(data[cursor+3:cursor+5])) - 20000, - "torque_nm": float64(int(binary.BigEndian.Uint16(data[cursor+5:cursor+7]))-20000) / 10, + "torque_nm": scaledIntWithOffset(int64(binary.BigEndian.Uint16(data[cursor+5:cursor+7])), 10, -20000), "motor_temperature_c": int(data[cursor+7]) - 40, - "controller_voltage_v": float64(binary.BigEndian.Uint16(data[cursor+8:cursor+10])) / 10, - "controller_current_a": float64(binary.BigEndian.Uint16(data[cursor+10:cursor+12]))/10 - 1000, + "controller_voltage_v": scaledU16(data[cursor+8:cursor+10], 10), + "controller_current_a": scaledU16WithOffset(data[cursor+10:cursor+12], 10, -1000), }) cursor += 12 } @@ -413,16 +413,16 @@ func parseFuelCellData(data []byte) map[string]any { cursor++ } out := map[string]any{ - "fuel_cell_voltage_v": float64(binary.BigEndian.Uint16(data[0:2])) / 10, - "fuel_cell_current_a": float64(binary.BigEndian.Uint16(data[2:4])) / 10, - "hydrogen_consumption_kg_per_100km": float64(binary.BigEndian.Uint16(data[4:6])) / 100, + "fuel_cell_voltage_v": scaledU16(data[0:2], 10), + "fuel_cell_current_a": scaledU16(data[2:4], 10), + "hydrogen_consumption_kg_per_100km": scaledU16(data[4:6], 100), "temperature_probe_count": probeCount, "temperature_probe_values_c": probes, - "max_hydrogen_temperature_c": float64(binary.BigEndian.Uint16(data[cursor:cursor+2]))/10 - 40, + "max_hydrogen_temperature_c": scaledU16WithOffset(data[cursor:cursor+2], 10, -40), "max_hydrogen_temperature_probe_id": int(data[cursor+2]), - "max_hydrogen_concentration_fraction": float64(binary.BigEndian.Uint16(data[cursor+3:cursor+5])) * 0.000001, + "max_hydrogen_concentration_fraction": scaledU16(data[cursor+3:cursor+5], 1_000_000), "max_hydrogen_concentration_probe_id": int(data[cursor+5]), - "max_hydrogen_pressure_mpa": float64(binary.BigEndian.Uint16(data[cursor+6:cursor+8])) / 10, + "max_hydrogen_pressure_mpa": scaledU16(data[cursor+6:cursor+8], 10), "max_hydrogen_pressure_probe_id": int(data[cursor+8]), "dc_dc_status": int(data[cursor+9]), } @@ -441,10 +441,10 @@ func parseExtremeData(data []byte) map[string]any { return map[string]any{ "max_voltage_subsystem_no": int(data[0]), "max_voltage_cell_no": int(data[1]), - "max_voltage_v": float64(binary.BigEndian.Uint16(data[2:4])) / 1000, + "max_voltage_v": scaledU16(data[2:4], 1000), "min_voltage_subsystem_no": int(data[4]), "min_voltage_cell_no": int(data[5]), - "min_voltage_v": float64(binary.BigEndian.Uint16(data[6:8])) / 1000, + "min_voltage_v": scaledU16(data[6:8], 1000), "max_temp_subsystem_no": int(data[8]), "max_temp_probe_no": int(data[9]), "max_temp_c": int(data[10]) - 40, @@ -516,17 +516,17 @@ func voltageDataSize(data []byte) (int, bool) { func parseVoltageData(data []byte) map[string]any { subCount := int(data[0]) cursor := 1 - totalVoltage := 0.0 + totalVoltageTenths := int64(0) maxCell := 0.0 minCell := 0.0 cellSeen := false for i := 0; i < subCount; i++ { - totalVoltage += float64(binary.BigEndian.Uint16(data[cursor+1:cursor+3])) / 10 + totalVoltageTenths += int64(binary.BigEndian.Uint16(data[cursor+1 : cursor+3])) cellCount := int(binary.BigEndian.Uint16(data[cursor+5 : cursor+7])) frameCellCount := int(data[cursor+9]) cursor += 10 for c := 0; c < frameCellCount; c++ { - voltage := float64(binary.BigEndian.Uint16(data[cursor:cursor+2])) / 1000 + voltage := scaledU16(data[cursor:cursor+2], 1000) if !cellSeen || voltage > maxCell { maxCell = voltage } @@ -540,7 +540,7 @@ func parseVoltageData(data []byte) map[string]any { } return map[string]any{ "subsystem_count": subCount, - "total_voltage_v": totalVoltage, + "total_voltage_v": scaledInt(totalVoltageTenths, 10), "max_cell_v": maxCell, "min_cell_v": minCell, } @@ -619,14 +619,14 @@ func parseGdFCStackData(data []byte) map[string]any { 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), + "hydrogen_inlet_pressure_kpa": nullableScaledU16WithOffset(data[cursor+2:cursor+4], 10, -100), + "air_inlet_pressure_kpa": nullableScaledU16WithOffset(data[cursor+4:cursor+6], 10, -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), + "max_cell_voltage_v": nullableScaledU16(data[cursor+11:cursor+13], 1000), + "min_cell_voltage_v": nullableScaledU16(data[cursor+13:cursor+15], 1000), + "avg_cell_voltage_v": nullableScaledU16(data[cursor+15:cursor+17], 1000), "cell_count": nullableU16(data[cursor+17 : cursor+19]), "frame_cell_start": nullableU16(data[cursor+19 : cursor+21]), "frame_cell_count": frameCellCount, @@ -636,7 +636,7 @@ func parseGdFCStackData(data []byte) map[string]any { minCell := 0.0 seen := false for c := 0; c < frameCellCount; c++ { - voltage := float64(binary.BigEndian.Uint16(data[cursor:cursor+2])) / 1000 + voltage := scaledU16(data[cursor:cursor+2], 1000) if !seen || voltage > maxCell { maxCell = voltage } @@ -672,14 +672,14 @@ func parseGdFCAuxiliaryData(data []byte) map[string]any { 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_motor_voltage_v": nullableScaledU16(data[cursor:cursor+2], 10), "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_motor_voltage_v": nullableScaledU16(data[cursor+4:cursor+6], 5), "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), + "water_pump_voltage_v": nullableScaledU16(data[cursor+8:cursor+10], 10), + "ptc_voltage_v": nullableScaledU16(data[cursor+10:cursor+12], 10), "ptc_power_kw": nullableU16WithOffset(data[cursor+12:cursor+14], -5), - "low_voltage_battery_voltage_v": nullableScaledU16(data[cursor+14:cursor+16], 0.1), + "low_voltage_battery_voltage_v": nullableScaledU16(data[cursor+14:cursor+16], 10), }) cursor += 16 } @@ -691,10 +691,10 @@ func parseGdFCAuxiliaryData(data []byte) map[string]any { 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), + "input_voltage_v": nullableScaledU16(data[0:2], 10), + "input_current_a": nullableScaledU16(data[2:4], 10), + "output_voltage_v": nullableScaledU16(data[4:6], 10), + "output_current_a": nullableScaledU16(data[6:8], 10), "controller_temp_c": nullableTempOffset40(data[8]), } } @@ -703,7 +703,7 @@ 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), + "compressor_input_voltage_v": nullableScaledU16(data[3:5], 10), } } @@ -711,8 +711,8 @@ 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), + "ambient_pressure_kpa": nullableScaledU16WithOffset(data[3:5], 10, -100), + "hydrogen_mass_kg": nullableScaledU16(data[5:7], 10), } } @@ -720,10 +720,10 @@ 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), + "air_compressor_voltage_v": nullableScaledU16(data[3:5], 10), + "air_compressor_current_a": nullableScaledU16WithOffset(data[5:7], 10, -1000), + "hydrogen_pump_voltage_v": nullableScaledU16(data[7:9], 10), + "hydrogen_pump_current_a": nullableScaledU16WithOffset(data[9:11], 10, -1000), } } @@ -760,20 +760,43 @@ func nullableU16WithOffset(data []byte, offset int) any { 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 scaledU16(data []byte, divisor int64) float64 { + return scaledInt(int64(binary.BigEndian.Uint16(data)), divisor) } -func nullableScaledU16WithOffset(data []byte, scale float64, offset float64) any { +func scaledU32(data []byte, divisor int64) float64 { + return scaledInt(int64(binary.BigEndian.Uint32(data)), divisor) +} + +func scaledU16WithOffset(data []byte, divisor int64, offset int64) float64 { + return scaledIntWithOffset(int64(binary.BigEndian.Uint16(data)), divisor, offset) +} + +func scaledIntWithOffset(value int64, divisor int64, offset int64) float64 { + return scaledInt(value+offset*divisor, divisor) +} + +func scaledInt(value int64, divisor int64) float64 { + if divisor <= 0 { + return float64(value) + } + return float64(value) / float64(divisor) +} + +func nullableScaledU16(data []byte, divisor int64) any { value := binary.BigEndian.Uint16(data) if value == 0xfffe || value == 0xffff { return nil } - return float64(value)*scale + offset + return scaledInt(int64(value), divisor) +} + +func nullableScaledU16WithOffset(data []byte, divisor int64, offset int64) any { + value := binary.BigEndian.Uint16(data) + if value == 0xfffe || value == 0xffff { + return nil + } + return scaledIntWithOffset(int64(value), divisor, offset) } func nullableTempOffset40(value byte) any { diff --git a/go/vehicle-gateway/internal/protocol/gb32960/parser_test.go b/go/vehicle-gateway/internal/protocol/gb32960/parser_test.go index cc7dc6f2..05c78f93 100644 --- a/go/vehicle-gateway/internal/protocol/gb32960/parser_test.go +++ b/go/vehicle-gateway/internal/protocol/gb32960/parser_test.go @@ -2,8 +2,10 @@ package gb32960 import ( "encoding/hex" + "encoding/json" "fmt" "math" + "strings" "testing" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" @@ -213,6 +215,54 @@ func TestParseFrameKeepsParsingRealFuelCellReportUntilUnknownExtension(t *testin } } +func TestScaledDecimalFieldsDoNotLeakBinaryFloatTails(t *testing.T) { + unit := parseDriveMotorData([]byte{ + 0x01, + 0x01, + 0x02, + 0x5d, + 0x6c, 0xc6, + 0x4e, 0x20, + 0x64, + 0x15, 0xb8, + 0x27, 0x6e, // 10094 -> 9.4A after -1000 offset. + }) + data, err := json.Marshal(unit) + if err != nil { + t.Fatal(err) + } + text := string(data) + if !strings.Contains(text, `"controller_current_a":9.4`) { + t.Fatalf("controller_current_a should render as 9.4 without float tail: %s", text) + } + if strings.Contains(text, "999999") || strings.Contains(text, "000000000000") { + t.Fatalf("scaled decimal field leaked binary float tail: %s", text) + } + + aux := parseGdFCAuxiliaryData([]byte{ + 0x01, + 0x01, 0x74, + 0x00, 0x05, + 0xff, 0xff, + 0x00, 0x05, + 0x01, 0x74, + 0xff, 0xff, + 0x00, 0x05, + 0x00, 0x84, // 132 -> 13.2V. + }) + data, err = json.Marshal(aux) + if err != nil { + t.Fatal(err) + } + text = string(data) + if !strings.Contains(text, `"low_voltage_battery_voltage_v":13.2`) { + t.Fatalf("low voltage should render as 13.2 without float tail: %s", text) + } + if strings.Contains(text, "13.200000000000") { + t.Fatalf("auxiliary scaled decimal leaked binary float tail: %s", text) + } +} + func assertFloatField(t *testing.T, env envelope.FrameEnvelope, key string, want float64) { t.Helper() got, ok := env.Fields[key].(float64)