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:
parent
67d0e0612a
commit
0d60592b9f
16 changed files with 266 additions and 289 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue