Clean up, format, lint and document entire codebase

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 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2025-08-24 10:29:25 +02:00
commit 9ebc7e143e
11 changed files with 1300 additions and 892 deletions

View file

@ -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

View file

@ -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
var Static embed.FS

View file

@ -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

View file

@ -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()

View file

@ -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
}
}
}

View file

@ -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
}
}

View file

@ -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

View file

@ -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

View file

@ -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
}
}

View file

@ -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", "*")

View file

@ -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)
}
}