From c8562a4f0d4ae5633bf025df889f5f0e4a82c4d9 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Sat, 23 Aug 2025 23:20:31 +0200 Subject: [PATCH] Enhance aircraft details display to match dump1090 format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add country lookup from ICAO hex codes with flag display - Implement UTC clocks and last update time indicators - Enhance aircraft table with ICAO, squawk, and RSSI columns - Add color-coded RSSI signal strength indicators - Fix calculateDistance returning string instead of number - Accept all SBS-1 message types for complete data capture - Improve data merging to preserve country and registration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- internal/client/dump1090.go | 8 ++ internal/parser/sbs1.go | 132 +++++++++++++++++++++++-- static/css/style.css | 135 ++++++++++++++++++++++++-- static/index.html | 37 +++++-- static/js/app.js | 188 ++++++++++++++++++++++++++++++++---- 5 files changed, 458 insertions(+), 42 deletions(-) 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 @@

SkyView

+
+
+
+
+
+
+
+
UTC
+
+
+
+
+
+
+
Last Update
+
+
+
0 aircraft Connected @@ -60,27 +78,32 @@
- +
+ - - + - + - + + @@ -100,8 +123,8 @@
0
-

Signal Strength

-
0 dB
+

Avg RSSI

+
0 dBFS

Max Range

diff --git a/static/js/app.js b/static/js/app.js index 68116e0..c69a031 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -11,6 +11,7 @@ class SkyView { this.currentView = 'map'; this.charts = {}; this.origin = { latitude: 37.7749, longitude: -122.4194, name: 'Default' }; + this.lastUpdateTime = new Date(); this.init(); } @@ -22,6 +23,7 @@ class SkyView { this.initializeWebSocket(); this.initializeEventListeners(); this.initializeCharts(); + this.initializeClocks(); this.startPeriodicUpdates(); }); @@ -188,8 +190,36 @@ class SkyView { }); } + initializeClocks() { + this.updateClocks(); + setInterval(() => this.updateClocks(), 1000); + } + + updateClocks() { + const now = new Date(); + const utcNow = new Date(now.getTime() + (now.getTimezoneOffset() * 60000)); + + this.updateClock('utc', utcNow); + this.updateClock('update', this.lastUpdateTime); + } + + updateClock(prefix, time) { + const hours = time.getUTCHours(); + const minutes = time.getUTCMinutes(); + + const hourAngle = (hours % 12) * 30 + minutes * 0.5; + const minuteAngle = minutes * 6; + + const hourHand = document.getElementById(`${prefix}-hour`); + const minuteHand = document.getElementById(`${prefix}-minute`); + + if (hourHand) hourHand.style.transform = `rotate(${hourAngle}deg)`; + if (minuteHand) minuteHand.style.transform = `rotate(${minuteAngle}deg)`; + } + updateAircraftData(data) { this.aircraftData = data.aircraft || []; + this.lastUpdateTime = new Date(); this.updateMapMarkers(); this.updateAircraftTable(); this.updateStats(); @@ -341,18 +371,100 @@ class SkyView { } createPopupContent(aircraft) { + const type = this.getAircraftType(aircraft); + const distance = this.calculateDistance(aircraft); + const distanceKm = distance ? (distance * 1.852).toFixed(1) : 'N/A'; + const altitudeM = aircraft.alt_baro ? Math.round(aircraft.alt_baro * 0.3048) : 'N/A'; + const speedKmh = aircraft.gs ? Math.round(aircraft.gs * 1.852) : 'N/A'; + const trackText = aircraft.track ? `${aircraft.track}° (${this.getTrackDirection(aircraft.track)})` : 'N/A'; + return `
-
${aircraft.flight || aircraft.hex}
-
-
Altitude:
${aircraft.alt_baro || 'N/A'} ft
-
Speed:
${aircraft.gs || 'N/A'} kts
-
Track:
${aircraft.track || 'N/A'}°
-
Squawk:
${aircraft.squawk || 'N/A'}
+ + +
`; } + + getCountryFlag(country) { + const flags = { + 'United States': '🇺🇸', + 'United Kingdom': '🇬🇧', + 'Germany': '🇩🇪', + 'France': '🇫🇷', + 'Netherlands': '🇳🇱', + 'Sweden': '🇸🇪', + 'Spain': '🇪🇸', + 'Italy': '🇮🇹', + 'Canada': '🇨🇦', + 'Japan': '🇯🇵', + 'Denmark': '🇩🇰', + 'Austria': '🇦🇹', + 'Belgium': '🇧🇪', + 'Finland': '🇫🇮', + 'Greece': '🇬🇷' + }; + return flags[country] || '🏳️'; + } + + getTrackDirection(track) { + const directions = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', + 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']; + const index = Math.round(track / 22.5) % 16; + return directions[index]; + } updatePopupContent(marker, aircraft) { marker.setPopupContent(this.createPopupContent(aircraft)); @@ -416,7 +528,8 @@ class SkyView { if (searchTerm) { filteredData = filteredData.filter(aircraft => (aircraft.flight && aircraft.flight.toLowerCase().includes(searchTerm)) || - aircraft.hex.toLowerCase().includes(searchTerm) + aircraft.hex.toLowerCase().includes(searchTerm) || + (aircraft.squawk && aircraft.squawk.includes(searchTerm)) ); } @@ -425,19 +538,39 @@ class SkyView { filteredData.forEach(aircraft => { const type = this.getAircraftType(aircraft); - const typeDisplay = type.charAt(0).toUpperCase() + type.slice(1); + const country = aircraft.country || 'Unknown'; + const countryFlag = this.getCountryFlag(country); + const age = aircraft.seen ? aircraft.seen.toFixed(0) : '0'; + const distance = this.calculateDistance(aircraft); + const distanceStr = distance ? distance.toFixed(1) : '-'; + const altitudeStr = aircraft.alt_baro ? + (aircraft.alt_baro >= 0 ? `▲ ${aircraft.alt_baro}` : `▼ ${Math.abs(aircraft.alt_baro)}`) : + '-'; const row = document.createElement('tr'); + // Color code RSSI values + let rssiStr = '-'; + let rssiClass = ''; + if (aircraft.rssi) { + const rssi = aircraft.rssi; + rssiStr = rssi.toFixed(1); + if (rssi > -10) rssiClass = 'rssi-strong'; + else if (rssi > -20) rssiClass = 'rssi-good'; + else if (rssi > -30) rssiClass = 'rssi-weak'; + else rssiClass = 'rssi-poor'; + } + row.innerHTML = ` +
- - - + + - - + + - + + `; 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);
ICAO FlightHexTypeSquawk Altitude SpeedTrack DistanceTrack MsgsSeenAgeRSSI
${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}