Complete Beast format implementation with enhanced features and fixes #19

Merged
olemd merged 38 commits from beast-format-refactor into main 2025-08-24 20:50:38 +02:00
16 changed files with 266 additions and 289 deletions
Showing only changes of commit 0d60592b9f - Show all commits

Clean up codebase and fix server host binding for IPv6 support

Cleanup:
- Remove unused aircraft-icon.svg (replaced by type-specific icons)
- Remove test files: beast-dump-with-heli.bin, beast.test, main, old.json, ux.png
- Remove duplicate config.json.example (kept config.example.json)
- Remove empty internal/coverage/ directory
- Move CLAUDE.md to project root
- Update assets.go documentation to reflect current icon structure
- Format all Go code with gofmt

Server Host Binding Fix:
- Fix critical bug where server host configuration was ignored
- Add host parameter to Server struct and NewWebServer constructor
- Rename NewServer to NewWebServer for better clarity
- Fix IPv6 address formatting in server binding (wrap in brackets)
- Update startup message to show correct bind address format
- Support localhost-only, IPv4, IPv6, and interface-specific binding

This resolves the "too many colons in address" error for IPv6 hosts like ::1
and enables proper localhost-only deployment as configured.

Closes #15

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Ole-Morten Duesund 2025-08-24 18:36:14 +02:00

View file

@ -1 +1,39 @@
- This project uses forgejo for source control and the fj client is available.
# SkyView Project Guidelines
## Documentation Requirements
- We should always have an up to date document describing our architecture and features
- Include links to any external resources we've used
- We should also always have an up to date README describing the project
- Shell scripts should be validated with shellcheck
- Always make sure the code is well documented with explanations for why and how a particular solution is selected
## Development Principles
- An overarching principle with all code is KISS, Keep It Simple Stupid
- We do not want to create code that is more complicated than necessary
- When changing code, always make sure to update any relevant tests
- Use proper error handling - aviation applications need reliability
## SkyView-Specific Guidelines
### Architecture & Design
- Multi-source ADS-B data fusion is the core feature - prioritize signal strength-based conflict resolution
- Embedded resources (SQLite ICAO database, static assets) over external dependencies
- Low-latency performance is critical - optimize for fast WebSocket updates
- Support concurrent aircraft tracking (100+ aircraft should work smoothly)
### Code Organization
- Keep Go packages focused: beast parsing, modes decoding, merger, server, clients
- Frontend should be modular: separate managers for aircraft, map, UI, websockets
- Database operations should be fast (use indexes, avoid N+1 queries)
### Performance Considerations
- Beast binary parsing must handle high message rates (1000+ msg/sec per source)
- WebSocket broadcasting should not block on slow clients
- Memory usage should be bounded (configurable history limits)
- CPU usage should remain low during normal operation
### Documentation Maintenance
- Always update docs/ARCHITECTURE.md when changing system design
- README.md should stay current with features and usage
- External resources (ICAO docs, ADS-B standards) should be linked in documentation
- Country database updates should be straightforward (replace SQLite file)

View file

@ -6,7 +6,7 @@
// - index.html: Main web interface with aircraft tracking map
// - css/style.css: Styling for the web interface
// - js/app.js: JavaScript client for WebSocket communication and map rendering
// - aircraft-icon.svg: SVG icon for aircraft markers
// - icons/*.svg: Type-specific SVG icons for aircraft markers
// - favicon.ico: Browser icon
//
// The embedded filesystem is used by the HTTP server to serve static content
@ -16,11 +16,11 @@ package assets
import "embed"
// Static contains all embedded static web assets from the static/ directory.
//
//
// Files are embedded at build time and can be accessed using the standard
// fs.FS interface. Path names within the embedded filesystem preserve the
// directory structure, so files are accessed as:
// - "static/index.html"
// - "static/index.html"
// - "static/css/style.css"
// - "static/js/app.js"
// - etc.

View file

@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#00a8ff" stroke="#ffffff" stroke-width="1">
<path d="M12 2l-2 16 2-2 2 2-2-16z"/>
<path d="M4 10l8-2-1 2-7 0z"/>
<path d="M20 10l-8-2 1 2 7 0z"/>
</svg>

Binary file not shown.

View file

@ -5,14 +5,16 @@
// in human-readable format on the console.
//
// Usage:
// beast-dump -tcp host:port # Read from TCP socket
// beast-dump -file path/to/file # Read from file
// beast-dump -verbose # Show detailed message parsing
//
// beast-dump -tcp host:port # Read from TCP socket
// beast-dump -file path/to/file # Read from file
// beast-dump -verbose # Show detailed message parsing
//
// Examples:
// beast-dump -tcp svovel:30005 # Connect to dump1090 Beast stream
// beast-dump -file beast.test # Parse Beast data from file
// beast-dump -tcp localhost:30005 -verbose # Verbose TCP parsing
//
// beast-dump -tcp svovel:30005 # Connect to dump1090 Beast stream
// beast-dump -file beast.test # Parse Beast data from file
// beast-dump -tcp localhost:30005 -verbose # Verbose TCP parsing
package main
import (
@ -42,23 +44,23 @@ type BeastDumper struct {
parser *beast.Parser
decoder *modes.Decoder
stats struct {
totalMessages int64
validMessages int64
aircraftSeen map[uint32]bool
startTime time.Time
lastMessageTime time.Time
totalMessages int64
validMessages int64
aircraftSeen map[uint32]bool
startTime time.Time
lastMessageTime time.Time
}
}
func main() {
config := parseFlags()
if config.TCPAddress == "" && config.FilePath == "" {
fmt.Fprintf(os.Stderr, "Error: Must specify either -tcp or -file\n")
flag.Usage()
os.Exit(1)
}
if config.TCPAddress != "" && config.FilePath != "" {
fmt.Fprintf(os.Stderr, "Error: Cannot specify both -tcp and -file\n")
flag.Usage()
@ -66,7 +68,7 @@ func main() {
}
dumper := NewBeastDumper(config)
if err := dumper.Run(); err != nil {
log.Fatalf("Error: %v", err)
}
@ -75,12 +77,12 @@ func main() {
// parseFlags parses command-line flags and returns configuration
func parseFlags() *Config {
config := &Config{}
flag.StringVar(&config.TCPAddress, "tcp", "", "TCP address for Beast stream (e.g., localhost:30005)")
flag.StringVar(&config.FilePath, "file", "", "File path for Beast data")
flag.BoolVar(&config.Verbose, "verbose", false, "Enable verbose output")
flag.IntVar(&config.Count, "count", 0, "Maximum messages to process (0 = unlimited)")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [options]\n", os.Args[0])
fmt.Fprintf(os.Stderr, "\nBeast format ADS-B data parser and console dumper\n\n")
@ -91,7 +93,7 @@ func parseFlags() *Config {
fmt.Fprintf(os.Stderr, " %s -file beast.test\n", os.Args[0])
fmt.Fprintf(os.Stderr, " %s -tcp localhost:30005 -verbose -count 100\n", os.Args[0])
}
flag.Parse()
return config
}
@ -102,11 +104,11 @@ func NewBeastDumper(config *Config) *BeastDumper {
config: config,
decoder: modes.NewDecoder(0.0, 0.0), // beast-dump doesn't have reference position, use default
stats: struct {
totalMessages int64
validMessages int64
aircraftSeen map[uint32]bool
startTime time.Time
lastMessageTime time.Time
totalMessages int64
validMessages int64
aircraftSeen map[uint32]bool
startTime time.Time
lastMessageTime time.Time
}{
aircraftSeen: make(map[uint32]bool),
startTime: time.Now(),
@ -118,10 +120,10 @@ func NewBeastDumper(config *Config) *BeastDumper {
func (d *BeastDumper) Run() error {
fmt.Printf("Beast Data Dumper\n")
fmt.Printf("=================\n\n")
var reader io.Reader
var closer io.Closer
if d.config.TCPAddress != "" {
conn, err := d.connectTCP()
if err != nil {
@ -139,34 +141,34 @@ func (d *BeastDumper) Run() error {
closer = file
fmt.Printf("Reading file: %s\n", d.config.FilePath)
}
defer closer.Close()
// Create Beast parser
d.parser = beast.NewParser(reader, "beast-dump")
fmt.Printf("Verbose mode: %t\n", d.config.Verbose)
if d.config.Count > 0 {
fmt.Printf("Message limit: %d\n", d.config.Count)
}
fmt.Printf("\nStarting Beast data parsing...\n")
fmt.Printf("%-8s %-6s %-12s %-8s %-10s %-6s %s\n",
fmt.Printf("%-8s %-6s %-12s %-8s %-10s %-6s %s\n",
"Time", "ICAO", "Type", "Signal", "Data", "Len", "Decoded")
fmt.Printf("%s\n",
fmt.Printf("%s\n",
"------------------------------------------------------------------------")
return d.parseMessages()
}
// connectTCP establishes TCP connection to Beast stream
func (d *BeastDumper) connectTCP() (net.Conn, error) {
fmt.Printf("Connecting to %s...\n", d.config.TCPAddress)
conn, err := net.DialTimeout("tcp", d.config.TCPAddress, 10*time.Second)
if err != nil {
return nil, err
}
return conn, nil
}
@ -176,14 +178,14 @@ func (d *BeastDumper) openFile() (*os.File, error) {
if err != nil {
return nil, err
}
// Check file size
stat, err := file.Stat()
if err != nil {
file.Close()
return nil, err
}
fmt.Printf("File size: %d bytes\n", stat.Size())
return file, nil
}
@ -196,7 +198,7 @@ func (d *BeastDumper) parseMessages() error {
fmt.Printf("\nReached message limit of %d\n", d.config.Count)
break
}
// Parse Beast message
msg, err := d.parser.ReadMessage()
if err != nil {
@ -209,21 +211,21 @@ func (d *BeastDumper) parseMessages() error {
}
continue
}
d.stats.totalMessages++
d.stats.lastMessageTime = time.Now()
// Display Beast message info
d.displayMessage(msg)
// Decode Mode S data if available
if msg.Type == beast.BeastModeS || msg.Type == beast.BeastModeSLong {
d.decodeAndDisplay(msg)
}
d.stats.validMessages++
}
d.displayStatistics()
return nil
}
@ -231,7 +233,7 @@ func (d *BeastDumper) parseMessages() error {
// displayMessage shows basic Beast message information
func (d *BeastDumper) displayMessage(msg *beast.Message) {
timestamp := msg.ReceivedAt.Format("15:04:05")
// Extract ICAO if available
icao := "------"
if msg.Type == beast.BeastModeS || msg.Type == beast.BeastModeSLong {
@ -240,18 +242,18 @@ func (d *BeastDumper) displayMessage(msg *beast.Message) {
d.stats.aircraftSeen[icaoAddr] = true
}
}
// Beast message type
typeStr := d.formatMessageType(msg.Type)
// Signal strength
signal := msg.GetSignalStrength()
signalStr := fmt.Sprintf("%6.1f", signal)
// Data preview
dataStr := d.formatDataPreview(msg.Data)
fmt.Printf("%-8s %-6s %-12s %-8s %-10s %-6d ",
fmt.Printf("%-8s %-6s %-12s %-8s %-10s %-6d ",
timestamp, icao, typeStr, signalStr, dataStr, len(msg.Data))
}
@ -266,11 +268,11 @@ func (d *BeastDumper) decodeAndDisplay(msg *beast.Message) {
}
return
}
// Display decoded information
info := d.formatAircraftInfo(aircraft)
fmt.Printf("%s\n", info)
// Verbose details
if d.config.Verbose {
d.displayVerboseInfo(aircraft, msg)
@ -298,7 +300,7 @@ func (d *BeastDumper) formatDataPreview(data []byte) string {
if len(data) == 0 {
return ""
}
preview := ""
for i, b := range data {
if i >= 4 { // Show first 4 bytes
@ -306,33 +308,33 @@ func (d *BeastDumper) formatDataPreview(data []byte) string {
}
preview += fmt.Sprintf("%02X", b)
}
if len(data) > 4 {
preview += "..."
}
return preview
}
// formatAircraftInfo creates a summary of decoded aircraft information
func (d *BeastDumper) formatAircraftInfo(aircraft *modes.Aircraft) string {
parts := []string{}
// Callsign
if aircraft.Callsign != "" {
parts = append(parts, fmt.Sprintf("CS:%s", aircraft.Callsign))
}
// Position
if aircraft.Latitude != 0 || aircraft.Longitude != 0 {
parts = append(parts, fmt.Sprintf("POS:%.4f,%.4f", aircraft.Latitude, aircraft.Longitude))
}
// Altitude
if aircraft.Altitude != 0 {
parts = append(parts, fmt.Sprintf("ALT:%dft", aircraft.Altitude))
}
// Speed and track
if aircraft.GroundSpeed != 0 {
parts = append(parts, fmt.Sprintf("SPD:%dkt", aircraft.GroundSpeed))
@ -340,26 +342,26 @@ func (d *BeastDumper) formatAircraftInfo(aircraft *modes.Aircraft) string {
if aircraft.Track != 0 {
parts = append(parts, fmt.Sprintf("HDG:%d°", aircraft.Track))
}
// Vertical rate
if aircraft.VerticalRate != 0 {
parts = append(parts, fmt.Sprintf("VS:%d", aircraft.VerticalRate))
}
// Squawk
if aircraft.Squawk != "" {
parts = append(parts, fmt.Sprintf("SQ:%s", aircraft.Squawk))
}
// Emergency
if aircraft.Emergency != "" && aircraft.Emergency != "None" {
parts = append(parts, fmt.Sprintf("EMG:%s", aircraft.Emergency))
}
if len(parts) == 0 {
return "(no data decoded)"
}
info := ""
for i, part := range parts {
if i > 0 {
@ -367,7 +369,7 @@ func (d *BeastDumper) formatAircraftInfo(aircraft *modes.Aircraft) string {
}
info += part
}
return info
}
@ -377,7 +379,7 @@ func (d *BeastDumper) displayVerboseInfo(aircraft *modes.Aircraft, msg *beast.Me
fmt.Printf(" Raw Data: %s\n", d.formatHexData(msg.Data))
fmt.Printf(" Timestamp: %s\n", msg.ReceivedAt.Format("15:04:05.000"))
fmt.Printf(" Signal: %.2f dBFS\n", msg.GetSignalStrength())
fmt.Printf(" Aircraft Data:\n")
if aircraft.Callsign != "" {
fmt.Printf(" Callsign: %s\n", aircraft.Callsign)
@ -418,23 +420,23 @@ func (d *BeastDumper) formatHexData(data []byte) string {
// displayStatistics shows final parsing statistics
func (d *BeastDumper) displayStatistics() {
duration := time.Since(d.stats.startTime)
fmt.Printf("\nStatistics:\n")
fmt.Printf("===========\n")
fmt.Printf("Total messages: %d\n", d.stats.totalMessages)
fmt.Printf("Valid messages: %d\n", d.stats.validMessages)
fmt.Printf("Unique aircraft: %d\n", len(d.stats.aircraftSeen))
fmt.Printf("Duration: %v\n", duration.Round(time.Second))
if d.stats.totalMessages > 0 && duration > 0 {
rate := float64(d.stats.totalMessages) / duration.Seconds()
fmt.Printf("Message rate: %.1f msg/sec\n", rate)
}
if len(d.stats.aircraftSeen) > 0 {
fmt.Printf("\nAircraft seen:\n")
for icao := range d.stats.aircraftSeen {
fmt.Printf(" %06X\n", icao)
}
}
}
}

View file

@ -1,15 +0,0 @@
{
"server": {
"address": ":8080",
"port": 8080
},
"dump1090": {
"host": "192.168.1.100",
"data_port": 30003
},
"origin": {
"latitude": 37.7749,
"longitude": -122.4194,
"name": "San Francisco"
}
}

View file

@ -1,39 +0,0 @@
# SkyView Project Guidelines
## Documentation Requirements
- We should always have an up to date document describing our architecture and features
- Include links to any external resources we've used
- We should also always have an up to date README describing the project
- Shell scripts should be validated with shellcheck
- Always make sure the code is well documented with explanations for why and how a particular solution is selected
## Development Principles
- An overarching principle with all code is KISS, Keep It Simple Stupid
- We do not want to create code that is more complicated than necessary
- When changing code, always make sure to update any relevant tests
- Use proper error handling - aviation applications need reliability
## SkyView-Specific Guidelines
### Architecture & Design
- Multi-source ADS-B data fusion is the core feature - prioritize signal strength-based conflict resolution
- Embedded resources (SQLite ICAO database, static assets) over external dependencies
- Low-latency performance is critical - optimize for fast WebSocket updates
- Support concurrent aircraft tracking (100+ aircraft should work smoothly)
### Code Organization
- Keep Go packages focused: beast parsing, modes decoding, merger, server, clients
- Frontend should be modular: separate managers for aircraft, map, UI, websockets
- Database operations should be fast (use indexes, avoid N+1 queries)
### Performance Considerations
- Beast binary parsing must handle high message rates (1000+ msg/sec per source)
- WebSocket broadcasting should not block on slow clients
- Memory usage should be bounded (configurable history limits)
- CPU usage should remain low during normal operation
### Documentation Maintenance
- Always update docs/ARCHITECTURE.md when changing system design
- README.md should stay current with features and usage
- External resources (ICAO docs, ADS-B standards) should be linked in documentation
- Country database updates should be straightforward (replace SQLite file)

View file

@ -88,12 +88,12 @@ func NewParser(r io.Reader, sourceID string) *Parser {
// ReadMessage reads and parses a single Beast message from the stream.
//
// The parsing process:
// 1. Search for the escape character (0x1A) that marks message start
// 2. Read and validate the message type byte
// 3. Read the 48-bit timestamp (big-endian, padded to 64-bit)
// 4. Read the signal level byte
// 5. Read the message payload (length depends on message type)
// 6. Process escape sequences in the payload data
// 1. Search for the escape character (0x1A) that marks message start
// 2. Read and validate the message type byte
// 3. Read the 48-bit timestamp (big-endian, padded to 64-bit)
// 4. Read the signal level byte
// 5. Read the message payload (length depends on message type)
// 6. Process escape sequences in the payload data
//
// The parser can recover from protocol errors by continuing to search for
// the next valid message boundary. Status messages are currently skipped
@ -253,7 +253,7 @@ func (msg *Message) GetSignalStrength() float64 {
// The ICAO address is a unique 24-bit identifier assigned to each aircraft.
// In Mode S messages, it's located in bytes 1-3 of the message payload:
// - Byte 1: Most significant 8 bits
// - Byte 2: Middle 8 bits
// - Byte 2: Middle 8 bits
// - Byte 3: Least significant 8 bits
//
// Mode A/C messages don't contain ICAO addresses and will return an error.

View file

@ -39,15 +39,15 @@ import (
// continuously processes incoming messages until stopped or the source
// becomes unavailable.
type BeastClient struct {
source *merger.Source // Source configuration and status
merger *merger.Merger // Data merger for multi-source fusion
decoder *modes.Decoder // Mode S/ADS-B message decoder
conn net.Conn // TCP connection to Beast source
parser *beast.Parser // Beast format message parser
source *merger.Source // Source configuration and status
merger *merger.Merger // Data merger for multi-source fusion
decoder *modes.Decoder // Mode S/ADS-B message decoder
conn net.Conn // TCP connection to Beast source
parser *beast.Parser // Beast format message parser
msgChan chan *beast.Message // Buffered channel for parsed messages
errChan chan error // Error reporting channel
stopChan chan struct{} // Shutdown signal channel
wg sync.WaitGroup // Wait group for goroutine coordination
errChan chan error // Error reporting channel
stopChan chan struct{} // Shutdown signal channel
wg sync.WaitGroup // Wait group for goroutine coordination
// Reconnection parameters
reconnectDelay time.Duration // Initial reconnect delay
@ -102,9 +102,9 @@ func (c *BeastClient) Start(ctx context.Context) {
// Stop gracefully shuts down the client and all associated goroutines.
//
// The shutdown process:
// 1. Signals all goroutines to stop via stopChan
// 2. Closes the TCP connection if active
// 3. Waits for all goroutines to complete
// 1. Signals all goroutines to stop via stopChan
// 2. Closes the TCP connection if active
// 3. Waits for all goroutines to complete
//
// This method blocks until the shutdown is complete.
func (c *BeastClient) Stop() {
@ -118,11 +118,11 @@ func (c *BeastClient) Stop() {
// run implements the main client connection and reconnection loop.
//
// This method handles the complete client lifecycle:
// 1. Connection establishment with timeout
// 2. Exponential backoff on connection failures
// 3. Message parsing and processing goroutine management
// 4. Connection monitoring and failure detection
// 5. Automatic reconnection on disconnection
// 1. Connection establishment with timeout
// 2. Exponential backoff on connection failures
// 3. Message parsing and processing goroutine management
// 4. Connection monitoring and failure detection
// 5. Automatic reconnection on disconnection
//
// The exponential backoff starts at reconnectDelay (5s) and doubles on each
// failure up to maxReconnect (60s), then resets on successful connection.
@ -210,10 +210,10 @@ func (c *BeastClient) readMessages() {
// processMessages runs in a dedicated goroutine to decode and merge aircraft data.
//
// For each received Beast message, this method:
// 1. Decodes the Mode S/ADS-B message payload
// 2. Extracts aircraft information (position, altitude, speed, etc.)
// 3. Updates the data merger with new aircraft state
// 4. Updates source statistics (message count)
// 1. Decodes the Mode S/ADS-B message payload
// 2. Extracts aircraft information (position, altitude, speed, etc.)
// 3. Updates the data merger with new aircraft state
// 4. Updates source statistics (message count)
//
// Invalid or unparseable messages are silently discarded to maintain
// system stability. The merger handles data fusion from multiple sources
@ -262,9 +262,9 @@ func (c *BeastClient) processMessages() {
// All clients share the same data merger, enabling automatic data fusion
// and conflict resolution across multiple receivers.
type MultiSourceClient struct {
clients []*BeastClient // Managed Beast clients
merger *merger.Merger // Shared data merger for all sources
mu sync.RWMutex // Protects clients slice
clients []*BeastClient // Managed Beast clients
merger *merger.Merger // Shared data merger for all sources
mu sync.RWMutex // Protects clients slice
}
// NewMultiSourceClient creates a client manager for multiple Beast format sources.
@ -292,9 +292,9 @@ func NewMultiSourceClient(merger *merger.Merger) *MultiSourceClient {
// AddSource registers and configures a new Beast format data source.
//
// This method:
// 1. Registers the source with the data merger
// 2. Creates a new BeastClient for the source
// 3. Adds the client to the managed clients list
// 1. Registers the source with the data merger
// 2. Creates a new BeastClient for the source
// 3. Adds the client to the managed clients list
//
// The source is not automatically started; call Start() to begin connections.
// Sources can be added before or after starting the multi-source client.

View file

@ -30,7 +30,7 @@ type CountryInfo struct {
// NewDatabase creates a new ICAO database with comprehensive allocation data
func NewDatabase() (*Database, error) {
allocations := getICAOAllocations()
// Sort allocations by start address for efficient binary search
sort.Slice(allocations, func(i, j int) bool {
return allocations[i].StartAddr < allocations[j].StartAddr
@ -265,4 +265,4 @@ func (d *Database) LookupCountry(icaoHex string) (*CountryInfo, error) {
// Close is a no-op since we don't have any resources to clean up
func (d *Database) Close() error {
return nil
}
}

View file

@ -111,7 +111,7 @@ func (a *AircraftState) MarshalJSON() ([]byte, error) {
SelectedAltitude int `json:"SelectedAltitude"`
SelectedHeading float64 `json:"SelectedHeading"`
BaroSetting float64 `json:"BaroSetting"`
// From AircraftState
Sources map[string]*SourceData `json:"sources"`
LastUpdate time.Time `json:"last_update"`
@ -155,7 +155,7 @@ func (a *AircraftState) MarshalJSON() ([]byte, error) {
SelectedAltitude: a.Aircraft.SelectedAltitude,
SelectedHeading: a.Aircraft.SelectedHeading,
BaroSetting: a.Aircraft.BaroSetting,
// Copy all fields from AircraftState
Sources: a.Sources,
LastUpdate: a.LastUpdate,
@ -238,7 +238,7 @@ type Merger struct {
sources map[string]*Source // Source ID -> source information
icaoDB *icao.Database // ICAO country lookup database
mu sync.RWMutex // Protects all maps and slices
historyLimit int // Maximum history points to retain
historyLimit int // Maximum history points to retain
staleTimeout time.Duration // Time before aircraft considered stale (15 seconds)
updateMetrics map[uint32]*updateMetric // ICAO24 -> update rate calculation data
}
@ -291,13 +291,13 @@ func (m *Merger) AddSource(source *Source) {
// UpdateAircraft merges new aircraft data from a source using intelligent fusion strategies.
//
// This is the core method of the merger, handling:
// 1. Aircraft state creation for new aircraft
// 2. Source data tracking and statistics
// 3. Multi-source data fusion with conflict resolution
// 4. Historical data updates with retention limits
// 5. Distance and bearing calculations
// 6. Update rate metrics
// 7. Source status maintenance
// 1. Aircraft state creation for new aircraft
// 2. Source data tracking and statistics
// 3. Multi-source data fusion with conflict resolution
// 4. Historical data updates with retention limits
// 5. Distance and bearing calculations
// 6. Update rate metrics
// 7. Source status maintenance
//
// Data fusion strategies:
// - Position: Use source with strongest signal
@ -326,7 +326,7 @@ func (m *Merger) UpdateAircraft(sourceID string, aircraft *modes.Aircraft, signa
AltitudeHistory: make([]AltitudePoint, 0),
SpeedHistory: make([]SpeedPoint, 0),
}
// Lookup country information for new aircraft
icaoHex := fmt.Sprintf("%06X", aircraft.ICAO24)
if countryInfo, err := m.icaoDB.LookupCountry(icaoHex); err == nil {
@ -339,7 +339,7 @@ func (m *Merger) UpdateAircraft(sourceID string, aircraft *modes.Aircraft, signa
state.CountryCode = "XX"
state.Flag = "🏳️"
}
m.aircraft[aircraft.ICAO24] = state
m.updateMetrics[aircraft.ICAO24] = &updateMetric{
updates: make([]time.Time, 0),
@ -585,10 +585,10 @@ func (m *Merger) updateHistories(state *AircraftState, aircraft *modes.Aircraft,
// updateUpdateRate calculates and maintains the message update rate for an aircraft.
//
// The calculation:
// 1. Records the timestamp of each update
// 2. Maintains a sliding 30-second window of updates
// 3. Calculates updates per second over this window
// 4. Updates the aircraft's UpdateRate field
// 1. Records the timestamp of each update
// 2. Maintains a sliding 30-second window of updates
// 3. Calculates updates per second over this window
// 4. Updates the aircraft's UpdateRate field
//
// This provides real-time feedback on data quality and can help identify
// aircraft that are updating frequently (close, good signal) vs infrequently
@ -644,10 +644,10 @@ func (m *Merger) getBestSignalSource(state *AircraftState) string {
// GetAircraft returns a snapshot of all current aircraft states.
//
// This method:
// 1. Filters out stale aircraft (older than staleTimeout)
// 2. Calculates current age for each aircraft
// 3. Determines closest receiver distance and bearing
// 4. Returns copies to prevent external modification
// 1. Filters out stale aircraft (older than staleTimeout)
// 2. Calculates current age for each aircraft
// 3. Determines closest receiver distance and bearing
// 4. Returns copies to prevent external modification
//
// The returned map uses ICAO24 addresses as keys and can be safely
// used by multiple goroutines without affecting the internal state.
@ -813,7 +813,7 @@ func calculateDistanceBearing(lat1, lon1, lat2, lon2 float64) (float64, float64)
func (m *Merger) Close() error {
m.mu.Lock()
defer m.mu.Unlock()
if m.icaoDB != nil {
return m.icaoDB.Close()
}

View file

@ -56,16 +56,16 @@ func validateModeSCRC(data []byte) bool {
if len(data) < 4 {
return false
}
// Calculate CRC for all bytes except the last 3 (which contain the CRC)
crc := uint32(0)
for i := 0; i < len(data)-3; i++ {
crc = ((crc << 8) ^ crcTable[((crc>>16)^uint32(data[i]))&0xFF]) & 0xFFFFFF
}
// Extract transmitted CRC from last 3 bytes
transmittedCRC := uint32(data[len(data)-3])<<16 | uint32(data[len(data)-2])<<8 | uint32(data[len(data)-1])
return crc == transmittedCRC
}
@ -107,37 +107,37 @@ const (
// depending on the messages received and aircraft capabilities.
type Aircraft struct {
// Core Identification
ICAO24 uint32 // 24-bit ICAO aircraft address (unique identifier)
Callsign string // 8-character flight callsign (from identification messages)
ICAO24 uint32 // 24-bit ICAO aircraft address (unique identifier)
Callsign string // 8-character flight callsign (from identification messages)
// Position and Navigation
Latitude float64 // Position latitude in decimal degrees
Longitude float64 // Position longitude in decimal degrees
Altitude int // Altitude in feet (barometric or geometric)
BaroAltitude int // Barometric altitude in feet (QNH corrected)
GeomAltitude int // Geometric altitude in feet (GNSS height)
Latitude float64 // Position latitude in decimal degrees
Longitude float64 // Position longitude in decimal degrees
Altitude int // Altitude in feet (barometric or geometric)
BaroAltitude int // Barometric altitude in feet (QNH corrected)
GeomAltitude int // Geometric altitude in feet (GNSS height)
// Motion and Dynamics
VerticalRate int // Vertical rate in feet per minute (climb/descent)
GroundSpeed int // Ground speed in knots (integer)
Track int // Track angle in degrees (0-359, integer)
Heading int // Aircraft heading in degrees (magnetic, integer)
VerticalRate int // Vertical rate in feet per minute (climb/descent)
GroundSpeed int // Ground speed in knots (integer)
Track int // Track angle in degrees (0-359, integer)
Heading int // Aircraft heading in degrees (magnetic, integer)
// Aircraft Information
Category string // Aircraft category (size, type, performance)
Squawk string // 4-digit transponder squawk code (octal)
Category string // Aircraft category (size, type, performance)
Squawk string // 4-digit transponder squawk code (octal)
// Status and Alerts
Emergency string // Emergency/priority status description
OnGround bool // Aircraft is on ground (surface movement)
Alert bool // Alert flag (ATC attention required)
SPI bool // Special Position Identification (pilot activated)
Emergency string // Emergency/priority status description
OnGround bool // Aircraft is on ground (surface movement)
Alert bool // Alert flag (ATC attention required)
SPI bool // Special Position Identification (pilot activated)
// Data Quality Indicators
NACp uint8 // Navigation Accuracy Category - Position (0-11)
NACv uint8 // Navigation Accuracy Category - Velocity (0-4)
SIL uint8 // Surveillance Integrity Level (0-3)
NACp uint8 // Navigation Accuracy Category - Position (0-11)
NACv uint8 // Navigation Accuracy Category - Velocity (0-4)
SIL uint8 // Surveillance Integrity Level (0-3)
// Autopilot/Flight Management
SelectedAltitude int // MCP/FCU selected altitude in feet
SelectedHeading float64 // MCP/FCU selected heading in degrees
@ -163,11 +163,11 @@ type Decoder struct {
cprOddLon map[uint32]float64 // Odd message longitude encoding (ICAO24 -> normalized lon)
cprEvenTime map[uint32]int64 // Timestamp of even message (for freshness comparison)
cprOddTime map[uint32]int64 // Timestamp of odd message (for freshness comparison)
// Reference position for CPR zone ambiguity resolution (receiver location)
refLatitude float64 // Receiver latitude in decimal degrees
refLongitude float64 // Receiver longitude in decimal degrees
// Mutex to protect concurrent access to CPR maps
mu sync.RWMutex
}
@ -199,10 +199,10 @@ func NewDecoder(refLat, refLon float64) *Decoder {
// Decode processes a Mode S message and extracts all available aircraft information.
//
// This is the main entry point for message decoding. The method:
// 1. Validates message length and extracts the Downlink Format (DF)
// 2. Extracts the ICAO24 aircraft address
// 3. Routes to appropriate decoder based on message type
// 4. Returns populated Aircraft struct with available data
// 1. Validates message length and extracts the Downlink Format (DF)
// 2. Extracts the ICAO24 aircraft address
// 3. Routes to appropriate decoder based on message type
// 4. Returns populated Aircraft struct with available data
//
// Different message types provide different information:
// - DF4/20: Altitude only
@ -369,10 +369,10 @@ func (d *Decoder) decodeIdentification(data []byte, aircraft *Aircraft) {
// - Even/odd flag for CPR decoding
//
// CPR (Compact Position Reporting) Process:
// 1. Extract the even/odd flag and CPR lat/lon values
// 2. Normalize CPR values to 0-1 range (divide by 2^17)
// 3. Store values for this aircraft's ICAO address
// 4. Attempt position decoding if both even and odd messages are available
// 1. Extract the even/odd flag and CPR lat/lon values
// 2. Normalize CPR values to 0-1 range (divide by 2^17)
// 3. Store values for this aircraft's ICAO address
// 4. Attempt position decoding if both even and odd messages are available
//
// The actual position calculation requires both even and odd messages to
// resolve the ambiguity inherent in the compressed encoding format.
@ -456,7 +456,7 @@ func (d *Decoder) decodeCPRPosition(aircraft *Aircraft) {
} else if latEven < -90 {
latEven = -180 - latEven
}
if latOdd > 90 {
latOdd = 180 - latOdd
} else if latOdd < -90 {
@ -473,7 +473,7 @@ func (d *Decoder) decodeCPRPosition(aircraft *Aircraft) {
// Calculate which decoded latitude is closer to the receiver
distToEven := math.Abs(latEven - d.refLatitude)
distToOdd := math.Abs(latOdd - d.refLatitude)
// Choose the latitude solution that's closer to the receiver position
if distToOdd < distToEven {
aircraft.Latitude = latOdd
@ -501,7 +501,7 @@ func (d *Decoder) decodeCPRPosition(aircraft *Aircraft) {
}
aircraft.Longitude = lon
// CPR decoding completed successfully
}
@ -576,13 +576,13 @@ func (d *Decoder) decodeVelocity(data []byte, aircraft *Aircraft) {
// Calculate ground speed in knots (rounded to integer)
speedKnots := math.Sqrt(ewVel*ewVel + nsVel*nsVel)
// Validate speed range (0-600 knots for civilian aircraft)
if speedKnots > 600 {
speedKnots = 600 // Cap at reasonable maximum
}
aircraft.GroundSpeed = int(math.Round(speedKnots))
// Calculate track in degrees (0-359)
trackDeg := math.Atan2(ewVel, nsVel) * 180 / math.Pi
if trackDeg < 0 {
@ -641,20 +641,20 @@ func (d *Decoder) decodeAltitudeBits(altCode uint16, tc uint8) int {
// Standard altitude encoding with 25 ft increments
// Check Q-bit (bit 4) for encoding type
qBit := (altCode >> 4) & 1
if qBit == 1 {
// Standard altitude with Q-bit set
// Remove Q-bit and reassemble 11-bit altitude code
n := ((altCode & 0x1F80) >> 2) | ((altCode & 0x0020) >> 1) | (altCode & 0x000F)
alt := int(n)*25 - 1000
// Validate altitude range
if alt < -1000 || alt > 60000 {
return 0
}
return alt
}
// Gray code altitude (100 ft increments) - legacy encoding
// Convert from Gray code to binary
n := altCode
@ -662,7 +662,7 @@ func (d *Decoder) decodeAltitudeBits(altCode uint16, tc uint8) int {
n ^= n >> 4
n ^= n >> 2
n ^= n >> 1
// Convert to altitude in feet
alt := int(n&0x7FF) * 100
if alt < 0 || alt > 60000 {
@ -835,7 +835,7 @@ func (d *Decoder) decodeTargetState(data []byte, aircraft *Aircraft) {
//
// Operational status messages (TC 31) contain:
// - Navigation Accuracy Category for Position (NACp): Position accuracy
// - Navigation Accuracy Category for Velocity (NACv): Velocity accuracy
// - Navigation Accuracy Category for Velocity (NACv): Velocity accuracy
// - Surveillance Integrity Level (SIL): System integrity confidence
//
// These parameters help receiving systems assess data quality and determine

View file

@ -22,6 +22,7 @@ import (
"net/http"
"path"
"strconv"
"strings"
"sync"
"time"
@ -35,8 +36,8 @@ import (
// This is used as the center point for the web map interface and for
// distance calculations in coverage analysis.
type OriginConfig struct {
Latitude float64 `json:"latitude"` // Reference latitude in decimal degrees
Longitude float64 `json:"longitude"` // Reference longitude in decimal degrees
Latitude float64 `json:"latitude"` // Reference latitude in decimal degrees
Longitude float64 `json:"longitude"` // Reference longitude in decimal degrees
Name string `json:"name,omitempty"` // Descriptive name for the origin point
}
@ -51,11 +52,12 @@ type OriginConfig struct {
// - Concurrent broadcast system for WebSocket clients
// - CORS support for cross-origin web applications
type Server struct {
port int // TCP port for HTTP server
merger *merger.Merger // Data source for aircraft information
staticFiles embed.FS // Embedded static web assets
server *http.Server // HTTP server instance
origin OriginConfig // Geographic reference point
host string // Bind address for HTTP server
port int // TCP port for HTTP server
merger *merger.Merger // Data source for aircraft information
staticFiles embed.FS // Embedded static web assets
server *http.Server // HTTP server instance
origin OriginConfig // Geographic reference point
// WebSocket management
wsClients map[*websocket.Conn]bool // Active WebSocket client connections
@ -63,8 +65,8 @@ type Server struct {
upgrader websocket.Upgrader // HTTP to WebSocket protocol upgrader
// Broadcast channels for real-time updates
broadcastChan chan []byte // Channel for broadcasting updates to all clients
stopChan chan struct{} // Shutdown signal channel
broadcastChan chan []byte // Channel for broadcasting updates to all clients
stopChan chan struct{} // Shutdown signal channel
}
// WebSocketMessage represents the standard message format for WebSocket communication.
@ -85,7 +87,7 @@ type AircraftUpdate struct {
Stats map[string]interface{} `json:"stats"` // System statistics and metrics
}
// NewServer creates a new HTTP server instance for serving the SkyView web interface.
// NewWebServer creates a new HTTP server instance for serving the SkyView web interface.
//
// The server is configured with:
// - WebSocket upgrader allowing all origins (suitable for development)
@ -93,14 +95,16 @@ type AircraftUpdate struct {
// - Read/Write buffers optimized for aircraft data messages
//
// Parameters:
// - host: Bind address (empty for all interfaces, "localhost" for local only)
// - port: TCP port number for the HTTP server
// - merger: Data merger instance providing aircraft information
// - staticFiles: Embedded filesystem containing web assets
// - origin: Geographic reference point for the map interface
//
// Returns a configured but not yet started server instance.
func NewServer(port int, merger *merger.Merger, staticFiles embed.FS, origin OriginConfig) *Server {
func NewWebServer(host string, port int, merger *merger.Merger, staticFiles embed.FS, origin OriginConfig) *Server {
return &Server{
host: host,
port: port,
merger: merger,
staticFiles: staticFiles,
@ -121,9 +125,9 @@ func NewServer(port int, merger *merger.Merger, staticFiles embed.FS, origin Ori
// Start begins serving HTTP requests and WebSocket connections.
//
// This method starts several background routines:
// 1. Broadcast routine - handles WebSocket message distribution
// 2. Periodic update routine - sends regular updates to WebSocket clients
// 3. HTTP server - serves API endpoints and static files
// 1. Broadcast routine - handles WebSocket message distribution
// 2. Periodic update routine - sends regular updates to WebSocket clients
// 3. HTTP server - serves API endpoints and static files
//
// The method blocks until the server encounters an error or is shut down.
// Use Stop() for graceful shutdown.
@ -139,8 +143,15 @@ func (s *Server) Start() error {
// Setup routes
router := s.setupRoutes()
// Format address correctly for IPv6
addr := fmt.Sprintf("%s:%d", s.host, s.port)
if strings.Contains(s.host, ":") {
// IPv6 address needs brackets
addr = fmt.Sprintf("[%s]:%d", s.host, s.port)
}
s.server = &http.Server{
Addr: fmt.Sprintf(":%d", s.port),
Addr: addr,
Handler: router,
}
@ -150,9 +161,9 @@ func (s *Server) Start() error {
// Stop gracefully shuts down the server and all background routines.
//
// This method:
// 1. Signals all background routines to stop via stopChan
// 2. Shuts down the HTTP server with a 5-second timeout
// 3. Closes WebSocket connections
// 1. Signals all background routines to stop via stopChan
// 2. Shuts down the HTTP server with a 5-second timeout
// 3. Closes WebSocket connections
//
// The shutdown is designed to be safe and allow in-flight requests to complete.
func (s *Server) Stop() {
@ -206,13 +217,13 @@ func (s *Server) setupRoutes() http.Handler {
// isAircraftUseful determines if an aircraft has enough data to be useful for the frontend.
//
// DESIGN NOTE: We WANT reasonable aircraft to appear in our table view, even if they
// don't have enough data to appear on the map. This provides users visibility into
// DESIGN NOTE: We WANT reasonable aircraft to appear in our table view, even if they
// don't have enough data to appear on the map. This provides users visibility into
// all tracked aircraft, not just those with complete position data.
//
// Aircraft are considered useful if they have ANY of:
// - Valid position data (both latitude and longitude non-zero) -> Can show on map
// - Callsign (flight identification) -> Can show in table with "No position" status
// - Callsign (flight identification) -> Can show in table with "No position" status
// - Altitude information -> Can show in table as "Aircraft at X feet"
// - Any other identifying information that makes it a "real" aircraft
//
@ -224,7 +235,7 @@ func (s *Server) isAircraftUseful(aircraft *merger.AircraftState) bool {
hasCallsign := aircraft.Callsign != ""
hasAltitude := aircraft.Altitude != 0
hasSquawk := aircraft.Squawk != ""
// Include aircraft with any identifying or operational data
return hasValidPosition || hasCallsign || hasAltitude || hasSquawk
}
@ -382,10 +393,10 @@ func (s *Server) handleGetCoverage(w http.ResponseWriter, r *http.Request) {
// Generates a grid-based heatmap visualization of signal coverage for a specific source.
//
// The heatmap is computed by:
// 1. Finding geographic bounds of all aircraft positions for the source
// 2. Creating a 100x100 grid covering the bounds
// 3. Accumulating signal strength values in each grid cell
// 4. Returning the grid data with boundary coordinates
// 1. Finding geographic bounds of all aircraft positions for the source
// 2. Creating a 100x100 grid covering the bounds
// 3. Accumulating signal strength values in each grid cell
// 4. Returning the grid data with boundary coordinates
//
// This provides a density-based visualization of where the source receives
// the strongest signals, useful for coverage analysis and antenna optimization.
@ -456,11 +467,11 @@ func (s *Server) handleGetHeatmap(w http.ResponseWriter, r *http.Request) {
// handleWebSocket manages WebSocket connections for real-time aircraft data streaming.
//
// This handler:
// 1. Upgrades the HTTP connection to WebSocket protocol
// 2. Registers the client for broadcast updates
// 3. Sends initial data snapshot to the client
// 4. Handles client messages (currently just ping/pong for keepalive)
// 5. Cleans up the connection when the client disconnects
// 1. Upgrades the HTTP connection to WebSocket protocol
// 2. Registers the client for broadcast updates
// 3. Sends initial data snapshot to the client
// 4. Handles client messages (currently just ping/pong for keepalive)
// 5. Cleans up the connection when the client disconnects
//
// WebSocket clients receive periodic updates with current aircraft positions,
// source status, and system statistics. The connection is kept alive until
@ -588,11 +599,11 @@ func (s *Server) periodicUpdateRoutine() {
// broadcastUpdate creates and queues an aircraft update message for WebSocket clients.
//
// This function:
// 1. Collects current aircraft data from the merger
// 2. Filters aircraft to only include "useful" ones (with position or callsign)
// 3. Formats the data as a WebSocketMessage with type "aircraft_update"
// 4. Converts ICAO addresses to hex strings for JSON compatibility
// 5. Queues the message for broadcast (non-blocking)
// 1. Collects current aircraft data from the merger
// 2. Filters aircraft to only include "useful" ones (with position or callsign)
// 3. Formats the data as a WebSocketMessage with type "aircraft_update"
// 4. Converts ICAO addresses to hex strings for JSON compatibility
// 5. Queues the message for broadcast (non-blocking)
//
// If the broadcast channel is full, the update is dropped to prevent blocking.
// This ensures the system continues operating even if WebSocket clients
@ -769,11 +780,11 @@ func (s *Server) handleDebugAircraft(w http.ResponseWriter, r *http.Request) {
}
response := map[string]interface{}{
"timestamp": time.Now().Unix(),
"all_aircraft": allAircraftMap,
"timestamp": time.Now().Unix(),
"all_aircraft": allAircraftMap,
"filtered_aircraft": filteredAircraftMap,
"all_count": len(allAircraftMap),
"filtered_count": len(filteredAircraftMap),
"all_count": len(allAircraftMap),
"filtered_count": len(filteredAircraftMap),
}
w.Header().Set("Content-Type", "application/json")

BIN
main

Binary file not shown.

View file

@ -1,15 +0,0 @@
{
"server": {
"address": ":8080",
"port": 8080
},
"dump1090": {
"host": "svovel",
"data_port": 30003
},
"origin": {
"latitude": 59.908127,
"longitude": 10.801460,
"name": "Etterstadsletta flyplass"
}
}

BIN
ux.png

Binary file not shown.