From 9ebc7e143e7cdd8fabdcfaef76cc51c6ed8ffefb Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Sun, 24 Aug 2025 10:29:25 +0200 Subject: [PATCH] Clean up, format, lint and document entire codebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major cleanup and documentation effort: Code Cleanup: - Remove 668+ lines of dead code from legacy SBS-1 implementation - Delete unused packages: internal/config, internal/parser, internal/client/dump1090 - Remove broken test file internal/server/server_test.go - Remove unused struct fields and imports Code Quality: - Format all Go code with gofmt - Fix all go vet issues - Fix staticcheck linting issues (error capitalization, unused fields) - Clean up module dependencies with go mod tidy Documentation: - Add comprehensive godoc documentation to all packages - Document CPR position decoding algorithm with mathematical details - Document multi-source data fusion strategies - Add function/method documentation with parameters and return values - Document error handling and recovery strategies - Add performance considerations and architectural decisions README Updates: - Update project structure to reflect assets/ organization - Add new features: smart origin, Reset Map button, map controls - Document origin configuration in config examples - Add /api/origin endpoint to API documentation - Update REST endpoints with /api/aircraft/{icao} Analysis: - Analyzed adsb-tools and go-adsb for potential improvements - Confirmed current Beast implementation is production-ready - Identified optional enhancements for future consideration The codebase is now clean, well-documented, and follows Go best practices with zero linting issues and comprehensive documentation throughout. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 14 +- assets/assets.go | 29 ++- internal/beast/parser.go | 180 +++++++++++-- internal/client/beast.go | 218 ++++++++++++++-- internal/client/dump1090.go | 267 ------------------- internal/config/config.go | 118 --------- internal/merger/merger.go | 343 ++++++++++++++++++------ internal/modes/decoder.go | 460 ++++++++++++++++++++++++++++----- internal/parser/sbs1.go | 224 ---------------- internal/server/server.go | 282 ++++++++++++++++++-- internal/server/server_test.go | 55 ---- 11 files changed, 1299 insertions(+), 891 deletions(-) delete mode 100644 internal/client/dump1090.go delete mode 100644 internal/config/config.go delete mode 100644 internal/parser/sbs1.go delete mode 100644 internal/server/server_test.go diff --git a/README.md b/README.md index b7ac6ef..42715a2 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ A high-performance, multi-source ADS-B aircraft tracking application that connec - **Flight Trails**: Historical aircraft movement tracking - **3D Radar View**: Three.js-powered 3D visualization (optional) - **Statistics Dashboard**: Real-time charts and metrics +- **Smart Origin**: Auto-calculated map center based on receiver locations +- **Map Controls**: Center on aircraft, reset to origin, toggle overlays ### Aircraft Data - **Complete Mode S Decoding**: Position, velocity, altitude, heading @@ -84,6 +86,11 @@ sudo systemctl enable skyview "history_limit": 1000, "stale_timeout": 60, "update_rate": 1 + }, + "origin": { + "latitude": 51.4700, + "longitude": -0.4600, + "name": "Custom Origin" } } ``` @@ -145,8 +152,10 @@ docker run -p 8080:8080 -v $(pwd)/config.json:/app/config.json skyview ### REST Endpoints - `GET /api/aircraft` - All aircraft data +- `GET /api/aircraft/{icao}` - Individual aircraft details - `GET /api/sources` - Data source information -- `GET /api/stats` - System statistics +- `GET /api/stats` - System statistics +- `GET /api/origin` - Map origin configuration - `GET /api/coverage/{sourceId}` - Coverage analysis - `GET /api/heatmap/{sourceId}` - Signal heatmap @@ -159,11 +168,12 @@ docker run -p 8080:8080 -v $(pwd)/config.json:/app/config.json skyview ``` skyview/ ├── cmd/skyview/ # Main application +├── assets/ # Embedded static web assets ├── internal/ │ ├── beast/ # Beast format parser │ ├── modes/ # Mode S decoder │ ├── merger/ # Multi-source merger -│ ├── client/ # TCP clients +│ ├── client/ # Beast TCP clients │ └── server/ # HTTP/WebSocket server ├── debian/ # Debian packaging └── scripts/ # Build scripts diff --git a/assets/assets.go b/assets/assets.go index 1831910..3cef7d0 100644 --- a/assets/assets.go +++ b/assets/assets.go @@ -1,11 +1,32 @@ // Package assets provides embedded static web assets for the SkyView application. -// This package embeds all files from the static/ directory at build time. +// +// This package uses Go 1.16+ embed functionality to include all static web files +// directly in the compiled binary, eliminating the need for external file dependencies +// at runtime. The embedded assets include: +// - 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 +// - favicon.ico: Browser icon +// +// The embedded filesystem is used by the HTTP server to serve static content +// and enables single-binary deployment without external asset dependencies. package assets import "embed" -// Static contains all embedded static assets -// The files are accessed with paths like "static/index.html", "static/css/style.css", etc. +// 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/css/style.css" +// - "static/js/app.js" +// - etc. +// +// This approach ensures the web interface is always available without requiring +// external file deployment or complicated asset management. // //go:embed static -var Static embed.FS \ No newline at end of file +var Static embed.FS diff --git a/internal/beast/parser.go b/internal/beast/parser.go index 2517765..178566e 100644 --- a/internal/beast/parser.go +++ b/internal/beast/parser.go @@ -1,3 +1,26 @@ +// Package beast provides Beast binary format parsing for ADS-B message streams. +// +// The Beast format is a binary protocol developed by FlightAware and used by +// dump1090, readsb, and other ADS-B software to stream real-time aircraft data +// over TCP connections (typically port 30005). +// +// Beast Format Structure: +// - Each message starts with escape byte 0x1A +// - Message type byte (0x31=Mode A/C, 0x32=Mode S Short, 0x33=Mode S Long) +// - 48-bit timestamp (12MHz clock ticks) +// - Signal level byte (RSSI) +// - Message payload (2, 7, or 14 bytes depending on type) +// - Escape sequences: 0x1A 0x1A represents literal 0x1A in data +// +// This package handles: +// - Binary message parsing and validation +// - Timestamp and signal strength extraction +// - Escape sequence processing +// - ICAO address and message type extraction +// - Continuous stream processing with error recovery +// +// The parser is designed to handle connection interruptions gracefully and +// can recover from malformed messages in the stream. package beast import ( @@ -9,32 +32,52 @@ import ( "time" ) -// Beast message types +// Beast format message type constants. +// These define the different types of messages in the Beast binary protocol. const ( - BeastModeAC = 0x31 // '1' - Mode A/C - BeastModeS = 0x32 // '2' - Mode S Short (56 bits) - BeastModeSLong = 0x33 // '3' - Mode S Long (112 bits) - BeastStatusMsg = 0x34 // '4' - Status message - BeastEscape = 0x1A // Escape character + BeastModeAC = 0x31 // '1' - Mode A/C squitter (2 bytes payload) + BeastModeS = 0x32 // '2' - Mode S Short squitter (7 bytes payload) + BeastModeSLong = 0x33 // '3' - Mode S Extended squitter (14 bytes payload) + BeastStatusMsg = 0x34 // '4' - Status message (variable length) + BeastEscape = 0x1A // Escape character (0x1A 0x1A = literal 0x1A) ) -// Message represents a Beast format message +// Message represents a parsed Beast format message with metadata. +// +// Contains both the raw Beast protocol fields and additional processing metadata: +// - Original Beast format fields (type, timestamp, signal, data) +// - Processing timestamp for age calculations +// - Source identification for multi-receiver setups type Message struct { - Type byte - Timestamp uint64 // 48-bit timestamp in 12MHz ticks - Signal uint8 // Signal level (RSSI) - Data []byte // Mode S data - ReceivedAt time.Time - SourceID string // Identifier for the source receiver + Type byte // Beast message type (0x31, 0x32, 0x33, 0x34) + Timestamp uint64 // 48-bit timestamp in 12MHz ticks from receiver + Signal uint8 // Signal level (RSSI) - 255 = 0 dBFS, 0 = minimum + Data []byte // Mode S message payload (2, 7, or 14 bytes) + ReceivedAt time.Time // Local processing timestamp + SourceID string // Identifier for the source receiver } -// Parser handles Beast binary format parsing +// Parser handles Beast binary format parsing from a stream. +// +// The parser maintains stream state and can recover from protocol errors +// by searching for the next valid message boundary. It uses buffered I/O +// for efficient byte-level parsing of the binary protocol. type Parser struct { - reader *bufio.Reader - sourceID string + reader *bufio.Reader // Buffered reader for efficient byte parsing + sourceID string // Source identifier for message tagging } -// NewParser creates a new Beast format parser +// NewParser creates a new Beast format parser for a data stream. +// +// The parser wraps the provided reader with a buffered reader for efficient +// parsing of the binary protocol. Each parsed message will be tagged with +// the provided sourceID for multi-source identification. +// +// Parameters: +// - r: Input stream containing Beast format data +// - sourceID: Identifier for this data source +// +// Returns a configured parser ready for message parsing. func NewParser(r io.Reader, sourceID string) *Parser { return &Parser{ reader: bufio.NewReader(r), @@ -42,7 +85,21 @@ func NewParser(r io.Reader, sourceID string) *Parser { } } -// ReadMessage reads and parses a single Beast message +// 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 +// +// The parser can recover from protocol errors by continuing to search for +// the next valid message boundary. Status messages are currently skipped +// as they contain variable-length data not needed for aircraft tracking. +// +// Returns the parsed message or an error if the stream is closed or corrupted. func (p *Parser) ReadMessage() (*Message, error) { // Look for escape character for { @@ -109,7 +166,20 @@ func (p *Parser) ReadMessage() (*Message, error) { }, nil } -// unescapeData removes escape sequences from Beast data +// unescapeData removes escape sequences from Beast format payload data. +// +// Beast format uses escape sequences to embed the escape character (0x1A) +// in message payloads: +// - 0x1A 0x1A in the stream represents a literal 0x1A byte in the data +// - Single 0x1A bytes are message boundaries, not data +// +// This method processes the payload after parsing to restore the original +// Mode S message bytes with any embedded escape characters. +// +// Parameters: +// - data: Raw payload bytes that may contain escape sequences +// +// Returns the unescaped data with literal 0x1A bytes restored. func (p *Parser) unescapeData(data []byte) []byte { result := make([]byte, 0, len(data)) i := 0 @@ -125,7 +195,20 @@ func (p *Parser) unescapeData(data []byte) []byte { return result } -// ParseStream continuously reads messages from the stream +// ParseStream continuously reads messages from the stream until an error occurs. +// +// This method runs in a loop, parsing messages and sending them to the provided +// channel. It handles various error conditions gracefully: +// - EOF and closed pipe errors terminate normally (expected on disconnect) +// - Other errors are reported via the error channel with source identification +// - Protocol errors within individual messages are recovered from automatically +// +// The method blocks until the stream closes or an unrecoverable error occurs. +// It's designed to run in a dedicated goroutine for continuous processing. +// +// Parameters: +// - msgChan: Channel for sending successfully parsed messages +// - errChan: Channel for reporting parsing errors func (p *Parser) ParseStream(msgChan chan<- *Message, errChan chan<- error) { for { msg, err := p.ReadMessage() @@ -139,7 +222,18 @@ func (p *Parser) ParseStream(msgChan chan<- *Message, errChan chan<- error) { } } -// GetSignalStrength converts signal byte to dBFS +// GetSignalStrength converts the Beast signal level byte to dBFS (decibels full scale). +// +// The Beast format encodes signal strength as: +// - 255 = 0 dBFS (maximum signal, clipping) +// - Lower values = weaker signals +// - 0 = minimum detectable signal (~-50 dBFS) +// +// The conversion provides a logarithmic scale suitable for signal quality +// comparison and coverage analysis. Values typically range from -50 to 0 dBFS +// in normal operation. +// +// Returns signal strength in dBFS (negative values, closer to 0 = stronger). func (msg *Message) GetSignalStrength() float64 { // Beast format: signal level is in units where 255 = 0 dBFS // Typical range is -50 to 0 dBFS @@ -149,10 +243,21 @@ func (msg *Message) GetSignalStrength() float64 { return float64(msg.Signal) * (-50.0 / 255.0) } -// GetICAO24 extracts the ICAO 24-bit address from Mode S messages +// GetICAO24 extracts the ICAO 24-bit aircraft address from Mode S messages. +// +// 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 3: Least significant 8 bits +// +// Mode A/C messages don't contain ICAO addresses and will return an error. +// The ICAO address is used as the primary key for aircraft tracking. +// +// Returns the 24-bit ICAO address as a uint32, or an error for invalid messages. func (msg *Message) GetICAO24() (uint32, error) { if msg.Type == BeastModeAC { - return 0, errors.New("Mode A/C messages don't contain ICAO address") + return 0, errors.New("mode A/C messages don't contain ICAO address") } if len(msg.Data) < 4 { @@ -164,7 +269,19 @@ func (msg *Message) GetICAO24() (uint32, error) { return icao, nil } -// GetDownlinkFormat returns the downlink format (first 5 bits) +// GetDownlinkFormat extracts the Downlink Format (DF) from Mode S messages. +// +// The DF field occupies the first 5 bits of every Mode S message and indicates +// the message type and structure: +// - DF 0: Short air-air surveillance +// - DF 4/5: Surveillance altitude/identity reply +// - DF 11: All-call reply +// - DF 17: Extended squitter (ADS-B) +// - DF 18: Extended squitter/non-transponder +// - DF 19: Military extended squitter +// - Others: Various surveillance and communication types +// +// Returns the 5-bit DF field value, or 0 if no data is available. func (msg *Message) GetDownlinkFormat() uint8 { if len(msg.Data) == 0 { return 0 @@ -172,7 +289,20 @@ func (msg *Message) GetDownlinkFormat() uint8 { return (msg.Data[0] >> 3) & 0x1F } -// GetTypeCode returns the message type code for extended squitter messages +// GetTypeCode extracts the Type Code (TC) from ADS-B extended squitter messages. +// +// The Type Code is a 5-bit field that indicates the specific type of ADS-B message: +// - TC 1-4: Aircraft identification and category +// - TC 5-8: Surface position messages +// - TC 9-18: Airborne position messages (different altitude sources) +// - TC 19: Airborne velocity messages +// - TC 20-22: Reserved for future use +// - Others: Various operational and status messages +// +// Only extended squitter messages (DF 17/18) contain type codes. Other +// message types will return an error. +// +// Returns the 5-bit type code, or an error for non-extended squitter messages. func (msg *Message) GetTypeCode() (uint8, error) { df := msg.GetDownlinkFormat() if df != 17 && df != 18 { // Extended squitter diff --git a/internal/client/beast.go b/internal/client/beast.go index d7df935..b5830bf 100644 --- a/internal/client/beast.go +++ b/internal/client/beast.go @@ -1,3 +1,17 @@ +// Package client provides Beast format TCP client implementations for connecting to ADS-B receivers. +// +// This package handles the network connectivity and data streaming from dump1090 or similar +// Beast format sources. It provides: +// - Single-source Beast TCP client with automatic reconnection +// - Multi-source client manager for handling multiple receivers +// - Exponential backoff for connection failures +// - Message parsing and Mode S decoding integration +// - Automatic stale aircraft cleanup +// +// The Beast format is a binary protocol commonly used by dump1090 and other ADS-B +// software to stream real-time aircraft data over TCP port 30005. This package +// abstracts the connection management and integrates with the merger for +// multi-source data fusion. package client import ( @@ -12,23 +26,48 @@ import ( "skyview/internal/modes" ) -// BeastClient handles connection to a single dump1090 Beast TCP stream +// BeastClient handles connection to a single dump1090 Beast format TCP stream. +// +// The client provides robust connectivity with: +// - Automatic reconnection with exponential backoff +// - Concurrent message reading and processing +// - Integration with Mode S decoder and data merger +// - Source status tracking and statistics +// - Graceful shutdown handling +// +// Each client maintains a persistent connection to one Beast source and +// continuously processes incoming messages until stopped or the source +// becomes unavailable. type BeastClient struct { - source *merger.Source - merger *merger.Merger - decoder *modes.Decoder - conn net.Conn - parser *beast.Parser - msgChan chan *beast.Message - errChan chan error - stopChan chan struct{} - wg sync.WaitGroup + 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 - reconnectDelay time.Duration - maxReconnect time.Duration + // Reconnection parameters + reconnectDelay time.Duration // Initial reconnect delay + maxReconnect time.Duration // Maximum reconnect delay (for backoff cap) } -// NewBeastClient creates a new Beast format TCP client +// NewBeastClient creates a new Beast format TCP client for a specific data source. +// +// The client is configured with: +// - Buffered message channel (1000 messages) to handle burst traffic +// - Error channel for connection and parsing issues +// - Initial reconnect delay of 5 seconds +// - Maximum reconnect delay of 60 seconds (exponential backoff cap) +// - Fresh Mode S decoder instance +// +// Parameters: +// - source: Source configuration including host, port, and metadata +// - merger: Data merger instance for aircraft state management +// +// Returns a configured but not yet started BeastClient. func NewBeastClient(source *merger.Source, merger *merger.Merger) *BeastClient { return &BeastClient{ source: source, @@ -42,13 +81,32 @@ func NewBeastClient(source *merger.Source, merger *merger.Merger) *BeastClient { } } -// Start begins the client connection and processing +// Start begins the client connection and message processing in the background. +// +// The client will: +// - Attempt to connect to the configured Beast source +// - Handle connection failures with exponential backoff +// - Start message reading and processing goroutines +// - Continuously reconnect if the connection is lost +// +// The method returns immediately; the client runs in background goroutines +// until Stop() is called or the context is cancelled. +// +// Parameters: +// - ctx: Context for cancellation and timeout control func (c *BeastClient) Start(ctx context.Context) { c.wg.Add(1) go c.run(ctx) } -// Stop gracefully stops the client +// 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 +// +// This method blocks until the shutdown is complete. func (c *BeastClient) Stop() { close(c.stopChan) if c.conn != nil { @@ -57,7 +115,19 @@ func (c *BeastClient) Stop() { c.wg.Wait() } -// run is the main client loop +// 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 +// +// The exponential backoff starts at reconnectDelay (5s) and doubles on each +// failure up to maxReconnect (60s), then resets on successful connection. +// +// Source status is updated to reflect connection state for monitoring. func (c *BeastClient) run(ctx context.Context) { defer c.wg.Done() @@ -122,13 +192,32 @@ func (c *BeastClient) run(ctx context.Context) { } } -// readMessages reads Beast messages from the TCP stream +// readMessages runs in a dedicated goroutine to read Beast format messages. +// +// This method: +// - Continuously reads from the TCP connection +// - Parses Beast format binary data into Message structs +// - Queues parsed messages for processing +// - Reports parsing errors to the error channel +// +// The method blocks on the parser's ParseStream call and exits when +// the connection is closed or an unrecoverable error occurs. func (c *BeastClient) readMessages() { defer c.wg.Done() c.parser.ParseStream(c.msgChan, c.errChan) } -// processMessages decodes and merges aircraft data +// 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) +// +// Invalid or unparseable messages are silently discarded to maintain +// system stability. The merger handles data fusion from multiple sources +// and conflict resolution based on signal strength. func (c *BeastClient) processMessages() { defer c.wg.Done() @@ -161,14 +250,38 @@ func (c *BeastClient) processMessages() { } } -// MultiSourceClient manages multiple Beast TCP clients +// MultiSourceClient manages multiple Beast TCP clients for multi-receiver setups. +// +// This client coordinator: +// - Manages connections to multiple Beast format sources simultaneously +// - Provides unified control for starting and stopping all clients +// - Runs periodic cleanup tasks for stale aircraft data +// - Aggregates statistics from all managed clients +// - Handles dynamic source addition and management +// +// All clients share the same data merger, enabling automatic data fusion +// and conflict resolution across multiple receivers. type MultiSourceClient struct { - clients []*BeastClient - merger *merger.Merger - mu sync.RWMutex + clients []*BeastClient // Managed Beast clients + merger *merger.Merger // Shared data merger for all sources + mu sync.RWMutex // Protects clients slice } -// NewMultiSourceClient creates a client that connects to multiple Beast sources +// NewMultiSourceClient creates a client manager for multiple Beast format sources. +// +// The multi-source client enables connecting to multiple dump1090 instances +// or other Beast format sources simultaneously. All sources feed into the +// same data merger, which handles automatic data fusion and conflict resolution. +// +// This is essential for: +// - Improved coverage from multiple receivers +// - Redundancy in case of individual receiver failures +// - Data quality improvement through signal strength comparison +// +// Parameters: +// - merger: Shared data merger instance for all sources +// +// Returns a configured multi-source client ready for source addition. func NewMultiSourceClient(merger *merger.Merger) *MultiSourceClient { return &MultiSourceClient{ clients: make([]*BeastClient, 0), @@ -176,7 +289,18 @@ func NewMultiSourceClient(merger *merger.Merger) *MultiSourceClient { } } -// AddSource adds a new Beast TCP source +// 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 +// +// The source is not automatically started; call Start() to begin connections. +// Sources can be added before or after starting the multi-source client. +// +// Parameters: +// - source: Source configuration including connection details and metadata func (m *MultiSourceClient) AddSource(source *merger.Source) { m.mu.Lock() defer m.mu.Unlock() @@ -189,7 +313,19 @@ func (m *MultiSourceClient) AddSource(source *merger.Source) { m.clients = append(m.clients, client) } -// Start begins all client connections +// Start begins connections to all configured Beast sources. +// +// This method: +// - Starts all managed BeastClient instances in parallel +// - Begins the periodic cleanup routine for stale aircraft data +// - Uses the provided context for cancellation control +// +// Each client will independently attempt connections with their own +// reconnection logic. The method returns immediately; all clients +// operate in background goroutines. +// +// Parameters: +// - ctx: Context for cancellation and timeout control func (m *MultiSourceClient) Start(ctx context.Context) { m.mu.RLock() defer m.mu.RUnlock() @@ -202,7 +338,11 @@ func (m *MultiSourceClient) Start(ctx context.Context) { go m.cleanupRoutine(ctx) } -// Stop gracefully stops all clients +// Stop gracefully shuts down all managed Beast clients. +// +// This method stops all clients in parallel and waits for their +// goroutines to complete. The shutdown is coordinated to ensure +// clean termination of all network connections and processing routines. func (m *MultiSourceClient) Stop() { m.mu.RLock() defer m.mu.RUnlock() @@ -212,7 +352,18 @@ func (m *MultiSourceClient) Stop() { } } -// cleanupRoutine periodically removes stale aircraft +// cleanupRoutine runs periodic maintenance tasks in a background goroutine. +// +// Currently performs: +// - Stale aircraft cleanup every 30 seconds +// - Removal of aircraft that haven't been updated recently +// +// The cleanup frequency is designed to balance memory usage with +// the typical aircraft update rates in ADS-B systems. Aircraft +// typically update their position every few seconds when in range. +// +// Parameters: +// - ctx: Context for cancellation when the client shuts down func (m *MultiSourceClient) cleanupRoutine(ctx context.Context) { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() @@ -227,7 +378,18 @@ func (m *MultiSourceClient) cleanupRoutine(ctx context.Context) { } } -// GetStatistics returns client statistics +// GetStatistics returns comprehensive statistics from all managed clients. +// +// The statistics include: +// - All merger statistics (aircraft count, message rates, etc.) +// - Number of active client connections +// - Total number of configured clients +// - Per-source connection status and message counts +// +// This information is useful for monitoring system health, diagnosing +// connectivity issues, and understanding data quality across sources. +// +// Returns a map of statistics suitable for JSON serialization and web display. func (m *MultiSourceClient) GetStatistics() map[string]interface{} { m.mu.RLock() defer m.mu.RUnlock() diff --git a/internal/client/dump1090.go b/internal/client/dump1090.go deleted file mode 100644 index 382655a..0000000 --- a/internal/client/dump1090.go +++ /dev/null @@ -1,267 +0,0 @@ -package client - -import ( - "bufio" - "context" - "fmt" - "log" - "net" - "sync" - "time" - - "skyview/internal/config" - "skyview/internal/parser" -) - -type Dump1090Client struct { - config *config.Config - aircraftMap map[string]*parser.Aircraft - mutex sync.RWMutex - subscribers []chan parser.AircraftData - subMutex sync.RWMutex -} - -func NewDump1090Client(cfg *config.Config) *Dump1090Client { - return &Dump1090Client{ - config: cfg, - aircraftMap: make(map[string]*parser.Aircraft), - subscribers: make([]chan parser.AircraftData, 0), - } -} - -func (c *Dump1090Client) Start(ctx context.Context) error { - go c.startDataStream(ctx) - go c.startPeriodicBroadcast(ctx) - go c.startCleanup(ctx) - return nil -} - -func (c *Dump1090Client) startDataStream(ctx context.Context) { - for { - select { - case <-ctx.Done(): - return - default: - if err := c.connectAndRead(ctx); err != nil { - log.Printf("Connection error: %v, retrying in 5s", err) - time.Sleep(5 * time.Second) - } - } - } -} - -func (c *Dump1090Client) connectAndRead(ctx context.Context) error { - address := fmt.Sprintf("%s:%d", c.config.Dump1090.Host, c.config.Dump1090.DataPort) - - conn, err := net.Dial("tcp", address) - if err != nil { - return fmt.Errorf("failed to connect to %s: %w", address, err) - } - defer conn.Close() - - log.Printf("Connected to dump1090 at %s", address) - - scanner := bufio.NewScanner(conn) - for scanner.Scan() { - select { - case <-ctx.Done(): - return nil - default: - line := scanner.Text() - c.processLine(line) - } - } - - return scanner.Err() -} - -func (c *Dump1090Client) processLine(line string) { - aircraft, err := parser.ParseSBS1Line(line) - if err != nil || aircraft == nil { - return - } - - c.mutex.Lock() - if existing, exists := c.aircraftMap[aircraft.Hex]; exists { - c.updateExistingAircraft(existing, aircraft) - } else { - c.aircraftMap[aircraft.Hex] = aircraft - } - c.mutex.Unlock() -} - -func (c *Dump1090Client) updateExistingAircraft(existing, update *parser.Aircraft) { - existing.LastSeen = update.LastSeen - existing.Messages++ - - if update.Flight != "" { - existing.Flight = update.Flight - } - if update.Altitude != 0 { - existing.Altitude = update.Altitude - } - if update.GroundSpeed != 0 { - existing.GroundSpeed = update.GroundSpeed - } - if update.Track != 0 { - existing.Track = update.Track - } - if update.Latitude != 0 && update.Longitude != 0 { - existing.Latitude = update.Latitude - existing.Longitude = update.Longitude - - // Add to track history if position changed significantly - if c.shouldAddTrackPoint(existing, update) { - trackPoint := parser.TrackPoint{ - Timestamp: update.LastSeen, - Latitude: update.Latitude, - Longitude: update.Longitude, - Altitude: update.Altitude, - Speed: update.GroundSpeed, - Track: update.Track, - } - - existing.TrackHistory = append(existing.TrackHistory, trackPoint) - - // Keep only last 200 points (about 3-4 hours at 1 point/minute) - if len(existing.TrackHistory) > 200 { - existing.TrackHistory = existing.TrackHistory[1:] - } - } - } - if update.VertRate != 0 { - existing.VertRate = update.VertRate - } - if update.Squawk != "" { - existing.Squawk = update.Squawk - } - existing.OnGround = update.OnGround - - // Preserve country and registration - if update.Country != "" && update.Country != "Unknown" { - existing.Country = update.Country - } - if update.Registration != "" { - existing.Registration = update.Registration - } -} - -func (c *Dump1090Client) shouldAddTrackPoint(existing, update *parser.Aircraft) bool { - // Add track point if: - // 1. No history yet - if len(existing.TrackHistory) == 0 { - return true - } - - lastPoint := existing.TrackHistory[len(existing.TrackHistory)-1] - - // 2. At least 30 seconds since last point - if time.Since(lastPoint.Timestamp) < 30*time.Second { - return false - } - - // 3. Position changed by at least 0.001 degrees (~100m) - latDiff := existing.Latitude - lastPoint.Latitude - lonDiff := existing.Longitude - lastPoint.Longitude - distanceChange := latDiff*latDiff + lonDiff*lonDiff - - return distanceChange > 0.000001 // ~0.001 degrees squared -} - -func (c *Dump1090Client) GetAircraftData() parser.AircraftData { - c.mutex.RLock() - defer c.mutex.RUnlock() - - aircraftMap := make(map[string]parser.Aircraft) - totalMessages := 0 - - for hex, aircraft := range c.aircraftMap { - aircraftMap[hex] = *aircraft - totalMessages += aircraft.Messages - } - - return parser.AircraftData{ - Now: time.Now().Unix(), - Messages: totalMessages, - Aircraft: aircraftMap, - } -} - -func (c *Dump1090Client) Subscribe() <-chan parser.AircraftData { - c.subMutex.Lock() - defer c.subMutex.Unlock() - - ch := make(chan parser.AircraftData, 10) - c.subscribers = append(c.subscribers, ch) - return ch -} - -func (c *Dump1090Client) startPeriodicBroadcast(ctx context.Context) { - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - data := c.GetAircraftData() - c.broadcastToSubscribers(data) - } - } -} - -func (c *Dump1090Client) broadcastToSubscribers(data parser.AircraftData) { - c.subMutex.RLock() - defer c.subMutex.RUnlock() - - for i, ch := range c.subscribers { - select { - case ch <- data: - default: - close(ch) - c.subMutex.RUnlock() - c.subMutex.Lock() - c.subscribers = append(c.subscribers[:i], c.subscribers[i+1:]...) - c.subMutex.Unlock() - c.subMutex.RLock() - } - } -} - -func (c *Dump1090Client) startCleanup(ctx context.Context) { - ticker := time.NewTicker(30 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - c.cleanupStaleAircraft() - } - } -} - -func (c *Dump1090Client) cleanupStaleAircraft() { - c.mutex.Lock() - defer c.mutex.Unlock() - - cutoff := time.Now().Add(-2 * time.Minute) - trackCutoff := time.Now().Add(-24 * time.Hour) - - for hex, aircraft := range c.aircraftMap { - if aircraft.LastSeen.Before(cutoff) { - delete(c.aircraftMap, hex) - } else { - // Clean up old track points (keep last 24 hours) - validTracks := make([]parser.TrackPoint, 0) - for _, point := range aircraft.TrackHistory { - if point.Timestamp.After(trackCutoff) { - validTracks = append(validTracks, point) - } - } - aircraft.TrackHistory = validTracks - } - } -} diff --git a/internal/config/config.go b/internal/config/config.go deleted file mode 100644 index bc6cfc0..0000000 --- a/internal/config/config.go +++ /dev/null @@ -1,118 +0,0 @@ -package config - -import ( - "encoding/json" - "fmt" - "os" - "strconv" -) - -type Config struct { - Server ServerConfig `json:"server"` - Dump1090 Dump1090Config `json:"dump1090"` - Origin OriginConfig `json:"origin"` -} - -type ServerConfig struct { - Address string `json:"address"` - Port int `json:"port"` -} - -type Dump1090Config struct { - Host string `json:"host"` - DataPort int `json:"data_port"` -} - -type OriginConfig struct { - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` - Name string `json:"name"` -} - -func Load() (*Config, error) { - cfg := &Config{ - Server: ServerConfig{ - Address: ":8080", - Port: 8080, - }, - Dump1090: Dump1090Config{ - Host: "localhost", - DataPort: 30003, - }, - Origin: OriginConfig{ - Latitude: 37.7749, - Longitude: -122.4194, - Name: "Default Location", - }, - } - - configFile := os.Getenv("SKYVIEW_CONFIG") - if configFile == "" { - // Check for config files in common locations - candidates := []string{"config.json", "./config.json", "skyview.json"} - for _, candidate := range candidates { - if _, err := os.Stat(candidate); err == nil { - configFile = candidate - break - } - } - } - - if configFile != "" { - if err := loadFromFile(cfg, configFile); err != nil { - return nil, fmt.Errorf("failed to load config file %s: %w", configFile, err) - } - } - - loadFromEnv(cfg) - - return cfg, nil -} - -func loadFromFile(cfg *Config, filename string) error { - data, err := os.ReadFile(filename) - if err != nil { - return err - } - - return json.Unmarshal(data, cfg) -} - -func loadFromEnv(cfg *Config) { - if addr := os.Getenv("SKYVIEW_ADDRESS"); addr != "" { - cfg.Server.Address = addr - } - - if portStr := os.Getenv("SKYVIEW_PORT"); portStr != "" { - if port, err := strconv.Atoi(portStr); err == nil { - cfg.Server.Port = port - cfg.Server.Address = fmt.Sprintf(":%d", port) - } - } - - if host := os.Getenv("DUMP1090_HOST"); host != "" { - cfg.Dump1090.Host = host - } - - if dataPortStr := os.Getenv("DUMP1090_DATA_PORT"); dataPortStr != "" { - if port, err := strconv.Atoi(dataPortStr); err == nil { - cfg.Dump1090.DataPort = port - } - } - - if latStr := os.Getenv("ORIGIN_LATITUDE"); latStr != "" { - if lat, err := strconv.ParseFloat(latStr, 64); err == nil { - cfg.Origin.Latitude = lat - } - } - - if lonStr := os.Getenv("ORIGIN_LONGITUDE"); lonStr != "" { - if lon, err := strconv.ParseFloat(lonStr, 64); err == nil { - cfg.Origin.Longitude = lon - } - } - - if name := os.Getenv("ORIGIN_NAME"); name != "" { - cfg.Origin.Name = name - } -} diff --git a/internal/merger/merger.go b/internal/merger/merger.go index 6dcfca7..c673538 100644 --- a/internal/merger/merger.go +++ b/internal/merger/merger.go @@ -1,3 +1,22 @@ +// Package merger provides multi-source aircraft data fusion and conflict resolution. +// +// This package is the core of SkyView's multi-source capability, handling the complex +// task of merging aircraft data from multiple ADS-B receivers. It provides: +// - Intelligent data fusion based on signal strength and recency +// - Historical tracking of aircraft positions, altitudes, and speeds +// - Per-source signal quality and update rate tracking +// - Automatic conflict resolution when sources disagree +// - Comprehensive aircraft state management +// - Distance and bearing calculations from receivers +// +// The merger uses several strategies for data fusion: +// - Position data: Uses source with strongest signal +// - Recent data: Prefers newer information for dynamic values +// - Best quality: Prioritizes higher accuracy navigation data +// - History tracking: Maintains trails for visualization and analysis +// +// All data structures are designed for concurrent access and JSON serialization +// for web API consumption. package merger import ( @@ -8,93 +27,131 @@ import ( "skyview/internal/modes" ) -// Source represents a data source (dump1090 receiver) +// 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. type Source struct { - ID string `json:"id"` - Name string `json:"name"` - Host string `json:"host"` - Port int `json:"port"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` - Altitude float64 `json:"altitude"` - Active bool `json:"active"` - LastSeen time.Time `json:"last_seen"` - Messages int64 `json:"messages"` - Aircraft int `json:"aircraft"` + ID string `json:"id"` // Unique identifier for this source + Name string `json:"name"` // Human-readable name + Host string `json:"host"` // Hostname or IP address + Port int `json:"port"` // TCP port number + Latitude float64 `json:"latitude"` // Receiver location latitude + Longitude float64 `json:"longitude"` // Receiver location longitude + Altitude float64 `json:"altitude"` // Receiver altitude above sea level + Active bool `json:"active"` // Currently connected and receiving data + LastSeen time.Time `json:"last_seen"` // Timestamp of last received message + Messages int64 `json:"messages"` // Total messages processed from this source + Aircraft int `json:"aircraft"` // Current aircraft count from this source } -// AircraftState represents merged aircraft state from all sources +// AircraftState represents the complete merged aircraft state from all sources. +// +// This structure combines the basic aircraft data from Mode S decoding with +// multi-source metadata, historical tracking, and derived information: +// - Embedded modes.Aircraft with all decoded ADS-B data +// - Per-source signal and quality information +// - Historical trails for position, altitude, speed, and signal strength +// - Distance and bearing from receivers +// - Update rate and age calculations +// - Data provenance tracking type AircraftState struct { - *modes.Aircraft - Sources map[string]*SourceData `json:"sources"` - LastUpdate time.Time `json:"last_update"` - FirstSeen time.Time `json:"first_seen"` - TotalMessages int64 `json:"total_messages"` - PositionHistory []PositionPoint `json:"position_history"` - SignalHistory []SignalPoint `json:"signal_history"` - AltitudeHistory []AltitudePoint `json:"altitude_history"` - SpeedHistory []SpeedPoint `json:"speed_history"` - Distance float64 `json:"distance"` // Distance from closest receiver - Bearing float64 `json:"bearing"` // Bearing from closest receiver - Age float64 `json:"age"` // Seconds since last update - MLATSources []string `json:"mlat_sources"` // Sources providing MLAT data - PositionSource string `json:"position_source"` // Source providing current position - UpdateRate float64 `json:"update_rate"` // Updates per second + *modes.Aircraft // Embedded decoded aircraft data + Sources map[string]*SourceData `json:"sources"` // Per-source information + LastUpdate time.Time `json:"last_update"` // Last update from any source + FirstSeen time.Time `json:"first_seen"` // First time this aircraft was seen + TotalMessages int64 `json:"total_messages"` // Total messages received for this aircraft + PositionHistory []PositionPoint `json:"position_history"` // Trail of position updates + SignalHistory []SignalPoint `json:"signal_history"` // Signal strength over time + AltitudeHistory []AltitudePoint `json:"altitude_history"` // Altitude and vertical rate history + SpeedHistory []SpeedPoint `json:"speed_history"` // Speed and track history + Distance float64 `json:"distance"` // Distance from closest receiver (km) + Bearing float64 `json:"bearing"` // Bearing from closest receiver (degrees) + Age float64 `json:"age"` // Seconds since last update + MLATSources []string `json:"mlat_sources"` // Sources providing MLAT position data + PositionSource string `json:"position_source"` // Source providing current position + UpdateRate float64 `json:"update_rate"` // Recent updates per second } -// SourceData represents data from a specific source +// SourceData represents data quality and statistics for a specific source-aircraft pair. +// This information is used for data fusion decisions and signal quality analysis. type SourceData struct { - SourceID string `json:"source_id"` - SignalLevel float64 `json:"signal_level"` - Messages int64 `json:"messages"` - LastSeen time.Time `json:"last_seen"` - Distance float64 `json:"distance"` - Bearing float64 `json:"bearing"` - UpdateRate float64 `json:"update_rate"` + SourceID string `json:"source_id"` // Unique identifier of the source + SignalLevel float64 `json:"signal_level"` // Signal strength (dBFS) + Messages int64 `json:"messages"` // Messages received from this source + LastSeen time.Time `json:"last_seen"` // Last message timestamp from this source + Distance float64 `json:"distance"` // Distance from receiver to aircraft (km) + Bearing float64 `json:"bearing"` // Bearing from receiver to aircraft (degrees) + UpdateRate float64 `json:"update_rate"` // Updates per second from this source } -// Position/Signal/Altitude/Speed history points +// PositionPoint represents a timestamped position update in aircraft history. +// Used to build position trails for visualization and track analysis. type PositionPoint struct { - Time time.Time `json:"time"` - Latitude float64 `json:"lat"` - Longitude float64 `json:"lon"` - Source string `json:"source"` + Time time.Time `json:"time"` // Timestamp when position was received + Latitude float64 `json:"lat"` // Latitude in decimal degrees + Longitude float64 `json:"lon"` // Longitude in decimal degrees + Source string `json:"source"` // Source that provided this position } +// SignalPoint represents a timestamped signal strength measurement. +// Used to track signal quality over time and analyze receiver performance. type SignalPoint struct { - Time time.Time `json:"time"` - Signal float64 `json:"signal"` - Source string `json:"source"` + Time time.Time `json:"time"` // Timestamp when signal was measured + Signal float64 `json:"signal"` // Signal strength in dBFS + Source string `json:"source"` // Source that measured this signal } +// AltitudePoint represents a timestamped altitude measurement. +// Includes vertical rate for flight profile analysis. type AltitudePoint struct { - Time time.Time `json:"time"` - Altitude int `json:"altitude"` - VRate int `json:"vrate"` + Time time.Time `json:"time"` // Timestamp when altitude was received + Altitude int `json:"altitude"` // Altitude in feet + VRate int `json:"vrate"` // Vertical rate in feet per minute } +// SpeedPoint represents a timestamped speed and track measurement. +// Used for aircraft performance analysis and track prediction. type SpeedPoint struct { - Time time.Time `json:"time"` - GroundSpeed float64 `json:"ground_speed"` - Track float64 `json:"track"` + Time time.Time `json:"time"` // Timestamp when speed was received + GroundSpeed float64 `json:"ground_speed"` // Ground speed in knots + Track float64 `json:"track"` // Track angle in degrees } -// Merger handles merging aircraft data from multiple sources +// Merger handles merging aircraft data from multiple sources with intelligent conflict resolution. +// +// The merger maintains: +// - Complete aircraft states with multi-source data fusion +// - Source registry with connection status and statistics +// - Historical data with configurable retention limits +// - Update rate metrics for performance monitoring +// - Automatic stale aircraft cleanup +// +// Thread safety is provided by RWMutex for concurrent read access while +// maintaining write consistency during updates. type Merger struct { - aircraft map[uint32]*AircraftState - sources map[string]*Source - mu sync.RWMutex - historyLimit int - staleTimeout time.Duration - updateMetrics map[uint32]*updateMetric + aircraft map[uint32]*AircraftState // ICAO24 -> merged aircraft state + sources map[string]*Source // Source ID -> source information + mu sync.RWMutex // Protects all maps and slices + historyLimit int // Maximum history points to retain + staleTimeout time.Duration // Time before aircraft considered stale + updateMetrics map[uint32]*updateMetric // ICAO24 -> update rate calculation data } +// updateMetric tracks recent update times for calculating update rates. +// Used internally to provide real-time update frequency information. type updateMetric struct { - lastUpdate time.Time - updates []time.Time + updates []time.Time // Recent update timestamps (last 30 seconds) } -// NewMerger creates a new aircraft data merger +// NewMerger creates a new aircraft data merger with default configuration. +// +// Default settings: +// - History limit: 500 points per aircraft +// - Stale timeout: 60 seconds +// - Empty aircraft and source maps +// - Update metrics tracking enabled +// +// The merger is ready for immediate use after creation. func NewMerger() *Merger { return &Merger{ aircraft: make(map[uint32]*AircraftState), @@ -105,14 +162,42 @@ func NewMerger() *Merger { } } -// AddSource registers a new data source +// AddSource registers a new data source with the merger. +// +// The source must have a unique ID and will be available for aircraft +// data updates immediately. Sources can be added at any time, even +// after the merger is actively processing data. +// +// Parameters: +// - source: Source configuration with ID, location, and connection details func (m *Merger) AddSource(source *Source) { m.mu.Lock() defer m.mu.Unlock() m.sources[source.ID] = source } -// UpdateAircraft merges new aircraft data from a 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 +// +// Data fusion strategies: +// - Position: Use source with strongest signal +// - Dynamic data: Prefer most recent updates +// - Quality indicators: Keep highest accuracy values +// - Identity: Use most recent non-empty values +// +// Parameters: +// - sourceID: Identifier of the source providing this data +// - aircraft: Decoded Mode S/ADS-B aircraft data +// - signal: Signal strength in dBFS +// - timestamp: When this data was received func (m *Merger) UpdateAircraft(sourceID string, aircraft *modes.Aircraft, signal float64, timestamp time.Time) { m.mu.Lock() defer m.mu.Unlock() @@ -177,7 +262,32 @@ func (m *Merger) UpdateAircraft(sourceID string, aircraft *modes.Aircraft, signa state.TotalMessages++ } -// mergeAircraftData intelligently merges data from multiple sources +// mergeAircraftData intelligently merges data from multiple sources with conflict resolution. +// +// This method implements the core data fusion logic: +// +// Position Data: +// - Uses source with strongest signal strength for best accuracy +// - Falls back to any available position if none exists +// - Tracks which source provided the current position +// +// Dynamic Data (altitude, speed, heading, vertical rate): +// - Always uses most recent data to reflect current aircraft state +// - Assumes more recent data is more accurate for rapidly changing values +// +// Identity Data (callsign, squawk, category): +// - Uses most recent non-empty values +// - Preserves existing values when new data is empty +// +// Quality Indicators (NACp, NACv, SIL): +// - Uses highest available accuracy values +// - Maintains best quality indicators across all sources +// +// Parameters: +// - state: Current merged aircraft state to update +// - new: New aircraft data from a source +// - 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 if new.Latitude != 0 && new.Longitude != 0 { @@ -269,7 +379,24 @@ func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, so } } -// updateHistories adds data points to history arrays +// updateHistories adds data points to historical tracking arrays. +// +// Maintains time-series data for: +// - Position trail for track visualization +// - Signal strength for coverage analysis +// - Altitude profile for flight analysis +// - Speed history for performance tracking +// +// Each history array is limited by historyLimit to prevent unbounded growth. +// Only non-zero values are recorded to avoid cluttering histories with +// invalid or missing data points. +// +// Parameters: +// - state: Aircraft state to update histories for +// - aircraft: New aircraft data containing values to record +// - sourceID: Source providing this data point +// - 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 if aircraft.Latitude != 0 && aircraft.Longitude != 0 { @@ -323,7 +450,21 @@ func (m *Merger) updateHistories(state *AircraftState, aircraft *modes.Aircraft, } } -// updateUpdateRate calculates message update rate +// 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 +// +// This provides real-time feedback on data quality and can help identify +// aircraft that are updating frequently (close, good signal) vs infrequently +// (distant, weak signal). +// +// Parameters: +// - icao: ICAO24 address of the aircraft +// - timestamp: Timestamp of this update func (m *Merger) updateUpdateRate(icao uint32, timestamp time.Time) { metric := m.updateMetrics[icao] metric.updates = append(metric.updates, timestamp) @@ -344,7 +485,16 @@ func (m *Merger) updateUpdateRate(icao uint32, timestamp time.Time) { } } -// getBestSignalSource returns the source ID with the strongest signal +// getBestSignalSource identifies the source with the strongest signal for this aircraft. +// +// Used in position data fusion to determine which source should provide +// the authoritative position. Sources with stronger signals typically +// provide more accurate position data. +// +// Parameters: +// - state: Aircraft state containing per-source signal data +// +// Returns the source ID with the highest signal level, or empty string if none. func (m *Merger) getBestSignalSource(state *AircraftState) string { var bestSource string var bestSignal float64 = -999 @@ -359,7 +509,18 @@ func (m *Merger) getBestSignalSource(state *AircraftState) string { return bestSource } -// GetAircraft returns current aircraft states +// 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 +// +// The returned map uses ICAO24 addresses as keys and can be safely +// used by multiple goroutines without affecting the internal state. +// +// Returns a map of ICAO24 -> AircraftState for all non-stale aircraft. func (m *Merger) GetAircraft() map[uint32]*AircraftState { m.mu.RLock() defer m.mu.RUnlock() @@ -394,7 +555,12 @@ func (m *Merger) GetAircraft() map[uint32]*AircraftState { return result } -// GetSources returns all registered sources +// GetSources returns all registered data sources. +// +// Provides access to source configuration, status, and statistics. +// Used by the web API to display source information and connection status. +// +// Returns a slice of all registered sources (active and inactive). func (m *Merger) GetSources() []*Source { m.mu.RLock() defer m.mu.RUnlock() @@ -406,7 +572,18 @@ func (m *Merger) GetSources() []*Source { return sources } -// GetStatistics returns merger statistics +// GetStatistics returns comprehensive merger and system statistics. +// +// The statistics include: +// - total_aircraft: Current number of tracked aircraft +// - total_messages: Sum of all messages processed +// - active_sources: Number of currently connected sources +// - aircraft_by_sources: Distribution of aircraft by number of tracking sources +// +// The aircraft_by_sources map shows data quality - aircraft tracked by +// multiple sources generally have better position accuracy and reliability. +// +// Returns a map suitable for JSON serialization and web display. func (m *Merger) GetStatistics() map[string]interface{} { m.mu.RLock() defer m.mu.RUnlock() @@ -435,7 +612,14 @@ func (m *Merger) GetStatistics() map[string]interface{} { } } -// CleanupStale removes stale aircraft +// CleanupStale removes aircraft that haven't been updated recently. +// +// Aircraft are considered stale if they haven't received updates for longer +// than staleTimeout (default 60 seconds). This cleanup prevents memory +// growth from aircraft that have left the coverage area or stopped transmitting. +// +// The cleanup also removes associated update metrics to free memory. +// This method is typically called periodically by the client manager. func (m *Merger) CleanupStale() { m.mu.Lock() defer m.mu.Unlock() @@ -449,8 +633,23 @@ func (m *Merger) CleanupStale() { } } -// Helper functions - +// calculateDistanceBearing computes great circle distance and bearing between two points. +// +// Uses the Haversine formula for distance calculation and forward azimuth +// for bearing calculation. Both calculations account for Earth's spherical +// nature for accuracy over long distances. +// +// Distance is calculated in kilometers, bearing in degrees (0-360° from North). +// This is used to calculate aircraft distance from receivers and for +// coverage analysis. +// +// Parameters: +// - lat1, lon1: First point (receiver) coordinates in decimal degrees +// - lat2, lon2: Second point (aircraft) coordinates in decimal degrees +// +// Returns: +// - distance: Great circle distance in kilometers +// - bearing: Forward azimuth in degrees (0° = North, 90° = East) func calculateDistanceBearing(lat1, lon1, lat2, lon2 float64) (float64, float64) { // Haversine formula for distance const R = 6371.0 // Earth radius in km diff --git a/internal/modes/decoder.go b/internal/modes/decoder.go index 49bd32d..9d59cc1 100644 --- a/internal/modes/decoder.go +++ b/internal/modes/decoder.go @@ -1,3 +1,30 @@ +// Package modes provides Mode S and ADS-B message decoding capabilities. +// +// Mode S is a secondary surveillance radar system that enables aircraft to transmit +// detailed information including position, altitude, velocity, and identification. +// ADS-B (Automatic Dependent Surveillance-Broadcast) is a modernization of Mode S +// that provides more precise and frequent position reports. +// +// This package implements: +// - Complete Mode S message decoding for all downlink formats +// - ADS-B extended squitter message parsing (DF17/18) +// - CPR (Compact Position Reporting) position decoding algorithm +// - Aircraft identification, category, and status decoding +// - Velocity and heading calculation from velocity messages +// - Navigation accuracy and integrity decoding +// +// Key Features: +// - CPR Global Position Decoding: Resolves ambiguous encoded positions using +// even/odd message pairs and trigonometric calculations +// - Multi-format Support: Handles surveillance replies, extended squitter, +// and various ADS-B message types +// - Real-time Processing: Maintains state for CPR decoding across messages +// - Comprehensive Data Extraction: Extracts all available aircraft parameters +// +// CPR Algorithm: +// The Compact Position Reporting format encodes latitude and longitude using +// two alternating formats (even/odd) that create overlapping grids. The decoder +// uses both messages to resolve the ambiguity and calculate precise positions. package modes import ( @@ -5,72 +32,109 @@ import ( "math" ) -// Downlink formats +// Mode S Downlink Format (DF) constants. +// The DF field (first 5 bits) determines the message type and structure. const ( - DF0 = 0 // Short air-air surveillance - DF4 = 4 // Surveillance altitude reply - DF5 = 5 // Surveillance identity reply - DF11 = 11 // All-call reply - DF16 = 16 // Long air-air surveillance - DF17 = 17 // Extended squitter - DF18 = 18 // Extended squitter/non-transponder + DF0 = 0 // Short air-air surveillance (ACAS) + DF4 = 4 // Surveillance altitude reply (interrogation response) + DF5 = 5 // Surveillance identity reply (squawk code response) + DF11 = 11 // All-call reply (capability and ICAO address) + DF16 = 16 // Long air-air surveillance (ACAS with altitude) + DF17 = 17 // Extended squitter (ADS-B from transponder) + DF18 = 18 // Extended squitter/non-transponder (ADS-B from other sources) DF19 = 19 // Military extended squitter - DF20 = 20 // Comm-B altitude reply - DF21 = 21 // Comm-B identity reply - DF24 = 24 // Comm-D (ELM) + DF20 = 20 // Comm-B altitude reply (BDS register data) + DF21 = 21 // Comm-B identity reply (BDS register data) + DF24 = 24 // Comm-D (ELM - Enhanced Length Message) ) -// Type codes for DF17/18 messages +// ADS-B Type Code (TC) constants for DF17/18 extended squitter messages. +// The type code (bits 32-36) determines the content and format of ADS-B messages. const ( - TC_IDENT_CATEGORY = 1 // Aircraft identification and category - TC_SURFACE_POS = 5 // Surface position - TC_AIRBORNE_POS_9 = 9 // Airborne position (w/ barometric altitude) - TC_AIRBORNE_POS_20 = 20 // Airborne position (w/ GNSS height) - TC_AIRBORNE_VEL = 19 // Airborne velocity - TC_AIRBORNE_POS_GPS = 22 // Airborne position (GNSS) - TC_RESERVED = 23 // Reserved + TC_IDENT_CATEGORY = 1 // Aircraft identification and category (callsign) + TC_SURFACE_POS = 5 // Surface position (airport ground movement) + TC_AIRBORNE_POS_9 = 9 // Airborne position (barometric altitude) + TC_AIRBORNE_POS_20 = 20 // Airborne position (GNSS height above ellipsoid) + TC_AIRBORNE_VEL = 19 // Airborne velocity (ground speed and track) + TC_AIRBORNE_POS_GPS = 22 // Airborne position (GNSS altitude) + TC_RESERVED = 23 // Reserved for future use TC_SURFACE_SYSTEM = 24 // Surface system status - TC_OPERATIONAL = 31 // Aircraft operational status + TC_OPERATIONAL = 31 // Aircraft operational status (capabilities) ) -// Aircraft represents decoded aircraft data +// Aircraft represents a complete set of decoded aircraft data from Mode S/ADS-B messages. +// +// This structure contains all possible information that can be extracted from +// various Mode S and ADS-B message types, including position, velocity, status, +// and navigation data. Not all fields will be populated for every aircraft, +// depending on the messages received and aircraft capabilities. type Aircraft struct { - ICAO24 uint32 // 24-bit ICAO address - Callsign string // 8-character callsign - Latitude float64 // Decimal degrees - Longitude float64 // Decimal degrees - Altitude int // Feet - VerticalRate int // Feet/minute - GroundSpeed float64 // Knots - Track float64 // Degrees - Heading float64 // Degrees (magnetic) - Category string // Aircraft category - Emergency string // Emergency/priority status - Squawk string // 4-digit squawk code - OnGround bool - Alert bool - SPI bool // Special Position Identification - NACp uint8 // Navigation Accuracy Category - Position - NACv uint8 // Navigation Accuracy Category - Velocity - SIL uint8 // Surveillance Integrity Level - BaroAltitude int // Barometric altitude - GeomAltitude int // Geometric altitude - SelectedAltitude int // MCP/FCU selected altitude - SelectedHeading float64 // MCP/FCU selected heading - BaroSetting float64 // QNH in millibars + // Core Identification + 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) + + // Motion and Dynamics + VerticalRate int // Vertical rate in feet per minute (climb/descent) + GroundSpeed float64 // Ground speed in knots + Track float64 // Track angle in degrees (direction of movement) + Heading float64 // Aircraft heading in degrees (magnetic) + + // Aircraft Information + 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) + + // 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) + + // Autopilot/Flight Management + SelectedAltitude int // MCP/FCU selected altitude in feet + SelectedHeading float64 // MCP/FCU selected heading in degrees + BaroSetting float64 // Barometric pressure setting (QNH) in millibars } -// Decoder handles Mode S message decoding +// Decoder handles Mode S and ADS-B message decoding with CPR position resolution. +// +// The decoder maintains state for CPR (Compact Position Reporting) decoding, +// which requires pairs of even/odd messages to resolve position ambiguity. +// Each aircraft (identified by ICAO24) has separate CPR state tracking. +// +// CPR Position Decoding: +// Aircraft positions are encoded using two alternating formats that create +// overlapping latitude/longitude grids. The decoder stores both even and odd +// encoded positions and uses trigonometric calculations to resolve the +// actual aircraft position when both are available. type Decoder struct { - cprEvenLat map[uint32]float64 - cprEvenLon map[uint32]float64 - cprOddLat map[uint32]float64 - cprOddLon map[uint32]float64 - cprEvenTime map[uint32]int64 - cprOddTime map[uint32]int64 + // CPR (Compact Position Reporting) state tracking per aircraft + cprEvenLat map[uint32]float64 // Even message latitude encoding (ICAO24 -> normalized lat) + cprEvenLon map[uint32]float64 // Even message longitude encoding (ICAO24 -> normalized lon) + cprOddLat map[uint32]float64 // Odd message latitude encoding (ICAO24 -> normalized lat) + 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) } -// NewDecoder creates a new Mode S decoder +// NewDecoder creates a new Mode S/ADS-B decoder with initialized CPR tracking. +// +// The decoder is ready to process Mode S messages immediately and will +// maintain CPR position state across multiple messages for accurate +// position decoding. +// +// Returns a configured decoder ready for message processing. func NewDecoder() *Decoder { return &Decoder{ cprEvenLat: make(map[uint32]float64), @@ -82,7 +146,23 @@ func NewDecoder() *Decoder { } } -// Decode processes a Mode S message +// 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 +// +// Different message types provide different information: +// - DF4/20: Altitude only +// - DF5/21: Squawk code only +// - DF17/18: Complete ADS-B data (position, velocity, identification, etc.) +// +// Parameters: +// - data: Raw Mode S message bytes (7 or 14 bytes depending on type) +// +// Returns decoded Aircraft struct or error for invalid/incomplete messages. func (d *Decoder) Decode(data []byte) (*Aircraft, error) { if len(data) < 7 { return nil, fmt.Errorf("message too short: %d bytes", len(data)) @@ -107,13 +187,41 @@ func (d *Decoder) Decode(data []byte) (*Aircraft, error) { return aircraft, nil } -// extractICAO extracts the ICAO address based on downlink format +// extractICAO extracts the ICAO 24-bit aircraft address from Mode S messages. +// +// For most downlink formats, the ICAO address is located in bytes 1-3 of the +// message. Some formats may have different layouts, but this implementation +// uses the standard position for all supported formats. +// +// Parameters: +// - data: Mode S message bytes +// - df: Downlink format (currently unused, but available for format-specific handling) +// +// Returns the 24-bit ICAO address as a uint32. func (d *Decoder) extractICAO(data []byte, df uint8) uint32 { // For most formats, ICAO is in bytes 1-3 return uint32(data[1])<<16 | uint32(data[2])<<8 | uint32(data[3]) } -// decodeExtendedSquitter handles DF17/18 extended squitter messages +// decodeExtendedSquitter processes ADS-B extended squitter messages (DF17/18). +// +// Extended squitter messages contain the richest aircraft data, including: +// - Aircraft identification and category (TC 1-4) +// - Surface position and movement (TC 5-8) +// - Airborne position with various altitude sources (TC 9-18, 20-22) +// - Velocity and heading information (TC 19) +// - Aircraft status and emergency codes (TC 28) +// - Target state and autopilot settings (TC 29) +// - Operational status and navigation accuracy (TC 31) +// +// The method routes messages to specific decoders based on the Type Code (TC) +// field in bits 32-36 of the message. +// +// Parameters: +// - data: 14-byte extended squitter message +// - aircraft: Aircraft struct to populate with decoded data +// +// Returns the updated Aircraft struct or an error for malformed messages. func (d *Decoder) decodeExtendedSquitter(data []byte, aircraft *Aircraft) (*Aircraft, error) { if len(data) < 14 { return nil, fmt.Errorf("extended squitter too short: %d bytes", len(data)) @@ -151,7 +259,21 @@ func (d *Decoder) decodeExtendedSquitter(data []byte, aircraft *Aircraft) (*Airc return aircraft, nil } -// decodeIdentification extracts callsign and category +// decodeIdentification extracts aircraft callsign and category from identification messages. +// +// Aircraft identification messages (TC 1-4) contain: +// - 8-character callsign encoded in 6-bit characters +// - Aircraft category indicating size, performance, and type +// +// Callsign Encoding: +// Each character is encoded in 6 bits using a custom character set: +// - Characters: "#ABCDEFGHIJKLMNOPQRSTUVWXYZ##### ###############0123456789######" +// - Index 0-63 maps to the character at that position +// - '#' represents space or invalid characters +// +// Parameters: +// - data: Extended squitter message containing identification data +// - aircraft: Aircraft struct to update with callsign and category func (d *Decoder) decodeIdentification(data []byte, aircraft *Aircraft) { tc := (data[4] >> 3) & 0x1F @@ -184,7 +306,25 @@ func (d *Decoder) decodeIdentification(data []byte, aircraft *Aircraft) { aircraft.Callsign = callsign } -// decodeAirbornePosition extracts position from CPR encoded data +// decodeAirbornePosition extracts aircraft position from CPR-encoded position messages. +// +// Airborne position messages (TC 9-18, 20-22) contain: +// - Altitude information (barometric or geometric) +// - CPR-encoded latitude and longitude +// - 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 +// +// The actual position calculation requires both even and odd messages to +// resolve the ambiguity inherent in the compressed encoding format. +// +// Parameters: +// - data: Extended squitter message containing position data +// - aircraft: Aircraft struct to update with position and altitude func (d *Decoder) decodeAirbornePosition(data []byte, aircraft *Aircraft) { tc := (data[4] >> 3) & 0x1F @@ -210,7 +350,29 @@ func (d *Decoder) decodeAirbornePosition(data []byte, aircraft *Aircraft) { d.decodeCPRPosition(aircraft) } -// decodeCPRPosition performs CPR global decoding +// decodeCPRPosition performs CPR (Compact Position Reporting) global position decoding. +// +// This is the core algorithm for resolving aircraft positions from CPR-encoded data. +// The algorithm requires both even and odd CPR messages to resolve position ambiguity. +// +// CPR Global Decoding Algorithm: +// 1. Check that both even and odd CPR values are available +// 2. Calculate latitude using even/odd zone boundaries +// 3. Determine which latitude zone contains the aircraft +// 4. Calculate longitude based on the resolved latitude +// 5. Apply range corrections to get final position +// +// Mathematical Process: +// - Latitude zones are spaced 360°/60 = 6° apart for even messages +// - Latitude zones are spaced 360°/59 = ~6.1° apart for odd messages +// - The zone offset calculation resolves which 6° band contains the aircraft +// - Longitude calculation depends on latitude due to Earth's spherical geometry +// +// Note: This implementation uses a simplified approach. Production systems +// should also consider message timestamps to choose the most recent position. +// +// Parameters: +// - aircraft: Aircraft struct to update with decoded position func (d *Decoder) decodeCPRPosition(aircraft *Aircraft) { evenLat, evenExists := d.cprEvenLat[aircraft.ICAO24] oddLat, oddExists := d.cprOddLat[aircraft.ICAO24] @@ -253,7 +415,24 @@ func (d *Decoder) decodeCPRPosition(aircraft *Aircraft) { aircraft.Longitude = lon } -// nlFunction calculates the number of longitude zones +// nlFunction calculates the number of longitude zones (NL) for a given latitude. +// +// This function implements the NL(lat) calculation defined in the CPR specification. +// The number of longitude zones decreases as latitude approaches the poles due to +// the convergence of meridians. +// +// Mathematical Background: +// - At the equator: 60 longitude zones (6° each) +// - At higher latitudes: fewer zones as meridians converge +// - At poles (±87°): only 2 zones (180° each) +// +// Formula: NL(lat) = floor(2π / arccos(1 - (1-cos(π/(2*NZ))) / cos²(lat))) +// Where NZ = 15 (number of latitude zones) +// +// Parameters: +// - lat: Latitude in decimal degrees +// +// Returns the number of longitude zones for this latitude. func (d *Decoder) nlFunction(lat float64) float64 { if math.Abs(lat) >= 87 { return 2 @@ -267,7 +446,26 @@ func (d *Decoder) nlFunction(lat float64) float64 { return math.Floor(nl) } -// decodeVelocity extracts speed and heading +// decodeVelocity extracts ground speed, track, and vertical rate from velocity messages. +// +// Velocity messages (TC 19) contain: +// - Ground speed components (East-West and North-South) +// - Vertical rate (climb/descent rate) +// - Intent change flag and other status bits +// +// Ground Speed Calculation: +// - East-West and North-South velocity components are encoded separately +// - Each component has a direction bit and magnitude +// - Ground speed = sqrt(EW² + NS²) +// - Track angle = atan2(EW, NS) converted to degrees +// +// Vertical Rate: +// - Encoded in 64 ft/min increments with sign bit +// - Range: approximately ±32,000 ft/min +// +// Parameters: +// - data: Extended squitter message containing velocity data +// - aircraft: Aircraft struct to update with velocity information func (d *Decoder) decodeVelocity(data []byte, aircraft *Aircraft) { subtype := (data[4]) & 0x07 @@ -304,13 +502,37 @@ func (d *Decoder) decodeVelocity(data []byte, aircraft *Aircraft) { } } -// decodeAltitude extracts altitude from Mode S altitude reply +// decodeAltitude extracts altitude from Mode S surveillance altitude replies. +// +// Mode S altitude replies (DF4, DF20) contain a 13-bit altitude code that +// must be converted from the transmitted encoding to actual altitude in feet. +// +// Parameters: +// - data: Mode S altitude reply message +// +// Returns altitude in feet above sea level. func (d *Decoder) decodeAltitude(data []byte) int { altCode := uint16(data[2])<<8 | uint16(data[3]) return d.decodeAltitudeBits(altCode>>3, 0) } -// decodeAltitudeBits converts altitude code to feet +// decodeAltitudeBits converts encoded altitude bits to altitude in feet. +// +// Altitude Encoding: +// - Uses modified Gray code for error resilience +// - 12-bit altitude code with 25-foot increments +// - Offset of -1000 feet (code 0 = -1000 ft) +// - Gray code conversion prevents single-bit errors from causing large altitude jumps +// +// Different altitude sources: +// - Standard: Barometric altitude (QNH corrected) +// - GNSS: Geometric altitude (height above WGS84 ellipsoid) +// +// Parameters: +// - altCode: 12-bit encoded altitude value +// - tc: Type code (determines altitude source interpretation) +// +// Returns altitude in feet, or 0 for invalid altitude codes. func (d *Decoder) decodeAltitudeBits(altCode uint16, tc uint8) int { if altCode == 0 { return 0 @@ -332,13 +554,41 @@ func (d *Decoder) decodeAltitudeBits(altCode uint16, tc uint8) int { return alt } -// decodeSquawk extracts squawk code +// decodeSquawk extracts the 4-digit squawk (transponder) code from identity replies. +// +// Squawk codes are 4-digit octal numbers (0000-7777) used by air traffic control +// for aircraft identification. They are transmitted in surveillance identity +// replies (DF5, DF21) and formatted as octal strings. +// +// Parameters: +// - data: Mode S identity reply message +// +// Returns 4-digit octal squawk code as a string (e.g., "1200", "7700"). func (d *Decoder) decodeSquawk(data []byte) string { code := uint16(data[2])<<8 | uint16(data[3]) return fmt.Sprintf("%04o", code>>3) } -// getAircraftCategory returns human-readable aircraft category +// getAircraftCategory converts type code and category fields to human-readable descriptions. +// +// Aircraft categories are encoded in identification messages using: +// - Type Code (TC): Broad category group (1-4) +// - Category (CA): Specific category within the group (0-7) +// +// Categories include: +// - TC 1: Reserved +// - TC 2: Surface vehicles (emergency, service, obstacles) +// - TC 3: Light aircraft (gliders, balloons, UAVs, etc.) +// - TC 4: Aircraft by weight class and performance +// +// These categories help ATC and other aircraft understand the type of vehicle +// and its performance characteristics for separation and routing. +// +// Parameters: +// - tc: Type code (1-4) from identification message +// - ca: Category field (0-7) providing specific subtype +// +// Returns human-readable category description. func (d *Decoder) getAircraftCategory(tc uint8, ca uint8) string { switch tc { case 1: @@ -395,7 +645,22 @@ func (d *Decoder) getAircraftCategory(tc uint8, ca uint8) string { } } -// decodeStatus handles aircraft status messages +// decodeStatus extracts emergency and priority status from aircraft status messages. +// +// Aircraft status messages (TC 28) contain emergency and priority codes that +// indicate special situations requiring ATC attention: +// - General emergency (Mayday) +// - Medical emergency (Lifeguard) +// - Minimum fuel +// - Communication failure +// - Unlawful interference (hijack) +// - Downed aircraft +// +// These codes trigger special handling by ATC and emergency services. +// +// Parameters: +// - data: Extended squitter message containing status information +// - aircraft: Aircraft struct to update with emergency status func (d *Decoder) decodeStatus(data []byte, aircraft *Aircraft) { subtype := data[4] & 0x07 @@ -421,7 +686,20 @@ func (d *Decoder) decodeStatus(data []byte, aircraft *Aircraft) { } } -// decodeTargetState handles target state and status messages +// decodeTargetState extracts autopilot and flight management system settings. +// +// Target state and status messages (TC 29) contain information about: +// - Selected altitude (MCP/FCU setting) +// - Barometric pressure setting (QNH) +// - Autopilot engagement status +// - Flight management system intentions +// +// This information helps ATC understand pilot intentions and autopilot settings, +// improving situational awareness and conflict prediction. +// +// Parameters: +// - data: Extended squitter message containing target state data +// - aircraft: Aircraft struct to update with autopilot settings func (d *Decoder) decodeTargetState(data []byte, aircraft *Aircraft) { // Selected altitude altBits := uint16(data[5]&0x7F)<<4 | uint16(data[6])>>4 @@ -436,7 +714,19 @@ func (d *Decoder) decodeTargetState(data []byte, aircraft *Aircraft) { } } -// decodeOperationalStatus handles operational status messages +// decodeOperationalStatus extracts navigation accuracy and system capability information. +// +// Operational status messages (TC 31) contain: +// - Navigation Accuracy Category for Position (NACp): Position 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 +// appropriate separation standards for the aircraft. +// +// Parameters: +// - data: Extended squitter message containing operational status +// - aircraft: Aircraft struct to update with accuracy indicators func (d *Decoder) decodeOperationalStatus(data []byte, aircraft *Aircraft) { // Navigation accuracy categories aircraft.NACp = (data[7] >> 4) & 0x0F @@ -444,7 +734,22 @@ func (d *Decoder) decodeOperationalStatus(data []byte, aircraft *Aircraft) { aircraft.SIL = (data[8] >> 6) & 0x03 } -// decodeSurfacePosition handles surface position messages +// decodeSurfacePosition extracts position and movement data for aircraft on the ground. +// +// Surface position messages (TC 5-8) are used for airport ground movement tracking: +// - Ground speed and movement direction +// - Track angle (direction of movement) +// - CPR-encoded position (same algorithm as airborne) +// - On-ground flag is automatically set +// +// Ground Movement Encoding: +// - Speed ranges from stationary to 175+ knots in non-linear increments +// - Track is encoded in 128 discrete directions (2.8125° resolution) +// - Position uses the same CPR encoding as airborne messages +// +// Parameters: +// - data: Extended squitter message containing surface position data +// - aircraft: Aircraft struct to update with ground movement information func (d *Decoder) decodeSurfacePosition(data []byte, aircraft *Aircraft) { aircraft.OnGround = true @@ -477,7 +782,24 @@ func (d *Decoder) decodeSurfacePosition(data []byte, aircraft *Aircraft) { d.decodeCPRPosition(aircraft) } -// decodeGroundSpeed converts movement field to ground speed +// decodeGroundSpeed converts the surface movement field to ground speed in knots. +// +// Surface movement is encoded in non-linear ranges optimized for typical +// ground operations: +// - 0: No movement information +// - 1: Stationary +// - 2-8: 0.125-1.0 kt (fine resolution for slow movement) +// - 9-12: 1.0-2.0 kt (taxi speeds) +// - 13-38: 2.0-15.0 kt (normal taxi) +// - 39-93: 15.0-70.0 kt (high speed taxi/runway) +// - 94-108: 70.0-100.0 kt (takeoff/landing roll) +// - 109-123: 100.0-175.0 kt (high speed operations) +// - 124: >175 kt +// +// Parameters: +// - movement: 7-bit movement field from surface position message +// +// Returns ground speed in knots, or 0 for invalid/no movement. func (d *Decoder) decodeGroundSpeed(movement uint8) float64 { if movement == 1 { return 0 diff --git a/internal/parser/sbs1.go b/internal/parser/sbs1.go deleted file mode 100644 index 873c764..0000000 --- a/internal/parser/sbs1.go +++ /dev/null @@ -1,224 +0,0 @@ -package parser - -import ( - "strconv" - "strings" - "time" -) - -type TrackPoint struct { - Timestamp time.Time `json:"timestamp"` - Latitude float64 `json:"lat"` - Longitude float64 `json:"lon"` - Altitude int `json:"altitude"` - Speed int `json:"speed"` - Track int `json:"track"` -} - -type Aircraft struct { - Hex string `json:"hex"` - Flight string `json:"flight,omitempty"` - Altitude int `json:"alt_baro,omitempty"` - GroundSpeed int `json:"gs,omitempty"` - Track int `json:"track,omitempty"` - Latitude float64 `json:"lat,omitempty"` - Longitude float64 `json:"lon,omitempty"` - VertRate int `json:"vert_rate,omitempty"` - Squawk string `json:"squawk,omitempty"` - Emergency bool `json:"emergency,omitempty"` - OnGround bool `json:"on_ground,omitempty"` - LastSeen time.Time `json:"last_seen"` - Messages int `json:"messages"` - TrackHistory []TrackPoint `json:"track_history,omitempty"` - RSSI float64 `json:"rssi,omitempty"` - Country string `json:"country,omitempty"` - Registration string `json:"registration,omitempty"` -} - -type AircraftData struct { - Now int64 `json:"now"` - Messages int `json:"messages"` - Aircraft map[string]Aircraft `json:"aircraft"` -} - -func ParseSBS1Line(line string) (*Aircraft, error) { - parts := strings.Split(strings.TrimSpace(line), ",") - if len(parts) < 22 { - return nil, nil - } - - // messageType := parts[1] - // Accept all message types to get complete data - // MSG types: 1=ES_IDENT_AND_CATEGORY, 2=ES_SURFACE_POS, 3=ES_AIRBORNE_POS - // 4=ES_AIRBORNE_VEL, 5=SURVEILLANCE_ALT, 6=SURVEILLANCE_ID, 7=AIR_TO_AIR, 8=ALL_CALL_REPLY - - aircraft := &Aircraft{ - Hex: strings.TrimSpace(parts[4]), - LastSeen: time.Now(), - Messages: 1, - } - - // Different message types contain different fields - // Always try to extract what's available - if parts[10] != "" { - aircraft.Flight = strings.TrimSpace(parts[10]) - } - - if parts[11] != "" { - if alt, err := strconv.Atoi(parts[11]); err == nil { - aircraft.Altitude = alt - } - } - - if parts[12] != "" { - if gs, err := strconv.Atoi(parts[12]); err == nil { - aircraft.GroundSpeed = gs - } - } - - if parts[13] != "" { - if track, err := strconv.ParseFloat(parts[13], 64); err == nil { - aircraft.Track = int(track) - } - } - - if parts[14] != "" && parts[15] != "" { - if lat, err := strconv.ParseFloat(parts[14], 64); err == nil { - aircraft.Latitude = lat - } - if lon, err := strconv.ParseFloat(parts[15], 64); err == nil { - aircraft.Longitude = lon - } - } - - if parts[16] != "" { - if vr, err := strconv.Atoi(parts[16]); err == nil { - aircraft.VertRate = vr - } - } - - if parts[17] != "" { - aircraft.Squawk = strings.TrimSpace(parts[17]) - } - - if parts[21] != "" { - aircraft.OnGround = parts[21] == "1" - } - - aircraft.Country = getCountryFromICAO(aircraft.Hex) - aircraft.Registration = getRegistrationFromICAO(aircraft.Hex) - - return aircraft, nil -} - -func getCountryFromICAO(icao string) string { - if len(icao) < 6 { - return "Unknown" - } - - prefix := icao[:1] - - switch prefix { - case "4": - return getCountryFrom4xxxx(icao) - case "A": - return "United States" - case "C": - return "Canada" - case "D": - return "Germany" - case "F": - return "France" - case "G": - return "United Kingdom" - case "I": - return "Italy" - case "J": - return "Japan" - case "P": - return getPCountry(icao) - case "S": - return getSCountry(icao) - case "O": - return getOCountry(icao) - default: - return "Unknown" - } -} - -func getCountryFrom4xxxx(icao string) string { - if len(icao) >= 2 { - switch icao[:2] { - case "40": - return "United Kingdom" - case "44": - return "Austria" - case "45": - return "Denmark" - case "46": - return "Germany" - case "47": - return "Germany" - case "48": - return "Netherlands" - case "49": - return "Netherlands" - } - } - return "Europe" -} - -func getPCountry(icao string) string { - if len(icao) >= 2 { - switch icao[:2] { - case "PH": - return "Netherlands" - case "PJ": - return "Netherlands Antilles" - } - } - return "Unknown" -} - -func getSCountry(icao string) string { - if len(icao) >= 2 { - switch icao[:2] { - case "SE": - return "Sweden" - case "SX": - return "Greece" - } - } - return "Unknown" -} - -func getOCountry(icao string) string { - if len(icao) >= 2 { - switch icao[:2] { - case "OO": - return "Belgium" - case "OH": - return "Finland" - } - } - return "Unknown" -} - -func getRegistrationFromICAO(icao string) string { - // This is a simplified conversion - real registration lookup would need a database - country := getCountryFromICAO(icao) - switch country { - case "Germany": - return "D-" + icao[2:] - case "United Kingdom": - return "G-" + icao[2:] - case "France": - return "F-" + icao[2:] - case "Netherlands": - return "PH-" + icao[2:] - case "Sweden": - return "SE-" + icao[2:] - default: - return icao - } -} diff --git a/internal/server/server.go b/internal/server/server.go index 6f974f8..e972c49 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,3 +1,16 @@ +// Package server provides HTTP and WebSocket services for the SkyView application. +// +// This package implements the web server that serves both static assets and real-time +// aircraft data via REST API endpoints and WebSocket connections. It handles: +// - Static web file serving from embedded assets +// - RESTful API endpoints for aircraft, sources, and statistics +// - Real-time WebSocket streaming for live aircraft updates +// - CORS handling for cross-origin requests +// - Coverage and heatmap data generation for visualization +// +// The server integrates with the merger component to access consolidated aircraft +// data from multiple sources and provides various data formats optimized for +// web consumption. package server import ( @@ -18,46 +31,74 @@ import ( "skyview/internal/merger" ) -// OriginConfig represents the reference point configuration +// OriginConfig represents the geographical reference point configuration. +// 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"` - Longitude float64 `json:"longitude"` - Name string `json:"name,omitempty"` + 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 } -// Server handles HTTP requests and WebSocket connections +// Server handles HTTP requests and WebSocket connections for the SkyView web interface. +// It serves static web assets, provides RESTful API endpoints for aircraft data, +// and maintains real-time WebSocket connections for live updates. +// +// The server architecture uses: +// - Gorilla mux for HTTP routing +// - Gorilla WebSocket for real-time communication +// - Embedded filesystem for static asset serving +// - Concurrent broadcast system for WebSocket clients +// - CORS support for cross-origin web applications type Server struct { - port int - merger *merger.Merger - staticFiles embed.FS - server *http.Server - origin OriginConfig + 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 - wsClientsMu sync.RWMutex - upgrader websocket.Upgrader + wsClients map[*websocket.Conn]bool // Active WebSocket client connections + wsClientsMu sync.RWMutex // Protects wsClients map + upgrader websocket.Upgrader // HTTP to WebSocket protocol upgrader - // Broadcast channels - broadcastChan chan []byte - stopChan chan struct{} + // Broadcast channels for real-time updates + broadcastChan chan []byte // Channel for broadcasting updates to all clients + stopChan chan struct{} // Shutdown signal channel } -// WebSocketMessage represents messages sent over WebSocket +// WebSocketMessage represents the standard message format for WebSocket communication. +// All messages sent to clients follow this structure to provide consistent +// message handling and enable message type discrimination on the client side. type WebSocketMessage struct { - Type string `json:"type"` - Timestamp int64 `json:"timestamp"` - Data interface{} `json:"data"` + Type string `json:"type"` // Message type ("initial_data", "aircraft_update", etc.) + Timestamp int64 `json:"timestamp"` // Unix timestamp when message was created + Data interface{} `json:"data"` // Message payload (varies by type) } -// AircraftUpdate represents aircraft data for WebSocket +// AircraftUpdate represents the complete aircraft data payload sent via WebSocket. +// This structure contains all information needed by the web interface to display +// current aircraft positions, source status, and system statistics. type AircraftUpdate struct { - Aircraft map[string]*merger.AircraftState `json:"aircraft"` - Sources []*merger.Source `json:"sources"` - Stats map[string]interface{} `json:"stats"` + Aircraft map[string]*merger.AircraftState `json:"aircraft"` // Current aircraft keyed by ICAO hex string + Sources []*merger.Source `json:"sources"` // Active data sources with status + Stats map[string]interface{} `json:"stats"` // System statistics and metrics } -// NewServer creates a new HTTP server +// 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) +// - Buffered broadcast channel for efficient message distribution +// - Read/Write buffers optimized for aircraft data messages +// +// Parameters: +// - 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 { return &Server{ port: port, @@ -77,7 +118,17 @@ func NewServer(port int, merger *merger.Merger, staticFiles embed.FS, origin Ori } } -// Start starts the HTTP server +// 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 +// +// The method blocks until the server encounters an error or is shut down. +// Use Stop() for graceful shutdown. +// +// Returns an error if the server fails to start or encounters a fatal error. func (s *Server) Start() error { // Start broadcast routine go s.broadcastRoutine() @@ -96,7 +147,14 @@ func (s *Server) Start() error { return s.server.ListenAndServe() } -// Stop gracefully stops the server +// 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 +// +// The shutdown is designed to be safe and allow in-flight requests to complete. func (s *Server) Stop() { close(s.stopChan) @@ -107,6 +165,17 @@ func (s *Server) Stop() { } } +// setupRoutes configures the HTTP routing for all server endpoints. +// +// The routing structure includes: +// - /api/* - RESTful API endpoints for data access +// - /ws - WebSocket endpoint for real-time updates +// - /static/* - Static file serving +// - / - Main application page +// +// All routes are wrapped with CORS middleware for cross-origin support. +// +// Returns a configured HTTP handler ready for use with the HTTP server. func (s *Server) setupRoutes() http.Handler { router := mux.NewRouter() @@ -134,6 +203,16 @@ func (s *Server) setupRoutes() http.Handler { return s.enableCORS(router) } +// handleGetAircraft serves the /api/aircraft endpoint. +// Returns all currently tracked aircraft with their latest state information. +// +// The response includes: +// - timestamp: Unix timestamp of the response +// - aircraft: Map of aircraft keyed by ICAO hex strings +// - count: Total number of aircraft +// +// Aircraft ICAO addresses are converted from uint32 to 6-digit hex strings +// for consistent JSON representation (e.g., 0xABC123 -> "ABC123"). func (s *Server) handleGetAircraft(w http.ResponseWriter, r *http.Request) { aircraft := s.merger.GetAircraft() @@ -153,6 +232,14 @@ func (s *Server) handleGetAircraft(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(response) } +// handleGetAircraftDetails serves the /api/aircraft/{icao} endpoint. +// Returns detailed information for a specific aircraft identified by ICAO address. +// +// The ICAO parameter should be a 6-digit hexadecimal string (e.g., "ABC123"). +// Returns 400 Bad Request for invalid ICAO format. +// Returns 404 Not Found if the aircraft is not currently tracked. +// +// On success, returns the complete AircraftState for the requested aircraft. func (s *Server) handleGetAircraftDetails(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) icaoStr := vars["icao"] @@ -173,6 +260,15 @@ func (s *Server) handleGetAircraftDetails(w http.ResponseWriter, r *http.Request } } +// handleGetSources serves the /api/sources endpoint. +// Returns information about all configured data sources and their current status. +// +// The response includes: +// - sources: Array of source configurations with connection status +// - count: Total number of configured sources +// +// This endpoint is useful for monitoring source connectivity and debugging +// multi-source setups. func (s *Server) handleGetSources(w http.ResponseWriter, r *http.Request) { sources := s.merger.GetSources() @@ -183,6 +279,16 @@ func (s *Server) handleGetSources(w http.ResponseWriter, r *http.Request) { }) } +// handleGetStats serves the /api/stats endpoint. +// Returns system statistics and performance metrics from the data merger. +// +// Statistics may include: +// - Message processing rates +// - Aircraft count by source +// - Connection status +// - Data quality metrics +// +// The exact statistics depend on the merger implementation. func (s *Server) handleGetStats(w http.ResponseWriter, r *http.Request) { stats := s.merger.GetStatistics() @@ -190,11 +296,29 @@ func (s *Server) handleGetStats(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(stats) } +// handleGetOrigin serves the /api/origin endpoint. +// Returns the configured geographical reference point used by the system. +// +// The origin point is used for: +// - Default map center in the web interface +// - Distance calculations in coverage analysis +// - Range circle calculations func (s *Server) handleGetOrigin(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(s.origin) } +// handleGetCoverage serves the /api/coverage/{sourceId} endpoint. +// Returns coverage data for a specific source based on aircraft positions and signal strength. +// +// The coverage data includes all positions where the specified source has received +// aircraft signals, along with signal strength and distance information. +// This is useful for visualizing receiver coverage patterns and range. +// +// Parameters: +// - sourceId: URL parameter identifying the source +// +// Returns array of coverage points with lat/lon, signal strength, distance, and altitude. func (s *Server) handleGetCoverage(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) sourceID := vars["sourceId"] @@ -222,6 +346,22 @@ func (s *Server) handleGetCoverage(w http.ResponseWriter, r *http.Request) { }) } +// handleGetHeatmap serves the /api/heatmap/{sourceId} endpoint. +// 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 +// +// This provides a density-based visualization of where the source receives +// the strongest signals, useful for coverage analysis and antenna optimization. +// +// Parameters: +// - sourceId: URL parameter identifying the source +// +// Returns grid data array and geographic bounds for visualization. func (s *Server) handleGetHeatmap(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) sourceID := vars["sourceId"] @@ -281,6 +421,18 @@ func (s *Server) handleGetHeatmap(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(heatmapData) } +// 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 +// +// WebSocket clients receive periodic updates with current aircraft positions, +// source status, and system statistics. The connection is kept alive until +// the client disconnects or the server shuts down. func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) { conn, err := s.upgrader.Upgrade(w, r, nil) if err != nil { @@ -311,6 +463,16 @@ func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) { s.wsClientsMu.Unlock() } +// sendInitialData sends a complete data snapshot to a newly connected WebSocket client. +// +// This includes: +// - All currently tracked aircraft with their state information +// - Status of all configured data sources +// - Current system statistics +// +// ICAO addresses are converted to hex strings for consistent JSON representation. +// This initial data allows the client to immediately display current aircraft +// without waiting for the next periodic update. func (s *Server) sendInitialData(conn *websocket.Conn) { aircraft := s.merger.GetAircraft() sources := s.merger.GetSources() @@ -337,6 +499,16 @@ func (s *Server) sendInitialData(conn *websocket.Conn) { conn.WriteJSON(msg) } +// broadcastRoutine runs in a dedicated goroutine to distribute WebSocket messages. +// +// This routine: +// - Listens for broadcast messages on the broadcastChan +// - Sends messages to all connected WebSocket clients +// - Handles client connection cleanup on write errors +// - Respects the shutdown signal from stopChan +// +// Using a dedicated routine for broadcasting ensures efficient message +// distribution without blocking the update generation. func (s *Server) broadcastRoutine() { for { select { @@ -355,6 +527,16 @@ func (s *Server) broadcastRoutine() { } } +// periodicUpdateRoutine generates regular WebSocket updates for all connected clients. +// +// Updates are sent every second and include: +// - Current aircraft positions and state +// - Data source status updates +// - Fresh system statistics +// +// The routine uses a ticker for consistent timing and respects the shutdown +// signal. Updates are queued through broadcastUpdate() which handles the +// actual message formatting and distribution. func (s *Server) periodicUpdateRoutine() { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() @@ -369,6 +551,17 @@ 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. Formats the data as a WebSocketMessage with type "aircraft_update" +// 3. Converts ICAO addresses to hex strings for JSON compatibility +// 4. 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 +// cannot keep up with updates. func (s *Server) broadcastUpdate() { aircraft := s.merger.GetAircraft() sources := s.merger.GetSources() @@ -401,6 +594,10 @@ func (s *Server) broadcastUpdate() { } } +// handleIndex serves the main application page at the root URL. +// Returns the embedded index.html file which contains the aircraft tracking interface. +// +// Returns 404 if the index.html file is not found in the embedded assets. func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { data, err := s.staticFiles.ReadFile("static/index.html") if err != nil { @@ -412,6 +609,10 @@ func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { w.Write(data) } +// handleFavicon serves the favicon.ico file for browser tab icons. +// Returns the embedded favicon file with appropriate content-type header. +// +// Returns 404 if the favicon.ico file is not found in the embedded assets. func (s *Server) handleFavicon(w http.ResponseWriter, r *http.Request) { data, err := s.staticFiles.ReadFile("static/favicon.ico") if err != nil { @@ -423,6 +624,16 @@ func (s *Server) handleFavicon(w http.ResponseWriter, r *http.Request) { w.Write(data) } +// staticFileHandler creates an HTTP handler for serving embedded static files. +// +// This handler: +// - Maps URL paths from /static/* to embedded file paths +// - Sets appropriate Content-Type headers based on file extension +// - Adds cache control headers for client-side caching (1 hour) +// - Returns 404 for missing files +// +// The handler serves files from the embedded filesystem, enabling +// single-binary deployment without external static file dependencies. func (s *Server) staticFileHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Remove /static/ prefix from URL path to get the actual file path @@ -446,6 +657,13 @@ func (s *Server) staticFileHandler() http.Handler { }) } +// getContentType returns the appropriate MIME type for a file extension. +// Supports common web file types used in the SkyView interface: +// - HTML, CSS, JavaScript files +// - JSON data files +// - Image formats (SVG, PNG, JPEG, ICO) +// +// Returns "application/octet-stream" for unknown extensions. func getContentType(ext string) string { switch ext { case ".html": @@ -469,6 +687,16 @@ func getContentType(ext string) string { } } +// enableCORS wraps an HTTP handler with Cross-Origin Resource Sharing headers. +// +// This middleware: +// - Allows requests from any origin (*) +// - Supports GET, POST, PUT, DELETE, and OPTIONS methods +// - Permits Content-Type and Authorization headers +// - Handles preflight OPTIONS requests +// +// CORS is enabled to support web applications hosted on different domains +// than the SkyView server, which is common in development and some deployment scenarios. func (s *Server) enableCORS(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") diff --git a/internal/server/server_test.go b/internal/server/server_test.go deleted file mode 100644 index 054ae7e..0000000 --- a/internal/server/server_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package server - -import ( - "embed" - "net/http" - "net/http/httptest" - "testing" - - "skyview/internal/config" -) - -//go:embed testdata/* -var testStaticFiles embed.FS - -func TestNew(t *testing.T) { - cfg := &config.Config{ - Server: config.ServerConfig{ - Address: ":8080", - Port: 8080, - }, - Dump1090: config.Dump1090Config{ - Host: "localhost", - Port: 8080, - URL: "http://localhost:8080", - }, - } - - handler := New(cfg, testStaticFiles) - if handler == nil { - t.Fatal("Expected handler to be created") - } -} - -func TestCORSHeaders(t *testing.T) { - cfg := &config.Config{ - Dump1090: config.Dump1090Config{ - URL: "http://localhost:8080", - }, - } - - handler := New(cfg, testStaticFiles) - - req := httptest.NewRequest("OPTIONS", "/api/aircraft", nil) - w := httptest.NewRecorder() - - handler.ServeHTTP(w, req) - - if w.Header().Get("Access-Control-Allow-Origin") != "*" { - t.Errorf("Expected CORS header, got %s", w.Header().Get("Access-Control-Allow-Origin")) - } - - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } -}