package beast import ( "bufio" "encoding/binary" "errors" "fmt" "io" "time" ) // Beast message types const ( BeastModeAC = 0x31 // '1' - Mode A/C BeastModeS = 0x32 // '2' - Mode S Short (56 bits) BeastModeSLong = 0x33 // '3' - Mode S Long (112 bits) BeastStatusMsg = 0x34 // '4' - Status message BeastEscape = 0x1A // Escape character ) // Message represents a Beast format message type Message struct { Type byte Timestamp uint64 // 48-bit timestamp in 12MHz ticks Signal uint8 // Signal level (RSSI) Data []byte // Mode S data ReceivedAt time.Time SourceID string // Identifier for the source receiver } // Parser handles Beast binary format parsing type Parser struct { reader *bufio.Reader sourceID string } // NewParser creates a new Beast format parser func NewParser(r io.Reader, sourceID string) *Parser { return &Parser{ reader: bufio.NewReader(r), sourceID: sourceID, } } // ReadMessage reads and parses a single Beast message 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() default: return nil, fmt.Errorf("unknown message type: 0x%02x", msgType) } // 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 data 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 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 signal byte to dBFS 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 address from Mode S 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 returns the downlink format (first 5 bits) func (msg *Message) GetDownlinkFormat() uint8 { if len(msg.Data) == 0 { return 0 } return (msg.Data[0] >> 3) & 0x1F } // GetTypeCode returns the message type code for 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 }