feat: scaffold go vehicle gateway core
This commit is contained in:
300
go/vehicle-gateway/internal/protocol/jt808/parser.go
Normal file
300
go/vehicle-gateway/internal/protocol/jt808/parser.go
Normal file
@@ -0,0 +1,300 @@
|
||||
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
|
||||
headerLen := 12
|
||||
packageInfo := map[string]any(nil)
|
||||
if props&0x2000 != 0 {
|
||||
headerLen = 16
|
||||
packageInfo = map[string]any{
|
||||
"total": binary.BigEndian.Uint16(raw[12:14]),
|
||||
"index": binary.BigEndian.Uint16(raw[14:16]),
|
||||
}
|
||||
}
|
||||
if len(raw) < headerLen+int(bodySize)+1 {
|
||||
return envelope.FrameEnvelope{}, fmt.Errorf("%w: bodySize=%d len=%d", ErrFrameTooShort, bodySize, len(raw))
|
||||
}
|
||||
|
||||
phoneBCD := raw[4:10]
|
||||
phone := bcdString(phoneBCD)
|
||||
body := raw[headerLen : headerLen+int(bodySize)]
|
||||
parsed := map[string]any{
|
||||
"header": map[string]any{
|
||||
"message_id": fmt.Sprintf("0x%04X", messageID),
|
||||
"properties": props,
|
||||
"body_size": bodySize,
|
||||
"phone_bcd_hex": strings.ToUpper(hex.EncodeToString(phoneBCD)),
|
||||
"phone": phone,
|
||||
"phone_trim_zero": strings.TrimLeft(phone, "0"),
|
||||
"sequence": binary.BigEndian.Uint16(raw[10:12]),
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
||||
env := envelope.FrameEnvelope{
|
||||
Protocol: envelope.ProtocolJT808,
|
||||
MessageID: fmt.Sprintf("0x%04X", messageID),
|
||||
Sequence: binary.BigEndian.Uint16(raw[10:12]),
|
||||
Phone: phone,
|
||||
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 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 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
|
||||
}
|
||||
Reference in New Issue
Block a user