266 lines
8.5 KiB
Elixir
266 lines
8.5 KiB
Elixir
defmodule Aprs.Packet do
|
|
@moduledoc false
|
|
use Aprs.Schema
|
|
|
|
import Ecto.Changeset
|
|
|
|
alias Aprs.DataExtended
|
|
|
|
schema "packets" do
|
|
field(:base_callsign, :string)
|
|
field(:data_type, :string)
|
|
field(:destination, :string)
|
|
field(:information_field, :string)
|
|
field(:path, :string)
|
|
field(:sender, :string)
|
|
field(:ssid, :string)
|
|
field(:received_at, :utc_datetime_usec)
|
|
field(:region, :string)
|
|
field(:lat, :float)
|
|
field(:lon, :float)
|
|
field(:has_position, :boolean, default: false)
|
|
|
|
# Original raw packet and symbol information
|
|
field(:raw_packet, :string)
|
|
field(:symbol_code, :string)
|
|
field(:symbol_table_id, :string)
|
|
|
|
# Additional packet data
|
|
field(:comment, :string)
|
|
field(:timestamp, :string)
|
|
field(:aprs_messaging, :boolean, default: false)
|
|
|
|
# Weather data
|
|
field(:temperature, :float)
|
|
field(:humidity, :float)
|
|
field(:wind_speed, :float)
|
|
field(:wind_direction, :integer)
|
|
field(:wind_gust, :float)
|
|
field(:pressure, :float)
|
|
field(:rain_1h, :float)
|
|
field(:rain_24h, :float)
|
|
field(:rain_since_midnight, :float)
|
|
|
|
# Equipment/status information
|
|
field(:manufacturer, :string)
|
|
field(:equipment_type, :string)
|
|
field(:course, :integer)
|
|
field(:speed, :float)
|
|
field(:altitude, :float)
|
|
|
|
# Message-specific fields
|
|
field(:addressee, :string)
|
|
field(:message_text, :string)
|
|
field(:message_number, :string)
|
|
|
|
embeds_one(:data_extended, DataExtended)
|
|
|
|
timestamps()
|
|
end
|
|
|
|
@doc false
|
|
def changeset(packet, attrs) do
|
|
# Convert atom data_type to string
|
|
attrs = normalize_data_type(attrs)
|
|
|
|
packet
|
|
|> cast(attrs, [
|
|
:base_callsign,
|
|
:data_type,
|
|
:destination,
|
|
:information_field,
|
|
:path,
|
|
:sender,
|
|
:ssid,
|
|
:received_at,
|
|
:region,
|
|
:lat,
|
|
:lon,
|
|
:has_position,
|
|
:raw_packet,
|
|
:symbol_code,
|
|
:symbol_table_id,
|
|
:comment,
|
|
:timestamp,
|
|
:aprs_messaging,
|
|
:temperature,
|
|
:humidity,
|
|
:wind_speed,
|
|
:wind_direction,
|
|
:wind_gust,
|
|
:pressure,
|
|
:rain_1h,
|
|
:rain_24h,
|
|
:rain_since_midnight,
|
|
:manufacturer,
|
|
:equipment_type,
|
|
:course,
|
|
:speed,
|
|
:altitude,
|
|
:addressee,
|
|
:message_text,
|
|
:message_number
|
|
])
|
|
|> validate_required([
|
|
:base_callsign,
|
|
:data_type,
|
|
:destination,
|
|
:information_field,
|
|
:path,
|
|
:sender,
|
|
:ssid,
|
|
:received_at
|
|
])
|
|
|> maybe_set_has_position()
|
|
end
|
|
|
|
defp maybe_set_has_position(changeset) do
|
|
if (get_field(changeset, :lat) && get_field(changeset, :lon)) ||
|
|
(get_change(changeset, :data_extended) &&
|
|
get_change(changeset, :data_extended).latitude &&
|
|
get_change(changeset, :data_extended).longitude) do
|
|
put_change(changeset, :has_position, true)
|
|
else
|
|
changeset
|
|
end
|
|
end
|
|
|
|
# Convert atom data_type to string for storage
|
|
defp normalize_data_type(%{data_type: data_type} = attrs) when is_atom(data_type) do
|
|
%{attrs | data_type: to_string(data_type)}
|
|
end
|
|
|
|
defp normalize_data_type(%{"data_type" => data_type} = attrs) when is_atom(data_type) do
|
|
%{attrs | "data_type" => to_string(data_type)}
|
|
end
|
|
|
|
# Handle :data_type key access format
|
|
defp normalize_data_type(attrs) when is_map(attrs) do
|
|
if Map.has_key?(attrs, :data_type) and is_atom(attrs.data_type) do
|
|
%{attrs | data_type: to_string(attrs.data_type)}
|
|
else
|
|
attrs
|
|
end
|
|
end
|
|
|
|
defp normalize_data_type(attrs), do: attrs
|
|
|
|
@doc """
|
|
Extracts additional data from the parsed packet's data_extended field
|
|
and merges it with the packet attributes for storage.
|
|
"""
|
|
def extract_additional_data(attrs, raw_packet \\ nil) do
|
|
data_extended = attrs[:data_extended] || attrs["data_extended"] || %{}
|
|
|
|
# Start with the base attributes and add the raw packet
|
|
base_attrs = Map.put(attrs, :raw_packet, raw_packet)
|
|
|
|
# Extract data based on the type of data_extended
|
|
additional_data =
|
|
case data_extended do
|
|
%{} when is_map(data_extended) ->
|
|
extract_from_map(data_extended)
|
|
|
|
%Parser.Types.MicE{} = mic_e ->
|
|
extract_from_mic_e(mic_e)
|
|
|
|
_ ->
|
|
%{}
|
|
end
|
|
|
|
Map.merge(base_attrs, additional_data)
|
|
end
|
|
|
|
# Extract data from standard map-based data_extended
|
|
defp extract_from_map(data_extended) do
|
|
%{}
|
|
|> maybe_put(:symbol_code, data_extended[:symbol_code] || data_extended["symbol_code"])
|
|
|> maybe_put(:symbol_table_id, data_extended[:symbol_table_id] || data_extended["symbol_table_id"])
|
|
|> maybe_put(:comment, data_extended[:comment] || data_extended["comment"])
|
|
|> maybe_put(:timestamp, data_extended[:timestamp] || data_extended["timestamp"])
|
|
|> maybe_put(:aprs_messaging, data_extended[:aprs_messaging?] || data_extended["aprs_messaging?"])
|
|
|> maybe_put(:temperature, data_extended[:temperature] || data_extended["temperature"])
|
|
|> maybe_put(:humidity, data_extended[:humidity] || data_extended["humidity"])
|
|
|> maybe_put(:wind_speed, data_extended[:wind_speed] || data_extended["wind_speed"])
|
|
|> maybe_put(:wind_direction, data_extended[:wind_direction] || data_extended["wind_direction"])
|
|
|> maybe_put(:wind_gust, data_extended[:wind_gust] || data_extended["wind_gust"])
|
|
|> maybe_put(:pressure, data_extended[:pressure] || data_extended["pressure"])
|
|
|> maybe_put(:rain_1h, data_extended[:rain_1h] || data_extended["rain_1h"])
|
|
|> maybe_put(:rain_24h, data_extended[:rain_24h] || data_extended["rain_24h"])
|
|
|> maybe_put(:rain_since_midnight, data_extended[:rain_since_midnight] || data_extended["rain_since_midnight"])
|
|
|> maybe_put(:manufacturer, data_extended[:manufacturer] || data_extended["manufacturer"])
|
|
|> maybe_put(:equipment_type, data_extended[:equipment_type] || data_extended["equipment_type"])
|
|
|> maybe_put(:course, data_extended[:course] || data_extended["course"])
|
|
|> maybe_put(:speed, data_extended[:speed] || data_extended["speed"])
|
|
|> maybe_put(:altitude, data_extended[:altitude] || data_extended["altitude"])
|
|
|> maybe_put(:addressee, data_extended[:addressee] || data_extended["addressee"])
|
|
|> maybe_put(:message_text, data_extended[:message_text] || data_extended["message_text"])
|
|
|> maybe_put(:message_number, data_extended[:message_number] || data_extended["message_number"])
|
|
|> extract_weather_data(data_extended)
|
|
end
|
|
|
|
# Extract data from MicE packets
|
|
defp extract_from_mic_e(mic_e) do
|
|
%{}
|
|
|> maybe_put(:comment, mic_e.message)
|
|
|> maybe_put(:manufacturer, mic_e.manufacturer)
|
|
|> maybe_put(:equipment_type, mic_e.equipment_type)
|
|
|> maybe_put(:course, mic_e.course)
|
|
|> maybe_put(:speed, mic_e.speed)
|
|
|> maybe_put(:symbol_code, mic_e.symbol_code)
|
|
|> maybe_put(:symbol_table_id, mic_e.symbol_table_id)
|
|
end
|
|
|
|
# Extract weather data from various formats
|
|
defp extract_weather_data(attrs, data_extended) do
|
|
# Look for weather report in different possible locations
|
|
weather_data =
|
|
data_extended[:weather] || data_extended["weather"] ||
|
|
data_extended[:weather_report] || data_extended["weather_report"]
|
|
|
|
case weather_data do
|
|
weather when is_binary(weather) ->
|
|
parse_weather_string(attrs, weather)
|
|
|
|
weather when is_map(weather) ->
|
|
Map.merge(attrs, weather)
|
|
|
|
_ ->
|
|
attrs
|
|
end
|
|
end
|
|
|
|
# Parse weather data from string format (basic implementation)
|
|
defp parse_weather_string(attrs, weather_string) do
|
|
# This is a simplified parser - a full implementation would handle
|
|
# the complete APRS weather format specification
|
|
attrs
|
|
|> maybe_extract_weather_field(weather_string, ~r/(\d{3})\/(\d{3})/, [:wind_direction, :wind_speed])
|
|
|> maybe_extract_weather_field(weather_string, ~r/t(\d{3})/, [:temperature])
|
|
|> maybe_extract_weather_field(weather_string, ~r/h(\d{2})/, [:humidity])
|
|
|> maybe_extract_weather_field(weather_string, ~r/b(\d{5})/, [:pressure])
|
|
end
|
|
|
|
# Helper to extract weather fields using regex
|
|
defp maybe_extract_weather_field(attrs, weather_string, regex, keys) do
|
|
case Regex.run(regex, weather_string) do
|
|
[_full | matches] ->
|
|
keys
|
|
|> Enum.zip(matches)
|
|
|> Enum.reduce(attrs, fn {key, value}, acc ->
|
|
case Integer.parse(value) do
|
|
{int_val, _} -> Map.put(acc, key, int_val)
|
|
:error -> acc
|
|
end
|
|
end)
|
|
|
|
_ ->
|
|
attrs
|
|
end
|
|
end
|
|
|
|
# Helper to put a value only if it's not nil
|
|
defp maybe_put(map, _key, nil), do: map
|
|
defp maybe_put(map, _key, ""), do: map
|
|
defp maybe_put(map, key, value), do: Map.put(map, key, value)
|
|
end
|