diff --git a/assets/static/css/style.css b/assets/static/css/style.css
index 750603c..5389c2d 100644
--- a/assets/static/css/style.css
+++ b/assets/static/css/style.css
@@ -679,4 +679,62 @@ body {
#aircraft-table td {
padding: 0.5rem 0.25rem;
}
+}
+
+/* Squawk Code Styling */
+.squawk-emergency {
+ background: #dc3545;
+ color: white;
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-weight: bold;
+ cursor: help;
+ border: 1px solid #b52532;
+}
+
+.squawk-special {
+ background: #fd7e14;
+ color: white;
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-weight: 500;
+ cursor: help;
+ border: 1px solid #e06911;
+}
+
+.squawk-military {
+ background: #6c757d;
+ color: white;
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-weight: 500;
+ cursor: help;
+ border: 1px solid #565e64;
+}
+
+.squawk-standard {
+ background: #28a745;
+ color: white;
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-weight: normal;
+ cursor: help;
+ border: 1px solid #1e7e34;
+}
+
+/* Hover effects for squawk codes */
+.squawk-emergency:hover {
+ background: #c82333;
+}
+
+.squawk-special:hover {
+ background: #e8590c;
+}
+
+.squawk-military:hover {
+ background: #5a6268;
+}
+
+.squawk-standard:hover {
+ background: #218838;
}
\ No newline at end of file
diff --git a/assets/static/js/modules/ui-manager.js b/assets/static/js/modules/ui-manager.js
index 88c692c..b891096 100644
--- a/assets/static/js/modules/ui-manager.js
+++ b/assets/static/js/modules/ui-manager.js
@@ -122,7 +122,7 @@ export class UIManager {
row.innerHTML = `
${icao} |
${aircraft.Callsign || '-'} |
- ${aircraft.Squawk || '-'} |
+ ${this.formatSquawk(aircraft)} |
${altitude ? `${altitude} ft` : '-'} |
${aircraft.GroundSpeed || '-'} kt |
${distance ? distance.toFixed(1) : '-'} km |
@@ -180,6 +180,33 @@ export class UIManager {
return 'signal-poor';
}
+ formatSquawk(aircraft) {
+ if (!aircraft.Squawk) return '-';
+
+ // If we have a description, format it nicely
+ if (aircraft.SquawkDescription) {
+ // Check if it's an emergency code (contains warning emoji)
+ if (aircraft.SquawkDescription.includes('⚠️')) {
+ return `${aircraft.Squawk}`;
+ }
+ // Check if it's a special code (contains special emoji)
+ else if (aircraft.SquawkDescription.includes('🔸')) {
+ return `${aircraft.Squawk}`;
+ }
+ // Check if it's a military code (contains military emoji)
+ else if (aircraft.SquawkDescription.includes('🔰')) {
+ return `${aircraft.Squawk}`;
+ }
+ // Standard codes
+ else {
+ return `${aircraft.Squawk}`;
+ }
+ }
+
+ // No description available, show just the code
+ return aircraft.Squawk;
+ }
+
updateSourceFilter() {
const select = document.getElementById('source-filter');
if (!select) return;
diff --git a/internal/merger/merger.go b/internal/merger/merger.go
index 7274219..5fe3a80 100644
--- a/internal/merger/merger.go
+++ b/internal/merger/merger.go
@@ -29,6 +29,7 @@ import (
"skyview/internal/icao"
"skyview/internal/modes"
+ "skyview/internal/squawk"
)
const (
@@ -126,6 +127,7 @@ func (a *AircraftState) MarshalJSON() ([]byte, error) {
Heading int `json:"Heading"`
Category string `json:"Category"`
Squawk string `json:"Squawk"`
+ SquawkDescription string `json:"SquawkDescription"`
Emergency string `json:"Emergency"`
OnGround bool `json:"OnGround"`
Alert bool `json:"Alert"`
@@ -173,6 +175,7 @@ func (a *AircraftState) MarshalJSON() ([]byte, error) {
Heading: a.Aircraft.Heading,
Category: a.Aircraft.Category,
Squawk: a.Aircraft.Squawk,
+ SquawkDescription: a.Aircraft.SquawkDescription,
Emergency: a.Aircraft.Emergency,
OnGround: a.Aircraft.OnGround,
Alert: a.Aircraft.Alert,
@@ -268,6 +271,7 @@ type Merger struct {
aircraft map[uint32]*AircraftState // ICAO24 -> merged aircraft state
sources map[string]*Source // Source ID -> source information
icaoDB *icao.Database // ICAO country lookup database
+ squawkDB *squawk.Database // Transponder code lookup database
mu sync.RWMutex // Protects all maps and slices
historyLimit int // Maximum history points to retain
staleTimeout time.Duration // Time before aircraft considered stale (15 seconds)
@@ -296,10 +300,13 @@ func NewMerger() (*Merger, error) {
return nil, fmt.Errorf("failed to initialize ICAO database: %w", err)
}
+ squawkDB := squawk.NewDatabase()
+
return &Merger{
aircraft: make(map[uint32]*AircraftState),
sources: make(map[string]*Source),
icaoDB: icaoDB,
+ squawkDB: squawkDB,
historyLimit: 500,
staleTimeout: 15 * time.Second, // Aircraft timeout - reasonable for ADS-B tracking
updateMetrics: make(map[uint32]*updateMetric),
@@ -535,6 +542,8 @@ func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, so
}
if new.Squawk != "" {
state.Squawk = new.Squawk
+ // Look up squawk description
+ state.SquawkDescription = m.squawkDB.FormatSquawkWithDescription(new.Squawk)
}
if new.Category != "" {
state.Category = new.Category
diff --git a/internal/modes/decoder.go b/internal/modes/decoder.go
index e120abf..959fa19 100644
--- a/internal/modes/decoder.go
+++ b/internal/modes/decoder.go
@@ -130,8 +130,9 @@ type Aircraft struct {
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)
+ 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
diff --git a/internal/squawk/squawk.go b/internal/squawk/squawk.go
new file mode 100644
index 0000000..2647bd1
--- /dev/null
+++ b/internal/squawk/squawk.go
@@ -0,0 +1,393 @@
+// Package squawk provides transponder code lookup and interpretation functionality.
+//
+// This package implements a comprehensive database of transponder (squawk) codes
+// used in aviation, providing textual descriptions and categorizations for
+// common codes including emergency, operational, and special-purpose codes.
+//
+// The lookup system supports:
+// - Emergency codes (7500, 7600, 7700)
+// - Standard VFR/IFR operational codes
+// - Military and special operations codes
+// - Regional variations and custom codes
+//
+// Code categories help with UI presentation and priority handling, with
+// emergency codes receiving special visual treatment in the interface.
+package squawk
+
+import (
+ "fmt"
+ "strconv"
+)
+
+// CodeType represents the category of a transponder code for UI presentation
+type CodeType int
+
+const (
+ // Unknown represents codes not in the lookup database
+ Unknown CodeType = iota
+ // Emergency represents emergency situation codes (7500, 7600, 7700)
+ Emergency
+ // Standard represents common operational codes (1200, 7000, etc.)
+ Standard
+ // Military represents military operation codes
+ Military
+ // Special represents special purpose codes (SAR, firefighting, etc.)
+ Special
+)
+
+// String returns the string representation of a CodeType
+func (ct CodeType) String() string {
+ switch ct {
+ case Emergency:
+ return "Emergency"
+ case Standard:
+ return "Standard"
+ case Military:
+ return "Military"
+ case Special:
+ return "Special"
+ default:
+ return "Unknown"
+ }
+}
+
+// CodeInfo contains detailed information about a transponder code
+type CodeInfo struct {
+ Code string `json:"code"` // The transponder code (e.g., "7700")
+ Description string `json:"description"` // Human-readable description
+ Type CodeType `json:"type"` // Category of the code
+ Region string `json:"region"` // Geographic region (e.g., "Global", "ICAO", "US", "EU")
+ Priority int `json:"priority"` // Display priority (higher = more important)
+ Notes string `json:"notes"` // Additional context or usage notes
+}
+
+// Database contains the transponder code lookup database
+type Database struct {
+ codes map[string]*CodeInfo // Map of code -> CodeInfo
+}
+
+// NewDatabase creates and initializes a new transponder code database
+// with comprehensive coverage of common aviation transponder codes
+func NewDatabase() *Database {
+ db := &Database{
+ codes: make(map[string]*CodeInfo),
+ }
+
+ // Initialize with standard transponder codes
+ db.loadStandardCodes()
+
+ return db
+}
+
+// loadStandardCodes populates the database with standard transponder codes
+func (db *Database) loadStandardCodes() {
+ codes := []*CodeInfo{
+ // Emergency Codes (Highest Priority)
+ {
+ Code: "7500",
+ Description: "Hijacking/Unlawful Interference",
+ Type: Emergency,
+ Region: "Global",
+ Priority: 100,
+ Notes: "Aircraft under unlawful interference or hijacking",
+ },
+ {
+ Code: "7600",
+ Description: "Radio Failure",
+ Type: Emergency,
+ Region: "Global",
+ Priority: 95,
+ Notes: "Loss of radio communication capability",
+ },
+ {
+ Code: "7700",
+ Description: "General Emergency",
+ Type: Emergency,
+ Region: "Global",
+ Priority: 90,
+ Notes: "General emergency situation requiring immediate attention",
+ },
+
+ // Standard VFR/IFR Codes
+ {
+ Code: "1200",
+ Description: "VFR - Visual Flight Rules",
+ Type: Standard,
+ Region: "US/Canada",
+ Priority: 10,
+ Notes: "Standard VFR code for uncontrolled airspace in North America",
+ },
+ {
+ Code: "7000",
+ Description: "VFR - Visual Flight Rules",
+ Type: Standard,
+ Region: "ICAO/Europe",
+ Priority: 10,
+ Notes: "Standard VFR code for most ICAO regions",
+ },
+ {
+ Code: "2000",
+ Description: "Uncontrolled Airspace",
+ Type: Standard,
+ Region: "Various",
+ Priority: 8,
+ Notes: "Used in some regions for uncontrolled airspace operations",
+ },
+ {
+ Code: "0000",
+ Description: "No Transponder/Military",
+ Type: Military,
+ Region: "Global",
+ Priority: 5,
+ Notes: "No transponder assigned or military operations",
+ },
+ {
+ Code: "1000",
+ Description: "Mode A/C Not Assigned",
+ Type: Standard,
+ Region: "Global",
+ Priority: 5,
+ Notes: "Transponder operating but no specific code assigned",
+ },
+
+ // Special Purpose Codes
+ {
+ Code: "1255",
+ Description: "Fire Fighting Aircraft",
+ Type: Special,
+ Region: "US",
+ Priority: 25,
+ Notes: "Aircraft engaged in firefighting operations",
+ },
+ {
+ Code: "1277",
+ Description: "Search and Rescue",
+ Type: Special,
+ Region: "US",
+ Priority: 30,
+ Notes: "Search and rescue operations",
+ },
+ {
+ Code: "7777",
+ Description: "Military Interceptor",
+ Type: Military,
+ Region: "US",
+ Priority: 35,
+ Notes: "Military interceptor aircraft",
+ },
+
+ // Military Ranges
+ {
+ Code: "4000",
+ Description: "Military Low Altitude",
+ Type: Military,
+ Region: "US",
+ Priority: 15,
+ Notes: "Military operations at low altitude (4000-4777 range)",
+ },
+ {
+ Code: "0100",
+ Description: "Military Operations",
+ Type: Military,
+ Region: "Various",
+ Priority: 12,
+ Notes: "Military interceptor operations (0100-0777 range)",
+ },
+
+ // Additional Common Codes
+ {
+ Code: "1201",
+ Description: "VFR - High Altitude",
+ Type: Standard,
+ Region: "US",
+ Priority: 8,
+ Notes: "VFR operations above 12,500 feet MSL",
+ },
+ {
+ Code: "4401",
+ Description: "Glider Operations",
+ Type: Special,
+ Region: "US",
+ Priority: 8,
+ Notes: "Glider and soaring aircraft operations",
+ },
+ {
+ Code: "1202",
+ Description: "VFR - Above 12,500ft",
+ Type: Standard,
+ Region: "US",
+ Priority: 8,
+ Notes: "VFR flight above 12,500 feet requiring transponder",
+ },
+
+ // European Specific
+ {
+ Code: "7001",
+ Description: "Conspicuity Code A",
+ Type: Standard,
+ Region: "Europe",
+ Priority: 10,
+ Notes: "Enhanced visibility in European airspace",
+ },
+ {
+ Code: "7004",
+ Description: "Aerobatic Flight",
+ Type: Special,
+ Region: "Europe",
+ Priority: 12,
+ Notes: "Aircraft engaged in aerobatic maneuvers",
+ },
+ {
+ Code: "7010",
+ Description: "GAT in OAT Area",
+ Type: Special,
+ Region: "Europe",
+ Priority: 8,
+ Notes: "General Air Traffic operating in Other Air Traffic area",
+ },
+ }
+
+ // Add all codes to the database
+ for _, code := range codes {
+ db.codes[code.Code] = code
+ }
+}
+
+// Lookup returns information about a given transponder code
+//
+// The method accepts both 4-digit strings and integers, automatically
+// formatting them as needed. Returns nil if the code is not found in the database.
+//
+// Parameters:
+// - code: Transponder code as string (e.g., "7700") or can be called with fmt.Sprintf
+//
+// Returns:
+// - *CodeInfo: Detailed information about the code, or nil if not found
+func (db *Database) Lookup(code string) *CodeInfo {
+ if info, exists := db.codes[code]; exists {
+ return info
+ }
+ return nil
+}
+
+// LookupInt is a convenience method for looking up codes as integers
+//
+// Parameters:
+// - code: Transponder code as integer (e.g., 7700)
+//
+// Returns:
+// - *CodeInfo: Detailed information about the code, or nil if not found
+func (db *Database) LookupInt(code int) *CodeInfo {
+ codeStr := fmt.Sprintf("%04d", code)
+ return db.Lookup(codeStr)
+}
+
+// LookupHex is a convenience method for looking up codes from hex strings
+//
+// Transponder codes are sometimes transmitted in hex format. This method
+// converts hex to decimal before lookup.
+//
+// Parameters:
+// - hexCode: Transponder code as hex string (e.g., "1E14" for 7700)
+//
+// Returns:
+// - *CodeInfo: Detailed information about the code, or nil if not found or invalid hex
+func (db *Database) LookupHex(hexCode string) *CodeInfo {
+ // Convert hex to decimal
+ if decimal, err := strconv.ParseInt(hexCode, 16, 16); err == nil {
+ // Convert decimal to 4-digit octal representation (squawk codes are octal)
+ octal := fmt.Sprintf("%04o", decimal)
+ return db.Lookup(octal)
+ }
+ return nil
+}
+
+// GetEmergencyCodes returns all emergency codes in the database
+//
+// Returns:
+// - []*CodeInfo: Slice of all emergency codes, sorted by priority (highest first)
+func (db *Database) GetEmergencyCodes() []*CodeInfo {
+ var emergencyCodes []*CodeInfo
+
+ for _, info := range db.codes {
+ if info.Type == Emergency {
+ emergencyCodes = append(emergencyCodes, info)
+ }
+ }
+
+ // Sort by priority (highest first)
+ for i := 0; i < len(emergencyCodes); i++ {
+ for j := i + 1; j < len(emergencyCodes); j++ {
+ if emergencyCodes[i].Priority < emergencyCodes[j].Priority {
+ emergencyCodes[i], emergencyCodes[j] = emergencyCodes[j], emergencyCodes[i]
+ }
+ }
+ }
+
+ return emergencyCodes
+}
+
+// IsEmergencyCode returns true if the given code represents an emergency situation
+//
+// Parameters:
+// - code: Transponder code as string (e.g., "7700")
+//
+// Returns:
+// - bool: True if the code is an emergency code
+func (db *Database) IsEmergencyCode(code string) bool {
+ if info := db.Lookup(code); info != nil {
+ return info.Type == Emergency
+ }
+ return false
+}
+
+// GetAllCodes returns all codes in the database
+//
+// Returns:
+// - []*CodeInfo: Slice of all codes in the database
+func (db *Database) GetAllCodes() []*CodeInfo {
+ codes := make([]*CodeInfo, 0, len(db.codes))
+ for _, info := range db.codes {
+ codes = append(codes, info)
+ }
+ return codes
+}
+
+// AddCustomCode allows adding custom transponder codes to the database
+//
+// This is useful for regional codes or organization-specific codes not
+// included in the standard database.
+//
+// Parameters:
+// - info: CodeInfo struct with the custom code information
+func (db *Database) AddCustomCode(info *CodeInfo) {
+ db.codes[info.Code] = info
+}
+
+// FormatSquawkWithDescription returns a formatted string combining code and description
+//
+// Formats transponder codes for display with appropriate emergency indicators.
+// Emergency codes are prefixed with warning symbols for visual emphasis.
+//
+// Parameters:
+// - code: Transponder code as string (e.g., "7700")
+//
+// Returns:
+// - string: Formatted string like "7700 (⚠️ EMERGENCY - General)" or "1200 (VFR - Visual Flight Rules)"
+func (db *Database) FormatSquawkWithDescription(code string) string {
+ info := db.Lookup(code)
+ if info == nil {
+ return code // Return just the code if no description available
+ }
+
+ switch info.Type {
+ case Emergency:
+ return fmt.Sprintf("%s (⚠️ EMERGENCY - %s)", code, info.Description)
+ case Special:
+ return fmt.Sprintf("%s (🔸 %s)", code, info.Description)
+ case Military:
+ return fmt.Sprintf("%s (🔰 %s)", code, info.Description)
+ default:
+ return fmt.Sprintf("%s (%s)", code, info.Description)
+ }
+}
\ No newline at end of file
diff --git a/internal/squawk/squawk_test.go b/internal/squawk/squawk_test.go
new file mode 100644
index 0000000..6a008db
--- /dev/null
+++ b/internal/squawk/squawk_test.go
@@ -0,0 +1,209 @@
+package squawk
+
+import (
+ "testing"
+)
+
+func TestNewDatabase(t *testing.T) {
+ db := NewDatabase()
+ if db == nil {
+ t.Fatal("NewDatabase() returned nil")
+ }
+
+ if len(db.codes) == 0 {
+ t.Error("Database should contain pre-loaded codes")
+ }
+}
+
+func TestEmergencyCodes(t *testing.T) {
+ db := NewDatabase()
+
+ emergencyCodes := []string{"7500", "7600", "7700"}
+
+ for _, code := range emergencyCodes {
+ info := db.Lookup(code)
+ if info == nil {
+ t.Errorf("Emergency code %s not found", code)
+ continue
+ }
+
+ if info.Type != Emergency {
+ t.Errorf("Code %s should be Emergency type, got %s", code, info.Type)
+ }
+
+ if !db.IsEmergencyCode(code) {
+ t.Errorf("IsEmergencyCode(%s) should return true", code)
+ }
+ }
+}
+
+func TestStandardCodes(t *testing.T) {
+ db := NewDatabase()
+
+ testCases := []struct {
+ code string
+ description string
+ }{
+ {"1200", "VFR - Visual Flight Rules"},
+ {"7000", "VFR - Visual Flight Rules"},
+ {"1000", "Mode A/C Not Assigned"},
+ }
+
+ for _, tc := range testCases {
+ info := db.Lookup(tc.code)
+ if info == nil {
+ t.Errorf("Standard code %s not found", tc.code)
+ continue
+ }
+
+ if info.Description != tc.description {
+ t.Errorf("Code %s: expected description %q, got %q",
+ tc.code, tc.description, info.Description)
+ }
+ }
+}
+
+func TestLookupInt(t *testing.T) {
+ db := NewDatabase()
+
+ // Test integer lookup
+ info := db.LookupInt(7700)
+ if info == nil {
+ t.Fatal("LookupInt(7700) returned nil")
+ }
+
+ if info.Code != "7700" {
+ t.Errorf("Expected code '7700', got '%s'", info.Code)
+ }
+
+ if info.Type != Emergency {
+ t.Errorf("Code 7700 should be Emergency type, got %s", info.Type)
+ }
+}
+
+func TestLookupHex(t *testing.T) {
+ db := NewDatabase()
+
+ // 7700 in octal is 3840 in decimal, which is F00 in hex
+ // However, squawk codes are transmitted differently in different formats
+ // For now, test with a simple hex conversion
+
+ // Test invalid hex
+ info := db.LookupHex("INVALID")
+ if info != nil {
+ t.Error("LookupHex with invalid hex should return nil")
+ }
+}
+
+func TestFormatSquawkWithDescription(t *testing.T) {
+ db := NewDatabase()
+
+ testCases := []struct {
+ code string
+ expected string
+ }{
+ {"7700", "7700 (⚠️ EMERGENCY - General Emergency)"},
+ {"1200", "1200 (VFR - Visual Flight Rules)"},
+ {"1277", "1277 (🔸 Search and Rescue)"},
+ {"0000", "0000 (🔰 No Transponder/Military)"},
+ {"9999", "9999"}, // Unknown code should return just the code
+ }
+
+ for _, tc := range testCases {
+ result := db.FormatSquawkWithDescription(tc.code)
+ if result != tc.expected {
+ t.Errorf("FormatSquawkWithDescription(%s): expected %q, got %q",
+ tc.code, tc.expected, result)
+ }
+ }
+}
+
+func TestGetEmergencyCodes(t *testing.T) {
+ db := NewDatabase()
+
+ emergencyCodes := db.GetEmergencyCodes()
+ if len(emergencyCodes) != 3 {
+ t.Errorf("Expected 3 emergency codes, got %d", len(emergencyCodes))
+ }
+
+ // Check that they're sorted by priority (highest first)
+ for i := 1; i < len(emergencyCodes); i++ {
+ if emergencyCodes[i-1].Priority < emergencyCodes[i].Priority {
+ t.Error("Emergency codes should be sorted by priority (highest first)")
+ }
+ }
+}
+
+func TestAddCustomCode(t *testing.T) {
+ db := NewDatabase()
+
+ customCode := &CodeInfo{
+ Code: "1234",
+ Description: "Test Custom Code",
+ Type: Special,
+ Region: "Test",
+ Priority: 50,
+ Notes: "This is a test custom code",
+ }
+
+ db.AddCustomCode(customCode)
+
+ info := db.Lookup("1234")
+ if info == nil {
+ t.Fatal("Custom code not found after adding")
+ }
+
+ if info.Description != "Test Custom Code" {
+ t.Errorf("Custom code description mismatch: expected %q, got %q",
+ "Test Custom Code", info.Description)
+ }
+}
+
+func TestCodeTypeString(t *testing.T) {
+ testCases := []struct {
+ codeType CodeType
+ expected string
+ }{
+ {Unknown, "Unknown"},
+ {Emergency, "Emergency"},
+ {Standard, "Standard"},
+ {Military, "Military"},
+ {Special, "Special"},
+ }
+
+ for _, tc := range testCases {
+ result := tc.codeType.String()
+ if result != tc.expected {
+ t.Errorf("CodeType.String(): expected %q, got %q", tc.expected, result)
+ }
+ }
+}
+
+func TestGetAllCodes(t *testing.T) {
+ db := NewDatabase()
+
+ allCodes := db.GetAllCodes()
+ if len(allCodes) == 0 {
+ t.Error("GetAllCodes() should return non-empty slice")
+ }
+
+ // Verify we can find known codes in the result
+ found7700 := false
+ found1200 := false
+
+ for _, code := range allCodes {
+ if code.Code == "7700" {
+ found7700 = true
+ }
+ if code.Code == "1200" {
+ found1200 = true
+ }
+ }
+
+ if !found7700 {
+ t.Error("Emergency code 7700 not found in GetAllCodes() result")
+ }
+ if !found1200 {
+ t.Error("Standard code 1200 not found in GetAllCodes() result")
+ }
+}
\ No newline at end of file