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
+}