- 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>
234 lines
6.7 KiB
Go
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
|
|
}
|