// 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) // 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 } // 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 // 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 = "" }