// 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 }