Add historical packet loading to mobile API

When mobile clients subscribe to bounds, the server now loads and sends
historical packets immediately, followed by real-time streaming packets.
This matches the web UI behavior and provides a complete view of APRS
activity in the requested area.

Optional parameters:
- limit: Max historical packets to load (default: 1000, max: 5000)
- hours_back: Hours of historical data (default: 1, max: 24)

Generated with Claude Code https://claude.com/claude-code
This commit is contained in:
Graham McIntire 2025-10-25 12:31:30 -05:00
parent f94ac8097c
commit d6c3ada6c4
No known key found for this signature in database
2 changed files with 92 additions and 20 deletions

View file

@ -44,7 +44,7 @@ Currently, connections are anonymous. Future versions will support token-based a
### Subscribing to Geographic Bounds
Subscribe to receive packets within specific 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`
@ -54,10 +54,23 @@ Subscribe to receive packets within specific geographic bounds.
"north": 33.2,
"south": 33.0,
"east": -96.0,
"west": -96.2
"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
{
@ -67,7 +80,9 @@ Subscribe to receive packets within specific geographic bounds.
"north": 33.2,
"south": 33.0,
"east": -96.0,
"west": -96.2
"west": -96.2,
"limit": 2000,
"hours_back": 2
},
"ref": "2"
}
@ -133,7 +148,7 @@ Update the geographic bounds (e.g., when user pans/zooms the map).
### Receiving Packets
Once subscribed, you'll receive real-time packets within your bounds.
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`

View file

@ -53,7 +53,11 @@ defmodule AprsmeWeb.MobileChannel do
end
@impl true
def handle_in("subscribe_bounds", %{"north" => north, "south" => south, "east" => east, "west" => west}, socket) do
def handle_in(
"subscribe_bounds",
%{"north" => north, "south" => south, "east" => east, "west" => west} = payload,
socket
) do
bounds = %{
north: ensure_float(north),
south: ensure_float(south),
@ -62,24 +66,28 @@ defmodule AprsmeWeb.MobileChannel do
}
# Validate bounds
with :ok <- validate_bounds(bounds) do
# Generate unique client ID
client_id = "mobile_#{:erlang.phash2(self())}"
case validate_bounds(bounds) do
:ok ->
# Generate unique client ID
client_id = "mobile_#{:erlang.phash2(self())}"
# Subscribe to StreamingPacketsPubSub with bounds
Aprsme.StreamingPacketsPubSub.subscribe_to_bounds(self(), bounds)
# Subscribe to StreamingPacketsPubSub with bounds
Aprsme.StreamingPacketsPubSub.subscribe_to_bounds(self(), bounds)
# Store client info in socket
socket =
socket
|> assign(:client_id, client_id)
|> assign(:bounds, bounds)
|> assign(:subscribed, true)
# Store client info in socket
socket =
socket
|> assign(:client_id, client_id)
|> assign(:bounds, bounds)
|> assign(:subscribed, true)
Logger.info("Mobile client #{client_id} subscribed to bounds: #{inspect(bounds)}")
Logger.info("Mobile client #{client_id} subscribed to bounds: #{inspect(bounds)}")
# Load and send historical packets
socket = load_historical_packets(socket, bounds, payload)
{:reply, {:ok, %{bounds: bounds, message: "Subscribed to packet stream"}}, socket}
{:reply, {:ok, %{bounds: bounds, message: "Subscribed to packet stream"}}, socket}
else
{:error, reason} ->
{:reply, {:error, %{message: reason}}, socket}
end
@ -194,7 +202,7 @@ defmodule AprsmeWeb.MobileChannel do
callsign: get_field(packet, :sender) || get_field(packet, :base_callsign),
lat: to_float(lat),
lng: to_float(lon),
timestamp: get_field(packet, :received_at) |> format_timestamp(),
timestamp: packet |> get_field(:received_at) |> format_timestamp(),
symbol_table_id: get_field(packet, :symbol_table_id, "/"),
symbol_code: get_field(packet, :symbol_code, ">"),
comment: get_field(packet, :comment),
@ -238,4 +246,53 @@ defmodule AprsmeWeb.MobileChannel do
end
defp format_timestamp(_), do: nil
defp load_historical_packets(socket, bounds, payload) do
# Get optional parameters from payload
limit = Map.get(payload, "limit", 1000)
hours_back = Map.get(payload, "hours_back", 1)
# Ensure reasonable limits
limit = min(limit, 5000)
hours_back = min(hours_back, 24)
# Query historical packets within bounds
bounds_list = [
bounds.west,
bounds.south,
bounds.east,
bounds.north
]
params = %{
bounds: bounds_list,
limit: limit,
offset: 0,
hours_back: hours_back
}
Logger.debug("Mobile client loading historical packets with params: #{inspect(params)}")
packets =
try do
Aprsme.Packets.get_recent_packets(params)
rescue
error ->
Logger.error("Error loading historical packets for mobile client: #{inspect(error)}")
[]
end
Logger.info("Loaded #{length(packets)} historical packets for mobile client")
# Send historical packets to client
if length(packets) > 0 do
# Convert packets to mobile format and send them
Enum.each(packets, fn packet ->
packet_data = build_mobile_packet(packet)
push(socket, "packet", packet_data)
end)
end
socket
end
end