Specialized Namespaces

Audio transcriptions, radio monitoring, and mobile threat detection namespaces

Specialized Namespaces

SkySpy provides dedicated namespaces for specialized features: audio/radio monitoring and mobile threat detection (Cannonball).

Overview

NamespacePathPurposePermission
Audio/audioRadio transmissions and transcriptionsRequires audio permission
Cannonball/cannonballMobile threat detection and proximity alertsPublic or authenticated
ACARS (optional)/acarsACARS-only stream for specialized clientsPublic or authenticated
📘

When to Use Specialized Namespaces

Use specialized namespaces when you only need a specific feature (e.g., audio monitoring) without aircraft tracking. This reduces bandwidth and simplifies client logic.

Audio Namespace (/audio)

Monitor radio transmissions and receive real-time transcriptions from ATC feeds.

Connection

const audioSocket = io('https://skyspy.example.com/audio', {
  path: '/socket.io',
  auth: { token: 'your_token' },
  transports: ['websocket']
});

audioSocket.on('connect', () => {
  console.log('Connected to audio namespace');
});
import socketio

sio = socketio.Client()

@sio.event
def connect():
    print('Connected to audio namespace')

sio.connect(
    'https://skyspy.example.com/audio',
    socketio_path='/socket.io',
    auth={'token': 'your_token'},
    transports=['websocket']
)
⚠️

Permission Required

The audio namespace requires the audio topic/feature permission. Ensure your user or API key has this permission enabled.

Audio Events

EventTriggerPayloadDescription
audio:snapshotOn connect{ transmissions[], count, timestamp }Recent transmissions snapshot
audio:transmissionNew radio transmissionTransmission objectRaw audio metadata and URL
audio:transcription_startedTranscription begins{ transmission_id, status }Transcription job started
audio:transcription_completedTranscription finished{ transmission_id, text, confidence }Transcribed text with confidence score
audio:transcription_failedTranscription error{ transmission_id, error }Transcription failed with error message

Transmission Payload

FieldTypeDescriptionExample
idstringUnique transmission ID"tx_abc123"
frequencynumberFrequency in MHz118.5
durationnumberDuration in seconds4.2
timestampstringISO 8601 timestamp"2024-01-15T10:30:00Z"
audio_urlstringURL to audio file (WAV or MP3)"https://cdn.../tx_abc123.mp3"
icao_hexstringAssociated aircraft (if known)"A1B2C3"
callsignstringAssociated callsign (if known)"UAL123"
transcriptionobjectTranscription data (if available)See transcription fields

Audio Example

audioSocket.on('audio:snapshot', (data) => {
  console.log(`Recent transmissions: ${data.count}`);
  data.transmissions.forEach(tx => {
    displayTransmission(tx);
  });
});

audioSocket.on('audio:transmission', (tx) => {
  console.log(`New transmission on ${tx.frequency} MHz`);
  
  // Play audio if autoplay enabled
  if (autoplayEnabled) {
    playAudio(tx.audio_url);
  }
  
  // Add to transmission list
  addToTransmissionList(tx);
});

audioSocket.on('audio:transcription_completed', (data) => {
  console.log(`Transcription: ${data.text}`);
  
  // Update UI with transcription
  updateTranscription(data.transmission_id, {
    text: data.text,
    confidence: data.confidence
  });
  
  // Highlight keywords
  if (containsKeywords(data.text, ['emergency', 'mayday', 'pan pan'])) {
    highlightTransmission(data.transmission_id, 'emergency');
  }
});

audioSocket.on('audio:transcription_failed', (data) => {
  console.warn(`Transcription failed: ${data.error}`);
  updateTranscription(data.transmission_id, {
    error: data.error
  });
});
@sio.event
def audio_snapshot(data):
    print(f"Recent transmissions: {data.get('count')}")
    for tx in data.get('transmissions', []):
        display_transmission(tx)

@sio.event
def audio_transmission(tx):
    print(f"New transmission on {tx.get('frequency')} MHz")
    
    # Play audio if autoplay enabled
    if autoplay_enabled:
        play_audio(tx.get('audio_url'))
    
    # Add to transmission list
    add_to_transmission_list(tx)

@sio.event
def audio_transcription_completed(data):
    print(f"Transcription: {data.get('text')}")
    
    # Update UI with transcription
    update_transcription(data.get('transmission_id'), {
        'text': data.get('text'),
        'confidence': data.get('confidence')
    })
    
    # Highlight keywords
    text = data.get('text', '').lower()
    if any(kw in text for kw in ['emergency', 'mayday', 'pan pan']):
        highlight_transmission(data.get('transmission_id'), 'emergency')

@sio.event
def audio_transcription_failed(data):
    print(f"Transcription failed: {data.get('error')}")
    update_transcription(data.get('transmission_id'), {
        'error': data.get('error')
    })

Audio Request Types

Use the request event to query historical transmissions and statistics.

Request TypeParametersDescription
transmissionshours, limit, offset, frequency, icao_hexQuery historical transmissions with filtering
transmissionidGet single transmission by ID
statshoursAudio statistics (transmissions per frequency, duration, etc.)

Cannonball Namespace (/cannonball)

Mobile threat detection for real-time proximity alerts. Designed for mobile apps that need low-latency threat detection based on the user's location.

Connection

const cannonballSocket = io('https://skyspy.example.com/cannonball', {
  path: '/socket.io',
  auth: { token: 'your_token' },
  transports: ['websocket']
});

cannonballSocket.on('connect', () => {
  console.log('Connected to cannonball namespace');
});
import socketio

sio = socketio.Client()

@sio.event
def connect():
    print('Connected to cannonball namespace')

sio.connect(
    'https://skyspy.example.com/cannonball',
    socketio_path='/socket.io',
    auth={'token': 'your_token'},
    transports=['websocket']
)

Cannonball Flow

sequenceDiagram
    participant M as Mobile App
    participant S as Server

    M->>S: connect()
    S->>M: session_started { session_id }

    loop Location Updates
        M->>S: position_update { lat, lon, heading? }
        S->>M: threats { aircraft[], threat_level }
    end

    M->>S: set_radius { radius_nm }
    S->>M: radius_updated { radius_nm }

Client → Server Events

EventPayloadDescriptionExample
position_update{ lat, lon, heading?, altitude? }Update mobile device position{ lat: 37.7749, lon: -122.4194 }
set_radius{ radius_nm }Set threat detection radius in nautical miles{ radius_nm: 10 }
get_threatsRequest immediate threat updatenull

Server → Client Events

EventTriggerPayloadDescription
session_startedOn connect{ session_id, default_radius_nm }Session initialization
threatsAfter position_update or periodic{ aircraft[], threat_level, timestamp }Filtered threats within radius
radius_updatedAfter set_radius{ radius_nm }Confirms radius change
errorInvalid request{ message }Error message

Threat Payload

FieldTypeDescriptionExample
hexstringICAO hex code"A1B2C3"
callsignstringAircraft callsign"N12345"
distance_nmnumberDistance from user in nautical miles5.2
bearingnumberBearing from user in degrees270.5
threat_levelstringThreat level: critical, high, medium, low"high"
trendstringDistance trend: approaching, receding, stable"approaching"
altitudenumberAltitude in feet2500
altitude_aglnumberAltitude above ground level in feet1200
gsnumberGround speed in knots120.5
tracknumberTrack angle in degrees180.0

Cannonball Example

let sessionId = null;

cannonballSocket.on('session_started', (data) => {
  sessionId = data.session_id;
  console.log(`Session started: ${sessionId}`);
  console.log(`Default radius: ${data.default_radius_nm} nm`);
});

cannonballSocket.on('threats', (data) => {
  console.log(`Threat level: ${data.threat_level}`);
  console.log(`Threats: ${data.aircraft.length}`);
  
  data.aircraft.forEach(threat => {
    console.log(
      `${threat.callsign || threat.hex}: ` +
      `${threat.distance_nm.toFixed(1)} nm @ ${threat.bearing}° ` +
      `(${threat.trend})`
    );
    
    // Show alert for critical threats
    if (threat.threat_level === 'critical') {
      showCriticalThreatAlert(threat);
    }
  });
  
  // Update UI
  updateThreatMap(data.aircraft);
});

cannonballSocket.on('radius_updated', (data) => {
  console.log(`Radius updated: ${data.radius_nm} nm`);
});

// Send position updates from GPS
navigator.geolocation.watchPosition(
  (position) => {
    cannonballSocket.emit('position_update', {
      lat: position.coords.latitude,
      lon: position.coords.longitude,
      heading: position.coords.heading,
      altitude: position.coords.altitude
    });
  },
  (error) => {
    console.error('GPS error:', error);
  },
  {
    enableHighAccuracy: true,
    maximumAge: 1000,
    timeout: 5000
  }
);

// Change detection radius
function setThreatRadius(radiusNm) {
  cannonballSocket.emit('set_radius', { radius_nm: radiusNm });
}
session_id = None

@sio.event
def session_started(data):
    global session_id
    session_id = data.get('session_id')
    print(f"Session started: {session_id}")
    print(f"Default radius: {data.get('default_radius_nm')} nm")

@sio.event
def threats(data):
    print(f"Threat level: {data.get('threat_level')}")
    aircraft = data.get('aircraft', [])
    print(f"Threats: {len(aircraft)}")
    
    for threat in aircraft:
        callsign = threat.get('callsign') or threat.get('hex')
        distance = threat.get('distance_nm', 0)
        bearing = threat.get('bearing', 0)
        trend = threat.get('trend', 'unknown')
        
        print(f"{callsign}: {distance:.1f} nm @ {bearing}° ({trend})")
        
        # Show alert for critical threats
        if threat.get('threat_level') == 'critical':
            show_critical_threat_alert(threat)
    
    # Update UI
    update_threat_map(aircraft)

@sio.event
def radius_updated(data):
    print(f"Radius updated: {data.get('radius_nm')} nm")

# Send position updates
def send_position(lat, lon, heading=None, altitude=None):
    payload = {'lat': lat, 'lon': lon}
    if heading is not None:
        payload['heading'] = heading
    if altitude is not None:
        payload['altitude'] = altitude
    sio.emit('position_update', payload)

# Change detection radius
def set_threat_radius(radius_nm):
    sio.emit('set_radius', {'radius_nm': radius_nm})

Threat Detection Logic

Threat LevelDistanceCriteriaAction
critical< 1 nmVery close, approachingImmediate visual alert, sound
high1-3 nmClose proximity, approachingAlert notification
medium3-5 nmModerate distance, approachingDisplay on map
low5+ nmWithin radius, any trendShow in list

ACARS Namespace (/acars)

Optional dedicated namespace for ACARS-only clients. Use this if you only need datalink messages without aircraft tracking.

Connection

const acarsSocket = io('https://skyspy.example.com/acars', {
  path: '/socket.io',
  transports: ['websocket']
});

acarsSocket.on('acars:message', (message) => {
  console.log(
    `ACARS from ${message.callsign}: ` +
    `Label ${message.label} - ${message.text}`
  );
});
import socketio

sio = socketio.Client()

@sio.event
def acars_message(message):
    callsign = message.get('callsign', 'Unknown')
    label = message.get('label', '')
    text = message.get('text', '')
    print(f"ACARS from {callsign}: Label {label} - {text}")

sio.connect(
    'https://skyspy.example.com/acars',
    socketio_path='/socket.io',
    transports=['websocket']
)
📘

Main Namespace Alternative

You can also receive ACARS messages on the main namespace by subscribing to the acars topic. The dedicated /acars namespace is for clients that only need ACARS without other features.

Next Steps

📘

Build Your Client