fix: align jt808 parser with protocol variants

This commit is contained in:
lingniu
2026-07-02 00:14:02 +08:00
parent b0150a5503
commit 9045b871d6
2 changed files with 244 additions and 26 deletions

View File

@@ -10,8 +10,8 @@ import (
"time"
"unicode/utf8"
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
"golang.org/x/text/encoding/simplifiedchinese"
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
)
var (
@@ -97,16 +97,19 @@ func ParseFrame(raw []byte, receivedAtMS int64, sourceEndpoint string) (envelope
phoneEnd = 15
serialStart = 15
}
if props&0x2000 != 0 {
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]),
}
}
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)
@@ -126,7 +129,7 @@ func ParseFrame(raw []byte, receivedAtMS int64, sourceEndpoint string) (envelope
"phone": phone,
"phone_trim_zero": phoneTrimZero,
"sequence": binary.BigEndian.Uint16(raw[serialStart : serialStart+2]),
"subpackage": props&0x2000 != 0,
"subpackage": subpackage,
"package_info": packageInfo,
"encryption_flags": (props >> 10) & 0x07,
},
@@ -155,7 +158,7 @@ func ParseFrame(raw []byte, receivedAtMS int64, sourceEndpoint string) (envelope
fields[envelope.FieldTotalMileageKM] = *location.TotalMileageKM
}
} else if messageID == 0x0100 {
registration, err := parseRegistration(body)
registration, err := parseRegistration(body, versioned)
if err != nil {
return envelope.FrameEnvelope{}, err
}
@@ -167,7 +170,7 @@ func ParseFrame(raw []byte, receivedAtMS int64, sourceEndpoint string) (envelope
fields["plate"] = plate
}
} else if messageID == 0x0102 {
authentication := parseAuthentication(body)
authentication := parseAuthentication(body, versioned)
parsed["authentication"] = authentication
if token, ok := authentication["token"].(string); ok {
fields["auth_token"] = token
@@ -195,37 +198,107 @@ func ParseFrame(raw []byte, receivedAtMS int64, sourceEndpoint string) (envelope
return env, nil
}
func parseRegistration(body []byte) (map[string]any, error) {
if len(body) < 37 {
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{
"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:]),
"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) map[string]any {
token := freeText(body)
out := map[string]any{"token": token}
return out
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: 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,
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]),
@@ -256,8 +329,8 @@ func parseAdditional(data []byte, location *Location) []AdditionalItem {
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 {
switch {
case id == 0x01 && size == 4:
mileage := float64(binary.BigEndian.Uint32(value)) / 10
location.TotalMileageKM = &mileage
item.Parsed = map[string]any{
@@ -265,6 +338,57 @@ func parseAdditional(data []byte, location *Location) []AdditionalItem {
"value": mileage,
"unit": "km",
}
case id == 0x02 && size == 2:
item.Parsed = map[string]any{
"name": "fuel_l",
"value": float64(binary.BigEndian.Uint16(value)) / 10,
"unit": "L",
}
case id == 0x03 && size == 2:
item.Parsed = map[string]any{
"name": "recorder_speed_kmh",
"value": float64(binary.BigEndian.Uint16(value)) / 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 == 0x06 && size == 2:
item.Parsed = map[string]any{
"name": "carriage_temperature_c",
"value": int16(binary.BigEndian.Uint16(value)),
"unit": "C",
}
case id == 0x25 && size == 4:
item.Parsed = map[string]any{
"name": "extended_vehicle_signal_status",
"value": binary.BigEndian.Uint32(value),
}
case id == 0x2a && size == 2:
item.Parsed = map[string]any{
"name": "io_status",
"value": binary.BigEndian.Uint16(value),
}
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:]