diff --git a/.gitignore b/.gitignore index 902c831..a1bd4d4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,10 +3,6 @@ skyview build/ dist/ -# Debian package build artifacts -debian/usr/bin/skyview -debian/usr/bin/beast-dump - # Configuration config.json diff --git a/Makefile b/Makefile index 2f71042..29cb180 100644 --- a/Makefile +++ b/Makefile @@ -1,26 +1,13 @@ -PACKAGE_NAME=skyview +BINARY_NAME=skyview BUILD_DIR=build VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") -LDFLAGS=-w -s -X main.version=$(VERSION) -.PHONY: build build-all clean run dev test lint deb deb-clean install-deps +.PHONY: build clean run dev test lint deb deb-clean install-deps -# Build main skyview binary build: - @echo "Building skyview..." + @echo "Building $(BINARY_NAME)..." @mkdir -p $(BUILD_DIR) - go build -ldflags="$(LDFLAGS)" -o $(BUILD_DIR)/skyview ./cmd/skyview - -# Build beast-dump utility binary -build-beast-dump: - @echo "Building beast-dump..." - @mkdir -p $(BUILD_DIR) - go build -ldflags="$(LDFLAGS)" -o $(BUILD_DIR)/beast-dump ./cmd/beast-dump - -# Build all binaries -build-all: build build-beast-dump - @echo "Built all binaries successfully:" - @ls -la $(BUILD_DIR)/ + go build -ldflags="-w -s -X main.version=$(VERSION)" -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/skyview clean: @echo "Cleaning..." diff --git a/README.md b/README.md index 2a3602b..645e56c 100644 --- a/README.md +++ b/README.md @@ -17,14 +17,13 @@ A high-performance, multi-source ADS-B aircraft tracking application that connec - **Multi-view Dashboard**: Map, Table, Statistics, Coverage, and 3D Radar views ### Professional Visualization -- **Signal Analysis**: Signal strength visualization and coverage analysis +- **Signal Analysis**: Signal strength heatmaps and coverage analysis - **Range Circles**: Configurable range rings for each receiver - **Flight Trails**: Historical aircraft movement tracking -- **3D Radar View**: Three.js-powered 3D visualization -- **Statistics Dashboard**: Aircraft count timeline *(additional charts under construction)* 🚧 +- **3D Radar View**: Three.js-powered 3D visualization (optional) +- **Statistics Dashboard**: Live charts and metrics - **Smart Origin**: Auto-calculated map center based on receiver locations - **Map Controls**: Center on aircraft, reset to origin, toggle overlays -- **Signal Heatmaps**: Coverage heatmap visualization *(under construction)* 🚧 ### Aircraft Data - **Complete Mode S Decoding**: Position, velocity, altitude, heading @@ -52,7 +51,7 @@ A high-performance, multi-source ADS-B aircraft tracking application that connec ```bash # Install -sudo dpkg -i skyview_0.0.2_amd64.deb +sudo dpkg -i skyview_2.0.0_amd64.deb # Configure sudo nano /etc/skyview/config.json @@ -120,18 +119,9 @@ Access the web interface at `http://localhost:8080` ### Views Available: - **Map View**: Interactive aircraft tracking with receiver locations - **Table View**: Sortable aircraft data with multi-source information -- **Statistics**: Aircraft count timeline *(additional charts planned)* 🚧 -- **Coverage**: Signal strength analysis *(heatmaps under construction)* 🚧 -- **3D Radar**: Three-dimensional aircraft visualization *(controls under construction)* 🚧 - -### 🚧 Features Under Construction -Some advanced features are currently in development: -- **Message Rate Charts**: Per-source message rate visualization -- **Signal Strength Distribution**: Signal strength histogram analysis -- **Altitude Distribution**: Aircraft altitude distribution charts -- **Interactive Heatmaps**: Leaflet.heat-based coverage heatmaps -- **3D Radar Controls**: Interactive 3D view manipulation (reset, auto-rotate, range) -- **Enhanced Error Notifications**: User-friendly toast notifications for issues +- **Statistics**: Live metrics and historical charts +- **Coverage**: Signal strength analysis and heatmaps +- **3D Radar**: Three-dimensional aircraft visualization ## šŸ”§ Building @@ -203,7 +193,7 @@ make check # Run all checks ### Systemd Service (Debian/Ubuntu) ```bash # Install package -sudo dpkg -i skyview_0.0.2_amd64.deb +sudo dpkg -i skyview_2.0.0_amd64.deb # Configure sources in /etc/skyview/config.json # Start service @@ -259,9 +249,9 @@ MIT License - see [LICENSE](LICENSE) file for details. ## šŸ†˜ Support -- [Issues](https://kode.naiv.no/olemd/skyview/issues) -- [Documentation](https://kode.naiv.no/olemd/skyview/wiki) -- [Configuration Examples](https://kode.naiv.no/olemd/skyview/src/branch/main/examples) +- [GitHub Issues](https://github.com/skyview/skyview/issues) +- [Documentation](https://github.com/skyview/skyview/wiki) +- [Configuration Examples](https://github.com/skyview/skyview/tree/main/examples) --- diff --git a/assets/assets.go b/assets/assets.go index 54e1c4a..3cef7d0 100644 --- a/assets/assets.go +++ b/assets/assets.go @@ -6,7 +6,7 @@ // - index.html: Main web interface with aircraft tracking map // - css/style.css: Styling for the web interface // - js/app.js: JavaScript client for WebSocket communication and map rendering -// - icons/*.svg: Type-specific SVG icons for aircraft markers +// - aircraft-icon.svg: SVG icon for aircraft markers // - favicon.ico: Browser icon // // The embedded filesystem is used by the HTTP server to serve static content @@ -16,11 +16,11 @@ package assets import "embed" // Static contains all embedded static web assets from the static/ directory. -// +// // Files are embedded at build time and can be accessed using the standard // fs.FS interface. Path names within the embedded filesystem preserve the // directory structure, so files are accessed as: -// - "static/index.html" +// - "static/index.html" // - "static/css/style.css" // - "static/js/app.js" // - etc. diff --git a/assets/static/aircraft-icon.svg b/assets/static/aircraft-icon.svg new file mode 100644 index 0000000..f2489d3 --- /dev/null +++ b/assets/static/aircraft-icon.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/assets/static/css/style.css b/assets/static/css/style.css index b34a241..26ce441 100644 --- a/assets/static/css/style.css +++ b/assets/static/css/style.css @@ -195,8 +195,8 @@ body { .display-options { position: absolute; - top: 320px; - right: 10px; + top: 10px; + left: 10px; z-index: 1000; background: rgba(45, 45, 45, 0.95); border: 1px solid #404040; @@ -417,115 +417,6 @@ body { color: #ffffff !important; } -/* Under Construction Styles */ -.under-construction { - color: #ff8c00; - font-size: 0.8em; - font-weight: normal; - margin-left: 8px; -} - -.construction-notice { - background: rgba(255, 140, 0, 0.1); - border: 1px solid #ff8c00; - border-radius: 4px; - padding: 8px; - margin: 8px 0; - font-size: 0.9em; - color: #ff8c00; - text-align: center; -} - -/* Toast Notifications */ -.toast-notification { - position: fixed; - top: 20px; - right: 20px; - background: rgba(40, 40, 40, 0.95); - border: 1px solid #555; - border-radius: 6px; - padding: 12px 20px; - color: #ffffff; - font-size: 0.9em; - max-width: 300px; - z-index: 10000; - transform: translateX(320px); - transition: transform 0.3s ease-in-out; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); -} - -.toast-notification.error { - border-color: #ff8c00; - background: rgba(255, 140, 0, 0.1); - color: #ff8c00; -} - -.toast-notification.show { - transform: translateX(0); -} - -/* Version Info */ -.version-info { - font-size: 0.6em; - color: #888; - font-weight: normal; - margin-left: 8px; -} - -/* Repository Link */ -.repo-link { - color: #888; - text-decoration: none; - font-size: 0.7em; - margin-left: 6px; - opacity: 0.6; - transition: opacity 0.2s ease, color 0.2s ease; -} - -.repo-link:hover { - color: #4a9eff; - opacity: 1; - text-decoration: none; -} - -/* Collapsible Sections */ -.collapsible-header { - cursor: pointer; - user-select: none; - display: flex; - justify-content: space-between; - align-items: center; - margin: 0 0 8px 0; - padding: 4px 0; - border-bottom: 1px solid #444; -} - -.collapsible-header:hover { - color: #4a9eff; -} - -.collapse-indicator { - font-size: 0.8em; - transition: transform 0.2s ease; - color: #888; -} - -.collapsible-header.collapsed .collapse-indicator { - transform: rotate(-90deg); -} - -.collapsible-content { - overflow: hidden; - transition: max-height 0.3s ease; - max-height: 200px; -} - -.collapsible-content.collapsed { - max-height: 0; - margin: 0; - padding: 0; -} - .leaflet-popup-tip { background: #2d2d2d !important; } diff --git a/assets/static/index.html b/assets/static/index.html index 0a00509..9849126 100644 --- a/assets/static/index.html +++ b/assets/static/index.html @@ -28,7 +28,7 @@
-

SkyView v0.0.2 āš™

+

SkyView

@@ -81,13 +81,10 @@
- +
- -
@@ -239,11 +233,10 @@
-
🚧 3D Controls Under Construction
- - + +
diff --git a/assets/static/js/app.js b/assets/static/js/app.js index f396a5f..8570ff7 100644 --- a/assets/static/js/app.js +++ b/assets/static/js/app.js @@ -107,9 +107,6 @@ class SkyView { }); } - // Setup collapsible sections - this.setupCollapsibleSections(); - const toggleDarkModeBtn = document.getElementById('toggle-dark-mode'); if (toggleDarkModeBtn) { toggleDarkModeBtn.addEventListener('click', () => { @@ -461,43 +458,6 @@ class SkyView { // Clean up old trail data, etc. }, 30000); } - - setupCollapsibleSections() { - // Setup Display Options collapsible - const displayHeader = document.getElementById('display-options-header'); - const displayContent = document.getElementById('display-options-content'); - - if (displayHeader && displayContent) { - displayHeader.addEventListener('click', () => { - const isCollapsed = displayContent.classList.contains('collapsed'); - - if (isCollapsed) { - // Expand - displayContent.classList.remove('collapsed'); - displayHeader.classList.remove('collapsed'); - } else { - // Collapse - displayContent.classList.add('collapsed'); - displayHeader.classList.add('collapsed'); - } - - // Save state to localStorage - localStorage.setItem('displayOptionsCollapsed', !isCollapsed); - }); - - // Restore saved state (default to collapsed) - const savedState = localStorage.getItem('displayOptionsCollapsed'); - const shouldCollapse = savedState === null ? true : savedState === 'true'; - - if (shouldCollapse) { - displayContent.classList.add('collapsed'); - displayHeader.classList.add('collapsed'); - } else { - displayContent.classList.remove('collapsed'); - displayHeader.classList.remove('collapsed'); - } - } - } } // Initialize application when DOM is ready diff --git a/assets/static/js/modules/aircraft-manager.js b/assets/static/js/modules/aircraft-manager.js index 1485d38..37e9e0f 100644 --- a/assets/static/js/modules/aircraft-manager.js +++ b/assets/static/js/modules/aircraft-manager.js @@ -362,14 +362,6 @@ export class AircraftManager {
Type: ${type}
- ${aircraft.TransponderCapability ? ` -
- Transponder: ${aircraft.TransponderCapability} -
` : ''} - ${aircraft.SignalQuality ? ` -
- Signal Quality: ${aircraft.SignalQuality} -
` : ''}
diff --git a/assets/static/js/modules/map-manager.js b/assets/static/js/modules/map-manager.js index bd2ac75..94dd323 100644 --- a/assets/static/js/modules/map-manager.js +++ b/assets/static/js/modules/map-manager.js @@ -352,14 +352,8 @@ export class MapManager { } createHeatmapOverlay(data) { - // 🚧 Under Construction: Heatmap visualization not yet implemented - // Planned: Use Leaflet.heat library for proper heatmap rendering - console.log('Heatmap overlay requested but not yet implemented'); - - // Show user-visible notice - if (window.uiManager) { - window.uiManager.showError('Heatmap visualization is under construction 🚧'); - } + // Simplified heatmap implementation + // In production, would use proper heatmap library like Leaflet.heat } setSelectedSource(sourceId) { diff --git a/assets/static/js/modules/ui-manager.js b/assets/static/js/modules/ui-manager.js index c4e7569..3af6789 100644 --- a/assets/static/js/modules/ui-manager.js +++ b/assets/static/js/modules/ui-manager.js @@ -316,22 +316,6 @@ export class UIManager { showError(message) { console.error(message); - - // Simple toast notification implementation - const toast = document.createElement('div'); - toast.className = 'toast-notification error'; - toast.textContent = message; - - // Add to page - document.body.appendChild(toast); - - // Show toast with animation - setTimeout(() => toast.classList.add('show'), 100); - - // Auto-remove after 5 seconds - setTimeout(() => { - toast.classList.remove('show'); - setTimeout(() => document.body.removeChild(toast), 300); - }, 5000); + // Could implement toast notifications here } } \ No newline at end of file diff --git a/beast-dump-with-heli.bin b/beast-dump-with-heli.bin new file mode 100644 index 0000000..fa579ea Binary files /dev/null and b/beast-dump-with-heli.bin differ diff --git a/cmd/beast-dump/main.go b/cmd/beast-dump/main.go index 8bd7a8b..4d585de 100644 --- a/cmd/beast-dump/main.go +++ b/cmd/beast-dump/main.go @@ -5,16 +5,14 @@ // in human-readable format on the console. // // Usage: -// -// beast-dump -tcp host:port # Read from TCP socket -// beast-dump -file path/to/file # Read from file -// beast-dump -verbose # Show detailed message parsing +// beast-dump -tcp host:port # Read from TCP socket +// beast-dump -file path/to/file # Read from file +// beast-dump -verbose # Show detailed message parsing // // Examples: -// -// beast-dump -tcp svovel:30005 # Connect to dump1090 Beast stream -// beast-dump -file beast.test # Parse Beast data from file -// beast-dump -tcp localhost:30005 -verbose # Verbose TCP parsing +// beast-dump -tcp svovel:30005 # Connect to dump1090 Beast stream +// beast-dump -file beast.test # Parse Beast data from file +// beast-dump -tcp localhost:30005 -verbose # Verbose TCP parsing package main import ( @@ -44,23 +42,23 @@ type BeastDumper struct { parser *beast.Parser decoder *modes.Decoder stats struct { - totalMessages int64 - validMessages int64 - aircraftSeen map[uint32]bool - startTime time.Time - lastMessageTime time.Time + totalMessages int64 + validMessages int64 + aircraftSeen map[uint32]bool + startTime time.Time + lastMessageTime time.Time } } func main() { config := parseFlags() - + if config.TCPAddress == "" && config.FilePath == "" { fmt.Fprintf(os.Stderr, "Error: Must specify either -tcp or -file\n") flag.Usage() os.Exit(1) } - + if config.TCPAddress != "" && config.FilePath != "" { fmt.Fprintf(os.Stderr, "Error: Cannot specify both -tcp and -file\n") flag.Usage() @@ -68,7 +66,7 @@ func main() { } dumper := NewBeastDumper(config) - + if err := dumper.Run(); err != nil { log.Fatalf("Error: %v", err) } @@ -77,12 +75,12 @@ func main() { // parseFlags parses command-line flags and returns configuration func parseFlags() *Config { config := &Config{} - + flag.StringVar(&config.TCPAddress, "tcp", "", "TCP address for Beast stream (e.g., localhost:30005)") flag.StringVar(&config.FilePath, "file", "", "File path for Beast data") flag.BoolVar(&config.Verbose, "verbose", false, "Enable verbose output") flag.IntVar(&config.Count, "count", 0, "Maximum messages to process (0 = unlimited)") - + flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: %s [options]\n", os.Args[0]) fmt.Fprintf(os.Stderr, "\nBeast format ADS-B data parser and console dumper\n\n") @@ -93,7 +91,7 @@ func parseFlags() *Config { fmt.Fprintf(os.Stderr, " %s -file beast.test\n", os.Args[0]) fmt.Fprintf(os.Stderr, " %s -tcp localhost:30005 -verbose -count 100\n", os.Args[0]) } - + flag.Parse() return config } @@ -104,11 +102,11 @@ func NewBeastDumper(config *Config) *BeastDumper { config: config, decoder: modes.NewDecoder(0.0, 0.0), // beast-dump doesn't have reference position, use default stats: struct { - totalMessages int64 - validMessages int64 - aircraftSeen map[uint32]bool - startTime time.Time - lastMessageTime time.Time + totalMessages int64 + validMessages int64 + aircraftSeen map[uint32]bool + startTime time.Time + lastMessageTime time.Time }{ aircraftSeen: make(map[uint32]bool), startTime: time.Now(), @@ -120,10 +118,10 @@ func NewBeastDumper(config *Config) *BeastDumper { func (d *BeastDumper) Run() error { fmt.Printf("Beast Data Dumper\n") fmt.Printf("=================\n\n") - + var reader io.Reader var closer io.Closer - + if d.config.TCPAddress != "" { conn, err := d.connectTCP() if err != nil { @@ -141,34 +139,34 @@ func (d *BeastDumper) Run() error { closer = file fmt.Printf("Reading file: %s\n", d.config.FilePath) } - + defer closer.Close() - + // Create Beast parser d.parser = beast.NewParser(reader, "beast-dump") - + fmt.Printf("Verbose mode: %t\n", d.config.Verbose) if d.config.Count > 0 { fmt.Printf("Message limit: %d\n", d.config.Count) } fmt.Printf("\nStarting Beast data parsing...\n") - fmt.Printf("%-8s %-6s %-12s %-8s %-10s %-6s %s\n", + fmt.Printf("%-8s %-6s %-12s %-8s %-10s %-6s %s\n", "Time", "ICAO", "Type", "Signal", "Data", "Len", "Decoded") - fmt.Printf("%s\n", + fmt.Printf("%s\n", "------------------------------------------------------------------------") - + return d.parseMessages() } // connectTCP establishes TCP connection to Beast stream func (d *BeastDumper) connectTCP() (net.Conn, error) { fmt.Printf("Connecting to %s...\n", d.config.TCPAddress) - + conn, err := net.DialTimeout("tcp", d.config.TCPAddress, 10*time.Second) if err != nil { return nil, err } - + return conn, nil } @@ -178,14 +176,14 @@ func (d *BeastDumper) openFile() (*os.File, error) { if err != nil { return nil, err } - + // Check file size stat, err := file.Stat() if err != nil { file.Close() return nil, err } - + fmt.Printf("File size: %d bytes\n", stat.Size()) return file, nil } @@ -198,7 +196,7 @@ func (d *BeastDumper) parseMessages() error { fmt.Printf("\nReached message limit of %d\n", d.config.Count) break } - + // Parse Beast message msg, err := d.parser.ReadMessage() if err != nil { @@ -211,21 +209,21 @@ func (d *BeastDumper) parseMessages() error { } continue } - + d.stats.totalMessages++ d.stats.lastMessageTime = time.Now() - + // Display Beast message info d.displayMessage(msg) - + // Decode Mode S data if available if msg.Type == beast.BeastModeS || msg.Type == beast.BeastModeSLong { d.decodeAndDisplay(msg) } - + d.stats.validMessages++ } - + d.displayStatistics() return nil } @@ -233,7 +231,7 @@ func (d *BeastDumper) parseMessages() error { // displayMessage shows basic Beast message information func (d *BeastDumper) displayMessage(msg *beast.Message) { timestamp := msg.ReceivedAt.Format("15:04:05") - + // Extract ICAO if available icao := "------" if msg.Type == beast.BeastModeS || msg.Type == beast.BeastModeSLong { @@ -242,18 +240,18 @@ func (d *BeastDumper) displayMessage(msg *beast.Message) { d.stats.aircraftSeen[icaoAddr] = true } } - + // Beast message type typeStr := d.formatMessageType(msg.Type) - + // Signal strength signal := msg.GetSignalStrength() signalStr := fmt.Sprintf("%6.1f", signal) - + // Data preview dataStr := d.formatDataPreview(msg.Data) - - fmt.Printf("%-8s %-6s %-12s %-8s %-10s %-6d ", + + fmt.Printf("%-8s %-6s %-12s %-8s %-10s %-6d ", timestamp, icao, typeStr, signalStr, dataStr, len(msg.Data)) } @@ -268,11 +266,11 @@ func (d *BeastDumper) decodeAndDisplay(msg *beast.Message) { } return } - + // Display decoded information info := d.formatAircraftInfo(aircraft) fmt.Printf("%s\n", info) - + // Verbose details if d.config.Verbose { d.displayVerboseInfo(aircraft, msg) @@ -300,7 +298,7 @@ func (d *BeastDumper) formatDataPreview(data []byte) string { if len(data) == 0 { return "" } - + preview := "" for i, b := range data { if i >= 4 { // Show first 4 bytes @@ -308,33 +306,33 @@ func (d *BeastDumper) formatDataPreview(data []byte) string { } preview += fmt.Sprintf("%02X", b) } - + if len(data) > 4 { preview += "..." } - + return preview } // formatAircraftInfo creates a summary of decoded aircraft information func (d *BeastDumper) formatAircraftInfo(aircraft *modes.Aircraft) string { parts := []string{} - + // Callsign if aircraft.Callsign != "" { parts = append(parts, fmt.Sprintf("CS:%s", aircraft.Callsign)) } - + // Position if aircraft.Latitude != 0 || aircraft.Longitude != 0 { parts = append(parts, fmt.Sprintf("POS:%.4f,%.4f", aircraft.Latitude, aircraft.Longitude)) } - + // Altitude if aircraft.Altitude != 0 { parts = append(parts, fmt.Sprintf("ALT:%dft", aircraft.Altitude)) } - + // Speed and track if aircraft.GroundSpeed != 0 { parts = append(parts, fmt.Sprintf("SPD:%dkt", aircraft.GroundSpeed)) @@ -342,26 +340,26 @@ func (d *BeastDumper) formatAircraftInfo(aircraft *modes.Aircraft) string { if aircraft.Track != 0 { parts = append(parts, fmt.Sprintf("HDG:%d°", aircraft.Track)) } - + // Vertical rate if aircraft.VerticalRate != 0 { parts = append(parts, fmt.Sprintf("VS:%d", aircraft.VerticalRate)) } - + // Squawk if aircraft.Squawk != "" { parts = append(parts, fmt.Sprintf("SQ:%s", aircraft.Squawk)) } - + // Emergency if aircraft.Emergency != "" && aircraft.Emergency != "None" { parts = append(parts, fmt.Sprintf("EMG:%s", aircraft.Emergency)) } - + if len(parts) == 0 { return "(no data decoded)" } - + info := "" for i, part := range parts { if i > 0 { @@ -369,7 +367,7 @@ func (d *BeastDumper) formatAircraftInfo(aircraft *modes.Aircraft) string { } info += part } - + return info } @@ -379,7 +377,7 @@ func (d *BeastDumper) displayVerboseInfo(aircraft *modes.Aircraft, msg *beast.Me fmt.Printf(" Raw Data: %s\n", d.formatHexData(msg.Data)) fmt.Printf(" Timestamp: %s\n", msg.ReceivedAt.Format("15:04:05.000")) fmt.Printf(" Signal: %.2f dBFS\n", msg.GetSignalStrength()) - + fmt.Printf(" Aircraft Data:\n") if aircraft.Callsign != "" { fmt.Printf(" Callsign: %s\n", aircraft.Callsign) @@ -420,23 +418,23 @@ func (d *BeastDumper) formatHexData(data []byte) string { // displayStatistics shows final parsing statistics func (d *BeastDumper) displayStatistics() { duration := time.Since(d.stats.startTime) - + fmt.Printf("\nStatistics:\n") fmt.Printf("===========\n") fmt.Printf("Total messages: %d\n", d.stats.totalMessages) fmt.Printf("Valid messages: %d\n", d.stats.validMessages) fmt.Printf("Unique aircraft: %d\n", len(d.stats.aircraftSeen)) fmt.Printf("Duration: %v\n", duration.Round(time.Second)) - + if d.stats.totalMessages > 0 && duration > 0 { rate := float64(d.stats.totalMessages) / duration.Seconds() fmt.Printf("Message rate: %.1f msg/sec\n", rate) } - + if len(d.stats.aircraftSeen) > 0 { fmt.Printf("\nAircraft seen:\n") for icao := range d.stats.aircraftSeen { fmt.Printf(" %06X\n", icao) } } -} +} \ No newline at end of file diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..15dbc78 --- /dev/null +++ b/config.json.example @@ -0,0 +1,15 @@ +{ + "server": { + "address": ":8080", + "port": 8080 + }, + "dump1090": { + "host": "192.168.1.100", + "data_port": 30003 + }, + "origin": { + "latitude": 37.7749, + "longitude": -122.4194, + "name": "San Francisco" + } +} \ No newline at end of file diff --git a/debian/DEBIAN/control b/debian/DEBIAN/control index 0d87129..a208957 100644 --- a/debian/DEBIAN/control +++ b/debian/DEBIAN/control @@ -1,10 +1,10 @@ Package: skyview -Version: 0.0.2 +Version: 2.0.0 Section: net Priority: optional Architecture: amd64 Depends: systemd -Maintainer: Ole-Morten Duesund +Maintainer: SkyView Team Description: Multi-source ADS-B aircraft tracker with Beast format support SkyView is a standalone application that connects to multiple dump1090 Beast format TCP streams and provides a modern web frontend for aircraft tracking. @@ -20,4 +20,4 @@ Description: Multi-source ADS-B aircraft tracker with Beast format support - Mobile-responsive design - Systemd integration for service management - Beast-dump utility for raw ADS-B data analysis -Homepage: https://kode.naiv.no/olemd/skyview +Homepage: https://github.com/skyview/skyview diff --git a/debian/DEBIAN/postinst b/debian/DEBIAN/postinst index c99d832..7ddbb80 100755 --- a/debian/DEBIAN/postinst +++ b/debian/DEBIAN/postinst @@ -5,34 +5,35 @@ case "$1" in configure) # Create skyview user and group if they don't exist if ! getent group skyview >/dev/null 2>&1; then - addgroup --system --quiet skyview + addgroup --system skyview fi if ! getent passwd skyview >/dev/null 2>&1; then adduser --system --ingroup skyview --home /var/lib/skyview \ - --no-create-home --disabled-password --quiet skyview + --no-create-home --disabled-password skyview fi # Create directories with proper permissions - mkdir -p /var/lib/skyview /var/log/skyview >/dev/null 2>&1 || true - chown skyview:skyview /var/lib/skyview /var/log/skyview >/dev/null 2>&1 || true - chmod 755 /var/lib/skyview /var/log/skyview >/dev/null 2>&1 || true + mkdir -p /var/lib/skyview + mkdir -p /var/log/skyview + chown skyview:skyview /var/lib/skyview + chown skyview:skyview /var/log/skyview + chmod 755 /var/lib/skyview + chmod 755 /var/log/skyview - # Set permissions on config files + # Set permissions on config file if [ -f /etc/skyview/config.json ]; then - chown root:skyview /etc/skyview/config.json >/dev/null 2>&1 || true - chmod 640 /etc/skyview/config.json >/dev/null 2>&1 || true + chown root:skyview /etc/skyview/config.json + chmod 640 /etc/skyview/config.json fi + # Enable and start the service + systemctl daemon-reload + systemctl enable skyview.service - # Handle systemd service - systemctl daemon-reload >/dev/null 2>&1 || true - - # Check if service was previously enabled - if systemctl is-enabled skyview >/dev/null 2>&1; then - # Service was enabled, restart it - systemctl restart skyview >/dev/null 2>&1 || true - fi + echo "SkyView has been installed and configured." + echo "Edit /etc/skyview/config.json to configure your dump1090 sources." + echo "Then run: systemctl start skyview" ;; esac diff --git a/debian/usr/bin/beast-dump b/debian/usr/bin/beast-dump new file mode 100755 index 0000000..99c154e Binary files /dev/null and b/debian/usr/bin/beast-dump differ diff --git a/debian/usr/share/man/man1/beast-dump.1 b/debian/usr/share/man/man1/beast-dump.1 index 2465981..bc94ad6 100644 --- a/debian/usr/share/man/man1/beast-dump.1 +++ b/debian/usr/share/man/man1/beast-dump.1 @@ -1,4 +1,4 @@ -.TH BEAST-DUMP 1 "2025-08-24" "SkyView 0.0.2" "User Commands" +.TH BEAST-DUMP 1 "2024-08-24" "SkyView 2.0.0" "User Commands" .SH NAME beast-dump \- Utility for analyzing raw ADS-B data in Beast binary format .SH SYNOPSIS @@ -90,6 +90,6 @@ Beast format files typically use .bin or .beast extensions. .BR skyview (1), .BR dump1090 (1) .SH BUGS -Report bugs at: https://kode.naiv.no/olemd/skyview/issues +Report bugs at: https://github.com/skyview/skyview/issues .SH AUTHOR -Ole-Morten Duesund \ No newline at end of file +SkyView Team \ No newline at end of file diff --git a/debian/usr/share/man/man1/skyview.1 b/debian/usr/share/man/man1/skyview.1 index 2c408c6..34241fc 100644 --- a/debian/usr/share/man/man1/skyview.1 +++ b/debian/usr/share/man/man1/skyview.1 @@ -1,4 +1,4 @@ -.TH SKYVIEW 1 "2025-08-24" "SkyView 0.0.2" "User Commands" +.TH SKYVIEW 1 "2024-08-24" "SkyView 2.0.0" "User Commands" .SH NAME skyview \- Multi-source ADS-B aircraft tracker with Beast format support .SH SYNOPSIS @@ -83,6 +83,6 @@ Coverage heatmaps and range circles .BR beast-dump (1), .BR dump1090 (1) .SH BUGS -Report bugs at: https://kode.naiv.no/olemd/skyview/issues +Report bugs at: https://github.com/skyview/skyview/issues .SH AUTHOR -Ole-Morten Duesund \ No newline at end of file +SkyView Team \ No newline at end of file diff --git a/CLAUDE.md b/docs/CLAUDE.md similarity index 100% rename from CLAUDE.md rename to docs/CLAUDE.md diff --git a/internal/beast/parser.go b/internal/beast/parser.go index ec5afed..436a728 100644 --- a/internal/beast/parser.go +++ b/internal/beast/parser.go @@ -88,12 +88,12 @@ func NewParser(r io.Reader, sourceID string) *Parser { // ReadMessage reads and parses a single Beast message from the stream. // // The parsing process: -// 1. Search for the escape character (0x1A) that marks message start -// 2. Read and validate the message type byte -// 3. Read the 48-bit timestamp (big-endian, padded to 64-bit) -// 4. Read the signal level byte -// 5. Read the message payload (length depends on message type) -// 6. Process escape sequences in the payload data +// 1. Search for the escape character (0x1A) that marks message start +// 2. Read and validate the message type byte +// 3. Read the 48-bit timestamp (big-endian, padded to 64-bit) +// 4. Read the signal level byte +// 5. Read the message payload (length depends on message type) +// 6. Process escape sequences in the payload data // // The parser can recover from protocol errors by continuing to search for // the next valid message boundary. Status messages are currently skipped @@ -253,7 +253,7 @@ func (msg *Message) GetSignalStrength() float64 { // The ICAO address is a unique 24-bit identifier assigned to each aircraft. // In Mode S messages, it's located in bytes 1-3 of the message payload: // - Byte 1: Most significant 8 bits -// - Byte 2: Middle 8 bits +// - Byte 2: Middle 8 bits // - Byte 3: Least significant 8 bits // // Mode A/C messages don't contain ICAO addresses and will return an error. diff --git a/internal/client/beast.go b/internal/client/beast.go index 6810874..cb30a74 100644 --- a/internal/client/beast.go +++ b/internal/client/beast.go @@ -39,15 +39,15 @@ import ( // continuously processes incoming messages until stopped or the source // becomes unavailable. type BeastClient struct { - source *merger.Source // Source configuration and status - merger *merger.Merger // Data merger for multi-source fusion - decoder *modes.Decoder // Mode S/ADS-B message decoder - conn net.Conn // TCP connection to Beast source - parser *beast.Parser // Beast format message parser + source *merger.Source // Source configuration and status + merger *merger.Merger // Data merger for multi-source fusion + decoder *modes.Decoder // Mode S/ADS-B message decoder + conn net.Conn // TCP connection to Beast source + parser *beast.Parser // Beast format message parser msgChan chan *beast.Message // Buffered channel for parsed messages - errChan chan error // Error reporting channel - stopChan chan struct{} // Shutdown signal channel - wg sync.WaitGroup // Wait group for goroutine coordination + 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 @@ -102,9 +102,9 @@ func (c *BeastClient) Start(ctx context.Context) { // Stop gracefully shuts down the client and all associated goroutines. // // The shutdown process: -// 1. Signals all goroutines to stop via stopChan -// 2. Closes the TCP connection if active -// 3. Waits for all goroutines to complete +// 1. Signals all goroutines to stop via stopChan +// 2. Closes the TCP connection if active +// 3. Waits for all goroutines to complete // // This method blocks until the shutdown is complete. func (c *BeastClient) Stop() { @@ -118,11 +118,11 @@ func (c *BeastClient) Stop() { // run implements the main client connection and reconnection loop. // // This method handles the complete client lifecycle: -// 1. Connection establishment with timeout -// 2. Exponential backoff on connection failures -// 3. Message parsing and processing goroutine management -// 4. Connection monitoring and failure detection -// 5. Automatic reconnection on disconnection +// 1. Connection establishment with timeout +// 2. Exponential backoff on connection failures +// 3. Message parsing and processing goroutine management +// 4. Connection monitoring and failure detection +// 5. Automatic reconnection on disconnection // // The exponential backoff starts at reconnectDelay (5s) and doubles on each // failure up to maxReconnect (60s), then resets on successful connection. @@ -210,10 +210,10 @@ func (c *BeastClient) readMessages() { // processMessages runs in a dedicated goroutine to decode and merge aircraft data. // // For each received Beast message, this method: -// 1. Decodes the Mode S/ADS-B message payload -// 2. Extracts aircraft information (position, altitude, speed, etc.) -// 3. Updates the data merger with new aircraft state -// 4. Updates source statistics (message count) +// 1. Decodes the Mode S/ADS-B message payload +// 2. Extracts aircraft information (position, altitude, speed, etc.) +// 3. Updates the data merger with new aircraft state +// 4. Updates source statistics (message count) // // Invalid or unparseable messages are silently discarded to maintain // system stability. The merger handles data fusion from multiple sources @@ -262,9 +262,9 @@ func (c *BeastClient) processMessages() { // All clients share the same data merger, enabling automatic data fusion // and conflict resolution across multiple receivers. type MultiSourceClient struct { - clients []*BeastClient // Managed Beast clients - merger *merger.Merger // Shared data merger for all sources - mu sync.RWMutex // Protects clients slice + clients []*BeastClient // Managed Beast clients + merger *merger.Merger // Shared data merger for all sources + mu sync.RWMutex // Protects clients slice } // NewMultiSourceClient creates a client manager for multiple Beast format sources. @@ -292,9 +292,9 @@ func NewMultiSourceClient(merger *merger.Merger) *MultiSourceClient { // AddSource registers and configures a new Beast format data source. // // This method: -// 1. Registers the source with the data merger -// 2. Creates a new BeastClient for the source -// 3. Adds the client to the managed clients list +// 1. Registers the source with the data merger +// 2. Creates a new BeastClient for the source +// 3. Adds the client to the managed clients list // // The source is not automatically started; call Start() to begin connections. // Sources can be added before or after starting the multi-source client. diff --git a/internal/icao/database.go b/internal/icao/database.go index 4b125b9..ab26af3 100644 --- a/internal/icao/database.go +++ b/internal/icao/database.go @@ -30,7 +30,7 @@ type CountryInfo struct { // NewDatabase creates a new ICAO database with comprehensive allocation data func NewDatabase() (*Database, error) { allocations := getICAOAllocations() - + // Sort allocations by start address for efficient binary search sort.Slice(allocations, func(i, j int) bool { return allocations[i].StartAddr < allocations[j].StartAddr @@ -265,4 +265,4 @@ func (d *Database) LookupCountry(icaoHex string) (*CountryInfo, error) { // Close is a no-op since we don't have any resources to clean up func (d *Database) Close() error { return nil -} +} \ No newline at end of file diff --git a/internal/merger/merger.go b/internal/merger/merger.go index 0215493..584d7a6 100644 --- a/internal/merger/merger.go +++ b/internal/merger/merger.go @@ -22,7 +22,6 @@ package merger import ( "encoding/json" "fmt" - "log" "math" "sync" "time" @@ -34,31 +33,8 @@ import ( const ( // MaxDistance represents an infinite distance for initialization MaxDistance = float64(999999) - - // Position validation constants - MaxSpeedKnots = 2000.0 // Maximum plausible aircraft speed (roughly Mach 3 at cruise altitude) - MaxDistanceNautMiles = 500.0 // Maximum position jump distance in nautical miles - MaxAltitudeFeet = 60000 // Maximum altitude in feet (commercial ceiling ~FL600) - MinAltitudeFeet = -500 // Minimum altitude (below sea level but allow for dead sea, etc.) - - // Earth coordinate bounds - MinLatitude = -90.0 - MaxLatitude = 90.0 - MinLongitude = -180.0 - MaxLongitude = 180.0 - - // Conversion factors - KnotsToKmh = 1.852 - NmToKm = 1.852 ) -// ValidationResult represents the result of position validation checks. -type ValidationResult struct { - Valid bool // Whether the position passed all validation checks - Errors []string // List of validation failures for debugging - Warnings []string // List of potential issues (not blocking) -} - // Source represents a data source (dump1090 receiver or similar ADS-B source). // It contains both static configuration and dynamic status information used // for data fusion decisions and source monitoring. @@ -112,33 +88,30 @@ func (a *AircraftState) MarshalJSON() ([]byte, error) { // Create a struct that mirrors AircraftState but with ICAO24 as string return json.Marshal(&struct { // From embedded modes.Aircraft - ICAO24 string `json:"ICAO24"` - Callsign string `json:"Callsign"` - Latitude float64 `json:"Latitude"` - Longitude float64 `json:"Longitude"` - Altitude int `json:"Altitude"` - BaroAltitude int `json:"BaroAltitude"` - GeomAltitude int `json:"GeomAltitude"` - VerticalRate int `json:"VerticalRate"` - GroundSpeed int `json:"GroundSpeed"` - Track int `json:"Track"` - Heading int `json:"Heading"` - Category string `json:"Category"` - Squawk string `json:"Squawk"` - Emergency string `json:"Emergency"` - OnGround bool `json:"OnGround"` - Alert bool `json:"Alert"` - SPI bool `json:"SPI"` - NACp uint8 `json:"NACp"` - NACv uint8 `json:"NACv"` - SIL uint8 `json:"SIL"` - TransponderCapability string `json:"TransponderCapability"` - TransponderLevel uint8 `json:"TransponderLevel"` - SignalQuality string `json:"SignalQuality"` - SelectedAltitude int `json:"SelectedAltitude"` - SelectedHeading float64 `json:"SelectedHeading"` - BaroSetting float64 `json:"BaroSetting"` - + ICAO24 string `json:"ICAO24"` + Callsign string `json:"Callsign"` + Latitude float64 `json:"Latitude"` + Longitude float64 `json:"Longitude"` + Altitude int `json:"Altitude"` + BaroAltitude int `json:"BaroAltitude"` + GeomAltitude int `json:"GeomAltitude"` + VerticalRate int `json:"VerticalRate"` + GroundSpeed int `json:"GroundSpeed"` + Track int `json:"Track"` + Heading int `json:"Heading"` + Category string `json:"Category"` + Squawk string `json:"Squawk"` + Emergency string `json:"Emergency"` + OnGround bool `json:"OnGround"` + Alert bool `json:"Alert"` + SPI bool `json:"SPI"` + NACp uint8 `json:"NACp"` + NACv uint8 `json:"NACv"` + SIL uint8 `json:"SIL"` + SelectedAltitude int `json:"SelectedAltitude"` + SelectedHeading float64 `json:"SelectedHeading"` + BaroSetting float64 `json:"BaroSetting"` + // From AircraftState Sources map[string]*SourceData `json:"sources"` LastUpdate time.Time `json:"last_update"` @@ -159,33 +132,30 @@ func (a *AircraftState) MarshalJSON() ([]byte, error) { Flag string `json:"flag"` }{ // Copy all fields from Aircraft - ICAO24: fmt.Sprintf("%06X", a.Aircraft.ICAO24), - Callsign: a.Aircraft.Callsign, - Latitude: a.Aircraft.Latitude, - Longitude: a.Aircraft.Longitude, - Altitude: a.Aircraft.Altitude, - BaroAltitude: a.Aircraft.BaroAltitude, - GeomAltitude: a.Aircraft.GeomAltitude, - VerticalRate: a.Aircraft.VerticalRate, - GroundSpeed: a.Aircraft.GroundSpeed, - Track: a.Aircraft.Track, - Heading: a.Aircraft.Heading, - Category: a.Aircraft.Category, - Squawk: a.Aircraft.Squawk, - Emergency: a.Aircraft.Emergency, - OnGround: a.Aircraft.OnGround, - Alert: a.Aircraft.Alert, - SPI: a.Aircraft.SPI, - NACp: a.Aircraft.NACp, - NACv: a.Aircraft.NACv, - SIL: a.Aircraft.SIL, - TransponderCapability: a.Aircraft.TransponderCapability, - TransponderLevel: a.Aircraft.TransponderLevel, - SignalQuality: a.Aircraft.SignalQuality, - SelectedAltitude: a.Aircraft.SelectedAltitude, - SelectedHeading: a.Aircraft.SelectedHeading, - BaroSetting: a.Aircraft.BaroSetting, - + ICAO24: fmt.Sprintf("%06X", a.Aircraft.ICAO24), + Callsign: a.Aircraft.Callsign, + Latitude: a.Aircraft.Latitude, + Longitude: a.Aircraft.Longitude, + Altitude: a.Aircraft.Altitude, + BaroAltitude: a.Aircraft.BaroAltitude, + GeomAltitude: a.Aircraft.GeomAltitude, + VerticalRate: a.Aircraft.VerticalRate, + GroundSpeed: a.Aircraft.GroundSpeed, + Track: a.Aircraft.Track, + Heading: a.Aircraft.Heading, + Category: a.Aircraft.Category, + Squawk: a.Aircraft.Squawk, + Emergency: a.Aircraft.Emergency, + OnGround: a.Aircraft.OnGround, + Alert: a.Aircraft.Alert, + SPI: a.Aircraft.SPI, + NACp: a.Aircraft.NACp, + NACv: a.Aircraft.NACv, + SIL: a.Aircraft.SIL, + SelectedAltitude: a.Aircraft.SelectedAltitude, + SelectedHeading: a.Aircraft.SelectedHeading, + BaroSetting: a.Aircraft.BaroSetting, + // Copy all fields from AircraftState Sources: a.Sources, LastUpdate: a.LastUpdate, @@ -268,7 +238,7 @@ type Merger struct { sources map[string]*Source // Source ID -> source information icaoDB *icao.Database // ICAO country lookup database mu sync.RWMutex // Protects all maps and slices - historyLimit int // Maximum history points to retain + historyLimit int // Maximum history points to retain staleTimeout time.Duration // Time before aircraft considered stale (15 seconds) updateMetrics map[uint32]*updateMetric // ICAO24 -> update rate calculation data } @@ -321,13 +291,13 @@ func (m *Merger) AddSource(source *Source) { // UpdateAircraft merges new aircraft data from a source using intelligent fusion strategies. // // This is the core method of the merger, handling: -// 1. Aircraft state creation for new aircraft -// 2. Source data tracking and statistics -// 3. Multi-source data fusion with conflict resolution -// 4. Historical data updates with retention limits -// 5. Distance and bearing calculations -// 6. Update rate metrics -// 7. Source status maintenance +// 1. Aircraft state creation for new aircraft +// 2. Source data tracking and statistics +// 3. Multi-source data fusion with conflict resolution +// 4. Historical data updates with retention limits +// 5. Distance and bearing calculations +// 6. Update rate metrics +// 7. Source status maintenance // // Data fusion strategies: // - Position: Use source with strongest signal @@ -356,7 +326,7 @@ func (m *Merger) UpdateAircraft(sourceID string, aircraft *modes.Aircraft, signa AltitudeHistory: make([]AltitudePoint, 0), SpeedHistory: make([]SpeedPoint, 0), } - + // Lookup country information for new aircraft icaoHex := fmt.Sprintf("%06X", aircraft.ICAO24) if countryInfo, err := m.icaoDB.LookupCountry(icaoHex); err == nil { @@ -369,7 +339,7 @@ func (m *Merger) UpdateAircraft(sourceID string, aircraft *modes.Aircraft, signa state.CountryCode = "XX" state.Flag = "šŸ³ļø" } - + m.aircraft[aircraft.ICAO24] = state m.updateMetrics[aircraft.ICAO24] = &updateMetric{ updates: make([]time.Time, 0), @@ -447,46 +417,28 @@ func (m *Merger) UpdateAircraft(sourceID string, aircraft *modes.Aircraft, signa // - sourceID: Identifier of source providing new data // - timestamp: Timestamp of new data func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, sourceID string, timestamp time.Time) { - // Position - use source with best signal or most recent, but validate first + // Position - use source with best signal or most recent if new.Latitude != 0 && new.Longitude != 0 { - // Always validate position before considering update - validation := m.validatePosition(new, state, timestamp) + updatePosition := false - if !validation.Valid { - // Log validation errors and skip position update - icaoHex := fmt.Sprintf("%06X", new.ICAO24) - for _, err := range validation.Errors { - log.Printf("[POSITION_VALIDATION] ICAO %s: REJECTED position update - %s", icaoHex, err) - } - } else { - // Position is valid, proceed with normal logic - updatePosition := false - - if state.Latitude == 0 { - // First position update + if state.Latitude == 0 { + // First position update + updatePosition = true + } else if srcData, ok := state.Sources[sourceID]; ok { + // Use position from source with strongest signal + currentBest := m.getBestSignalSource(state) + if currentBest == "" || srcData.SignalLevel > state.Sources[currentBest].SignalLevel { + updatePosition = true + } else if currentBest == sourceID { + // Same source as current best - allow updates for moving aircraft updatePosition = true - } else if srcData, ok := state.Sources[sourceID]; ok { - // Use position from source with strongest signal - currentBest := m.getBestSignalSource(state) - if currentBest == "" || srcData.SignalLevel > state.Sources[currentBest].SignalLevel { - updatePosition = true - } else if currentBest == sourceID { - // Same source as current best - allow updates for moving aircraft - updatePosition = true - } - } - - if updatePosition { - state.Latitude = new.Latitude - state.Longitude = new.Longitude - state.PositionSource = sourceID } } - // Log warnings even if position is valid - for _, warning := range validation.Warnings { - icaoHex := fmt.Sprintf("%06X", new.ICAO24) - log.Printf("[POSITION_VALIDATION] ICAO %s: WARNING - %s", icaoHex, warning) + if updatePosition { + state.Latitude = new.Latitude + state.Longitude = new.Longitude + state.PositionSource = sourceID } } @@ -557,27 +509,6 @@ func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, so if new.BaroSetting != 0 { state.BaroSetting = new.BaroSetting } - - // Transponder information - use most recent non-empty - if new.TransponderCapability != "" { - state.TransponderCapability = new.TransponderCapability - } - if new.TransponderLevel > 0 { - state.TransponderLevel = new.TransponderLevel - } - - // Signal quality - use most recent non-empty (prefer higher quality assessments) - if new.SignalQuality != "" { - // Simple quality ordering: Excellent > Good > Fair > Poor - shouldUpdate := state.SignalQuality == "" || - (new.SignalQuality == "Excellent") || - (new.SignalQuality == "Good" && state.SignalQuality != "Excellent") || - (new.SignalQuality == "Fair" && state.SignalQuality == "Poor") - - if shouldUpdate { - state.SignalQuality = new.SignalQuality - } - } } // updateHistories adds data points to historical tracking arrays. @@ -599,31 +530,14 @@ func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, so // - signal: Signal strength measurement // - timestamp: When this data was received func (m *Merger) updateHistories(state *AircraftState, aircraft *modes.Aircraft, sourceID string, signal float64, timestamp time.Time) { - // Position history with validation + // Position history if aircraft.Latitude != 0 && aircraft.Longitude != 0 { - // Validate position before adding to history - validation := m.validatePosition(aircraft, state, timestamp) - - if validation.Valid { - state.PositionHistory = append(state.PositionHistory, PositionPoint{ - Time: timestamp, - Latitude: aircraft.Latitude, - Longitude: aircraft.Longitude, - Source: sourceID, - }) - } else { - // Log validation errors for debugging - icaoHex := fmt.Sprintf("%06X", aircraft.ICAO24) - for _, err := range validation.Errors { - log.Printf("[POSITION_VALIDATION] ICAO %s: REJECTED - %s", icaoHex, err) - } - } - - // Log warnings even for valid positions - for _, warning := range validation.Warnings { - icaoHex := fmt.Sprintf("%06X", aircraft.ICAO24) - log.Printf("[POSITION_VALIDATION] ICAO %s: WARNING - %s", icaoHex, warning) - } + state.PositionHistory = append(state.PositionHistory, PositionPoint{ + Time: timestamp, + Latitude: aircraft.Latitude, + Longitude: aircraft.Longitude, + Source: sourceID, + }) } // Signal history @@ -671,10 +585,10 @@ func (m *Merger) updateHistories(state *AircraftState, aircraft *modes.Aircraft, // updateUpdateRate calculates and maintains the message update rate for an aircraft. // // The calculation: -// 1. Records the timestamp of each update -// 2. Maintains a sliding 30-second window of updates -// 3. Calculates updates per second over this window -// 4. Updates the aircraft's UpdateRate field +// 1. Records the timestamp of each update +// 2. Maintains a sliding 30-second window of updates +// 3. Calculates updates per second over this window +// 4. Updates the aircraft's UpdateRate field // // This provides real-time feedback on data quality and can help identify // aircraft that are updating frequently (close, good signal) vs infrequently @@ -730,10 +644,10 @@ func (m *Merger) getBestSignalSource(state *AircraftState) string { // GetAircraft returns a snapshot of all current aircraft states. // // This method: -// 1. Filters out stale aircraft (older than staleTimeout) -// 2. Calculates current age for each aircraft -// 3. Determines closest receiver distance and bearing -// 4. Returns copies to prevent external modification +// 1. Filters out stale aircraft (older than staleTimeout) +// 2. Calculates current age for each aircraft +// 3. Determines closest receiver distance and bearing +// 4. Returns copies to prevent external modification // // The returned map uses ICAO24 addresses as keys and can be safely // used by multiple goroutines without affecting the internal state. @@ -895,135 +809,11 @@ func calculateDistanceBearing(lat1, lon1, lat2, lon2 float64) (float64, float64) return distance, bearing } -// validatePosition performs comprehensive validation of aircraft position data to filter out -// obviously incorrect flight paths and implausible position updates. -// -// This function implements multiple validation checks to improve data quality: -// -// 1. **Coordinate Validation**: Ensures latitude/longitude are within Earth's bounds -// 2. **Altitude Validation**: Rejects impossible altitudes (negative or > FL600) -// 3. **Speed Validation**: Calculates implied speed and rejects >Mach 3 movements -// 4. **Distance Validation**: Rejects position jumps >500nm without time justification -// 5. **Time Validation**: Ensures timestamps are chronologically consistent -// -// Parameters: -// - aircraft: New aircraft position data to validate -// - state: Current aircraft state with position history -// - timestamp: Timestamp of the new position data -// -// Returns: -// - ValidationResult with valid flag and detailed error/warning messages -func (m *Merger) validatePosition(aircraft *modes.Aircraft, state *AircraftState, timestamp time.Time) *ValidationResult { - result := &ValidationResult{ - Valid: true, - Errors: make([]string, 0), - Warnings: make([]string, 0), - } - - // Skip validation if no position data - if aircraft.Latitude == 0 && aircraft.Longitude == 0 { - return result // No position to validate - } - - // 1. Geographic coordinate validation - if aircraft.Latitude < MinLatitude || aircraft.Latitude > MaxLatitude { - result.Valid = false - result.Errors = append(result.Errors, fmt.Sprintf("Invalid latitude: %.6f (must be between %.1f and %.1f)", - aircraft.Latitude, MinLatitude, MaxLatitude)) - } - - if aircraft.Longitude < MinLongitude || aircraft.Longitude > MaxLongitude { - result.Valid = false - result.Errors = append(result.Errors, fmt.Sprintf("Invalid longitude: %.6f (must be between %.1f and %.1f)", - aircraft.Longitude, MinLongitude, MaxLongitude)) - } - - // 2. Altitude validation - if aircraft.Altitude != 0 { // Only validate non-zero altitudes - if aircraft.Altitude < MinAltitudeFeet { - result.Valid = false - result.Errors = append(result.Errors, fmt.Sprintf("Impossible altitude: %d feet (below minimum %d)", - aircraft.Altitude, MinAltitudeFeet)) - } - - if aircraft.Altitude > MaxAltitudeFeet { - result.Valid = false - result.Errors = append(result.Errors, fmt.Sprintf("Impossible altitude: %d feet (above maximum %d)", - aircraft.Altitude, MaxAltitudeFeet)) - } - } - - // 3. Speed and distance validation (requires position history) - if len(state.PositionHistory) > 0 && state.Latitude != 0 && state.Longitude != 0 { - lastPos := state.PositionHistory[len(state.PositionHistory)-1] - - // Calculate distance between positions - distance, _ := calculateDistanceBearing(lastPos.Latitude, lastPos.Longitude, - aircraft.Latitude, aircraft.Longitude) - - // Calculate time difference - timeDiff := timestamp.Sub(lastPos.Time).Seconds() - - if timeDiff > 0 { - // Calculate implied speed in knots - distanceNm := distance / NmToKm - speedKnots := (distanceNm / timeDiff) * 3600 // Convert to knots per hour - - // Distance validation: reject jumps >500nm - if distanceNm > MaxDistanceNautMiles { - result.Valid = false - result.Errors = append(result.Errors, fmt.Sprintf("Impossible position jump: %.1f nm in %.1f seconds (max allowed: %.1f nm)", - distanceNm, timeDiff, MaxDistanceNautMiles)) - } - - // Speed validation: reject >2000 knots (roughly Mach 3) - if speedKnots > MaxSpeedKnots { - result.Valid = false - result.Errors = append(result.Errors, fmt.Sprintf("Impossible speed: %.0f knots (max allowed: %.0f knots)", - speedKnots, MaxSpeedKnots)) - } - - // Warning for high but possible speeds (>800 knots) - if speedKnots > 800 && speedKnots <= MaxSpeedKnots { - result.Warnings = append(result.Warnings, fmt.Sprintf("High speed detected: %.0f knots", speedKnots)) - } - } else if timeDiff < 0 { - // 4. Time validation: reject out-of-order timestamps - result.Valid = false - result.Errors = append(result.Errors, fmt.Sprintf("Out-of-order timestamp: %.1f seconds in the past", -timeDiff)) - } - } - - // 5. Aircraft-specific validations based on reported speed vs. position - if aircraft.GroundSpeed > 0 && len(state.PositionHistory) > 0 { - // Check if reported ground speed is consistent with position changes - lastPos := state.PositionHistory[len(state.PositionHistory)-1] - distance, _ := calculateDistanceBearing(lastPos.Latitude, lastPos.Longitude, - aircraft.Latitude, aircraft.Longitude) - timeDiff := timestamp.Sub(lastPos.Time).Seconds() - - if timeDiff > 0 { - distanceNm := distance / NmToKm - impliedSpeed := (distanceNm / timeDiff) * 3600 - reportedSpeed := float64(aircraft.GroundSpeed) - - // Warning if speeds differ significantly (>100 knots difference) - if math.Abs(impliedSpeed-reportedSpeed) > 100 && reportedSpeed > 50 { - result.Warnings = append(result.Warnings, - fmt.Sprintf("Speed inconsistency: reported %d knots, implied %.0f knots", - aircraft.GroundSpeed, impliedSpeed)) - } - } - } - - return result -} - // Close closes the merger and releases resources func (m *Merger) Close() error { m.mu.Lock() defer m.mu.Unlock() - + if m.icaoDB != nil { return m.icaoDB.Close() } diff --git a/internal/modes/decoder.go b/internal/modes/decoder.go index 397a1f9..385b460 100644 --- a/internal/modes/decoder.go +++ b/internal/modes/decoder.go @@ -56,16 +56,16 @@ func validateModeSCRC(data []byte) bool { if len(data) < 4 { return false } - + // Calculate CRC for all bytes except the last 3 (which contain the CRC) crc := uint32(0) for i := 0; i < len(data)-3; i++ { crc = ((crc << 8) ^ crcTable[((crc>>16)^uint32(data[i]))&0xFF]) & 0xFFFFFF } - + // Extract transmitted CRC from last 3 bytes transmittedCRC := uint32(data[len(data)-3])<<16 | uint32(data[len(data)-2])<<8 | uint32(data[len(data)-1]) - + return crc == transmittedCRC } @@ -107,44 +107,37 @@ const ( // depending on the messages received and aircraft capabilities. type Aircraft struct { // Core Identification - ICAO24 uint32 // 24-bit ICAO aircraft address (unique identifier) - Callsign string // 8-character flight callsign (from identification messages) - + ICAO24 uint32 // 24-bit ICAO aircraft address (unique identifier) + Callsign string // 8-character flight callsign (from identification messages) + // Position and Navigation - Latitude float64 // Position latitude in decimal degrees - Longitude float64 // Position longitude in decimal degrees - Altitude int // Altitude in feet (barometric or geometric) - BaroAltitude int // Barometric altitude in feet (QNH corrected) - GeomAltitude int // Geometric altitude in feet (GNSS height) - + Latitude float64 // Position latitude in decimal degrees + Longitude float64 // Position longitude in decimal degrees + Altitude int // Altitude in feet (barometric or geometric) + BaroAltitude int // Barometric altitude in feet (QNH corrected) + GeomAltitude int // Geometric altitude in feet (GNSS height) + // Motion and Dynamics - VerticalRate int // Vertical rate in feet per minute (climb/descent) - GroundSpeed int // Ground speed in knots (integer) - Track int // Track angle in degrees (0-359, integer) - Heading int // Aircraft heading in degrees (magnetic, integer) - + VerticalRate int // Vertical rate in feet per minute (climb/descent) + GroundSpeed int // Ground speed in knots (integer) + Track int // Track angle in degrees (0-359, integer) + Heading int // Aircraft heading in degrees (magnetic, integer) + // Aircraft Information - Category string // Aircraft category (size, type, performance) - Squawk string // 4-digit transponder squawk code (octal) - + Category string // Aircraft category (size, type, performance) + Squawk string // 4-digit transponder squawk code (octal) + // Status and Alerts - Emergency string // Emergency/priority status description - OnGround bool // Aircraft is on ground (surface movement) - Alert bool // Alert flag (ATC attention required) - SPI bool // Special Position Identification (pilot activated) - + Emergency string // Emergency/priority status description + OnGround bool // Aircraft is on ground (surface movement) + Alert bool // Alert flag (ATC attention required) + SPI bool // Special Position Identification (pilot activated) + // Data Quality Indicators - NACp uint8 // Navigation Accuracy Category - Position (0-11) - NACv uint8 // Navigation Accuracy Category - Velocity (0-4) - SIL uint8 // Surveillance Integrity Level (0-3) - - // Transponder Information - TransponderCapability string // Transponder capability level (from DF11 messages) - TransponderLevel uint8 // Transponder level (0-7 from capability field) - - // Combined Data Quality Assessment - SignalQuality string // Combined assessment of position/velocity accuracy and integrity - + NACp uint8 // Navigation Accuracy Category - Position (0-11) + NACv uint8 // Navigation Accuracy Category - Velocity (0-4) + SIL uint8 // Surveillance Integrity Level (0-3) + // Autopilot/Flight Management SelectedAltitude int // MCP/FCU selected altitude in feet SelectedHeading float64 // MCP/FCU selected heading in degrees @@ -170,11 +163,11 @@ type Decoder struct { cprOddLon map[uint32]float64 // Odd message longitude encoding (ICAO24 -> normalized lon) cprEvenTime map[uint32]int64 // Timestamp of even message (for freshness comparison) cprOddTime map[uint32]int64 // Timestamp of odd message (for freshness comparison) - + // Reference position for CPR zone ambiguity resolution (receiver location) refLatitude float64 // Receiver latitude in decimal degrees refLongitude float64 // Receiver longitude in decimal degrees - + // Mutex to protect concurrent access to CPR maps mu sync.RWMutex } @@ -206,10 +199,10 @@ func NewDecoder(refLat, refLon float64) *Decoder { // Decode processes a Mode S message and extracts all available aircraft information. // // This is the main entry point for message decoding. The method: -// 1. Validates message length and extracts the Downlink Format (DF) -// 2. Extracts the ICAO24 aircraft address -// 3. Routes to appropriate decoder based on message type -// 4. Returns populated Aircraft struct with available data +// 1. Validates message length and extracts the Downlink Format (DF) +// 2. Extracts the ICAO24 aircraft address +// 3. Routes to appropriate decoder based on message type +// 4. Returns populated Aircraft struct with available data // // Different message types provide different information: // - DF4/20: Altitude only @@ -238,32 +231,14 @@ func (d *Decoder) Decode(data []byte) (*Aircraft, error) { } switch df { - case DF0: - // Short Air-Air Surveillance (ACAS) - aircraft.Altitude = d.decodeAltitude(data) case DF4, DF20: aircraft.Altitude = d.decodeAltitude(data) case DF5, DF21: aircraft.Squawk = d.decodeSquawk(data) - case DF11: - // All-Call Reply - extract capability and interrogator identifier - d.decodeAllCallReply(data, aircraft) - case DF16: - // Long Air-Air Surveillance (ACAS with altitude) - aircraft.Altitude = d.decodeAltitude(data) case DF17, DF18: return d.decodeExtendedSquitter(data, aircraft) - case DF19: - // Military Extended Squitter - similar to DF17/18 but with military codes - return d.decodeMilitaryExtendedSquitter(data, aircraft) - case DF24: - // Comm-D Enhanced Length Message - variable length data - d.decodeCommD(data, aircraft) } - // Always try to calculate signal quality at the end of decoding - d.calculateSignalQuality(aircraft) - return aircraft, nil } @@ -336,12 +311,6 @@ func (d *Decoder) decodeExtendedSquitter(data []byte, aircraft *Aircraft) (*Airc d.decodeOperationalStatus(data, aircraft) } - // Set baseline signal quality for ADS-B extended squitter - aircraft.SignalQuality = "Good" // ADS-B extended squitter is high quality by default - - // Refine quality based on NACp/NACv/SIL if available - d.calculateSignalQuality(aircraft) - return aircraft, nil } @@ -400,10 +369,10 @@ func (d *Decoder) decodeIdentification(data []byte, aircraft *Aircraft) { // - Even/odd flag for CPR decoding // // CPR (Compact Position Reporting) Process: -// 1. Extract the even/odd flag and CPR lat/lon values -// 2. Normalize CPR values to 0-1 range (divide by 2^17) -// 3. Store values for this aircraft's ICAO address -// 4. Attempt position decoding if both even and odd messages are available +// 1. Extract the even/odd flag and CPR lat/lon values +// 2. Normalize CPR values to 0-1 range (divide by 2^17) +// 3. Store values for this aircraft's ICAO address +// 4. Attempt position decoding if both even and odd messages are available // // The actual position calculation requires both even and odd messages to // resolve the ambiguity inherent in the compressed encoding format. @@ -434,20 +403,8 @@ func (d *Decoder) decodeAirbornePosition(data []byte, aircraft *Aircraft) { } d.mu.Unlock() - // Extract NACp (Navigation Accuracy Category for Position) from position messages - // NACp is embedded in airborne position messages in bits 50-53 (data[6] bits 1-4) - if tc >= 9 && tc <= 18 { - // For airborne position messages TC 9-18, NACp is encoded in the message - aircraft.NACp = uint8(tc - 8) // TC 9->NACp 1, TC 10->NACp 2, etc. - // Note: This is a simplified mapping. Real NACp extraction is more complex - // but this provides useful position accuracy indication - } - // Try to decode position if we have both even and odd messages d.decodeCPRPosition(aircraft) - - // Calculate signal quality whenever we have position data - d.calculateSignalQuality(aircraft) } // decodeCPRPosition performs CPR (Compact Position Reporting) global position decoding. @@ -499,7 +456,7 @@ func (d *Decoder) decodeCPRPosition(aircraft *Aircraft) { } else if latEven < -90 { latEven = -180 - latEven } - + if latOdd > 90 { latOdd = 180 - latOdd } else if latOdd < -90 { @@ -516,7 +473,7 @@ func (d *Decoder) decodeCPRPosition(aircraft *Aircraft) { // Calculate which decoded latitude is closer to the receiver distToEven := math.Abs(latEven - d.refLatitude) distToOdd := math.Abs(latOdd - d.refLatitude) - + // Choose the latitude solution that's closer to the receiver position if distToOdd < distToEven { aircraft.Latitude = latOdd @@ -544,7 +501,7 @@ func (d *Decoder) decodeCPRPosition(aircraft *Aircraft) { } aircraft.Longitude = lon - + // CPR decoding completed successfully } @@ -619,13 +576,13 @@ func (d *Decoder) decodeVelocity(data []byte, aircraft *Aircraft) { // Calculate ground speed in knots (rounded to integer) speedKnots := math.Sqrt(ewVel*ewVel + nsVel*nsVel) - + // Validate speed range (0-600 knots for civilian aircraft) if speedKnots > 600 { speedKnots = 600 // Cap at reasonable maximum } aircraft.GroundSpeed = int(math.Round(speedKnots)) - + // Calculate track in degrees (0-359) trackDeg := math.Atan2(ewVel, nsVel) * 180 / math.Pi if trackDeg < 0 { @@ -684,20 +641,20 @@ func (d *Decoder) decodeAltitudeBits(altCode uint16, tc uint8) int { // Standard altitude encoding with 25 ft increments // Check Q-bit (bit 4) for encoding type qBit := (altCode >> 4) & 1 - + if qBit == 1 { // Standard altitude with Q-bit set // Remove Q-bit and reassemble 11-bit altitude code n := ((altCode & 0x1F80) >> 2) | ((altCode & 0x0020) >> 1) | (altCode & 0x000F) alt := int(n)*25 - 1000 - + // Validate altitude range if alt < -1000 || alt > 60000 { return 0 } return alt } - + // Gray code altitude (100 ft increments) - legacy encoding // Convert from Gray code to binary n := altCode @@ -705,7 +662,7 @@ func (d *Decoder) decodeAltitudeBits(altCode uint16, tc uint8) int { n ^= n >> 4 n ^= n >> 2 n ^= n >> 1 - + // Convert to altitude in feet alt := int(n&0x7FF) * 100 if alt < 0 || alt > 60000 { @@ -878,7 +835,7 @@ func (d *Decoder) decodeTargetState(data []byte, aircraft *Aircraft) { // // Operational status messages (TC 31) contain: // - Navigation Accuracy Category for Position (NACp): Position accuracy -// - Navigation Accuracy Category for Velocity (NACv): Velocity accuracy +// - Navigation Accuracy Category for Velocity (NACv): Velocity accuracy // - Surveillance Integrity Level (SIL): System integrity confidence // // These parameters help receiving systems assess data quality and determine @@ -892,9 +849,6 @@ func (d *Decoder) decodeOperationalStatus(data []byte, aircraft *Aircraft) { aircraft.NACp = (data[7] >> 4) & 0x0F aircraft.NACv = data[7] & 0x0F aircraft.SIL = (data[8] >> 6) & 0x03 - - // Calculate combined signal quality from NACp, NACv, and SIL - d.calculateSignalQuality(aircraft) } // decodeSurfacePosition extracts position and movement data for aircraft on the ground. @@ -986,164 +940,3 @@ func (d *Decoder) decodeGroundSpeed(movement uint8) float64 { } return 0 } - -// decodeAllCallReply extracts capability and interrogator identifier from DF11 messages. -// -// DF11 All-Call Reply messages contain: -// - Capability (CA) field (3 bits): transponder capabilities and modes -// - Interrogator Identifier (II) field (4 bits): which radar interrogated -// - ICAO24 address (24 bits): aircraft identifier -// -// The capability field indicates transponder features and operational modes: -// - 0: Level 1 transponder -// - 1: Level 2 transponder -// - 2: Level 2+ transponder with additional capabilities -// - 3: Level 2+ transponder with enhanced surveillance -// - 4: Level 2+ transponder with enhanced surveillance and extended squitter -// - 5: Level 2+ transponder with enhanced surveillance, extended squitter, and enhanced surveillance capability -// - 6: Level 2+ transponder with enhanced surveillance, extended squitter, and enhanced surveillance capability -// - 7: Level 2+ transponder, downlink request value is 0, or the flight status is alert, SPI, or emergency -// -// Parameters: -// - data: 7-byte DF11 message -// - aircraft: Aircraft struct to populate -func (d *Decoder) decodeAllCallReply(data []byte, aircraft *Aircraft) { - if len(data) < 7 { - return - } - - // Extract Capability (CA) - bits 6-8 of first byte - capability := (data[0] >> 0) & 0x07 - - // Extract Interrogator Identifier (II) - would be in control field if present - // For DF11, this information is typically implied by the interrogating radar - - // Store transponder capability information in dedicated fields - aircraft.TransponderLevel = capability - switch capability { - case 0: - aircraft.TransponderCapability = "Level 1" - case 1: - aircraft.TransponderCapability = "Level 2" - case 2, 3: - aircraft.TransponderCapability = "Level 2+" - case 4, 5, 6: - aircraft.TransponderCapability = "Enhanced" - case 7: - aircraft.TransponderCapability = "Alert/Emergency" - } -} - -// decodeMilitaryExtendedSquitter processes DF19 military extended squitter messages. -// -// DF19 messages have the same structure as DF17/18 ADS-B extended squitter but -// may contain military-specific type codes or enhanced data formats. -// This implementation treats them similarly to civilian extended squitter -// but could be extended for military-specific capabilities. -// -// Parameters: -// - data: 14-byte DF19 message -// - aircraft: Aircraft struct to populate -// -// Returns updated Aircraft struct or error for malformed messages. -func (d *Decoder) decodeMilitaryExtendedSquitter(data []byte, aircraft *Aircraft) (*Aircraft, error) { - if len(data) != 14 { - return nil, fmt.Errorf("invalid military extended squitter length: %d bytes", len(data)) - } - - // For now, treat military extended squitter similar to civilian - // Could be enhanced to handle military-specific type codes - return d.decodeExtendedSquitter(data, aircraft) -} - -// decodeCommD extracts data from DF24 Comm-D Enhanced Length Messages. -// -// DF24 messages are variable-length data link communications that can contain: -// - Weather information and updates -// - Flight plan modifications -// - Controller-pilot data link messages -// - Air traffic management information -// - Future air navigation system data -// -// Due to the complexity and variety of DF24 message content, this implementation -// provides basic structure extraction. Full decoding would require extensive -// knowledge of specific data link protocols and message formats. -// -// Parameters: -// - data: Variable-length DF24 message (minimum 7 bytes) -// - aircraft: Aircraft struct to populate -func (d *Decoder) decodeCommD(data []byte, aircraft *Aircraft) { - if len(data) < 7 { - return - } - - // DF24 messages contain variable data that would require protocol-specific decoding - // For now, we note that this is a data communication message but don't overwrite aircraft category - // Could set a separate field for message type if needed in the future - - // The actual message content would require: - // - Protocol identifier extraction - // - Message type determination - // - Format-specific field extraction - // - Possible message reassembly for multi-part messages - // - // This could be extended based on specific requirements and available documentation -} - -// calculateSignalQuality combines NACp, NACv, and SIL into an overall data quality assessment. -// -// This function provides a human-readable quality indicator that considers: -// - Position accuracy (NACp): How precise the aircraft's position data is -// - Velocity accuracy (NACv): How precise the speed/heading data is -// - Surveillance integrity (SIL): How reliable/trustworthy the data is -// -// The algorithm prioritizes integrity first (SIL), then position accuracy (NACp), -// then velocity accuracy (NACv) to provide a meaningful overall assessment. -// -// Quality levels: -// - "Excellent": High integrity with very precise position/velocity -// - "Good": Good integrity with reasonable precision -// - "Fair": Moderate quality suitable for tracking -// - "Poor": Low quality but still usable -// - "Unknown": No quality indicators available -// -// Parameters: -// - aircraft: Aircraft struct containing NACp, NACv, and SIL values -func (d *Decoder) calculateSignalQuality(aircraft *Aircraft) { - nacp := aircraft.NACp - nacv := aircraft.NACv - sil := aircraft.SIL - - // If no quality indicators are available, don't set anything - if nacp == 0 && nacv == 0 && sil == 0 { - // Don't overwrite existing quality assessment - return - } - - // Excellent: High integrity with high accuracy OR very high accuracy alone - if (sil >= 2 && nacp >= 9) || nacp >= 10 { - aircraft.SignalQuality = "Excellent" - return - } - - // Good: Good integrity with moderate accuracy OR high accuracy alone - if (sil >= 2 && nacp >= 6) || (sil >= 1 && nacp >= 9) || nacp >= 8 { - aircraft.SignalQuality = "Good" - return - } - - // Fair: Some integrity with basic accuracy OR moderate accuracy alone - if (sil >= 1 && nacp >= 3) || nacp >= 5 { - aircraft.SignalQuality = "Fair" - return - } - - // Poor: Low but usable quality indicators - if sil > 0 || nacp >= 1 || nacv > 0 { - aircraft.SignalQuality = "Poor" - return - } - - // Default fallback - aircraft.SignalQuality = "" -} diff --git a/internal/server/server.go b/internal/server/server.go index 76e89a8..caeb1ed 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -22,7 +22,6 @@ import ( "net/http" "path" "strconv" - "strings" "sync" "time" @@ -36,8 +35,8 @@ import ( // This is used as the center point for the web map interface and for // distance calculations in coverage analysis. type OriginConfig struct { - Latitude float64 `json:"latitude"` // Reference latitude in decimal degrees - Longitude float64 `json:"longitude"` // Reference longitude in decimal degrees + Latitude float64 `json:"latitude"` // Reference latitude in decimal degrees + Longitude float64 `json:"longitude"` // Reference longitude in decimal degrees Name string `json:"name,omitempty"` // Descriptive name for the origin point } @@ -52,12 +51,11 @@ type OriginConfig struct { // - Concurrent broadcast system for WebSocket clients // - CORS support for cross-origin web applications type Server struct { - host string // Bind address for HTTP server - port int // TCP port for HTTP server - merger *merger.Merger // Data source for aircraft information - staticFiles embed.FS // Embedded static web assets - server *http.Server // HTTP server instance - origin OriginConfig // Geographic reference point + port int // TCP port for HTTP server + merger *merger.Merger // Data source for aircraft information + staticFiles embed.FS // Embedded static web assets + server *http.Server // HTTP server instance + origin OriginConfig // Geographic reference point // WebSocket management wsClients map[*websocket.Conn]bool // Active WebSocket client connections @@ -65,8 +63,8 @@ type Server struct { upgrader websocket.Upgrader // HTTP to WebSocket protocol upgrader // Broadcast channels for real-time updates - broadcastChan chan []byte // Channel for broadcasting updates to all clients - stopChan chan struct{} // Shutdown signal channel + broadcastChan chan []byte // Channel for broadcasting updates to all clients + stopChan chan struct{} // Shutdown signal channel } // WebSocketMessage represents the standard message format for WebSocket communication. @@ -87,7 +85,7 @@ type AircraftUpdate struct { Stats map[string]interface{} `json:"stats"` // System statistics and metrics } -// NewWebServer creates a new HTTP server instance for serving the SkyView web interface. +// NewServer creates a new HTTP server instance for serving the SkyView web interface. // // The server is configured with: // - WebSocket upgrader allowing all origins (suitable for development) @@ -95,16 +93,14 @@ type AircraftUpdate struct { // - Read/Write buffers optimized for aircraft data messages // // Parameters: -// - host: Bind address (empty for all interfaces, "localhost" for local only) // - port: TCP port number for the HTTP server // - merger: Data merger instance providing aircraft information // - staticFiles: Embedded filesystem containing web assets // - origin: Geographic reference point for the map interface // // Returns a configured but not yet started server instance. -func NewWebServer(host string, port int, merger *merger.Merger, staticFiles embed.FS, origin OriginConfig) *Server { +func NewServer(port int, merger *merger.Merger, staticFiles embed.FS, origin OriginConfig) *Server { return &Server{ - host: host, port: port, merger: merger, staticFiles: staticFiles, @@ -125,9 +121,9 @@ func NewWebServer(host string, port int, merger *merger.Merger, staticFiles embe // Start begins serving HTTP requests and WebSocket connections. // // This method starts several background routines: -// 1. Broadcast routine - handles WebSocket message distribution -// 2. Periodic update routine - sends regular updates to WebSocket clients -// 3. HTTP server - serves API endpoints and static files +// 1. Broadcast routine - handles WebSocket message distribution +// 2. Periodic update routine - sends regular updates to WebSocket clients +// 3. HTTP server - serves API endpoints and static files // // The method blocks until the server encounters an error or is shut down. // Use Stop() for graceful shutdown. @@ -143,15 +139,8 @@ func (s *Server) Start() error { // Setup routes router := s.setupRoutes() - // Format address correctly for IPv6 - addr := fmt.Sprintf("%s:%d", s.host, s.port) - if strings.Contains(s.host, ":") { - // IPv6 address needs brackets - addr = fmt.Sprintf("[%s]:%d", s.host, s.port) - } - s.server = &http.Server{ - Addr: addr, + Addr: fmt.Sprintf(":%d", s.port), Handler: router, } @@ -161,9 +150,9 @@ func (s *Server) Start() error { // Stop gracefully shuts down the server and all background routines. // // This method: -// 1. Signals all background routines to stop via stopChan -// 2. Shuts down the HTTP server with a 5-second timeout -// 3. Closes WebSocket connections +// 1. Signals all background routines to stop via stopChan +// 2. Shuts down the HTTP server with a 5-second timeout +// 3. Closes WebSocket connections // // The shutdown is designed to be safe and allow in-flight requests to complete. func (s *Server) Stop() { @@ -217,13 +206,13 @@ func (s *Server) setupRoutes() http.Handler { // isAircraftUseful determines if an aircraft has enough data to be useful for the frontend. // -// DESIGN NOTE: We WANT reasonable aircraft to appear in our table view, even if they -// don't have enough data to appear on the map. This provides users visibility into +// DESIGN NOTE: We WANT reasonable aircraft to appear in our table view, even if they +// don't have enough data to appear on the map. This provides users visibility into // all tracked aircraft, not just those with complete position data. // // Aircraft are considered useful if they have ANY of: // - Valid position data (both latitude and longitude non-zero) -> Can show on map -// - Callsign (flight identification) -> Can show in table with "No position" status +// - Callsign (flight identification) -> Can show in table with "No position" status // - Altitude information -> Can show in table as "Aircraft at X feet" // - Any other identifying information that makes it a "real" aircraft // @@ -235,7 +224,7 @@ func (s *Server) isAircraftUseful(aircraft *merger.AircraftState) bool { hasCallsign := aircraft.Callsign != "" hasAltitude := aircraft.Altitude != 0 hasSquawk := aircraft.Squawk != "" - + // Include aircraft with any identifying or operational data return hasValidPosition || hasCallsign || hasAltitude || hasSquawk } @@ -393,10 +382,10 @@ func (s *Server) handleGetCoverage(w http.ResponseWriter, r *http.Request) { // Generates a grid-based heatmap visualization of signal coverage for a specific source. // // The heatmap is computed by: -// 1. Finding geographic bounds of all aircraft positions for the source -// 2. Creating a 100x100 grid covering the bounds -// 3. Accumulating signal strength values in each grid cell -// 4. Returning the grid data with boundary coordinates +// 1. Finding geographic bounds of all aircraft positions for the source +// 2. Creating a 100x100 grid covering the bounds +// 3. Accumulating signal strength values in each grid cell +// 4. Returning the grid data with boundary coordinates // // This provides a density-based visualization of where the source receives // the strongest signals, useful for coverage analysis and antenna optimization. @@ -467,11 +456,11 @@ func (s *Server) handleGetHeatmap(w http.ResponseWriter, r *http.Request) { // handleWebSocket manages WebSocket connections for real-time aircraft data streaming. // // This handler: -// 1. Upgrades the HTTP connection to WebSocket protocol -// 2. Registers the client for broadcast updates -// 3. Sends initial data snapshot to the client -// 4. Handles client messages (currently just ping/pong for keepalive) -// 5. Cleans up the connection when the client disconnects +// 1. Upgrades the HTTP connection to WebSocket protocol +// 2. Registers the client for broadcast updates +// 3. Sends initial data snapshot to the client +// 4. Handles client messages (currently just ping/pong for keepalive) +// 5. Cleans up the connection when the client disconnects // // WebSocket clients receive periodic updates with current aircraft positions, // source status, and system statistics. The connection is kept alive until @@ -599,11 +588,11 @@ func (s *Server) periodicUpdateRoutine() { // broadcastUpdate creates and queues an aircraft update message for WebSocket clients. // // This function: -// 1. Collects current aircraft data from the merger -// 2. Filters aircraft to only include "useful" ones (with position or callsign) -// 3. Formats the data as a WebSocketMessage with type "aircraft_update" -// 4. Converts ICAO addresses to hex strings for JSON compatibility -// 5. Queues the message for broadcast (non-blocking) +// 1. Collects current aircraft data from the merger +// 2. Filters aircraft to only include "useful" ones (with position or callsign) +// 3. Formats the data as a WebSocketMessage with type "aircraft_update" +// 4. Converts ICAO addresses to hex strings for JSON compatibility +// 5. Queues the message for broadcast (non-blocking) // // If the broadcast channel is full, the update is dropped to prevent blocking. // This ensures the system continues operating even if WebSocket clients @@ -780,11 +769,11 @@ func (s *Server) handleDebugAircraft(w http.ResponseWriter, r *http.Request) { } response := map[string]interface{}{ - "timestamp": time.Now().Unix(), - "all_aircraft": allAircraftMap, + "timestamp": time.Now().Unix(), + "all_aircraft": allAircraftMap, "filtered_aircraft": filteredAircraftMap, - "all_count": len(allAircraftMap), - "filtered_count": len(filteredAircraftMap), + "all_count": len(allAircraftMap), + "filtered_count": len(filteredAircraftMap), } w.Header().Set("Content-Type", "application/json") diff --git a/main b/main new file mode 100755 index 0000000..12ac49d Binary files /dev/null and b/main differ diff --git a/old.json b/old.json new file mode 100644 index 0000000..6f3e473 --- /dev/null +++ b/old.json @@ -0,0 +1,15 @@ +{ + "server": { + "address": ":8080", + "port": 8080 + }, + "dump1090": { + "host": "svovel", + "data_port": 30003 + }, + "origin": { + "latitude": 59.908127, + "longitude": 10.801460, + "name": "Etterstadsletta flyplass" + } +} diff --git a/scripts/build-deb.sh b/scripts/build-deb.sh index 7534373..21f8c11 100755 --- a/scripts/build-deb.sh +++ b/scripts/build-deb.sh @@ -34,40 +34,25 @@ mkdir -p "$BUILD_DIR" # Change to project directory cd "$PROJECT_DIR" -# Build the applications -echo_info "Building SkyView applications..." +# Build the application +echo_info "Building SkyView application..." export CGO_ENABLED=0 export GOOS=linux export GOARCH=amd64 -VERSION=$(git describe --tags --always --dirty) -LDFLAGS="-w -s -X main.version=$VERSION" - -# Build main skyview binary -echo_info "Building skyview..." -if ! go build -ldflags="$LDFLAGS" \ +go build -ldflags="-w -s -X main.version=$(git describe --tags --always --dirty)" \ -o "$DEB_DIR/usr/bin/skyview" \ - ./cmd/skyview; then - echo_error "Failed to build skyview" + ./cmd/skyview + +if [ $? -ne 0 ]; then + echo_error "Failed to build application" exit 1 fi -# Build beast-dump utility -echo_info "Building beast-dump..." -if ! go build -ldflags="$LDFLAGS" \ - -o "$DEB_DIR/usr/bin/beast-dump" \ - ./cmd/beast-dump; then - echo_error "Failed to build beast-dump" - exit 1 -fi +echo_info "Built binary: $(file "$DEB_DIR/usr/bin/skyview")" -echo_info "Built binaries:" -echo_info " skyview: $(file "$DEB_DIR/usr/bin/skyview")" -echo_info " beast-dump: $(file "$DEB_DIR/usr/bin/beast-dump")" - -# Set executable permissions +# Set executable permission chmod +x "$DEB_DIR/usr/bin/skyview" -chmod +x "$DEB_DIR/usr/bin/beast-dump" # Get package info VERSION=$(grep "Version:" "$DEB_DIR/DEBIAN/control" | cut -d' ' -f2) @@ -84,7 +69,9 @@ sed -i "s/Installed-Size:.*/Installed-Size: $INSTALLED_SIZE/" "$DEB_DIR/DEBIAN/c echo "Installed-Size: $INSTALLED_SIZE" >> "$DEB_DIR/DEBIAN/control" # Build the package -if dpkg-deb --root-owner-group --build "$DEB_DIR" "$BUILD_DIR/$DEB_FILE"; then +dpkg-deb --build "$DEB_DIR" "$BUILD_DIR/$DEB_FILE" + +if [ $? -eq 0 ]; then echo_info "Successfully created: $BUILD_DIR/$DEB_FILE" # Show package info diff --git a/ux.png b/ux.png new file mode 100644 index 0000000..ec40c79 Binary files /dev/null and b/ux.png differ