Files
lingniu-vehicle-ingest/go/vehicle-gateway/internal/protocol/jt808/parser.go
2026-07-01 23:17:29 +08:00

374 lines
10 KiB
Go

package jt808
import (
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"strings"
"time"
"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
}
if props&0x2000 != 0 {
headerLen += 4
packageInfo = map[string]any{
"total": binary.BigEndian.Uint16(raw[serialStart+2 : serialStart+4]),
"index": binary.BigEndian.Uint16(raw[serialStart+4 : serialStart+6]),
}
}
if len(raw) < headerLen+int(bodySize)+1 {
return envelope.FrameEnvelope{}, fmt.Errorf("%w: bodySize=%d len=%d", ErrFrameTooShort, bodySize, len(raw))
}
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": props&0x2000 != 0,
"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)
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)
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) (map[string]any, error) {
if len(body) < 37 {
return nil, fmt.Errorf("%w: registration body len=%d", ErrFrameTooShort, len(body))
}
registration := map[string]any{
"province": binary.BigEndian.Uint16(body[0:2]),
"city": binary.BigEndian.Uint16(body[2:4]),
"manufacturer": fixedText(body[4:9]),
"device_type": fixedText(body[9:29]),
"device_id": fixedText(body[29:36]),
"plate_color": body[36],
"plate": freeText(body[37:]),
}
return registration, nil
}
func parseAuthentication(body []byte) map[string]any {
token := freeText(body)
out := map[string]any{"token": token}
return out
}
func parseLocation(body []byte) (Location, error) {
if len(body) < 28 {
return Location{}, fmt.Errorf("%w: location body len=%d", ErrFrameTooShort, len(body))
}
location := Location{
AlarmFlag: binary.BigEndian.Uint32(body[0:4]),
StatusFlag: 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,
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)),
}
// JT/T 808-2011 table 27: id 0x01 is GPS total mileage, DWORD, 0.1 km.
if id == 0x01 && size == 4 {
mileage := float64(binary.BigEndian.Uint32(value)) / 10
location.TotalMileageKM = &mileage
item.Parsed = map[string]any{
"name": envelope.FieldTotalMileageKM,
"value": mileage,
"unit": "km",
}
}
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 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 {
return strings.TrimRight(strings.TrimSpace(string(data)), "\x00")
}
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
}