// 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 ( "bufio" "encoding/binary" "errors" "fmt" "io" "time" ) // Beast format message type constants. // These define the different types of messages in the Beast binary protocol. const ( 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 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 // 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 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 // Buffered reader for efficient byte parsing sourceID string // Source identifier for message tagging } // 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), sourceID: sourceID, } } // 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 { b, err := p.reader.ReadByte() if err != nil { return nil, err } if b == BeastEscape { break } } // Read message type msgType, err := p.reader.ReadByte() if err != nil { return nil, err } // Validate message type var dataLen int switch msgType { case BeastModeAC: dataLen = 2 case BeastModeS: dataLen = 7 case BeastModeSLong: dataLen = 14 case BeastStatusMsg: // Status messages have variable length, skip for now return p.ReadMessage() case BeastEscape: // Handle double escape sequence (0x1A 0x1A) - skip and continue return p.ReadMessage() default: // Skip unknown message types and continue parsing instead of failing // This makes the parser more resilient to malformed or extended Beast formats return p.ReadMessage() } // Read timestamp (6 bytes, 48-bit) timestampBytes := make([]byte, 8) if _, err := io.ReadFull(p.reader, timestampBytes[2:]); err != nil { return nil, err } timestamp := binary.BigEndian.Uint64(timestampBytes) // Read signal level (1 byte) signal, err := p.reader.ReadByte() if err != nil { return nil, err } // Read Mode S data data := make([]byte, dataLen) if _, err := io.ReadFull(p.reader, data); err != nil { return nil, err } // Unescape data if needed data = p.unescapeData(data) return &Message{ Type: msgType, Timestamp: timestamp, Signal: signal, Data: data, ReceivedAt: time.Now(), SourceID: p.sourceID, }, nil } // 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 for i < len(data) { if i < len(data)-1 && data[i] == BeastEscape && data[i+1] == BeastEscape { result = append(result, BeastEscape) i += 2 } else { result = append(result, data[i]) i++ } } return result } // 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() if err != nil { if err != io.EOF && !errors.Is(err, io.ErrClosedPipe) { errChan <- fmt.Errorf("parser error from %s: %w", p.sourceID, err) } return } msgChan <- msg } } // 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 if msg.Signal == 0 { return -50.0 // Minimum detectable signal } return float64(msg.Signal) * (-50.0 / 255.0) } // 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") } if len(msg.Data) < 4 { return 0, errors.New("insufficient data for ICAO address") } // ICAO address is in bytes 1-3 of Mode S messages icao := uint32(msg.Data[1])<<16 | uint32(msg.Data[2])<<8 | uint32(msg.Data[3]) return icao, nil } // 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 } return (msg.Data[0] >> 3) & 0x1F } // 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 return 0, errors.New("not an extended squitter message") } if len(msg.Data) < 5 { return 0, errors.New("insufficient data for type code") } return (msg.Data[4] >> 3) & 0x1F, nil }