feat: build vehicle data platform and production pipeline

This commit is contained in:
lingniu
2026-07-14 12:35:33 +08:00
parent b452be3b94
commit bb59303a4b
270 changed files with 88016 additions and 1975 deletions

View File

@@ -124,6 +124,28 @@ func TestAutoResponseEchoesRawVINAndOriginalTime(t *testing.T) {
}
}
func TestAutoResponseRejectsEnforcedInvalidPlatformLogin(t *testing.T) {
body := []byte{0x1a, 0x07, 0x02, 0x00, 0x26, 0x0f, 0x00, 0x01}
body = append(body, fixedASCII("platform-a", 12)...)
body = append(body, fixedASCII("wrong-password", 20)...)
body = append(body, 0x01)
request := buildFrame(0x05, 0xfe, "12345678901234567", body)
env, err := ParseFrame(request, 1782914969584, "127.0.0.1:32960")
if err != nil {
t.Fatal(err)
}
env.AuthenticationEnforced = true
env.AuthenticationStatus = "rejected"
response, ok, err := AutoResponse(request, env)
if err != nil || !ok {
t.Fatalf("AutoResponse() ok=%v err=%v", ok, err)
}
if response[3] != responseError {
t.Fatalf("response flag = 0x%02x, want 0x%02x", response[3], responseError)
}
}
func TestParseFrameExtractsRealtimeVehicleMileageAndPosition(t *testing.T) {
body := []byte{0x1a, 0x06, 0x1e, 0x16, 0x17, 0x39}
body = append(body, 0x01)

View File

@@ -12,6 +12,7 @@ var ErrResponseFrameTooShort = errors.New("gb32960 response frame too short")
const (
responseSuccess = byte(0x01)
responseError = byte(0x02)
responseCommand = byte(0xfe)
encryptNone = byte(0x01)
)
@@ -37,7 +38,11 @@ func AutoResponse(raw []byte, env envelope.FrameEnvelope) ([]byte, bool, error)
if timestamp := responseTime(raw, command); !timestamp.IsZero() {
body = encodeGBTime(timestamp)
}
return buildResponse(raw[0], command, responseSuccess, raw[4:21], body), true, nil
responseFlag := responseSuccess
if command == 0x05 && env.AuthenticationEnforced && env.AuthenticationStatus != "accepted" {
responseFlag = responseError
}
return buildResponse(raw[0], command, responseFlag, raw[4:21], body), true, nil
}
func shouldRespond(command byte) bool {

View File

@@ -416,6 +416,32 @@ func TestAutoResponderBuildsVersionedGeneralAck(t *testing.T) {
}
}
func TestAutoResponderRejectsEnforcedInvalidAuthentication(t *testing.T) {
request := buildFrame(0x0102, "064646848757", 13, []byte("wrong-code"))
env, err := ParseFrame(request, 1782918600000, "127.0.0.1:808")
if err != nil {
t.Fatal(err)
}
env.AuthenticationEnforced = true
env.AuthenticationStatus = "rejected"
response, ok, err := NewAutoResponder("issued-code").Respond(request, env)
if err != nil || !ok {
t.Fatalf("Respond() ok=%v err=%v", ok, err)
}
frames, remainder, err := ExtractFrames(response)
if err != nil || len(frames) != 1 || len(remainder) != 0 {
t.Fatalf("ExtractFrames() frames=%d remainder=%x err=%v", len(frames), remainder, err)
}
payload := frames[0]
if got := binary.BigEndian.Uint16(payload[0:2]); got != msgPlatformGeneralResponse {
t.Fatalf("message id = 0x%04x", got)
}
if result := payload[len(payload)-2]; result != 1 {
t.Fatalf("authentication response result = %d, want 1", result)
}
}
func TestExtractFramesHandlesEscapedPayload(t *testing.T) {
payload := []byte{0x02, 0x00, 0x00, 0x02, 0x01, 0x33, 0x07, 0x79, 0x54, 0x25, 0x00, 0x01, 0x7e, 0x7d}
frame := append([]byte{0x7e}, escape(append(payload, checksum(payload)))...)

View File

@@ -49,6 +49,9 @@ func (r AutoResponder) Respond(raw []byte, env envelope.FrameEnvelope) ([]byte,
binary.BigEndian.PutUint16(body[0:2], header.sequence)
binary.BigEndian.PutUint16(body[2:4], header.messageID)
body[4] = 0
if header.messageID == 0x0102 && env.AuthenticationEnforced && env.AuthenticationStatus != "accepted" {
body[4] = 1
}
return encodeResponse(msgPlatformGeneralResponse, header, body), true, nil
}

View File

@@ -52,6 +52,9 @@ func ParseMessage(endpoint string, topic string, payload []byte, receivedAtMS in
DeviceID: deviceID,
Plate: plate,
SourceEndpoint: "mqtt://" + endpoint + topic,
SourceCode: sourceCodeFromEndpoint(endpoint),
PlatformName: platformNameFromEndpoint(endpoint),
SourceKind: "PLATFORM",
EventTimeMS: eventTimeMS,
ReceivedAtMS: receivedAtMS,
RawText: string(payload),
@@ -63,6 +66,32 @@ func ParseMessage(endpoint string, topic string, payload []byte, receivedAtMS in
return env, nil
}
func platformNameFromEndpoint(endpoint string) string {
endpoint = strings.TrimSpace(endpoint)
if strings.EqualFold(endpoint, "yutong") {
return "宇通"
}
return endpoint
}
func sourceCodeFromEndpoint(endpoint string) string {
endpoint = strings.TrimSpace(endpoint)
var builder strings.Builder
for _, r := range endpoint {
switch {
case r >= 'a' && r <= 'z':
builder.WriteRune(r)
case r >= 'A' && r <= 'Z':
builder.WriteRune(r)
case r >= '0' && r <= '9':
builder.WriteRune(r)
case r == '_' || r == '-' || r == '.':
builder.WriteRune(r)
}
}
return builder.String()
}
func fieldsFromData(data map[string]any) map[string]any {
fields := map[string]any{}
if speed, ok := firstFloat(data, "METER_SPEED", "speed", "speed_kmh"); ok {

View File

@@ -47,6 +47,20 @@ func TestParseMessageMapsYutongPayloadToEnvelope(t *testing.T) {
if env.RawText == "" || env.Parsed["data"] == nil {
t.Fatalf("raw/parsed missing: %#v", env)
}
if env.SourceCode != "endpoint-a" || env.PlatformName != "endpoint-a" || env.SourceKind != "PLATFORM" {
t.Fatalf("source metadata = code:%q platform:%q kind:%q", env.SourceCode, env.PlatformName, env.SourceKind)
}
}
func TestParseMessageUsesYutongEndpointDisplayName(t *testing.T) {
payload := []byte(`{"device":"LMRKH9AC3R1004101","time":"20260413100000","data":{"TOTAL_MILEAGE":56905000}}`)
env, err := ParseMessage("yutong", "/ytforward/shln/1", payload, 1782745114999)
if err != nil {
t.Fatalf("ParseMessage() error = %v", err)
}
if env.SourceCode != "yutong" || env.PlatformName != "宇通" || env.SourceKind != "PLATFORM" {
t.Fatalf("source metadata = code:%q platform:%q kind:%q", env.SourceCode, env.PlatformName, env.SourceKind)
}
}
func TestParseMessageUsesDeviceAsVehicleKeyWhenNotVIN(t *testing.T) {