fix: support extended gb32960 platform login
This commit is contained in:
@@ -38,7 +38,7 @@ func ExtractFrames(stream []byte) (frames [][]byte, remainder []byte, err error)
|
|||||||
return frames, append([]byte(nil), remaining...), nil
|
return frames, append([]byte(nil), remaining...), nil
|
||||||
}
|
}
|
||||||
bodyLen := int(binary.BigEndian.Uint16(remaining[22:24]))
|
bodyLen := int(binary.BigEndian.Uint16(remaining[22:24]))
|
||||||
frameLen := headerLen + bodyLen + 1
|
frameLen := frameLength(remaining, bodyLen)
|
||||||
if len(remaining) < frameLen {
|
if len(remaining) < frameLen {
|
||||||
return frames, append([]byte(nil), remaining...), nil
|
return frames, append([]byte(nil), remaining...), nil
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,8 @@ func ParseFrame(raw []byte, receivedAtMS int64, sourceEndpoint string) (envelope
|
|||||||
return envelope.FrameEnvelope{}, fmt.Errorf("%w: %s", ErrBadStartSymbol, hex.EncodeToString(raw[:2]))
|
return envelope.FrameEnvelope{}, fmt.Errorf("%w: %s", ErrBadStartSymbol, hex.EncodeToString(raw[:2]))
|
||||||
}
|
}
|
||||||
bodyLen := int(binary.BigEndian.Uint16(raw[22:24]))
|
bodyLen := int(binary.BigEndian.Uint16(raw[22:24]))
|
||||||
if len(raw) != headerLen+bodyLen+1 {
|
actualBodyLen := len(raw) - headerLen - 1
|
||||||
|
if len(raw) != headerLen+bodyLen+1 && !isExtendedPlatformLogin(raw[2], bodyLen, actualBodyLen) {
|
||||||
return envelope.FrameEnvelope{}, fmt.Errorf("%w: declared=%d actual=%d", ErrBodyLength, bodyLen, len(raw)-headerLen-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 {
|
if got, want := bcc(raw[2:len(raw)-1]), raw[len(raw)-1]; got != want {
|
||||||
@@ -69,7 +70,7 @@ func ParseFrame(raw []byte, receivedAtMS int64, sourceEndpoint string) (envelope
|
|||||||
command := raw[2]
|
command := raw[2]
|
||||||
responseFlag := raw[3]
|
responseFlag := raw[3]
|
||||||
vin := strings.TrimRight(string(raw[4:21]), "\x00 ")
|
vin := strings.TrimRight(string(raw[4:21]), "\x00 ")
|
||||||
body := raw[headerLen : headerLen+bodyLen]
|
body := raw[headerLen : headerLen+actualBodyLen]
|
||||||
parsed := map[string]any{
|
parsed := map[string]any{
|
||||||
"header": map[string]any{
|
"header": map[string]any{
|
||||||
"version": version,
|
"version": version,
|
||||||
@@ -78,6 +79,7 @@ func ParseFrame(raw []byte, receivedAtMS int64, sourceEndpoint string) (envelope
|
|||||||
"vin": vin,
|
"vin": vin,
|
||||||
"encrypt": raw[21],
|
"encrypt": raw[21],
|
||||||
"body_length": bodyLen,
|
"body_length": bodyLen,
|
||||||
|
"actual_body_length": actualBodyLen,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
fields := map[string]any{}
|
fields := map[string]any{}
|
||||||
@@ -91,6 +93,16 @@ func ParseFrame(raw []byte, receivedAtMS int64, sourceEndpoint string) (envelope
|
|||||||
parsed["device_time"] = eventTime.Format(time.RFC3339)
|
parsed["device_time"] = eventTime.Format(time.RFC3339)
|
||||||
}
|
}
|
||||||
parsed["data_units"] = units
|
parsed["data_units"] = units
|
||||||
|
} else if command == 0x05 {
|
||||||
|
login := parsePlatformLogin(body)
|
||||||
|
parsed["platform_login"] = login
|
||||||
|
if deviceTime, ok := login["login_time"].(string); ok && deviceTime != "" {
|
||||||
|
fields["device_time"] = deviceTime
|
||||||
|
parsed["device_time"] = deviceTime
|
||||||
|
}
|
||||||
|
if account, ok := login["username"].(string); ok && account != "" {
|
||||||
|
fields["platform_account"] = account
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
env := envelope.FrameEnvelope{
|
env := envelope.FrameEnvelope{
|
||||||
@@ -113,7 +125,7 @@ func parseDataBody(version string, body []byte, fields map[string]any) (time.Tim
|
|||||||
if len(body) < 6 {
|
if len(body) < 6 {
|
||||||
return time.Time{}, nil
|
return time.Time{}, nil
|
||||||
}
|
}
|
||||||
eventTime := parseBCDTime(body[:6])
|
eventTime := parseGBTime(body[:6])
|
||||||
cursor := 6
|
cursor := 6
|
||||||
var units []map[string]any
|
var units []map[string]any
|
||||||
for cursor < len(body) {
|
for cursor < len(body) {
|
||||||
@@ -303,6 +315,54 @@ func parseDataBody(version string, body []byte, fields map[string]any) (time.Tim
|
|||||||
return eventTime, units
|
return eventTime, units
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func frameLength(frameStart []byte, bodyLen int) int {
|
||||||
|
standardLen := headerLen + bodyLen + 1
|
||||||
|
if len(frameStart) < headerLen || frameStart[2] != 0x05 || bodyLen != 41 {
|
||||||
|
return standardLen
|
||||||
|
}
|
||||||
|
// Some field platform-login frames declare the standard 41-byte body but
|
||||||
|
// carry a 32-byte password, making the actual body 53 bytes. Only consume
|
||||||
|
// the extended form when its own BCC verifies so the splitter does not eat
|
||||||
|
// bytes from the next frame.
|
||||||
|
extendedLen := headerLen + 53 + 1
|
||||||
|
if len(frameStart) < extendedLen {
|
||||||
|
return standardLen
|
||||||
|
}
|
||||||
|
extended := frameStart[:extendedLen]
|
||||||
|
if bcc(extended[2:len(extended)-1]) == extended[len(extended)-1] {
|
||||||
|
return extendedLen
|
||||||
|
}
|
||||||
|
return standardLen
|
||||||
|
}
|
||||||
|
|
||||||
|
func isExtendedPlatformLogin(command byte, declaredBodyLen int, actualBodyLen int) bool {
|
||||||
|
return command == 0x05 && declaredBodyLen == 41 && actualBodyLen == 53
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePlatformLogin(body []byte) map[string]any {
|
||||||
|
login := map[string]any{
|
||||||
|
"raw_body_length": len(body),
|
||||||
|
}
|
||||||
|
if len(body) < 9 {
|
||||||
|
login["error"] = "truncated"
|
||||||
|
return login
|
||||||
|
}
|
||||||
|
loginTime := parseGBTime(body[:6])
|
||||||
|
if !loginTime.IsZero() {
|
||||||
|
login["login_time"] = loginTime.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
login["serial_no"] = binary.BigEndian.Uint16(body[6:8])
|
||||||
|
if len(body) >= 21 {
|
||||||
|
login["username"] = trimASCII(body[8:20])
|
||||||
|
}
|
||||||
|
if len(body) > 21 {
|
||||||
|
passwordEnd := len(body) - 1
|
||||||
|
login["password"] = trimASCII(body[20:passwordEnd])
|
||||||
|
login["encrypt_rule"] = body[passwordEnd]
|
||||||
|
}
|
||||||
|
return login
|
||||||
|
}
|
||||||
|
|
||||||
func parseVehicleData(data []byte) map[string]any {
|
func parseVehicleData(data []byte) map[string]any {
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"vehicle_status": int(data[0]),
|
"vehicle_status": int(data[0]),
|
||||||
@@ -751,24 +811,24 @@ func version(a byte, b byte) (string, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseBCDTime(data []byte) time.Time {
|
func parseGBTime(data []byte) time.Time {
|
||||||
if len(data) != 6 {
|
if len(data) != 6 {
|
||||||
return time.Time{}
|
return time.Time{}
|
||||||
}
|
}
|
||||||
year := 2000 + bcdByte(data[0])
|
year := 2000 + int(data[0])
|
||||||
month := time.Month(bcdByte(data[1]))
|
month := time.Month(data[1])
|
||||||
day := bcdByte(data[2])
|
day := int(data[2])
|
||||||
hour := bcdByte(data[3])
|
hour := int(data[3])
|
||||||
minute := bcdByte(data[4])
|
minute := int(data[4])
|
||||||
second := bcdByte(data[5])
|
second := int(data[5])
|
||||||
if month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59 {
|
if month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59 {
|
||||||
return time.Time{}
|
return time.Time{}
|
||||||
}
|
}
|
||||||
return time.Date(year, month, day, hour, minute, second, 0, time.FixedZone("Asia/Shanghai", 8*3600))
|
return time.Date(year, month, day, hour, minute, second, 0, time.FixedZone("Asia/Shanghai", 8*3600))
|
||||||
}
|
}
|
||||||
|
|
||||||
func bcdByte(value byte) int {
|
func trimASCII(data []byte) string {
|
||||||
return int(value>>4)*10 + int(value&0x0f)
|
return strings.TrimRight(string(data), "\x00 ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func bcc(data []byte) byte {
|
func bcc(data []byte) byte {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
const realFuelCellFrame = "232302FE4C423941333241323152304C53313730370102ED1A070116091C0102030100000008297D161F27104C020007D0000002010104564E204E205815CA271003000A000000B40002666902800200000100A101000400FFFF001205000733444601E2B5DB06013A0F6C018E0F4001014B01054A07000000000000000000080101161F271000900001900F520F530F520F640F660F650F650F650F650F660F650F650F640F640F550F550F540F560F580F570F540F570F560F550F5A0F550F580F570F5C0F5F0F5E0F610F5E0F5D0F5E0F5F0F5E0F5D0F610F5F0F610F600F600F610F610F610F600F630F630F600F610F610F610F620F620F610F6A0F6C0F6B0F6A0F6C0F6B0F6A0F6B0F6B0F6B0F6A0F610F630F630F630F640F660F640F630F630F630F580F5A0F580F5B0F550F580F5A0F5A0F590F580F5A0F620F650F650F650F660F640F640F640F640F640F660F650F640F650F660F650F670F670F670F670F630F620F680F670F650F650F670F650F610F5F0F600F620F5E0F5C0F590F4C0F490F480F440F470F480F470F420F430F450F420F450F440F440F430F530F540F520F400F410F4109010100084B4B4B4B4A4A4B4A3001026907B2FF00FF00380002002800000008006C00016C00080008000000080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008002800070007000700070007000700070007000700070007000700070007000700070007000700070007000700070007000700080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000831010E9C0005FFFFFFFF0E9CFFFFFFFF0083320E9C1B6115E212465733FFFFFFFFFF34FFFFFFFFFF002B800009690E9C2710000D000E83002402020500002700080001000000FD00000000FFFFFFFFFFFFFFFFFFFFFFFF1FFE1FF3001DBA"
|
const realFuelCellFrame = "232302FE4C423941333241323152304C53313730370102ED1A070116091C0102030100000008297D161F27104C020007D0000002010104564E204E205815CA271003000A000000B40002666902800200000100A101000400FFFF001205000733444601E2B5DB06013A0F6C018E0F4001014B01054A07000000000000000000080101161F271000900001900F520F530F520F640F660F650F650F650F650F660F650F650F640F640F550F550F540F560F580F570F540F570F560F550F5A0F550F580F570F5C0F5F0F5E0F610F5E0F5D0F5E0F5F0F5E0F5D0F610F5F0F610F600F600F610F610F610F600F630F630F600F610F610F610F620F620F610F6A0F6C0F6B0F6A0F6C0F6B0F6A0F6B0F6B0F6B0F6A0F610F630F630F630F640F660F640F630F630F630F580F5A0F580F5B0F550F580F5A0F5A0F590F580F5A0F620F650F650F650F660F640F640F640F640F640F660F650F640F650F660F650F670F670F670F670F630F620F680F670F650F650F670F650F610F5F0F600F620F5E0F5C0F590F4C0F490F480F440F470F480F470F420F430F450F420F450F440F440F430F530F540F520F400F410F4109010100084B4B4B4B4A4A4B4A3001026907B2FF00FF00380002002800000008006C00016C00080008000000080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008002800070007000700070007000700070007000700070007000700070007000700070007000700070007000700070007000700080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000831010E9C0005FFFFFFFF0E9CFFFFFFFF0083320E9C1B6115E212465733FFFFFFFFFF34FFFFFFFFFF002B800009690E9C2710000D000E83002402020500002700080001000000FD00000000FFFFFFFFFFFFFFFFFFFFFFFF1FFE1FF3001DBA"
|
||||||
|
|
||||||
func TestExtractFramesKeepsPartialRemainderAndParsesHeader(t *testing.T) {
|
func TestExtractFramesKeepsPartialRemainderAndParsesHeader(t *testing.T) {
|
||||||
first := buildFrame(0x02, 0xfe, "LNBSCB3D4R1234567", []byte{0x1a, 0x06, 0x30, 0x16, 0x23, 0x57})
|
first := buildFrame(0x02, 0xfe, "LNBSCB3D4R1234567", []byte{0x1a, 0x06, 0x1e, 0x16, 0x17, 0x39})
|
||||||
second := buildFrame(0x05, 0xfe, "PLATFORM-LOGIN001", nil)
|
second := buildFrame(0x05, 0xfe, "PLATFORM-LOGIN001", nil)
|
||||||
stream := append([]byte{0x00, 0x01}, first...)
|
stream := append([]byte{0x00, 0x01}, first...)
|
||||||
stream = append(stream, second[:len(second)-3]...)
|
stream = append(stream, second[:len(second)-3]...)
|
||||||
@@ -49,8 +49,49 @@ func TestParseFrameRejectsBadBCC(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExtractFramesSupportsExtendedPlatformLogin(t *testing.T) {
|
||||||
|
body := []byte{0x1a, 0x07, 0x02, 0x00, 0x26, 0x1f, 0x00, 0x01}
|
||||||
|
body = append(body, fixedASCII("Hyundai", 12)...)
|
||||||
|
body = append(body, fixedASCII("f2e3445d7cda409fb4f278f6fb890734", 32)...)
|
||||||
|
body = append(body, 0x01)
|
||||||
|
frame := buildFrameWithDeclaredLength(0x05, 0xfe, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", body, 41)
|
||||||
|
|
||||||
|
frames, remainder, err := ExtractFrames(append(frame, []byte{0x23, 0x23, 0x05}...))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ExtractFrames() error = %v", err)
|
||||||
|
}
|
||||||
|
if len(frames) != 1 || len(remainder) != 3 {
|
||||||
|
t.Fatalf("unexpected split frames=%d remainder=%s", len(frames), hex.EncodeToString(remainder))
|
||||||
|
}
|
||||||
|
if len(frames[0]) != headerLen+53+1 {
|
||||||
|
t.Fatalf("extended frame len = %d", len(frames[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
env, err := ParseFrame(frames[0], 1782745114999, "8.134.95.166:39376")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseFrame() error = %v", err)
|
||||||
|
}
|
||||||
|
if env.MessageID != "0x05" || env.ParseStatus != envelope.ParseOK {
|
||||||
|
t.Fatalf("unexpected env: %#v", env)
|
||||||
|
}
|
||||||
|
header := env.Parsed["header"].(map[string]any)
|
||||||
|
if header["body_length"] != 41 || header["actual_body_length"] != 53 {
|
||||||
|
t.Fatalf("unexpected length header: %#v", header)
|
||||||
|
}
|
||||||
|
login := env.Parsed["platform_login"].(map[string]any)
|
||||||
|
if login["username"] != "Hyundai" || login["password"] != "f2e3445d7cda409fb4f278f6fb890734" {
|
||||||
|
t.Fatalf("unexpected platform login: %#v", login)
|
||||||
|
}
|
||||||
|
if login["login_time"] != "2026-07-02T00:38:31+08:00" {
|
||||||
|
t.Fatalf("unexpected login time: %#v", login)
|
||||||
|
}
|
||||||
|
if env.Fields["platform_account"] != "Hyundai" {
|
||||||
|
t.Fatalf("platform account not exposed: %#v", env.Fields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseFrameExtractsRealtimeVehicleMileageAndPosition(t *testing.T) {
|
func TestParseFrameExtractsRealtimeVehicleMileageAndPosition(t *testing.T) {
|
||||||
body := []byte{0x1a, 0x06, 0x30, 0x16, 0x23, 0x57}
|
body := []byte{0x1a, 0x06, 0x1e, 0x16, 0x17, 0x39}
|
||||||
body = append(body, 0x01)
|
body = append(body, 0x01)
|
||||||
body = append(body,
|
body = append(body,
|
||||||
0x01, // vehicle status
|
0x01, // vehicle status
|
||||||
@@ -163,13 +204,23 @@ func assertIntField(t *testing.T, env envelope.FrameEnvelope, key string, want i
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildFrame(command byte, response byte, vin string, body []byte) []byte {
|
func buildFrame(command byte, response byte, vin string, body []byte) []byte {
|
||||||
|
return buildFrameWithDeclaredLength(command, response, vin, body, len(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildFrameWithDeclaredLength(command byte, response byte, vin string, body []byte, declaredBodyLen int) []byte {
|
||||||
frame := []byte{'#', '#', command, response}
|
frame := []byte{'#', '#', command, response}
|
||||||
vinBytes := []byte(vin)
|
vinBytes := []byte(vin)
|
||||||
if len(vinBytes) < 17 {
|
if len(vinBytes) < 17 {
|
||||||
vinBytes = append(vinBytes, make([]byte, 17-len(vinBytes))...)
|
vinBytes = append(vinBytes, make([]byte, 17-len(vinBytes))...)
|
||||||
}
|
}
|
||||||
frame = append(frame, vinBytes[:17]...)
|
frame = append(frame, vinBytes[:17]...)
|
||||||
frame = append(frame, 0x01, byte(len(body)>>8), byte(len(body)))
|
frame = append(frame, 0x01, byte(declaredBodyLen>>8), byte(declaredBodyLen))
|
||||||
frame = append(frame, body...)
|
frame = append(frame, body...)
|
||||||
return append(frame, bcc(frame[2:]))
|
return append(frame, bcc(frame[2:]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fixedASCII(value string, size int) []byte {
|
||||||
|
out := make([]byte, size)
|
||||||
|
copy(out, []byte(value))
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user