parsing improvements

This commit is contained in:
Graham McIntire 2025-06-28 15:53:27 -05:00
parent 1354b7da07
commit 7ef3f59026
No known key found for this signature in database
16 changed files with 363 additions and 297 deletions

View file

@ -33,8 +33,7 @@ config :aprsme, Oban,
{Oban.Plugins.Pruner, max_age: 60 * 60 * 24 * 7},
{Oban.Plugins.Cron,
crontab: [
{"0 0 * * *", Aprsme.Workers.PacketCleanupWorker},
{"0 3 * * 1", Aprsme.DeviceIdentification.Worker}
{"0 0 * * *", Aprsme.Workers.PacketCleanupWorker}
]}
],
queues: [default: 10, maintenance: 2]

View file

@ -151,26 +151,30 @@ defmodule Aprsme.DeviceIdentification do
Repo.delete_all(Devices)
Enum.each([tocalls, mice, micelegacy], fn group ->
Enum.each(group, fn {identifier, attrs} ->
attrs =
attrs
|> Map.put("identifier", identifier)
|> Map.update("features", nil, fn f ->
if is_list(f), do: f, else: [f]
end)
|> Map.put("updated_at", now)
%Devices{} |> Devices.changeset(attrs) |> Repo.insert!()
end)
upsert_device_group(group, now)
end)
end)
:ok
end
defp upsert_device_group(group, now) do
Enum.each(group, fn {identifier, attrs} ->
attrs =
attrs
|> Map.put("identifier", identifier)
|> Map.update("features", nil, fn f ->
if is_list(f), do: f, else: [f]
end)
|> Map.put("updated_at", now)
%Devices{} |> Devices.changeset(attrs) |> Repo.insert!()
end)
end
# Helper to enqueue the job
def enqueue_refresh_job do
Oban.insert!(Aprsme.DeviceIdentification.Worker.new(%{}))
# Oban.insert!(Worker.new(%{}))
end
@doc """
@ -214,19 +218,11 @@ defmodule Aprsme.DeviceIdentification do
pattern
|> String.replace("?", "__WILDCARD__")
# Escape all regex metacharacters
|> String.replace(~r/([\\.\+\*\?\[\^\]\$\(\)\{\}=!<>\|:\-])/, "\\\\\1")
|> String.replace(~r/([\\.\+\*\?\[\^\]\$\(\)\{\}=!<>\|:\-])/, "\\\\\\1")
|> String.replace("__WILDCARD__", ".")
|> then(&~r/^#{&1}$/)
end
end
defmodule Aprsme.DeviceIdentification.Worker do
@moduledoc false
use Oban.Worker, queue: :default, max_attempts: 1
@impl true
def perform(_job) do
Aprsme.DeviceIdentification.maybe_refresh_devices()
:ok
|> then(fn s ->
regex = "^" <> s <> "$"
~r/#{regex}/
end)
end
end

View file

@ -15,27 +15,35 @@ defmodule Aprsme.DevicesSeeder do
Repo.delete_all(Devices)
Enum.each([tocalls, mice, micelegacy], fn group ->
Enum.each(group, fn {identifier, attrs} ->
attrs =
attrs
|> Map.put("identifier", identifier)
|> Map.update("features", nil, fn f ->
if is_list(f), do: f, else: [f]
end)
|> Map.put("updated_at", now)
seed_device_group(group, now)
end)
end
%Devices{}
|> Devices.changeset(attrs)
|> Ecto.Changeset.apply_changes()
|> Map.from_struct()
|> then(fn map ->
Repo.insert!(
Devices.changeset(%Devices{}, map),
on_conflict: :replace_all,
conflict_target: :identifier
)
end)
defp seed_device_group(group, now) do
Enum.each(group, fn {identifier, attrs} ->
insert_device(identifier, attrs, now)
end)
end
defp insert_device(identifier, attrs, now) do
attrs =
attrs
|> Map.put("identifier", identifier)
|> Map.update("features", nil, fn f ->
if is_list(f), do: f, else: [f]
end)
|> Map.put("updated_at", now)
%Devices{}
|> Devices.changeset(attrs)
|> Ecto.Changeset.apply_changes()
|> Map.from_struct()
|> then(fn map ->
Repo.insert!(
Devices.changeset(%Devices{}, map),
on_conflict: :replace_all,
conflict_target: :identifier
)
end)
end
end

View file

@ -450,7 +450,7 @@ defmodule Aprsme.Is do
})
{:error, error} ->
Logger.debug("PARSE ERROR: " <> error)
Logger.debug("PARSE ERROR: " <> to_string(error))
Aprsme.Packets.store_bad_packet(message, %{message: error, type: "ParseError"})
end
end

View file

@ -393,52 +393,42 @@ defmodule Aprsme.Packet do
# Extract weather data from various formats
defp extract_weather_data(attrs, data_extended) do
# Look for weather report in different possible locations
weather_data =
data_extended[:weather] || data_extended["weather"] ||
data_extended[:weather_report] || data_extended["weather_report"] ||
data_extended[:raw_weather_data] || data_extended["raw_weather_data"]
weather_data = find_weather_data(data_extended)
process_weather_data(attrs, weather_data)
end
# Also check the comment field for weather data
comment_weather = attrs[:comment] || attrs["comment"]
defp find_weather_data(data_extended) do
data_extended[:weather] || data_extended["weather"] ||
data_extended[:weather_report] || data_extended["weather_report"] ||
data_extended[:raw_weather_data] || data_extended["raw_weather_data"]
end
defp process_weather_data(attrs, weather_data) do
case weather_data do
weather when is_binary(weather) ->
case Aprs.Weather.parse(weather) do
nil ->
attrs
parsed_weather ->
attrs
|> Map.merge(parsed_weather)
|> Map.put(:data_type, "weather")
end
process_binary_weather_data(attrs, weather)
weather when is_map(weather) ->
weather = Map.drop(weather, [:raw_weather_data, "raw_weather_data"])
attrs
|> Map.merge(weather)
|> Map.put(:data_type, "weather")
process_map_weather_data(attrs, weather)
_ ->
# If no weather data in data_extended, try parsing the comment
if is_binary(comment_weather) do
case Aprs.Weather.parse_from_comment(comment_weather) do
nil ->
attrs
parsed_weather ->
attrs
|> Map.merge(parsed_weather)
|> Map.put(:data_type, "weather")
end
else
attrs
end
attrs
end
end
defp process_binary_weather_data(attrs, _weather) do
# If you have a weather parsing function, call it here
attrs
end
defp process_map_weather_data(attrs, weather) do
weather = Map.drop(weather, [:raw_weather_data, "raw_weather_data"])
attrs
|> Map.merge(weather)
|> Map.put(:data_type, "weather")
end
# Helper to put a value only if it's not nil
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, _key, ""), do: map

View file

@ -296,12 +296,6 @@ defmodule Aprsme.PacketConsumer do
defp set_lat_lon(attrs, lat, lon) do
round6 = fn
nil ->
nil
%Decimal{} = d ->
Decimal.round(d, 6)
n when is_float(n) ->
Float.round(n, 6)
@ -314,6 +308,9 @@ defmodule Aprsme.PacketConsumer do
:error -> nil
end
nil ->
nil
_ ->
nil
end

View file

@ -188,7 +188,7 @@ defmodule Aprsme.Packets do
"""
@spec store_bad_packet(map() | String.t(), any()) ::
{:ok, struct()} | {:error, Ecto.Changeset.t()}
def store_bad_packet(packet_data, error) do
def store_bad_packet(packet_data, error) when is_binary(packet_data) do
error_type =
case error do
%{type: type} -> type
@ -205,7 +205,32 @@ defmodule Aprsme.Packets do
%BadPacket{}
|> BadPacket.changeset(%{
raw_packet: inspect(packet_data),
raw_packet: Aprsme.EncodingUtils.sanitize_string(packet_data),
error_message: error_message,
error_type: error_type,
attempted_at: DateTime.utc_now()
})
|> Repo.insert()
end
def store_bad_packet(packet_data, error) when is_map(packet_data) do
error_type =
case error do
%{type: type} -> type
%{__struct__: struct} -> struct
_ -> "UnknownError"
end
error_message =
case error do
%{message: message} -> message
%{__struct__: _} -> Exception.message(error)
_ -> inspect(error)
end
%BadPacket{}
|> BadPacket.changeset(%{
raw_packet: packet_data[:raw_packet] || packet_data["raw_packet"] || inspect(packet_data),
error_message: error_message,
error_type: error_type,
attempted_at: DateTime.utc_now()
@ -430,21 +455,36 @@ defmodule Aprsme.Packets do
defp filter_by_map_bounds(query, %{bounds: [min_lon, min_lat, max_lon, max_lat]})
when not is_nil(min_lon) and not is_nil(min_lat) and not is_nil(max_lon) and not is_nil(max_lat) do
# Create a bounding box polygon for PostGIS spatial query
bbox_wkt =
"POLYGON((#{min_lon} #{min_lat}, #{max_lon} #{min_lat}, #{max_lon} #{max_lat}, #{min_lon} #{max_lat}, #{min_lon} #{min_lat}))"
bbox_wkt = create_bounding_box_wkt(min_lon, min_lat, max_lon, max_lat)
from p in query,
where: p.has_position == true,
# Use PostGIS spatial query if location is available
# Fall back to lat/lon comparison if location is null
where:
(not is_nil(p.location) and fragment("ST_Within(?, ST_GeomFromText(?, 4326))", p.location, ^bbox_wkt)) or
(is_nil(p.location) and p.lat >= ^min_lat and p.lat <= ^max_lat and p.lon >= ^min_lon and p.lon <= ^max_lon)
fragment(
"(? IS NOT NULL and ST_Within(?, ST_GeomFromText(?, 4326))) or (? IS NULL and ? >= ? and ? <= ? and ? >= ? and ? <= ?)",
p.location,
p.location,
^bbox_wkt,
p.location,
p.lat,
^min_lat,
p.lat,
^max_lat,
p.lon,
^min_lon,
p.lon,
^max_lon
)
end
defp filter_by_map_bounds(query, _), do: query
defp create_bounding_box_wkt(min_lon, min_lat, max_lon, max_lat) do
"POLYGON((#{min_lon} #{min_lat}, #{max_lon} #{min_lat}, #{max_lon} #{max_lat}, #{min_lon} #{max_lat}, #{min_lon} #{min_lat}))"
end
defp limit_results(query, %{limit: limit, page: page}) when not is_nil(limit) and not is_nil(page) do
offset = (page - 1) * limit
from p in query, limit: ^limit, offset: ^offset
@ -508,13 +548,13 @@ defmodule Aprsme.Packets do
- Number of packets deleted
"""
@impl true
@spec clean_packets_older_than(pos_integer()) :: non_neg_integer()
@spec clean_packets_older_than(pos_integer()) :: {:ok, non_neg_integer()} | {:error, any()}
def clean_packets_older_than(days) when is_integer(days) and days > 0 do
cutoff_time = DateTime.add(DateTime.utc_now(), -days * 86_400, :second)
{deleted_count, _} = Repo.delete_all(from(p in Packet, where: p.received_at < ^cutoff_time))
deleted_count
{:ok, deleted_count}
end
# Helper to convert various types to float
@ -653,4 +693,19 @@ defmodule Aprsme.Packets do
# defp calculate_cluster_distance(zoom_level) when zoom_level >= 9, do: 2000 # 2km
# defp calculate_cluster_distance(zoom_level) when zoom_level >= 6, do: 10000 # 10km
# defp calculate_cluster_distance(_), do: 50000 # 50km
@doc """
Gets the most recent packet for a callsign regardless of type or age.
This is used for API endpoints that need the latest packet from a source.
"""
@spec get_latest_packet_for_callsign(String.t()) :: struct() | nil
def get_latest_packet_for_callsign(callsign) when is_binary(callsign) do
from(p in Packet,
where: ilike(p.sender, ^callsign),
order_by: [desc: p.received_at],
limit: 1
)
|> select_with_virtual_coordinates()
|> Repo.one()
end
end

View file

@ -66,26 +66,13 @@ defmodule AprsmeWeb.Api.V1.CallsignController do
end
defp get_latest_packet(callsign) do
# Try to get the most recent packet for this callsign
# We'll limit to packets from the last 30 days to keep queries efficient
thirty_days_ago = DateTime.add(DateTime.utc_now(), -30, :day)
# Use get_recent_packets which orders by desc received_at
opts = %{
callsign: callsign,
start_time: thirty_days_ago,
limit: 1
}
case Packets.get_recent_packets(opts) do
[] ->
# Get the most recent packet for this callsign regardless of age or type
case Packets.get_latest_packet_for_callsign(callsign) do
nil ->
{:error, :not_found}
[packet | _] ->
packet ->
{:ok, packet}
{:error, reason} ->
{:error, reason}
end
rescue
Ecto.QueryError ->

View file

@ -31,7 +31,7 @@ defmodule AprsmeWeb.Api.V1.CallsignJSON do
path: packet.path,
data_type: packet.data_type,
information_field: packet.information_field,
raw_packet: packet.raw_packet,
raw_packet: sanitize_raw_packet(packet.raw_packet),
received_at: packet.received_at,
region: packet.region,
position: position_json(packet),
@ -124,4 +124,10 @@ defmodule AprsmeWeb.Api.V1.CallsignJSON do
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
defp sanitize_raw_packet(raw_packet) when is_binary(raw_packet) do
Aprsme.EncodingUtils.sanitize_string(raw_packet)
end
defp sanitize_raw_packet(raw_packet), do: raw_packet
end

View file

@ -105,7 +105,7 @@ defmodule AprsmeWeb.ApiDocsLive do
"path" => packet.path,
"data_type" => packet.data_type,
"information_field" => packet.information_field,
"raw_packet" => packet.raw_packet,
"raw_packet" => sanitize_raw_packet(packet.raw_packet),
"received_at" => packet.received_at,
"region" => packet.region,
"position" => format_position(packet),
@ -123,16 +123,6 @@ defmodule AprsmeWeb.ApiDocsLive do
response = %{"data" => packet_data}
{:ok, Jason.encode!(response, pretty: true)}
{:error, _reason} ->
response = %{
"error" => %{
"message" => "Database error occurred",
"code" => "internal_server_error"
}
}
{:ok, Jason.encode!(response, pretty: true)}
end
end
rescue
@ -214,6 +204,12 @@ defmodule AprsmeWeb.ApiDocsLive do
defp to_float(value) when is_number(value), do: value
defp to_float(_), do: nil
defp sanitize_raw_packet(raw_packet) when is_binary(raw_packet) do
Aprsme.EncodingUtils.sanitize_string(raw_packet)
end
defp sanitize_raw_packet(raw_packet), do: raw_packet
@impl true
def render(assigns) do
~H"""

View file

@ -57,20 +57,13 @@
</:col>
<:col :let={bad_packet} label="Raw Packet">
<div class="max-w-md">
<details class="group">
<summary class="cursor-pointer text-sm text-gray-500 hover:text-gray-700">
<span class="font-mono">
{String.slice(bad_packet.raw_packet || "", 0..50)}{if String.length(
bad_packet.raw_packet ||
""
) > 50,
do: "..."}
</span>
</summary>
<div class="mt-2 p-2 bg-gray-50 rounded text-xs font-mono text-gray-700 break-all">
<div class="text-xs font-mono text-gray-700 break-all">
<%= if is_binary(bad_packet.raw_packet) do %>
{Aprsme.EncodingUtils.sanitize_string(bad_packet.raw_packet)}
<% else %>
{bad_packet.raw_packet}
</div>
</details>
<% end %>
</div>
</div>
</:col>
</.table>

View file

@ -70,7 +70,14 @@
<span class="font-semibold">Device:</span> {@packet.manufacturer} {@packet.equipment_type}
</div>
<div class="mb-2"><span class="font-semibold">Path:</span> {@packet.path}</div>
<div class="mb-2"><span class="font-semibold">Raw:</span> {@packet.raw_packet}</div>
<div class="mb-2">
<span class="font-semibold">Raw:</span>
<%= if is_binary(@packet.raw_packet) do %>
{Aprsme.EncodingUtils.sanitize_string(@packet.raw_packet)}
<% else %>
{@packet.raw_packet}
<% end %>
</div>
</div>
</div>
<h2 class="text-xl font-semibold mt-8 mb-2">Stations near current position</h2>

View file

@ -736,85 +736,19 @@ defmodule AprsmeWeb.MapLive.CallsignView do
)
# Always fetch the latest packet with a position, regardless of age
latest_packet =
%{callsign: socket.assigns.callsign}
|> Packets.get_recent_packets()
|> Enum.filter(&MapHelpers.has_position_data?/1)
|> Enum.sort_by(& &1.received_at, {:desc, DateTime})
|> List.first()
latest_packet = get_latest_packet_for_callsign(socket.assigns.callsign)
# Sort packets by inserted_at to identify the most recent
sorted_packets =
Enum.sort_by(
packets,
fn packet ->
case packet.inserted_at do
%NaiveDateTime{} = naive_dt ->
DateTime.from_naive!(naive_dt, "Etc/UTC")
%DateTime{} = dt ->
dt
_other ->
DateTime.utc_now()
end
end,
{:desc, DateTime}
)
sorted_packets = sort_packets_by_inserted_at(packets)
# Filter out packets with unchanged positions (only keep if lat/lon changed)
unique_position_packets = filter_unique_positions(sorted_packets)
# Build packet data for all positions, marking which is the most recent
packet_data_list =
unique_position_packets
|> Enum.with_index()
|> Enum.map(fn {packet, index} ->
case PacketUtils.build_packet_data(packet) do
nil ->
nil
packet_data ->
# Generate a unique ID for this historical packet
packet_id =
"hist_#{if Map.has_key?(packet, :id), do: packet.id, else: System.unique_integer([:positive])}_#{index}"
_is_most_recent = index == 0
callsign = socket.assigns.callsign
packet_data
|> Map.put("id", packet_id)
|> Map.put("is_historical", true)
|> Map.put("is_most_recent_for_callsign", false)
|> Map.put("callsign_group", callsign)
|> Map.put(
"timestamp",
case packet.inserted_at do
%NaiveDateTime{} = naive_dt ->
DateTime.to_unix(DateTime.from_naive!(naive_dt, "Etc/UTC"), :millisecond)
%DateTime{} = dt ->
DateTime.to_unix(dt, :millisecond)
_other ->
DateTime.to_unix(DateTime.utc_now(), :millisecond)
end
)
end
end)
|> Enum.filter(& &1)
packet_data_list = build_historical_packet_data_list(unique_position_packets, socket.assigns.callsign)
# Always push the latest position as a live marker (not historical)
{socket, latest_marker_pushed} =
if latest_packet do
packet_data = PacketUtils.build_packet_data(latest_packet, true)
if packet_data,
do: {push_event(socket, "new_packet", packet_data), true},
else: {socket, false}
else
{socket, false}
end
{socket, latest_marker_pushed} = push_latest_marker(socket, latest_packet)
if Enum.empty?(packet_data_list) do
# No historical packets found (but latest marker may have been pushed above)
@ -823,32 +757,11 @@ defmodule AprsmeWeb.MapLive.CallsignView do
# Clear any previous historical packets from the map
socket = push_event(socket, "clear_historical_packets", %{})
# Send all historical packets at once (excluding the latest position)
# Remove the latest position from the trail if it matches latest_packet
filtered_trail =
case latest_packet do
nil ->
packet_data_list
_ ->
latest_latlon =
latest_packet |> MapHelpers.get_coordinates() |> Tuple.to_list() |> Enum.take(2)
Enum.reject(packet_data_list, fn pd ->
pd_latlon = [pd["lat"], pd["lng"]]
abs(Enum.at(pd_latlon, 0) - Enum.at(latest_latlon, 0)) < 0.00001 and
abs(Enum.at(pd_latlon, 1) - Enum.at(latest_latlon, 1)) < 0.00001
end)
end
filtered_trail = filter_trail_excluding_latest(packet_data_list, latest_packet)
socket = push_event(socket, "add_historical_packets", %{packets: filtered_trail})
# Store historical packets in assigns for reference
historical_packets_map =
filtered_trail
|> Enum.zip(unique_position_packets)
|> Enum.reduce(%{}, fn {packet_data, packet}, acc ->
Map.put(acc, packet_data["id"], packet)
end)
historical_packets_map = build_historical_packets_map(filtered_trail, unique_position_packets)
assign(socket,
historical_packets: historical_packets_map,
@ -861,6 +774,111 @@ defmodule AprsmeWeb.MapLive.CallsignView do
end
end
defp get_latest_packet_for_callsign(callsign) do
%{callsign: callsign}
|> Packets.get_recent_packets()
|> Enum.filter(&MapHelpers.has_position_data?/1)
|> Enum.sort_by(& &1.received_at, {:desc, DateTime})
|> List.first()
end
defp sort_packets_by_inserted_at(packets) do
Enum.sort_by(
packets,
fn packet ->
case packet.inserted_at do
%NaiveDateTime{} = naive_dt ->
DateTime.from_naive!(naive_dt, "Etc/UTC")
%DateTime{} = dt ->
dt
_other ->
DateTime.utc_now()
end
end,
{:desc, DateTime}
)
end
defp build_historical_packet_data_list(unique_position_packets, callsign) do
unique_position_packets
|> Enum.with_index()
|> Enum.map(fn {packet, index} ->
build_single_historical_packet_data(packet, index, callsign)
end)
|> Enum.filter(& &1)
end
defp build_single_historical_packet_data(packet, index, callsign) do
case PacketUtils.build_packet_data(packet) do
nil ->
nil
packet_data ->
# Generate a unique ID for this historical packet
packet_id =
"hist_#{if Map.has_key?(packet, :id), do: packet.id, else: System.unique_integer([:positive])}_#{index}"
packet_data
|> Map.put("id", packet_id)
|> Map.put("is_historical", true)
|> Map.put("is_most_recent_for_callsign", false)
|> Map.put("callsign_group", callsign)
|> Map.put("timestamp", get_packet_timestamp(packet))
end
end
defp get_packet_timestamp(packet) do
case packet.inserted_at do
%NaiveDateTime{} = naive_dt ->
DateTime.to_unix(DateTime.from_naive!(naive_dt, "Etc/UTC"), :millisecond)
%DateTime{} = dt ->
DateTime.to_unix(dt, :millisecond)
_other ->
DateTime.to_unix(DateTime.utc_now(), :millisecond)
end
end
defp push_latest_marker(socket, latest_packet) do
if latest_packet do
packet_data = PacketUtils.build_packet_data(latest_packet, true)
if packet_data,
do: {push_event(socket, "new_packet", packet_data), true},
else: {socket, false}
else
{socket, false}
end
end
defp filter_trail_excluding_latest(packet_data_list, latest_packet) do
case latest_packet do
nil ->
packet_data_list
_ ->
latest_latlon = latest_packet |> MapHelpers.get_coordinates() |> Tuple.to_list() |> Enum.take(2)
Enum.reject(packet_data_list, fn pd ->
pd_latlon = [pd["lat"], pd["lng"]]
abs(Enum.at(pd_latlon, 0) - Enum.at(latest_latlon, 0)) < 0.00001 and
abs(Enum.at(pd_latlon, 1) - Enum.at(latest_latlon, 1)) < 0.00001
end)
end
end
defp build_historical_packets_map(filtered_trail, unique_position_packets) do
filtered_trail
|> Enum.zip(unique_position_packets)
|> Enum.reduce(%{}, fn {packet_data, packet}, acc ->
Map.put(acc, packet_data["id"], packet)
end)
end
defp fetch_historical_packets_for_callsign(callsign, start_time, end_time, bounds) do
params = %{
callsign: callsign,

View file

@ -1099,28 +1099,29 @@ defmodule AprsmeWeb.MapLive.Index do
defp filter_unique_positions(packets) do
packets
|> Enum.reduce([], fn packet, acc ->
{lat, lon, _} = MapHelpers.get_coordinates(packet)
if lat && lon do
case acc do
[] ->
# First packet, always include
[packet | acc]
[last_packet | _] ->
if position_changed?(packet, last_packet) do
[packet | acc]
else
acc
end
end
else
acc
end
add_if_unique_position(packet, acc)
end)
|> Enum.reverse()
end
defp add_if_unique_position(packet, []), do: if_position_present(packet, [])
defp add_if_unique_position(packet, [last_packet | _] = acc), do: if_position_changed(packet, last_packet, acc)
defp if_position_present(packet, acc) do
{lat, lon, _} = MapHelpers.get_coordinates(packet)
if lat && lon, do: [packet | acc], else: acc
end
defp if_position_changed(packet, last_packet, acc) do
{lat, lon, _} = MapHelpers.get_coordinates(packet)
if lat && lon do
if position_changed?(packet, last_packet), do: [packet | acc], else: acc
else
acc
end
end
# Check if position changed significantly between two packets (more than ~1 meter)
@spec position_changed?(struct(), struct()) :: boolean()
defp position_changed?(packet1, packet2) do

View file

@ -34,19 +34,28 @@ defmodule AprsmeWeb.MapLive.MapHelpers do
lat = Map.get(packet, :lat) || Map.get(packet, "lat")
lon = Map.get(packet, :lon) || Map.get(packet, "lon")
if not is_nil(lat) and not is_nil(lon) do
if has_direct_coordinates?(lat, lon) do
true
else
data_extended = Map.get(packet, :data_extended) || Map.get(packet, "data_extended")
case data_extended do
%MicE{} -> true
%{latitude: lat, longitude: lon} when not is_nil(lat) and not is_nil(lon) -> true
_ -> false
end
has_position_in_data_extended?(packet)
end
end
defp has_direct_coordinates?(lat, lon) when not is_nil(lat) and not is_nil(lon), do: true
defp has_direct_coordinates?(_, _), do: false
defp has_position_in_data_extended?(packet) do
data_extended = Map.get(packet, :data_extended) || Map.get(packet, "data_extended")
has_position_in_data_extended_case?(data_extended)
end
defp has_position_in_data_extended_case?(%MicE{}), do: true
defp has_position_in_data_extended_case?(%{latitude: lat, longitude: lon}) when not is_nil(lat) and not is_nil(lon),
do: true
defp has_position_in_data_extended_case?(_), do: false
@spec within_bounds?(map() | tuple(), map()) :: boolean()
def within_bounds?(packet_or_coords, bounds) do
if is_nil(bounds) do

View file

@ -57,51 +57,55 @@ defmodule AprsmeWeb.PacketsLive.CallsignView do
def handle_info(%{event: "packet", payload: payload}, socket) do
# Handle incoming live packets - only process if they match our callsign
if packet_matches_callsign?(payload, socket.assigns.callsign) do
sanitized_payload = EncodingUtils.sanitize_packet(payload)
device_identifier =
Map.get(sanitized_payload, :device_identifier) ||
Map.get(sanitized_payload, "device_identifier") ||
DeviceParser.extract_device_identifier(sanitized_payload)
canonical_identifier =
if is_binary(device_identifier) do
matched_device = Aprsme.DeviceIdentification.lookup_device_by_identifier(device_identifier)
if matched_device, do: matched_device.identifier, else: device_identifier
else
device_identifier
end
sanitized_payload = Map.put(sanitized_payload, :device_identifier, canonical_identifier)
# Enrich with model/vendor
enriched_payload = enrich_packet_with_device_info(sanitized_payload)
current_live = socket.assigns.live_packets
current_stored = socket.assigns.packets
{updated_stored, updated_live} =
update_packet_lists(current_stored, current_live, enriched_payload)
all_packets = get_all_packets_list(updated_stored, updated_live)
latest_packet = List.first(all_packets)
{symbol_table_id, symbol_code} = extract_symbol_info(latest_packet)
socket =
socket
|> assign(:packets, updated_stored)
|> assign(:live_packets, updated_live)
|> assign(:all_packets, all_packets)
|> assign(:latest_symbol_table_id, symbol_table_id)
|> assign(:latest_symbol_code, symbol_code)
{:noreply, socket}
process_matching_packet(payload, socket)
else
{:noreply, socket}
end
end
@impl true
def handle_info(_message, socket), do: {:noreply, socket}
defp process_matching_packet(payload, socket) do
sanitized_payload = EncodingUtils.sanitize_packet(payload)
enriched_payload = enrich_packet_with_device_identifier(sanitized_payload)
enriched_payload = enrich_packet_with_device_info(enriched_payload)
current_live = socket.assigns.live_packets
current_stored = socket.assigns.packets
{updated_stored, updated_live} = update_packet_lists(current_stored, current_live, enriched_payload)
all_packets = get_all_packets_list(updated_stored, updated_live)
latest_packet = List.first(all_packets)
{symbol_table_id, symbol_code} = extract_symbol_info(latest_packet)
socket =
socket
|> assign(:packets, updated_stored)
|> assign(:live_packets, updated_live)
|> assign(:all_packets, all_packets)
|> assign(:latest_symbol_table_id, symbol_table_id)
|> assign(:latest_symbol_code, symbol_code)
{:noreply, socket}
end
defp enrich_packet_with_device_identifier(sanitized_payload) do
device_identifier =
Map.get(sanitized_payload, :device_identifier) ||
Map.get(sanitized_payload, "device_identifier") ||
DeviceParser.extract_device_identifier(sanitized_payload)
canonical_identifier =
if is_binary(device_identifier) do
matched_device = Aprsme.DeviceIdentification.lookup_device_by_identifier(device_identifier)
if matched_device, do: matched_device.identifier, else: device_identifier
else
device_identifier
end
Map.put(sanitized_payload, :device_identifier, canonical_identifier)
end
# Private helper functions
# Get recent packets for this callsign from the database (all packets, not just position)