From cd79c3df7e55af0dcefd3565283f7f5a6dfb012e Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Mon, 4 Aug 2025 10:19:34 -0500 Subject: [PATCH] feat: Add database support for APRS items/objects and DAO extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added item_name, object_name fields to store names - Added is_item, is_object boolean flags for filtering - Added dao field to store DAO extension data for extra position precision - Automatic detection during packet processing to populate these fields - Added database indexes for efficient queries - Now preserving itemname as item_name and dao data instead of discarding This enables future features like: - Showing/hiding items and objects separately from stations - Querying for specific objects (events, repeaters, etc.) - Using DAO data for high-precision applications 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CHANGELOG.md | 8 +++ lib/aprsme/packet.ex | 16 ++++- lib/aprsme/packet_consumer.ex | 60 +++++++++++++++++-- ...20250108151500_add_item_and_dao_fields.exs | 22 +++++++ 4 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 priv/repo/migrations/20250108151500_add_item_and_dao_fields.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 113d58e..cfedf01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Database fields for APRS items and objects support: + - `item_name`, `object_name` fields to identify items/objects + - `is_item`, `is_object` boolean flags for filtering + - `dao` field to store DAO (Datum As Offset) extension data for extra position precision +- Automatic detection of items/objects during packet processing +- Database indexes for efficient item/object queries + ### Fixed - Fixed packet storage failure in production due to non-schema fields from updated APRS parser - Remove `type`, `digipeaters`, `daodatumbyte`, `mbits`, `message`, `phg`, `wx`, `resultcode`, `resultmsg` fields before database insertion diff --git a/lib/aprsme/packet.ex b/lib/aprsme/packet.ex index 2ddc797..37f6d28 100644 --- a/lib/aprsme/packet.ex +++ b/lib/aprsme/packet.ex @@ -96,6 +96,15 @@ defmodule Aprsme.Packet do field(:device_identifier, :string) + # APRS Items/Objects support + field(:item_name, :string) + field(:object_name, :string) + field(:is_item, :boolean, default: false) + field(:is_object, :boolean, default: false) + + # DAO (Datum As Offset) extension for extra position precision + field(:dao, :map) + embeds_one(:data_extended, DataExtended) timestamps(type: :utc_datetime) @@ -172,7 +181,12 @@ defmodule Aprsme.Packet do :messaging, :rain_midnight, :has_weather, - :device_identifier + :device_identifier, + :item_name, + :object_name, + :is_item, + :is_object, + :dao ]) |> validate_required([ :base_callsign, diff --git a/lib/aprsme/packet_consumer.ex b/lib/aprsme/packet_consumer.ex index 6583bac..b10f650 100644 --- a/lib/aprsme/packet_consumer.ex +++ b/lib/aprsme/packet_consumer.ex @@ -303,6 +303,9 @@ defmodule Aprsme.PacketConsumer do # Extract additional data from the parsed packet including raw packet attrs = Aprsme.Packet.extract_additional_data(attrs, attrs[:raw_packet] || "") + # Detect and set item/object fields + attrs = detect_item_or_object(attrs) + # Normalize data_type to string if it's an atom attrs = normalize_data_type(attrs) @@ -384,15 +387,11 @@ defmodule Aprsme.PacketConsumer do |> Map.delete("gpsfixstatus") |> Map.delete(:raw_weather_data) |> Map.delete("raw_weather_data") - # Additional fields found in production logs - |> Map.delete(:dao) - |> Map.delete("dao") + # Additional fields that need coordinate name mapping |> Map.delete(:longitude) |> Map.delete("longitude") |> Map.delete(:latitude) |> Map.delete("latitude") - |> Map.delete(:itemname) - |> Map.delete("itemname") end # Helper functions for coordinate validation and point creation @@ -424,6 +423,57 @@ defmodule Aprsme.PacketConsumer do defp valid_packet?(%{sender: sender}) when is_binary(sender) and byte_size(sender) > 0, do: true defp valid_packet?(_), do: false + # Detect if packet is an item or object and set appropriate fields + defp detect_item_or_object(attrs) do + cond do + # Check for itemname field from parser + Map.has_key?(attrs, :itemname) or Map.has_key?(attrs, "itemname") -> + item_name = Map.get(attrs, :itemname) || Map.get(attrs, "itemname") + + attrs + |> Map.put(:item_name, item_name) + |> Map.put(:is_item, true) + |> Map.delete(:itemname) + |> Map.delete("itemname") + + # Check for object data type and extract object name from data_extended + attrs[:data_type] == "object" or attrs["data_type"] == "object" -> + object_name = extract_object_name(attrs) + + attrs + |> Map.put(:object_name, object_name) + |> Map.put(:is_object, true) + + # Check information field for object format (starts with ;) + is_binary(attrs[:information_field]) and String.starts_with?(attrs[:information_field], ";") -> + # Extract object name from information field + object_name = extract_object_name_from_info_field(attrs[:information_field]) + + attrs + |> Map.put(:object_name, object_name) + |> Map.put(:is_object, true) + + true -> + attrs + end + end + + defp extract_object_name(attrs) do + case attrs[:data_extended] do + %{name: name} when is_binary(name) -> name + %{"name" => name} when is_binary(name) -> name + _ -> nil + end + end + + defp extract_object_name_from_info_field(info_field) do + # Object format: ;OBJECTNAM*DDHHMMz... + case Regex.run(~r/^;([A-Z0-9 ]{9})\*/, info_field) do + [_, name] -> String.trim(name) + _ -> nil + end + end + # Convert latitude/longitude to lat/lon if present at top level defp convert_coordinate_field_names(attrs) do attrs diff --git a/priv/repo/migrations/20250108151500_add_item_and_dao_fields.exs b/priv/repo/migrations/20250108151500_add_item_and_dao_fields.exs new file mode 100644 index 0000000..6733ff7 --- /dev/null +++ b/priv/repo/migrations/20250108151500_add_item_and_dao_fields.exs @@ -0,0 +1,22 @@ +defmodule Aprsme.Repo.Migrations.AddItemAndDaoFields do + use Ecto.Migration + + def change do + alter table(:packets) do + # For APRS items/objects + add :item_name, :string + add :object_name, :string + add :is_item, :boolean, default: false + add :is_object, :boolean, default: false + + # DAO extension data for extra position precision + add :dao, :map + end + + # Add indexes for querying items/objects + create index(:packets, [:is_item]) + create index(:packets, [:is_object]) + create index(:packets, [:item_name]) + create index(:packets, [:object_name]) + end +end