aprs.me/docs/mobile-api.md
Graham McIntire ddbb72b1b2
Add mobile API for iOS/Android real-time packet streaming
- Create MobileChannel for geographic bounds-based packet filtering
- Add MobileUserSocket for mobile client connections
- Implement subscribe_bounds, update_bounds, and unsubscribe events
- Leverage existing StreamingPacketsPubSub infrastructure
- Add comprehensive mobile API documentation with Swift examples
- WebSocket endpoint: wss://aprs.me/mobile/websocket
- Channel: mobile:packets

This enables iOS/Android apps to receive real-time APRS packets
filtered by geographic viewport, with efficient bandwidth usage.
2025-10-25 11:25:18 -05:00

7.8 KiB

Mobile API Documentation

Overview

The APRS.me Mobile API provides real-time streaming of APRS packets filtered by geographic bounds for iOS and Android applications.

Connection

WebSocket URL: wss://aprs.me/mobile/websocket

Protocol: Phoenix Channels over WebSocket

Authentication

Currently, connections are anonymous. Future versions will support token-based authentication.

Channel: mobile:packets

Joining

{
  "topic": "mobile:packets",
  "event": "phx_join",
  "payload": {},
  "ref": "1"
}

Response:

{
  "topic": "mobile:packets",
  "event": "phx_reply",
  "payload": {
    "status": "ok",
    "response": {
      "message": "Connected to APRS mobile channel"
    }
  },
  "ref": "1"
}

Subscribing to Geographic Bounds

Subscribe to receive packets within specific geographic bounds.

Event: subscribe_bounds

Payload:

{
  "north": 33.2,
  "south": 33.0,
  "east": -96.0,
  "west": -96.2
}

Example:

{
  "topic": "mobile:packets",
  "event": "subscribe_bounds",
  "payload": {
    "north": 33.2,
    "south": 33.0,
    "east": -96.0,
    "west": -96.2
  },
  "ref": "2"
}

Response:

{
  "topic": "mobile:packets",
  "event": "phx_reply",
  "payload": {
    "status": "ok",
    "response": {
      "bounds": {
        "north": 33.2,
        "south": 33.0,
        "east": -96.0,
        "west": -96.2
      },
      "message": "Subscribed to packet stream"
    }
  },
  "ref": "2"
}

Updating Bounds

Update the geographic bounds (e.g., when user pans/zooms the map).

Event: update_bounds

Payload:

{
  "north": 33.3,
  "south": 32.9,
  "east": -95.9,
  "west": -96.3
}

Response:

{
  "topic": "mobile:packets",
  "event": "phx_reply",
  "payload": {
    "status": "ok",
    "response": {
      "bounds": {
        "north": 33.3,
        "south": 32.9,
        "east": -95.9,
        "west": -96.3
      },
      "message": "Bounds updated"
    }
  },
  "ref": "3"
}

Receiving Packets

Once subscribed, you'll receive real-time packets within your bounds.

Event: packet

Payload:

{
  "id": "d7249877-d4a6-45c2-b314-2a8a355d2566",
  "callsign": "K5GVL-10",
  "lat": 33.1225,
  "lng": -96.124,
  "timestamp": "2025-10-25T16:17:20Z",
  "symbol_table_id": "/",
  "symbol_code": "#",
  "comment": "6/SVARA U=13.9V,T=75.3F",
  "altitude": 150.5,
  "speed": 45.2,
  "course": 180,
  "path": "TCPIP*,qAS,K5GVL"
}

Packet Fields:

Field Type Description Required
id string Unique packet identifier Yes
callsign string Station callsign Yes
lat float Latitude (-90 to 90) Yes
lng float Longitude (-180 to 180) Yes
timestamp string ISO 8601 timestamp Yes
symbol_table_id string APRS symbol table (/, \) Yes
symbol_code string APRS symbol code Yes
comment string Station comment/status Optional
altitude float Altitude in meters Optional
speed float Speed in knots Optional
course integer Course in degrees (0-359) Optional
path string APRS digipeater path Optional

Unsubscribing

Stop receiving packets.

Event: unsubscribe

Payload: {}

Response:

{
  "topic": "mobile:packets",
  "event": "phx_reply",
  "payload": {
    "status": "ok",
    "response": {
      "message": "Unsubscribed from packet stream"
    }
  },
  "ref": "4"
}

Error Responses

Invalid Bounds:

{
  "topic": "mobile:packets",
  "event": "phx_reply",
  "payload": {
    "status": "error",
    "response": {
      "message": "North must be greater than south"
    }
  },
  "ref": "2"
}

Not Subscribed:

{
  "topic": "mobile:packets",
  "event": "phx_reply",
  "payload": {
    "status": "error",
    "response": {
      "message": "Not subscribed. Call subscribe_bounds first."
    }
  },
  "ref": "3"
}

Swift Example

import SwiftPhoenixClient

class APRSService {
    let socket: Socket
    var channel: Channel?

    init() {
        socket = Socket("wss://aprs.me/mobile/websocket")
        socket.connect()
    }

    func joinChannel() {
        channel = socket.channel("mobile:packets")

        channel?.on("packet") { message in
            if let packet = message.payload as? [String: Any] {
                self.handlePacket(packet)
            }
        }

        channel?.join()
            .receive("ok") { _ in
                print("Joined mobile:packets channel")
                self.subscribeToBounds()
            }
            .receive("error") { error in
                print("Failed to join: \\(error)")
            }
    }

    func subscribeToBounds(north: Double, south: Double, east: Double, west: Double) {
        let bounds = [
            "north": north,
            "south": south,
            "east": east,
            "west": west
        ]

        channel?.push("subscribe_bounds", payload: bounds)
            .receive("ok") { response in
                print("Subscribed to bounds: \\(response)")
            }
            .receive("error") { error in
                print("Subscribe error: \\(error)")
            }
    }

    func updateBounds(north: Double, south: Double, east: Double, west: Double) {
        let bounds = [
            "north": north,
            "south": south,
            "east": east,
            "west": west
        ]

        channel?.push("update_bounds", payload: bounds)
    }

    func handlePacket(_ packet: [String: Any]) {
        guard let callsign = packet["callsign"] as? String,
              let lat = packet["lat"] as? Double,
              let lng = packet["lng"] as? Double else {
            return
        }

        // Update your map with the new packet
        print("Received packet from \\(callsign) at \\(lat), \\(lng)")
    }
}

SwiftUI MapKit Integration

import SwiftUI
import MapKit

struct APRSMapView: View {
    @StateObject private var aprsService = APRSService()
    @State private var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 33.1, longitude: -96.1),
        span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2)
    )

    var body: some View {
        Map(coordinateRegion: $region)
            .onAppear {
                aprsService.joinChannel()
            }
            .onChange(of: region) { newRegion in
                let center = newRegion.center
                let span = newRegion.span

                aprsService.updateBounds(
                    north: center.latitude + span.latitudeDelta / 2,
                    south: center.latitude - span.latitudeDelta / 2,
                    east: center.longitude + span.longitudeDelta / 2,
                    west: center.longitude - span.longitudeDelta / 2
                )
            }
    }
}

Rate Limiting

The API is rate-limited to prevent abuse:

  • 200 requests per minute per IP address
  • Connections are automatically closed if idle for >60 seconds

Best Practices

  1. Update bounds only when map movement stops - Use a debounce to avoid excessive updates
  2. Unsubscribe when app backgrounds - Save battery and bandwidth
  3. Handle reconnection - Phoenix Channels will automatically reconnect on connection loss
  4. Validate coordinates - Always check lat/lng before adding to map
  5. Limit visible area - Don't subscribe to bounds larger than what the user can see

Dependencies

Swift:

Installation (Swift Package Manager):

dependencies: [
    .package(url: "https://github.com/davidstump/SwiftPhoenixClient.git", from: "5.3.0")
]

Support

For issues or questions: