feat: parse gb32960 realtime data units

This commit is contained in:
lingniu
2026-07-01 22:21:21 +08:00
parent e0068a750d
commit 5f4f4fd124
2 changed files with 329 additions and 1 deletions

View File

@@ -149,11 +149,84 @@ func parseDataBody(version string, body []byte, fields map[string]any) (time.Tim
fields[envelope.FieldLongitude] = unit["longitude"] fields[envelope.FieldLongitude] = unit["longitude"]
fields[envelope.FieldLatitude] = unit["latitude"] fields[envelope.FieldLatitude] = unit["latitude"]
cursor += 9 cursor += 9
case 0x02:
if len(body[cursor:]) < 1 {
units = append(units, map[string]any{"type": "0x02", "error": "truncated"})
return eventTime, units
}
size := 1 + int(body[cursor])*12
if len(body[cursor:]) < size {
units = append(units, map[string]any{"type": "0x02", "error": "truncated"})
return eventTime, units
}
unit := parseDriveMotorData(body[cursor : cursor+size])
units = append(units, map[string]any{"type": "0x02", "name": "drive_motor", "value": unit})
cursor += size
case 0x03:
if len(body[cursor:]) < 8 {
units = append(units, map[string]any{"type": "0x03", "error": "truncated"})
return eventTime, units
}
probeCount := int(binary.BigEndian.Uint16(body[cursor+6 : cursor+8]))
size := 8 + probeCount + 10
if len(body[cursor:]) < size {
units = append(units, map[string]any{"type": "0x03", "error": "truncated"})
return eventTime, units
}
unit := parseFuelCellData(body[cursor : cursor+size])
units = append(units, map[string]any{"type": "0x03", "name": "fuel_cell", "value": unit})
fields["fuel_cell_hydrogen_consumption_kg_per_100km"] = unit["hydrogen_consumption_kg_per_100km"]
fields["fuel_cell_voltage_v"] = unit["fuel_cell_voltage_v"]
fields["fuel_cell_current_a"] = unit["fuel_cell_current_a"]
cursor += size
case 0x04:
if len(body[cursor:]) < 5 {
units = append(units, map[string]any{"type": "0x04", "error": "truncated"})
return eventTime, units
}
unit := parseEngineData(body[cursor : cursor+5])
units = append(units, map[string]any{"type": "0x04", "name": "engine", "value": unit})
cursor += 5
case 0x06:
if len(body[cursor:]) < 14 {
units = append(units, map[string]any{"type": "0x06", "error": "truncated"})
return eventTime, units
}
unit := parseExtremeData(body[cursor : cursor+14])
units = append(units, map[string]any{"type": "0x06", "name": "extreme", "value": unit})
cursor += 14
case 0x07:
size, ok := alarmDataSize(body[cursor:])
if !ok {
units = append(units, map[string]any{"type": "0x07", "error": "truncated"})
return eventTime, units
}
unit := parseAlarmData(body[cursor : cursor+size])
units = append(units, map[string]any{"type": "0x07", "name": "alarm", "value": unit})
cursor += size
case 0x08:
size, ok := voltageDataSize(body[cursor:])
if !ok {
units = append(units, map[string]any{"type": "0x08", "error": "truncated"})
return eventTime, units
}
unit := parseVoltageData(body[cursor : cursor+size])
units = append(units, map[string]any{"type": "0x08", "name": "voltage", "value": unit})
cursor += size
case 0x09:
size, ok := temperatureDataSize(body[cursor:])
if !ok {
units = append(units, map[string]any{"type": "0x09", "error": "truncated"})
return eventTime, units
}
unit := parseTemperatureData(body[cursor : cursor+size])
units = append(units, map[string]any{"type": "0x09", "name": "temperature", "value": unit})
cursor += size
default: default:
units = append(units, map[string]any{ units = append(units, map[string]any{
"type": fmt.Sprintf("0x%02X", unitType), "type": fmt.Sprintf("0x%02X", unitType),
"raw_tail": strings.ToUpper(hex.EncodeToString(body[cursor:])), "raw_tail": strings.ToUpper(hex.EncodeToString(body[cursor:])),
"parse": "unsupported_unit", "parse": "unknown_unit",
"byte_size": len(body) - cursor, "byte_size": len(body) - cursor,
}) })
return eventTime, units return eventTime, units
@@ -180,6 +253,217 @@ func parseVehicleData(data []byte) map[string]any {
} }
} }
func parseDriveMotorData(data []byte) map[string]any {
count := int(data[0])
motors := make([]map[string]any, 0, count)
cursor := 1
for i := 0; i < count; i++ {
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": float64(int(binary.BigEndian.Uint16(data[cursor+5:cursor+7]))-20000) / 10,
"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,
})
cursor += 12
}
return map[string]any{
"count": count,
"motors": motors,
}
}
func parseFuelCellData(data []byte) map[string]any {
probeCount := int(binary.BigEndian.Uint16(data[6:8]))
probes := make([]int, 0, probeCount)
cursor := 8
for i := 0; i < probeCount; i++ {
probes = append(probes, int(data[cursor])-40)
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,
"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_probe_id": int(data[cursor+2]),
"max_hydrogen_concentration_fraction": float64(binary.BigEndian.Uint16(data[cursor+3:cursor+5])) * 0.000001,
"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_probe_id": int(data[cursor+8]),
"dc_dc_status": int(data[cursor+9]),
}
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]),
}
}
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,
"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,
"max_temp_subsystem_no": int(data[8]),
"max_temp_probe_no": int(data[9]),
"max_temp_c": int(data[10]) - 40,
"min_temp_subsystem_no": int(data[11]),
"min_temp_probe_no": int(data[12]),
"min_temp_c": int(data[13]) - 40,
}
}
func alarmDataSize(data []byte) (int, bool) {
if len(data) < 5 {
return 0, false
}
cursor := 5
for i := 0; i < 4; i++ {
if len(data[cursor:]) < 1 {
return 0, false
}
count := int(data[cursor])
cursor++
cursor += count * 4
if cursor > len(data) {
return 0, false
}
}
return cursor, true
}
func parseAlarmData(data []byte) map[string]any {
cursor := 5
readList := func() []string {
count := int(data[cursor])
cursor++
values := make([]string, 0, count)
for i := 0; i < count; i++ {
values = append(values, fmt.Sprintf("0x%08X", binary.BigEndian.Uint32(data[cursor:cursor+4])))
cursor += 4
}
return values
}
return map[string]any{
"max_alarm_level": int(data[0]),
"general_alarm_flag": fmt.Sprintf("0x%08X", binary.BigEndian.Uint32(data[1:5])),
"battery_faults": readList(),
"motor_faults": readList(),
"engine_faults": readList(),
"other_faults": readList(),
}
}
func voltageDataSize(data []byte) (int, bool) {
if len(data) < 1 {
return 0, false
}
cursor := 1
for i := 0; i < int(data[0]); i++ {
if len(data[cursor:]) < 10 {
return 0, false
}
frameCellCount := int(data[cursor+9])
cursor += 10 + frameCellCount*2
if cursor > len(data) {
return 0, false
}
}
return cursor, true
}
func parseVoltageData(data []byte) map[string]any {
subCount := int(data[0])
cursor := 1
totalVoltage := 0.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
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
if !cellSeen || voltage > maxCell {
maxCell = voltage
}
if !cellSeen || voltage < minCell {
minCell = voltage
}
cellSeen = true
cursor += 2
}
_ = cellCount
}
return map[string]any{
"subsystem_count": subCount,
"total_voltage_v": totalVoltage,
"max_cell_v": maxCell,
"min_cell_v": minCell,
}
}
func temperatureDataSize(data []byte) (int, bool) {
if len(data) < 1 {
return 0, false
}
cursor := 1
for i := 0; i < int(data[0]); i++ {
if len(data[cursor:]) < 3 {
return 0, false
}
probeCount := int(binary.BigEndian.Uint16(data[cursor+1 : cursor+3]))
cursor += 3 + probeCount
if cursor > len(data) {
return 0, false
}
}
return cursor, true
}
func parseTemperatureData(data []byte) map[string]any {
subCount := int(data[0])
cursor := 1
maxTemp := 0
minTemp := 0
seen := false
for i := 0; i < subCount; i++ {
probeCount := int(binary.BigEndian.Uint16(data[cursor+1 : cursor+3]))
cursor += 3
for p := 0; p < probeCount; p++ {
temp := int(data[cursor]) - 40
if !seen || temp > maxTemp {
maxTemp = temp
}
if !seen || temp < minTemp {
minTemp = temp
}
seen = true
cursor++
}
}
return map[string]any{
"subsystem_count": subCount,
"max_temp_c": maxTemp,
"min_temp_c": minTemp,
}
}
func parsePositionData(data []byte) map[string]any { func parsePositionData(data []byte) map[string]any {
return map[string]any{ return map[string]any{
"position_status": int(data[0]), "position_status": int(data[0]),

View File

@@ -2,11 +2,14 @@ package gb32960
import ( import (
"encoding/hex" "encoding/hex"
"fmt"
"testing" "testing"
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
) )
const realFuelCellFrame = "232302FE4C423941333241323152304C53313730370102ED1A070116091C0102030100000008297D161F27104C020007D0000002010104564E204E205815CA271003000A000000B40002666902800200000100A101000400FFFF001205000733444601E2B5DB06013A0F6C018E0F4001014B01054A07000000000000000000080101161F271000900001900F520F530F520F640F660F650F650F650F650F660F650F650F640F640F550F550F540F560F580F570F540F570F560F550F5A0F550F580F570F5C0F5F0F5E0F610F5E0F5D0F5E0F5F0F5E0F5D0F610F5F0F610F600F600F610F610F610F600F630F630F600F610F610F610F620F620F610F6A0F6C0F6B0F6A0F6C0F6B0F6A0F6B0F6B0F6B0F6A0F610F630F630F630F640F660F640F630F630F630F580F5A0F580F5B0F550F580F5A0F5A0F590F580F5A0F620F650F650F650F660F640F640F640F640F640F660F650F640F650F660F650F670F670F670F670F630F620F680F670F650F650F670F650F610F5F0F600F620F5E0F5C0F590F4C0F490F480F440F470F480F470F420F430F450F420F450F440F440F430F530F540F520F400F410F4109010100084B4B4B4B4A4A4B4A3001026907B2FF00FF00380002002800000008006C00016C00080008000000080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008002800070007000700070007000700070007000700070007000700070007000700070007000700070007000700070007000700080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000831010E9C0005FFFFFFFF0E9CFFFFFFFF0083320E9C1B6115E212465733FFFFFFFFFF34FFFFFFFFFF002B800009690E9C2710000D000E83002402020500002700080001000000FD00000000FFFFFFFFFFFFFFFFFFFFFFFF1FFE1FF3001DBA"
func TestExtractFramesKeepsPartialRemainderAndParsesHeader(t *testing.T) { func TestExtractFramesKeepsPartialRemainderAndParsesHeader(t *testing.T) {
first := buildFrame(0x02, 0xfe, "LNBSCB3D4R1234567", []byte{0x1a, 0x06, 0x30, 0x16, 0x23, 0x57}) first := buildFrame(0x02, 0xfe, "LNBSCB3D4R1234567", []byte{0x1a, 0x06, 0x30, 0x16, 0x23, 0x57})
second := buildFrame(0x05, 0xfe, "PLATFORM-LOGIN001", nil) second := buildFrame(0x05, 0xfe, "PLATFORM-LOGIN001", nil)
@@ -82,6 +85,47 @@ func TestParseFrameExtractsRealtimeVehicleMileageAndPosition(t *testing.T) {
} }
} }
func TestParseFrameKeepsParsingRealFuelCellReportUntilUnknownExtension(t *testing.T) {
frame, err := hex.DecodeString(realFuelCellFrame)
if err != nil {
t.Fatal(err)
}
env, err := ParseFrame(frame, 1782914969584, "115.29.187.205:32960")
if err != nil {
t.Fatalf("ParseFrame() error = %v", err)
}
if env.VIN != "LB9A32A21R0LS1707" {
t.Fatalf("vin = %q", env.VIN)
}
assertFloatField(t, env, envelope.FieldTotalMileageKM, 53490.9)
assertFloatField(t, env, "fuel_cell_hydrogen_consumption_kg_per_100km", 1.8)
assertFloatField(t, env, envelope.FieldLongitude, 120.800326)
assertFloatField(t, env, envelope.FieldLatitude, 31.634907)
units, ok := env.Parsed["data_units"].([]map[string]any)
if !ok {
t.Fatalf("data_units missing: %#v", env.Parsed["data_units"])
}
gotTypes := make([]string, 0, len(units))
for _, unit := range units {
gotTypes = append(gotTypes, fmt.Sprint(unit["type"]))
}
wantTypes := []string{"0x01", "0x02", "0x03", "0x04", "0x05", "0x06", "0x07", "0x08", "0x09", "0x30"}
if fmt.Sprint(gotTypes) != fmt.Sprint(wantTypes) {
t.Fatalf("unit types = %v, want %v", gotTypes, wantTypes)
}
if units[1]["name"] != "drive_motor" {
t.Fatalf("unexpected motor unit: %#v", units[1])
}
if units[2]["name"] != "fuel_cell" {
t.Fatalf("unexpected fuel cell unit: %#v", units[2])
}
if units[9]["parse"] != "unknown_unit" {
t.Fatalf("unexpected unknown unit: %#v", units[9])
}
}
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)