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