package jt808 import ( "bytes" "encoding/binary" "encoding/hex" "errors" "fmt" "strings" "time" "unicode/utf8" "golang.org/x/text/encoding/simplifiedchinese" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" ) var ( ErrFrameTooShort = errors.New("jt808 frame too short") ErrChecksum = errors.New("jt808 checksum mismatch") ErrEscape = errors.New("jt808 invalid escape sequence") ) type Location struct { AlarmFlag uint32 StatusFlag uint32 Latitude float64 Longitude float64 AltitudeM uint16 SpeedKMH float64 DirectionDeg uint16 Time time.Time TotalMileageKM *float64 Additional []AdditionalItem } type AdditionalItem struct { ID byte Length int ValueHex string Parsed any } // ExtractFrames splits JT/T 808 TCP streams by 0x7e delimiters and unescapes // complete frame payloads. Returned frames exclude the leading and trailing 0x7e. func ExtractFrames(stream []byte) (frames [][]byte, remainder []byte, err error) { start := -1 for i, value := range stream { if value != 0x7e { continue } if start < 0 { start = i continue } if i == start+1 { start = i continue } frame, err := unescape(stream[start+1 : i]) if err != nil { return nil, nil, err } frames = append(frames, frame) start = i } if start >= 0 && start < len(stream)-1 { return frames, append([]byte(nil), stream[start:]...), nil } return frames, nil, nil } func ParseFrame(raw []byte, receivedAtMS int64, sourceEndpoint string) (envelope.FrameEnvelope, error) { if len(raw) < 13 { return envelope.FrameEnvelope{}, fmt.Errorf("%w: len=%d", ErrFrameTooShort, len(raw)) } if got, want := checksum(raw[:len(raw)-1]), raw[len(raw)-1]; got != want { return envelope.FrameEnvelope{}, fmt.Errorf("%w: got=0x%02x want=0x%02x", ErrChecksum, got, want) } messageID := binary.BigEndian.Uint16(raw[0:2]) props := binary.BigEndian.Uint16(raw[2:4]) bodySize := props & 0x03ff versioned := props&0x4000 != 0 headerLen := 12 phoneStart := 4 phoneEnd := 10 serialStart := 10 packageInfo := map[string]any(nil) protocolVersion := byte(0) if versioned { headerLen = 17 if len(raw) < headerLen { return envelope.FrameEnvelope{}, fmt.Errorf("%w: versioned header len=%d", ErrFrameTooShort, len(raw)) } protocolVersion = raw[4] phoneStart = 5 phoneEnd = 15 serialStart = 15 } subpackage := props&0x2000 != 0 if subpackage { headerLen += 4 } if len(raw) < headerLen+int(bodySize)+1 { return envelope.FrameEnvelope{}, fmt.Errorf("%w: bodySize=%d len=%d", ErrFrameTooShort, bodySize, len(raw)) } if subpackage { packageInfo = map[string]any{ "total": binary.BigEndian.Uint16(raw[serialStart+2 : serialStart+4]), "index": binary.BigEndian.Uint16(raw[serialStart+4 : serialStart+6]), } } phoneBCD := raw[phoneStart:phoneEnd] phone := bcdString(phoneBCD) phoneTrimZero := strings.TrimLeft(phone, "0") if versioned && phoneTrimZero != "" { phone = phoneTrimZero } body := raw[headerLen : headerLen+int(bodySize)] parsed := map[string]any{ "header": map[string]any{ "message_id": fmt.Sprintf("0x%04X", messageID), "protocol_version": protocolVersion, "versioned": versioned, "properties": props, "body_size": bodySize, "phone_bcd_hex": strings.ToUpper(hex.EncodeToString(phoneBCD)), "phone": phone, "phone_trim_zero": phoneTrimZero, "sequence": binary.BigEndian.Uint16(raw[serialStart : serialStart+2]), "subpackage": subpackage, "package_info": packageInfo, "encryption_flags": (props >> 10) & 0x07, }, } fields := map[string]any{} eventTimeMS := receivedAtMS if messageID == 0x0200 { location, err := parseLocation(body) if err != nil { return envelope.FrameEnvelope{}, err } parsed["location"] = locationToMap(location) fields[envelope.FieldLatitude] = location.Latitude fields[envelope.FieldLongitude] = location.Longitude fields[envelope.FieldSpeedKMH] = location.SpeedKMH fields["altitude_m"] = location.AltitudeM fields["direction_deg"] = location.DirectionDeg fields["alarm_flag"] = location.AlarmFlag fields["status_flag"] = location.StatusFlag if !location.Time.IsZero() { eventTimeMS = location.Time.UnixMilli() fields["device_time"] = location.Time.Format(time.RFC3339) } if location.TotalMileageKM != nil { fields[envelope.FieldTotalMileageKM] = *location.TotalMileageKM } } else if messageID == 0x0100 { registration, err := parseRegistration(body, versioned) if err != nil { return envelope.FrameEnvelope{}, err } parsed["registration"] = registration if deviceID, ok := registration["device_id"].(string); ok { fields["device_id"] = deviceID } if plate, ok := registration["plate"].(string); ok { fields["plate"] = plate } } else if messageID == 0x0102 { authentication := parseAuthentication(body, versioned) parsed["authentication"] = authentication if token, ok := authentication["token"].(string); ok { fields["auth_token"] = token } } env := envelope.FrameEnvelope{ Protocol: envelope.ProtocolJT808, MessageID: fmt.Sprintf("0x%04X", messageID), Sequence: binary.BigEndian.Uint16(raw[serialStart : serialStart+2]), Phone: phone, SourceEndpoint: sourceEndpoint, EventTimeMS: eventTimeMS, ReceivedAtMS: receivedAtMS, RawHex: strings.ToUpper(hex.EncodeToString(raw)), Parsed: parsed, Fields: fields, ParseStatus: envelope.ParseOK, } if registration, ok := parsed["registration"].(map[string]any); ok { env.DeviceID, _ = registration["device_id"].(string) env.Plate, _ = registration["plate"].(string) } env.EventID = env.StableEventID() return env, nil } func parseRegistration(body []byte, versioned bool) (map[string]any, error) { schemaVersion := "2011_2013" manufacturerSize := 5 deviceTypeSize := 20 deviceIDSize := 7 if versioned { schemaVersion = "2019" manufacturerSize = 11 deviceTypeSize = 30 deviceIDSize = 30 } minLen := 2 + 2 + manufacturerSize + deviceTypeSize + deviceIDSize + 1 if len(body) < minLen { return nil, fmt.Errorf("%w: registration body len=%d", ErrFrameTooShort, len(body)) } manufacturerStart := 4 deviceTypeStart := manufacturerStart + manufacturerSize deviceIDStart := deviceTypeStart + deviceTypeSize plateColorStart := deviceIDStart + deviceIDSize registration := map[string]any{ "schema_version": schemaVersion, "province": binary.BigEndian.Uint16(body[0:2]), "city": binary.BigEndian.Uint16(body[2:4]), "manufacturer": fixedText(body[manufacturerStart:deviceTypeStart]), "device_type": fixedText(body[deviceTypeStart:deviceIDStart]), "device_id": fixedText(body[deviceIDStart:plateColorStart]), "plate_color": body[plateColorStart], "plate": freeText(body[plateColorStart+1:]), } return registration, nil } func parseAuthentication(body []byte, versioned bool) map[string]any { if versioned { if out, ok := parseVersionedAuthentication(body); ok { return out } if out, ok := parseDelimitedVersionedAuthentication(body); ok { return out } } return map[string]any{"token": freeText(body)} } func parseVersionedAuthentication(body []byte) (map[string]any, bool) { if len(body) < 17 { return nil, false } tokenLen := int(body[0]) imeiStart := 1 + tokenLen softwareLenStart := imeiStart + 15 if tokenLen <= 0 || len(body) < softwareLenStart+1 { return nil, false } softwareLen := int(body[softwareLenStart]) softwareStart := softwareLenStart + 1 if len(body) < softwareStart+softwareLen { return nil, false } return map[string]any{ "schema_version": "2019", "token": freeText(body[1:imeiStart]), "imei": fixedText(body[imeiStart:softwareLenStart]), "software_version": freeText(body[softwareStart : softwareStart+softwareLen]), }, true } func parseDelimitedVersionedAuthentication(body []byte) (map[string]any, bool) { parts := bytes.Split(body, []byte{0xff, 0xff}) if len(parts) < 2 { return nil, false } out := map[string]any{ "schema_version": "2019_delimited", "token": freeText(parts[0]), "imei": freeText(parts[1]), } if len(parts) > 2 { out["software_version"] = freeText(parts[2]) } return out, true } func parseLocation(body []byte) (Location, error) { if len(body) < 28 { return Location{}, fmt.Errorf("%w: location body len=%d", ErrFrameTooShort, len(body)) } status := binary.BigEndian.Uint32(body[4:8]) latitude := float64(binary.BigEndian.Uint32(body[8:12])) / 1_000_000 longitude := float64(binary.BigEndian.Uint32(body[12:16])) / 1_000_000 if status&(1<<2) != 0 { latitude = -latitude } if status&(1<<3) != 0 { longitude = -longitude } location := Location{ AlarmFlag: binary.BigEndian.Uint32(body[0:4]), StatusFlag: status, Latitude: latitude, Longitude: longitude, AltitudeM: binary.BigEndian.Uint16(body[16:18]), SpeedKMH: float64(binary.BigEndian.Uint16(body[18:20])) / 10, DirectionDeg: binary.BigEndian.Uint16(body[20:22]), Time: parseBCDTime(body[22:28]), } location.Additional = parseAdditional(body[28:], &location) return location, nil } func parseAdditional(data []byte, location *Location) []AdditionalItem { var out []AdditionalItem for len(data) >= 2 { id := data[0] size := int(data[1]) data = data[2:] if len(data) < size { out = append(out, AdditionalItem{ ID: id, Length: size, ValueHex: strings.ToUpper(hex.EncodeToString(data)), Parsed: "truncated", }) return out } value := data[:size] item := AdditionalItem{ ID: id, Length: size, ValueHex: strings.ToUpper(hex.EncodeToString(value)), } switch { case id == 0x01 && size == 4: rawMileage := binary.BigEndian.Uint32(value) mileage := float64(rawMileage) / 10 location.TotalMileageKM = &mileage item.Parsed = map[string]any{ "name": envelope.FieldTotalMileageKM, "raw_value_tenth_km": rawMileage, "value": mileage, "unit": "km", } case id == 0x02 && size == 2: rawFuel := binary.BigEndian.Uint16(value) item.Parsed = map[string]any{ "name": "fuel_l", "raw_value_tenth_l": rawFuel, "value": float64(rawFuel) / 10, "unit": "L", } case id == 0x03 && size == 2: rawSpeed := binary.BigEndian.Uint16(value) item.Parsed = map[string]any{ "name": "recorder_speed_kmh", "raw_value_tenth_kmh": rawSpeed, "value": float64(rawSpeed) / 10, "unit": "km/h", } case id == 0x04 && size == 2: item.Parsed = map[string]any{ "name": "manual_alarm_event_id", "value": binary.BigEndian.Uint16(value), } case id == 0x05 && size == 30: item.Parsed = map[string]any{ "name": "tire_pressure", "values": parseTirePressure(value), } case id == 0x06 && size == 2: item.Parsed = map[string]any{ "name": "carriage_temperature_c", "value": int16(binary.BigEndian.Uint16(value)), "unit": "C", } case id == 0x11 && (size == 1 || size == 5): parsed := map[string]any{ "name": "overspeed_alarm", "location_type": value[0], } if size == 5 { parsed["area_id"] = binary.BigEndian.Uint32(value[1:5]) } item.Parsed = parsed case id == 0x12 && size == 6: item.Parsed = map[string]any{ "name": "area_route_alarm", "location_type": value[0], "area_id": binary.BigEndian.Uint32(value[1:5]), "direction": value[5], } case id == 0x13 && size == 7: item.Parsed = map[string]any{ "name": "road_section_driving_time_alarm", "road_section_id": binary.BigEndian.Uint32(value[0:4]), "driving_time_seconds": binary.BigEndian.Uint16(value[4:6]), "result": value[6], "result_description": roadSectionDrivingTimeResult(value[6]), "driving_time_result_source": "jt808_0x0200_additional_0x13", } case id == 0x25 && size == 4: rawStatus := binary.BigEndian.Uint32(value) item.Parsed = map[string]any{ "name": "extended_vehicle_signal_status", "value": rawStatus, "bits": parseExtendedVehicleSignal(rawStatus), } case id == 0x2a && size == 2: rawStatus := binary.BigEndian.Uint16(value) item.Parsed = map[string]any{ "name": "io_status", "value": rawStatus, "bits": parseIOStatus(rawStatus), } case id == 0x2b && size == 4: analog := binary.BigEndian.Uint32(value) item.Parsed = map[string]any{ "name": "analog", "ad0": analog & 0xffff, "ad1": analog >> 16, "value": analog, } case id == 0x30 && size == 1: item.Parsed = map[string]any{ "name": "network_signal_strength", "value": value[0], } case id == 0x31 && size == 1: item.Parsed = map[string]any{ "name": "gnss_satellite_count", "value": value[0], } } out = append(out, item) data = data[size:] } return out } func locationToMap(location Location) map[string]any { out := map[string]any{ "alarm_flag": location.AlarmFlag, "status_flag": location.StatusFlag, "latitude": location.Latitude, "longitude": location.Longitude, "altitude_m": location.AltitudeM, "speed_kmh": location.SpeedKMH, "direction_deg": location.DirectionDeg, "additional": additionalToMaps(location.Additional), } if !location.Time.IsZero() { out["device_time"] = location.Time.Format(time.RFC3339) } if location.TotalMileageKM != nil { out[envelope.FieldTotalMileageKM] = *location.TotalMileageKM } return out } func additionalToMaps(items []AdditionalItem) []map[string]any { out := make([]map[string]any, 0, len(items)) for _, item := range items { value := map[string]any{ "id": fmt.Sprintf("0x%02X", item.ID), "length": item.Length, "value_hex": item.ValueHex, } if item.Parsed != nil { value["parsed"] = item.Parsed } out = append(out, value) } return out } func parseTirePressure(data []byte) []map[string]any { out := make([]map[string]any, 0, len(data)) for index, value := range data { out = append(out, map[string]any{ "position": index, "value": value, "valid": value != 0xff, }) } return out } func parseExtendedVehicleSignal(value uint32) map[string]bool { return map[string]bool{ "low_beam": value&0x0001 != 0, "high_beam": value&0x0002 != 0, "right_turn": value&0x0004 != 0, "left_turn": value&0x0008 != 0, "brake": value&0x0010 != 0, "reverse_gear": value&0x0020 != 0, "fog_light": value&0x0040 != 0, "clearance_light": value&0x0080 != 0, "horn": value&0x0100 != 0, "air_conditioner": value&0x0200 != 0, "neutral": value&0x0400 != 0, "retarder": value&0x0800 != 0, "abs": value&0x1000 != 0, "heater": value&0x2000 != 0, "clutch": value&0x4000 != 0, } } func parseIOStatus(value uint16) map[string]bool { return map[string]bool{ "deep_sleep": value&0x0001 != 0, "sleep": value&0x0002 != 0, } } func roadSectionDrivingTimeResult(value byte) string { if value == 0 { return "insufficient" } if value == 1 { return "too_long" } return "unknown" } func bcdString(data []byte) string { out := make([]byte, 0, len(data)*2) for _, value := range data { out = append(out, '0'+((value>>4)&0x0f), '0'+(value&0x0f)) } return string(out) } func fixedText(data []byte) string { return strings.Trim(freeText(data), "\x00 ") } func freeText(data []byte) string { data = bytes.Trim(data, "\x00 ") if len(data) == 0 { return "" } if utf8.Valid(data) { return string(data) } decoded, err := simplifiedchinese.GBK.NewDecoder().Bytes(data) if err == nil && utf8.Valid(decoded) { return string(decoded) } return strings.ToValidUTF8(string(data), "") } 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 unescape(data []byte) ([]byte, error) { out := make([]byte, 0, len(data)) for i := 0; i < len(data); i++ { if data[i] != 0x7d { out = append(out, data[i]) continue } if i+1 >= len(data) { return nil, ErrEscape } i++ switch data[i] { case 0x01: out = append(out, 0x7d) case 0x02: out = append(out, 0x7e) default: return nil, ErrEscape } } return out, nil } func checksum(data []byte) byte { var out byte for _, value := range data { out ^= value } return out }