fix: support extended gb32960 platform login

This commit is contained in:
lingniu
2026-07-02 00:47:01 +08:00
parent e37dd39c89
commit 055373c405
2 changed files with 133 additions and 22 deletions

View File

@@ -38,7 +38,7 @@ func ExtractFrames(stream []byte) (frames [][]byte, remainder []byte, err error)
return frames, append([]byte(nil), remaining...), nil
}
bodyLen := int(binary.BigEndian.Uint16(remaining[22:24]))
frameLen := headerLen + bodyLen + 1
frameLen := frameLength(remaining, bodyLen)
if len(remaining) < frameLen {
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]))
}
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)
}
if got, want := bcc(raw[2:len(raw)-1]), raw[len(raw)-1]; got != want {
@@ -69,15 +70,16 @@ func ParseFrame(raw []byte, receivedAtMS int64, sourceEndpoint string) (envelope
command := raw[2]
responseFlag := raw[3]
vin := strings.TrimRight(string(raw[4:21]), "\x00 ")
body := raw[headerLen : headerLen+bodyLen]
body := raw[headerLen : headerLen+actualBodyLen]
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,
"version": version,
"command": fmt.Sprintf("0x%02X", command),
"response_flag": fmt.Sprintf("0x%02X", responseFlag),
"vin": vin,
"encrypt": raw[21],
"body_length": bodyLen,
"actual_body_length": actualBodyLen,
},
}
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["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{
@@ -113,7 +125,7 @@ func parseDataBody(version string, body []byte, fields map[string]any) (time.Tim
if len(body) < 6 {
return time.Time{}, nil
}
eventTime := parseBCDTime(body[:6])
eventTime := parseGBTime(body[:6])
cursor := 6
var units []map[string]any
for cursor < len(body) {
@@ -303,6 +315,54 @@ func parseDataBody(version string, body []byte, fields map[string]any) (time.Tim
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 {
return map[string]any{
"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 {
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])
year := 2000 + int(data[0])
month := time.Month(data[1])
day := int(data[2])
hour := int(data[3])
minute := int(data[4])
second := int(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 trimASCII(data []byte) string {
return strings.TrimRight(string(data), "\x00 ")
}
func bcc(data []byte) byte {

View File

@@ -12,7 +12,7 @@ import (
const realFuelCellFrame = "232302FE4C423941333241323152304C53313730370102ED1A070116091C0102030100000008297D161F27104C020007D0000002010104564E204E205815CA271003000A000000B40002666902800200000100A101000400FFFF001205000733444601E2B5DB06013A0F6C018E0F4001014B01054A07000000000000000000080101161F271000900001900F520F530F520F640F660F650F650F650F650F660F650F650F640F640F550F550F540F560F580F570F540F570F560F550F5A0F550F580F570F5C0F5F0F5E0F610F5E0F5D0F5E0F5F0F5E0F5D0F610F5F0F610F600F600F610F610F610F600F630F630F600F610F610F610F620F620F610F6A0F6C0F6B0F6A0F6C0F6B0F6A0F6B0F6B0F6B0F6A0F610F630F630F630F640F660F640F630F630F630F580F5A0F580F5B0F550F580F5A0F5A0F590F580F5A0F620F650F650F650F660F640F640F640F640F640F660F650F640F650F660F650F670F670F670F670F630F620F680F670F650F650F670F650F610F5F0F600F620F5E0F5C0F590F4C0F490F480F440F470F480F470F420F430F450F420F450F440F440F430F530F540F520F400F410F4109010100084B4B4B4B4A4A4B4A3001026907B2FF00FF00380002002800000008006C00016C00080008000000080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008002800070007000700070007000700070007000700070007000700070007000700070007000700070007000700070007000700080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000831010E9C0005FFFFFFFF0E9CFFFFFFFF0083320E9C1B6115E212465733FFFFFFFFFF34FFFFFFFFFF002B800009690E9C2710000D000E83002402020500002700080001000000FD00000000FFFFFFFFFFFFFFFFFFFFFFFF1FFE1FF3001DBA"
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)
stream := append([]byte{0x00, 0x01}, first...)
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) {
body := []byte{0x1a, 0x06, 0x30, 0x16, 0x23, 0x57}
body := []byte{0x1a, 0x06, 0x1e, 0x16, 0x17, 0x39}
body = append(body, 0x01)
body = append(body,
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 {
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}
vinBytes := []byte(vin)
if len(vinBytes) < 17 {
vinBytes = append(vinBytes, make([]byte, 17-len(vinBytes))...)
}
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...)
return append(frame, bcc(frame[2:]))
}
func fixedASCII(value string, size int) []byte {
out := make([]byte, size)
copy(out, []byte(value))
return out
}