Implements comprehensive callsign search and live tracking: - search_callsign: Search for callsigns with wildcard support - "W5ISP" finds W5ISP and all SSIDs (W5ISP-1, W5ISP-9, etc.) - "W5ISP-9" finds only that specific SSID - "W5ISP*" finds all callsigns starting with W5ISP - subscribe_callsign: Subscribe to live updates for a callsign - Loads historical packets for the callsign - Receives real-time streaming updates - Supports wildcard patterns - Works with or without geographic bounds - unsubscribe_callsign: Stop tracking callsign When both bounds and callsign are subscribed, packets are filtered by both criteria (callsign AND within bounds). Generated with Claude Code https://claude.com/claude-code
494 lines
11 KiB
Markdown
494 lines
11 KiB
Markdown
# 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
|
|
|
|
```json
|
|
{
|
|
"topic": "mobile:packets",
|
|
"event": "phx_join",
|
|
"payload": {},
|
|
"ref": "1"
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"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. Upon subscription, the server will immediately send historical packets within the bounds, followed by real-time streaming packets.
|
|
|
|
**Event:** `subscribe_bounds`
|
|
|
|
**Payload:**
|
|
```json
|
|
{
|
|
"north": 33.2,
|
|
"south": 33.0,
|
|
"east": -96.0,
|
|
"west": -96.2,
|
|
"limit": 1000,
|
|
"hours_back": 1
|
|
}
|
|
```
|
|
|
|
**Payload Fields:**
|
|
|
|
| Field | Type | Required | Default | Description |
|
|
|-------|------|----------|---------|-------------|
|
|
| `north` | float | Yes | - | Northern boundary latitude |
|
|
| `south` | float | Yes | - | Southern boundary latitude |
|
|
| `east` | float | Yes | - | Eastern boundary longitude |
|
|
| `west` | float | Yes | - | Western boundary longitude |
|
|
| `limit` | integer | No | 1000 | Maximum historical packets to load (max: 5000) |
|
|
| `hours_back` | integer | No | 1 | How many hours of historical data to load (max: 24) |
|
|
|
|
**Example:**
|
|
```json
|
|
{
|
|
"topic": "mobile:packets",
|
|
"event": "subscribe_bounds",
|
|
"payload": {
|
|
"north": 33.2,
|
|
"south": 33.0,
|
|
"east": -96.0,
|
|
"west": -96.2,
|
|
"limit": 2000,
|
|
"hours_back": 2
|
|
},
|
|
"ref": "2"
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```json
|
|
{
|
|
"north": 33.3,
|
|
"south": 32.9,
|
|
"east": -95.9,
|
|
"west": -96.3
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"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 packets within your bounds. Historical packets are sent immediately after subscription, followed by real-time streaming packets as they arrive.
|
|
|
|
**Event:** `packet`
|
|
|
|
**Payload:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```json
|
|
{
|
|
"topic": "mobile:packets",
|
|
"event": "phx_reply",
|
|
"payload": {
|
|
"status": "ok",
|
|
"response": {
|
|
"message": "Unsubscribed from packet stream"
|
|
}
|
|
},
|
|
"ref": "4"
|
|
}
|
|
```
|
|
|
|
### Searching for Callsigns
|
|
|
|
Search for callsigns matching a pattern.
|
|
|
|
**Event:** `search_callsign`
|
|
|
|
**Payload:**
|
|
```json
|
|
{
|
|
"query": "W5ISP",
|
|
"limit": 50
|
|
}
|
|
```
|
|
|
|
**Payload Fields:**
|
|
|
|
| Field | Type | Required | Default | Description |
|
|
|-------|------|----------|---------|-------------|
|
|
| `query` | string | Yes | - | Callsign to search (supports * wildcard) |
|
|
| `limit` | integer | No | 50 | Maximum results to return (max: 500) |
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"topic": "mobile:packets",
|
|
"event": "phx_reply",
|
|
"payload": {
|
|
"status": "ok",
|
|
"response": {
|
|
"results": [
|
|
{
|
|
"callsign": "W5ISP-9",
|
|
"base_callsign": "W5ISP",
|
|
"last_seen": "2025-10-25T17:30:00Z",
|
|
"lat": 33.1225,
|
|
"lng": -96.124
|
|
}
|
|
],
|
|
"count": 1
|
|
}
|
|
},
|
|
"ref": "5"
|
|
}
|
|
```
|
|
|
|
**Examples:**
|
|
- `"W5ISP"` - Finds W5ISP and all SSIDs (W5ISP-1, W5ISP-9, etc.)
|
|
- `"W5ISP-9"` - Finds only W5ISP-9
|
|
- `"W5ISP*"` - Finds all callsigns starting with W5ISP
|
|
|
|
### Subscribing to Callsign Updates
|
|
|
|
Subscribe to live updates for a specific callsign or pattern. When subscribed, you'll receive historical packets for that callsign, followed by real-time updates.
|
|
|
|
**Event:** `subscribe_callsign`
|
|
|
|
**Payload:**
|
|
```json
|
|
{
|
|
"callsign": "W5ISP-9",
|
|
"hours_back": 24
|
|
}
|
|
```
|
|
|
|
**Payload Fields:**
|
|
|
|
| Field | Type | Required | Default | Description |
|
|
|-------|------|----------|---------|-------------|
|
|
| `callsign` | string | Yes | - | Callsign to track (supports * wildcard) |
|
|
| `hours_back` | integer | No | 24 | Hours of historical data to load (max: 168) |
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"topic": "mobile:packets",
|
|
"event": "phx_reply",
|
|
"payload": {
|
|
"status": "ok",
|
|
"response": {
|
|
"callsign": "W5ISP-9",
|
|
"message": "Subscribed to callsign updates"
|
|
}
|
|
},
|
|
"ref": "6"
|
|
}
|
|
```
|
|
|
|
**Note:** When tracking a callsign, you'll receive packets matching that callsign even if they're outside your geographic bounds. To receive only callsign packets within bounds, use both `subscribe_bounds` and `subscribe_callsign` together.
|
|
|
|
### Unsubscribing from Callsign Updates
|
|
|
|
Stop receiving updates for the tracked callsign.
|
|
|
|
**Event:** `unsubscribe_callsign`
|
|
|
|
**Payload:** `{}`
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"topic": "mobile:packets",
|
|
"event": "phx_reply",
|
|
"payload": {
|
|
"status": "ok",
|
|
"response": {
|
|
"message": "Unsubscribed from callsign updates"
|
|
}
|
|
},
|
|
"ref": "7"
|
|
}
|
|
```
|
|
|
|
## Error Responses
|
|
|
|
**Invalid Bounds:**
|
|
```json
|
|
{
|
|
"topic": "mobile:packets",
|
|
"event": "phx_reply",
|
|
"payload": {
|
|
"status": "error",
|
|
"response": {
|
|
"message": "North must be greater than south"
|
|
}
|
|
},
|
|
"ref": "2"
|
|
}
|
|
```
|
|
|
|
**Not Subscribed:**
|
|
```json
|
|
{
|
|
"topic": "mobile:packets",
|
|
"event": "phx_reply",
|
|
"payload": {
|
|
"status": "error",
|
|
"response": {
|
|
"message": "Not subscribed. Call subscribe_bounds first."
|
|
}
|
|
},
|
|
"ref": "3"
|
|
}
|
|
```
|
|
|
|
## Swift Example
|
|
|
|
```swift
|
|
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
|
|
|
|
```swift
|
|
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:**
|
|
- [SwiftPhoenixClient](https://github.com/davidstump/SwiftPhoenixClient) - Phoenix Channels client
|
|
|
|
**Installation (Swift Package Manager):**
|
|
```swift
|
|
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
|