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 {