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]),
"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 {

View File

@@ -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)