skyview/internal/vrs/parser.go
Ole-Morten Duesund 66a995b4d0 Fix issue #21 and add aircraft position tracking indicators
- Fix Debian package upgrade issue by separating upgrade vs remove behavior in prerm script
- Add aircraft position tracking statistics in merger GetStatistics() method
- Update frontend to display position tracking indicators in both header and stats view
- Format Go code to maintain consistency

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 11:25:42 +02:00

234 lines
6.7 KiB
Go

// Package vrs provides Virtual Radar Server JSON format parsing for ADS-B message streams.
//
// The VRS JSON format is a simplified alternative to the Beast binary protocol,
// providing aircraft data as newline-delimited JSON objects over TCP connections
// (typically port 33005 when using readsb --net-vrs-port).
//
// VRS JSON Format Structure:
// - Single-line JSON object per update: {"acList":[{aircraft1},{aircraft2},...]}
// - Updates sent at configurable intervals (default 5 seconds)
// - Each aircraft object contains ICAO, position, altitude, speed, etc.
// - Simpler parsing compared to binary Beast format
//
// This package handles:
// - JSON message parsing and validation
// - Aircraft data extraction from VRS format
// - Continuous stream processing with error recovery
// - Conversion to internal aircraft data structures
package vrs
import (
"bufio"
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"time"
)
// VRSMessage represents a complete VRS JSON message containing multiple aircraft
type VRSMessage struct {
AcList []VRSAircraft `json:"acList"`
}
// VRSAircraft represents a single aircraft in VRS JSON format
type VRSAircraft struct {
Icao string `json:"Icao"` // ICAO hex address (may have ~ prefix for non-ICAO)
Lat float64 `json:"Lat"` // Latitude
Long float64 `json:"Long"` // Longitude
Alt int `json:"Alt"` // Barometric altitude in feet
GAlt int `json:"GAlt"` // Geometric altitude in feet
Spd float64 `json:"Spd"` // Speed in knots
Trak float64 `json:"Trak"` // Track/heading in degrees
Vsi int `json:"Vsi"` // Vertical speed in feet/min
Sqk string `json:"Sqk"` // Squawk code
Call string `json:"Call"` // Callsign
Gnd bool `json:"Gnd"` // On ground flag
TAlt int `json:"TAlt"` // Target altitude
Mlat bool `json:"Mlat"` // MLAT position flag
Tisb bool `json:"Tisb"` // TIS-B flag
Sat bool `json:"Sat"` // Satellite (JAERO) position flag
// Additional fields that may be present
Reg string `json:"Reg"` // Registration
Type string `json:"Type"` // Aircraft type
Mdl string `json:"Mdl"` // Model
Op string `json:"Op"` // Operator
From string `json:"From"` // Departure airport
To string `json:"To"` // Destination airport
// Timing fields
PosTime int64 `json:"PosTime"` // Position timestamp (milliseconds)
}
// Parser handles VRS JSON format parsing from a stream
type Parser struct {
reader *bufio.Reader
sourceID string
}
// NewParser creates a new VRS JSON format parser for a data stream
//
// Parameters:
// - r: Input stream containing VRS JSON 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 VRS JSON message from the stream
//
// The VRS format sends complete JSON objects on single lines, with each
// object containing an array of aircraft updates. This is much simpler
// than Beast binary parsing.
//
// Returns the parsed message or an error if the stream is closed or corrupted
func (p *Parser) ReadMessage() (*VRSMessage, error) {
// Read complete line (VRS sends one JSON object per line)
line, err := p.reader.ReadString('\n')
if err != nil {
return nil, err
}
// Trim whitespace
line = strings.TrimSpace(line)
if line == "" {
// Empty line, try again
return p.ReadMessage()
}
// Parse JSON
var msg VRSMessage
if err := json.Unmarshal([]byte(line), &msg); err != nil {
// Invalid JSON, skip and continue
return p.ReadMessage()
}
return &msg, nil
}
// 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)
// - JSON parsing errors are recovered from automatically
// - Other errors are reported via the error channel
//
// Parameters:
// - msgChan: Channel for sending successfully parsed messages
// - errChan: Channel for reporting parsing errors
func (p *Parser) ParseStream(msgChan chan<- *VRSMessage, errChan chan<- error) {
for {
msg, err := p.ReadMessage()
if err != nil {
if err != io.EOF {
errChan <- fmt.Errorf("VRS parser error from %s: %w", p.sourceID, err)
}
return
}
msgChan <- msg
}
}
// GetICAO24 extracts and cleans the ICAO 24-bit address from VRS format
//
// VRS format may prefix non-ICAO addresses with '~'. This method:
// - Removes the '~' prefix if present
// - Converts hex string to uint32
// - Returns 0 for invalid addresses
func (a *VRSAircraft) GetICAO24() (uint32, error) {
// Remove non-ICAO prefix if present
icaoStr := strings.TrimPrefix(a.Icao, "~")
// Parse hex string
icao64, err := strconv.ParseUint(icaoStr, 16, 24)
if err != nil {
return 0, fmt.Errorf("invalid ICAO address: %s", a.Icao)
}
return uint32(icao64), nil
}
// HasPosition returns true if the aircraft has valid position data
func (a *VRSAircraft) HasPosition() bool {
// Check if we have non-zero lat/lon
return a.Lat != 0 || a.Long != 0
}
// HasAltitude returns true if the aircraft has valid altitude data
func (a *VRSAircraft) HasAltitude() bool {
return a.Alt != 0 || a.GAlt != 0
}
// GetAltitude returns the best available altitude (barometric preferred)
func (a *VRSAircraft) GetAltitude() int {
if a.Alt != 0 {
return a.Alt
}
return a.GAlt
}
// GetSpeed returns the speed in knots
func (a *VRSAircraft) GetSpeed() float64 {
return a.Spd
}
// GetHeading returns the track/heading in degrees
func (a *VRSAircraft) GetHeading() float64 {
return a.Trak
}
// GetVerticalRate returns the vertical speed in feet/min
func (a *VRSAircraft) GetVerticalRate() int {
return a.Vsi
}
// GetSquawk returns the squawk code as an integer
func (a *VRSAircraft) GetSquawk() (uint16, error) {
if a.Sqk == "" {
return 0, fmt.Errorf("no squawk code")
}
// Parse hex squawk code
squawk64, err := strconv.ParseUint(a.Sqk, 16, 16)
if err != nil {
return 0, fmt.Errorf("invalid squawk code: %s", a.Sqk)
}
return uint16(squawk64), nil
}
// GetPositionSource returns the source of position data
func (a *VRSAircraft) GetPositionSource() string {
if a.Mlat {
return "MLAT"
}
if a.Tisb {
return "TIS-B"
}
if a.Sat {
return "Satellite"
}
return "ADS-B"
}
// GetTimestamp returns the position timestamp as time.Time
func (a *VRSAircraft) GetTimestamp() time.Time {
if a.PosTime > 0 {
return time.Unix(0, a.PosTime*int64(time.Millisecond))
}
// If no timestamp provided, use current time
return time.Now()
}
// IsOnGround returns true if the aircraft is on the ground
func (a *VRSAircraft) IsOnGround() bool {
return a.Gnd
}