Complete multi-source Beast format implementation
Major features implemented: - Beast binary format parser with full Mode S/ADS-B decoding - Multi-source data merger with intelligent signal-based fusion - Advanced web frontend with 5 view modes (Map, Table, Stats, Coverage, 3D) - Real-time WebSocket updates with sub-second latency - Signal strength analysis and coverage heatmaps - Debian packaging with systemd integration - Production-ready deployment with security hardening Technical highlights: - Concurrent TCP clients with auto-reconnection - CPR position decoding and aircraft identification - Historical flight tracking with position trails - Range circles and receiver location visualization - Mobile-responsive design with professional UI - REST API and WebSocket real-time updates - Comprehensive build system and documentation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c8562a4f0d
commit
7340a9d6eb
15 changed files with 2332 additions and 238 deletions
187
internal/beast/parser.go
Normal file
187
internal/beast/parser.go
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue