feat(platform): harden telemetry pipeline and unify Semi UI workspaces

This commit is contained in:
lingniu
2026-07-18 00:26:36 +08:00
parent 65b4e4f055
commit 159c80b0ae
136 changed files with 21616 additions and 1785 deletions

View File

@@ -151,16 +151,23 @@ func parseDataBody(version string, body []byte, fields map[string]any) (time.Tim
fields[envelope.FieldSOCPercent] = unit["soc_percent"]
cursor += size
case 0x05:
if len(body[cursor:]) < 9 {
size := 9
if version == "V2025" {
size = 10
}
if len(body[cursor:]) < size {
units = append(units, map[string]any{"type": "0x05", "error": "truncated"})
return eventTime, units
}
unit := parsePositionData(body[cursor : cursor+9])
unit := parsePositionData(version, body[cursor:cursor+size])
units = append(units, map[string]any{"type": "0x05", "name": "position", "value": unit})
fields["position_status"] = unit["position_status"]
if coordinateSystem, ok := unit["coordinate_system"]; ok {
fields["coordinate_system"] = coordinateSystem
}
fields[envelope.FieldLongitude] = unit["longitude"]
fields[envelope.FieldLatitude] = unit["latitude"]
cursor += 9
cursor += size
case 0x02:
if len(body[cursor:]) < 1 {
units = append(units, map[string]any{"type": "0x02", "error": "truncated"})
@@ -364,21 +371,27 @@ func parsePlatformLogin(body []byte) map[string]any {
}
func parseVehicleData(data []byte) map[string]any {
return map[string]any{
out := map[string]any{
"vehicle_status": int(data[0]),
"charge_status": int(data[1]),
"running_mode": int(data[2]),
"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]),
"speed_kmh": nullableScaledU16(data[3:5], 10),
"total_mileage_km": nullableScaledU32(data[5:9], 10),
"total_voltage_v": nullableScaledU16(data[9:11], 10),
"total_current_a": nullableScaledU16WithOffset(data[11:13], 10, -1000),
"soc_percent": nullableByte(data[13]),
"dc_dc_status": int(data[14]),
"gear": int(data[15]),
"insulation_kohm": binary.BigEndian.Uint16(data[16:18]),
"accelerator_pct": int(data[18]),
"brake_pct": int(data[19]),
"insulation_kohm": nullableU16(data[16:18]),
}
// GB/T 32960.3-2025 removes the 2016 accelerator and brake bytes from
// the 0x01 vehicle unit. Keep them only when the frame actually carries
// the 20-byte 2016 payload.
if len(data) >= 20 {
out["accelerator_pct"] = nullableByte(data[18])
out["brake_pct"] = nullableByte(data[19])
}
return out
}
func parseDriveMotorData(data []byte) map[string]any {
@@ -389,12 +402,12 @@ func parseDriveMotorData(data []byte) map[string]any {
motors = append(motors, map[string]any{
"serial_no": int(data[cursor]),
"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": scaledIntWithOffset(int64(binary.BigEndian.Uint16(data[cursor+5:cursor+7])), 10, -20000),
"motor_temperature_c": int(data[cursor+7]) - 40,
"controller_voltage_v": scaledU16(data[cursor+8:cursor+10], 10),
"controller_current_a": scaledU16WithOffset(data[cursor+10:cursor+12], 10, -1000),
"controller_temperature_c": nullableTempOffset40(data[cursor+2]),
"speed_rpm": nullableU16WithOffset(data[cursor+3:cursor+5], -20000),
"torque_nm": nullableScaledU16WithOffset(data[cursor+5:cursor+7], 10, -20000),
"motor_temperature_c": nullableTempOffset40(data[cursor+7]),
"controller_voltage_v": nullableScaledU16(data[cursor+8:cursor+10], 10),
"controller_current_a": nullableScaledU16WithOffset(data[cursor+10:cursor+12], 10, -1000),
})
cursor += 12
}
@@ -406,34 +419,45 @@ func parseDriveMotorData(data []byte) map[string]any {
func parseFuelCellData(data []byte) map[string]any {
probeCount := int(binary.BigEndian.Uint16(data[6:8]))
probes := make([]int, 0, probeCount)
probes := make([]any, 0, probeCount)
cursor := 8
for i := 0; i < probeCount; i++ {
probes = append(probes, int(data[cursor])-40)
probes = append(probes, nullableTempOffset40(data[cursor]))
cursor++
}
out := map[string]any{
"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),
"fuel_cell_voltage_v": nullableScaledU16(data[0:2], 10),
"fuel_cell_current_a": nullableScaledU16(data[2:4], 10),
"hydrogen_consumption_kg_per_100km": nullableScaledU16(data[4:6], 100),
"temperature_probe_count": probeCount,
"temperature_probe_values_c": probes,
"max_hydrogen_temperature_c": scaledU16WithOffset(data[cursor:cursor+2], 10, -40),
"max_hydrogen_temperature_c": nullableScaledU16WithOffset(data[cursor:cursor+2], 10, -40),
"max_hydrogen_temperature_probe_id": int(data[cursor+2]),
"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": scaledU16(data[cursor+6:cursor+8], 10),
"max_hydrogen_pressure_mpa": nullableScaledU16(data[cursor+6:cursor+8], 10),
"max_hydrogen_pressure_probe_id": int(data[cursor+8]),
"dc_dc_status": int(data[cursor+9]),
}
if concentration := nullableU16(data[cursor+3 : cursor+5]); concentration != nil {
raw := concentration.(int)
out["max_hydrogen_concentration_fraction"] = float64(raw) / 1_000_000
out["max_hydrogen_concentration_ppm"] = raw
out["max_hydrogen_concentration_percent"] = float64(raw) / 10_000
} else {
out["max_hydrogen_concentration_fraction"] = nil
out["max_hydrogen_concentration_ppm"] = nil
out["max_hydrogen_concentration_percent"] = nil
}
return out
}
func parseEngineData(data []byte) map[string]any {
return map[string]any{
"engine_status": int(data[0]),
"crank_speed_rpm": binary.BigEndian.Uint16(data[1:3]),
"fuel_rate": binary.BigEndian.Uint16(data[3:5]),
"crank_speed_rpm": nullableU16(data[1:3]),
// The supplied authoritative reference does not define this field's
// business unit or scale, so preserve the decoded protocol integer.
"fuel_rate": nullableU16(data[3:5]),
}
}
@@ -632,11 +656,13 @@ func parseGdFCStackData(data []byte) map[string]any {
"frame_cell_count": frameCellCount,
}
cursor += 22
frameVoltages := make([]float64, 0, frameCellCount)
maxCell := 0.0
minCell := 0.0
seen := false
for c := 0; c < frameCellCount; c++ {
voltage := scaledU16(data[cursor:cursor+2], 1000)
frameVoltages = append(frameVoltages, voltage)
if !seen || voltage > maxCell {
maxCell = voltage
}
@@ -650,6 +676,7 @@ func parseGdFCStackData(data []byte) map[string]any {
summary["frame_max_cell_voltage_v"] = maxCell
summary["frame_min_cell_voltage_v"] = minCell
}
summary["frame_cell_voltages_v"] = frameVoltages
summaries = append(summaries, summary)
}
return map[string]any{
@@ -760,6 +787,13 @@ func nullableU16WithOffset(data []byte, offset int) any {
return int(value) + offset
}
func nullableByte(value byte) any {
if value == 0xfe || value == 0xff {
return nil
}
return int(value)
}
func scaledU16(data []byte, divisor int64) float64 {
return scaledInt(int64(binary.BigEndian.Uint16(data)), divisor)
}
@@ -791,6 +825,14 @@ func nullableScaledU16(data []byte, divisor int64) any {
return scaledInt(int64(value), divisor)
}
func nullableScaledU32(data []byte, divisor int64) any {
value := binary.BigEndian.Uint32(data)
if value == 0xfffffffe || value == 0xffffffff {
return nil
}
return scaledInt(int64(value), divisor)
}
func nullableScaledU16WithOffset(data []byte, divisor int64, offset int64) any {
value := binary.BigEndian.Uint16(data)
if value == 0xfffe || value == 0xffff {
@@ -806,12 +848,16 @@ func nullableTempOffset40(value byte) any {
return int(value) - 40
}
func parsePositionData(data []byte) map[string]any {
return map[string]any{
"position_status": int(data[0]),
"longitude": float64(binary.BigEndian.Uint32(data[1:5])) / 1_000_000,
"latitude": float64(binary.BigEndian.Uint32(data[5:9])) / 1_000_000,
func parsePositionData(version string, data []byte) map[string]any {
offset := 1
out := map[string]any{"position_status": int(data[0])}
if version == "V2025" {
out["coordinate_system"] = int(data[1])
offset = 2
}
out["longitude"] = float64(binary.BigEndian.Uint32(data[offset:offset+4])) / 1_000_000
out["latitude"] = float64(binary.BigEndian.Uint32(data[offset+4:offset+8])) / 1_000_000
return out
}
func indexStart(data []byte) int {

View File

@@ -222,6 +222,13 @@ func TestParseFrameKeepsParsingRealFuelCellReportUntilUnknownExtension(t *testin
if units[2]["name"] != "fuel_cell" {
t.Fatalf("unexpected fuel cell unit: %#v", units[2])
}
fuelCell, ok := units[2]["value"].(map[string]any)
if !ok {
t.Fatalf("fuel cell payload missing: %#v", units[2])
}
if _, ok := fuelCell["max_hydrogen_concentration_ppm"].(int); !ok {
t.Fatalf("hydrogen concentration ppm missing: %#v", fuelCell["max_hydrogen_concentration_ppm"])
}
if units[9]["name"] != "gd_fc_stack" {
t.Fatalf("unexpected gd stack unit: %#v", units[9])
}
@@ -237,6 +244,69 @@ func TestParseFrameKeepsParsingRealFuelCellReportUntilUnknownExtension(t *testin
}
}
func TestParseFuelCellDataExposesHydrogenConcentrationInReferencePercent(t *testing.T) {
data := make([]byte, 18)
data[11] = 0x01
data[12] = 0xf4
fuelCell := parseFuelCellData(data)
if concentration, ok := fuelCell["max_hydrogen_concentration_ppm"].(int); !ok || concentration != 500 {
t.Fatalf("hydrogen concentration ppm = %#v, want int(500)", fuelCell["max_hydrogen_concentration_ppm"])
}
if fraction, ok := fuelCell["max_hydrogen_concentration_fraction"].(float64); !ok || math.Abs(fraction-0.0005) > 0.0000001 {
t.Fatalf("hydrogen concentration fraction = %#v, want 0.0005", fuelCell["max_hydrogen_concentration_fraction"])
}
if percent, ok := fuelCell["max_hydrogen_concentration_percent"].(float64); !ok || math.Abs(percent-0.05) > 0.0000001 {
t.Fatalf("hydrogen concentration percent = %#v, want 0.05", fuelCell["max_hydrogen_concentration_percent"])
}
}
func TestParseFuelCellDataTreatsProtocolInvalidMarkersAsMissing(t *testing.T) {
data := make([]byte, 18)
for index := range data {
data[index] = 0xff
}
data[6], data[7] = 0, 0
fuelCell := parseFuelCellData(data)
for _, key := range []string{"fuel_cell_voltage_v", "fuel_cell_current_a", "hydrogen_consumption_kg_per_100km", "max_hydrogen_temperature_c", "max_hydrogen_concentration_percent", "max_hydrogen_pressure_mpa"} {
if fuelCell[key] != nil {
t.Fatalf("%s should be nil for protocol invalid marker: %#v", key, fuelCell[key])
}
}
}
func TestParseV2025VehicleAndPositionUsesCoordinateSystemByte(t *testing.T) {
body := []byte{0x1a, 0x07, 0x11, 0x0a, 0x00, 0x00, 0x01}
body = append(body,
0x01, 0x03, 0x01,
0x00, 0x64,
0x00, 0x00, 0x03, 0xe8,
0x13, 0x88,
0x27, 0x10,
80, 0x01, 0x00,
0x03, 0xe8)
body = append(body,
0x05,
0x00, 0x02,
0x07, 0x36, 0x50, 0x40,
0x01, 0xd2, 0x4f, 0x00)
fields := map[string]any{}
_, units := parseDataBody("V2025", body, fields)
if len(units) != 2 {
t.Fatalf("units = %#v", units)
}
vehicle := units[0]["value"].(map[string]any)
if _, exists := vehicle["accelerator_pct"]; exists {
t.Fatalf("2025 vehicle unit must not invent removed pedal fields: %#v", vehicle)
}
position := units[1]["value"].(map[string]any)
if position["coordinate_system"] != 2 {
t.Fatalf("coordinate system = %#v", position["coordinate_system"])
}
if longitude := position["longitude"].(float64); math.Abs(longitude-121) > 0.000001 {
t.Fatalf("longitude = %v", longitude)
}
}
func TestScaledDecimalFieldsDoNotLeakBinaryFloatTails(t *testing.T) {
unit := parseDriveMotorData([]byte{
0x01,