Fix aircraft markers not updating positions in real-time
Root cause: The merger was blocking position updates from the same source after the first position was established, designed for multi-source scenarios but preventing single-source position updates. Changes: - Refactor JavaScript into modular architecture (WebSocketManager, AircraftManager, MapManager, UIManager) - Add CPR coordinate validation to prevent invalid latitude/longitude values - Fix merger to allow position updates from same source for moving aircraft - Add comprehensive coordinate bounds checking in CPR decoder - Update HTML to use new modular JavaScript with cache busting - Add WebSocket debug logging to track data flow Technical details: - CPR decoder now validates coordinates within ±90° latitude, ±180° longitude - Merger allows updates when currentBest == sourceID (same source continuous updates) - JavaScript modules provide better separation of concerns and debugging - WebSocket properly transmits updated aircraft coordinates to frontend 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
ddffe1428d
commit
1de3e092ae
13 changed files with 2222 additions and 33 deletions
|
|
@ -20,6 +20,8 @@
|
|||
package merger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -72,6 +74,94 @@ type AircraftState struct {
|
|||
UpdateRate float64 `json:"update_rate"` // Recent updates per second
|
||||
}
|
||||
|
||||
// MarshalJSON provides custom JSON marshaling for AircraftState to format ICAO24 as hex.
|
||||
func (a *AircraftState) MarshalJSON() ([]byte, error) {
|
||||
// Create a struct that mirrors AircraftState but with ICAO24 as string
|
||||
return json.Marshal(&struct {
|
||||
// From embedded modes.Aircraft
|
||||
ICAO24 string `json:"ICAO24"`
|
||||
Callsign string `json:"Callsign"`
|
||||
Latitude float64 `json:"Latitude"`
|
||||
Longitude float64 `json:"Longitude"`
|
||||
Altitude int `json:"Altitude"`
|
||||
BaroAltitude int `json:"BaroAltitude"`
|
||||
GeomAltitude int `json:"GeomAltitude"`
|
||||
VerticalRate int `json:"VerticalRate"`
|
||||
GroundSpeed int `json:"GroundSpeed"`
|
||||
Track int `json:"Track"`
|
||||
Heading int `json:"Heading"`
|
||||
Category string `json:"Category"`
|
||||
Squawk string `json:"Squawk"`
|
||||
Emergency string `json:"Emergency"`
|
||||
OnGround bool `json:"OnGround"`
|
||||
Alert bool `json:"Alert"`
|
||||
SPI bool `json:"SPI"`
|
||||
NACp uint8 `json:"NACp"`
|
||||
NACv uint8 `json:"NACv"`
|
||||
SIL uint8 `json:"SIL"`
|
||||
SelectedAltitude int `json:"SelectedAltitude"`
|
||||
SelectedHeading float64 `json:"SelectedHeading"`
|
||||
BaroSetting float64 `json:"BaroSetting"`
|
||||
|
||||
// From AircraftState
|
||||
Sources map[string]*SourceData `json:"sources"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
FirstSeen time.Time `json:"first_seen"`
|
||||
TotalMessages int64 `json:"total_messages"`
|
||||
PositionHistory []PositionPoint `json:"position_history"`
|
||||
SignalHistory []SignalPoint `json:"signal_history"`
|
||||
AltitudeHistory []AltitudePoint `json:"altitude_history"`
|
||||
SpeedHistory []SpeedPoint `json:"speed_history"`
|
||||
Distance float64 `json:"distance"`
|
||||
Bearing float64 `json:"bearing"`
|
||||
Age float64 `json:"age"`
|
||||
MLATSources []string `json:"mlat_sources"`
|
||||
PositionSource string `json:"position_source"`
|
||||
UpdateRate float64 `json:"update_rate"`
|
||||
}{
|
||||
// Copy all fields from Aircraft
|
||||
ICAO24: fmt.Sprintf("%06X", a.Aircraft.ICAO24),
|
||||
Callsign: a.Aircraft.Callsign,
|
||||
Latitude: a.Aircraft.Latitude,
|
||||
Longitude: a.Aircraft.Longitude,
|
||||
Altitude: a.Aircraft.Altitude,
|
||||
BaroAltitude: a.Aircraft.BaroAltitude,
|
||||
GeomAltitude: a.Aircraft.GeomAltitude,
|
||||
VerticalRate: a.Aircraft.VerticalRate,
|
||||
GroundSpeed: a.Aircraft.GroundSpeed,
|
||||
Track: a.Aircraft.Track,
|
||||
Heading: a.Aircraft.Heading,
|
||||
Category: a.Aircraft.Category,
|
||||
Squawk: a.Aircraft.Squawk,
|
||||
Emergency: a.Aircraft.Emergency,
|
||||
OnGround: a.Aircraft.OnGround,
|
||||
Alert: a.Aircraft.Alert,
|
||||
SPI: a.Aircraft.SPI,
|
||||
NACp: a.Aircraft.NACp,
|
||||
NACv: a.Aircraft.NACv,
|
||||
SIL: a.Aircraft.SIL,
|
||||
SelectedAltitude: a.Aircraft.SelectedAltitude,
|
||||
SelectedHeading: a.Aircraft.SelectedHeading,
|
||||
BaroSetting: a.Aircraft.BaroSetting,
|
||||
|
||||
// Copy all fields from AircraftState
|
||||
Sources: a.Sources,
|
||||
LastUpdate: a.LastUpdate,
|
||||
FirstSeen: a.FirstSeen,
|
||||
TotalMessages: a.TotalMessages,
|
||||
PositionHistory: a.PositionHistory,
|
||||
SignalHistory: a.SignalHistory,
|
||||
AltitudeHistory: a.AltitudeHistory,
|
||||
SpeedHistory: a.SpeedHistory,
|
||||
Distance: a.Distance,
|
||||
Bearing: a.Bearing,
|
||||
Age: a.Age,
|
||||
MLATSources: a.MLATSources,
|
||||
PositionSource: a.PositionSource,
|
||||
UpdateRate: a.UpdateRate,
|
||||
})
|
||||
}
|
||||
|
||||
// SourceData represents data quality and statistics for a specific source-aircraft pair.
|
||||
// This information is used for data fusion decisions and signal quality analysis.
|
||||
type SourceData struct {
|
||||
|
|
@ -133,7 +223,7 @@ type Merger struct {
|
|||
sources map[string]*Source // Source ID -> source information
|
||||
mu sync.RWMutex // Protects all maps and slices
|
||||
historyLimit int // Maximum history points to retain
|
||||
staleTimeout time.Duration // Time before aircraft considered stale
|
||||
staleTimeout time.Duration // Time before aircraft considered stale (15 seconds)
|
||||
updateMetrics map[uint32]*updateMetric // ICAO24 -> update rate calculation data
|
||||
}
|
||||
|
||||
|
|
@ -147,7 +237,7 @@ type updateMetric struct {
|
|||
//
|
||||
// Default settings:
|
||||
// - History limit: 500 points per aircraft
|
||||
// - Stale timeout: 60 seconds
|
||||
// - Stale timeout: 15 seconds
|
||||
// - Empty aircraft and source maps
|
||||
// - Update metrics tracking enabled
|
||||
//
|
||||
|
|
@ -157,7 +247,7 @@ func NewMerger() *Merger {
|
|||
aircraft: make(map[uint32]*AircraftState),
|
||||
sources: make(map[string]*Source),
|
||||
historyLimit: 500,
|
||||
staleTimeout: 60 * time.Second,
|
||||
staleTimeout: 15 * time.Second, // Aircraft timeout - reasonable for ADS-B tracking
|
||||
updateMetrics: make(map[uint32]*updateMetric),
|
||||
}
|
||||
}
|
||||
|
|
@ -219,6 +309,8 @@ func (m *Merger) UpdateAircraft(sourceID string, aircraft *modes.Aircraft, signa
|
|||
updates: make([]time.Time, 0),
|
||||
}
|
||||
}
|
||||
// Note: For existing aircraft, we don't overwrite state.Aircraft here
|
||||
// The mergeAircraftData function will handle selective field updates
|
||||
|
||||
// Update or create source data
|
||||
srcData, srcExists := state.Sources[sourceID]
|
||||
|
|
@ -294,12 +386,16 @@ func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, so
|
|||
updatePosition := false
|
||||
|
||||
if state.Latitude == 0 {
|
||||
// First position update
|
||||
updatePosition = true
|
||||
} else if srcData, ok := state.Sources[sourceID]; ok {
|
||||
// Use position from source with strongest signal
|
||||
currentBest := m.getBestSignalSource(state)
|
||||
if currentBest == "" || srcData.SignalLevel > state.Sources[currentBest].SignalLevel {
|
||||
updatePosition = true
|
||||
} else if currentBest == sourceID {
|
||||
// Same source as current best - allow updates for moving aircraft
|
||||
updatePosition = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -615,7 +711,7 @@ func (m *Merger) GetStatistics() map[string]interface{} {
|
|||
// CleanupStale removes aircraft that haven't been updated recently.
|
||||
//
|
||||
// Aircraft are considered stale if they haven't received updates for longer
|
||||
// than staleTimeout (default 60 seconds). This cleanup prevents memory
|
||||
// than staleTimeout (default 15 seconds). This cleanup prevents memory
|
||||
// growth from aircraft that have left the coverage area or stopped transmitting.
|
||||
//
|
||||
// The cleanup also removes associated update metrics to free memory.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue