From 66a995b4d0b9189828e3a19480b37afce047e823 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Sun, 31 Aug 2025 11:25:42 +0200 Subject: [PATCH] Fix issue #21 and add aircraft position tracking indicators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix Debian package upgrade issue by separating upgrade vs remove behavior in prerm script - Add aircraft position tracking statistics in merger GetStatistics() method - Update frontend to display position tracking indicators in both header and stats view - Format Go code to maintain consistency 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- assets/static/index.html | 9 ++++ assets/static/js/modules/ui-manager.js | 18 ++++++++ cmd/vrs-test/main.go | 2 +- debian/DEBIAN/prerm | 11 ++++- internal/client/vrs.go | 64 +++++++++++++------------- internal/merger/merger.go | 24 ++++++++-- internal/modes/decoder.go | 8 ++-- internal/vrs/parser.go | 62 ++++++++++++------------- 8 files changed, 124 insertions(+), 74 deletions(-) diff --git a/assets/static/index.html b/assets/static/index.html index 306114e..8493683 100644 --- a/assets/static/index.html +++ b/assets/static/index.html @@ -53,6 +53,7 @@
0 aircraft + 0 positioned 0 sources 1 viewer Connecting... @@ -208,6 +209,14 @@

Max Range

0 km
+
+

Aircraft with Position

+
0
+
+
+

Aircraft without Position

+
0
+
diff --git a/assets/static/js/modules/ui-manager.js b/assets/static/js/modules/ui-manager.js index 9697471..88c692c 100644 --- a/assets/static/js/modules/ui-manager.js +++ b/assets/static/js/modules/ui-manager.js @@ -244,6 +244,8 @@ export class UIManager { const activeViewersEl = document.getElementById('active-viewers'); const maxRangeEl = document.getElementById('max-range'); const messagesSecEl = document.getElementById('messages-sec'); + const aircraftWithPositionEl = document.getElementById('aircraft-with-position'); + const aircraftWithoutPositionEl = document.getElementById('aircraft-without-position'); if (totalAircraftEl) totalAircraftEl.textContent = this.aircraftData.size; if (activeSourcesEl) { @@ -253,6 +255,14 @@ export class UIManager { activeViewersEl.textContent = this.stats.active_clients || 1; } + // Update position tracking statistics from backend + if (aircraftWithPositionEl) { + aircraftWithPositionEl.textContent = this.stats.aircraft_with_position || 0; + } + if (aircraftWithoutPositionEl) { + aircraftWithoutPositionEl.textContent = this.stats.aircraft_without_position || 0; + } + // Calculate max range let maxDistance = 0; for (const aircraft of this.aircraftData.values()) { @@ -270,10 +280,18 @@ export class UIManager { updateHeaderInfo() { const aircraftCountEl = document.getElementById('aircraft-count'); + const positionSummaryEl = document.getElementById('position-summary'); const sourcesCountEl = document.getElementById('sources-count'); const activeClientsEl = document.getElementById('active-clients'); if (aircraftCountEl) aircraftCountEl.textContent = `${this.aircraftData.size} aircraft`; + + // Update position summary in header + if (positionSummaryEl) { + const positioned = this.stats.aircraft_with_position || 0; + positionSummaryEl.textContent = `${positioned} positioned`; + } + if (sourcesCountEl) sourcesCountEl.textContent = `${this.sourcesData.size} sources`; // Update active clients count diff --git a/cmd/vrs-test/main.go b/cmd/vrs-test/main.go index 0e8512b..e7e72bb 100644 --- a/cmd/vrs-test/main.go +++ b/cmd/vrs-test/main.go @@ -130,4 +130,4 @@ func main() { fmt.Println("----------------------------------------") } } -} \ No newline at end of file +} diff --git a/debian/DEBIAN/prerm b/debian/DEBIAN/prerm index 7fe39c4..9f95827 100755 --- a/debian/DEBIAN/prerm +++ b/debian/DEBIAN/prerm @@ -2,8 +2,8 @@ set -e case "$1" in - remove|upgrade|deconfigure) - # Stop and disable the service + remove|deconfigure) + # Stop and disable the service on removal if systemctl is-active --quiet skyview-adsb.service; then systemctl stop skyview-adsb.service fi @@ -12,6 +12,13 @@ case "$1" in systemctl disable skyview-adsb.service fi ;; + upgrade) + # Only stop service during upgrade, preserve enabled state + if systemctl is-active --quiet skyview-adsb.service; then + systemctl stop skyview-adsb.service + fi + # Don't disable - postinst will restart if service was enabled + ;; esac exit 0 \ No newline at end of file diff --git a/internal/client/vrs.go b/internal/client/vrs.go index ab72115..a5396c2 100644 --- a/internal/client/vrs.go +++ b/internal/client/vrs.go @@ -36,7 +36,7 @@ type VRSClient struct { errChan chan error // Error reporting channel stopChan chan struct{} // Shutdown signal channel wg sync.WaitGroup // Wait group for goroutine coordination - + // Reconnection parameters reconnectDelay time.Duration // Initial reconnect delay maxReconnect time.Duration // Maximum reconnect delay (for backoff cap) @@ -116,9 +116,9 @@ func (c *VRSClient) Stop() { // Source status is updated to reflect connection state for monitoring func (c *VRSClient) run(ctx context.Context) { defer c.wg.Done() - + reconnectDelay := c.reconnectDelay - + for { select { case <-ctx.Done(): @@ -127,16 +127,16 @@ func (c *VRSClient) run(ctx context.Context) { return default: } - + // Connect to VRS JSON stream addr := fmt.Sprintf("%s:%d", c.source.Host, c.source.Port) fmt.Printf("Connecting to VRS JSON stream at %s (%s)...\n", addr, c.source.Name) - + conn, err := net.DialTimeout("tcp", addr, 30*time.Second) if err != nil { fmt.Printf("Failed to connect to VRS source %s: %v\n", c.source.Name, err) c.source.Active = false - + // Exponential backoff time.Sleep(reconnectDelay) if reconnectDelay < c.maxReconnect { @@ -144,21 +144,21 @@ func (c *VRSClient) run(ctx context.Context) { } continue } - + c.conn = conn c.source.Active = true reconnectDelay = c.reconnectDelay // Reset backoff - + fmt.Printf("Connected to VRS source %s at %s\n", c.source.Name, addr) - + // Create parser for this connection c.parser = vrs.NewParser(conn, c.source.ID) - + // Start processing messages c.wg.Add(2) go c.readMessages() go c.processMessages() - + // Wait for disconnect select { case <-ctx.Done(): @@ -172,7 +172,7 @@ func (c *VRSClient) run(ctx context.Context) { c.conn.Close() c.source.Active = false } - + // Wait for goroutines to finish time.Sleep(1 * time.Second) } @@ -206,7 +206,7 @@ func (c *VRSClient) readMessages() { // and conflict resolution based on signal strength func (c *VRSClient) processMessages() { defer c.wg.Done() - + for { select { case <-c.stopChan: @@ -215,7 +215,7 @@ func (c *VRSClient) processMessages() { if msg == nil { return } - + // Process each aircraft in the message for _, vrsAircraft := range msg.AcList { // Convert VRS aircraft to internal format @@ -223,7 +223,7 @@ func (c *VRSClient) processMessages() { if aircraft == nil { continue // Skip invalid aircraft } - + // Update merger with new data // Note: VRS doesn't provide signal strength, so we use a default value c.merger.UpdateAircraft( @@ -232,7 +232,7 @@ func (c *VRSClient) processMessages() { -30.0, // Default signal strength for VRS sources vrsAircraft.GetTimestamp(), ) - + // Update source statistics c.source.Messages++ } @@ -258,75 +258,75 @@ func (c *VRSClient) convertVRSToAircraft(vrs *vrs.VRSAircraft) *modes.Aircraft { if err != nil { return nil // Invalid ICAO, skip this aircraft } - + // Create aircraft structure aircraft := &modes.Aircraft{ ICAO24: icao, } - + // Set position if available if vrs.HasPosition() { aircraft.Latitude = vrs.Lat aircraft.Longitude = vrs.Long aircraft.PositionValid = true } - + // Set altitude if available if vrs.HasAltitude() { aircraft.Altitude = vrs.GetAltitude() aircraft.AltitudeValid = true - + // Set barometric altitude specifically if vrs.Alt != 0 { aircraft.BaroAltitude = vrs.Alt } - + // Set geometric altitude if different from barometric if vrs.GAlt != 0 { aircraft.GeomAltitude = vrs.GAlt aircraft.GeomAltitudeValid = true } } - + // Set speed if available if vrs.Spd > 0 { aircraft.GroundSpeed = int(vrs.Spd) aircraft.GroundSpeedValid = true } - + // Set heading/track if available if vrs.Trak > 0 { aircraft.Track = int(vrs.Trak) aircraft.TrackValid = true } - + // Set vertical rate if available if vrs.Vsi != 0 { aircraft.VerticalRate = vrs.Vsi aircraft.VerticalRateValid = true } - + // Set callsign if available if vrs.Call != "" { aircraft.Callsign = vrs.Call aircraft.CallsignValid = true } - + // Set squawk if available if squawk, err := vrs.GetSquawk(); err == nil { aircraft.Squawk = fmt.Sprintf("%04X", squawk) // Convert to hex string aircraft.SquawkValid = true } - + // Set ground status aircraft.OnGround = vrs.IsOnGround() aircraft.OnGroundValid = true - + // Set selected altitude if available if vrs.TAlt != 0 { aircraft.SelectedAltitude = vrs.TAlt } - + // Set position source switch vrs.GetPositionSource() { case "MLAT": @@ -334,7 +334,7 @@ func (c *VRSClient) convertVRSToAircraft(vrs *vrs.VRSAircraft) *modes.Aircraft { case "TIS-B": aircraft.PositionTISB = true } - + // Set additional metadata if available if vrs.Reg != "" { aircraft.Registration = vrs.Reg @@ -345,6 +345,6 @@ func (c *VRSClient) convertVRSToAircraft(vrs *vrs.VRSAircraft) *modes.Aircraft { if vrs.Op != "" { aircraft.Operator = vrs.Op } - + return aircraft -} \ No newline at end of file +} diff --git a/internal/merger/merger.go b/internal/merger/merger.go index 2783af9..7274219 100644 --- a/internal/merger/merger.go +++ b/internal/merger/merger.go @@ -795,10 +795,13 @@ func (m *Merger) GetSources() []*Source { // // The statistics include: // - total_aircraft: Current number of tracked aircraft +// - aircraft_with_position: Number of aircraft with valid position data +// - aircraft_without_position: Number of aircraft without position data // - total_messages: Sum of all messages processed // - active_sources: Number of currently connected sources // - aircraft_by_sources: Distribution of aircraft by number of tracking sources // +// The position statistics help assess data quality and tracking effectiveness. // The aircraft_by_sources map shows data quality - aircraft tracked by // multiple sources generally have better position accuracy and reliability. // @@ -810,11 +813,22 @@ func (m *Merger) GetStatistics() map[string]interface{} { totalMessages := int64(0) activeSources := 0 aircraftBySources := make(map[int]int) // Count by number of sources + aircraftWithPosition := 0 + aircraftWithoutPosition := 0 for _, state := range m.aircraft { totalMessages += state.TotalMessages numSources := len(state.Sources) aircraftBySources[numSources]++ + + // Check if aircraft has valid position data + if state.Aircraft.PositionValid && + state.Aircraft.Latitude != 0.0 && + state.Aircraft.Longitude != 0.0 { + aircraftWithPosition++ + } else { + aircraftWithoutPosition++ + } } for _, src := range m.sources { @@ -824,10 +838,12 @@ func (m *Merger) GetStatistics() map[string]interface{} { } return map[string]interface{}{ - "total_aircraft": len(m.aircraft), - "total_messages": totalMessages, - "active_sources": activeSources, - "aircraft_by_sources": aircraftBySources, + "total_aircraft": len(m.aircraft), + "aircraft_with_position": aircraftWithPosition, + "aircraft_without_position": aircraftWithoutPosition, + "total_messages": totalMessages, + "active_sources": activeSources, + "aircraft_by_sources": aircraftBySources, } } diff --git a/internal/modes/decoder.go b/internal/modes/decoder.go index 5695e0f..80b65f2 100644 --- a/internal/modes/decoder.go +++ b/internal/modes/decoder.go @@ -157,10 +157,10 @@ type Aircraft struct { BaroSetting float64 // Barometric pressure setting (QNH) in millibars // Additional fields from VRS JSON and extended sources - Registration string // Aircraft registration (e.g., "N12345") - AircraftType string // Aircraft type (e.g., "B738") - Operator string // Airline or operator name - + Registration string // Aircraft registration (e.g., "N12345") + AircraftType string // Aircraft type (e.g., "B738") + Operator string // Airline or operator name + // Validity flags for optional fields (used by VRS and other sources) CallsignValid bool // Whether callsign is valid PositionValid bool // Whether position is valid diff --git a/internal/vrs/parser.go b/internal/vrs/parser.go index 4a9d07e..e83fa79 100644 --- a/internal/vrs/parser.go +++ b/internal/vrs/parser.go @@ -34,30 +34,30 @@ type VRSMessage struct { // VRSAircraft represents a single aircraft in VRS JSON format type VRSAircraft struct { - Icao string `json:"Icao"` // ICAO hex address (may have ~ prefix for non-ICAO) - Lat float64 `json:"Lat"` // Latitude - Long float64 `json:"Long"` // Longitude - Alt int `json:"Alt"` // Barometric altitude in feet - GAlt int `json:"GAlt"` // Geometric altitude in feet - Spd float64 `json:"Spd"` // Speed in knots - Trak float64 `json:"Trak"` // Track/heading in degrees - Vsi int `json:"Vsi"` // Vertical speed in feet/min - Sqk string `json:"Sqk"` // Squawk code - Call string `json:"Call"` // Callsign - Gnd bool `json:"Gnd"` // On ground flag - TAlt int `json:"TAlt"` // Target altitude - Mlat bool `json:"Mlat"` // MLAT position flag - Tisb bool `json:"Tisb"` // TIS-B flag - Sat bool `json:"Sat"` // Satellite (JAERO) position flag - + Icao string `json:"Icao"` // ICAO hex address (may have ~ prefix for non-ICAO) + Lat float64 `json:"Lat"` // Latitude + Long float64 `json:"Long"` // Longitude + Alt int `json:"Alt"` // Barometric altitude in feet + GAlt int `json:"GAlt"` // Geometric altitude in feet + Spd float64 `json:"Spd"` // Speed in knots + Trak float64 `json:"Trak"` // Track/heading in degrees + Vsi int `json:"Vsi"` // Vertical speed in feet/min + Sqk string `json:"Sqk"` // Squawk code + Call string `json:"Call"` // Callsign + Gnd bool `json:"Gnd"` // On ground flag + TAlt int `json:"TAlt"` // Target altitude + Mlat bool `json:"Mlat"` // MLAT position flag + Tisb bool `json:"Tisb"` // TIS-B flag + Sat bool `json:"Sat"` // Satellite (JAERO) position flag + // Additional fields that may be present - Reg string `json:"Reg"` // Registration - Type string `json:"Type"` // Aircraft type - Mdl string `json:"Mdl"` // Model - Op string `json:"Op"` // Operator - From string `json:"From"` // Departure airport - To string `json:"To"` // Destination airport - + Reg string `json:"Reg"` // Registration + Type string `json:"Type"` // Aircraft type + Mdl string `json:"Mdl"` // Model + Op string `json:"Op"` // Operator + From string `json:"From"` // Departure airport + To string `json:"To"` // Destination airport + // Timing fields PosTime int64 `json:"PosTime"` // Position timestamp (milliseconds) } @@ -95,21 +95,21 @@ func (p *Parser) ReadMessage() (*VRSMessage, error) { if err != nil { return nil, err } - + // Trim whitespace line = strings.TrimSpace(line) if line == "" { // Empty line, try again return p.ReadMessage() } - + // Parse JSON var msg VRSMessage if err := json.Unmarshal([]byte(line), &msg); err != nil { // Invalid JSON, skip and continue return p.ReadMessage() } - + return &msg, nil } @@ -146,13 +146,13 @@ func (p *Parser) ParseStream(msgChan chan<- *VRSMessage, errChan chan<- error) { func (a *VRSAircraft) GetICAO24() (uint32, error) { // Remove non-ICAO prefix if present icaoStr := strings.TrimPrefix(a.Icao, "~") - + // Parse hex string icao64, err := strconv.ParseUint(icaoStr, 16, 24) if err != nil { return 0, fmt.Errorf("invalid ICAO address: %s", a.Icao) } - + return uint32(icao64), nil } @@ -195,13 +195,13 @@ func (a *VRSAircraft) GetSquawk() (uint16, error) { if a.Sqk == "" { return 0, fmt.Errorf("no squawk code") } - + // Parse hex squawk code squawk64, err := strconv.ParseUint(a.Sqk, 16, 16) if err != nil { return 0, fmt.Errorf("invalid squawk code: %s", a.Sqk) } - + return uint16(squawk64), nil } @@ -231,4 +231,4 @@ func (a *VRSAircraft) GetTimestamp() time.Time { // IsOnGround returns true if the aircraft is on the ground func (a *VRSAircraft) IsOnGround() bool { return a.Gnd -} \ No newline at end of file +}