diff --git a/lib/aprs/device_identification.ex b/lib/aprs/device_identification.ex index 94e3d96..79cca14 100644 --- a/lib/aprs/device_identification.ex +++ b/lib/aprs/device_identification.ex @@ -3,6 +3,11 @@ defmodule Aprs.DeviceIdentification do Handles APRS device identification based on the APRS device identification database. """ + import Ecto.Query + + alias Aprs.Devices + alias Aprs.Repo + @device_patterns [ {~r/^ \x00\x00$/, "Original MIC-E"}, {~r/^>\x00\^$/, "Kenwood TH-D74"}, @@ -33,6 +38,17 @@ defmodule Aprs.DeviceIdentification do @doc """ Identifies the manufacturer and model of an APRS device based on its symbol pattern. Returns a tuple of {manufacturer, model} or "Unknown" if the device cannot be identified. + + ## Examples + + iex> Aprs.DeviceIdentification.identify_device(">" <> <<0>> <> "^") + "Kenwood TH-D74" + + iex> Aprs.DeviceIdentification.identify_device("`_#") + "Yaesu VX-8G" + + iex> Aprs.DeviceIdentification.identify_device("not-a-match") + "Unknown" """ @spec identify_device(String.t()) :: String.t() def identify_device(symbols) do @@ -45,6 +61,14 @@ defmodule Aprs.DeviceIdentification do @doc """ Returns a list of all known device manufacturers. + + ## Examples + + iex> "Kenwood" in Aprs.DeviceIdentification.known_manufacturers() + true + + iex> Enum.member?(Aprs.DeviceIdentification.known_manufacturers(), "AP510") + true """ @spec known_manufacturers() :: [String.t()] def known_manufacturers do @@ -66,6 +90,14 @@ defmodule Aprs.DeviceIdentification do @doc """ Returns a list of all known device models for a given manufacturer. + + ## Examples + + iex> Aprs.DeviceIdentification.known_models("Kenwood") + ["TH-D74", "TH-D74A", "DM-710", "DM-700"] + + iex> Aprs.DeviceIdentification.known_models("Unknown") + [] """ @spec known_models(String.t()) :: [String.t()] def known_models("Kenwood"), do: ["TH-D74", "TH-D74A", "DM-710", "DM-700"] @@ -76,20 +108,19 @@ defmodule Aprs.DeviceIdentification do def known_models("SCS GmbH & Co."), do: ["P4dragon DR-7400 modems", "P4dragon DR-7800 modems"] def known_models(_), do: [] - alias Aprs.{Devices, Repo} - import Ecto.Query - @url "https://aprs-deviceid.aprsfoundation.org/tocalls.dense.json" @week_seconds 7 * 24 * 60 * 60 def maybe_refresh_devices do last = Repo.one(from d in Devices, order_by: [desc: d.updated_at], limit: 1) + last_time = case last && last.updated_at do %NaiveDateTime{} = naive -> DateTime.from_naive!(naive, "Etc/UTC") %DateTime{} = dt -> dt _ -> nil end + if last == nil or (last_time && DateTime.diff(DateTime.utc_now(), last_time) > @week_seconds) do fetch_and_upsert_devices() else @@ -99,10 +130,12 @@ defmodule Aprs.DeviceIdentification do def fetch_and_upsert_devices do req = Finch.build(:get, @url) + case Finch.request(req, Aprs.Finch) do {:ok, %Finch.Response{status: 200, body: body}} -> {:ok, json} = Jason.decode(body) upsert_devices(json) + err -> err end @@ -116,6 +149,7 @@ defmodule Aprs.DeviceIdentification do Repo.transaction(fn -> Repo.delete_all(Devices) + Enum.each([tocalls, mice, micelegacy], fn group -> Enum.each(group, fn {identifier, attrs} -> attrs = @@ -125,10 +159,12 @@ defmodule Aprs.DeviceIdentification do if is_list(f), do: f, else: [f] end) |> Map.put("updated_at", now) + %Devices{} |> Devices.changeset(attrs) |> Repo.insert!() end) end) end) + :ok end @@ -140,12 +176,16 @@ defmodule Aprs.DeviceIdentification do @doc """ Looks up a device by identifier, using ? as a single-character wildcard. Returns the device struct if found, or nil. + + Note: This function requires the devices table to be seeded and a running Repo context. """ def lookup_device_by_identifier(identifier) when is_binary(identifier) do # Fetch all device patterns from DB devices = Repo.all(Devices) + Enum.find(devices, fn device -> pattern = device.identifier + cond do String.contains?(pattern, "?") -> try do @@ -155,9 +195,11 @@ defmodule Aprs.DeviceIdentification do _e in Regex.CompileError -> false end + String.contains?(pattern, "*") -> # Compare literally if pattern contains * but not ? pattern == identifier + true -> pattern == identifier end @@ -177,6 +219,7 @@ defmodule Aprs.DeviceIdentification do end defmodule Aprs.DeviceIdentification.Worker do + @moduledoc false use Oban.Worker, queue: :default, max_attempts: 1 @impl true diff --git a/lib/aprs/devices.ex b/lib/aprs/devices.ex index c2d21f9..d903460 100644 --- a/lib/aprs/devices.ex +++ b/lib/aprs/devices.ex @@ -1,5 +1,7 @@ defmodule Aprs.Devices do + @moduledoc false use Ecto.Schema + import Ecto.Changeset schema "devices" do diff --git a/lib/aprs/packets.ex b/lib/aprs/packets.ex index 41d0369..15f90e8 100644 --- a/lib/aprs/packets.ex +++ b/lib/aprs/packets.ex @@ -28,21 +28,22 @@ defmodule Aprs.Packets do |> normalize_packet_attrs() |> set_received_at() |> patch_lat_lon_from_data_extended() - |> (fn attrs -> + |> then(fn attrs -> {lat, lon} = extract_position(attrs) set_lat_lon(attrs, lat, lon) - end).() + end) |> normalize_ssid() - |> (fn attrs -> + |> then(fn attrs -> device_identifier = Parser.DeviceParser.extract_device_identifier(packet_data) matched_device = Aprs.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) - end).() + end) |> sanitize_packet_strings() + # require Logger # Logger.debug("Sanitized packet_attrs before insert: #{inspect(packet_attrs)}") - packet_attrs = Enum.into(packet_attrs, %{}, fn {k, v} -> {k, sanitize_packet_strings(v)} end) + packet_attrs = Map.new(packet_attrs, fn {k, v} -> {k, sanitize_packet_strings(v)} end) insert_packet(packet_attrs, packet_data) rescue error -> @@ -530,10 +531,12 @@ defmodule Aprs.Packets do defp sanitize_packet_strings(%NaiveDateTime{} = ndt), do: ndt defp sanitize_packet_strings(%_struct{} = struct), do: struct |> Map.from_struct() |> sanitize_packet_strings() defp sanitize_packet_strings(list) when is_list(list), do: Enum.map(list, &sanitize_packet_strings/1) + defp sanitize_packet_strings(binary) when is_binary(binary) do s = Aprs.EncodingUtils.sanitize_string(binary) if is_binary(s), do: s, else: "" end + defp sanitize_packet_strings(other), do: other # Get packets from last hour only - used to initialize the map diff --git a/lib/aprs_web/live/info_live/show.html.heex b/lib/aprs_web/live/info_live/show.html.heex index 687ab4d..972bed6 100644 --- a/lib/aprs_web/live/info_live/show.html.heex +++ b/lib/aprs_web/live/info_live/show.html.heex @@ -43,7 +43,9 @@ alt="APRS symbol" /> <% end %> - <.link navigate={~p"/packets/#{@callsign}"} class="ml-2 text-sm text-blue-600 hover:underline">View packets + <.link navigate={~p"/packets/#{@callsign}"} class="ml-2 text-sm text-blue-600 hover:underline"> + View packets + <%= if @packet do %>