pubsub
This commit is contained in:
parent
d98388895b
commit
c151c34857
8 changed files with 347 additions and 105 deletions
|
|
@ -31,7 +31,9 @@ defmodule Aprs.Application do
|
||||||
{Cluster.Supervisor, [topologies, [name: Aprs.ClusterSupervisor]]},
|
{Cluster.Supervisor, [topologies, [name: Aprs.ClusterSupervisor]]},
|
||||||
# Start Oban for background jobs
|
# Start Oban for background jobs
|
||||||
{Oban, :aprs |> Application.get_env(Oban, []) |> Keyword.put(:queues, default: 10, maintenance: 2)},
|
{Oban, :aprs |> Application.get_env(Oban, []) |> Keyword.put(:queues, default: 10, maintenance: 2)},
|
||||||
Aprs.Presence
|
Aprs.Presence,
|
||||||
|
Aprs.AprsIsConnection,
|
||||||
|
Aprs.PostgresNotifier
|
||||||
]
|
]
|
||||||
|
|
||||||
children =
|
children =
|
||||||
|
|
|
||||||
123
lib/aprs/aprs_is_connection.ex
Normal file
123
lib/aprs/aprs_is_connection.ex
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
defmodule Aprs.AprsIsConnection do
|
||||||
|
@moduledoc """
|
||||||
|
Maintains a supervised TCP connection to APRS-IS, with reconnection logic and
|
||||||
|
exponential backoff. Broadcasts each received line via Phoenix.PubSub and emits
|
||||||
|
telemetry events for connection, disconnection, errors, and packet receipt.
|
||||||
|
"""
|
||||||
|
use GenServer
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@type state :: %{
|
||||||
|
socket: port() | nil,
|
||||||
|
backoff: non_neg_integer()
|
||||||
|
}
|
||||||
|
|
||||||
|
@reconnect_initial 2_000
|
||||||
|
@reconnect_max 60_000
|
||||||
|
@pubsub_topic "aprs_is:raw"
|
||||||
|
|
||||||
|
# Public API
|
||||||
|
@spec start_link(Keyword.t()) :: GenServer.on_start()
|
||||||
|
def start_link(_opts) do
|
||||||
|
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Send a raw string to APRS-IS.
|
||||||
|
"""
|
||||||
|
@spec send_packet(String.t()) :: :ok | {:error, term()}
|
||||||
|
def send_packet(packet) do
|
||||||
|
GenServer.call(__MODULE__, {:send, packet})
|
||||||
|
end
|
||||||
|
|
||||||
|
# GenServer callbacks
|
||||||
|
@impl true
|
||||||
|
def init(state) do
|
||||||
|
schedule_connect(0)
|
||||||
|
{:ok, Map.merge(%{socket: nil, backoff: @reconnect_initial}, state)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info(:connect, state) do
|
||||||
|
case connect_aprs_is() do
|
||||||
|
{:ok, socket} ->
|
||||||
|
Logger.info("Connected to APRS-IS")
|
||||||
|
:telemetry.execute([:aprs, :is, :connected], %{}, %{})
|
||||||
|
{:noreply, %{state | socket: socket, backoff: @reconnect_initial}}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.error("APRS-IS connection failed: #{inspect(reason)}. Retrying in #{state.backoff} ms.")
|
||||||
|
|
||||||
|
:telemetry.execute([:aprs, :is, :connect_error], %{}, %{reason: reason})
|
||||||
|
schedule_connect(state.backoff)
|
||||||
|
{:noreply, %{state | socket: nil, backoff: min(state.backoff * 2, @reconnect_max)}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:tcp, _socket, data}, state) do
|
||||||
|
# Each line received from APRS-IS
|
||||||
|
Phoenix.PubSub.broadcast(Aprs.PubSub, @pubsub_topic, {:aprs_is_line, data})
|
||||||
|
:telemetry.execute([:aprs, :is, :packet], %{size: byte_size(data)}, %{data: data})
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:tcp_closed, _socket}, state) do
|
||||||
|
Logger.warning("APRS-IS connection closed. Reconnecting...")
|
||||||
|
:telemetry.execute([:aprs, :is, :disconnected], %{}, %{})
|
||||||
|
schedule_connect(@reconnect_initial)
|
||||||
|
{:noreply, %{state | socket: nil, backoff: @reconnect_initial}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:tcp_error, _socket, reason}, state) do
|
||||||
|
Logger.error("APRS-IS TCP error: #{inspect(reason)}. Reconnecting...")
|
||||||
|
:telemetry.execute([:aprs, :is, :tcp_error], %{}, %{reason: reason})
|
||||||
|
schedule_connect(@reconnect_initial)
|
||||||
|
{:noreply, %{state | socket: nil, backoff: @reconnect_initial}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call({:send, packet}, _from, %{socket: socket} = state) when is_port(socket) do
|
||||||
|
:ok = :gen_tcp.send(socket, packet <> "\r\n")
|
||||||
|
{:reply, :ok, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_call({:send, _packet}, _from, state) do
|
||||||
|
{:reply, {:error, :not_connected}, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def terminate(_reason, %{socket: socket}) when is_port(socket) do
|
||||||
|
:gen_tcp.close(socket)
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def terminate(_reason, _state), do: :ok
|
||||||
|
|
||||||
|
defp schedule_connect(delay) do
|
||||||
|
Process.send_after(self(), :connect, delay)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp connect_aprs_is do
|
||||||
|
host = Application.get_env(:aprs, :aprs_is_host, ~c"rotate.aprs2.net")
|
||||||
|
port = Application.get_env(:aprs, :aprs_is_port, 14_580)
|
||||||
|
callsign = Application.get_env(:aprs, :aprs_is_callsign, "N0CALL")
|
||||||
|
passcode = Application.get_env(:aprs, :aprs_is_passcode, "00000")
|
||||||
|
filter = Application.get_env(:aprs, :aprs_is_filter, "")
|
||||||
|
|
||||||
|
opts = [:binary, active: true, packet: :line, keepalive: true]
|
||||||
|
|
||||||
|
case :gen_tcp.connect(host, port, opts) do
|
||||||
|
{:ok, socket} ->
|
||||||
|
login = "user #{callsign} pass #{passcode} vers aprs.me 0.1 #{filter}\r\n"
|
||||||
|
:ok = :gen_tcp.send(socket, login)
|
||||||
|
{:ok, socket}
|
||||||
|
|
||||||
|
error ->
|
||||||
|
error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
44
lib/aprs/postgres_notifier.ex
Normal file
44
lib/aprs/postgres_notifier.ex
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
defmodule Aprs.PostgresNotifier do
|
||||||
|
@moduledoc """
|
||||||
|
Listens to PostgreSQL NOTIFY events on the "aprs_events" and "aprs_packets" channels and broadcasts
|
||||||
|
them via Phoenix.PubSub for reactive, event-driven updates.
|
||||||
|
"""
|
||||||
|
use GenServer
|
||||||
|
|
||||||
|
@event_channel "aprs_events"
|
||||||
|
@event_topic "postgres:aprs_events"
|
||||||
|
@packet_channel "aprs_packets"
|
||||||
|
@packet_topic "postgres:aprs_packets"
|
||||||
|
|
||||||
|
def start_link(_opts) do
|
||||||
|
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(_) do
|
||||||
|
{:ok, conn} = Postgrex.Notifications.start_link(Aprs.Repo.config())
|
||||||
|
{:ok, _ref1} = Postgrex.Notifications.listen(conn, @event_channel)
|
||||||
|
{:ok, _ref2} = Postgrex.Notifications.listen(conn, @packet_channel)
|
||||||
|
{:ok, %{conn: conn}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:notification, _conn, _pid, @event_channel, payload}, state) do
|
||||||
|
Phoenix.PubSub.broadcast(Aprs.PubSub, @event_topic, {:postgres_notify, payload})
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:notification, _conn, _pid, @packet_channel, payload}, state) do
|
||||||
|
case Jason.decode(payload) do
|
||||||
|
{:ok, packet} ->
|
||||||
|
Phoenix.PubSub.broadcast(Aprs.PubSub, @packet_topic, {:postgres_packet, packet})
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
:noop
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info(_msg, state), do: {:noreply, state}
|
||||||
|
end
|
||||||
|
|
@ -10,13 +10,19 @@ defmodule AprsWeb.BadPacketsLive.Index do
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
if connected?(socket) do
|
if connected?(socket) do
|
||||||
|
# Subscribe to Postgres notifications for bad packets
|
||||||
|
Phoenix.PubSub.subscribe(Aprs.PubSub, "postgres:aprs_events")
|
||||||
# Load initial bad packets
|
# Load initial bad packets
|
||||||
bad_packets = fetch_bad_packets()
|
bad_packets = fetch_bad_packets()
|
||||||
# Extra safeguard to ensure we never show more than 100
|
# Extra safeguard to ensure we never show more than 100
|
||||||
limited_packets = Enum.take(bad_packets, 100)
|
limited_packets = Enum.take(bad_packets, 100)
|
||||||
# Schedule automatic refresh every 5 seconds
|
|
||||||
:timer.send_interval(5000, self(), :refresh_bad_packets)
|
{:ok,
|
||||||
{:ok, assign(socket, bad_packets: limited_packets, loading: false, last_updated: DateTime.utc_now())}
|
assign(socket,
|
||||||
|
bad_packets: limited_packets,
|
||||||
|
loading: false,
|
||||||
|
last_updated: DateTime.utc_now()
|
||||||
|
)}
|
||||||
else
|
else
|
||||||
{:ok, assign(socket, bad_packets: [], loading: false, last_updated: nil)}
|
{:ok, assign(socket, bad_packets: [], loading: false, last_updated: nil)}
|
||||||
end
|
end
|
||||||
|
|
@ -38,7 +44,8 @@ defmodule AprsWeb.BadPacketsLive.Index do
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info(:refresh_bad_packets, socket) do
|
def handle_info({:postgres_notify, _payload}, socket) do
|
||||||
|
# Optionally filter payload for bad packet events
|
||||||
send(self(), :do_refresh)
|
send(self(), :do_refresh)
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
@ -48,7 +55,13 @@ defmodule AprsWeb.BadPacketsLive.Index do
|
||||||
bad_packets = fetch_bad_packets()
|
bad_packets = fetch_bad_packets()
|
||||||
# Extra safeguard to ensure we never show more than 100
|
# Extra safeguard to ensure we never show more than 100
|
||||||
limited_packets = Enum.take(bad_packets, 100)
|
limited_packets = Enum.take(bad_packets, 100)
|
||||||
{:noreply, assign(socket, bad_packets: limited_packets, loading: false, last_updated: DateTime.utc_now())}
|
|
||||||
|
{:noreply,
|
||||||
|
assign(socket,
|
||||||
|
bad_packets: limited_packets,
|
||||||
|
loading: false,
|
||||||
|
last_updated: DateTime.utc_now()
|
||||||
|
)}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fetch_bad_packets(limit \\ 100) do
|
defp fetch_bad_packets(limit \\ 100) do
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
"""
|
"""
|
||||||
use AprsWeb, :live_view
|
use AprsWeb, :live_view
|
||||||
|
|
||||||
alias Aprs.EncodingUtils
|
|
||||||
alias AprsWeb.Endpoint
|
alias AprsWeb.Endpoint
|
||||||
alias AprsWeb.Helpers.AprsSymbols
|
alias AprsWeb.Helpers.AprsSymbols
|
||||||
alias Parser.Types.MicE
|
alias Parser.Types.MicE
|
||||||
|
|
@ -15,15 +14,18 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
@ip_api_url "https://ip-api.com/json/"
|
@ip_api_url "https://ip-api.com/json/"
|
||||||
@finch_name Aprs.Finch
|
@finch_name Aprs.Finch
|
||||||
@default_replay_speed 1000
|
@default_replay_speed 1000
|
||||||
|
@debounce_interval 200
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
one_hour_ago = DateTime.add(DateTime.utc_now(), -3600, :second)
|
one_hour_ago = DateTime.add(DateTime.utc_now(), -3600, :second)
|
||||||
|
|
||||||
socket = assign_defaults(socket, one_hour_ago)
|
socket = assign_defaults(socket, one_hour_ago)
|
||||||
|
socket = assign(socket, packet_buffer: [], buffer_timer: nil)
|
||||||
|
|
||||||
if connected?(socket) do
|
if connected?(socket) do
|
||||||
Endpoint.subscribe("aprs_messages")
|
Endpoint.subscribe("aprs_messages")
|
||||||
|
Phoenix.PubSub.subscribe(Aprs.PubSub, "postgres:aprs_packets")
|
||||||
maybe_start_geolocation(socket)
|
maybe_start_geolocation(socket)
|
||||||
schedule_timers()
|
schedule_timers()
|
||||||
end
|
end
|
||||||
|
|
@ -278,61 +280,23 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
|
|
||||||
@spec process_bounds_update(map(), Socket.t()) :: Socket.t()
|
@spec process_bounds_update(map(), Socket.t()) :: Socket.t()
|
||||||
defp process_bounds_update(map_bounds, socket) do
|
defp process_bounds_update(map_bounds, socket) do
|
||||||
# Filter visible packets to only include those within the new bounds and time threshold
|
# Remove out-of-bounds packets and markers immediately
|
||||||
new_visible_packets =
|
new_visible_packets =
|
||||||
socket.assigns.visible_packets
|
socket.assigns.visible_packets
|
||||||
|> Enum.filter(fn {_callsign, packet} ->
|
|> Enum.filter(fn {_k, packet} -> within_bounds?(packet, map_bounds) end)
|
||||||
within_bounds?(packet, map_bounds) &&
|
|
||||||
packet_within_time_threshold?(packet, socket.assigns.packet_age_threshold)
|
|
||||||
end)
|
|
||||||
|> Map.new()
|
|> Map.new()
|
||||||
|
|
||||||
# Get packets that are no longer visible (to remove from map)
|
|
||||||
packets_to_remove =
|
packets_to_remove =
|
||||||
socket.assigns.visible_packets
|
socket.assigns.visible_packets
|
||||||
|> Enum.reject(fn {_callsign, packet} ->
|
|> Enum.reject(fn {_k, packet} -> within_bounds?(packet, map_bounds) end)
|
||||||
within_bounds?(packet, map_bounds)
|
|> Enum.map(fn {k, _} -> k end)
|
||||||
|
|
||||||
|
socket =
|
||||||
|
Enum.reduce(packets_to_remove, socket, fn k, acc ->
|
||||||
|
push_event(acc, "remove_marker", %{id: k})
|
||||||
end)
|
end)
|
||||||
|> Enum.map(fn {callsign, _packet} -> callsign end)
|
|
||||||
|
|
||||||
# Clear markers that are outside the new bounds
|
assign(socket, map_bounds: map_bounds, visible_packets: new_visible_packets)
|
||||||
socket =
|
|
||||||
if Enum.any?(packets_to_remove) do
|
|
||||||
# Remove markers that are outside bounds
|
|
||||||
socket =
|
|
||||||
Enum.reduce(packets_to_remove, socket, fn callsign, acc_socket ->
|
|
||||||
push_event(acc_socket, "remove_marker", %{id: callsign})
|
|
||||||
end)
|
|
||||||
|
|
||||||
# Also send a general filter event to clean up any remaining out-of-bounds markers
|
|
||||||
push_event(socket, "filter_markers_by_bounds", %{bounds: map_bounds})
|
|
||||||
else
|
|
||||||
socket
|
|
||||||
end
|
|
||||||
|
|
||||||
# If replay is not active, update the replay packets based on the new bounds
|
|
||||||
socket =
|
|
||||||
if socket.assigns.replay_active do
|
|
||||||
socket
|
|
||||||
else
|
|
||||||
# Clear any existing replay data when bounds change significantly
|
|
||||||
socket =
|
|
||||||
assign(socket,
|
|
||||||
replay_packets: [],
|
|
||||||
replay_index: 0,
|
|
||||||
historical_packets: %{},
|
|
||||||
map_ready: true
|
|
||||||
)
|
|
||||||
|
|
||||||
socket
|
|
||||||
end
|
|
||||||
|
|
||||||
assign(socket,
|
|
||||||
map_bounds: map_bounds,
|
|
||||||
visible_packets: new_visible_packets,
|
|
||||||
bounds_update_timer: nil,
|
|
||||||
pending_bounds: nil
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
@ -392,46 +356,60 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
# Clean up packets older than 1 hour from the map display
|
# Clean up packets older than 1 hour from the map display
|
||||||
handle_cleanup_old_packets(socket)
|
handle_cleanup_old_packets(socket)
|
||||||
|
|
||||||
%{event: "packet", payload: payload} ->
|
{:postgres_packet, packet} ->
|
||||||
# Sanitize the packet to prevent encoding errors
|
|
||||||
sanitized_packet = EncodingUtils.sanitize_packet(payload)
|
|
||||||
|
|
||||||
# Add received timestamp if not present
|
|
||||||
sanitized_packet = Map.put_new(sanitized_packet, :received_at, DateTime.utc_now())
|
|
||||||
|
|
||||||
# Only process packets with position data that are within current map bounds
|
# Only process packets with position data that are within current map bounds
|
||||||
# AND are not older than 1 hour
|
if within_bounds?(packet, socket.assigns.map_bounds) do
|
||||||
if has_position_data?(sanitized_packet) &&
|
# Add to buffer for debounced batch update
|
||||||
within_bounds?(sanitized_packet, socket.assigns.map_bounds) &&
|
buffer = [packet | socket.assigns.packet_buffer]
|
||||||
packet_within_time_threshold?(sanitized_packet, socket.assigns.packet_age_threshold) do
|
socket = assign(socket, packet_buffer: buffer)
|
||||||
# Convert to a simple map structure for JSON encoding
|
# If no timer, start one
|
||||||
packet_data = build_packet_data(sanitized_packet)
|
socket =
|
||||||
|
if socket.assigns.buffer_timer == nil do
|
||||||
# Only push if we have valid packet data
|
timer = Process.send_after(self(), :flush_packet_buffer, @debounce_interval)
|
||||||
if packet_data do
|
assign(socket, buffer_timer: timer)
|
||||||
# Generate a unique key for this packet
|
else
|
||||||
callsign_key =
|
|
||||||
"#{sanitized_packet.base_callsign}#{if sanitized_packet.ssid, do: "-#{sanitized_packet.ssid}", else: ""}"
|
|
||||||
|
|
||||||
# Update visible packets tracking
|
|
||||||
visible_packets =
|
|
||||||
Map.put(socket.assigns.visible_packets, callsign_key, sanitized_packet)
|
|
||||||
|
|
||||||
# Push the packet to the client-side JavaScript
|
|
||||||
socket =
|
|
||||||
socket
|
socket
|
||||||
|> push_event("new_packet", packet_data)
|
end
|
||||||
|> assign(visible_packets: visible_packets)
|
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
else
|
|
||||||
# Invalid packet data, skip it
|
|
||||||
{:noreply, socket}
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
# Ignore packets without position data, outside bounds, or too old
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
:flush_packet_buffer ->
|
||||||
|
packets = Enum.reverse(socket.assigns.packet_buffer)
|
||||||
|
visible_packets = socket.assigns.visible_packets
|
||||||
|
|
||||||
|
{new_visible_packets, events} =
|
||||||
|
Enum.reduce(packets, {visible_packets, []}, fn packet, {vis, evs} ->
|
||||||
|
packet_data = build_packet_data(packet)
|
||||||
|
|
||||||
|
if packet_data do
|
||||||
|
callsign_key =
|
||||||
|
if Map.has_key?(packet, "id"),
|
||||||
|
do: to_string(packet["id"]),
|
||||||
|
else: System.unique_integer([:positive])
|
||||||
|
|
||||||
|
{Map.put(vis, callsign_key, packet), [{:new_packet, packet_data} | evs]}
|
||||||
|
else
|
||||||
|
{vis, evs}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Push all new packets in one event (or as a batch)
|
||||||
|
socket =
|
||||||
|
Enum.reduce(events, socket, fn {:new_packet, data}, acc ->
|
||||||
|
push_event(acc, "new_packet", data)
|
||||||
|
end)
|
||||||
|
|
||||||
|
socket =
|
||||||
|
assign(socket,
|
||||||
|
visible_packets: new_visible_packets,
|
||||||
|
packet_buffer: [],
|
||||||
|
buffer_timer: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -800,23 +778,6 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
Enum.sort_by(packets, fn packet -> packet.received_at end)
|
Enum.sort_by(packets, fn packet -> packet.received_at end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec has_position_data?(map() | struct()) :: boolean()
|
|
||||||
defp has_position_data?(packet) do
|
|
||||||
case packet.data_extended do
|
|
||||||
%MicE{} = mic_e ->
|
|
||||||
# MicE packets have lat/lon in separate components
|
|
||||||
is_number(mic_e.lat_degrees) && is_number(mic_e.lat_minutes) &&
|
|
||||||
is_number(mic_e.lon_degrees) && is_number(mic_e.lon_minutes)
|
|
||||||
|
|
||||||
%{latitude: lat, longitude: lon} ->
|
|
||||||
# Regular position packets have decimal lat/lon
|
|
||||||
is_number(lat) && is_number(lon)
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec within_bounds?(map() | struct(), map()) :: boolean()
|
@spec within_bounds?(map() | struct(), map()) :: boolean()
|
||||||
defp within_bounds?(packet, bounds) do
|
defp within_bounds?(packet, bounds) do
|
||||||
{lat, lng} = get_coordinates(packet)
|
{lat, lng} = get_coordinates(packet)
|
||||||
|
|
@ -1040,6 +1001,7 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def terminate(_reason, socket) do
|
def terminate(_reason, socket) do
|
||||||
|
if socket.assigns.buffer_timer, do: Process.cancel_timer(socket.assigns.buffer_timer)
|
||||||
# Clean up any pending bounds update timer
|
# Clean up any pending bounds update timer
|
||||||
if socket.assigns[:bounds_update_timer] do
|
if socket.assigns[:bounds_update_timer] do
|
||||||
Process.cancel_timer(socket.assigns.bounds_update_timer)
|
Process.cancel_timer(socket.assigns.bounds_update_timer)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
defmodule Aprs.Repo.Migrations.NotifyOnBadpacketsInsert do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
execute """
|
||||||
|
CREATE OR REPLACE FUNCTION notify_badpackets_insert() RETURNS trigger AS $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM pg_notify('aprs_events', 'bad_packet');
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
"""
|
||||||
|
|
||||||
|
execute "DROP TRIGGER IF EXISTS badpackets_notify_insert ON badpackets;"
|
||||||
|
|
||||||
|
execute """
|
||||||
|
CREATE TRIGGER badpackets_notify_insert
|
||||||
|
AFTER INSERT ON badpackets
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION notify_badpackets_insert();
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
execute "DROP TRIGGER IF EXISTS badpackets_notify_insert ON badpackets;"
|
||||||
|
execute "DROP FUNCTION IF EXISTS notify_badpackets_insert();"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
defmodule Aprs.Repo.Migrations.NotifyOnPacketsInsert do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
execute """
|
||||||
|
CREATE OR REPLACE FUNCTION notify_packets_insert() RETURNS trigger AS $$
|
||||||
|
DECLARE
|
||||||
|
payload TEXT;
|
||||||
|
BEGIN
|
||||||
|
payload := json_build_object(
|
||||||
|
'id', NEW.id,
|
||||||
|
'lat', NEW.lat,
|
||||||
|
'lon', NEW.lon,
|
||||||
|
'inserted_at', NEW.inserted_at
|
||||||
|
)::text;
|
||||||
|
PERFORM pg_notify('aprs_packets', payload);
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
"""
|
||||||
|
|
||||||
|
execute "DROP TRIGGER IF EXISTS packets_notify_insert ON packets;"
|
||||||
|
|
||||||
|
execute """
|
||||||
|
CREATE TRIGGER packets_notify_insert
|
||||||
|
AFTER INSERT ON packets
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION notify_packets_insert();
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
execute "DROP TRIGGER IF EXISTS packets_notify_insert ON packets;"
|
||||||
|
execute "DROP FUNCTION IF EXISTS notify_packets_insert();"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
defmodule Aprs.Repo.Migrations.RecreateNotifyPacketsInsertFunction do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
execute "DROP TRIGGER IF EXISTS packets_notify_insert ON packets;"
|
||||||
|
execute "DROP FUNCTION IF EXISTS notify_packets_insert();"
|
||||||
|
|
||||||
|
execute """
|
||||||
|
CREATE OR REPLACE FUNCTION notify_packets_insert() RETURNS trigger AS $$
|
||||||
|
DECLARE
|
||||||
|
payload TEXT;
|
||||||
|
BEGIN
|
||||||
|
payload := json_build_object(
|
||||||
|
'id', NEW.id,
|
||||||
|
'lat', NEW.lat,
|
||||||
|
'lon', NEW.lon,
|
||||||
|
'inserted_at', NEW.inserted_at
|
||||||
|
)::text;
|
||||||
|
PERFORM pg_notify('aprs_packets', payload);
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
"""
|
||||||
|
|
||||||
|
execute """
|
||||||
|
CREATE TRIGGER packets_notify_insert
|
||||||
|
AFTER INSERT ON packets
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION notify_packets_insert();
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
execute "DROP TRIGGER IF EXISTS packets_notify_insert ON packets;"
|
||||||
|
execute "DROP FUNCTION IF EXISTS notify_packets_insert();"
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Add table
Reference in a new issue