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>
This commit is contained in:
Ole-Morten Duesund 2025-08-24 18:36:14 +02:00
commit 0d60592b9f
16 changed files with 266 additions and 289 deletions

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