package gb32960 import ( "encoding/binary" "encoding/hex" "errors" "fmt" "strings" "time" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" ) var ( ErrFrameTooShort = errors.New("gb32960 frame too short") ErrBadStartSymbol = errors.New("gb32960 bad start symbol") ErrBodyLength = errors.New("gb32960 body length mismatch") ErrBCC = errors.New("gb32960 bcc mismatch") ) const ( minFrameLen = 25 headerLen = 24 ) // ExtractFrames splits GB/T 32960 TCP streams using the protocol length field. // Returned frames include the start symbols and trailing BCC byte. func ExtractFrames(stream []byte) (frames [][]byte, remainder []byte, err error) { offset := 0 for { start := indexStart(stream[offset:]) if start < 0 { return frames, nil, nil } offset += start remaining := stream[offset:] if len(remaining) < headerLen { return frames, append([]byte(nil), remaining...), nil } bodyLen := int(binary.BigEndian.Uint16(remaining[22:24])) frameLen := headerLen + bodyLen + 1 if len(remaining) < frameLen { return frames, append([]byte(nil), remaining...), nil } frames = append(frames, append([]byte(nil), remaining[:frameLen]...)) offset += frameLen if offset >= len(stream) { return frames, nil, nil } } } func ParseFrame(raw []byte, receivedAtMS int64, sourceEndpoint string) (envelope.FrameEnvelope, error) { if len(raw) < minFrameLen { return envelope.FrameEnvelope{}, fmt.Errorf("%w: len=%d", ErrFrameTooShort, len(raw)) } version, ok := version(raw[0], raw[1]) if !ok { return envelope.FrameEnvelope{}, fmt.Errorf("%w: %s", ErrBadStartSymbol, hex.EncodeToString(raw[:2])) } bodyLen := int(binary.BigEndian.Uint16(raw[22:24])) if len(raw) != headerLen+bodyLen+1 { return envelope.FrameEnvelope{}, fmt.Errorf("%w: declared=%d actual=%d", ErrBodyLength, bodyLen, len(raw)-headerLen-1) } if got, want := bcc(raw[2:len(raw)-1]), raw[len(raw)-1]; got != want { return envelope.FrameEnvelope{}, fmt.Errorf("%w: got=0x%02x want=0x%02x", ErrBCC, got, want) } command := raw[2] responseFlag := raw[3] vin := strings.TrimRight(string(raw[4:21]), "\x00 ") body := raw[headerLen : headerLen+bodyLen] parsed := map[string]any{ "header": map[string]any{ "version": version, "command": fmt.Sprintf("0x%02X", command), "response_flag": fmt.Sprintf("0x%02X", responseFlag), "vin": vin, "encrypt": raw[21], "body_length": bodyLen, }, } fields := map[string]any{} eventTimeMS := receivedAtMS if command == 0x02 || command == 0x03 { eventTime, units := parseDataBody(version, body, fields) if !eventTime.IsZero() { eventTimeMS = eventTime.UnixMilli() fields["device_time"] = eventTime.Format(time.RFC3339) parsed["device_time"] = eventTime.Format(time.RFC3339) } parsed["data_units"] = units } env := envelope.FrameEnvelope{ Protocol: envelope.ProtocolGB32960, MessageID: fmt.Sprintf("0x%02X", command), VIN: vin, SourceEndpoint: sourceEndpoint, EventTimeMS: eventTimeMS, ReceivedAtMS: receivedAtMS, RawHex: strings.ToUpper(hex.EncodeToString(raw)), Parsed: parsed, Fields: fields, ParseStatus: envelope.ParseOK, } env.EventID = env.StableEventID() return env, nil } func parseDataBody(version string, body []byte, fields map[string]any) (time.Time, []map[string]any) { if len(body) < 6 { return time.Time{}, nil } eventTime := parseBCDTime(body[:6]) cursor := 6 var units []map[string]any for cursor < len(body) { unitType := body[cursor] cursor++ switch unitType { case 0x01: size := 20 if version == "V2025" { size = 18 } if len(body[cursor:]) < size { units = append(units, map[string]any{"type": "0x01", "error": "truncated"}) return eventTime, units } unit := parseVehicleData(body[cursor : cursor+size]) units = append(units, map[string]any{"type": "0x01", "name": "vehicle", "value": unit}) fields["vehicle_status"] = unit["vehicle_status"] fields["charge_status"] = unit["charge_status"] fields["running_mode"] = unit["running_mode"] fields[envelope.FieldSpeedKMH] = unit["speed_kmh"] fields[envelope.FieldTotalMileageKM] = unit["total_mileage_km"] fields[envelope.FieldSOCPercent] = unit["soc_percent"] cursor += size case 0x05: if len(body[cursor:]) < 9 { units = append(units, map[string]any{"type": "0x05", "error": "truncated"}) return eventTime, units } unit := parsePositionData(body[cursor : cursor+9]) units = append(units, map[string]any{"type": "0x05", "name": "position", "value": unit}) fields["position_status"] = unit["position_status"] fields[envelope.FieldLongitude] = unit["longitude"] fields[envelope.FieldLatitude] = unit["latitude"] cursor += 9 default: units = append(units, map[string]any{ "type": fmt.Sprintf("0x%02X", unitType), "raw_tail": strings.ToUpper(hex.EncodeToString(body[cursor:])), "parse": "unsupported_unit", "byte_size": len(body) - cursor, }) return eventTime, units } } return eventTime, units } func parseVehicleData(data []byte) map[string]any { return 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, "soc_percent": int(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]), } } 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 indexStart(data []byte) int { for i := 0; i+1 < len(data); i++ { if _, ok := version(data[i], data[i+1]); ok { return i } } return -1 } func version(a byte, b byte) (string, bool) { switch { case a == '#' && b == '#': return "V2016", true case a == '$' && b == '$': return "V2025", true default: return "", false } } func parseBCDTime(data []byte) time.Time { if len(data) != 6 { return time.Time{} } year := 2000 + bcdByte(data[0]) month := time.Month(bcdByte(data[1])) day := bcdByte(data[2]) hour := bcdByte(data[3]) minute := bcdByte(data[4]) second := bcdByte(data[5]) if month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59 { return time.Time{} } return time.Date(year, month, day, hour, minute, second, 0, time.FixedZone("Asia/Shanghai", 8*3600)) } func bcdByte(value byte) int { return int(value>>4)*10 + int(value&0x0f) } func bcc(data []byte) byte { var out byte for _, value := range data { out ^= value } return out }