dont parse device identifiers on each insert

This commit is contained in:
Graham McIntire 2025-07-10 15:16:01 -05:00
parent 38f240b3eb
commit b1ee1f92f9
No known key found for this signature in database
6 changed files with 153 additions and 49 deletions

View file

@ -1319,25 +1319,9 @@ let MapAPRSMap = {
}
// For historical packets that are not the most recent for their callsign,
// still show the proper APRS symbol but with reduced opacity
// show a simple dot instead of the full APRS symbol
if (data.historical && !data.is_most_recent_for_callsign) {
// Use server-generated symbol HTML if available
if (data.symbol_html) {
// Add opacity to the symbol HTML for historical markers
const historicalHtml = data.symbol_html.replace(
/style="([^"]*)"/,
'style="$1 opacity: 0.7;"'
);
return L.divIcon({
html: historicalHtml,
className: "historical-aprs-marker",
iconSize: [120, 32],
iconAnchor: [16, 16],
});
}
// Fallback: red dot for historical positions without symbol data
// Always show a red dot for historical positions
const iconHtml = `<div style="
width: 8px;
height: 8px;
@ -1346,7 +1330,7 @@ let MapAPRSMap = {
border-radius: 50%;
opacity: 0.8;
box-shadow: 0 0 2px rgba(0,0,0,0.3);
" title="Historical position for ${data.callsign} (position changed)"></div>`;
" title="Historical position for ${data.callsign}"></div>`;
return L.divIcon({
html: iconHtml,

View file

@ -31,6 +31,8 @@ defmodule Aprsme.Application do
%{id: :symbol_cache, start: {Cachex, :start_link, [:symbol_cache, [limit: 1_000]]}},
# Start circuit breaker
Aprsme.CircuitBreaker,
# Start device cache manager
Aprsme.DeviceCache,
# Start the Endpoint (http/https)
AprsmeWeb.Endpoint,

130
lib/aprsme/device_cache.ex Normal file
View file

@ -0,0 +1,130 @@
defmodule Aprsme.DeviceCache do
@moduledoc """
Caches device information and provides efficient lookup by identifier with wildcard matching.
"""
use GenServer
alias Aprsme.Devices
alias Aprsme.Repo
@cache_name :device_cache
@refresh_interval to_timeout(day: 1)
# Client API
@doc """
Starts the DeviceCache GenServer.
"""
def start_link(_opts) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@doc """
Looks up a device by identifier using wildcard matching.
Returns the device struct if found, or nil.
"""
def lookup_device(nil), do: nil
def lookup_device(identifier) when is_binary(identifier) do
case Cachex.get(@cache_name, :all_devices) do
{:ok, nil} ->
# Cache miss - load devices
GenServer.call(__MODULE__, :refresh_cache)
lookup_device(identifier)
{:ok, devices} when is_list(devices) ->
find_matching_device(devices, identifier)
_ ->
nil
end
end
@doc """
Refreshes the device cache from the database.
"""
def refresh_cache do
GenServer.call(__MODULE__, :refresh_cache)
end
# Server callbacks
@impl true
def init(_) do
# Load devices on startup
load_devices_into_cache()
# Schedule periodic refresh
Process.send_after(self(), :refresh_cache, @refresh_interval)
{:ok, %{}}
end
@impl true
def handle_call(:refresh_cache, _from, state) do
result = load_devices_into_cache()
{:reply, result, state}
end
@impl true
def handle_info(:refresh_cache, state) do
load_devices_into_cache()
# Schedule next refresh
Process.send_after(self(), :refresh_cache, @refresh_interval)
{:noreply, state}
end
# Private functions
defp load_devices_into_cache do
devices = Repo.all(Devices)
# Store all devices in cache
case Cachex.put(@cache_name, :all_devices, devices) do
{:ok, true} -> :ok
error -> error
end
end
defp find_matching_device(devices, identifier) do
Enum.find(devices, fn device ->
pattern = device.identifier
cond do
String.contains?(pattern, "?") ->
try do
regex = wildcard_pattern_to_regex(pattern)
Regex.match?(regex, identifier)
rescue
_e in Regex.CompileError ->
false
end
String.contains?(pattern, "*") ->
# Compare literally if pattern contains * but not ?
pattern == identifier
true ->
pattern == identifier
end
end)
end
# Converts a pattern with ? wildcards to a regex
defp wildcard_pattern_to_regex(pattern) when is_binary(pattern) do
# Replace ? with a placeholder, escape all regex metacharacters except the placeholder,
# then replace placeholder with .
pattern
|> String.replace("?", "__WILDCARD__")
# Escape all regex metacharacters
|> String.replace(~r/([\\.\+\*\?\[\^\]\$\(\)\{\}=!<>\|:\-])/, "\\\\\\1")
|> String.replace("__WILDCARD__", ".")
|> then(fn s ->
regex = "^" <> s <> "$"
~r/#{regex}/
end)
end
end

View file

@ -209,9 +209,7 @@ defmodule Aprsme.PacketConsumer do
|> normalize_ssid()
|> then(fn attrs ->
device_identifier = Aprsme.DeviceParser.extract_device_identifier(packet_data)
matched_device = Aprsme.DeviceIdentification.lookup_device_by_identifier(device_identifier)
canonical_identifier = if matched_device, do: matched_device.identifier, else: device_identifier
Map.put(attrs, :device_identifier, canonical_identifier)
Map.put(attrs, :device_identifier, device_identifier)
end)
|> sanitize_packet_strings()
|> Map.put(:inserted_at, current_time)

View file

@ -3,6 +3,7 @@ defmodule AprsmeWeb.Api.V1.CallsignJSON do
Renders callsign and packet data for API v1.
"""
alias Aprsme.DeviceCache
alias Aprsme.Packet
def render("show.json", %{packet: packet}) do
@ -97,10 +98,23 @@ defmodule AprsmeWeb.Api.V1.CallsignJSON do
end
defp equipment_json(%Packet{} = packet) do
# Look up device info based on device_identifier
device =
case packet.device_identifier do
nil -> nil
"" -> nil
identifier -> DeviceCache.lookup_device(identifier)
end
equipment_data =
%{}
|> maybe_add(:device_identifier, packet.device_identifier)
|> maybe_add(:manufacturer, packet.manufacturer)
|> maybe_add(:equipment_type, packet.equipment_type)
|> maybe_add(:device_model, device && device.model)
|> maybe_add(:device_vendor, device && device.vendor)
|> maybe_add(:device_contact, device && device.contact)
|> maybe_add(:device_class, device && device.class)
case equipment_data do
empty when empty == %{} -> nil

View file

@ -5,8 +5,7 @@ defmodule AprsmeWeb.Live.SharedPacketHandler do
"""
alias Aprsme.Callsign
alias Aprsme.DeviceIdentification
alias Aprsme.DeviceParser
alias Aprsme.DeviceCache
alias Aprsme.EncodingUtils
@doc """
@ -67,13 +66,13 @@ defmodule AprsmeWeb.Live.SharedPacketHandler do
Enriches packet with device information.
"""
def enrich_with_device_info(packet) do
device_identifier = extract_device_identifier(packet)
device_identifier = Map.get(packet, :device_identifier) || Map.get(packet, "device_identifier")
device =
case device_identifier do
nil -> nil
"" -> nil
identifier -> DeviceIdentification.lookup_device_by_identifier(identifier)
identifier -> DeviceCache.lookup_device(identifier)
end
packet
@ -100,27 +99,4 @@ defmodule AprsmeWeb.Live.SharedPacketHandler do
packet_matches_callsign?(packet, callsign)
end
end
# Private functions
defp extract_device_identifier(packet) do
comment = Map.get(packet, :comment) || Map.get(packet, "comment") || ""
normalized_comment =
case comment do
comment when is_binary(comment) ->
comment
|> String.trim()
|> String.replace(~r/[^\x20-\x7E]+/, "")
|> String.trim()
_ ->
""
end
case DeviceParser.extract_device_identifier(%{comment: normalized_comment}) do
nil -> nil
identifier -> String.trim(identifier)
end
end
end