feat: build vehicle data platform and production pipeline
This commit is contained in:
170
go/vehicle-gateway/internal/telemetry/location.go
Normal file
170
go/vehicle-gateway/internal/telemetry/location.go
Normal file
@@ -0,0 +1,170 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user