+ `;
+
+ // Check if marker already exists
+ if (this.markers.has(callsign)) {
+ // Update existing marker
+ const existingMarker = this.markers.get(callsign);
+ existingMarker.setLatLng([lat, lng]);
+ existingMarker.setPopupContent(popupContent);
+ } else {
+ // Create new marker
+ const icon = this.createAPRSIcon(
+ packet["data_extended"]["symbol_table_id"] || "/",
+ packet["data_extended"]["symbol_code"] || ">",
+ );
+
+ const marker = L.marker([lat, lng], { icon: icon }).addTo(this.map).bindPopup(popupContent);
+
+ this.markers.set(callsign, marker);
+ console.log("New marker added for:", callsign, "Total markers:", this.markers.size);
+
+ // Update packet counter
+ this.packetCount++;
+ const counterElement = document.getElementById("packet-count");
+ if (counterElement) {
+ counterElement.textContent = this.packetCount;
+ }
+ }
+ },
+
+ createAPRSIcon(symbolTable, symbolCode) {
+ // Default icon color based on symbol table
+ const color = symbolTable === "/" ? "#2563eb" : "#dc2626";
+
+ return L.divIcon({
+ html: ``,
+ className: "aprs-marker",
+ iconSize: [16, 16],
+ iconAnchor: [8, 8],
+ popupAnchor: [0, -8],
+ });
+ },
+
+ destroyed() {
+ console.log("APRSMap hook destroyed");
+ if (this.map) {
+ console.log("Removing map instance");
+ this.map.remove();
+ this.map = null;
+ }
+ this.markers.clear();
+ },
+};
+
let liveSocket = new LiveSocket("/live", Socket, {
+ longPollFallbackMs: 2500,
params: { _csrf_token: csrfToken },
+ hooks: Hooks,
});
// Show progress bar on live navigation and form submits
topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" });
-window.addEventListener("phx:page-loading-start", (info) =>
- topbar.delayedShow(200)
-);
+window.addEventListener("phx:page-loading-start", (info) => topbar.delayedShow(200));
window.addEventListener("phx:page-loading-stop", (info) => topbar.hide());
// connect if there are any LiveViews on the page
diff --git a/lib/aprs/is/is.ex b/lib/aprs/is/is.ex
index 76ace59..a8e450e 100644
--- a/lib/aprs/is/is.ex
+++ b/lib/aprs/is/is.ex
@@ -4,7 +4,8 @@ defmodule Aprs.Is do
require Logger
- @aprs_timeout 30 * 1000
+ @aprs_timeout 60 * 1000
+ @keepalive_interval 20 * 1000
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
@@ -15,11 +16,14 @@ defmodule Aprs.Is do
# Trap exits so we can gracefully shut down
Process.flag(:trap_exit, true)
+ # Add a small delay to prevent rapid reconnection attempts
+ Process.sleep(2000)
+
# Get startup parameters
server = Application.get_env(:aprs, :aprs_is_server, ~c"rotate.aprs2.net")
port = Application.get_env(:aprs, :aprs_is_port, 14_580)
default_filter = Application.get_env(:aprs, :aprs_is_default_filter, "r/33/-96/100")
- aprs_user_id = Application.get_env(:aprs, :aprs_is_login_id, "w5isp")
+ aprs_user_id = Application.get_env(:aprs, :aprs_is_login_id, "W5ISP")
aprs_passcode = Application.get_env(:aprs, :aprs_is_password, "-1")
# Set up ets tables
@@ -44,13 +48,20 @@ defmodule Aprs.Is do
with {:ok, socket} <- connect_to_aprs_is(server, port),
:ok <- send_login_string(socket, aprs_user_id, aprs_passcode, default_filter) do
timer = create_timer(@aprs_timeout)
+ keepalive_timer = create_keepalive_timer(@keepalive_interval)
{:ok,
%{
server: server,
port: port,
socket: socket,
- timer: timer
+ timer: timer,
+ keepalive_timer: keepalive_timer,
+ login_params: %{
+ user_id: aprs_user_id,
+ passcode: aprs_passcode,
+ filter: default_filter
+ }
}}
else
_ ->
@@ -88,9 +99,9 @@ defmodule Aprs.Is do
defp send_login_string(socket, aprs_user_id, aprs_passcode, filter) do
login_string =
- "user #{aprs_user_id} pass #{aprs_passcode} vers aprs.me 0.1 filter #{filter} \n"
+ "user #{aprs_user_id} pass #{aprs_passcode} vers aprs.me 0.1 filter #{filter}\r\n"
- Logger.debug("Sending login string: #{login_string}")
+ Logger.info("Sending login string: user #{aprs_user_id} pass ***** vers aprs.me 0.1 filter #{filter}")
:gen_tcp.send(socket, login_string)
end
@@ -99,6 +110,10 @@ defmodule Aprs.Is do
Process.send_after(self(), :aprs_no_message_timeout, timeout)
end
+ defp create_keepalive_timer(interval) do
+ Process.send_after(self(), :send_keepalive, interval)
+ end
+
@impl true
def handle_call({:send_message, message}, _from, state) do
next_ack_number = :ets.update_counter(:aprs, :message_number, 1)
@@ -116,6 +131,20 @@ defmodule Aprs.Is do
{:stop, :aprs_timeout, state}
end
+ def handle_info(:send_keepalive, state) do
+ # Send a comment line as keepalive (APRS-IS standard)
+ case :gen_tcp.send(state.socket, "# keepalive\r\n") do
+ :ok ->
+ Logger.debug("Sent keepalive")
+ keepalive_timer = create_keepalive_timer(@keepalive_interval)
+ {:noreply, %{state | keepalive_timer: keepalive_timer}}
+
+ {:error, reason} ->
+ Logger.error("Failed to send keepalive: #{inspect(reason)}")
+ {:stop, :normal, state}
+ end
+ end
+
def handle_info({:tcp, _socket, packet}, state) do
# Cancel the previous timer
Process.cancel_timer(state.timer)
@@ -139,21 +168,29 @@ defmodule Aprs.Is do
end
def handle_info({:tcp_closed, _socket}, state) do
- Logger.info("Socket has been closed")
+ Logger.warning("Socket has been closed by remote server - will reconnect")
{:stop, :normal, state}
end
def handle_info({:tcp_error, _socket, reason}, state) do
- Logger.error("Connection closed due to #{inspect(reason)}")
+ Logger.error("Connection error: #{inspect(reason)}")
{:stop, :normal, state}
end
@impl true
def terminate(reason, state) do
# Do Shutdown Stuff
- Logger.info("Going Down: #{inspect(reason)} - #{inspect(state)}")
- Logger.info("Closing socket")
- :gen_tcp.close(state.socket)
+ Logger.info("Terminating APRS-IS connection: #{inspect(reason)}")
+
+ # Cancel timers
+ if Map.has_key?(state, :timer), do: Process.cancel_timer(state.timer)
+ if Map.has_key?(state, :keepalive_timer), do: Process.cancel_timer(state.keepalive_timer)
+
+ # Close socket
+ if Map.has_key?(state, :socket) do
+ Logger.info("Closing socket")
+ :gen_tcp.close(state.socket)
+ end
:normal
end
diff --git a/lib/aprs/is/is_supervisor.ex b/lib/aprs/is/is_supervisor.ex
index 9cca041..681a40d 100644
--- a/lib/aprs/is/is_supervisor.ex
+++ b/lib/aprs/is/is_supervisor.ex
@@ -9,9 +9,11 @@ defmodule Aprs.Is.IsSupervisor do
@impl true
def init(:ok) do
children = [
- Aprs.Is
+ {Aprs.Is, []}
]
- Supervisor.init(children, strategy: :one_for_one)
+ # Supervisor will restart children max 3 times in 5 seconds
+ # This prevents rapid reconnection attempts
+ Supervisor.init(children, strategy: :one_for_one, max_restarts: 3, max_seconds: 5)
end
end
diff --git a/lib/aprs_web/components/layouts.ex b/lib/aprs_web/components/layouts.ex
index 294e433..e905149 100644
--- a/lib/aprs_web/components/layouts.ex
+++ b/lib/aprs_web/components/layouts.ex
@@ -3,4 +3,20 @@ defmodule AprsWeb.Layouts do
use AprsWeb, :html
embed_templates "layouts/*"
+
+ def body_class(assigns) do
+ classes = ["bg-white antialiased"]
+
+ # Check if this is the home page (map)
+ is_home =
+ cond do
+ # For regular controller requests
+ assigns[:conn] && assigns.conn.request_path == "/" -> true
+ # For LiveView requests
+ assigns[:socket] && assigns.socket.view == AprsWeb.MapLive.Index -> true
+ true -> false
+ end
+
+ if is_home, do: ["home-page" | classes], else: classes
+ end
end
diff --git a/lib/aprs_web/components/layouts/root.html.heex b/lib/aprs_web/components/layouts/root.html.heex
index 2f1424c..f86de4a 100644
--- a/lib/aprs_web/components/layouts/root.html.heex
+++ b/lib/aprs_web/components/layouts/root.html.heex
@@ -11,7 +11,7 @@
-
+
<%= if @current_user do %>
diff --git a/lib/aprs_web/controllers/page_controller.ex b/lib/aprs_web/controllers/page_controller.ex
index 15c54f3..9a524b9 100644
--- a/lib/aprs_web/controllers/page_controller.ex
+++ b/lib/aprs_web/controllers/page_controller.ex
@@ -2,13 +2,11 @@ defmodule AprsWeb.PageController do
use AprsWeb, :controller
def home(conn, _params) do
- # The home page is often custom made,
- # so skip the default app layout.
- render(conn, :home, layout: false)
+ render(conn, :home)
end
def map(conn, _params) do
- render(conn, :map, layout: false)
+ render(conn, :map)
end
def packets(conn, _params) do
diff --git a/lib/aprs_web/controllers/page_html/home.html.heex b/lib/aprs_web/controllers/page_html/home.html.heex
deleted file mode 100644
index ca89da6..0000000
--- a/lib/aprs_web/controllers/page_html/home.html.heex
+++ /dev/null
@@ -1,236 +0,0 @@
-
-
-
-
-
-
-
- Phoenix Framework
-
- v1.7
-
-
-
- Peace of mind from prototype to production.
-
-
- Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale.
-