// Package main provides a utility for parsing and displaying Beast format ADS-B data. // // beast-dump can read from TCP sockets (dump1090 streams) or files containing // Beast binary data, decode Mode S/ADS-B messages, and display the results // 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 // // 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 package main import ( "flag" "fmt" "io" "log" "net" "os" "time" "skyview/internal/beast" "skyview/internal/modes" ) // Config holds command-line configuration type Config struct { TCPAddress string // TCP address for Beast stream (e.g., "localhost:30005") FilePath string // File path for Beast data Verbose bool // Enable verbose output Count int // Maximum messages to process (0 = unlimited) } // BeastDumper handles Beast data parsing and console output type BeastDumper struct { config *Config parser *beast.Parser decoder *modes.Decoder stats struct { 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() os.Exit(1) } dumper := NewBeastDumper(config) if err := dumper.Run(); err != nil { log.Fatalf("Error: %v", err) } } // 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") fmt.Fprintf(os.Stderr, "Options:\n") flag.PrintDefaults() fmt.Fprintf(os.Stderr, "\nExamples:\n") fmt.Fprintf(os.Stderr, " %s -tcp svovel:30005\n", os.Args[0]) 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 } // NewBeastDumper creates a new Beast data dumper func NewBeastDumper(config *Config) *BeastDumper { return &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 }{ aircraftSeen: make(map[uint32]bool), startTime: time.Now(), }, } } // Run starts the Beast data processing 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 { return fmt.Errorf("TCP connection failed: %w", err) } reader = conn closer = conn fmt.Printf("Connected to: %s\n", d.config.TCPAddress) } else { file, err := d.openFile() if err != nil { return fmt.Errorf("file open failed: %w", err) } reader = file 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", "Time", "ICAO", "Type", "Signal", "Data", "Len", "Decoded") 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 } // openFile opens Beast data file func (d *BeastDumper) openFile() (*os.File, error) { file, err := os.Open(d.config.FilePath) 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 } // parseMessages processes Beast messages and outputs decoded data func (d *BeastDumper) parseMessages() error { for { // Check message count limit if d.config.Count > 0 && d.stats.totalMessages >= int64(d.config.Count) { fmt.Printf("\nReached message limit of %d\n", d.config.Count) break } // Parse Beast message msg, err := d.parser.ReadMessage() if err != nil { if err == io.EOF { fmt.Printf("\nEnd of data reached\n") break } if d.config.Verbose { fmt.Printf("Parse error: %v\n", err) } 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 } // 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 { if icaoAddr, err := msg.GetICAO24(); err == nil { icao = fmt.Sprintf("%06X", icaoAddr) 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 ", timestamp, icao, typeStr, signalStr, dataStr, len(msg.Data)) } // decodeAndDisplay attempts to decode Mode S message and display results func (d *BeastDumper) decodeAndDisplay(msg *beast.Message) { aircraft, err := d.decoder.Decode(msg.Data) if err != nil { if d.config.Verbose { fmt.Printf("Decode error: %v\n", err) } else { fmt.Printf("(decode failed)\n") } return } // Display decoded information info := d.formatAircraftInfo(aircraft) fmt.Printf("%s\n", info) // Verbose details if d.config.Verbose { d.displayVerboseInfo(aircraft, msg) } } // formatMessageType converts Beast message type to string func (d *BeastDumper) formatMessageType(msgType uint8) string { switch msgType { case beast.BeastModeAC: return "Mode A/C" case beast.BeastModeS: return "Mode S" case beast.BeastModeSLong: return "Mode S Long" case beast.BeastStatusMsg: return "Status" default: return fmt.Sprintf("Type %02X", msgType) } } // formatDataPreview creates a hex preview of message data 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 break } 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)) } 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 { info += " " } info += part } return info } // displayVerboseInfo shows detailed aircraft information func (d *BeastDumper) displayVerboseInfo(aircraft *modes.Aircraft, msg *beast.Message) { fmt.Printf(" Message Details:\n") 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) } if aircraft.Latitude != 0 || aircraft.Longitude != 0 { fmt.Printf(" Position: %.6f, %.6f\n", aircraft.Latitude, aircraft.Longitude) } if aircraft.Altitude != 0 { fmt.Printf(" Altitude: %d ft\n", aircraft.Altitude) } if aircraft.GroundSpeed != 0 || aircraft.Track != 0 { fmt.Printf(" Speed/Track: %d kt @ %d°\n", aircraft.GroundSpeed, aircraft.Track) } if aircraft.VerticalRate != 0 { fmt.Printf(" Vertical Rate: %d ft/min\n", aircraft.VerticalRate) } if aircraft.Squawk != "" { fmt.Printf(" Squawk: %s\n", aircraft.Squawk) } if aircraft.Category != "" { fmt.Printf(" Category: %s\n", aircraft.Category) } fmt.Printf("\n") } // formatHexData creates a formatted hex dump of data func (d *BeastDumper) formatHexData(data []byte) string { result := "" for i, b := range data { if i > 0 { result += " " } result += fmt.Sprintf("%02X", b) } return result } // 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) } } }