package yutongmqtt import ( "bytes" "encoding/json" "errors" "fmt" "strconv" "strings" "time" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" ) var ErrInvalidJSON = errors.New("yutong mqtt invalid json") func ParseMessage(endpoint string, topic string, payload []byte, receivedAtMS int64) (envelope.FrameEnvelope, error) { root := map[string]any{} decoder := json.NewDecoder(bytes.NewReader(payload)) decoder.UseNumber() if err := decoder.Decode(&root); err != nil { return envelope.FrameEnvelope{}, fmt.Errorf("%w: %v", ErrInvalidJSON, err) } data := objectValue(root, "data") if data == nil { data = root } deviceID := firstText(root, "device", "deviceId", "terminalId", "terminalID", "imei", "IMEI", "mqttDeviceId") externalVIN := firstText(root, "vin", "VIN", "externalVin", "vehicleVin") vin := externalVIN if vin == "" && looksLikeVIN(deviceID) { vin = deviceID } plate := firstText(root, "plateNo", "carNo", "plate", "vehicleNo") phone := firstText(root, "sim", "phone", "mobile") eventTimeMS := parseTimeMS(firstText(root, "time", "deviceTime", "eventTime", "gpsTime"), receivedAtMS) fields := fieldsFromData(data) parsed := map[string]any{ "endpoint": endpoint, "topic": topic, "root": root, "data": data, } env := envelope.FrameEnvelope{ Protocol: envelope.ProtocolYutongMQTT, MessageID: "MQTT", VIN: vin, VehicleKeyHint: deviceID, Phone: phone, DeviceID: deviceID, Plate: plate, SourceEndpoint: "mqtt://" + endpoint + topic, EventTimeMS: eventTimeMS, ReceivedAtMS: receivedAtMS, RawText: string(payload), Parsed: parsed, Fields: fields, ParseStatus: envelope.ParseOK, } env.EventID = env.StableEventID() return env, nil } func fieldsFromData(data map[string]any) map[string]any { fields := map[string]any{} if speed, ok := firstFloat(data, "METER_SPEED", "speed", "speed_kmh"); ok { fields[envelope.FieldSpeedKMH] = speed } if mileage, ok := firstFloat(data, "TOTAL_MILEAGE", "totalMileage", "total_mileage_km"); ok { fields[envelope.FieldTotalMileageKM] = mileage } if soc, ok := firstFloat(data, "BATTERY_CAPACITY_SOC", "soc", "SOC", "soc_percent"); ok { fields[envelope.FieldSOCPercent] = soc } if longitude, ok := firstCoord(data, "LONGITUDE", "longitude", "lng"); ok { fields[envelope.FieldLongitude] = longitude } if latitude, ok := firstCoord(data, "LATITUDE", "latitude", "lat"); ok { fields[envelope.FieldLatitude] = latitude } if direction, ok := firstFloat(data, "GPSDirection", "direction", "direction_deg"); ok { fields["direction_deg"] = direction } if gear, ok := firstFloat(data, "ON_GEAR", "gear"); ok { fields["gear"] = gear } return fields } func objectValue(root map[string]any, key string) map[string]any { value, ok := root[key] if !ok { return nil } typed, ok := value.(map[string]any) if !ok { return nil } return typed } func firstText(root map[string]any, keys ...string) string { for _, key := range keys { if value, ok := root[key]; ok && value != nil { text := strings.TrimSpace(fmt.Sprint(value)) if text != "" && text != "" { return text } } } return "" } func firstFloat(root map[string]any, keys ...string) (float64, bool) { for _, key := range keys { value, ok := root[key] if !ok || value == nil { continue } if parsed, ok := toFloat(value); ok { return parsed, true } } return 0, false } func firstCoord(root map[string]any, keys ...string) (float64, bool) { value, ok := firstFloat(root, keys...) if !ok { return 0, false } if value == 0 || int64(value) == 999999 { return 0, false } if value > 1000 || value < -1000 { value = value / 1_000_000 } return value, true } func toFloat(value any) (float64, bool) { switch typed := value.(type) { case json.Number: parsed, err := typed.Float64() return parsed, err == nil case float64: return typed, true case float32: return float64(typed), true case int: return float64(typed), true case int64: return float64(typed), true case string: parsed, err := strconv.ParseFloat(strings.TrimSpace(typed), 64) return parsed, err == nil default: return 0, false } } func parseTimeMS(raw string, fallback int64) int64 { raw = strings.TrimSpace(raw) if raw == "" { return fallback } if len(raw) == 14 { if parsed, err := time.ParseInLocation("20060102150405", raw, time.FixedZone("Asia/Shanghai", 8*3600)); err == nil { return parsed.UnixMilli() } } if parsed, err := time.Parse(time.RFC3339, raw); err == nil { return parsed.UnixMilli() } if epoch, err := strconv.ParseInt(raw, 10, 64); err == nil { if epoch > 10_000_000_000 { return epoch } return epoch * 1000 } return fallback } func looksLikeVIN(value string) bool { value = strings.TrimSpace(value) return len(value) == 17 }