skyview/internal/beast/parser.go
Ole-Morten Duesund 1425f0a018 Restructure assets to top-level package and add Reset Map button
- Move assets from internal/assets to top-level assets/ package for clean embed directive
- Consolidate all static files in single location (assets/static/)
- Remove duplicate static file locations to maintain single source of truth
- Add Reset Map button to map controls with full functionality
- Implement resetMap() method to return map to calculated origin position
- Store origin in this.mapOrigin for reset functionality
- Fix go:embed pattern to work without parent directory references

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-24 00:57:49 +02:00

187 lines
4.3 KiB
Go

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
}