package telemetry import ( "strings" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" ) type LocationProjection struct { Latitude float64 Longitude float64 SpeedKMH *float64 SOCPercent *float64 AltitudeM *float64 DirectionDeg *float64 AlarmFlag *int64 StatusFlag *int64 } type locationFieldMapping struct { Latitude []string Longitude []string SpeedKMH []string SOCPercent []string AltitudeM []string DirectionDeg []string AlarmFlag []string StatusFlag []string } // HasRealtimeFields separates protocol telemetry from headers, identity // annotations, and transport metadata before realtime/stat projections. func HasRealtimeFields(protocol envelope.Protocol, fields map[string]any) bool { switch protocol { case envelope.ProtocolGB32960: for field := range fields { if !strings.HasPrefix(field, "gb32960.") { continue } if strings.HasPrefix(field, "gb32960.header.") || strings.HasPrefix(field, "gb32960.platform.") || strings.HasPrefix(field, "gb32960.identity.") || strings.HasPrefix(field, "gb32960.device_time.") { continue } return true } return false case envelope.ProtocolJT808: return hasPrefix(fields, "jt808.location.") case envelope.ProtocolYutongMQTT: return hasPrefix(fields, "yutong_mqtt.data.") || hasPrefix(fields, "yutong_mqtt.root.data.") default: return false } } // LocationProjectionForProtocol is the single protocol-field contract used by // storage projections. It intentionally ignores bare standardized field names. func LocationProjectionForProtocol(protocol envelope.Protocol, fields map[string]any) (LocationProjection, bool) { mapping := locationMapping(protocol) latitude, hasLatitude := firstNumber(fields, mapping.Latitude) longitude, hasLongitude := firstNumber(fields, mapping.Longitude) if !hasLatitude || !hasLongitude { return LocationProjection{}, false } return LocationProjection{ Latitude: latitude, Longitude: longitude, SpeedKMH: numberPointer(fields, mapping.SpeedKMH), SOCPercent: numberPointer(fields, mapping.SOCPercent), AltitudeM: numberPointer(fields, mapping.AltitudeM), DirectionDeg: numberPointer(fields, mapping.DirectionDeg), AlarmFlag: integerPointer(fields, mapping.AlarmFlag), StatusFlag: integerPointer(fields, mapping.StatusFlag), }, true } func locationMapping(protocol envelope.Protocol) locationFieldMapping { switch protocol { case envelope.ProtocolGB32960: return locationFieldMapping{ Latitude: []string{"gb32960.position.latitude"}, Longitude: []string{"gb32960.position.longitude"}, SpeedKMH: []string{"gb32960.vehicle.speed_kmh"}, SOCPercent: []string{"gb32960.vehicle.soc_percent"}, } case envelope.ProtocolJT808: return locationFieldMapping{ Latitude: []string{"jt808.location.latitude"}, Longitude: []string{"jt808.location.longitude"}, SpeedKMH: []string{"jt808.location.speed_kmh"}, AltitudeM: []string{"jt808.location.altitude_m"}, DirectionDeg: []string{"jt808.location.direction_deg"}, AlarmFlag: []string{"jt808.location.alarm_flag"}, StatusFlag: []string{"jt808.location.status_flag"}, } case envelope.ProtocolYutongMQTT: return locationFieldMapping{ Latitude: []string{ "yutong_mqtt.data.latitude", "yutong_mqtt.root.data.latitude", }, Longitude: []string{ "yutong_mqtt.data.longitude", "yutong_mqtt.root.data.longitude", }, SpeedKMH: []string{ "yutong_mqtt.data.meter_speed", "yutong_mqtt.root.data.meter_speed", "yutong_mqtt.data.speed_kmh", "yutong_mqtt.root.data.speed_kmh", "yutong_mqtt.data.speed", "yutong_mqtt.root.data.speed", }, SOCPercent: []string{ "yutong_mqtt.data.battery_capacity_soc", "yutong_mqtt.root.data.battery_capacity_soc", "yutong_mqtt.data.soc_percent", "yutong_mqtt.root.data.soc_percent", }, DirectionDeg: []string{ "yutong_mqtt.data.gpsdirection", "yutong_mqtt.root.data.gpsdirection", "yutong_mqtt.data.direction_deg", "yutong_mqtt.root.data.direction_deg", "yutong_mqtt.data.direction", "yutong_mqtt.root.data.direction", }, } default: return locationFieldMapping{} } } func firstNumber(fields map[string]any, keys []string) (float64, bool) { for _, key := range keys { if value, ok := Number(fields, key); ok { return value, true } } return 0, false } func numberPointer(fields map[string]any, keys []string) *float64 { value, ok := firstNumber(fields, keys) if !ok { return nil } return &value } func integerPointer(fields map[string]any, keys []string) *int64 { value, ok := firstNumber(fields, keys) if !ok { return nil } integer := int64(value) return &integer } func hasPrefix(fields map[string]any, prefix string) bool { for field := range fields { if strings.HasPrefix(field, prefix) { return true } } return false }