feat: build vehicle data platform and production pipeline
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)))...)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user