feat: scaffold go vehicle gateway core
This commit is contained in:
237
go/vehicle-gateway/internal/protocol/gb32960/parser.go
Normal file
237
go/vehicle-gateway/internal/protocol/gb32960/parser.go
Normal file
@@ -0,0 +1,237 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user