Fix transponder capability overwriting aircraft type in popups

Add dedicated transponder fields to prevent Mode S decoder from overwriting
the aircraft Category field with transponder capability information.

**Problem Fixed:**
- DF11 All-Call Reply messages were setting aircraft.Category to "Enhanced Transponder"
- This overwrote the actual aircraft type (Light, Medium, Heavy, etc.) from ADS-B
- Users saw "Enhanced Transponder" instead of proper aircraft classification

**Solution:**
- Added dedicated TransponderCapability and TransponderLevel fields to Aircraft struct
- Updated JSON marshaling to include new transponder fields
- Modified decodeAllCallReply() to use dedicated fields instead of Category
- Enhanced popup display to show transponder info separately from aircraft type
- Removed Category overwriting in decodeCommD() for DF24 messages

**New Aircraft Fields:**
- TransponderCapability: Human-readable capability description
- TransponderLevel: Numeric capability level (0-7)

**Popup Display:**
- Aircraft type now shows correctly (Light, Medium, Heavy, etc.)
- Transponder capability displayed as separate "Transponder:" field when available
- Only shows transponder info when DF11 messages have been received

**Data Quality Indicators Clarification:**
- NACp: Navigation Accuracy Category - Position (0-11, higher = more accurate)
- NACv: Navigation Accuracy Category - Velocity (0-4, higher = more accurate)
- SIL: Surveillance Integrity Level (0-3, higher = more reliable)

Users now see both proper aircraft classification AND transponder capability
information without conflicts between different message types.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2025-08-24 19:28:12 +02:00
commit 056c2b8346
3 changed files with 178 additions and 46 deletions

View file

@ -112,29 +112,31 @@ func (a *AircraftState) MarshalJSON() ([]byte, error) {
// Create a struct that mirrors AircraftState but with ICAO24 as string
return json.Marshal(&struct {
// From embedded modes.Aircraft
ICAO24 string `json:"ICAO24"`
Callsign string `json:"Callsign"`
Latitude float64 `json:"Latitude"`
Longitude float64 `json:"Longitude"`
Altitude int `json:"Altitude"`
BaroAltitude int `json:"BaroAltitude"`
GeomAltitude int `json:"GeomAltitude"`
VerticalRate int `json:"VerticalRate"`
GroundSpeed int `json:"GroundSpeed"`
Track int `json:"Track"`
Heading int `json:"Heading"`
Category string `json:"Category"`
Squawk string `json:"Squawk"`
Emergency string `json:"Emergency"`
OnGround bool `json:"OnGround"`
Alert bool `json:"Alert"`
SPI bool `json:"SPI"`
NACp uint8 `json:"NACp"`
NACv uint8 `json:"NACv"`
SIL uint8 `json:"SIL"`
SelectedAltitude int `json:"SelectedAltitude"`
SelectedHeading float64 `json:"SelectedHeading"`
BaroSetting float64 `json:"BaroSetting"`
ICAO24 string `json:"ICAO24"`
Callsign string `json:"Callsign"`
Latitude float64 `json:"Latitude"`
Longitude float64 `json:"Longitude"`
Altitude int `json:"Altitude"`
BaroAltitude int `json:"BaroAltitude"`
GeomAltitude int `json:"GeomAltitude"`
VerticalRate int `json:"VerticalRate"`
GroundSpeed int `json:"GroundSpeed"`
Track int `json:"Track"`
Heading int `json:"Heading"`
Category string `json:"Category"`
Squawk string `json:"Squawk"`
Emergency string `json:"Emergency"`
OnGround bool `json:"OnGround"`
Alert bool `json:"Alert"`
SPI bool `json:"SPI"`
NACp uint8 `json:"NACp"`
NACv uint8 `json:"NACv"`
SIL uint8 `json:"SIL"`
TransponderCapability string `json:"TransponderCapability"`
TransponderLevel uint8 `json:"TransponderLevel"`
SelectedAltitude int `json:"SelectedAltitude"`
SelectedHeading float64 `json:"SelectedHeading"`
BaroSetting float64 `json:"BaroSetting"`
// From AircraftState
Sources map[string]*SourceData `json:"sources"`
@ -156,29 +158,31 @@ func (a *AircraftState) MarshalJSON() ([]byte, error) {
Flag string `json:"flag"`
}{
// Copy all fields from Aircraft
ICAO24: fmt.Sprintf("%06X", a.Aircraft.ICAO24),
Callsign: a.Aircraft.Callsign,
Latitude: a.Aircraft.Latitude,
Longitude: a.Aircraft.Longitude,
Altitude: a.Aircraft.Altitude,
BaroAltitude: a.Aircraft.BaroAltitude,
GeomAltitude: a.Aircraft.GeomAltitude,
VerticalRate: a.Aircraft.VerticalRate,
GroundSpeed: a.Aircraft.GroundSpeed,
Track: a.Aircraft.Track,
Heading: a.Aircraft.Heading,
Category: a.Aircraft.Category,
Squawk: a.Aircraft.Squawk,
Emergency: a.Aircraft.Emergency,
OnGround: a.Aircraft.OnGround,
Alert: a.Aircraft.Alert,
SPI: a.Aircraft.SPI,
NACp: a.Aircraft.NACp,
NACv: a.Aircraft.NACv,
SIL: a.Aircraft.SIL,
SelectedAltitude: a.Aircraft.SelectedAltitude,
SelectedHeading: a.Aircraft.SelectedHeading,
BaroSetting: a.Aircraft.BaroSetting,
ICAO24: fmt.Sprintf("%06X", a.Aircraft.ICAO24),
Callsign: a.Aircraft.Callsign,
Latitude: a.Aircraft.Latitude,
Longitude: a.Aircraft.Longitude,
Altitude: a.Aircraft.Altitude,
BaroAltitude: a.Aircraft.BaroAltitude,
GeomAltitude: a.Aircraft.GeomAltitude,
VerticalRate: a.Aircraft.VerticalRate,
GroundSpeed: a.Aircraft.GroundSpeed,
Track: a.Aircraft.Track,
Heading: a.Aircraft.Heading,
Category: a.Aircraft.Category,
Squawk: a.Aircraft.Squawk,
Emergency: a.Aircraft.Emergency,
OnGround: a.Aircraft.OnGround,
Alert: a.Aircraft.Alert,
SPI: a.Aircraft.SPI,
NACp: a.Aircraft.NACp,
NACv: a.Aircraft.NACv,
SIL: a.Aircraft.SIL,
TransponderCapability: a.Aircraft.TransponderCapability,
TransponderLevel: a.Aircraft.TransponderLevel,
SelectedAltitude: a.Aircraft.SelectedAltitude,
SelectedHeading: a.Aircraft.SelectedHeading,
BaroSetting: a.Aircraft.BaroSetting,
// Copy all fields from AircraftState
Sources: a.Sources,

View file

@ -138,6 +138,10 @@ type Aircraft struct {
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)
// Autopilot/Flight Management
SelectedAltitude int // MCP/FCU selected altitude in feet
SelectedHeading float64 // MCP/FCU selected heading in degrees
@ -231,12 +235,29 @@ func (d *Decoder) Decode(data []byte) (*Aircraft, error) {
}
switch df {
case DF0:
// Short Air-Air Surveillance (ACAS)
aircraft.Altitude = d.decodeAltitude(data)
// Additional ACAS-specific data could be extracted here
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)
// Additional ACAS-specific data could be extracted here
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)
}
return aircraft, nil
@ -940,3 +961,106 @@ func (d *Decoder) decodeGroundSpeed(movement uint8) float64 {
}
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 Transponder"
case 1:
aircraft.TransponderCapability = "Level 2 Transponder"
case 2, 3:
aircraft.TransponderCapability = "Level 2+ Transponder"
case 4, 5, 6:
aircraft.TransponderCapability = "Enhanced Transponder"
case 7:
aircraft.TransponderCapability = "Alert/Emergency Status"
}
}
// 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
}