diff --git a/internal/client/dump1090.go b/internal/client/dump1090.go index 1c1510f..0e08352 100644 --- a/internal/client/dump1090.go +++ b/internal/client/dump1090.go @@ -136,6 +136,14 @@ func (c *Dump1090Client) updateExistingAircraft(existing, update *parser.Aircraf existing.Squawk = update.Squawk } existing.OnGround = update.OnGround + + // Preserve country and registration + if update.Country != "" && update.Country != "Unknown" { + existing.Country = update.Country + } + if update.Registration != "" { + existing.Registration = update.Registration + } } func (c *Dump1090Client) shouldAddTrackPoint(existing, update *parser.Aircraft) bool { diff --git a/internal/parser/sbs1.go b/internal/parser/sbs1.go index 7bb66ee..8106988 100644 --- a/internal/parser/sbs1.go +++ b/internal/parser/sbs1.go @@ -30,6 +30,9 @@ type Aircraft struct { LastSeen time.Time `json:"last_seen"` Messages int `json:"messages"` TrackHistory []TrackPoint `json:"track_history,omitempty"` + RSSI float64 `json:"rssi,omitempty"` + Country string `json:"country,omitempty"` + Registration string `json:"registration,omitempty"` } type AircraftData struct { @@ -44,10 +47,10 @@ func ParseSBS1Line(line string) (*Aircraft, error) { return nil, nil } - messageType := parts[1] - if messageType != "1" && messageType != "3" && messageType != "4" { - return nil, nil - } + // messageType := parts[1] + // Accept all message types to get complete data + // MSG types: 1=ES_IDENT_AND_CATEGORY, 2=ES_SURFACE_POS, 3=ES_AIRBORNE_POS + // 4=ES_AIRBORNE_VEL, 5=SURVEILLANCE_ALT, 6=SURVEILLANCE_ID, 7=AIR_TO_AIR, 8=ALL_CALL_REPLY aircraft := &Aircraft{ Hex: strings.TrimSpace(parts[4]), @@ -55,6 +58,8 @@ func ParseSBS1Line(line string) (*Aircraft, error) { Messages: 1, } + // Different message types contain different fields + // Always try to extract what's available if parts[10] != "" { aircraft.Flight = strings.TrimSpace(parts[10]) } @@ -72,8 +77,8 @@ func ParseSBS1Line(line string) (*Aircraft, error) { } if parts[13] != "" { - if track, err := strconv.Atoi(parts[13]); err == nil { - aircraft.Track = track + if track, err := strconv.ParseFloat(parts[13], 64); err == nil { + aircraft.Track = int(track) } } @@ -100,6 +105,121 @@ func ParseSBS1Line(line string) (*Aircraft, error) { aircraft.OnGround = parts[21] == "1" } + aircraft.Country = getCountryFromICAO(aircraft.Hex) + aircraft.Registration = getRegistrationFromICAO(aircraft.Hex) + return aircraft, nil } +func getCountryFromICAO(icao string) string { + if len(icao) < 6 { + return "Unknown" + } + + prefix := icao[:1] + + switch prefix { + case "4": + return getCountryFrom4xxxx(icao) + case "A": + return "United States" + case "C": + return "Canada" + case "D": + return "Germany" + case "F": + return "France" + case "G": + return "United Kingdom" + case "I": + return "Italy" + case "J": + return "Japan" + case "P": + return getPCountry(icao) + case "S": + return getSCountry(icao) + case "O": + return getOCountry(icao) + default: + return "Unknown" + } +} + +func getCountryFrom4xxxx(icao string) string { + if len(icao) >= 2 { + switch icao[:2] { + case "40": + return "United Kingdom" + case "44": + return "Austria" + case "45": + return "Denmark" + case "46": + return "Germany" + case "47": + return "Germany" + case "48": + return "Netherlands" + case "49": + return "Netherlands" + } + } + return "Europe" +} + +func getPCountry(icao string) string { + if len(icao) >= 2 { + switch icao[:2] { + case "PH": + return "Netherlands" + case "PJ": + return "Netherlands Antilles" + } + } + return "Unknown" +} + +func getSCountry(icao string) string { + if len(icao) >= 2 { + switch icao[:2] { + case "SE": + return "Sweden" + case "SX": + return "Greece" + } + } + return "Unknown" +} + +func getOCountry(icao string) string { + if len(icao) >= 2 { + switch icao[:2] { + case "OO": + return "Belgium" + case "OH": + return "Finland" + } + } + return "Unknown" +} + +func getRegistrationFromICAO(icao string) string { + // This is a simplified conversion - real registration lookup would need a database + country := getCountryFromICAO(icao) + switch country { + case "Germany": + return "D-" + icao[2:] + case "United Kingdom": + return "G-" + icao[2:] + case "France": + return "F-" + icao[2:] + case "Netherlands": + return "PH-" + icao[2:] + case "Sweden": + return "SE-" + icao[2:] + default: + return icao + } +} + diff --git a/static/css/style.css b/static/css/style.css index c4e9c1a..0f2e125 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -27,6 +27,75 @@ body { border-bottom: 1px solid #404040; } +.clock-section { + flex: 1; + display: flex; + justify-content: center; +} + +.clock-display { + display: flex; + gap: 2rem; +} + +.clock { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} + +.clock-face { + position: relative; + width: 60px; + height: 60px; + border: 2px solid #00a8ff; + border-radius: 50%; + background: #1a1a1a; +} + +.clock-face::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 4px; + height: 4px; + background: #00a8ff; + border-radius: 50%; + transform: translate(-50%, -50%); + z-index: 3; +} + +.clock-hand { + position: absolute; + background: #00a8ff; + transform-origin: bottom center; + border-radius: 2px; +} + +.hour-hand { + width: 3px; + height: 18px; + top: 12px; + left: 50%; + margin-left: -1.5px; +} + +.minute-hand { + width: 2px; + height: 25px; + top: 5px; + left: 50%; + margin-left: -1px; +} + +.clock-label { + font-size: 0.8rem; + color: #888; + text-align: center; +} + .header h1 { font-size: 1.5rem; color: #00a8ff; @@ -227,6 +296,12 @@ body { .type-badge.ga { background: #ffff00; } .type-badge.ground { background: #888888; color: #ffffff; } +/* RSSI signal strength colors */ +.rssi-strong { color: #00ff88; } +.rssi-good { color: #ffff00; } +.rssi-weak { color: #ff8c00; } +.rssi-poor { color: #ff4444; } + .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); @@ -288,21 +363,65 @@ body { } .aircraft-popup { - min-width: 200px; + min-width: 300px; + max-width: 400px; } -.aircraft-popup .flight { - font-weight: bold; +.popup-header { + border-bottom: 1px solid #404040; + padding-bottom: 0.5rem; + margin-bottom: 0.75rem; +} + +.flight-info { font-size: 1.1rem; - margin-bottom: 0.5rem; - color: #00a8ff; + font-weight: bold; } -.aircraft-popup .details { +.icao-flag { + font-size: 1.2rem; + margin-right: 0.5rem; +} + +.flight-id { + color: #00a8ff; + font-family: monospace; +} + +.callsign { + color: #00ff88; +} + +.popup-details { + font-size: 0.9rem; +} + +.detail-row { + margin-bottom: 0.5rem; + padding: 0.25rem 0; +} + +.detail-grid { display: grid; grid-template-columns: 1fr 1fr; - gap: 0.25rem; - font-size: 0.9rem; + gap: 0.5rem; + margin: 0.75rem 0; +} + +.detail-item { + display: flex; + flex-direction: column; +} + +.detail-item .label { + font-size: 0.8rem; + color: #888; + margin-bottom: 0.1rem; +} + +.detail-item .value { + font-weight: bold; + color: #ffffff; } @media (max-width: 768px) { diff --git a/static/index.html b/static/index.html index 1298550..d8b3b0f 100644 --- a/static/index.html +++ b/static/index.html @@ -12,6 +12,24 @@
| ICAO | Flight | -Hex | -Type | +Squawk | Altitude | Speed | -Track | Distance | +Track | Msgs | -Seen | +Age | +RSSI | ${aircraft.hex} | ${aircraft.flight || '-'} | -${aircraft.hex} | -${typeDisplay} | -${aircraft.alt_baro || '-'} | +${aircraft.squawk || '-'} | +${altitudeStr} | ${aircraft.gs || '-'} | -${aircraft.track || '-'} | -${this.calculateDistance(aircraft) || '-'} | +${distanceStr} | +${aircraft.track || '-'}° | ${aircraft.messages || '-'} | -${aircraft.seen ? aircraft.seen.toFixed(1) : '-'}s | +${age} | +${rssiStr} | `; row.addEventListener('click', () => { @@ -482,6 +615,14 @@ class SkyView { return (b.gs || 0) - (a.gs || 0); case 'flight': return (a.flight || a.hex).localeCompare(b.flight || b.hex); + case 'icao': + return a.hex.localeCompare(b.hex); + case 'squawk': + return (a.squawk || '').localeCompare(b.squawk || ''); + case 'age': + return (a.seen || 0) - (b.seen || 0); + case 'rssi': + return (b.rssi || -999) - (a.rssi || -999); default: return 0; } @@ -501,7 +642,7 @@ class SkyView { Math.cos(centerLat * Math.PI / 180) * Math.cos(aircraft.lat * Math.PI / 180) * Math.sin(dLon/2) * Math.sin(dLon/2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); - return (R * c).toFixed(1); + return R * c; // Return a number, not a string } updateStats() { @@ -512,11 +653,12 @@ class SkyView { .filter(a => a.alt_baro) .reduce((sum, a) => sum + a.alt_baro, 0) / this.aircraftData.length || 0; - const maxDistance = Math.max(...this.aircraftData + const distances = this.aircraftData .map(a => this.calculateDistance(a)) - .filter(d => d !== null)) || 0; + .filter(d => d !== null); + const maxDistance = distances.length > 0 ? Math.max(...distances) : 0; - document.getElementById('max-range').textContent = `${maxDistance} nm`; + document.getElementById('max-range').textContent = `${maxDistance.toFixed(1)} nm`; this.updateChartData(); } @@ -589,9 +731,13 @@ class SkyView { document.getElementById('messages-sec').textContent = Math.round(messagesPerSec); } - if (stats.total && stats.total.signal_power) { - document.getElementById('signal-strength').textContent = - `${stats.total.signal_power.toFixed(1)} dB`; + // Calculate average RSSI from aircraft data + const aircraftWithRSSI = this.aircraftData.filter(a => a.rssi); + if (aircraftWithRSSI.length > 0) { + const avgRSSI = aircraftWithRSSI.reduce((sum, a) => sum + a.rssi, 0) / aircraftWithRSSI.length; + document.getElementById('signal-strength').textContent = `${avgRSSI.toFixed(1)} dBFS`; + } else { + document.getElementById('signal-strength').textContent = '0 dBFS'; } } catch (error) { console.error('Failed to fetch stats:', error);
|---|