fix(go): avoid gb32960 decimal float tails

This commit is contained in:
lingniu
2026-07-03 14:47:11 +08:00
parent 96e9382d73
commit a0a4997e79
2 changed files with 122 additions and 49 deletions

View File

@@ -368,10 +368,10 @@ func parseVehicleData(data []byte) map[string]any {
"vehicle_status": int(data[0]), "vehicle_status": int(data[0]),
"charge_status": int(data[1]), "charge_status": int(data[1]),
"running_mode": int(data[2]), "running_mode": int(data[2]),
"speed_kmh": float64(binary.BigEndian.Uint16(data[3:5])) / 10, "speed_kmh": scaledU16(data[3:5], 10),
"total_mileage_km": float64(binary.BigEndian.Uint32(data[5:9])) / 10, "total_mileage_km": scaledU32(data[5:9], 10),
"total_voltage_v": float64(binary.BigEndian.Uint16(data[9:11])) / 10, "total_voltage_v": scaledU16(data[9:11], 10),
"total_current_a": float64(binary.BigEndian.Uint16(data[11:13]))/10 - 1000, "total_current_a": scaledU16WithOffset(data[11:13], 10, -1000),
"soc_percent": int(data[13]), "soc_percent": int(data[13]),
"dc_dc_status": int(data[14]), "dc_dc_status": int(data[14]),
"gear": int(data[15]), "gear": int(data[15]),
@@ -391,10 +391,10 @@ func parseDriveMotorData(data []byte) map[string]any {
"state": int(data[cursor+1]), "state": int(data[cursor+1]),
"controller_temperature_c": int(data[cursor+2]) - 40, "controller_temperature_c": int(data[cursor+2]) - 40,
"speed_rpm": int(binary.BigEndian.Uint16(data[cursor+3:cursor+5])) - 20000, "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, "motor_temperature_c": int(data[cursor+7]) - 40,
"controller_voltage_v": float64(binary.BigEndian.Uint16(data[cursor+8:cursor+10])) / 10, "controller_voltage_v": scaledU16(data[cursor+8:cursor+10], 10),
"controller_current_a": float64(binary.BigEndian.Uint16(data[cursor+10:cursor+12]))/10 - 1000, "controller_current_a": scaledU16WithOffset(data[cursor+10:cursor+12], 10, -1000),
}) })
cursor += 12 cursor += 12
} }
@@ -413,16 +413,16 @@ func parseFuelCellData(data []byte) map[string]any {
cursor++ cursor++
} }
out := map[string]any{ out := map[string]any{
"fuel_cell_voltage_v": float64(binary.BigEndian.Uint16(data[0:2])) / 10, "fuel_cell_voltage_v": scaledU16(data[0:2], 10),
"fuel_cell_current_a": float64(binary.BigEndian.Uint16(data[2:4])) / 10, "fuel_cell_current_a": scaledU16(data[2:4], 10),
"hydrogen_consumption_kg_per_100km": float64(binary.BigEndian.Uint16(data[4:6])) / 100, "hydrogen_consumption_kg_per_100km": scaledU16(data[4:6], 100),
"temperature_probe_count": probeCount, "temperature_probe_count": probeCount,
"temperature_probe_values_c": probes, "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_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_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]), "max_hydrogen_pressure_probe_id": int(data[cursor+8]),
"dc_dc_status": int(data[cursor+9]), "dc_dc_status": int(data[cursor+9]),
} }
@@ -441,10 +441,10 @@ func parseExtremeData(data []byte) map[string]any {
return map[string]any{ return map[string]any{
"max_voltage_subsystem_no": int(data[0]), "max_voltage_subsystem_no": int(data[0]),
"max_voltage_cell_no": int(data[1]), "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_subsystem_no": int(data[4]),
"min_voltage_cell_no": int(data[5]), "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_subsystem_no": int(data[8]),
"max_temp_probe_no": int(data[9]), "max_temp_probe_no": int(data[9]),
"max_temp_c": int(data[10]) - 40, "max_temp_c": int(data[10]) - 40,
@@ -516,17 +516,17 @@ func voltageDataSize(data []byte) (int, bool) {
func parseVoltageData(data []byte) map[string]any { func parseVoltageData(data []byte) map[string]any {
subCount := int(data[0]) subCount := int(data[0])
cursor := 1 cursor := 1
totalVoltage := 0.0 totalVoltageTenths := int64(0)
maxCell := 0.0 maxCell := 0.0
minCell := 0.0 minCell := 0.0
cellSeen := false cellSeen := false
for i := 0; i < subCount; i++ { 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])) cellCount := int(binary.BigEndian.Uint16(data[cursor+5 : cursor+7]))
frameCellCount := int(data[cursor+9]) frameCellCount := int(data[cursor+9])
cursor += 10 cursor += 10
for c := 0; c < frameCellCount; c++ { 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 { if !cellSeen || voltage > maxCell {
maxCell = voltage maxCell = voltage
} }
@@ -540,7 +540,7 @@ func parseVoltageData(data []byte) map[string]any {
} }
return map[string]any{ return map[string]any{
"subsystem_count": subCount, "subsystem_count": subCount,
"total_voltage_v": totalVoltage, "total_voltage_v": scaledInt(totalVoltageTenths, 10),
"max_cell_v": maxCell, "max_cell_v": maxCell,
"min_cell_v": minCell, "min_cell_v": minCell,
} }
@@ -619,14 +619,14 @@ func parseGdFCStackData(data []byte) map[string]any {
summary := map[string]any{ summary := map[string]any{
"engine_work_state": int(data[cursor]), "engine_work_state": int(data[cursor]),
"stack_water_outlet_temp_c": nullableTempOffset40(data[cursor+1]), "stack_water_outlet_temp_c": nullableTempOffset40(data[cursor+1]),
"hydrogen_inlet_pressure_kpa": nullableScaledU16WithOffset(data[cursor+2:cursor+4], 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], 0.1, -100), "air_inlet_pressure_kpa": nullableScaledU16WithOffset(data[cursor+4:cursor+6], 10, -100),
"air_inlet_temp_c": nullableTempOffset40(data[cursor+6]), "air_inlet_temp_c": nullableTempOffset40(data[cursor+6]),
"max_cell_voltage_id": nullableU16(data[cursor+7 : cursor+9]), "max_cell_voltage_id": nullableU16(data[cursor+7 : cursor+9]),
"min_cell_voltage_id": nullableU16(data[cursor+9 : cursor+11]), "min_cell_voltage_id": nullableU16(data[cursor+9 : cursor+11]),
"max_cell_voltage_v": nullableScaledU16(data[cursor+11:cursor+13], 0.001), "max_cell_voltage_v": nullableScaledU16(data[cursor+11:cursor+13], 1000),
"min_cell_voltage_v": nullableScaledU16(data[cursor+13:cursor+15], 0.001), "min_cell_voltage_v": nullableScaledU16(data[cursor+13:cursor+15], 1000),
"avg_cell_voltage_v": nullableScaledU16(data[cursor+15:cursor+17], 0.001), "avg_cell_voltage_v": nullableScaledU16(data[cursor+15:cursor+17], 1000),
"cell_count": nullableU16(data[cursor+17 : cursor+19]), "cell_count": nullableU16(data[cursor+17 : cursor+19]),
"frame_cell_start": nullableU16(data[cursor+19 : cursor+21]), "frame_cell_start": nullableU16(data[cursor+19 : cursor+21]),
"frame_cell_count": frameCellCount, "frame_cell_count": frameCellCount,
@@ -636,7 +636,7 @@ func parseGdFCStackData(data []byte) map[string]any {
minCell := 0.0 minCell := 0.0
seen := false seen := false
for c := 0; c < frameCellCount; c++ { 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 { if !seen || voltage > maxCell {
maxCell = voltage maxCell = voltage
} }
@@ -672,14 +672,14 @@ func parseGdFCAuxiliaryData(data []byte) map[string]any {
subsystems := make([]map[string]any, 0, count) subsystems := make([]map[string]any, 0, count)
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
subsystems = append(subsystems, map[string]any{ 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), "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), "hydrogen_pump_power_kw": nullableU16WithOffset(data[cursor+6:cursor+8], -5),
"water_pump_voltage_v": nullableScaledU16(data[cursor+8:cursor+10], 0.1), "water_pump_voltage_v": nullableScaledU16(data[cursor+8:cursor+10], 10),
"ptc_voltage_v": nullableScaledU16(data[cursor+10:cursor+12], 0.1), "ptc_voltage_v": nullableScaledU16(data[cursor+10:cursor+12], 10),
"ptc_power_kw": nullableU16WithOffset(data[cursor+12:cursor+14], -5), "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 cursor += 16
} }
@@ -691,10 +691,10 @@ func parseGdFCAuxiliaryData(data []byte) map[string]any {
func parseGdFCDcDcData(data []byte) map[string]any { func parseGdFCDcDcData(data []byte) map[string]any {
return map[string]any{ return map[string]any{
"input_voltage_v": nullableScaledU16(data[0:2], 0.1), "input_voltage_v": nullableScaledU16(data[0:2], 10),
"input_current_a": nullableScaledU16(data[2:4], 0.1), "input_current_a": nullableScaledU16(data[2:4], 10),
"output_voltage_v": nullableScaledU16(data[4:6], 0.1), "output_voltage_v": nullableScaledU16(data[4:6], 10),
"output_current_a": nullableScaledU16(data[6:8], 0.1), "output_current_a": nullableScaledU16(data[6:8], 10),
"controller_temp_c": nullableTempOffset40(data[8]), "controller_temp_c": nullableTempOffset40(data[8]),
} }
} }
@@ -703,7 +703,7 @@ func parseGdFCAirConditionerData(data []byte) map[string]any {
return map[string]any{ return map[string]any{
"status": int(data[0]), "status": int(data[0]),
"power_kw": nullableU16WithOffset(data[1:3], -5), "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{ return map[string]any{
"collision_alarm": int(data[0]), "collision_alarm": int(data[0]),
"ambient_temp_c": nullableU16WithOffset(data[1:3], -40), "ambient_temp_c": nullableU16WithOffset(data[1:3], -40),
"ambient_pressure_kpa": nullableScaledU16WithOffset(data[3:5], 0.1, -100), "ambient_pressure_kpa": nullableScaledU16WithOffset(data[3:5], 10, -100),
"hydrogen_mass_kg": nullableScaledU16(data[5:7], 0.1), "hydrogen_mass_kg": nullableScaledU16(data[5:7], 10),
} }
} }
@@ -720,10 +720,10 @@ func parseGdFCDemoExtensionData(data []byte) map[string]any {
return map[string]any{ return map[string]any{
"declared_length": int(binary.BigEndian.Uint16(data[0:2])), "declared_length": int(binary.BigEndian.Uint16(data[0:2])),
"stack_temp_c": nullableTempOffset40(data[2]), "stack_temp_c": nullableTempOffset40(data[2]),
"air_compressor_voltage_v": nullableScaledU16(data[3:5], 0.1), "air_compressor_voltage_v": nullableScaledU16(data[3:5], 10),
"air_compressor_current_a": nullableScaledU16WithOffset(data[5:7], 0.1, -1000), "air_compressor_current_a": nullableScaledU16WithOffset(data[5:7], 10, -1000),
"hydrogen_pump_voltage_v": nullableScaledU16(data[7:9], 0.1), "hydrogen_pump_voltage_v": nullableScaledU16(data[7:9], 10),
"hydrogen_pump_current_a": nullableScaledU16WithOffset(data[9:11], 0.1, -1000), "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 return int(value) + offset
} }
func nullableScaledU16(data []byte, scale float64) any { func scaledU16(data []byte, divisor int64) float64 {
value := binary.BigEndian.Uint16(data) return scaledInt(int64(binary.BigEndian.Uint16(data)), divisor)
if value == 0xfffe || value == 0xffff {
return nil
}
return float64(value) * scale
} }
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) value := binary.BigEndian.Uint16(data)
if value == 0xfffe || value == 0xffff { if value == 0xfffe || value == 0xffff {
return nil 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 { func nullableTempOffset40(value byte) any {

View File

@@ -2,8 +2,10 @@ package gb32960
import ( import (
"encoding/hex" "encoding/hex"
"encoding/json"
"fmt" "fmt"
"math" "math"
"strings"
"testing" "testing"
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" "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) { func assertFloatField(t *testing.T, env envelope.FrameEnvelope, key string, want float64) {
t.Helper() t.Helper()
got, ok := env.Fields[key].(float64) got, ok := env.Fields[key].(float64)