skyview/internal/modes/decoder.go
Ole-Morten Duesund 62ace55fe1 Implement transponder code (squawk) lookup and textual descriptions
Resolves #30

- Add comprehensive squawk code lookup database with emergency, standard, military, and special codes
- Implement squawk.Database with 20+ common transponder codes including:
  * Emergency codes: 7700 (General Emergency), 7600 (Radio Failure), 7500 (Hijacking)
  * Standard codes: 1200/7000 (VFR), operational codes by region
  * Special codes: 1277 (SAR), 1255 (Fire Fighting), military codes
- Add SquawkDescription field to Aircraft struct and JSON marshaling
- Integrate squawk database into merger for automatic description population
- Update frontend with color-coded squawk display and tooltips:
  * Red for emergency codes with warning symbols
  * Orange for special operations
  * Gray for military codes
  * Green for standard operational codes
- Add comprehensive test coverage for squawk lookup functionality

Features:
- Visual emergency code identification for safety
- Educational descriptions for aviation enthusiasts
- Configurable lookup system for regional variations
- Hover tooltips with full code explanations
- Graceful fallback for unknown codes

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

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

1217 lines
42 KiB
Go

// Package modes provides Mode S and ADS-B message decoding capabilities.
//
// Mode S is a secondary surveillance radar system that enables aircraft to transmit
// detailed information including position, altitude, velocity, and identification.
// ADS-B (Automatic Dependent Surveillance-Broadcast) is a modernization of Mode S
// that provides more precise and frequent position reports.
//
// This package implements:
// - Complete Mode S message decoding for all downlink formats
// - ADS-B extended squitter message parsing (DF17/18)
// - CPR (Compact Position Reporting) position decoding algorithm
// - Aircraft identification, category, and status decoding
// - Velocity and heading calculation from velocity messages
// - Navigation accuracy and integrity decoding
//
// Key Features:
// - CPR Global Position Decoding: Resolves ambiguous encoded positions using
// even/odd message pairs and trigonometric calculations
// - Multi-format Support: Handles surveillance replies, extended squitter,
// and various ADS-B message types
// - Real-time Processing: Maintains state for CPR decoding across messages
// - Comprehensive Data Extraction: Extracts all available aircraft parameters
//
// CPR Algorithm Implementation:
// The Compact Position Reporting format encodes latitude and longitude using
// two alternating formats (even/odd) that create overlapping grids. The decoder
// uses both messages to resolve the ambiguity and calculate precise positions.
//
// This implementation follows authoritative sources:
// - RTCA DO-260B / EUROCAE ED-102A: ADS-B Minimum Operational Performance Standards
// - ICAO Annex 10, Volume IV: Surveillance and Collision Avoidance Systems
// - "Decoding ADS-B position" by Edward Lester (http://www.lll.lu/~edward/edward/adsb/DecodingADSBposition.html)
// - PyModeS library reference implementation (https://github.com/junzis/pyModeS)
package modes
import (
"fmt"
"math"
"sync"
)
// crcTable for Mode S CRC-24 validation
var crcTable [256]uint32
func init() {
// Initialize CRC table for Mode S CRC-24 (polynomial 0x1FFF409)
for i := 0; i < 256; i++ {
crc := uint32(i) << 16
for j := 0; j < 8; j++ {
if crc&0x800000 != 0 {
crc = (crc << 1) ^ 0x1FFF409
} else {
crc = crc << 1
}
}
crcTable[i] = crc & 0xFFFFFF
}
}
// validateModeSCRC validates the 24-bit CRC of a Mode S message
func validateModeSCRC(data []byte) bool {
if len(data) < 4 {
return false
}
// Calculate CRC for all bytes except the last 3 (which contain the CRC)
crc := uint32(0)
for i := 0; i < len(data)-3; i++ {
crc = ((crc << 8) ^ crcTable[((crc>>16)^uint32(data[i]))&0xFF]) & 0xFFFFFF
}
// Extract transmitted CRC from last 3 bytes
transmittedCRC := uint32(data[len(data)-3])<<16 | uint32(data[len(data)-2])<<8 | uint32(data[len(data)-1])
return crc == transmittedCRC
}
// Mode S Downlink Format (DF) constants.
// The DF field (first 5 bits) determines the message type and structure.
const (
DF0 = 0 // Short air-air surveillance (ACAS)
DF4 = 4 // Surveillance altitude reply (interrogation response)
DF5 = 5 // Surveillance identity reply (squawk code response)
DF11 = 11 // All-call reply (capability and ICAO address)
DF16 = 16 // Long air-air surveillance (ACAS with altitude)
DF17 = 17 // Extended squitter (ADS-B from transponder)
DF18 = 18 // Extended squitter/non-transponder (ADS-B from other sources)
DF19 = 19 // Military extended squitter
DF20 = 20 // Comm-B altitude reply (BDS register data)
DF21 = 21 // Comm-B identity reply (BDS register data)
DF24 = 24 // Comm-D (ELM - Enhanced Length Message)
)
// ADS-B Type Code (TC) constants for DF17/18 extended squitter messages.
// The type code (bits 32-36) determines the content and format of ADS-B messages.
const (
TC_IDENT_CATEGORY = 1 // Aircraft identification and category (callsign)
TC_SURFACE_POS = 5 // Surface position (airport ground movement)
TC_AIRBORNE_POS_9 = 9 // Airborne position (barometric altitude)
TC_AIRBORNE_POS_20 = 20 // Airborne position (GNSS height above ellipsoid)
TC_AIRBORNE_VEL = 19 // Airborne velocity (ground speed and track)
TC_AIRBORNE_POS_GPS = 22 // Airborne position (GNSS altitude)
TC_RESERVED = 23 // Reserved for future use
TC_SURFACE_SYSTEM = 24 // Surface system status
TC_OPERATIONAL = 31 // Aircraft operational status (capabilities)
)
// Aircraft represents a complete set of decoded aircraft data from Mode S/ADS-B messages.
//
// This structure contains all possible information that can be extracted from
// various Mode S and ADS-B message types, including position, velocity, status,
// and navigation data. Not all fields will be populated for every aircraft,
// depending on the messages received and aircraft capabilities.
type Aircraft struct {
// Core Identification
ICAO24 uint32 // 24-bit ICAO aircraft address (unique identifier)
Callsign string // 8-character flight callsign (from identification messages)
// Position and Navigation
Latitude float64 // Position latitude in decimal degrees
Longitude float64 // Position longitude in decimal degrees
Altitude int // Altitude in feet (barometric or geometric)
BaroAltitude int // Barometric altitude in feet (QNH corrected)
GeomAltitude int // Geometric altitude in feet (GNSS height)
// Motion and Dynamics
VerticalRate int // Vertical rate in feet per minute (climb/descent)
GroundSpeed int // Ground speed in knots (integer)
Track int // Track angle in degrees (0-359, integer)
Heading int // Aircraft heading in degrees (magnetic, integer)
// Aircraft Information
Category string // Aircraft category (size, type, performance)
Squawk string // 4-digit transponder squawk code (octal)
SquawkDescription string // Human-readable description of transponder code
// Status and Alerts
Emergency string // Emergency/priority status description
OnGround bool // Aircraft is on ground (surface movement)
Alert bool // Alert flag (ATC attention required)
SPI bool // Special Position Identification (pilot activated)
// Data Quality Indicators
NACp uint8 // Navigation Accuracy Category - Position (0-11)
NACv uint8 // Navigation Accuracy Category - Velocity (0-4)
SIL uint8 // Surveillance Integrity Level (0-3)
// Transponder Information
TransponderCapability string // Transponder capability level (from DF11 messages)
TransponderLevel uint8 // Transponder level (0-7 from capability field)
// Combined Data Quality Assessment
SignalQuality string // Combined assessment of position/velocity accuracy and integrity
// Autopilot/Flight Management
SelectedAltitude int // MCP/FCU selected altitude in feet
SelectedHeading float64 // MCP/FCU selected heading in degrees
BaroSetting float64 // Barometric pressure setting (QNH) in millibars
// Additional fields from VRS JSON and extended sources
Registration string // Aircraft registration (e.g., "N12345")
AircraftType string // Aircraft type (e.g., "B738")
Operator string // Airline or operator name
// Validity flags for optional fields (used by VRS and other sources)
CallsignValid bool // Whether callsign is valid
PositionValid bool // Whether position is valid
AltitudeValid bool // Whether altitude is valid
GeomAltitudeValid bool // Whether geometric altitude is valid
GroundSpeedValid bool // Whether ground speed is valid
TrackValid bool // Whether track is valid
VerticalRateValid bool // Whether vertical rate is valid
SquawkValid bool // Whether squawk code is valid
OnGroundValid bool // Whether on-ground status is valid
// Position source indicators
PositionMLAT bool // Position derived from MLAT
PositionTISB bool // Position from TIS-B
}
// Decoder handles Mode S and ADS-B message decoding with CPR position resolution.
//
// The decoder maintains state for CPR (Compact Position Reporting) decoding,
// which requires pairs of even/odd messages to resolve position ambiguity.
// Each aircraft (identified by ICAO24) has separate CPR state tracking.
//
// CPR Position Decoding:
// Aircraft positions are encoded using two alternating formats that create
// overlapping latitude/longitude grids. The decoder stores both even and odd
// encoded positions and uses trigonometric calculations to resolve the
// actual aircraft position when both are available.
type Decoder struct {
// CPR (Compact Position Reporting) state tracking per aircraft
cprEvenLat map[uint32]float64 // Even message latitude encoding (ICAO24 -> normalized lat)
cprEvenLon map[uint32]float64 // Even message longitude encoding (ICAO24 -> normalized lon)
cprOddLat map[uint32]float64 // Odd message latitude encoding (ICAO24 -> normalized lat)
cprOddLon map[uint32]float64 // Odd message longitude encoding (ICAO24 -> normalized lon)
cprEvenTime map[uint32]int64 // Timestamp of even message (for freshness comparison)
cprOddTime map[uint32]int64 // Timestamp of odd message (for freshness comparison)
// Reference position for CPR zone ambiguity resolution (receiver location)
refLatitude float64 // Receiver latitude in decimal degrees
refLongitude float64 // Receiver longitude in decimal degrees
// Mutex to protect concurrent access to CPR maps
mu sync.RWMutex
}
// NewDecoder creates a new Mode S/ADS-B decoder with initialized CPR tracking.
//
// The reference position (typically the receiver location) is used to resolve
// CPR zone ambiguity during position decoding. Without a proper reference,
// aircraft can appear many degrees away from their actual position.
//
// Parameters:
// - refLat: Reference latitude in decimal degrees (receiver location)
// - refLon: Reference longitude in decimal degrees (receiver location)
//
// Returns a configured decoder ready for message processing.
func NewDecoder(refLat, refLon float64) *Decoder {
return &Decoder{
cprEvenLat: make(map[uint32]float64),
cprEvenLon: make(map[uint32]float64),
cprOddLat: make(map[uint32]float64),
cprOddLon: make(map[uint32]float64),
cprEvenTime: make(map[uint32]int64),
cprOddTime: make(map[uint32]int64),
refLatitude: refLat,
refLongitude: refLon,
}
}
// Decode processes a Mode S message and extracts all available aircraft information.
//
// This is the main entry point for message decoding. The method:
// 1. Validates message length and extracts the Downlink Format (DF)
// 2. Extracts the ICAO24 aircraft address
// 3. Routes to appropriate decoder based on message type
// 4. Returns populated Aircraft struct with available data
//
// Different message types provide different information:
// - DF4/20: Altitude only
// - DF5/21: Squawk code only
// - DF17/18: Complete ADS-B data (position, velocity, identification, etc.)
//
// Parameters:
// - data: Raw Mode S message bytes (7 or 14 bytes depending on type)
//
// Returns decoded Aircraft struct or error for invalid/incomplete messages.
func (d *Decoder) Decode(data []byte) (*Aircraft, error) {
if len(data) < 7 {
return nil, fmt.Errorf("message too short: %d bytes", len(data))
}
// Validate CRC to reject corrupted messages that create ghost targets
if !validateModeSCRC(data) {
return nil, fmt.Errorf("invalid CRC - corrupted message")
}
df := (data[0] >> 3) & 0x1F
icao := d.extractICAO(data, df)
aircraft := &Aircraft{
ICAO24: icao,
}
switch df {
case DF0:
// Short Air-Air Surveillance (ACAS)
aircraft.Altitude = d.decodeAltitude(data)
case DF4, DF20:
aircraft.Altitude = d.decodeAltitude(data)
case DF5, DF21:
aircraft.Squawk = d.decodeSquawk(data)
case DF11:
// All-Call Reply - extract capability and interrogator identifier
d.decodeAllCallReply(data, aircraft)
case DF16:
// Long Air-Air Surveillance (ACAS with altitude)
aircraft.Altitude = d.decodeAltitude(data)
case DF17, DF18:
return d.decodeExtendedSquitter(data, aircraft)
case DF19:
// Military Extended Squitter - similar to DF17/18 but with military codes
return d.decodeMilitaryExtendedSquitter(data, aircraft)
case DF24:
// Comm-D Enhanced Length Message - variable length data
d.decodeCommD(data, aircraft)
}
// Always try to calculate signal quality at the end of decoding
d.calculateSignalQuality(aircraft)
return aircraft, nil
}
// extractICAO extracts the ICAO 24-bit aircraft address from Mode S messages.
//
// For most downlink formats, the ICAO address is located in bytes 1-3 of the
// message. Some formats may have different layouts, but this implementation
// uses the standard position for all supported formats.
//
// Parameters:
// - data: Mode S message bytes
// - df: Downlink format (currently unused, but available for format-specific handling)
//
// Returns the 24-bit ICAO address as a uint32.
func (d *Decoder) extractICAO(data []byte, df uint8) uint32 {
// For most formats, ICAO is in bytes 1-3
return uint32(data[1])<<16 | uint32(data[2])<<8 | uint32(data[3])
}
// decodeExtendedSquitter processes ADS-B extended squitter messages (DF17/18).
//
// Extended squitter messages contain the richest aircraft data, including:
// - Aircraft identification and category (TC 1-4)
// - Surface position and movement (TC 5-8)
// - Airborne position with various altitude sources (TC 9-18, 20-22)
// - Velocity and heading information (TC 19)
// - Aircraft status and emergency codes (TC 28)
// - Target state and autopilot settings (TC 29)
// - Operational status and navigation accuracy (TC 31)
//
// The method routes messages to specific decoders based on the Type Code (TC)
// field in bits 32-36 of the message.
//
// Parameters:
// - data: 14-byte extended squitter message
// - aircraft: Aircraft struct to populate with decoded data
//
// Returns the updated Aircraft struct or an error for malformed messages.
func (d *Decoder) decodeExtendedSquitter(data []byte, aircraft *Aircraft) (*Aircraft, error) {
if len(data) < 14 {
return nil, fmt.Errorf("extended squitter too short: %d bytes", len(data))
}
tc := (data[4] >> 3) & 0x1F
switch {
case tc >= 1 && tc <= 4:
// Aircraft identification
d.decodeIdentification(data, aircraft)
case tc >= 5 && tc <= 8:
// Surface position
d.decodeSurfacePosition(data, aircraft)
case tc >= 9 && tc <= 18:
// Airborne position
d.decodeAirbornePosition(data, aircraft)
case tc == 19:
// Airborne velocity
d.decodeVelocity(data, aircraft)
case tc >= 20 && tc <= 22:
// Airborne position with GNSS
d.decodeAirbornePosition(data, aircraft)
case tc == 28:
// Aircraft status
d.decodeStatus(data, aircraft)
case tc == 29:
// Target state and status
d.decodeTargetState(data, aircraft)
case tc == 31:
// Operational status
d.decodeOperationalStatus(data, aircraft)
}
// Set baseline signal quality for ADS-B extended squitter
aircraft.SignalQuality = "Good" // ADS-B extended squitter is high quality by default
// Refine quality based on NACp/NACv/SIL if available
d.calculateSignalQuality(aircraft)
return aircraft, nil
}
// decodeIdentification extracts aircraft callsign and category from identification messages.
//
// Aircraft identification messages (TC 1-4) contain:
// - 8-character callsign encoded in 6-bit characters
// - Aircraft category indicating size, performance, and type
//
// Callsign Encoding:
// Each character is encoded in 6 bits using a custom character set:
// - Characters: "#ABCDEFGHIJKLMNOPQRSTUVWXYZ##### ###############0123456789######"
// - Index 0-63 maps to the character at that position
// - '#' represents space or invalid characters
//
// Parameters:
// - data: Extended squitter message containing identification data
// - aircraft: Aircraft struct to update with callsign and category
func (d *Decoder) decodeIdentification(data []byte, aircraft *Aircraft) {
tc := (data[4] >> 3) & 0x1F
// Category
aircraft.Category = d.getAircraftCategory(tc, data[4]&0x07)
// Callsign - 8 characters encoded in 6 bits each
chars := "#ABCDEFGHIJKLMNOPQRSTUVWXYZ##### ###############0123456789######"
callsign := ""
// Extract 48 bits starting from bit 40
for i := 0; i < 8; i++ {
bitOffset := 40 + i*6
byteOffset := bitOffset / 8
bitShift := bitOffset % 8
var charCode uint8
if bitShift <= 2 {
charCode = (data[byteOffset] >> (2 - bitShift)) & 0x3F
} else {
charCode = ((data[byteOffset] << (bitShift - 2)) & 0x3F) |
(data[byteOffset+1] >> (10 - bitShift))
}
if charCode < 64 {
callsign += string(chars[charCode])
}
}
aircraft.Callsign = callsign
}
// decodeAirbornePosition extracts aircraft position from CPR-encoded position messages.
//
// Airborne position messages (TC 9-18, 20-22) contain:
// - Altitude information (barometric or geometric)
// - CPR-encoded latitude and longitude
// - Even/odd flag for CPR decoding
//
// CPR (Compact Position Reporting) Process:
// 1. Extract the even/odd flag and CPR lat/lon values
// 2. Normalize CPR values to 0-1 range (divide by 2^17)
// 3. Store values for this aircraft's ICAO address
// 4. Attempt position decoding if both even and odd messages are available
//
// The actual position calculation requires both even and odd messages to
// resolve the ambiguity inherent in the compressed encoding format.
//
// Parameters:
// - data: Extended squitter message containing position data
// - aircraft: Aircraft struct to update with position and altitude
func (d *Decoder) decodeAirbornePosition(data []byte, aircraft *Aircraft) {
tc := (data[4] >> 3) & 0x1F
// Altitude
altBits := (uint16(data[5])<<4 | uint16(data[6])>>4) & 0x0FFF
aircraft.Altitude = d.decodeAltitudeBits(altBits, tc)
// CPR latitude/longitude
cprLat := uint32(data[6]&0x03)<<15 | uint32(data[7])<<7 | uint32(data[8])>>1
cprLon := uint32(data[8]&0x01)<<16 | uint32(data[9])<<8 | uint32(data[10])
oddFlag := (data[6] >> 2) & 0x01
// Store CPR values for later decoding (protected by mutex)
d.mu.Lock()
if oddFlag == 1 {
d.cprOddLat[aircraft.ICAO24] = float64(cprLat) / 131072.0
d.cprOddLon[aircraft.ICAO24] = float64(cprLon) / 131072.0
} else {
d.cprEvenLat[aircraft.ICAO24] = float64(cprLat) / 131072.0
d.cprEvenLon[aircraft.ICAO24] = float64(cprLon) / 131072.0
}
d.mu.Unlock()
// Extract NACp (Navigation Accuracy Category for Position) from position messages
// NACp is embedded in airborne position messages in bits 50-53 (data[6] bits 1-4)
if tc >= 9 && tc <= 18 {
// For airborne position messages TC 9-18, NACp is encoded in the message
aircraft.NACp = uint8(tc - 8) // TC 9->NACp 1, TC 10->NACp 2, etc.
// Note: This is a simplified mapping. Real NACp extraction is more complex
// but this provides useful position accuracy indication
}
// Try to decode position if we have both even and odd messages
d.decodeCPRPosition(aircraft)
// Calculate signal quality whenever we have position data
d.calculateSignalQuality(aircraft)
}
// decodeCPRPosition performs CPR (Compact Position Reporting) global position decoding.
//
// Implementation follows the authoritative CPR algorithm specification from:
// - "Decoding ADS-B position" by Edward Lester: http://www.lll.lu/~edward/edward/adsb/DecodingADSBposition.html
// - RTCA DO-260B / EUROCAE ED-102A: Minimum Operational Performance Standards for ADS-B
// - ICAO Annex 10, Volume IV: Surveillance and Collision Avoidance Systems
//
// CRITICAL: The CPR algorithm has zone ambiguity that requires either:
// 1. A reference position (receiver location) to resolve zones correctly, OR
// 2. Message timestamp comparison to choose the most recent valid position
//
// Without proper zone resolution, aircraft can appear 6+ degrees away from actual position.
// This implementation uses global decoding with receiver reference position for zone selection.
//
// Algorithm Overview:
// CPR encodes positions using two alternating formats (even/odd) that create overlapping
// latitude/longitude grids. Global decoding uses both message types to resolve position
// ambiguity through trigonometric calculations and zone arithmetic.
//
// Parameters:
// - aircraft: Aircraft struct to update with decoded position
func (d *Decoder) decodeCPRPosition(aircraft *Aircraft) {
// Read CPR values with read lock
d.mu.RLock()
evenLat, evenExists := d.cprEvenLat[aircraft.ICAO24]
oddLat, oddExists := d.cprOddLat[aircraft.ICAO24]
if !evenExists || !oddExists {
d.mu.RUnlock()
return
}
evenLon := d.cprEvenLon[aircraft.ICAO24]
oddLon := d.cprOddLon[aircraft.ICAO24]
d.mu.RUnlock()
// CPR input values ready for decoding
// CPR decoding algorithm
dLat := 360.0 / 60.0
j := math.Floor(evenLat*59 - oddLat*60 + 0.5)
latEven := dLat * (math.Mod(j, 60) + evenLat)
latOdd := dLat * (math.Mod(j, 59) + oddLat)
if latEven >= 270 {
latEven -= 360
}
if latOdd >= 270 {
latOdd -= 360
}
// Additional range correction to ensure valid latitude bounds (-90° to +90°)
if latEven > 90 {
latEven = 180 - latEven
} else if latEven < -90 {
latEven = -180 - latEven
}
if latOdd > 90 {
latOdd = 180 - latOdd
} else if latOdd < -90 {
latOdd = -180 - latOdd
}
// Validate final latitude values are within acceptable range
if math.Abs(latOdd) > 90 || math.Abs(latEven) > 90 {
// Invalid CPR decoding - skip position update
return
}
// Zone ambiguity resolution using receiver reference position
// CPR global decoding produces two possible position solutions (even/odd).
// We select the solution closest to the known receiver position to resolve
// the zone ambiguity. This prevents aircraft from appearing in wrong geographic zones.
distToEven := math.Abs(latEven - d.refLatitude)
distToOdd := math.Abs(latOdd - d.refLatitude)
// Choose the latitude solution that's closer to the receiver position
// CRITICAL: This choice must be consistent for both latitude and longitude calculations
var selectedLat float64
var useOddForLongitude bool
if distToOdd < distToEven {
selectedLat = latOdd
useOddForLongitude = true
} else {
selectedLat = latEven
useOddForLongitude = false
}
aircraft.Latitude = selectedLat
// Longitude calculation using correct CPR global decoding algorithm
// Reference: http://www.lll.lu/~edward/edward/adsb/DecodingADSBposition.html
nl := d.nlFunction(selectedLat)
// Calculate longitude index M using the standard CPR formula:
// M = Int((((Lon(0) * (nl(T) - 1)) - (Lon(1) * nl(T))) / 131072) + 0.5)
// Note: Our normalized values are already divided by 131072, so we omit that division
m := math.Floor(evenLon*(nl-1) - oddLon*nl + 0.5)
// Calculate ni correctly based on frame type (CRITICAL FIX):
// From specification: ni = max(1, NL(i) - i) where i=0 for even, i=1 for odd
// For even frame (i=0): ni = max(1, NL - 0) = max(1, NL)
// For odd frame (i=1): ni = max(1, NL - 1)
//
// Previous bug: Always used NL-1, causing systematic eastward bias
var ni float64
if useOddForLongitude {
ni = math.Max(1, nl-1) // Odd frame: NL - 1
} else {
ni = math.Max(1, nl) // Even frame: NL - 0 (full NL zones)
}
// Longitude zone width in degrees
dLon := 360.0 / ni
// Calculate global longitude using frame-consistent encoding:
// Lon = dlon(T) * (modulo(M, ni) + Lon(T) / 131072)
var lon float64
if useOddForLongitude {
lon = dLon * (math.Mod(m, ni) + oddLon)
} else {
lon = dLon * (math.Mod(m, ni) + evenLon)
}
if lon >= 180 {
lon -= 360
} else if lon <= -180 {
lon += 360
}
// Validate longitude is within acceptable range
if math.Abs(lon) > 180 {
// Invalid longitude - skip position update
return
}
aircraft.Longitude = lon
aircraft.PositionValid = true
// CPR decoding completed successfully
}
// nlFunction calculates the number of longitude zones (NL) for a given latitude.
//
// This function implements the NL(lat) calculation defined in the CPR specification.
// The number of longitude zones decreases as latitude approaches the poles due to
// the convergence of meridians.
//
// Mathematical Background:
// - At the equator: 60 longitude zones (6° each)
// - At higher latitudes: fewer zones as meridians converge
// - At poles (±87°): only 2 zones (180° each)
//
// Formula: NL(lat) = floor(2π / arccos(1 - (1-cos(π/(2*NZ))) / cos²(lat)))
// Where NZ = 15 (number of latitude zones)
//
// Parameters:
// - lat: Latitude in decimal degrees
//
// Returns the number of longitude zones for this latitude.
func (d *Decoder) nlFunction(lat float64) float64 {
if math.Abs(lat) >= 87 {
return 2
}
nz := 15.0
a := 1 - math.Cos(math.Pi/(2*nz))
b := math.Pow(math.Cos(math.Pi/180.0*math.Abs(lat)), 2)
nl := 2 * math.Pi / math.Acos(1-a/b)
return math.Floor(nl)
}
// decodeVelocity extracts ground speed, track, and vertical rate from velocity messages.
//
// Velocity messages (TC 19) contain:
// - Ground speed components (East-West and North-South)
// - Vertical rate (climb/descent rate)
// - Intent change flag and other status bits
//
// Ground Speed Calculation:
// - East-West and North-South velocity components are encoded separately
// - Each component has a direction bit and magnitude
// - Ground speed = sqrt(EW² + NS²)
// - Track angle = atan2(EW, NS) converted to degrees
//
// Vertical Rate:
// - Encoded in 64 ft/min increments with sign bit
// - Range: approximately ±32,000 ft/min
//
// Parameters:
// - data: Extended squitter message containing velocity data
// - aircraft: Aircraft struct to update with velocity information
func (d *Decoder) decodeVelocity(data []byte, aircraft *Aircraft) {
subtype := (data[4]) & 0x07
if subtype == 1 || subtype == 2 {
// Ground speed
ewRaw := uint16(data[5]&0x03)<<8 | uint16(data[6])
nsRaw := uint16(data[7])<<3 | uint16(data[8])>>5
ewVel := float64(ewRaw - 1)
nsVel := float64(nsRaw - 1)
if data[5]&0x04 != 0 {
ewVel = -ewVel
}
if data[7]&0x80 != 0 {
nsVel = -nsVel
}
// Calculate ground speed in knots (rounded to integer)
speedKnots := math.Sqrt(ewVel*ewVel + nsVel*nsVel)
aircraft.GroundSpeed = int(math.Round(speedKnots))
// Calculate track in degrees (0-359)
trackDeg := math.Atan2(ewVel, nsVel) * 180 / math.Pi
if trackDeg < 0 {
trackDeg += 360
}
aircraft.Track = int(math.Round(trackDeg))
}
// Vertical rate
vrSign := (data[8] >> 3) & 0x01
vrBits := uint16(data[8]&0x07)<<6 | uint16(data[9])>>2
if vrBits != 0 {
aircraft.VerticalRate = int(vrBits-1) * 64
if vrSign != 0 {
aircraft.VerticalRate = -aircraft.VerticalRate
}
}
}
// decodeAltitude extracts altitude from Mode S surveillance altitude replies.
//
// Mode S altitude replies (DF4, DF20) contain a 13-bit altitude code that
// must be converted from the transmitted encoding to actual altitude in feet.
//
// Parameters:
// - data: Mode S altitude reply message
//
// Returns altitude in feet above sea level.
func (d *Decoder) decodeAltitude(data []byte) int {
altCode := uint16(data[2])<<8 | uint16(data[3])
return d.decodeAltitudeBits(altCode>>3, 0)
}
// decodeAltitudeBits converts encoded altitude bits to altitude in feet.
//
// Altitude Encoding:
// - Uses modified Gray code for error resilience
// - 12-bit altitude code with 25-foot increments
// - Offset of -1000 feet (code 0 = -1000 ft)
// - Gray code conversion prevents single-bit errors from causing large altitude jumps
//
// Different altitude sources:
// - Standard: Barometric altitude (QNH corrected)
// - GNSS: Geometric altitude (height above WGS84 ellipsoid)
//
// Parameters:
// - altCode: 12-bit encoded altitude value
// - tc: Type code (determines altitude source interpretation)
//
// Returns altitude in feet, or 0 for invalid altitude codes.
func (d *Decoder) decodeAltitudeBits(altCode uint16, tc uint8) int {
if altCode == 0 {
return 0
}
// Standard altitude encoding with 25 ft increments
// Check Q-bit (bit 4) for encoding type
qBit := (altCode >> 4) & 1
if qBit == 1 {
// Standard altitude with Q-bit set
// Remove Q-bit and reassemble 11-bit altitude code
n := ((altCode & 0x1F80) >> 2) | ((altCode & 0x0020) >> 1) | (altCode & 0x000F)
alt := int(n)*25 - 1000
// Validate altitude range
if alt < -1000 || alt > 60000 {
return 0
}
return alt
}
// Gray code altitude (100 ft increments) - legacy encoding
// Convert from Gray code to binary
n := altCode
n ^= n >> 8
n ^= n >> 4
n ^= n >> 2
n ^= n >> 1
// Convert to altitude in feet
alt := int(n&0x7FF) * 100
if alt < 0 || alt > 60000 {
return 0
}
return alt
}
// decodeSquawk extracts the 4-digit squawk (transponder) code from identity replies.
//
// Squawk codes are 4-digit octal numbers (0000-7777) used by air traffic control
// for aircraft identification. They are transmitted in surveillance identity
// replies (DF5, DF21) and formatted as octal strings.
//
// Parameters:
// - data: Mode S identity reply message
//
// Returns 4-digit octal squawk code as a string (e.g., "1200", "7700").
func (d *Decoder) decodeSquawk(data []byte) string {
code := uint16(data[2])<<8 | uint16(data[3])
return fmt.Sprintf("%04o", code>>3)
}
// getAircraftCategory converts type code and category fields to human-readable descriptions.
//
// Aircraft categories are encoded in identification messages using:
// - Type Code (TC): Broad category group (1-4)
// - Category (CA): Specific category within the group (0-7)
//
// Categories include:
// - TC 1: Reserved
// - TC 2: Surface vehicles (emergency, service, obstacles)
// - TC 3: Light aircraft (gliders, balloons, UAVs, etc.)
// - TC 4: Aircraft by weight class and performance
//
// These categories help ATC and other aircraft understand the type of vehicle
// and its performance characteristics for separation and routing.
//
// Parameters:
// - tc: Type code (1-4) from identification message
// - ca: Category field (0-7) providing specific subtype
//
// Returns human-readable category description.
func (d *Decoder) getAircraftCategory(tc uint8, ca uint8) string {
switch tc {
case 1:
return "Reserved"
case 2:
switch ca {
case 1:
return "Surface Emergency Vehicle"
case 3:
return "Surface Service Vehicle"
case 4, 5, 6, 7:
return "Ground Obstruction"
default:
return "Surface Vehicle"
}
case 3:
switch ca {
case 1:
return "Glider/Sailplane"
case 2:
return "Lighter-than-Air"
case 3:
return "Parachutist/Skydiver"
case 4:
return "Ultralight/Hang-glider"
case 6:
return "UAV"
case 7:
return "Space Vehicle"
default:
return "Light Aircraft"
}
case 4:
switch ca {
case 1:
return "Light < 7000kg"
case 2:
return "Medium 7000-34000kg"
case 3:
return "Large 34000-136000kg"
case 4:
return "High Vortex Large"
case 5:
return "Heavy > 136000kg"
case 6:
return "High Performance"
case 7:
return "Rotorcraft"
default:
return "Aircraft"
}
default:
return "Unknown"
}
}
// decodeStatus extracts emergency and priority status from aircraft status messages.
//
// Aircraft status messages (TC 28) contain emergency and priority codes that
// indicate special situations requiring ATC attention:
// - General emergency (Mayday)
// - Medical emergency (Lifeguard)
// - Minimum fuel
// - Communication failure
// - Unlawful interference (hijack)
// - Downed aircraft
//
// These codes trigger special handling by ATC and emergency services.
//
// Parameters:
// - data: Extended squitter message containing status information
// - aircraft: Aircraft struct to update with emergency status
func (d *Decoder) decodeStatus(data []byte, aircraft *Aircraft) {
subtype := data[4] & 0x07
if subtype == 1 {
// Emergency/priority status
emergency := (data[5] >> 5) & 0x07
switch emergency {
case 0:
aircraft.Emergency = "None"
case 1:
aircraft.Emergency = "General Emergency"
case 2:
aircraft.Emergency = "Lifeguard/Medical"
case 3:
aircraft.Emergency = "Minimum Fuel"
case 4:
aircraft.Emergency = "No Communications"
case 5:
aircraft.Emergency = "Unlawful Interference"
case 6:
aircraft.Emergency = "Downed Aircraft"
}
}
}
// decodeTargetState extracts autopilot and flight management system settings.
//
// Target state and status messages (TC 29) contain information about:
// - Selected altitude (MCP/FCU setting)
// - Barometric pressure setting (QNH)
// - Autopilot engagement status
// - Flight management system intentions
//
// This information helps ATC understand pilot intentions and autopilot settings,
// improving situational awareness and conflict prediction.
//
// Parameters:
// - data: Extended squitter message containing target state data
// - aircraft: Aircraft struct to update with autopilot settings
func (d *Decoder) decodeTargetState(data []byte, aircraft *Aircraft) {
// Selected altitude
altBits := uint16(data[5]&0x7F)<<4 | uint16(data[6])>>4
if altBits != 0 {
aircraft.SelectedAltitude = int(altBits)*32 - 32
}
// Barometric pressure setting
baroBits := uint16(data[7])<<1 | uint16(data[8])>>7
if baroBits != 0 {
aircraft.BaroSetting = float64(baroBits)*0.8 + 800
}
}
// decodeOperationalStatus extracts navigation accuracy and system capability information.
//
// Operational status messages (TC 31) contain:
// - Navigation Accuracy Category for Position (NACp): Position accuracy
// - Navigation Accuracy Category for Velocity (NACv): Velocity accuracy
// - Surveillance Integrity Level (SIL): System integrity confidence
//
// These parameters help receiving systems assess data quality and determine
// appropriate separation standards for the aircraft.
//
// Parameters:
// - data: Extended squitter message containing operational status
// - aircraft: Aircraft struct to update with accuracy indicators
func (d *Decoder) decodeOperationalStatus(data []byte, aircraft *Aircraft) {
// Navigation accuracy categories
aircraft.NACp = (data[7] >> 4) & 0x0F
aircraft.NACv = data[7] & 0x0F
aircraft.SIL = (data[8] >> 6) & 0x03
// Calculate combined signal quality from NACp, NACv, and SIL
d.calculateSignalQuality(aircraft)
}
// decodeSurfacePosition extracts position and movement data for aircraft on the ground.
//
// Surface position messages (TC 5-8) are used for airport ground movement tracking:
// - Ground speed and movement direction
// - Track angle (direction of movement)
// - CPR-encoded position (same algorithm as airborne)
// - On-ground flag is automatically set
//
// Ground Movement Encoding:
// - Speed ranges from stationary to 175+ knots in non-linear increments
// - Track is encoded in 128 discrete directions (2.8125° resolution)
// - Position uses the same CPR encoding as airborne messages
//
// Parameters:
// - data: Extended squitter message containing surface position data
// - aircraft: Aircraft struct to update with ground movement information
func (d *Decoder) decodeSurfacePosition(data []byte, aircraft *Aircraft) {
aircraft.OnGround = true
// Movement
movement := uint8(data[4]&0x07)<<4 | uint8(data[5])>>4
if movement > 0 && movement < 125 {
aircraft.GroundSpeed = int(math.Round(d.decodeGroundSpeed(movement)))
}
// Track
trackValid := (data[5] >> 3) & 0x01
if trackValid != 0 {
trackBits := uint16(data[5]&0x07)<<4 | uint16(data[6])>>4
aircraft.Track = int(math.Round(float64(trackBits) * 360.0 / 128.0))
}
// CPR position (similar to airborne)
cprLat := uint32(data[6]&0x03)<<15 | uint32(data[7])<<7 | uint32(data[8])>>1
cprLon := uint32(data[8]&0x01)<<16 | uint32(data[9])<<8 | uint32(data[10])
oddFlag := (data[6] >> 2) & 0x01
// Store CPR values for later decoding (protected by mutex)
d.mu.Lock()
if oddFlag == 1 {
d.cprOddLat[aircraft.ICAO24] = float64(cprLat) / 131072.0
d.cprOddLon[aircraft.ICAO24] = float64(cprLon) / 131072.0
} else {
d.cprEvenLat[aircraft.ICAO24] = float64(cprLat) / 131072.0
d.cprEvenLon[aircraft.ICAO24] = float64(cprLon) / 131072.0
}
d.mu.Unlock()
d.decodeCPRPosition(aircraft)
}
// decodeGroundSpeed converts the surface movement field to ground speed in knots.
//
// Surface movement is encoded in non-linear ranges optimized for typical
// ground operations:
// - 0: No movement information
// - 1: Stationary
// - 2-8: 0.125-1.0 kt (fine resolution for slow movement)
// - 9-12: 1.0-2.0 kt (taxi speeds)
// - 13-38: 2.0-15.0 kt (normal taxi)
// - 39-93: 15.0-70.0 kt (high speed taxi/runway)
// - 94-108: 70.0-100.0 kt (takeoff/landing roll)
// - 109-123: 100.0-175.0 kt (high speed operations)
// - 124: >175 kt
//
// Parameters:
// - movement: 7-bit movement field from surface position message
//
// Returns ground speed in knots, or 0 for invalid/no movement.
func (d *Decoder) decodeGroundSpeed(movement uint8) float64 {
if movement == 1 {
return 0
} else if movement >= 2 && movement <= 8 {
return float64(movement-2)*0.125 + 0.125
} else if movement >= 9 && movement <= 12 {
return float64(movement-9)*0.25 + 1.0
} else if movement >= 13 && movement <= 38 {
return float64(movement-13)*0.5 + 2.0
} else if movement >= 39 && movement <= 93 {
return float64(movement-39)*1.0 + 15.0
} else if movement >= 94 && movement <= 108 {
return float64(movement-94)*2.0 + 70.0
} else if movement >= 109 && movement <= 123 {
return float64(movement-109)*5.0 + 100.0
} else if movement == 124 {
return 175.0
}
return 0
}
// decodeAllCallReply extracts capability and interrogator identifier from DF11 messages.
//
// DF11 All-Call Reply messages contain:
// - Capability (CA) field (3 bits): transponder capabilities and modes
// - Interrogator Identifier (II) field (4 bits): which radar interrogated
// - ICAO24 address (24 bits): aircraft identifier
//
// The capability field indicates transponder features and operational modes:
// - 0: Level 1 transponder
// - 1: Level 2 transponder
// - 2: Level 2+ transponder with additional capabilities
// - 3: Level 2+ transponder with enhanced surveillance
// - 4: Level 2+ transponder with enhanced surveillance and extended squitter
// - 5: Level 2+ transponder with enhanced surveillance, extended squitter, and enhanced surveillance capability
// - 6: Level 2+ transponder with enhanced surveillance, extended squitter, and enhanced surveillance capability
// - 7: Level 2+ transponder, downlink request value is 0, or the flight status is alert, SPI, or emergency
//
// Parameters:
// - data: 7-byte DF11 message
// - aircraft: Aircraft struct to populate
func (d *Decoder) decodeAllCallReply(data []byte, aircraft *Aircraft) {
if len(data) < 7 {
return
}
// Extract Capability (CA) - bits 6-8 of first byte
capability := (data[0] >> 0) & 0x07
// Extract Interrogator Identifier (II) - would be in control field if present
// For DF11, this information is typically implied by the interrogating radar
// Store transponder capability information in dedicated fields
aircraft.TransponderLevel = capability
switch capability {
case 0:
aircraft.TransponderCapability = "Level 1"
case 1:
aircraft.TransponderCapability = "Level 2"
case 2, 3:
aircraft.TransponderCapability = "Level 2+"
case 4, 5, 6:
aircraft.TransponderCapability = "Enhanced"
case 7:
aircraft.TransponderCapability = "Alert/Emergency"
}
}
// decodeMilitaryExtendedSquitter processes DF19 military extended squitter messages.
//
// DF19 messages have the same structure as DF17/18 ADS-B extended squitter but
// may contain military-specific type codes or enhanced data formats.
// This implementation treats them similarly to civilian extended squitter
// but could be extended for military-specific capabilities.
//
// Parameters:
// - data: 14-byte DF19 message
// - aircraft: Aircraft struct to populate
//
// Returns updated Aircraft struct or error for malformed messages.
func (d *Decoder) decodeMilitaryExtendedSquitter(data []byte, aircraft *Aircraft) (*Aircraft, error) {
if len(data) != 14 {
return nil, fmt.Errorf("invalid military extended squitter length: %d bytes", len(data))
}
// For now, treat military extended squitter similar to civilian
// Could be enhanced to handle military-specific type codes
return d.decodeExtendedSquitter(data, aircraft)
}
// decodeCommD extracts data from DF24 Comm-D Enhanced Length Messages.
//
// DF24 messages are variable-length data link communications that can contain:
// - Weather information and updates
// - Flight plan modifications
// - Controller-pilot data link messages
// - Air traffic management information
// - Future air navigation system data
//
// Due to the complexity and variety of DF24 message content, this implementation
// provides basic structure extraction. Full decoding would require extensive
// knowledge of specific data link protocols and message formats.
//
// Parameters:
// - data: Variable-length DF24 message (minimum 7 bytes)
// - aircraft: Aircraft struct to populate
func (d *Decoder) decodeCommD(data []byte, aircraft *Aircraft) {
if len(data) < 7 {
return
}
// DF24 messages contain variable data that would require protocol-specific decoding
// For now, we note that this is a data communication message but don't overwrite aircraft category
// Could set a separate field for message type if needed in the future
// The actual message content would require:
// - Protocol identifier extraction
// - Message type determination
// - Format-specific field extraction
// - Possible message reassembly for multi-part messages
//
// This could be extended based on specific requirements and available documentation
}
// calculateSignalQuality combines NACp, NACv, and SIL into an overall data quality assessment.
//
// This function provides a human-readable quality indicator that considers:
// - Position accuracy (NACp): How precise the aircraft's position data is
// - Velocity accuracy (NACv): How precise the speed/heading data is
// - Surveillance integrity (SIL): How reliable/trustworthy the data is
//
// The algorithm prioritizes integrity first (SIL), then position accuracy (NACp),
// then velocity accuracy (NACv) to provide a meaningful overall assessment.
//
// Quality levels:
// - "Excellent": High integrity with very precise position/velocity
// - "Good": Good integrity with reasonable precision
// - "Fair": Moderate quality suitable for tracking
// - "Poor": Low quality but still usable
// - "Unknown": No quality indicators available
//
// Parameters:
// - aircraft: Aircraft struct containing NACp, NACv, and SIL values
func (d *Decoder) calculateSignalQuality(aircraft *Aircraft) {
nacp := aircraft.NACp
nacv := aircraft.NACv
sil := aircraft.SIL
// If no quality indicators are available, don't set anything
if nacp == 0 && nacv == 0 && sil == 0 {
// Don't overwrite existing quality assessment
return
}
// Excellent: High integrity with high accuracy OR very high accuracy alone
if (sil >= 2 && nacp >= 9) || nacp >= 10 {
aircraft.SignalQuality = "Excellent"
return
}
// Good: Good integrity with moderate accuracy OR high accuracy alone
if (sil >= 2 && nacp >= 6) || (sil >= 1 && nacp >= 9) || nacp >= 8 {
aircraft.SignalQuality = "Good"
return
}
// Fair: Some integrity with basic accuracy OR moderate accuracy alone
if (sil >= 1 && nacp >= 3) || nacp >= 5 {
aircraft.SignalQuality = "Fair"
return
}
// Poor: Low but usable quality indicators
if sil > 0 || nacp >= 1 || nacv > 0 {
aircraft.SignalQuality = "Poor"
return
}
// Default fallback
aircraft.SignalQuality = ""
}