defmodule AprsWeb.ApiDocsLive do @moduledoc """ LiveView for API documentation page. """ use AprsWeb, :live_view @impl true def mount(_params, _session, socket) do {:ok, socket |> assign(page_title: "API Documentation") |> assign(test_callsign: "") |> assign(loading: false) |> assign(api_result: nil) |> assign(error: nil)} end @impl true def handle_event("update_callsign", %{"callsign" => callsign}, socket) do {:noreply, socket |> assign(test_callsign: callsign) |> assign(error: nil)} end @impl true def handle_event("test_api", %{"callsign" => callsign}, socket) do normalized_callsign = callsign |> String.trim() |> String.upcase() if normalized_callsign == "" do {:noreply, assign(socket, error: "Please enter a callsign")} else socket = socket |> assign(loading: true) |> assign(error: nil) |> assign(api_result: nil) # Make the API call send(self(), {:call_api, normalized_callsign}) {:noreply, socket} end end @impl true def handle_info({:call_api, callsign}, socket) do case call_api(callsign) do {:ok, result} -> {:noreply, socket |> assign(loading: false) |> assign(api_result: result)} {:error, error_message} -> {:noreply, socket |> assign(loading: false) |> assign(error: error_message)} end end defp call_api(callsign) do # Use the same validation and lookup logic as the API controller alias Aprs.Packets # Validate callsign format if validate_callsign_format(callsign) do # Try to get the most recent packet for this callsign thirty_days_ago = DateTime.add(DateTime.utc_now(), -30, :day) opts = %{ callsign: callsign, start_time: thirty_days_ago, limit: 1 } case Packets.get_recent_packets(opts) do [] -> {:ok, %{"data" => nil, "message" => "No packets found for callsign #{callsign}"}} [packet | _] -> # Format the packet data similar to the JSON view packet_data = format_packet_for_display(packet) {:ok, %{"data" => packet_data}} {:error, _reason} -> {:error, "Database error occurred while fetching packet data"} end else {:error, "Invalid callsign format"} end rescue error -> {:error, "An error occurred: #{inspect(error)}"} end defp validate_callsign_format(callsign) do # Same validation as in the controller callsign_regex = ~r/^[A-Z0-9]{1,3}[0-9][A-Z]{1,4}(-[0-9]{1,2})?$/ String.match?(callsign, callsign_regex) and String.length(callsign) <= 12 end defp format_packet_for_display(%Aprs.Packet{} = packet) do %{ "id" => packet.id, "callsign" => format_callsign(packet.base_callsign, packet.ssid), "base_callsign" => packet.base_callsign, "ssid" => packet.ssid, "sender" => packet.sender, "destination" => packet.destination, "path" => packet.path, "data_type" => packet.data_type, "information_field" => packet.information_field, "raw_packet" => packet.raw_packet, "received_at" => packet.received_at, "region" => packet.region, "position" => position_data(packet), "symbol" => symbol_data(packet), "comment" => packet.comment, "timestamp" => packet.timestamp, "aprs_messaging" => packet.aprs_messaging, "weather" => weather_data(packet), "equipment" => equipment_data(packet), "message" => message_data(packet), "has_position" => packet.has_position, "inserted_at" => packet.inserted_at, "updated_at" => packet.updated_at } end defp format_callsign(base_callsign, nil), do: base_callsign defp format_callsign(base_callsign, "0"), do: base_callsign defp format_callsign(base_callsign, ssid), do: "#{base_callsign}-#{ssid}" defp position_data(%Aprs.Packet{has_position: false}), do: nil defp position_data(%Aprs.Packet{lat: nil, lon: nil}), do: nil defp position_data(%Aprs.Packet{} = packet) do base_position = %{ "latitude" => to_float(packet.lat), "longitude" => to_float(packet.lon) } base_position |> maybe_add("course", packet.course) |> maybe_add("speed", packet.speed) |> maybe_add("altitude", packet.altitude) end defp symbol_data(%Aprs.Packet{symbol_code: nil, symbol_table_id: nil}), do: nil defp symbol_data(%Aprs.Packet{} = packet) do %{ "code" => packet.symbol_code, "table_id" => packet.symbol_table_id } end defp weather_data(%Aprs.Packet{} = packet) do weather_data = %{} |> maybe_add("temperature", packet.temperature) |> maybe_add("humidity", packet.humidity) |> maybe_add("wind_speed", packet.wind_speed) |> maybe_add("wind_direction", packet.wind_direction) |> maybe_add("wind_gust", packet.wind_gust) |> maybe_add("pressure", packet.pressure) |> maybe_add("rain_1h", packet.rain_1h) |> maybe_add("rain_24h", packet.rain_24h) |> maybe_add("rain_since_midnight", packet.rain_since_midnight) case weather_data do empty when empty == %{} -> nil data -> data end end defp equipment_data(%Aprs.Packet{} = packet) do equipment_data = %{} |> maybe_add("manufacturer", packet.manufacturer) |> maybe_add("equipment_type", packet.equipment_type) case equipment_data do empty when empty == %{} -> nil data -> data end end defp message_data(%Aprs.Packet{addressee: nil, message_text: nil, message_number: nil}), do: nil defp message_data(%Aprs.Packet{} = packet) do %{} |> maybe_add("addressee", packet.addressee) |> maybe_add("text", packet.message_text) |> maybe_add("number", packet.message_number) end defp maybe_add(map, _key, nil), do: map defp maybe_add(map, _key, ""), do: map defp maybe_add(map, key, value), do: Map.put(map, key, value) defp to_float(%Decimal{} = decimal), do: Decimal.to_float(decimal) defp to_float(value) when is_number(value), do: value defp to_float(_), do: nil @impl true def render(assigns) do ~H"""
RESTful JSON API for accessing APRS packet data and station information.
The APRS.me API provides programmatic access to Amateur Radio APRS (Automatic Packet Reporting System) data. All API endpoints return JSON data and follow RESTful conventions.
https://aprs.me/api/v1
application/json
Currently no rate limiting is enforced, but please be respectful and avoid excessive requests. Rate limiting may be implemented in the future.
GET /api/v1/callsign/{"{callsign}"}
Retrieves the most recent APRS packet for the specified callsign. The callsign can include an SSID (e.g., N0CALL-9) or just the base callsign (e.g., N0CALL).
| Parameter | Type | Required | Description |
|---|---|---|---|
| callsign | string | Yes | Amateur radio callsign with optional SSID (e.g., N0CALL or N0CALL-9) |
curl -X GET "https://aprs.me/api/v1/callsign/N0CALL-9" \
-H "Accept: application/json"
<%= raw ~s|{
"data": {
"id": 12345,
"callsign": "N0CALL-9",
"base_callsign": "N0CALL",
"ssid": "9",
"sender": "N0CALL-9",
"destination": "APRS",
"path": "WIDE1-1,WIDE2-1",
"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",
"region": "US-West",
"position": {
"latitude": 47.666667,
"longitude": -122.0,
"course": 90,
"speed": 35.5,
"altitude": 152.4
},
"symbol": {
"code": ">",
"table_id": "/"
},
"comment": "Mobile Station",
"timestamp": null,
"aprs_messaging": false,
"weather": null,
"equipment": {
"manufacturer": "Kenwood",
"equipment_type": "TM-D710"
},
"message": null,
"has_position": true,
"inserted_at": "2024-01-15T10:30:45.123456Z",
"updated_at": "2024-01-15T10:30:45.123456Z"
}
}| %>
<%= raw ~s|{
"data": null,
"message": "No packets found for callsign N0CALL-9"
}| %>
<%= raw ~s|{
"error": {
"message": "Invalid callsign format",
"code": "bad_request"
}
}| %>
| Field | Type | Description |
|---|---|---|
| id | integer | Unique packet identifier |
| callsign | string | Full callsign with SSID (e.g., "N0CALL-9") |
| base_callsign | string | Base callsign without SSID |
| ssid | string | SSID (Secondary Station Identifier), null if not present |
| received_at | datetime | When the packet was received (ISO 8601 format) |
| position | object | Position data (latitude, longitude, course, speed, altitude) |
| symbol | object | APRS symbol information (code and table_id) |
| weather | object | Weather data if present (temperature, humidity, wind, etc.) |
| equipment | object | Equipment information (manufacturer, type) |
| message | object | Message data if the packet is a message |
| raw_packet | string | Original raw APRS packet as received |
| Status Code | Description |
|---|---|
| 200 OK | Request successful, packet data returned |
| 400 Bad Request | Invalid callsign format or malformed request |
| 404 Not Found | No packets found for the specified callsign |
| 408 Request Timeout | Request took too long to process |
| 500 Internal Server Error | Server error occurred while processing the request |
The following endpoints are planned for future releases:
GET /api/v1/callsign/{"{callsign}"}/history
Get historical packets for a callsign
GET /api/v1/packets/recent
Get recent packets with filtering options
GET /api/v1/packets/area
Get packets within a geographic area
GET /api/v1/weather/{"{callsign}"}
Get weather data from weather stations
Try the API directly from this page. Enter a callsign to see the most recent packet data.
{@error}
<%= Jason.encode!(@api_result, pretty: true) %>
{@api_result["message"]}
<%= Jason.encode!(@api_result, pretty: true) %>
This API is provided free of charge for amateur radio and educational purposes. If you encounter issues or have suggestions for improvements, please reach out.