diff --git a/config/prod.exs b/config/prod.exs index 2542265..120a831 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -13,13 +13,6 @@ config :aprs, AprsWeb.Endpoint, cache_static_manifest: "priv/static/cache_manife # Runtime production configuration, including reading -# Do not print debug messages in production -# of environment variables, is done on config/runtime.exs. -config :logger, level: :info - -# Configures Swoosh API Client -config :swoosh, :api_client, Aprs.Finch - config :esbuild, version: "0.17.11", default: [ @@ -27,3 +20,10 @@ config :esbuild, cd: Path.expand("../assets", __DIR__), env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} ] + +# Do not print debug messages in production +# of environment variables, is done on config/runtime.exs. +config :logger, level: :info + +# Configures Swoosh API Client +config :swoosh, :api_client, Aprs.Finch diff --git a/lib/aprs/device_identification.ex b/lib/aprs/device_identification.ex new file mode 100644 index 0000000..b836625 --- /dev/null +++ b/lib/aprs/device_identification.ex @@ -0,0 +1,80 @@ +defmodule Aprs.DeviceIdentification do + @moduledoc """ + Handles APRS device identification based on the APRS device identification database. + """ + + @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. + """ + @spec identify_device(String.t()) :: String.t() + def identify_device(symbols) do + case symbols do + # Original MIC-E devices + " " <> <<0, 0>> -> "Original MIC-E" + # Kenwood devices + ">" <> <<0>> <> "^" -> "Kenwood TH-D74" + ">" <> <<0, 0>> -> "Kenwood TH-D74A" + "]" <> <<0>> <> "=" -> "Kenwood DM-710" + "]" <> <<0, 0>> -> "Kenwood DM-700" + # Yaesu devices + "`_" <> " " -> "Yaesu VX-8" + "`_" <> "\"" -> "Yaesu FTM-350" + "`_" <> "#" -> "Yaesu VX-8G" + "`_" <> "$" -> "Yaesu FT1D" + "`_" <> "%" -> "Yaesu FTM-400DR" + "`_" <> ")" -> "Yaesu FTM-100D" + "`_" <> "(" -> "Yaesu FT2D" + # Other devices + "` X" -> "AP510" + "`" <> <<0, 0>> -> "Mic-Emsg" + "'|3" -> "Byonics TinyTrack3" + "'|4" -> "Byonics TinyTrack4" + "':4" -> "SCS GmbH & Co. P4dragon DR-7400 modems" + "':8" -> "SCS GmbH & Co. P4dragon DR-7800 modems" + "'" <> <<0, 0>> -> "McTrackr" + <<0>> <> "\"" <> <<0>> -> "Hamhud" + <<0>> <> "/" <> <<0>> -> "Argent" + <<0>> <> "^" <> <<0>> -> "HinzTec anyfrog" + <<0>> <> "*" <> <<0>> -> "APOZxx www.KissOZ.dk Tracker. OZ1EKD and OZ7HVO" + <<0>> <> "~" <> <<0>> -> "Other" + # Unknown devices + _ -> "Unknown" + end + end + + @doc """ + Returns a list of all known device manufacturers. + """ + @spec known_manufacturers() :: [String.t()] + def known_manufacturers do + [ + "Original MIC-E", + "Kenwood", + "Yaesu", + "AP510", + "Byonics", + "SCS GmbH & Co.", + "McTrackr", + "Hamhud", + "Argent", + "HinzTec", + "APOZxx", + "Other" + ] + end + + @doc """ + Returns a list of all known device models for a given manufacturer. + """ + @spec known_models(String.t()) :: [String.t()] + def known_models(manufacturer) do + case manufacturer do + "Kenwood" -> ["TH-D74", "TH-D74A", "DM-710", "DM-700"] + "Yaesu" -> ["VX-8", "FTM-350", "VX-8G", "FT1D", "FTM-400DR", "FTM-100D", "FT2D"] + "Byonics" -> ["TinyTrack3", "TinyTrack4"] + "SCS GmbH & Co." -> ["P4dragon DR-7400 modems", "P4dragon DR-7800 modems"] + _ -> [] + end + end +end diff --git a/lib/aprs_web/components/layouts/root.html.heex b/lib/aprs_web/components/layouts/root.html.heex index 08e8352..45dac42 100644 --- a/lib/aprs_web/components/layouts/root.html.heex +++ b/lib/aprs_web/components/layouts/root.html.heex @@ -9,8 +9,16 @@ - - + + {@inner_content} diff --git a/lib/parser.ex b/lib/parser.ex index 042209d..3981a30 100644 --- a/lib/parser.ex +++ b/lib/parser.ex @@ -644,31 +644,9 @@ defmodule Parser do end @spec parse_manufacturer(binary()) :: String.t() - def parse_manufacturer(<<" ", _::binary-size(2)>>), do: "Original MIC-E" - def parse_manufacturer(<<">", _, "^">>), do: "Kenwood TH-D74" - def parse_manufacturer(<<">", _::binary-size(2)>>), do: "Kenwood TH-D74A" - def parse_manufacturer(<<"]", _, "=">>), do: "Kenwood DM-710" - def parse_manufacturer(<<"]", _::binary-size(2)>>), do: "Kenwood DM-700" - def parse_manufacturer(<<"`", "_", " ">>), do: "Yaesu VX-8" - def parse_manufacturer(<<"`", "_", "\"">>), do: "Yaesu FTM-350" - def parse_manufacturer(<<"`", "_", "#">>), do: "Yaesu VX-8G" - def parse_manufacturer(<<"`", "_", "$">>), do: "Yaesu FT1D" - def parse_manufacturer(<<"`", "_", "%">>), do: "Yaesu FTM-400DR" - def parse_manufacturer(<<"`", "_", ")">>), do: "Yaesu FTM-100D" - def parse_manufacturer(<<"`", "_", "(">>), do: "Yaesu FT2D" - def parse_manufacturer(<<"`", " ", "X">>), do: "AP510" - def parse_manufacturer(<<"`", _::binary-size(2)>>), do: "Mic-Emsg" - def parse_manufacturer(<<"'", "|", "3">>), do: "Byonics TinyTrack3" - def parse_manufacturer(<<"'", "|", "4">>), do: "Byonics TinyTrack4" - def parse_manufacturer(<<"'", ":", "4">>), do: "SCS GmbH & Co. P4dragon DR-7400 modems" - def parse_manufacturer(<<"'", ":", "8">>), do: "SCS GmbH & Co. P4dragon DR-7800 modems" - def parse_manufacturer(<<"'", _::binary-size(2)>>), do: "McTrackr" - def parse_manufacturer(<<_, "\"", _>>), do: "Hamhud ?" - def parse_manufacturer(<<_, "/", _>>), do: "Argent ?" - def parse_manufacturer(<<_, "^", _>>), do: "HinzTec anyfrog" - def parse_manufacturer(<<_, "*", _>>), do: "APOZxx www.KissOZ.dk Tracker. OZ1EKD and OZ7HVO" - def parse_manufacturer(<<_, "~", _>>), do: "Other" - def parse_manufacturer(_), do: "Unknown" + def parse_manufacturer(symbols) do + Aprs.DeviceIdentification.identify_device(symbols) + end @spec find_matches(Regex.t(), String.t()) :: [String.t()] defp find_matches(regex, text) do diff --git a/test/aprs/device_identification_test.exs b/test/aprs/device_identification_test.exs new file mode 100644 index 0000000..f781b8b --- /dev/null +++ b/test/aprs/device_identification_test.exs @@ -0,0 +1,92 @@ +defmodule Aprs.DeviceIdentificationTest do + use ExUnit.Case + + alias Aprs.DeviceIdentification + + describe "identify_device/1" do + test "identifies Original MIC-E devices" do + assert DeviceIdentification.identify_device(" " <> <<0, 0>>) == "Original MIC-E" + end + + test "identifies Kenwood devices" do + assert DeviceIdentification.identify_device(">" <> <<0>> <> "^") == "Kenwood TH-D74" + assert DeviceIdentification.identify_device(">" <> <<0, 0>>) == "Kenwood TH-D74A" + assert DeviceIdentification.identify_device("]" <> <<0>> <> "=") == "Kenwood DM-710" + assert DeviceIdentification.identify_device("]" <> <<0, 0>>) == "Kenwood DM-700" + end + + test "identifies Yaesu devices" do + assert DeviceIdentification.identify_device("`_" <> " ") == "Yaesu VX-8" + assert DeviceIdentification.identify_device("`_" <> "\"") == "Yaesu FTM-350" + assert DeviceIdentification.identify_device("`_" <> "#") == "Yaesu VX-8G" + assert DeviceIdentification.identify_device("`_" <> "$") == "Yaesu FT1D" + assert DeviceIdentification.identify_device("`_" <> "%") == "Yaesu FTM-400DR" + assert DeviceIdentification.identify_device("`_" <> ")") == "Yaesu FTM-100D" + assert DeviceIdentification.identify_device("`_" <> "(") == "Yaesu FT2D" + end + + test "identifies other devices" do + assert DeviceIdentification.identify_device("` X") == "AP510" + assert DeviceIdentification.identify_device("`" <> <<0, 0>>) == "Mic-Emsg" + assert DeviceIdentification.identify_device("'|3") == "Byonics TinyTrack3" + assert DeviceIdentification.identify_device("'|4") == "Byonics TinyTrack4" + + assert DeviceIdentification.identify_device("':4") == + "SCS GmbH & Co. P4dragon DR-7400 modems" + + assert DeviceIdentification.identify_device("':8") == + "SCS GmbH & Co. P4dragon DR-7800 modems" + + assert DeviceIdentification.identify_device("'" <> <<0, 0>>) == "McTrackr" + assert DeviceIdentification.identify_device(<<0>> <> "\"" <> <<0>>) == "Hamhud" + assert DeviceIdentification.identify_device(<<0>> <> "/" <> <<0>>) == "Argent" + assert DeviceIdentification.identify_device(<<0>> <> "^" <> <<0>>) == "HinzTec anyfrog" + + assert DeviceIdentification.identify_device(<<0>> <> "*" <> <<0>>) == + "APOZxx www.KissOZ.dk Tracker. OZ1EKD and OZ7HVO" + + assert DeviceIdentification.identify_device(<<0>> <> "~" <> <<0>>) == "Other" + end + + test "returns Unknown for unidentified devices" do + assert DeviceIdentification.identify_device(<<0, 0, 0>>) == "Unknown" + end + end + + describe "known_manufacturers/0" do + test "returns list of known manufacturers" do + manufacturers = DeviceIdentification.known_manufacturers() + assert is_list(manufacturers) + assert "Kenwood" in manufacturers + assert "Yaesu" in manufacturers + assert "Byonics" in manufacturers + end + end + + describe "known_models/1" do + test "returns list of known models for Kenwood" do + models = DeviceIdentification.known_models("Kenwood") + assert is_list(models) + assert "TH-D74" in models + assert "TH-D74A" in models + assert "DM-710" in models + assert "DM-700" in models + end + + test "returns list of known models for Yaesu" do + models = DeviceIdentification.known_models("Yaesu") + assert is_list(models) + assert "VX-8" in models + assert "FTM-350" in models + assert "VX-8G" in models + assert "FT1D" in models + assert "FTM-400DR" in models + assert "FTM-100D" in models + assert "FT2D" in models + end + + test "returns empty list for unknown manufacturer" do + assert DeviceIdentification.known_models("Unknown") == [] + end + end +end diff --git a/test/parser/parser_test.exs b/test/parser/parser_test.exs index 15df5e2..7015798 100644 --- a/test/parser/parser_test.exs +++ b/test/parser/parser_test.exs @@ -297,16 +297,34 @@ defmodule Parser.ParserTest do test "handles various timestamp errors" do # Invalid hour (>23) - result = Parser.parse_position_with_timestamp(false, "252345z4903.50N/07201.75W>", :timestamped_position) + result = + Parser.parse_position_with_timestamp( + false, + "252345z4903.50N/07201.75W>", + :timestamped_position + ) + assert result.data_type == :position assert result.time =~ "252345" # Invalid minute (>59) - result = Parser.parse_position_with_timestamp(false, "096045z4903.50N/07201.75W>", :timestamped_position) + result = + Parser.parse_position_with_timestamp( + false, + "096045z4903.50N/07201.75W>", + :timestamped_position + ) + assert result.data_type == :position # Invalid second (>59) - result = Parser.parse_position_with_timestamp(false, "092361z4903.50N/07201.75W>", :timestamped_position) + result = + Parser.parse_position_with_timestamp( + false, + "092361z4903.50N/07201.75W>", + :timestamped_position + ) + assert result.data_type == :position # Wrong format/length @@ -316,7 +334,13 @@ defmodule Parser.ParserTest do test "handles MDHM timestamp format errors" do # Invalid MDHM format - result = Parser.parse_position_with_timestamp(false, "9999999/4903.50N/07201.75W>", :timestamped_position) + result = + Parser.parse_position_with_timestamp( + false, + "9999999/4903.50N/07201.75W>", + :timestamped_position + ) + assert result.data_type == :position end end @@ -475,10 +499,13 @@ defmodule Parser.ParserTest do %{matcher: "':4", result: "SCS GmbH & Co. P4dragon DR-7400 modems"}, %{matcher: "':8", result: "SCS GmbH & Co. P4dragon DR-7800 modems"}, %{matcher: "'" <> <<0, 0>>, result: "McTrackr"}, - %{matcher: <<0>> <> "\"" <> <<0>>, result: "Hamhud ?"}, - %{matcher: <<0>> <> "/" <> <<0>>, result: "Argent ?"}, + %{matcher: <<0>> <> "\"" <> <<0>>, result: "Hamhud"}, + %{matcher: <<0>> <> "/" <> <<0>>, result: "Argent"}, %{matcher: <<0>> <> "^" <> <<0>>, result: "HinzTec anyfrog"}, - %{matcher: <<0>> <> "*" <> <<0>>, result: "APOZxx www.KissOZ.dk Tracker. OZ1EKD and OZ7HVO"}, + %{ + matcher: <<0>> <> "*" <> <<0>>, + result: "APOZxx www.KissOZ.dk Tracker. OZ1EKD and OZ7HVO" + }, %{matcher: <<0>> <> "~" <> <<0>>, result: "Other"}, %{matcher: <<0, 0, 0>>, result: "Unknown"} ],