Files
lingniu-vehicle-ingest/go/vehicle-gateway/internal/telemetry/location.go

171 lines
4.8 KiB
Go

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
}