diff --git a/docs/mobile-api.md b/docs/mobile-api.md index 012423d..26b7a30 100644 --- a/docs/mobile-api.md +++ b/docs/mobile-api.md @@ -111,7 +111,7 @@ Subscribe to receive packets within specific geographic bounds. Upon subscriptio ### Updating Bounds -Update the geographic bounds (e.g., when user pans/zooms the map). +Update the geographic bounds (e.g., when user pans/zooms the map). This updates the real-time streaming filter only; it does not re-send historical packets for the new bounds. **Event:** `update_bounds` @@ -174,19 +174,21 @@ Once subscribed, you'll receive packets within your bounds. Historical packets a | Field | Type | Description | Required | |-------|------|-------------|----------| -| `id` | string | Unique packet identifier | Yes | -| `callsign` | string | Station callsign | Yes | +| `id` | string | Unique packet identifier (UUID) | Yes | +| `callsign` | string | Station callsign (sender). For APRS objects/items, this is still the sender callsign, not the object name. | 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 | +| `altitude` | float | Altitude in feet | Optional | | `speed` | float | Speed in knots | Optional | | `course` | integer | Course in degrees (0-359) | Optional | | `path` | string | APRS digipeater path | Optional | +**Note:** Optional fields are omitted from the payload entirely when their value is `nil`. + ### Unsubscribing Stop receiving packets. @@ -245,7 +247,7 @@ Search for callsigns matching a pattern. "base_callsign": "W5ISP", "last_seen": "2025-10-25T17:30:00Z", "lat": 33.1225, - "lng": -96.124 + "lon": -96.124 } ], "count": 1 @@ -461,11 +463,9 @@ struct APRSMapView: View { } ``` -## Rate Limiting +## Connection Timeout -The API is rate-limited to prevent abuse: -- 200 requests per minute per IP address -- Connections are automatically closed if idle for >60 seconds +WebSocket connections have a 60-second heartbeat timeout. If the server does not receive a heartbeat within 60 seconds, the connection is closed. Phoenix Channels clients send heartbeats automatically (every 30 seconds by default). ## Best Practices diff --git a/lib/aprsme_web/channels/mobile_channel.ex b/lib/aprsme_web/channels/mobile_channel.ex index 757d90e..a676d5d 100644 --- a/lib/aprsme_web/channels/mobile_channel.ex +++ b/lib/aprsme_web/channels/mobile_channel.ex @@ -44,7 +44,8 @@ defmodule AprsmeWeb.MobileChannel do comment: string (optional), altitude: float (optional), speed: float (optional), - course: integer (optional) + course: integer (optional), + path: string (optional) } - "subscription_confirmed" - Bounds subscription successful diff --git a/lib/aprsme_web/live/api_docs_live.ex b/lib/aprsme_web/live/api_docs_live.ex index 6a8bb1d..a0e59f6 100644 --- a/lib/aprsme_web/live/api_docs_live.ex +++ b/lib/aprsme_web/live/api_docs_live.ex @@ -177,10 +177,22 @@ defmodule AprsmeWeb.ApiDocsLive do end defp format_equipment(packet) do + device = + case packet.device_identifier do + nil -> nil + "" -> nil + identifier -> Aprsme.DeviceCache.lookup_device(identifier) + end + equipment = %{} + |> maybe_add("device_identifier", packet.device_identifier) |> maybe_add("manufacturer", packet.manufacturer) |> maybe_add("equipment_type", packet.equipment_type) + |> maybe_add("device_model", device && device.model) + |> maybe_add("device_vendor", device && device.vendor) + |> maybe_add("device_contact", device && device.contact) + |> maybe_add("device_class", device && device.class) if map_size(equipment) == 0, do: nil, else: equipment end @@ -266,7 +278,7 @@ defmodule AprsmeWeb.ApiDocsLive do

Rate Limiting

- Currently no rate limiting is enforced, but please be respectful and avoid excessive requests. Rate limiting may be implemented in the future. + API requests are rate limited to 100 requests per minute per IP address. Exceeding this limit will return a 429 Too Many Requests response.
@@ -344,7 +356,7 @@ defmodule AprsmeWeb.ApiDocsLive do
<%= raw ~s|{
     "data": {
-    "id": 12345,
+    "id": "d7249877-d4a6-45c2-b314-2a8a355d2566",
     "callsign": "N0CALL-9",
     "base_callsign": "N0CALL",
     "ssid": "9",
@@ -354,7 +366,7 @@ defmodule AprsmeWeb.ApiDocsLive do
     "data_type": "position",
     "information_field": "!4740.00N/12200.00W>Mobile Station",
     "raw_packet": "N0CALL-9>APRS,WIDE1-1,WIDE2-1:!4740.00N/12200.00W>Mobile Station",
-    "received_at": "2024-01-15T10:30:45.123456Z",
+    "received_at": "2024-01-15T10:30:45Z",
     "region": "US-West",
     "position": {
       "latitude": 47.666667,
@@ -372,13 +384,17 @@ defmodule AprsmeWeb.ApiDocsLive do
     "aprs_messaging": false,
     "weather": null,
     "equipment": {
+      "device_identifier": "APK004",
       "manufacturer": "Kenwood",
-      "equipment_type": "TM-D710"
+      "equipment_type": "TM-D710",
+      "device_model": "TM-D710G",
+      "device_vendor": "Kenwood",
+      "device_class": "rig"
     },
     "message": null,
     "has_position": true,
-    "inserted_at": "2024-01-15T10:30:45.123456Z",
-    "updated_at": "2024-01-15T10:30:45.123456Z"
+    "inserted_at": "2024-01-15T10:30:45Z",
+    "updated_at": "2024-01-15T10:30:45Z"
     }
     }| %>
@@ -420,7 +436,7 @@ defmodule AprsmeWeb.ApiDocsLive do id - integer + string (UUID) Unique packet identifier @@ -480,7 +496,7 @@ defmodule AprsmeWeb.ApiDocsLive do object - Equipment information (manufacturer, type) + Equipment information (device_identifier, manufacturer, equipment_type, device_model, device_vendor, device_class, device_contact) @@ -544,6 +560,14 @@ defmodule AprsmeWeb.ApiDocsLive do Request took too long to process + + + 429 Too Many Requests + + + Rate limit exceeded (100 requests per minute per IP) + + 500 Internal Server Error diff --git a/test/aprsme/packet_consumer_pool_test.exs b/test/aprsme/packet_consumer_pool_test.exs index 30d4663..0523359 100644 --- a/test/aprsme/packet_consumer_pool_test.exs +++ b/test/aprsme/packet_consumer_pool_test.exs @@ -1,5 +1,5 @@ defmodule Aprsme.PacketConsumerPoolTest do - use ExUnit.Case, async: true + use ExUnit.Case, async: false alias Aprsme.PacketConsumerPool @@ -13,15 +13,15 @@ defmodule Aprsme.PacketConsumerPoolTest do end describe "init/1" do - test "returns supervisor flags with one_for_one strategy and 3 children by default" do - {:ok, {sup_flags, children}} = PacketConsumerPool.init([]) + test "returns supervisor flags with one_for_one strategy and 3 children" do + {:ok, {sup_flags, children}} = PacketConsumerPool.init(num_consumers: 3) assert sup_flags.strategy == :one_for_one assert length(children) == 3 end test "each child has a unique id of {Aprsme.PacketConsumer, index}" do - {:ok, {_sup_flags, children}} = PacketConsumerPool.init([]) + {:ok, {_sup_flags, children}} = PacketConsumerPool.init(num_consumers: 3) ids = Enum.map(children, & &1.id) @@ -72,7 +72,7 @@ defmodule Aprsme.PacketConsumerPoolTest do Application.delete_env(:aprsme, :packet_pipeline) - {:ok, {_sup_flags, children}} = PacketConsumerPool.init([]) + {:ok, {_sup_flags, children}} = PacketConsumerPool.init(num_consumers: 3) expected_demand = div(250, 3) diff --git a/test/aprsme/packet_pipeline_supervisor_test.exs b/test/aprsme/packet_pipeline_supervisor_test.exs index 2d9e872..0ccc3fa 100644 --- a/test/aprsme/packet_pipeline_supervisor_test.exs +++ b/test/aprsme/packet_pipeline_supervisor_test.exs @@ -1,5 +1,5 @@ defmodule Aprsme.PacketPipelineSupervisorTest do - use ExUnit.Case, async: true + use ExUnit.Case, async: false alias Aprsme.PacketPipelineSupervisor