From ddf26a99ce80a7aac3520b2b2b0bc067397087a5 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Fri, 20 Feb 2026 13:15:13 -0600 Subject: [PATCH] Strip weather data, RNG, and negative altitudes from packet comments Weather packets had raw data strings (e.g. 007/000g000t054...) stored as the comment instead of the trailing human-readable text. Now iteratively strips fixed-width APRS weather fields in any order, handling both position-included (XXX/XXX) and positionless (cXXXsXXX) formats. Also strips RNG data and fixes altitude regex to handle negative values. Empty comments after cleaning return nil. --- lib/aprsme/packet.ex | 53 +++++++++++++++- test/aprsme/packet_test.exs | 118 ++++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 1 deletion(-) diff --git a/lib/aprsme/packet.ex b/lib/aprsme/packet.ex index 00fa8da..6d177a8 100644 --- a/lib/aprsme/packet.ex +++ b/lib/aprsme/packet.ex @@ -750,15 +750,66 @@ defmodule Aprsme.Packet do defp clean_comment(comment) when is_binary(comment) do comment + |> strip_weather_data() # Remove altitude - |> String.replace(~r/\s*\/A=\d{6}/, "") + |> String.replace(~r/\s*\/A=-?\d{5,6}/, "") # Remove PHG |> String.replace(~r/PHG\d{4}\s*/, "") + # Remove RNG + |> String.replace(~r/RNG\d{4}\s*/, "") |> String.trim() + |> case do + "" -> nil + cleaned -> cleaned + end end defp clean_comment(comment), do: comment + # APRS weather fields: each is a letter prefix + fixed number of digits/spaces + @wx_field_patterns [ + ~r/^g[\d .]{3}/, + ~r/^t[\d .-]{3}/, + ~r/^r[\d .]{3}/, + ~r/^p[\d .]{3}/, + ~r/^P[\d .]{3}/, + ~r/^h[\d .]{2}/, + ~r/^b[\d .]{5}/, + ~r/^s[\d .]{3}/, + ~r/^[Ll][\d .]{3}/ + ] + + # Matches position-included format (XXX/XXX) and positionless format (cXXXsXXX) + @wx_preamble_pattern ~r/^_?(?:[\d .]{3}\/[\d .]{3}|c[\d .]{3}s[\d .]{3})/ + + defp strip_weather_data(comment) do + case Regex.run(@wx_preamble_pattern, comment) do + [match] -> + comment + |> String.slice(String.length(match)..-1//1) + |> strip_wx_fields() + + nil -> + comment + end + end + + defp strip_wx_fields(str) do + case match_wx_field(str) do + nil -> str + match -> str |> String.slice(String.length(match)..-1//1) |> strip_wx_fields() + end + end + + defp match_wx_field(str) do + Enum.find_value(@wx_field_patterns, fn pat -> + case Regex.run(pat, str) do + [match] -> match + nil -> nil + end + end) + end + # Extract altitude from APRS comment field (e.g., "/A=000680" means 680 feet) defp extract_altitude_from_comment(nil), do: nil diff --git a/test/aprsme/packet_test.exs b/test/aprsme/packet_test.exs index 20030b1..91921cf 100644 --- a/test/aprsme/packet_test.exs +++ b/test/aprsme/packet_test.exs @@ -91,4 +91,122 @@ defmodule Aprsme.PacketTest do assert get_in(result, [:data, "telemetry_bits"]) == nil end end + + describe "comment cleaning" do + test "strips weather data from weather packet comments" do + attrs = %{ + sender: "TEST", + data_type: "weather", + data_extended: %{ + comment: "007/000g000t054r000p001P001b10052h90eMB61", + data_type: :weather, + wx: %{temperature: 54, humidity: 90, wind_speed: 0}, + latitude: 36.124, + longitude: -75.723 + } + } + + result = Packet.extract_additional_data(attrs, "test_packet") + assert result[:comment] == "eMB61" + end + + test "strips weather data with luminosity and preserves trailing comment" do + attrs = %{ + sender: "TEST", + data_type: "weather", + data_extended: %{ + comment: "225/004g009t075r000p000h61b10206Plano, TX weather", + data_type: :weather, + wx: %{temperature: 75, humidity: 61, wind_speed: 4}, + latitude: 32.0, + longitude: -96.0 + } + } + + result = Packet.extract_additional_data(attrs, "test_packet") + assert result[:comment] == "Plano, TX weather" + end + + test "strips weather data with snow and luminosity fields" do + attrs = %{ + sender: "TEST", + data_type: "weather", + data_extended: %{ + comment: "082/002g005t074r000p000P000b10099h58L708eMB63", + data_type: :weather, + wx: %{temperature: 74, humidity: 58, wind_speed: 2}, + latitude: 30.0, + longitude: -90.0 + } + } + + result = Packet.extract_additional_data(attrs, "test_packet") + assert result[:comment] == "eMB63" + end + + test "handles weather comment that is all weather data" do + attrs = %{ + sender: "TEST", + data_type: "weather", + data_extended: %{ + comment: "_180/010g015t072r000p000h45b10132s005", + data_type: :weather, + wx: %{temperature: 72, humidity: 45, wind_speed: 10}, + latitude: 32.0, + longitude: -96.0 + } + } + + result = Packet.extract_additional_data(attrs, "test_packet") + assert result[:comment] in [nil, ""] + end + + test "strips positionless weather format (cXXXsXXX)" do + attrs = %{ + sender: "TEST", + data_type: "weather", + data_extended: %{ + comment: "c011s000g000t077r000P000L000h95b10080GW2000C MaxGust:1.1mph", + data_type: :weather, + wx: %{temperature: 77, humidity: 95}, + latitude: 6.0, + longitude: 79.0 + } + } + + result = Packet.extract_additional_data(attrs, "test_packet") + assert result[:comment] == "GW2000C MaxGust:1.1mph" + end + + test "strips RNG data from comments" do + attrs = %{ + sender: "TEST", + data_type: "position", + data_extended: %{ + comment: "RNG0050 2m Voice 145.57500MHz", + latitude: 61.0, + longitude: 14.0, + radiorange: "0050" + } + } + + result = Packet.extract_additional_data(attrs, "test_packet") + assert result[:comment] == "2m Voice 145.57500MHz" + end + + test "does not modify non-weather, non-RNG comments" do + attrs = %{ + sender: "TEST", + data_type: "position", + data_extended: %{ + comment: "70cm Voice (D-Star) 447.12500MHz -3.0000MHz", + latitude: 46.0, + longitude: -94.0 + } + } + + result = Packet.extract_additional_data(attrs, "test_packet") + assert result[:comment] == "70cm Voice (D-Star) 447.12500MHz -3.0000MHz" + end + end end