fix(go): avoid gb32960 decimal float tails
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user