GPS Forwarding
GPS Forwarding relays location data from the Peplink router to one or more external APIs. Each destination gets its own queue with independent retry logic and health tracking.
Configuration
Forwarding is configured in config/default.toml (or via environment variables). It is disabled when no destinations are configured.
Single Destination (backward compatible)
[forwarding]
interval_secs = 60
url = "https://api.example.com"
token = "your-api-token"
device_id = "550e8400-e29b-41d4-a716-446655440000" # optional
health_url = "https://api.example.com/health" # optional
Multi-Destination
[forwarding]
interval_secs = 60
max_queue_size = 100
max_retries = 3
retry_delay_ms = 5000
timeout_ms = 10000
[[forwarding.destinations]]
name = "primary"
url = "https://api1.example.com"
token = "token1"
device_id = "550e8400-e29b-41d4-a716-446655440000"
[[forwarding.destinations]]
name = "backup"
url = "https://api2.example.com"
token = "token2"
health_url = "https://api2.example.com/health"
When both [[forwarding.destinations]] and top-level url/token are set, the destinations array takes precedence.
Environment Variables
| Variable | Description |
|---|---|
APOLLON__FORWARDING__INTERVAL_SECS | Polling interval in seconds (default: 60) |
APOLLON__FORWARDING__URL | Single destination URL |
APOLLON__FORWARDING__TOKEN | Single destination API token (sent as Bearer) |
APOLLON__FORWARDING__DEVICE_ID | Single destination device ID (sent as X-Device-ID header) |
APOLLON__FORWARDING__HEALTH_URL | Single destination health check URL |
APOLLON__FORWARDING__MAX_QUEUE_SIZE | Max queued payloads per destination (default: 100) |
APOLLON__FORWARDING__MAX_RETRIES | Retry attempts per failed forward (default: 3) |
APOLLON__FORWARDING__RETRY_DELAY_MS | Delay between retries in ms (default: 5000) |
APOLLON__FORWARDING__TIMEOUT_MS | HTTP request timeout in ms (default: 10000) |
Per-destination configuration via environment variables is limited. Use TOML config files for multi-destination setups.
Shared Settings
These settings apply to all destinations:
| Setting | Default | Description |
|---|---|---|
interval_secs | 60 | GPS polling and forwarding interval |
max_queue_size | 100 | Max queued payloads per destination (oldest dropped when full) |
max_retries | 3 | Retry attempts per failed forward |
retry_delay_ms | 5000 | Delay between retry attempts |
timeout_ms | 10000 | HTTP request timeout |
Payload Format
Each forward sends a POST request to {url}/v1/gps with:
- Header:
Authorization: Bearer {token} - Header:
X-Device-ID: {device_id}(optional, only sent when configured) - Body: JSON with the following fields:
{
"latitude": 50.1234,
"longitude": 8.5678,
"altitude": 105.2,
"heading": 180.5,
"timestamp": 1718620800,
"speed_kmh": 12.5,
"speed_mps": 3.47,
"speed_mph": 7.77,
"speed_knots": 6.75,
"pdop": 1.8,
"hdop": 1.2,
"vdop": 1.4
}
| Field | Type | Description |
|---|---|---|
latitude | f64 | Latitude in decimal degrees |
longitude | f64 | Longitude in decimal degrees |
altitude | f64 | null | Altitude in meters |
heading | f64 | null | Heading in degrees (0-360) |
timestamp | i64 | Unix timestamp (seconds) |
speed_kmh | f64 | Speed in km/h |
speed_mps | f64 | Speed in m/s (raw from Peplink) |
speed_mph | f64 | Speed in mph |
speed_knots | f64 | Speed in knots |
pdop | f64 | null | Position dilution of precision |
hdop | f64 | null | Horizontal dilution of precision |
vdop | f64 | null | Vertical dilution of precision |
Retry Logic
Failed forwards are retried based on the HTTP status code:
- Retryable (408, 429, 500, 502, 503, 504): Retried up to
max_retriestimes withretry_delay_msbetween attempts. - Non-retryable (4xx except 408/429): Dropped immediately.
- Network errors: Retried like retryable status codes.
Each destination tracks failures independently. A destination is marked unhealthy after 5 consecutive failures — unless a health_url is configured, in which case the live health check result determines the status.
Health-Based Forwarding
When a destination has a health_url configured, the forwarding service performs periodic health checks and pauses forwarding to unhealthy destinations:
- Healthy destinations: Health check every 60 seconds
- Unhealthy destinations: Health check every 10 seconds (faster recovery detection)
- While unhealthy: GPS data stays in the queue, nothing is sent
- On recovery: The entire queued backlog is drained immediately — no GPS points are lost
Queue Drain Behavior
When a destination recovers, all queued items are sent sequentially in one cycle. Backlog items are logged with status catchup (instead of success) so they are distinguishable in the logs. If a send fails during drain, the remaining items stay in the queue for the next cycle.
Log Statuses
| Status | Description |
|---|---|
success | Normal GPS forward |
catchup | Backlog item sent after queue drain |
health_up | Destination recovered (state change) |
health_down | Destination unreachable, forwarding paused (state change) |
retrying | Send failed, will retry |
failed | Send failed permanently (max retries exceeded) |
Health events (health_up / health_down) are only logged on state changes, not on every check.
Health Monitoring
When a destination has a health_url configured, the /v1/health endpoint performs a live GET request to that URL. A 2xx response means the destination is healthy. If no health_url is set, health is inferred from consecutive failure count (< 5 = healthy).
REST
GET /v1/health
{
"services": {
"forwarding": {
"enabled": true,
"healthy": true,
"destinations": [
{
"name": "primary",
"healthy": true,
"queueLength": 0,
"consecutiveFailures": 0,
"healthUrl": "https://api1.example.com/health",
"healthCheckOk": true,
"healthCheckResponseTimeMs": 8
}
]
}
}
}
| Field | Type | Description |
|---|---|---|
healthUrl | string | absent | Only present when health_url is configured |
healthCheckOk | bool | null | Live health check result (true/false), null if no health_url |
healthCheckResponseTimeMs | number | absent | Response time of the health check in ms |
GraphQL
query {
health {
forwardingEnabled
forwardingDestinations {
name
healthy
queueLength
consecutiveFailures
healthUrl
healthCheckOk
healthCheckResponseTimeMs
}
}
}
REST API
Status
GET /v1/forwarding/status
Returns per-destination health, queue depth, and failure count.
Logs
GET /v1/forwarding/logs?start=&end=&destination=&status=&limit=
Query forwarding log entries from TimescaleDB. Parameters:
start/end— Unix timestamps (default: last 24h)destination— Filter by destination namestatus— Filter by status (success,catchup,failed,retrying,health_up,health_down)limit— Max entries (default: 100)
Stats
GET /v1/forwarding/stats?start=&end=&destination=
Returns hourly aggregated stats (total, success count, avg/max latency) per destination.
Queue
GET /v1/forwarding/queue
Returns current queue contents per destination (items with retries, age, coordinates).
DELETE /v1/forwarding/queue/{destination}
Clears the retry queue for a specific destination. Returns 204 on success, 404 if destination not found.
GraphQL API
Queries
query {
forwardingStatus { name healthy queueLength consecutiveFailures healthUrl healthCheckOk healthCheckResponseTimeMs }
forwardingLogs(start: Int, end: Int, destination: String, status: String, limit: Int) {
start end count logs { id timestamp destination status statusCode errorMessage postBody latencyMs retries }
}
forwardingStats(start: Int, end: Int, destination: String) {
start end buckets { bucket destination total successCount avgLatencyMs maxLatencyMs }
}
forwardingQueue { name count items { retries ageSecs latitude longitude } }
}
Mutations
mutation {
clearForwardingQueue(destination: "primary")
}
WebSocket Channel
Subscribe to the forwarding channel for live forwarding events:
{"action": "subscribe", "channels": ["forwarding"]}
Events:
{
"type": "forwarding",
"data": {
"destination": "primary",
"status": "success",
"statusCode": 200,
"latencyMs": 45,
"queueDepth": 0,
"consecutiveFailures": 0
}
}
Logging
When log_enabled = true (default), every forwarding attempt is logged to the forwarding_logs TimescaleDB hypertable. Logs are automatically cleaned up after log_retention_days (default: 30) by the cleanup scheduler.
| Config | Default | Description |
|---|---|---|
log_enabled | true | Log forwarding attempts to TimescaleDB |
log_retention_days | 30 | Auto-delete logs older than this |
Graceful Shutdown
On shutdown, the forwarding service attempts to drain all remaining queue entries (best-effort). This ensures data collected before shutdown is forwarded if possible.
Troubleshooting
| Problem | Solution |
|---|---|
| Forwarding disabled | Ensure at least one destination has url and token set |
| 5+ consecutive failures | Check destination URL, token, and network connectivity |
| Queue filling up | Destination may be unreachable or slow. Check max_queue_size and timeout_ms |
| Data not forwarded | Verify interval_secs and check logs for GPS forwarding messages |