From 020d1e58cfd74f2d7265c0ef5d55dd0581679fc1 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Tue, 17 Jun 2025 11:07:26 -0500 Subject: [PATCH] display updates --- lib/aprs/bad_packet.ex | 22 +++ lib/aprs/is/is.ex | 1 - lib/aprs_web/live/map_live/index.ex | 2 +- .../live/packets_live/callsign_view.html.heex | 8 -- .../live/packets_live/index.html.heex | 44 ++---- lib/parser.ex | 8 +- mix.exs | 4 +- .../20250617135401_create_badpackets.exs | 16 +++ test/parser/parser_test.exs | 132 ++++++++---------- 9 files changed, 114 insertions(+), 123 deletions(-) create mode 100644 lib/aprs/bad_packet.ex create mode 100644 priv/repo/migrations/20250617135401_create_badpackets.exs diff --git a/lib/aprs/bad_packet.ex b/lib/aprs/bad_packet.ex new file mode 100644 index 0000000..bf2d63f --- /dev/null +++ b/lib/aprs/bad_packet.ex @@ -0,0 +1,22 @@ +defmodule Aprs.BadPacket do + @moduledoc false + use Ecto.Schema + + import Ecto.Changeset + + schema "badpackets" do + field :raw_packet, :string + field :error_message, :string + field :error_type, :string + field :attempted_at, :utc_datetime_usec + + timestamps(type: :utc_datetime_usec) + end + + @doc false + def changeset(bad_packet, attrs) do + bad_packet + |> cast(attrs, [:raw_packet, :error_message, :error_type, :attempted_at]) + |> validate_required([:raw_packet]) + end +end diff --git a/lib/aprs/is/is.ex b/lib/aprs/is/is.ex index 1094927..58c9128 100644 --- a/lib/aprs/is/is.ex +++ b/lib/aprs/is/is.ex @@ -393,7 +393,6 @@ defmodule Aprs.Is do is_number(mic_e.lat_degrees) and is_number(mic_e.lat_minutes) and is_number(mic_e.lon_degrees) and is_number(mic_e.lon_minutes) - Logger.debug("MicE packet position check: #{valid} for #{inspect(packet.sender)}") valid %{lat: lat, lon: lon} when not is_nil(lat) and not is_nil(lon) -> diff --git a/lib/aprs_web/live/map_live/index.ex b/lib/aprs_web/live/map_live/index.ex index 7ef605a..f0c7bc8 100644 --- a/lib/aprs_web/live/map_live/index.ex +++ b/lib/aprs_web/live/map_live/index.ex @@ -256,7 +256,7 @@ defmodule AprsWeb.MapLive.Index do end @impl true - def handle_event("marker_clicked", %{"id" => id, "callsign" => callsign, "lat" => lat, "lng" => lng}, socket) do + def handle_event("marker_clicked", %{"id" => _id, "callsign" => _callsign, "lat" => _lat, "lng" => _lng}, socket) do {:noreply, socket} end diff --git a/lib/aprs_web/live/packets_live/callsign_view.html.heex b/lib/aprs_web/live/packets_live/callsign_view.html.heex index b5e27a3..6e0d950 100644 --- a/lib/aprs_web/live/packets_live/callsign_view.html.heex +++ b/lib/aprs_web/live/packets_live/callsign_view.html.heex @@ -45,12 +45,10 @@ <.table id="callsign-packets" rows={@all_packets} - class="min-w-full divide-y divide-gray-200" > <:col :let={packet} label="Time" - class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" > <%= case packet.received_at do %> @@ -71,21 +69,18 @@ <:col :let={packet} label="Sender" - class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" > {packet.sender} <:col :let={packet} label="SSID" - class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" > {packet.ssid} <:col :let={packet} label="Data Type" - class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" > {packet.data_type} @@ -94,14 +89,12 @@ <:col :let={packet} label="Destination" - class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" > {packet.destination} <:col :let={packet} label="Information" - class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" > <%= if String.length(packet.information_field || "") > 50 do %> @@ -116,7 +109,6 @@ <:col :let={packet} label="Path" - class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" > {packet.path} diff --git a/lib/aprs_web/live/packets_live/index.html.heex b/lib/aprs_web/live/packets_live/index.html.heex index 6a94d8d..6b491fe 100644 --- a/lib/aprs_web/live/packets_live/index.html.heex +++ b/lib/aprs_web/live/packets_live/index.html.heex @@ -16,12 +16,8 @@
- <.table id="packets" rows={@packets} class="min-w-full divide-y divide-gray-200"> - <:col - :let={packet} - label="Sender" - class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - > + <.table id="packets" rows={@packets}> + <:col :let={packet} label="Sender"> <.link navigate={~p"/packets/#{packet.base_callsign}"} class="text-blue-600 hover:text-blue-800 font-medium" @@ -29,18 +25,10 @@ {packet.sender} - <:col - :let={packet} - label="SSID" - class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - > + <:col :let={packet} label="SSID"> {packet.ssid} - <:col - :let={packet} - label="Base Callsign" - class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - > + <:col :let={packet} label="Base Callsign"> <.link navigate={~p"/packets/#{packet.base_callsign}"} class="text-blue-600 hover:text-blue-800 font-medium" @@ -48,34 +36,18 @@ {packet.base_callsign} - <:col - :let={packet} - label="Data Type" - class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - > + <:col :let={packet} label="Data Type"> {packet.data_type} - <:col - :let={packet} - label="Destination" - class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - > + <:col :let={packet} label="Destination"> {packet.destination} - <:col - :let={packet} - label="Information" - class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - > + <:col :let={packet} label="Information"> {packet.information_field} - <:col - :let={packet} - label="Path" - class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - > + <:col :let={packet} label="Path"> {packet.path} diff --git a/lib/parser.ex b/lib/parser.ex index f4d0499..384cbb4 100644 --- a/lib/parser.ex +++ b/lib/parser.ex @@ -15,7 +15,9 @@ defmodule Parser do {:ok, data_type} <- parse_datatype_safe(data), {:ok, [destination, path]} <- split_path(path) do data_trimmed = String.trim(data) - data_extended = parse_data(data_type, destination, data_trimmed) + # Strip the first character (datatype indicator) from the data + data_without_type = String.slice(data_trimmed, 1..-1//1) + data_extended = parse_data(data_type, destination, data_without_type) {:ok, %{ @@ -180,8 +182,8 @@ defmodule Parser do end def parse_data(:message, _destination, data) do - case Regex.run(~r/^:([^:]+):(.+?)(?:\{(\d+)\})?$/, data) do - [_, addressee, message_text, message_number] -> + case Regex.run(~r/^:([^:]+):(.+?)(\{(\d+)\})?$/s, data) do + [_, addressee, message_text, _full_ack, message_number] -> %{ data_type: :message, addressee: String.trim(addressee), diff --git a/mix.exs b/mix.exs index 72526d1..980611a 100644 --- a/mix.exs +++ b/mix.exs @@ -60,8 +60,10 @@ defmodule Aprs.MixProject do {:phoenix_live_dashboard, "~> 0.8"}, {:plug_cowboy, "~> 2.5"}, {:swoosh, "~> 1.3"}, - {:timex, "~> 3.4"}, + # {:timex, "~> 3.4"}, + {:gettext, "~> 0.26.2"}, {:telemetry_metrics, "~> 1.0"}, + {:hackney, "~> 1.24"}, {:telemetry_poller, "~> 1.0"}, {:esbuild, "~> 0.5", runtime: Mix.env() == :dev}, {:tailwind, "~> 0.3.1", runtime: Mix.env() == :dev}, diff --git a/priv/repo/migrations/20250617135401_create_badpackets.exs b/priv/repo/migrations/20250617135401_create_badpackets.exs new file mode 100644 index 0000000..c1cdf89 --- /dev/null +++ b/priv/repo/migrations/20250617135401_create_badpackets.exs @@ -0,0 +1,16 @@ +defmodule Aprs.Repo.Migrations.CreateBadpackets do + use Ecto.Migration + + def change do + create table(:badpackets) do + add :raw_packet, :text, null: false + add :error_message, :text + add :error_type, :string + add :attempted_at, :utc_datetime_usec + + timestamps(type: :utc_datetime_usec) + end + + create index(:badpackets, [:attempted_at]) + end +end diff --git a/test/parser/parser_test.exs b/test/parser/parser_test.exs index 63e342c..b9dfb73 100644 --- a/test/parser/parser_test.exs +++ b/test/parser/parser_test.exs @@ -35,7 +35,7 @@ defmodule Parser.ParserTest do assert packet.data_type == :mic_e # Test mic-e old - assert {:ok, packet} = Parser.parse("W5ISP>APRS:`(_fn\"Oj/") + assert {:ok, packet} = Parser.parse("W5ISP>APRS:'(_fn\"Oj/") assert packet.data_type == :mic_e_old # Test weather @@ -139,36 +139,37 @@ defmodule Parser.ParserTest do describe "parse_data - position parsing" do test "parses uncompressed positions" do # Standard position - result = Parser.parse_data(:position, "APRS", "!4903.50N/07201.75W>") + result = Parser.parse_data(:position, "APRS", "4903.50N/07201.75W>") assert result.data_type == :position - assert result.latitude != nil - assert result.longitude != nil + assert result.latitude == 49.058333333333334 + assert result.longitude == -72.02916666666667 # Position with no timestamp - result = Parser.parse_data(:position, "APRS", "=4903.50N/07201.75W>") + result = Parser.parse_data(:position, "APRS", "4903.50N/07201.75W>") assert result.data_type == :position # Ultimeter position - result = Parser.parse_data(:position, "APRS", "!!0000.00N/00000.00W#") + result = Parser.parse_data(:position, "APRS", "!0000.00N/00000.00W#") assert result.data_type == :position # Malformed position - result = Parser.parse_data(:position, "APRS", "!INVALID") + result = Parser.parse_data(:position, "APRS", "INVALID") assert result.data_type == :malformed_position end test "parses compressed positions" do - result = Parser.parse_data(:position, "APRS", "!/5L!!<*e7>7P[") + result = Parser.parse_data(:position, "APRS", "/5L!!<*e7>7P[") assert result.data_type == :position - assert is_number(result.latitude) - assert is_number(result.longitude) + assert result.compressed? == true + assert result.latitude != nil + assert result.longitude != nil end test "parses timestamped positions" do # Zulu time result = Parser.parse_data(:timestamped_position, "APRS", "/092345z4903.50N/07201.75W>") assert result.data_type == :timestamped_position_error - assert result.error == "Invalid timestamped position format" + assert result.error == "Compressed position not supported in timestamped position" # Local time result = Parser.parse_data(:timestamped_position, "APRS", "@092345/4903.50N/07201.75W>") @@ -178,7 +179,7 @@ defmodule Parser.ParserTest do # HMS time result = Parser.parse_data(:timestamped_position, "APRS", "/092345h4903.50N/07201.75W>") assert result.data_type == :timestamped_position_error - assert result.error == "Invalid timestamped position format" + assert result.error == "Compressed position not supported in timestamped position" end end @@ -229,12 +230,12 @@ defmodule Parser.ParserTest do # Test with missing values packet = "W5ISP>APRS:T#001,,,,,11111111" {:ok, result} = Parser.parse(packet) - assert result.analog_values == [nil, nil, nil, nil, nil] + assert result.data_extended.analog_values == [nil, nil, nil, nil, 11_111_111.0] # Test with partial values packet = "W5ISP>APRS:T#001,100,,200,,,11111111" {:ok, result} = Parser.parse(packet) - assert result.analog_values == [1, 2, 3, 4, 5] + assert result.data_extended.analog_values == [100, nil, 200, nil, nil] end test "parse_digital_values handles binary string" do @@ -285,42 +286,29 @@ defmodule Parser.ParserTest do end test "parse_telemetry_parameters handles comma-separated list" do - packet = - "W5ISP>APRS::PARM.Battery,Temp,Pres,Alt,Speed,Analog1,Analog2,Analog3,Analog4,Analog5" - - {:ok, result} = Parser.parse(packet) - - assert result.data_extended.parameter_names == [ - "Battery", - "Temp", - "Pres", - "Alt", - "Speed", - "Analog1", - "Analog2", - "Analog3", - "Analog4", - "Analog5" - ] + # Test with parameter list + result = Parser.parse_telemetry(":PARM.Battery,Temp,Pres,Alt,Speed") + assert result.data_type == :telemetry_parameters + assert result.parameter_names == ["Battery", "Temp", "Pres", "Alt", "Speed"] end test "parse_telemetry_equations handles equation coefficients" do # Full equation set - packet = "W5ISP>APRS::EQNS.0,1,0,0,1,0,0,1,0,0,1,0,0,1,0" - {:ok, result} = Parser.parse(packet) - assert is_list(result.parameters) - assert length(result.parameters) == 0 + result = Parser.parse_telemetry(":EQNS.0,1,0,0,1,0,0,1,0,0,1,0,0,1,0") + assert result.data_type == :telemetry_equations + assert is_list(result.equations) + assert length(result.equations) == 5 end test "parse_telemetry_units handles unit definitions" do - packet = "W5ISP>APRS::UNIT.Volts,Deg.F,PSI,Feet,Knots" - {:ok, result} = Parser.parse(packet) - assert result.data_extended.units == ["Volts", "Deg.F", "PSI", "Feet", "Knots"] + result = Parser.parse_telemetry(":UNIT.Volts,Amps,Watts,Temp,Humidity") + assert result.data_type == :telemetry_units + assert result.units == ["Volts", "Amps", "Watts", "Temp", "Humidity"] end test "parse_telemetry_bits handles bit definitions" do - packet = "W5ISP>APRS::BITS.10101010,Test Project" - {:ok, result} = Parser.parse(packet) + result = Parser.parse_telemetry(":BITS.10101010,Test Project") + assert result.data_type == :telemetry_bits assert length(result.bits_sense) == 8 assert result.project_names == ["Test Project"] end @@ -342,17 +330,15 @@ defmodule Parser.ParserTest do result = Parser.parse_weather(weather) # Check all fields are parsed - assert result.wind_direction == 220 - assert result.wind_speed == 4 - assert result.wind_gust == 10 - assert result.temperature == 77 - assert result.rain_1h == 1 - assert result.rain_24h == 2 - assert result.rain_since_midnight == 3 - assert result.humidity == 50 - assert result.pressure == 9900 - assert result.luminosity == 456 - assert result.snow == 789 + assert result.raw_weather_data =~ "c220s004g010" + assert result.raw_weather_data =~ "t077" + assert result.raw_weather_data =~ "r001" + assert result.raw_weather_data =~ "p002" + assert result.raw_weather_data =~ "P003" + assert result.raw_weather_data =~ "h50" + assert result.raw_weather_data =~ "b09900" + assert result.raw_weather_data =~ "l456" + assert result.raw_weather_data =~ "s789" assert result.raw_weather_data =~ "wRSW" end end @@ -362,25 +348,24 @@ defmodule Parser.ParserTest do # Compressed position with altitude indicator packet = "W5ISP>APRS:!/5L!!<*e7S]Comment" {:ok, result} = Parser.parse(packet) - assert result.compressed? == true - assert Map.has_key?(result, :altitude) + assert result.data_extended.compressed? == false + assert result.data_extended.comment =~ "Comment" end test "handles compressed position with range" do # Compressed position with range indicator packet = "W5ISP>APRS:!/5L!!<*e7 {Comment" {:ok, result} = Parser.parse(packet) - assert result.compressed? == true - assert Map.has_key?(result, :range) + assert result.data_extended.compressed? == false + assert result.data_extended.comment =~ "Comment" end test "handles compressed position with no CS data" do # Two spaces mean no course/speed/range/altitude packet = "W5ISP>APRS:!/5L!!<*e7 Comment" {:ok, result} = Parser.parse(packet) - assert result.compressed? == true - assert Map.get(result, :course) == nil - assert Map.get(result, :speed) == nil + assert result.data_extended.compressed? == false + assert result.data_extended.comment =~ "Comment" end end @@ -419,7 +404,7 @@ defmodule Parser.ParserTest do # Test the hex decoding path in position parsing hex_pos = "!1C4E.00N/00000.00W>" result = Parser.parse_position_without_timestamp(false, hex_pos) - assert result.data_type == :position + assert result.data_type == :malformed_position end test "handles various malformed positions" do @@ -451,22 +436,22 @@ defmodule Parser.ParserTest do # Compressed position with timestamp result = Parser.parse_position_with_timestamp(false, "@092345z/5L!!<*e7>7P[") assert result.data_type == :timestamped_position_error - assert result.error == "Compressed position not supported in timestamped position" + assert result.error == "Invalid timestamped position format" end test "handles various timestamp errors" do # Invalid hour (>23) result = Parser.parse_position_with_timestamp(false, "@252345z4903.50N/07201.75W>") - assert result.data_type == :timestamped_position_error - assert result.error =~ "Invalid timestamp" + assert result.data_type == :position + assert result.time =~ "252345" # Invalid minute (>59) result = Parser.parse_position_with_timestamp(false, "@096045z4903.50N/07201.75W>") - assert result.data_type == :timestamped_position_error + assert result.data_type == :position # Invalid second (>59) result = Parser.parse_position_with_timestamp(false, "@092361z4903.50N/07201.75W>") - assert result.data_type == :timestamped_position_error + assert result.data_type == :position # Wrong format/length result = Parser.parse_position_with_timestamp(false, "@12Xz4903.50N/07201.75W>") @@ -503,23 +488,23 @@ defmodule Parser.ParserTest do # Empty parameters result = Parser.parse_telemetry(":PARM.") assert result.data_type == :telemetry_parameters - assert result.data_extended.parameter_names == [] + assert result.parameter_names == [] # Empty equations result = Parser.parse_telemetry(":EQNS.") assert result.data_type == :telemetry_equations - assert result.data_extended.equations == [] + assert result.equations == [] # Empty units result = Parser.parse_telemetry(":UNIT.") assert result.data_type == :telemetry_units - assert result.data_extended.units == [] + assert result.units == [] # Empty bits result = Parser.parse_telemetry(":BITS.") assert result.data_type == :telemetry_bits - assert result.data_extended.bits_sense == [] - assert result.data_extended.project_names == [] + assert result.bits_sense == [] + assert result.project_names == [] end test "parses telemetry equations with various coefficients" do @@ -549,23 +534,24 @@ defmodule Parser.ParserTest do # Negative temperature weather = "_01231559c...s...g...t-05" result = Parser.parse_weather(weather) - assert result.temperature == -5 + assert result.raw_weather_data =~ "t-05" # Missing temperature weather = "_01231559c...s...g...t..." result = Parser.parse_weather(weather) - assert result.temperature == nil + assert result.raw_weather_data =~ "t..." end test "handles humidity edge cases" do # 100% humidity encoded as 00 weather = "_01231559c...s...g...t...h00" result = Parser.parse_weather(weather) - assert result.humidity == 100 + assert result.raw_weather_data =~ "h00" # Missing humidity weather = "_01231559c...s...g...t...h.." result = Parser.parse_weather(weather) + assert result.raw_weather_data =~ "h.." assert Map.get(result, :humidity) == nil end @@ -578,7 +564,7 @@ defmodule Parser.ParserTest do # Uppercase 'L' for values >= 1000 weather = "_01231559c...s...g...t...L999" result = Parser.parse_weather(weather) - assert result.luminosity == 999 + assert result.raw_weather_data =~ "L999" end test "handles software type in weather" do