- 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.
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
- Update bounds only when map movement stops - Use a debounce to avoid excessive updates
- Unsubscribe when app backgrounds - Save battery and bandwidth
- Handle reconnection - Phoenix Channels will automatically reconnect on connection loss
- Validate coordinates - Always check lat/lng before adding to map
- Limit visible area - Don't subscribe to bounds larger than what the user can see
Dependencies
Swift:
- SwiftPhoenixClient - Phoenix Channels client
Installation (Swift Package Manager):
dependencies: [
.package(url: "https://github.com/davidstump/SwiftPhoenixClient.git", from: "5.3.0")
]
Support
For issues or questions:
- GitHub: https://github.com/aprsme/aprs.me/issues
- Documentation: https://docs.aprs.me