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:
Ole-Morten Duesund 2025-08-24 14:04:17 +02:00
commit 1de3e092ae
13 changed files with 2222 additions and 33 deletions

View file

@ -30,8 +30,45 @@ package modes
import (
"fmt"
"math"
"sync"
)
// crcTable for Mode S CRC-24 validation
var crcTable [256]uint32
func init() {
// Initialize CRC table for Mode S CRC-24 (polynomial 0x1FFF409)
for i := 0; i < 256; i++ {
crc := uint32(i) << 16
for j := 0; j < 8; j++ {
if crc&0x800000 != 0 {
crc = (crc << 1) ^ 0x1FFF409
} else {
crc = crc << 1
}
}
crcTable[i] = crc & 0xFFFFFF
}
}
// validateModeSCRC validates the 24-bit CRC of a Mode S message
func validateModeSCRC(data []byte) bool {
if len(data) < 4 {
return false
}
// Calculate CRC for all bytes except the last 3 (which contain the CRC)
crc := uint32(0)
for i := 0; i < len(data)-3; i++ {
crc = ((crc << 8) ^ crcTable[((crc>>16)^uint32(data[i]))&0xFF]) & 0xFFFFFF
}
// Extract transmitted CRC from last 3 bytes
transmittedCRC := uint32(data[len(data)-3])<<16 | uint32(data[len(data)-2])<<8 | uint32(data[len(data)-1])
return crc == transmittedCRC
}
// Mode S Downlink Format (DF) constants.
// The DF field (first 5 bits) determines the message type and structure.
const (
@ -126,6 +163,9 @@ type Decoder struct {
cprOddLon map[uint32]float64 // Odd message longitude encoding (ICAO24 -> normalized lon)
cprEvenTime map[uint32]int64 // Timestamp of even message (for freshness comparison)
cprOddTime map[uint32]int64 // Timestamp of odd message (for freshness comparison)
// Mutex to protect concurrent access to CPR maps
mu sync.RWMutex
}
// NewDecoder creates a new Mode S/ADS-B decoder with initialized CPR tracking.
@ -168,6 +208,11 @@ func (d *Decoder) Decode(data []byte) (*Aircraft, error) {
return nil, fmt.Errorf("message too short: %d bytes", len(data))
}
// Validate CRC to reject corrupted messages that create ghost targets
if !validateModeSCRC(data) {
return nil, fmt.Errorf("invalid CRC - corrupted message")
}
df := (data[0] >> 3) & 0x1F
icao := d.extractICAO(data, df)
@ -337,7 +382,8 @@ func (d *Decoder) decodeAirbornePosition(data []byte, aircraft *Aircraft) {
cprLon := uint32(data[8]&0x01)<<16 | uint32(data[9])<<8 | uint32(data[10])
oddFlag := (data[6] >> 2) & 0x01
// Store CPR values for later decoding
// Store CPR values for later decoding (protected by mutex)
d.mu.Lock()
if oddFlag == 1 {
d.cprOddLat[aircraft.ICAO24] = float64(cprLat) / 131072.0
d.cprOddLon[aircraft.ICAO24] = float64(cprLon) / 131072.0
@ -345,6 +391,7 @@ func (d *Decoder) decodeAirbornePosition(data []byte, aircraft *Aircraft) {
d.cprEvenLat[aircraft.ICAO24] = float64(cprLat) / 131072.0
d.cprEvenLon[aircraft.ICAO24] = float64(cprLon) / 131072.0
}
d.mu.Unlock()
// Try to decode position if we have both even and odd messages
d.decodeCPRPosition(aircraft)
@ -374,15 +421,23 @@ func (d *Decoder) decodeAirbornePosition(data []byte, aircraft *Aircraft) {
// Parameters:
// - aircraft: Aircraft struct to update with decoded position
func (d *Decoder) decodeCPRPosition(aircraft *Aircraft) {
// Read CPR values with read lock
d.mu.RLock()
evenLat, evenExists := d.cprEvenLat[aircraft.ICAO24]
oddLat, oddExists := d.cprOddLat[aircraft.ICAO24]
if !evenExists || !oddExists {
d.mu.RUnlock()
return
}
evenLon := d.cprEvenLon[aircraft.ICAO24]
oddLon := d.cprOddLon[aircraft.ICAO24]
d.mu.RUnlock()
// Debug: Log CPR input values
fmt.Printf("CPR Debug %s: even=[%.6f,%.6f] odd=[%.6f,%.6f]\n",
aircraft.ICAO24, evenLat, evenLon, oddLat, oddLon)
// CPR decoding algorithm
dLat := 360.0 / 60.0
@ -398,6 +453,25 @@ func (d *Decoder) decodeCPRPosition(aircraft *Aircraft) {
latOdd -= 360
}
// Additional range correction to ensure valid latitude bounds (-90° to +90°)
if latEven > 90 {
latEven = 180 - latEven
} else if latEven < -90 {
latEven = -180 - latEven
}
if latOdd > 90 {
latOdd = 180 - latOdd
} else if latOdd < -90 {
latOdd = -180 - latOdd
}
// Validate final latitude values are within acceptable range
if math.Abs(latOdd) > 90 || math.Abs(latEven) > 90 {
// Invalid CPR decoding - skip position update
return
}
// Choose the most recent position
aircraft.Latitude = latOdd // Use odd for now, should check timestamps
@ -410,9 +484,20 @@ func (d *Decoder) decodeCPRPosition(aircraft *Aircraft) {
if lon >= 180 {
lon -= 360
} else if lon <= -180 {
lon += 360
}
// Validate longitude is within acceptable range
if math.Abs(lon) > 180 {
// Invalid longitude - skip position update
return
}
aircraft.Longitude = lon
// Debug: Log final decoded coordinates
fmt.Printf("CPR Result %s: lat=%.6f lon=%.6f\n", aircraft.ICAO24, aircraft.Latitude, aircraft.Longitude)
}
// nlFunction calculates the number of longitude zones (NL) for a given latitude.
@ -486,6 +571,11 @@ func (d *Decoder) decodeVelocity(data []byte, aircraft *Aircraft) {
// Calculate ground speed in knots (rounded to integer)
speedKnots := math.Sqrt(ewVel*ewVel + nsVel*nsVel)
// Validate speed range (0-600 knots for civilian aircraft)
if speedKnots > 600 {
speedKnots = 600 // Cap at reasonable maximum
}
aircraft.GroundSpeed = int(math.Round(speedKnots))
// Calculate track in degrees (0-359)
@ -793,6 +883,8 @@ func (d *Decoder) decodeSurfacePosition(data []byte, aircraft *Aircraft) {
cprLon := uint32(data[8]&0x01)<<16 | uint32(data[9])<<8 | uint32(data[10])
oddFlag := (data[6] >> 2) & 0x01
// Store CPR values for later decoding (protected by mutex)
d.mu.Lock()
if oddFlag == 1 {
d.cprOddLat[aircraft.ICAO24] = float64(cprLat) / 131072.0
d.cprOddLon[aircraft.ICAO24] = float64(cprLon) / 131072.0
@ -800,6 +892,7 @@ func (d *Decoder) decodeSurfacePosition(data []byte, aircraft *Aircraft) {
d.cprEvenLat[aircraft.ICAO24] = float64(cprLat) / 131072.0
d.cprEvenLon[aircraft.ICAO24] = float64(cprLon) / 131072.0
}
d.mu.Unlock()
d.decodeCPRPosition(aircraft)
}