diff --git a/CLAUDE.md b/CLAUDE.md index 17110eb..336d043 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1,39 @@ -- This project uses forgejo for source control and the fj client is available. \ No newline at end of file +# SkyView Project Guidelines + +## Documentation Requirements +- We should always have an up to date document describing our architecture and features +- Include links to any external resources we've used +- We should also always have an up to date README describing the project +- Shell scripts should be validated with shellcheck +- Always make sure the code is well documented with explanations for why and how a particular solution is selected + +## Development Principles +- An overarching principle with all code is KISS, Keep It Simple Stupid +- We do not want to create code that is more complicated than necessary +- When changing code, always make sure to update any relevant tests +- Use proper error handling - aviation applications need reliability + +## SkyView-Specific Guidelines + +### Architecture & Design +- Multi-source ADS-B data fusion is the core feature - prioritize signal strength-based conflict resolution +- Embedded resources (SQLite ICAO database, static assets) over external dependencies +- Low-latency performance is critical - optimize for fast WebSocket updates +- Support concurrent aircraft tracking (100+ aircraft should work smoothly) + +### Code Organization +- Keep Go packages focused: beast parsing, modes decoding, merger, server, clients +- Frontend should be modular: separate managers for aircraft, map, UI, websockets +- Database operations should be fast (use indexes, avoid N+1 queries) + +### Performance Considerations +- Beast binary parsing must handle high message rates (1000+ msg/sec per source) +- WebSocket broadcasting should not block on slow clients +- Memory usage should be bounded (configurable history limits) +- CPU usage should remain low during normal operation + +### Documentation Maintenance +- Always update docs/ARCHITECTURE.md when changing system design +- README.md should stay current with features and usage +- External resources (ICAO docs, ADS-B standards) should be linked in documentation +- Country database updates should be straightforward (replace SQLite file) \ No newline at end of file diff --git a/assets/assets.go b/assets/assets.go index 3cef7d0..54e1c4a 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 -// - aircraft-icon.svg: SVG icon for aircraft markers +// - icons/*.svg: Type-specific SVG icons 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 deleted file mode 100644 index f2489d3..0000000 --- a/assets/static/aircraft-icon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/beast-dump-with-heli.bin b/beast-dump-with-heli.bin deleted file mode 100644 index fa579ea..0000000 Binary files a/beast-dump-with-heli.bin and /dev/null differ diff --git a/cmd/beast-dump/main.go b/cmd/beast-dump/main.go index 4d585de..8bd7a8b 100644 --- a/cmd/beast-dump/main.go +++ b/cmd/beast-dump/main.go @@ -5,14 +5,16 @@ // 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 ( @@ -42,23 +44,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() @@ -66,7 +68,7 @@ func main() { } dumper := NewBeastDumper(config) - + if err := dumper.Run(); err != nil { log.Fatalf("Error: %v", err) } @@ -75,12 +77,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") @@ -91,7 +93,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 } @@ -102,11 +104,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(), @@ -118,10 +120,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 { @@ -139,34 +141,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 } @@ -176,14 +178,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 } @@ -196,7 +198,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 { @@ -209,21 +211,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 } @@ -231,7 +233,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 { @@ -240,18 +242,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)) } @@ -266,11 +268,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) @@ -298,7 +300,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 @@ -306,33 +308,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)) @@ -340,26 +342,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 { @@ -367,7 +369,7 @@ func (d *BeastDumper) formatAircraftInfo(aircraft *modes.Aircraft) string { } info += part } - + return info } @@ -377,7 +379,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) @@ -418,23 +420,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 deleted file mode 100644 index 15dbc78..0000000 --- a/config.json.example +++ /dev/null @@ -1,15 +0,0 @@ -{ - "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/docs/CLAUDE.md b/docs/CLAUDE.md deleted file mode 100644 index 336d043..0000000 --- a/docs/CLAUDE.md +++ /dev/null @@ -1,39 +0,0 @@ -# SkyView Project Guidelines - -## Documentation Requirements -- We should always have an up to date document describing our architecture and features -- Include links to any external resources we've used -- We should also always have an up to date README describing the project -- Shell scripts should be validated with shellcheck -- Always make sure the code is well documented with explanations for why and how a particular solution is selected - -## Development Principles -- An overarching principle with all code is KISS, Keep It Simple Stupid -- We do not want to create code that is more complicated than necessary -- When changing code, always make sure to update any relevant tests -- Use proper error handling - aviation applications need reliability - -## SkyView-Specific Guidelines - -### Architecture & Design -- Multi-source ADS-B data fusion is the core feature - prioritize signal strength-based conflict resolution -- Embedded resources (SQLite ICAO database, static assets) over external dependencies -- Low-latency performance is critical - optimize for fast WebSocket updates -- Support concurrent aircraft tracking (100+ aircraft should work smoothly) - -### Code Organization -- Keep Go packages focused: beast parsing, modes decoding, merger, server, clients -- Frontend should be modular: separate managers for aircraft, map, UI, websockets -- Database operations should be fast (use indexes, avoid N+1 queries) - -### Performance Considerations -- Beast binary parsing must handle high message rates (1000+ msg/sec per source) -- WebSocket broadcasting should not block on slow clients -- Memory usage should be bounded (configurable history limits) -- CPU usage should remain low during normal operation - -### Documentation Maintenance -- Always update docs/ARCHITECTURE.md when changing system design -- README.md should stay current with features and usage -- External resources (ICAO docs, ADS-B standards) should be linked in documentation -- Country database updates should be straightforward (replace SQLite file) \ No newline at end of file diff --git a/internal/beast/parser.go b/internal/beast/parser.go index 436a728..ec5afed 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 cb30a74..6810874 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 ab26af3..4b125b9 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 584d7a6..a4f10ef 100644 --- a/internal/merger/merger.go +++ b/internal/merger/merger.go @@ -111,7 +111,7 @@ func (a *AircraftState) MarshalJSON() ([]byte, error) { 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"` @@ -155,7 +155,7 @@ func (a *AircraftState) MarshalJSON() ([]byte, error) { SelectedAltitude: a.Aircraft.SelectedAltitude, SelectedHeading: a.Aircraft.SelectedHeading, BaroSetting: a.Aircraft.BaroSetting, - + // Copy all fields from AircraftState Sources: a.Sources, LastUpdate: a.LastUpdate, @@ -238,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 } @@ -291,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 @@ -326,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 { @@ -339,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), @@ -585,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 @@ -644,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. @@ -813,7 +813,7 @@ func calculateDistanceBearing(lat1, lon1, lat2, lon2 float64) (float64, float64) 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 385b460..a8d5afb 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,37 +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) - + 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 @@ -163,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 } @@ -199,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 @@ -369,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. @@ -456,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 { @@ -473,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 @@ -501,7 +501,7 @@ func (d *Decoder) decodeCPRPosition(aircraft *Aircraft) { } aircraft.Longitude = lon - + // CPR decoding completed successfully } @@ -576,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 { @@ -641,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 @@ -662,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 { @@ -835,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 diff --git a/internal/server/server.go b/internal/server/server.go index caeb1ed..fd66b4c 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -22,6 +22,7 @@ import ( "net/http" "path" "strconv" + "strings" "sync" "time" @@ -35,8 +36,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 } @@ -51,11 +52,12 @@ type OriginConfig struct { // - Concurrent broadcast system for WebSocket clients // - CORS support for cross-origin web applications type Server struct { - 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 + 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 // WebSocket management wsClients map[*websocket.Conn]bool // Active WebSocket client connections @@ -63,8 +65,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. @@ -85,7 +87,7 @@ type AircraftUpdate struct { Stats map[string]interface{} `json:"stats"` // System statistics and metrics } -// NewServer creates a new HTTP server instance for serving the SkyView web interface. +// NewWebServer 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) @@ -93,14 +95,16 @@ 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 NewServer(port int, merger *merger.Merger, staticFiles embed.FS, origin OriginConfig) *Server { +func NewWebServer(host string, port int, merger *merger.Merger, staticFiles embed.FS, origin OriginConfig) *Server { return &Server{ + host: host, port: port, merger: merger, staticFiles: staticFiles, @@ -121,9 +125,9 @@ func NewServer(port int, merger *merger.Merger, staticFiles embed.FS, origin Ori // 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. @@ -139,8 +143,15 @@ 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: fmt.Sprintf(":%d", s.port), + Addr: addr, Handler: router, } @@ -150,9 +161,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() { @@ -206,13 +217,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 // @@ -224,7 +235,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 } @@ -382,10 +393,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. @@ -456,11 +467,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 @@ -588,11 +599,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 @@ -769,11 +780,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 deleted file mode 100755 index 12ac49d..0000000 Binary files a/main and /dev/null differ diff --git a/old.json b/old.json deleted file mode 100644 index 6f3e473..0000000 --- a/old.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "server": { - "address": ":8080", - "port": 8080 - }, - "dump1090": { - "host": "svovel", - "data_port": 30003 - }, - "origin": { - "latitude": 59.908127, - "longitude": 10.801460, - "name": "Etterstadsletta flyplass" - } -} diff --git a/ux.png b/ux.png deleted file mode 100644 index ec40c79..0000000 Binary files a/ux.png and /dev/null differ