refactoring
This commit is contained in:
parent
2d13346f92
commit
a05ec4b32c
33 changed files with 567 additions and 376 deletions
|
|
@ -42,7 +42,13 @@ defmodule Aprs.Accounts do
|
||||||
"""
|
"""
|
||||||
def get_user_by_email_and_password(email, password) when is_binary(email) and is_binary(password) do
|
def get_user_by_email_and_password(email, password) when is_binary(email) and is_binary(password) do
|
||||||
user = Repo.get_by(User, email: email)
|
user = Repo.get_by(User, email: email)
|
||||||
if User.valid_password?(user, password), do: user
|
validate_user_password(user, password)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_user_password(user, password) do
|
||||||
|
if User.valid_password?(user, password) do
|
||||||
|
user
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
@ -257,15 +263,16 @@ defmodule Aprs.Accounts do
|
||||||
{:error, :already_confirmed}
|
{:error, :already_confirmed}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def deliver_user_confirmation_instructions(%User{} = user, confirmation_url_fun)
|
def deliver_user_confirmation_instructions(%User{confirmed_at: nil} = user, confirmation_url_fun)
|
||||||
when is_function(confirmation_url_fun, 1) do
|
when is_function(confirmation_url_fun, 1) do
|
||||||
if user.confirmed_at do
|
{encoded_token, user_token} = UserToken.build_email_token(user, "confirm")
|
||||||
{:error, :already_confirmed}
|
Repo.insert!(user_token)
|
||||||
else
|
UserNotifier.deliver_confirmation_instructions(user, confirmation_url_fun.(encoded_token))
|
||||||
{encoded_token, user_token} = UserToken.build_email_token(user, "confirm")
|
end
|
||||||
Repo.insert!(user_token)
|
|
||||||
UserNotifier.deliver_confirmation_instructions(user, confirmation_url_fun.(encoded_token))
|
def deliver_user_confirmation_instructions(%User{confirmed_at: confirmed_at} = _user, _confirmation_url_fun)
|
||||||
end
|
when not is_nil(confirmed_at) do
|
||||||
|
{:error, :already_confirmed}
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,11 @@ defmodule Aprs.Accounts.User do
|
||||||
hash_password? = Keyword.get(opts, :hash_password, true)
|
hash_password? = Keyword.get(opts, :hash_password, true)
|
||||||
password = get_change(changeset, :password)
|
password = get_change(changeset, :password)
|
||||||
|
|
||||||
if hash_password? && password && changeset.valid? do
|
do_hash_password(changeset, hash_password?, password)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_hash_password(changeset, true, password) when is_binary(password) do
|
||||||
|
if changeset.valid? do
|
||||||
changeset
|
changeset
|
||||||
# If using Bcrypt, then further validate it is at most 72 bytes long
|
# If using Bcrypt, then further validate it is at most 72 bytes long
|
||||||
|> validate_length(:password, max: 72, count: :bytes)
|
|> validate_length(:password, max: 72, count: :bytes)
|
||||||
|
|
@ -76,16 +80,21 @@ defmodule Aprs.Accounts.User do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp do_hash_password(changeset, _, _), do: changeset
|
||||||
|
|
||||||
defp maybe_validate_unique_email(changeset, opts) do
|
defp maybe_validate_unique_email(changeset, opts) do
|
||||||
if Keyword.get(opts, :validate_email, true) do
|
validate_email? = Keyword.get(opts, :validate_email, true)
|
||||||
changeset
|
do_validate_unique_email(changeset, validate_email?)
|
||||||
|> unsafe_validate_unique(:email, Aprs.Repo)
|
|
||||||
|> unique_constraint(:email)
|
|
||||||
else
|
|
||||||
changeset
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp do_validate_unique_email(changeset, true) do
|
||||||
|
changeset
|
||||||
|
|> unsafe_validate_unique(:email, Aprs.Repo)
|
||||||
|
|> unique_constraint(:email)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_validate_unique_email(changeset, false), do: changeset
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
A user changeset for changing the email.
|
A user changeset for changing the email.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,12 +36,7 @@ defmodule Aprs.Application do
|
||||||
Aprs.PostgresNotifier
|
Aprs.PostgresNotifier
|
||||||
]
|
]
|
||||||
|
|
||||||
children =
|
children = maybe_add_is_supervisor(children, Application.get_env(:aprs, :env))
|
||||||
if Application.get_env(:aprs, :env) in [:prod, :dev] do
|
|
||||||
children ++ [Aprs.Is.IsSupervisor]
|
|
||||||
else
|
|
||||||
children
|
|
||||||
end
|
|
||||||
|
|
||||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||||
# for other strategies and supported options
|
# for other strategies and supported options
|
||||||
|
|
@ -58,17 +53,8 @@ defmodule Aprs.Application do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp migrate do
|
defp migrate do
|
||||||
if Application.get_env(:aprs, :auto_migrate, true) do
|
auto_migrate = Application.get_env(:aprs, :auto_migrate, true)
|
||||||
require Logger
|
do_migrate(auto_migrate)
|
||||||
|
|
||||||
Logger.info("Running database migrations...")
|
|
||||||
Aprs.Release.migrate()
|
|
||||||
Logger.info("Database migrations completed")
|
|
||||||
else
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
Logger.info("Automatic migrations disabled")
|
|
||||||
end
|
|
||||||
rescue
|
rescue
|
||||||
error ->
|
error ->
|
||||||
require Logger
|
require Logger
|
||||||
|
|
@ -77,4 +63,24 @@ defmodule Aprs.Application do
|
||||||
# Don't crash the application, just log the error
|
# Don't crash the application, just log the error
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp maybe_add_is_supervisor(children, env) when env in [:prod, :dev] do
|
||||||
|
children ++ [Aprs.Is.IsSupervisor]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_add_is_supervisor(children, _env), do: children
|
||||||
|
|
||||||
|
defp do_migrate(true) do
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
Logger.info("Running database migrations...")
|
||||||
|
Aprs.Release.migrate()
|
||||||
|
Logger.info("Database migrations completed")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_migrate(false) do
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
Logger.info("Automatic migrations disabled")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,9 @@ defmodule Aprs.DeviceIdentification do
|
||||||
@spec identify_device(String.t()) :: String.t()
|
@spec identify_device(String.t()) :: String.t()
|
||||||
def identify_device(symbols) do
|
def identify_device(symbols) do
|
||||||
Enum.find_value(@device_patterns, "Unknown", fn {regex, name} ->
|
Enum.find_value(@device_patterns, "Unknown", fn {regex, name} ->
|
||||||
if Regex.match?(regex, symbols), do: name
|
if Regex.match?(regex, symbols) do
|
||||||
|
name
|
||||||
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -66,13 +68,11 @@ defmodule Aprs.DeviceIdentification do
|
||||||
Returns a list of all known device models for a given manufacturer.
|
Returns a list of all known device models for a given manufacturer.
|
||||||
"""
|
"""
|
||||||
@spec known_models(String.t()) :: [String.t()]
|
@spec known_models(String.t()) :: [String.t()]
|
||||||
def known_models(manufacturer) do
|
def known_models("Kenwood"), do: ["TH-D74", "TH-D74A", "DM-710", "DM-700"]
|
||||||
case manufacturer do
|
|
||||||
"Kenwood" -> ["TH-D74", "TH-D74A", "DM-710", "DM-700"]
|
def known_models("Yaesu"), do: ["VX-8", "FTM-350", "VX-8G", "FT1D", "FTM-400DR", "FTM-100D", "FT2D"]
|
||||||
"Yaesu" -> ["VX-8", "FTM-350", "VX-8G", "FT1D", "FTM-400DR", "FTM-100D", "FT2D"]
|
|
||||||
"Byonics" -> ["TinyTrack3", "TinyTrack4"]
|
def known_models("Byonics"), do: ["TinyTrack3", "TinyTrack4"]
|
||||||
"SCS GmbH & Co." -> ["P4dragon DR-7400 modems", "P4dragon DR-7800 modems"]
|
def known_models("SCS GmbH & Co."), do: ["P4dragon DR-7400 modems", "P4dragon DR-7800 modems"]
|
||||||
_ -> []
|
def known_models(_), do: []
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -69,43 +69,43 @@ defmodule Aprs.EncodingUtils do
|
||||||
def sanitize_data_extended(data_extended) when is_map(data_extended) do
|
def sanitize_data_extended(data_extended) when is_map(data_extended) do
|
||||||
# Handle generic maps by sanitizing all string values
|
# Handle generic maps by sanitizing all string values
|
||||||
Enum.reduce(data_extended, %{}, fn {key, value}, acc ->
|
Enum.reduce(data_extended, %{}, fn {key, value}, acc ->
|
||||||
sanitized_value =
|
sanitized_value = sanitize_map_value(value)
|
||||||
case value do
|
|
||||||
val when is_binary(val) -> sanitize_string(val)
|
|
||||||
val -> val
|
|
||||||
end
|
|
||||||
|
|
||||||
Map.put(acc, key, sanitized_value)
|
Map.put(acc, key, sanitized_value)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
def sanitize_data_extended(data_extended), do: data_extended
|
def sanitize_data_extended(data_extended), do: data_extended
|
||||||
|
|
||||||
|
defp sanitize_map_value(val) when is_binary(val), do: sanitize_string(val)
|
||||||
|
defp sanitize_map_value(val), do: val
|
||||||
|
|
||||||
# Private helper functions
|
# Private helper functions
|
||||||
|
|
||||||
@spec scrub_problematic_bytes(binary()) :: String.t()
|
@spec scrub_problematic_bytes(binary()) :: String.t()
|
||||||
defp scrub_problematic_bytes(binary) do
|
defp scrub_problematic_bytes(binary) do
|
||||||
# Handle both invalid UTF-8 sequences and problematic control characters
|
# Handle both invalid UTF-8 sequences and problematic control characters
|
||||||
cleaned =
|
cleaned = clean_binary_by_validity(binary, String.valid?(binary))
|
||||||
if String.valid?(binary) do
|
|
||||||
# String is valid UTF-8, just remove control characters
|
|
||||||
binary
|
|
||||||
# Remove null bytes
|
|
||||||
|> String.replace(<<0>>, "")
|
|
||||||
# Remove other control chars
|
|
||||||
|> String.replace(~r/[\x01-\x08\x0B\x0C\x0E-\x1F\x7F]/, "")
|
|
||||||
else
|
|
||||||
# String has invalid UTF-8, filter byte by byte
|
|
||||||
binary
|
|
||||||
|> :binary.bin_to_list()
|
|
||||||
|> Enum.filter(&valid_utf8_byte?/1)
|
|
||||||
|> :binary.list_to_bin()
|
|
||||||
|> ensure_valid_utf8()
|
|
||||||
end
|
|
||||||
|
|
||||||
String.trim(cleaned)
|
String.trim(cleaned)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp clean_binary_by_validity(binary, true) do
|
||||||
|
# String is valid UTF-8, just remove control characters
|
||||||
|
binary
|
||||||
|
# Remove null bytes
|
||||||
|
|> String.replace(<<0>>, "")
|
||||||
|
# Remove other control chars
|
||||||
|
|> String.replace(~r/[\x01-\x08\x0B\x0C\x0E-\x1F\x7F]/, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp clean_binary_by_validity(binary, false) do
|
||||||
|
# String has invalid UTF-8, filter byte by byte
|
||||||
|
binary
|
||||||
|
|> :binary.bin_to_list()
|
||||||
|
|> Enum.filter(&valid_utf8_byte?/1)
|
||||||
|
|> :binary.list_to_bin()
|
||||||
|
|> ensure_valid_utf8()
|
||||||
|
end
|
||||||
|
|
||||||
# Check if byte should be kept (ASCII printable + safe whitespace)
|
# Check if byte should be kept (ASCII printable + safe whitespace)
|
||||||
@spec valid_utf8_byte?(integer()) :: boolean()
|
@spec valid_utf8_byte?(integer()) :: boolean()
|
||||||
defp valid_utf8_byte?(byte) when byte >= 32 and byte <= 126, do: true
|
defp valid_utf8_byte?(byte) when byte >= 32 and byte <= 126, do: true
|
||||||
|
|
@ -115,7 +115,11 @@ defmodule Aprs.EncodingUtils do
|
||||||
# Ensure the final result is valid UTF-8
|
# Ensure the final result is valid UTF-8
|
||||||
@spec ensure_valid_utf8(binary()) :: binary()
|
@spec ensure_valid_utf8(binary()) :: binary()
|
||||||
defp ensure_valid_utf8(binary) do
|
defp ensure_valid_utf8(binary) do
|
||||||
if String.valid?(binary), do: binary, else: try_convert_utf8(binary)
|
if String.valid?(binary) do
|
||||||
|
binary
|
||||||
|
else
|
||||||
|
try_convert_utf8(binary)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp try_convert_utf8(binary) do
|
defp try_convert_utf8(binary) do
|
||||||
|
|
@ -125,13 +129,17 @@ defmodule Aprs.EncodingUtils do
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
# Last resort: keep only ASCII
|
# Last resort: keep only ASCII
|
||||||
binary
|
fallback_to_ascii(binary)
|
||||||
|> :binary.bin_to_list()
|
|
||||||
|> Enum.filter(fn byte -> byte >= 32 and byte <= 126 end)
|
|
||||||
|> :binary.list_to_bin()
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp fallback_to_ascii(binary) do
|
||||||
|
binary
|
||||||
|
|> :binary.bin_to_list()
|
||||||
|
|> Enum.filter(fn byte -> byte >= 32 and byte <= 126 end)
|
||||||
|
|> :binary.list_to_bin()
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Converts a binary to a hex string representation for debugging.
|
Converts a binary to a hex string representation for debugging.
|
||||||
|
|
||||||
|
|
@ -169,29 +177,29 @@ defmodule Aprs.EncodingUtils do
|
||||||
byte_count: byte_count
|
byte_count: byte_count
|
||||||
}
|
}
|
||||||
|
|
||||||
if valid do
|
add_encoding_details(base_info, binary, valid)
|
||||||
Map.put(base_info, :char_count, String.length(binary))
|
end
|
||||||
else
|
|
||||||
# Try to find where the invalid sequence starts
|
defp add_encoding_details(base_info, binary, true) do
|
||||||
invalid_at = find_invalid_byte_position(binary, 0)
|
Map.put(base_info, :char_count, String.length(binary))
|
||||||
Map.put(base_info, :invalid_at, invalid_at)
|
end
|
||||||
end
|
|
||||||
|
defp add_encoding_details(base_info, binary, false) do
|
||||||
|
# Try to find where the invalid sequence starts
|
||||||
|
invalid_at = find_invalid_byte_position(binary, 0)
|
||||||
|
Map.put(base_info, :invalid_at, invalid_at)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec find_invalid_byte_position(binary(), non_neg_integer()) :: non_neg_integer() | nil
|
@spec find_invalid_byte_position(binary(), non_neg_integer()) :: non_neg_integer() | nil
|
||||||
defp find_invalid_byte_position(<<>>, _pos), do: nil
|
defp find_invalid_byte_position(<<>>, _pos), do: nil
|
||||||
|
|
||||||
defp find_invalid_byte_position(binary, pos) do
|
defp find_invalid_byte_position(<<head::binary-size(1), tail::binary>>, pos) do
|
||||||
case binary do
|
if String.valid?(head) do
|
||||||
<<head::binary-size(1), tail::binary>> ->
|
find_invalid_byte_position(tail, pos + 1)
|
||||||
if String.valid?(head) do
|
else
|
||||||
find_invalid_byte_position(tail, pos + 1)
|
pos
|
||||||
else
|
|
||||||
pos
|
|
||||||
end
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
pos
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp find_invalid_byte_position(_, pos), do: pos
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -29,63 +29,73 @@ defmodule Aprs.Is do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init(_opts) do
|
def init(_opts) do
|
||||||
# Prevent APRS-IS connections in test environment
|
env = Application.get_env(:aprs, :env)
|
||||||
if Application.get_env(:aprs, :env) == :test or
|
disable_connection = Application.get_env(:aprs, :disable_aprs_connection, false)
|
||||||
Application.get_env(:aprs, :disable_aprs_connection, false) do
|
|
||||||
Logger.warning("APRS-IS connection disabled in test environment")
|
do_init(env, disable_connection)
|
||||||
{:stop, :test_environment_disabled}
|
end
|
||||||
|
|
||||||
|
defp do_init(:test, _) do
|
||||||
|
Logger.warning("APRS-IS connection disabled in test environment")
|
||||||
|
{:stop, :test_environment_disabled}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_init(_, true) do
|
||||||
|
Logger.warning("APRS-IS connection disabled in test environment")
|
||||||
|
{:stop, :test_environment_disabled}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_init(_env, false) do
|
||||||
|
# Trap exits so we can gracefully shut down
|
||||||
|
Process.flag(:trap_exit, true)
|
||||||
|
|
||||||
|
# Add a small delay to prevent rapid reconnection attempts
|
||||||
|
Process.sleep(2000)
|
||||||
|
|
||||||
|
# Get startup parameters
|
||||||
|
server = Application.get_env(:aprs, :aprs_is_server, ~c"rotate.aprs2.net")
|
||||||
|
port = Application.get_env(:aprs, :aprs_is_port, 14_580)
|
||||||
|
default_filter = Application.get_env(:aprs, :aprs_is_default_filter, "r/33/-96/100")
|
||||||
|
aprs_user_id = Application.get_env(:aprs, :aprs_is_login_id, "W5ISP")
|
||||||
|
aprs_passcode = Application.get_env(:aprs, :aprs_is_password, "-1")
|
||||||
|
|
||||||
|
# Record connection start time
|
||||||
|
connected_at = DateTime.utc_now()
|
||||||
|
|
||||||
|
# Initialize packet statistics
|
||||||
|
packet_stats = %{
|
||||||
|
total_packets: 0,
|
||||||
|
last_packet_at: nil,
|
||||||
|
packets_per_second: 0,
|
||||||
|
last_second_count: 0,
|
||||||
|
last_second_timestamp: System.system_time(:second)
|
||||||
|
}
|
||||||
|
|
||||||
|
with {:ok, socket} <- connect_to_aprs_is(server, port),
|
||||||
|
:ok <- send_login_string(socket, aprs_user_id, aprs_passcode, default_filter) do
|
||||||
|
timer = create_timer(@aprs_timeout)
|
||||||
|
keepalive_timer = create_keepalive_timer(@keepalive_interval)
|
||||||
|
|
||||||
|
{:ok,
|
||||||
|
%{
|
||||||
|
server: server,
|
||||||
|
port: port,
|
||||||
|
socket: socket,
|
||||||
|
timer: timer,
|
||||||
|
keepalive_timer: keepalive_timer,
|
||||||
|
connected_at: connected_at,
|
||||||
|
packet_stats: packet_stats,
|
||||||
|
buffer: "",
|
||||||
|
login_params: %{
|
||||||
|
user_id: aprs_user_id,
|
||||||
|
passcode: aprs_passcode,
|
||||||
|
filter: default_filter
|
||||||
|
}
|
||||||
|
}}
|
||||||
else
|
else
|
||||||
# Trap exits so we can gracefully shut down
|
_ ->
|
||||||
Process.flag(:trap_exit, true)
|
Logger.error("Unable to establish connection or log in to APRS-IS")
|
||||||
|
{:stop, :aprs_connection_failed}
|
||||||
# Add a small delay to prevent rapid reconnection attempts
|
|
||||||
Process.sleep(2000)
|
|
||||||
|
|
||||||
# Get startup parameters
|
|
||||||
server = Application.get_env(:aprs, :aprs_is_server, ~c"rotate.aprs2.net")
|
|
||||||
port = Application.get_env(:aprs, :aprs_is_port, 14_580)
|
|
||||||
default_filter = Application.get_env(:aprs, :aprs_is_default_filter, "r/33/-96/100")
|
|
||||||
aprs_user_id = Application.get_env(:aprs, :aprs_is_login_id, "W5ISP")
|
|
||||||
aprs_passcode = Application.get_env(:aprs, :aprs_is_password, "-1")
|
|
||||||
|
|
||||||
# Record connection start time
|
|
||||||
connected_at = DateTime.utc_now()
|
|
||||||
|
|
||||||
# Initialize packet statistics
|
|
||||||
packet_stats = %{
|
|
||||||
total_packets: 0,
|
|
||||||
last_packet_at: nil,
|
|
||||||
packets_per_second: 0,
|
|
||||||
last_second_count: 0,
|
|
||||||
last_second_timestamp: System.system_time(:second)
|
|
||||||
}
|
|
||||||
|
|
||||||
with {:ok, socket} <- connect_to_aprs_is(server, port),
|
|
||||||
:ok <- send_login_string(socket, aprs_user_id, aprs_passcode, default_filter) do
|
|
||||||
timer = create_timer(@aprs_timeout)
|
|
||||||
keepalive_timer = create_keepalive_timer(@keepalive_interval)
|
|
||||||
|
|
||||||
{:ok,
|
|
||||||
%{
|
|
||||||
server: server,
|
|
||||||
port: port,
|
|
||||||
socket: socket,
|
|
||||||
timer: timer,
|
|
||||||
keepalive_timer: keepalive_timer,
|
|
||||||
connected_at: connected_at,
|
|
||||||
packet_stats: packet_stats,
|
|
||||||
buffer: "",
|
|
||||||
login_params: %{
|
|
||||||
user_id: aprs_user_id,
|
|
||||||
passcode: aprs_passcode,
|
|
||||||
filter: default_filter
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
else
|
|
||||||
_ ->
|
|
||||||
Logger.error("Unable to establish connection or log in to APRS-IS")
|
|
||||||
{:stop, :aprs_connection_failed}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -157,15 +167,26 @@ defmodule Aprs.Is do
|
||||||
{:ok, :ssl.sslsocket()} | {:error, any()}
|
{:ok, :ssl.sslsocket()} | {:error, any()}
|
||||||
defp connect_to_aprs_is(server, port) do
|
defp connect_to_aprs_is(server, port) do
|
||||||
# Additional safeguard: prevent connections in test environment
|
# Additional safeguard: prevent connections in test environment
|
||||||
if Application.get_env(:aprs, :env) == :test or
|
env = Application.get_env(:aprs, :env)
|
||||||
Application.get_env(:aprs, :disable_aprs_connection, false) do
|
disable_connection = Application.get_env(:aprs, :disable_aprs_connection, false)
|
||||||
Logger.warning("Attempted APRS-IS connection blocked in test environment")
|
|
||||||
{:error, :test_environment_blocked}
|
do_connect_to_aprs_is(server, port, env, disable_connection)
|
||||||
else
|
end
|
||||||
Logger.debug("Connecting to: #{server}:#{port}")
|
|
||||||
opts = [:binary, active: true]
|
defp do_connect_to_aprs_is(_server, _port, :test, _) do
|
||||||
:gen_tcp.connect(String.to_charlist(server), port, opts)
|
Logger.warning("Attempted APRS-IS connection blocked in test environment")
|
||||||
end
|
{:error, :test_environment_blocked}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_connect_to_aprs_is(_server, _port, _, true) do
|
||||||
|
Logger.warning("Attempted APRS-IS connection blocked in test environment")
|
||||||
|
{:error, :test_environment_blocked}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_connect_to_aprs_is(server, port, _env, false) do
|
||||||
|
Logger.debug("Connecting to: #{server}:#{port}")
|
||||||
|
opts = [:binary, active: true]
|
||||||
|
:gen_tcp.connect(String.to_charlist(server), port, opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec send_login_string(:ssl.sslsocket(), String.t(), String.t(), String.t()) ::
|
@spec send_login_string(:ssl.sslsocket(), String.t(), String.t(), String.t()) ::
|
||||||
|
|
@ -352,8 +373,9 @@ defmodule Aprs.Is do
|
||||||
Logger.info("Terminating APRS-IS connection: #{inspect(reason)}")
|
Logger.info("Terminating APRS-IS connection: #{inspect(reason)}")
|
||||||
|
|
||||||
# Log any remaining buffered data
|
# Log any remaining buffered data
|
||||||
if Map.has_key?(state, :buffer) and state.buffer != "" do
|
case Map.get(state, :buffer, "") do
|
||||||
Logger.warning("Terminating with incomplete packet in buffer: #{inspect(state.buffer)}")
|
"" -> :ok
|
||||||
|
buffer -> Logger.warning("Terminating with incomplete packet in buffer: #{inspect(buffer)}")
|
||||||
end
|
end
|
||||||
|
|
||||||
# Cancel timers
|
# Cancel timers
|
||||||
|
|
@ -361,9 +383,13 @@ defmodule Aprs.Is do
|
||||||
if Map.has_key?(state, :keepalive_timer), do: Process.cancel_timer(state.keepalive_timer)
|
if Map.has_key?(state, :keepalive_timer), do: Process.cancel_timer(state.keepalive_timer)
|
||||||
|
|
||||||
# Close socket
|
# Close socket
|
||||||
if Map.has_key?(state, :socket) do
|
case Map.get(state, :socket) do
|
||||||
Logger.info("Closing socket")
|
nil ->
|
||||||
:gen_tcp.close(state.socket)
|
:ok
|
||||||
|
|
||||||
|
socket ->
|
||||||
|
Logger.info("Closing socket")
|
||||||
|
:gen_tcp.close(socket)
|
||||||
end
|
end
|
||||||
|
|
||||||
:normal
|
:normal
|
||||||
|
|
@ -470,11 +496,11 @@ defmodule Aprs.Is do
|
||||||
new_second_count = stats.last_second_count + 1
|
new_second_count = stats.last_second_count + 1
|
||||||
|
|
||||||
%{
|
%{
|
||||||
stats
|
total_packets: new_total,
|
||||||
| total_packets: new_total,
|
last_packet_at: DateTime.utc_now(),
|
||||||
last_packet_at: DateTime.utc_now(),
|
packets_per_second: new_second_count,
|
||||||
packets_per_second: new_second_count,
|
last_second_count: new_second_count,
|
||||||
last_second_count: new_second_count
|
last_second_timestamp: stats.last_second_timestamp
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -130,69 +130,79 @@ defmodule Aprs.Packet do
|
||||||
lon = get_field(changeset, :lon) || get_change(changeset, :lon)
|
lon = get_field(changeset, :lon) || get_change(changeset, :lon)
|
||||||
|
|
||||||
# Also check data_extended for coordinates
|
# Also check data_extended for coordinates
|
||||||
{lat, lon} =
|
{lat, lon} = extract_coordinates_from_changeset(changeset, {lat, lon})
|
||||||
case {lat, lon} do
|
|
||||||
{nil, nil} ->
|
|
||||||
data_extended = get_change(changeset, :data_extended)
|
|
||||||
|
|
||||||
if data_extended do
|
create_geometry_from_coordinates(changeset, lat, lon)
|
||||||
{data_extended[:latitude], data_extended[:longitude]}
|
end
|
||||||
else
|
|
||||||
{nil, nil}
|
|
||||||
end
|
|
||||||
|
|
||||||
coords ->
|
defp extract_coordinates_from_changeset(changeset, {nil, nil}) do
|
||||||
coords
|
data_extended = get_change(changeset, :data_extended)
|
||||||
end
|
extract_coordinates_from_data_extended(data_extended)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp extract_coordinates_from_changeset(_changeset, coords), do: coords
|
||||||
|
|
||||||
|
defp extract_coordinates_from_data_extended(nil), do: {nil, nil}
|
||||||
|
|
||||||
|
defp extract_coordinates_from_data_extended(data_extended) when is_map(data_extended) do
|
||||||
|
{data_extended[:latitude], data_extended[:longitude]}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp extract_coordinates_from_data_extended(_), do: {nil, nil}
|
||||||
|
|
||||||
|
defp create_geometry_from_coordinates(changeset, lat, lon) do
|
||||||
if valid_coordinates?(lat, lon) do
|
if valid_coordinates?(lat, lon) do
|
||||||
try do
|
create_and_set_location(changeset, lat, lon)
|
||||||
location = create_point(lat, lon)
|
|
||||||
|
|
||||||
if location do
|
|
||||||
put_change(changeset, :location, location)
|
|
||||||
else
|
|
||||||
changeset
|
|
||||||
end
|
|
||||||
rescue
|
|
||||||
error ->
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
Logger.error("Failed to create geometry for lat=#{lat}, lon=#{lon}: #{inspect(error)}")
|
|
||||||
changeset
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
changeset
|
changeset
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp create_and_set_location(changeset, lat, lon) do
|
||||||
|
case create_point(lat, lon) do
|
||||||
|
nil -> changeset
|
||||||
|
location -> put_change(changeset, :location, location)
|
||||||
|
end
|
||||||
|
rescue
|
||||||
|
error ->
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
Logger.error("Failed to create geometry for lat=#{lat}, lon=#{lon}: #{inspect(error)}")
|
||||||
|
changeset
|
||||||
|
end
|
||||||
|
|
||||||
defp maybe_set_has_position(changeset) do
|
defp maybe_set_has_position(changeset) do
|
||||||
location = get_field(changeset, :location) || get_change(changeset, :location)
|
location = get_field(changeset, :location) || get_change(changeset, :location)
|
||||||
|
|
||||||
if location do
|
case location do
|
||||||
|
nil -> check_legacy_coordinates(changeset)
|
||||||
|
_location -> put_change(changeset, :has_position, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp check_legacy_coordinates(changeset) do
|
||||||
|
lat = get_field(changeset, :lat) || get_change(changeset, :lat)
|
||||||
|
lon = get_field(changeset, :lon) || get_change(changeset, :lon)
|
||||||
|
|
||||||
|
if valid_coordinates?(lat, lon) do
|
||||||
put_change(changeset, :has_position, true)
|
put_change(changeset, :has_position, true)
|
||||||
else
|
else
|
||||||
# Check legacy lat/lon fields
|
changeset
|
||||||
lat = get_field(changeset, :lat) || get_change(changeset, :lat)
|
|
||||||
lon = get_field(changeset, :lon) || get_change(changeset, :lon)
|
|
||||||
|
|
||||||
if valid_coordinates?(lat, lon) do
|
|
||||||
put_change(changeset, :has_position, true)
|
|
||||||
else
|
|
||||||
changeset
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp valid_coordinates?(lat, lon) do
|
defp valid_coordinates?(lat, lon) do
|
||||||
lat = if is_struct(lat, Decimal), do: Decimal.to_float(lat), else: lat
|
lat = normalize_coordinate(lat)
|
||||||
lon = if is_struct(lon, Decimal), do: Decimal.to_float(lon), else: lon
|
lon = normalize_coordinate(lon)
|
||||||
|
|
||||||
is_number(lat) && is_number(lon) &&
|
is_number(lat) && is_number(lon) &&
|
||||||
lat >= -90 && lat <= 90 &&
|
lat >= -90 && lat <= 90 &&
|
||||||
lon >= -180 && lon <= 180
|
lon >= -180 && lon <= 180
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp normalize_coordinate(%Decimal{} = decimal), do: Decimal.to_float(decimal)
|
||||||
|
defp normalize_coordinate(coord), do: coord
|
||||||
|
|
||||||
# Convert atom data_type to string for storage
|
# Convert atom data_type to string for storage
|
||||||
defp normalize_data_type(%{data_type: data_type} = attrs) when is_atom(data_type) do
|
defp normalize_data_type(%{data_type: data_type} = attrs) when is_atom(data_type) do
|
||||||
%{attrs | data_type: to_string(data_type)}
|
%{attrs | data_type: to_string(data_type)}
|
||||||
|
|
@ -202,12 +212,13 @@ defmodule Aprs.Packet do
|
||||||
%{attrs | "data_type" => to_string(data_type)}
|
%{attrs | "data_type" => to_string(data_type)}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Handle :data_type key access format
|
|
||||||
defp normalize_data_type(attrs) when is_map(attrs) do
|
defp normalize_data_type(attrs) when is_map(attrs) do
|
||||||
if Map.has_key?(attrs, :data_type) and is_atom(attrs.data_type) do
|
case {Map.has_key?(attrs, :data_type), Map.get(attrs, :data_type)} do
|
||||||
%{attrs | data_type: to_string(attrs.data_type)}
|
{true, data_type} when is_atom(data_type) ->
|
||||||
else
|
%{attrs | data_type: to_string(data_type)}
|
||||||
attrs
|
|
||||||
|
_ ->
|
||||||
|
attrs
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -485,8 +496,8 @@ defmodule Aprs.Packet do
|
||||||
@spec create_point(number() | nil, number() | nil) :: Geo.Point.t() | nil
|
@spec create_point(number() | nil, number() | nil) :: Geo.Point.t() | nil
|
||||||
def create_point(lat, lon)
|
def create_point(lat, lon)
|
||||||
when (is_number(lat) or is_struct(lat, Decimal)) and (is_number(lon) or is_struct(lon, Decimal)) do
|
when (is_number(lat) or is_struct(lat, Decimal)) and (is_number(lon) or is_struct(lon, Decimal)) do
|
||||||
lat = if is_struct(lat, Decimal), do: Decimal.to_float(lat), else: lat
|
lat = normalize_coordinate(lat)
|
||||||
lon = if is_struct(lon, Decimal), do: Decimal.to_float(lon), else: lon
|
lon = normalize_coordinate(lon)
|
||||||
|
|
||||||
if valid_coordinates?(lat, lon) do
|
if valid_coordinates?(lat, lon) do
|
||||||
%Geo.Point{coordinates: {lon, lat}, srid: 4326}
|
%Geo.Point{coordinates: {lon, lat}, srid: 4326}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
defmodule AprsWeb.ErrorHTML do
|
defmodule AprsWeb.ErrorHTML do
|
||||||
|
@moduledoc false
|
||||||
use AprsWeb, :html
|
use AprsWeb, :html
|
||||||
|
|
||||||
# If you want to customize your error pages,
|
# If you want to customize your error pages,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
defmodule AprsWeb.ErrorJSON do
|
defmodule AprsWeb.ErrorJSON do
|
||||||
|
@moduledoc false
|
||||||
# If you want to customize a particular status code,
|
# If you want to customize a particular status code,
|
||||||
# you may add your own clauses, such as:
|
# you may add your own clauses, such as:
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
defmodule AprsWeb.PageController do
|
defmodule AprsWeb.PageController do
|
||||||
|
@moduledoc false
|
||||||
use AprsWeb, :controller
|
use AprsWeb, :controller
|
||||||
|
|
||||||
def home(conn, _params) do
|
def home(conn, _params) do
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
defmodule AprsWeb.PageHTML do
|
defmodule AprsWeb.PageHTML do
|
||||||
|
@moduledoc false
|
||||||
use AprsWeb, :html
|
use AprsWeb, :html
|
||||||
|
|
||||||
embed_templates "page_html/*"
|
embed_templates "page_html/*"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
defmodule AprsWeb.UserSessionController do
|
defmodule AprsWeb.UserSessionController do
|
||||||
|
@moduledoc false
|
||||||
use AprsWeb, :controller
|
use AprsWeb, :controller
|
||||||
|
|
||||||
alias Aprs.Accounts
|
alias Aprs.Accounts
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
defmodule AprsWeb.Endpoint do
|
defmodule AprsWeb.Endpoint do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
use Phoenix.Endpoint, otp_app: :aprs
|
use Phoenix.Endpoint, otp_app: :aprs
|
||||||
|
|
||||||
# The session will be stored in the cookie and signed,
|
# The session will be stored in the cookie and signed,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
defmodule AprsWeb.MapLive.CallsignView do
|
defmodule AprsWeb.MapLive.CallsignView do
|
||||||
|
@moduledoc false
|
||||||
use AprsWeb, :live_view
|
use AprsWeb, :live_view
|
||||||
|
|
||||||
alias Aprs.EncodingUtils
|
alias Aprs.EncodingUtils
|
||||||
|
|
@ -817,28 +818,27 @@ defmodule AprsWeb.MapLive.CallsignView do
|
||||||
packets
|
packets
|
||||||
|> Enum.reduce([], fn packet, acc ->
|
|> Enum.reduce([], fn packet, acc ->
|
||||||
{lat, lng, _} = MapHelpers.get_coordinates(packet)
|
{lat, lng, _} = MapHelpers.get_coordinates(packet)
|
||||||
|
process_packet_position(packet, acc, lat, lng)
|
||||||
if lat && lng do
|
|
||||||
# Check if this position is different from the last position
|
|
||||||
case acc do
|
|
||||||
[] ->
|
|
||||||
# First packet, always include
|
|
||||||
[packet | acc]
|
|
||||||
|
|
||||||
[last_packet | _] ->
|
|
||||||
if position_changed?(packet, last_packet) do
|
|
||||||
[packet | acc]
|
|
||||||
else
|
|
||||||
acc
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
acc
|
|
||||||
end
|
|
||||||
end)
|
end)
|
||||||
|> Enum.reverse()
|
|> Enum.reverse()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp process_packet_position(packet, acc, lat, lng) when not is_nil(lat) and not is_nil(lng) do
|
||||||
|
check_position_uniqueness(packet, acc)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp process_packet_position(_packet, acc, _lat, _lng), do: acc
|
||||||
|
|
||||||
|
defp check_position_uniqueness(packet, []), do: [packet]
|
||||||
|
|
||||||
|
defp check_position_uniqueness(packet, [last_packet | _] = acc) do
|
||||||
|
if position_changed?(packet, last_packet) do
|
||||||
|
[packet | acc]
|
||||||
|
else
|
||||||
|
acc
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Check if position changed significantly between two packets (more than ~1 meter)
|
# Check if position changed significantly between two packets (more than ~1 meter)
|
||||||
@spec position_changed?(struct(), struct()) :: boolean()
|
@spec position_changed?(struct(), struct()) :: boolean()
|
||||||
defp position_changed?(packet1, packet2) do
|
defp position_changed?(packet1, packet2) do
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,6 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
alias AprsWeb.MapLive.PacketUtils
|
alias AprsWeb.MapLive.PacketUtils
|
||||||
alias Phoenix.LiveView.Socket
|
alias Phoenix.LiveView.Socket
|
||||||
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
@default_center %{lat: 39.8283, lng: -98.5795}
|
@default_center %{lat: 39.8283, lng: -98.5795}
|
||||||
@default_zoom 5
|
@default_zoom 5
|
||||||
@finch_name Aprs.Finch
|
@finch_name Aprs.Finch
|
||||||
|
|
@ -143,15 +141,11 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("bounds_changed", %{"bounds" => bounds}, socket) do
|
def handle_event("bounds_changed", %{"bounds" => bounds}, socket) do
|
||||||
Logger.debug("handle_event bounds_changed: #{inspect(bounds)} vs current #{inspect(socket.assigns.map_bounds)}")
|
|
||||||
|
|
||||||
handle_bounds_update(bounds, socket)
|
handle_bounds_update(bounds, socket)
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("update_bounds", %{"bounds" => bounds}, socket) do
|
def handle_event("update_bounds", %{"bounds" => bounds}, socket) do
|
||||||
Logger.debug("handle_event update_bounds: #{inspect(bounds)} vs current #{inspect(socket.assigns.map_bounds)}")
|
|
||||||
|
|
||||||
handle_bounds_update(bounds, socket)
|
handle_bounds_update(bounds, socket)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -304,6 +298,12 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("get_assigns", _params, socket) do
|
||||||
|
send(self(), {:test_assigns, socket.assigns})
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
@spec handle_bounds_update(map(), Socket.t()) :: {:noreply, Socket.t()}
|
@spec handle_bounds_update(map(), Socket.t()) :: {:noreply, Socket.t()}
|
||||||
defp handle_bounds_update(bounds, socket) do
|
defp handle_bounds_update(bounds, socket) do
|
||||||
# Update the map bounds from the client
|
# Update the map bounds from the client
|
||||||
|
|
@ -314,8 +314,6 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
west: bounds["west"]
|
west: bounds["west"]
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.debug("handle_bounds_update: new #{inspect(map_bounds)} vs current #{inspect(socket.assigns.map_bounds)}")
|
|
||||||
|
|
||||||
# Validate bounds to prevent invalid coordinates
|
# Validate bounds to prevent invalid coordinates
|
||||||
if map_bounds.north > 90 or map_bounds.south < -90 or
|
if map_bounds.north > 90 or map_bounds.south < -90 or
|
||||||
map_bounds.north <= map_bounds.south do
|
map_bounds.north <= map_bounds.south do
|
||||||
|
|
@ -340,8 +338,6 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
|
|
||||||
@spec process_bounds_update(map(), Socket.t()) :: Socket.t()
|
@spec process_bounds_update(map(), Socket.t()) :: Socket.t()
|
||||||
defp process_bounds_update(map_bounds, socket) do
|
defp process_bounds_update(map_bounds, socket) do
|
||||||
Logger.debug("process_bounds_update: Loading historical packets for bounds #{inspect(map_bounds)}")
|
|
||||||
|
|
||||||
# Remove out-of-bounds packets and markers immediately
|
# Remove out-of-bounds packets and markers immediately
|
||||||
new_visible_packets =
|
new_visible_packets =
|
||||||
socket.assigns.visible_packets
|
socket.assigns.visible_packets
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
defmodule AprsWeb.MapLive.MapHelpers do
|
defmodule AprsWeb.MapLive.MapHelpers do
|
||||||
@moduledoc """
|
@moduledoc false
|
||||||
Shared helpers for APRS map LiveViews (main map, callsign map, etc).
|
|
||||||
Provides coordinate extraction, position checks, and bounds logic.
|
|
||||||
"""
|
|
||||||
|
|
||||||
alias Parser.Types.MicE
|
alias Parser.Types.MicE
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
defmodule AprsWeb.PacketsLive.CallsignView do
|
defmodule AprsWeb.PacketsLive.CallsignView do
|
||||||
@moduledoc """
|
@moduledoc false
|
||||||
LiveView for displaying packets specific to a single callsign.
|
|
||||||
|
|
||||||
Shows up to 100 packets total (stored + live) for the specified callsign.
|
|
||||||
Includes both stored packets from the database and live incoming packets.
|
|
||||||
"""
|
|
||||||
use AprsWeb, :live_view
|
use AprsWeb, :live_view
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
defmodule AprsWeb.WeatherLive.CallsignView do
|
defmodule AprsWeb.WeatherLive.CallsignView do
|
||||||
|
@moduledoc false
|
||||||
use AprsWeb, :live_view
|
use AprsWeb, :live_view
|
||||||
|
|
||||||
alias Aprs.Packets
|
alias Aprs.Packets
|
||||||
|
|
|
||||||
|
|
@ -10,18 +10,20 @@ defmodule AprsWeb.TimeHelpers do
|
||||||
now = DateTime.utc_now()
|
now = DateTime.utc_now()
|
||||||
diff_seconds = DateTime.diff(now, datetime, :second)
|
diff_seconds = DateTime.diff(now, datetime, :second)
|
||||||
|
|
||||||
cond do
|
format_time_diff(diff_seconds) <> " ago"
|
||||||
diff_seconds < 60 -> "less than a minute"
|
|
||||||
diff_seconds < 120 -> "1 minute"
|
|
||||||
diff_seconds < 3600 -> "#{div(diff_seconds, 60)} minutes"
|
|
||||||
diff_seconds < 7200 -> "1 hour"
|
|
||||||
diff_seconds < 86_400 -> "#{div(diff_seconds, 3600)} hours"
|
|
||||||
diff_seconds < 172_800 -> "1 day"
|
|
||||||
diff_seconds < 2_592_000 -> "#{div(diff_seconds, 86_400)} days"
|
|
||||||
diff_seconds < 5_184_000 -> "1 month"
|
|
||||||
diff_seconds < 31_536_000 -> "#{div(diff_seconds, 2_592_000)} months"
|
|
||||||
diff_seconds < 63_072_000 -> "1 year"
|
|
||||||
true -> "#{div(diff_seconds, 31_536_000)} years"
|
|
||||||
end <> " ago"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp format_time_diff(seconds) when seconds < 60, do: "less than a minute"
|
||||||
|
defp format_time_diff(seconds) when seconds < 120, do: "1 minute"
|
||||||
|
defp format_time_diff(seconds) when seconds < 3600, do: "#{div(seconds, 60)} minutes"
|
||||||
|
defp format_time_diff(seconds) when seconds < 7200, do: "1 hour"
|
||||||
|
defp format_time_diff(seconds) when seconds < 86_400, do: "#{div(seconds, 3600)} hours"
|
||||||
|
defp format_time_diff(seconds) when seconds < 172_800, do: "1 day"
|
||||||
|
defp format_time_diff(seconds) when seconds < 2_592_000, do: "#{div(seconds, 86_400)} days"
|
||||||
|
defp format_time_diff(seconds) when seconds < 5_184_000, do: "1 month"
|
||||||
|
|
||||||
|
defp format_time_diff(seconds) when seconds < 31_536_000, do: "#{div(seconds, 2_592_000)} months"
|
||||||
|
|
||||||
|
defp format_time_diff(seconds) when seconds < 63_072_000, do: "1 year"
|
||||||
|
defp format_time_diff(seconds), do: "#{div(seconds, 31_536_000)} years"
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -41,9 +41,7 @@ defmodule AprsWeb.UserAuth do
|
||||||
put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options)
|
put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_write_remember_me_cookie(conn, _token, _params) do
|
defp maybe_write_remember_me_cookie(conn, _token, _params), do: conn
|
||||||
conn
|
|
||||||
end
|
|
||||||
|
|
||||||
# This function renews the session ID and erases the whole
|
# This function renews the session ID and erases the whole
|
||||||
# session to avoid fixation attacks. If there is any data
|
# session to avoid fixation attacks. If there is any data
|
||||||
|
|
@ -96,16 +94,18 @@ defmodule AprsWeb.UserAuth do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp ensure_user_token(conn) do
|
defp ensure_user_token(conn) do
|
||||||
if token = get_session(conn, :user_token) do
|
case get_session(conn, :user_token) do
|
||||||
{token, conn}
|
nil -> ensure_user_token_from_cookie(conn)
|
||||||
else
|
token -> {token, conn}
|
||||||
conn = fetch_cookies(conn, signed: [@remember_me_cookie])
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if token = conn.cookies[@remember_me_cookie] do
|
defp ensure_user_token_from_cookie(conn) do
|
||||||
{token, put_token_in_session(conn, token)}
|
conn = fetch_cookies(conn, signed: [@remember_me_cookie])
|
||||||
else
|
|
||||||
{nil, conn}
|
case conn.cookies[@remember_me_cookie] do
|
||||||
end
|
nil -> {nil, conn}
|
||||||
|
token -> {token, put_token_in_session(conn, token)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -151,25 +151,26 @@ defmodule AprsWeb.UserAuth do
|
||||||
def on_mount(:ensure_authenticated, _params, session, socket) do
|
def on_mount(:ensure_authenticated, _params, session, socket) do
|
||||||
socket = mount_current_user(session, socket)
|
socket = mount_current_user(session, socket)
|
||||||
|
|
||||||
if socket.assigns.current_user do
|
case socket.assigns.current_user do
|
||||||
{:cont, socket}
|
nil ->
|
||||||
else
|
socket =
|
||||||
socket =
|
socket
|
||||||
socket
|
|> Phoenix.LiveView.put_flash(:error, "You must log in to access this page.")
|
||||||
|> Phoenix.LiveView.put_flash(:error, "You must log in to access this page.")
|
|> Phoenix.LiveView.redirect(to: ~p"/users/log_in")
|
||||||
|> Phoenix.LiveView.redirect(to: ~p"/users/log_in")
|
|
||||||
|
|
||||||
{:halt, socket}
|
{:halt, socket}
|
||||||
|
|
||||||
|
_user ->
|
||||||
|
{:cont, socket}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def on_mount(:redirect_if_user_is_authenticated, _params, session, socket) do
|
def on_mount(:redirect_if_user_is_authenticated, _params, session, socket) do
|
||||||
socket = mount_current_user(session, socket)
|
socket = mount_current_user(session, socket)
|
||||||
|
|
||||||
if socket.assigns.current_user do
|
case socket.assigns.current_user do
|
||||||
{:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path(socket))}
|
nil -> {:cont, socket}
|
||||||
else
|
_user -> {:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path(socket))}
|
||||||
{:cont, socket}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -189,12 +190,14 @@ defmodule AprsWeb.UserAuth do
|
||||||
Used for routes that require the user to not be authenticated.
|
Used for routes that require the user to not be authenticated.
|
||||||
"""
|
"""
|
||||||
def redirect_if_user_is_authenticated(conn, _opts) do
|
def redirect_if_user_is_authenticated(conn, _opts) do
|
||||||
if conn.assigns[:current_user] do
|
case conn.assigns[:current_user] do
|
||||||
conn
|
nil ->
|
||||||
|> redirect(to: signed_in_path(conn))
|
conn
|
||||||
|> halt()
|
|
||||||
else
|
_user ->
|
||||||
conn
|
conn
|
||||||
|
|> redirect(to: signed_in_path(conn))
|
||||||
|
|> halt()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -205,14 +208,16 @@ defmodule AprsWeb.UserAuth do
|
||||||
they use the application at all, here would be a good place.
|
they use the application at all, here would be a good place.
|
||||||
"""
|
"""
|
||||||
def require_authenticated_user(conn, _opts) do
|
def require_authenticated_user(conn, _opts) do
|
||||||
if conn.assigns[:current_user] do
|
case conn.assigns[:current_user] do
|
||||||
conn
|
nil ->
|
||||||
else
|
conn
|
||||||
conn
|
|> put_flash(:error, "You must log in to access this page.")
|
||||||
|> put_flash(:error, "You must log in to access this page.")
|
|> maybe_store_return_to()
|
||||||
|> maybe_store_return_to()
|
|> redirect(to: ~p"/users/log_in")
|
||||||
|> redirect(to: ~p"/users/log_in")
|
|> halt()
|
||||||
|> halt()
|
|
||||||
|
_user ->
|
||||||
|
conn
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@ defmodule Parser do
|
||||||
alias Aprs.Convert
|
alias Aprs.Convert
|
||||||
alias Parser.MicE
|
alias Parser.MicE
|
||||||
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
# Simple APRS position parsing to replace parse_aprs_position
|
# Simple APRS position parsing to replace parse_aprs_position
|
||||||
defp parse_aprs_position(lat, lon) do
|
defp parse_aprs_position(lat, lon) do
|
||||||
# Regex for latitude: 2 deg, 2+ min, 1 dir (N/S)
|
# Regex for latitude: 2 deg, 2+ min, 1 dir (N/S)
|
||||||
|
|
@ -54,8 +52,7 @@ defmodule Parser do
|
||||||
def parse(message) when is_binary(message) do
|
def parse(message) when is_binary(message) do
|
||||||
do_parse(message)
|
do_parse(message)
|
||||||
rescue
|
rescue
|
||||||
error ->
|
_ ->
|
||||||
Logger.debug("PARSE ERROR: #{inspect(error)} for message: #{message}")
|
|
||||||
{:error, :invalid_packet}
|
{:error, :invalid_packet}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -112,8 +109,7 @@ defmodule Parser do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
rescue
|
rescue
|
||||||
error ->
|
_ ->
|
||||||
Logger.debug("PARSE ERROR: #{inspect(error)} for message: #{message}")
|
|
||||||
{:error, :invalid_packet}
|
{:error, :invalid_packet}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ defmodule Parser.Helpers do
|
||||||
Public helper functions for APRS parsing (NMEA, PHG/DF, compressed position, etc).
|
Public helper functions for APRS parsing (NMEA, PHG/DF, compressed position, etc).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import Decimal, only: [new: 1, add: 2, negate: 1]
|
||||||
|
|
||||||
@type phg_power :: {integer() | nil, String.t()}
|
@type phg_power :: {integer() | nil, String.t()}
|
||||||
@type phg_height :: {integer() | nil, String.t()}
|
@type phg_height :: {integer() | nil, String.t()}
|
||||||
@type phg_gain :: {integer() | nil, String.t()}
|
@type phg_gain :: {integer() | nil, String.t()}
|
||||||
|
|
@ -16,7 +18,7 @@ defmodule Parser.Helpers do
|
||||||
{coord, _} ->
|
{coord, _} ->
|
||||||
coord = coord / 100.0
|
coord = coord / 100.0
|
||||||
coord = apply_nmea_direction(coord, direction)
|
coord = apply_nmea_direction(coord, direction)
|
||||||
if is_tuple(coord), do: coord, else: {:ok, coord}
|
handle_coordinate_result(coord)
|
||||||
|
|
||||||
:error ->
|
:error ->
|
||||||
{:error, "Invalid coordinate value"}
|
{:error, "Invalid coordinate value"}
|
||||||
|
|
@ -26,6 +28,9 @@ defmodule Parser.Helpers do
|
||||||
@spec parse_nmea_coordinate(any(), any()) :: {:error, String.t()}
|
@spec parse_nmea_coordinate(any(), any()) :: {:error, String.t()}
|
||||||
def parse_nmea_coordinate(_, _), do: {:error, "Invalid coordinate format"}
|
def parse_nmea_coordinate(_, _), do: {:error, "Invalid coordinate format"}
|
||||||
|
|
||||||
|
defp handle_coordinate_result(coord) when is_tuple(coord), do: coord
|
||||||
|
defp handle_coordinate_result(coord), do: {:ok, coord}
|
||||||
|
|
||||||
defp apply_nmea_direction(coord, direction) do
|
defp apply_nmea_direction(coord, direction) do
|
||||||
case direction do
|
case direction do
|
||||||
"N" -> coord
|
"N" -> coord
|
||||||
|
|
@ -317,13 +322,16 @@ defmodule Parser.Helpers do
|
||||||
case Regex.run(~r/h(\d{2})/, weather_data) do
|
case Regex.run(~r/h(\d{2})/, weather_data) do
|
||||||
[_, humidity] ->
|
[_, humidity] ->
|
||||||
val = String.to_integer(humidity)
|
val = String.to_integer(humidity)
|
||||||
if val == 0, do: 100, else: val
|
normalize_humidity(val)
|
||||||
|
|
||||||
nil ->
|
nil ->
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp normalize_humidity(0), do: 100
|
||||||
|
defp normalize_humidity(val), do: val
|
||||||
|
|
||||||
@spec parse_pressure(String.t()) :: float() | nil
|
@spec parse_pressure(String.t()) :: float() | nil
|
||||||
def parse_pressure(weather_data) do
|
def parse_pressure(weather_data) do
|
||||||
case Regex.run(~r/b(\d{5})/, weather_data) do
|
case Regex.run(~r/b(\d{5})/, weather_data) do
|
||||||
|
|
@ -349,18 +357,23 @@ defmodule Parser.Helpers do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Ambiguity and utility helpers
|
# Ambiguity and utility helpers
|
||||||
|
@doc false
|
||||||
def count_spaces(str) do
|
def count_spaces(str) do
|
||||||
str
|
str
|
||||||
|> String.graphemes()
|
|> String.graphemes()
|
||||||
|> Enum.count(fn c -> c == " " end)
|
|> Enum.count(fn c -> c == " " end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
def count_leading_braces(packet), do: count_leading_braces(packet, 0)
|
def count_leading_braces(packet), do: count_leading_braces(packet, 0)
|
||||||
|
|
||||||
|
@doc false
|
||||||
def count_leading_braces(<<"}", rest::binary>>, count), do: count_leading_braces(rest, count + 1)
|
def count_leading_braces(<<"}", rest::binary>>, count), do: count_leading_braces(rest, count + 1)
|
||||||
|
|
||||||
|
@doc false
|
||||||
def count_leading_braces(_packet, count), do: count
|
def count_leading_braces(_packet, count), do: count
|
||||||
|
|
||||||
|
@doc false
|
||||||
def calculate_position_ambiguity(latitude, longitude) do
|
def calculate_position_ambiguity(latitude, longitude) do
|
||||||
lat_spaces = count_spaces(latitude)
|
lat_spaces = count_spaces(latitude)
|
||||||
lon_spaces = count_spaces(longitude)
|
lon_spaces = count_spaces(longitude)
|
||||||
|
|
@ -372,6 +385,7 @@ defmodule Parser.Helpers do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
def calculate_compressed_ambiguity(compression_type) do
|
def calculate_compressed_ambiguity(compression_type) do
|
||||||
case compression_type do
|
case compression_type do
|
||||||
" " -> 0
|
" " -> 0
|
||||||
|
|
@ -383,6 +397,7 @@ defmodule Parser.Helpers do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
def find_matches(regex, text) do
|
def find_matches(regex, text) do
|
||||||
case Regex.names(regex) do
|
case Regex.names(regex) do
|
||||||
[] ->
|
[] ->
|
||||||
|
|
@ -397,10 +412,20 @@ defmodule Parser.Helpers do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
def parse_manufacturer(symbols) do
|
def parse_manufacturer(symbols) do
|
||||||
Aprs.DeviceIdentification.identify_device(symbols)
|
Aprs.DeviceIdentification.identify_device(symbols)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
defp apply_latitude_direction(lat_val, "S"), do: negate(lat_val)
|
||||||
|
defp apply_latitude_direction(lat_val, _), do: lat_val
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
defp apply_longitude_direction(lon_val, "W"), do: negate(lon_val)
|
||||||
|
defp apply_longitude_direction(lon_val, _), do: lon_val
|
||||||
|
|
||||||
|
@doc false
|
||||||
def convert_to_base91(<<value::binary-size(4)>>) do
|
def convert_to_base91(<<value::binary-size(4)>>) do
|
||||||
[v1, v2, v3, v4] = to_charlist(value)
|
[v1, v2, v3, v4] = to_charlist(value)
|
||||||
(v1 - 33) * 91 * 91 * 91 + (v2 - 33) * 91 * 91 + (v3 - 33) * 91 + v4
|
(v1 - 33) * 91 * 91 * 91 + (v2 - 33) * 91 * 91 + (v3 - 33) * 91 + v4
|
||||||
|
|
@ -429,14 +454,13 @@ defmodule Parser.Helpers do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
def validate_position_data(latitude, longitude) do
|
def validate_position_data(latitude, longitude) do
|
||||||
import Decimal, only: [new: 1, add: 2, negate: 1]
|
|
||||||
|
|
||||||
lat =
|
lat =
|
||||||
case Regex.run(~r/^(\d{2})(\d{2}\.\d+)([NS])$/, latitude) do
|
case Regex.run(~r/^(\d{2})(\d{2}\.\d+)([NS])$/, latitude) do
|
||||||
[_, degrees, minutes, direction] ->
|
[_, degrees, minutes, direction] ->
|
||||||
lat_val = add(new(degrees), Decimal.div(new(minutes), new("60")))
|
lat_val = add(new(degrees), Decimal.div(new(minutes), new("60")))
|
||||||
if direction == "S", do: negate(lat_val), else: lat_val
|
apply_latitude_direction(lat_val, direction)
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
nil
|
nil
|
||||||
|
|
@ -446,7 +470,7 @@ defmodule Parser.Helpers do
|
||||||
case Regex.run(~r/^(\d{3})(\d{2}\.\d+)([EW])$/, longitude) do
|
case Regex.run(~r/^(\d{3})(\d{2}\.\d+)([EW])$/, longitude) do
|
||||||
[_, degrees, minutes, direction] ->
|
[_, degrees, minutes, direction] ->
|
||||||
lon_val = add(new(degrees), Decimal.div(new(minutes), new("60")))
|
lon_val = add(new(degrees), Decimal.div(new(minutes), new("60")))
|
||||||
if direction == "W", do: negate(lon_val), else: lon_val
|
apply_longitude_direction(lon_val, direction)
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
nil
|
nil
|
||||||
|
|
@ -459,5 +483,6 @@ defmodule Parser.Helpers do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
def validate_timestamp(_time), do: nil
|
def validate_timestamp(_time), do: nil
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,16 @@ defmodule Parser.MicE do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@spec parse(binary(), String.t()) :: map()
|
@spec parse(binary(), String.t()) :: map()
|
||||||
def parse(data, destination \\ nil) do
|
def parse(_data, nil) do
|
||||||
|
%{
|
||||||
|
latitude: nil,
|
||||||
|
longitude: nil,
|
||||||
|
error: "Destination is nil",
|
||||||
|
data_type: :mic_e_error
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse(data, destination) do
|
||||||
with {:ok, dest_info} <- parse_destination(destination),
|
with {:ok, dest_info} <- parse_destination(destination),
|
||||||
{:ok, info_info} <- parse_information(data, dest_info.longitude_offset) do
|
{:ok, info_info} <- parse_information(data, dest_info.longitude_offset) do
|
||||||
lat =
|
lat =
|
||||||
|
|
@ -19,7 +28,7 @@ defmodule Parser.MicE do
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
lat = if dest_info.lat_direction == :south, do: Decimal.negate(lat), else: lat
|
lat = apply_lat_direction(lat, dest_info.lat_direction)
|
||||||
|
|
||||||
lon =
|
lon =
|
||||||
Decimal.add(
|
Decimal.add(
|
||||||
|
|
@ -33,7 +42,7 @@ defmodule Parser.MicE do
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
lon = if dest_info.lon_direction == :west, do: Decimal.negate(lon), else: lon
|
lon = apply_lon_direction(lon, dest_info.lon_direction)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
latitude: lat,
|
latitude: lat,
|
||||||
|
|
@ -212,7 +221,7 @@ defmodule Parser.MicE do
|
||||||
sp = sp_c - 28
|
sp = sp_c - 28
|
||||||
dc = dc_c - 28
|
dc = dc_c - 28
|
||||||
speed = div(sp, 10) * 100 + rem(sp, 10) * 10 + div(dc, 10)
|
speed = div(sp, 10) * 100 + rem(sp, 10) * 10 + div(dc, 10)
|
||||||
speed = if speed >= 800, do: speed - 800, else: speed
|
speed = normalize_speed(speed)
|
||||||
speed * 0.868976
|
speed * 0.868976
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -220,6 +229,18 @@ defmodule Parser.MicE do
|
||||||
dc = dc_c - 28
|
dc = dc_c - 28
|
||||||
se = se_c - 28
|
se = se_c - 28
|
||||||
course = rem(dc, 10) * 100 + se
|
course = rem(dc, 10) * 100 + se
|
||||||
if course >= 400, do: course - 400, else: course
|
normalize_course(course)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp apply_lat_direction(lat, :south), do: Decimal.negate(lat)
|
||||||
|
defp apply_lat_direction(lat, _), do: lat
|
||||||
|
|
||||||
|
defp apply_lon_direction(lon, :west), do: Decimal.negate(lon)
|
||||||
|
defp apply_lon_direction(lon, _), do: lon
|
||||||
|
|
||||||
|
defp normalize_speed(speed) when speed >= 800, do: speed - 800
|
||||||
|
defp normalize_speed(speed), do: speed
|
||||||
|
|
||||||
|
defp normalize_course(course) when course >= 400, do: course - 400
|
||||||
|
defp normalize_course(course), do: course
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -11,20 +11,6 @@ defmodule Parser.Object do
|
||||||
def parse(<<";", object_name::binary-size(9), live_killed::binary-size(1), timestamp::binary-size(7), rest::binary>>) do
|
def parse(<<";", object_name::binary-size(9), live_killed::binary-size(1), timestamp::binary-size(7), rest::binary>>) do
|
||||||
position_data =
|
position_data =
|
||||||
case rest do
|
case rest do
|
||||||
<<latitude::binary-size(8), sym_table_id::binary-size(1), longitude::binary-size(9), symbol_code::binary-size(1),
|
|
||||||
comment::binary>> ->
|
|
||||||
%{latitude: lat, longitude: lon} =
|
|
||||||
Parser.Position.parse_aprs_position(latitude, longitude)
|
|
||||||
|
|
||||||
%{
|
|
||||||
latitude: lat,
|
|
||||||
longitude: lon,
|
|
||||||
symbol_table_id: sym_table_id,
|
|
||||||
symbol_code: symbol_code,
|
|
||||||
comment: comment,
|
|
||||||
position_format: :uncompressed
|
|
||||||
}
|
|
||||||
|
|
||||||
<<"/", latitude_compressed::binary-size(4), longitude_compressed::binary-size(4), symbol_code::binary-size(1),
|
<<"/", latitude_compressed::binary-size(4), longitude_compressed::binary-size(4), symbol_code::binary-size(1),
|
||||||
cs::binary-size(2), compression_type::binary-size(1), comment::binary>> ->
|
cs::binary-size(2), compression_type::binary-size(1), comment::binary>> ->
|
||||||
try do
|
try do
|
||||||
|
|
@ -47,6 +33,20 @@ defmodule Parser.Object do
|
||||||
_ -> %{latitude: nil, longitude: nil, comment: comment, position_format: :compressed}
|
_ -> %{latitude: nil, longitude: nil, comment: comment, position_format: :compressed}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
<<latitude::binary-size(8), sym_table_id::binary-size(1), longitude::binary-size(9), symbol_code::binary-size(1),
|
||||||
|
comment::binary>> ->
|
||||||
|
%{latitude: lat, longitude: lon} =
|
||||||
|
Parser.Position.parse_aprs_position(latitude, longitude)
|
||||||
|
|
||||||
|
%{
|
||||||
|
latitude: lat,
|
||||||
|
longitude: lon,
|
||||||
|
symbol_table_id: sym_table_id,
|
||||||
|
symbol_code: symbol_code,
|
||||||
|
comment: comment,
|
||||||
|
position_format: :uncompressed
|
||||||
|
}
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
%{comment: rest, position_format: :unknown}
|
%{comment: rest, position_format: :unknown}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,10 @@ defmodule Parser.Weather do
|
||||||
result = %{timestamp: timestamp, data_type: :weather, raw_weather_data: weather_data}
|
result = %{timestamp: timestamp, data_type: :weather, raw_weather_data: weather_data}
|
||||||
|
|
||||||
Enum.reduce(weather_values, result, fn {key, value}, acc ->
|
Enum.reduce(weather_values, result, fn {key, value}, acc ->
|
||||||
if is_nil(value), do: acc, else: Map.put(acc, key, value)
|
put_weather_value(acc, key, value)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp put_weather_value(acc, _key, nil), do: acc
|
||||||
|
defp put_weather_value(acc, key, value), do: Map.put(acc, key, value)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ defmodule Aprs.IsTest do
|
||||||
use ExUnit.Case, async: false
|
use ExUnit.Case, async: false
|
||||||
use Aprs.DataCase
|
use Aprs.DataCase
|
||||||
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
describe "APRS-IS mock functionality" do
|
describe "APRS-IS mock functionality" do
|
||||||
setup do
|
setup do
|
||||||
# Start the mock if not already running
|
# Start the mock if not already running
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ defmodule AprsWeb.Integration.AprsStatusTest do
|
||||||
|
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
|
alias AprsWeb.Endpoint
|
||||||
|
alias Ecto.Adapters.SQL
|
||||||
|
|
||||||
describe "APRS status endpoints without external connections" do
|
describe "APRS status endpoints without external connections" do
|
||||||
test "status JSON endpoint returns proper response without APRS connection", %{conn: conn} do
|
test "status JSON endpoint returns proper response without APRS connection", %{conn: conn} do
|
||||||
conn = get(conn, "/status.json")
|
conn = get(conn, "/status.json")
|
||||||
|
|
@ -55,7 +58,7 @@ defmodule AprsWeb.Integration.AprsStatusTest do
|
||||||
case live(conn, "/status") do
|
case live(conn, "/status") do
|
||||||
{:ok, view, html} ->
|
{:ok, view, html} ->
|
||||||
# If status page exists, verify it handles disconnected state
|
# If status page exists, verify it handles disconnected state
|
||||||
assert html =~ "Status"
|
assert html =~ "STATUS"
|
||||||
|
|
||||||
# Should show disconnected state information
|
# Should show disconnected state information
|
||||||
assert has_element?(view, "[data-testid='connection-status']") ||
|
assert has_element?(view, "[data-testid='connection-status']") ||
|
||||||
|
|
@ -128,7 +131,7 @@ defmodule AprsWeb.Integration.AprsStatusTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
# This should not cause any updates since APRS.Is is not running
|
# This should not cause any updates since APRS.Is is not running
|
||||||
AprsWeb.Endpoint.broadcast("aprs_messages", "packet", test_packet)
|
Endpoint.broadcast("aprs_messages", "packet", test_packet)
|
||||||
|
|
||||||
# Give a moment for any potential updates
|
# Give a moment for any potential updates
|
||||||
Process.sleep(100)
|
Process.sleep(100)
|
||||||
|
|
@ -162,7 +165,7 @@ defmodule AprsWeb.Integration.AprsStatusTest do
|
||||||
# Verify that core application functionality works without APRS
|
# Verify that core application functionality works without APRS
|
||||||
|
|
||||||
# Database should be accessible
|
# Database should be accessible
|
||||||
assert Ecto.Adapters.SQL.query!(Aprs.Repo, "SELECT 1", [])
|
assert SQL.query!(Aprs.Repo, "SELECT 1", [])
|
||||||
|
|
||||||
# Web interface should load
|
# Web interface should load
|
||||||
{:ok, _view, html} = live(conn, "/")
|
{:ok, _view, html} = live(conn, "/")
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ defmodule AprsWeb.MapLive.CallsignViewTest do
|
||||||
test "renders callsign view with valid callsign", %{conn: conn} do
|
test "renders callsign view with valid callsign", %{conn: conn} do
|
||||||
{:ok, view, html} = live(conn, "/W5ISP-9")
|
{:ok, view, html} = live(conn, "/W5ISP-9")
|
||||||
|
|
||||||
assert html =~ "📡 W5ISP-9"
|
assert html =~ "W5ISP-9"
|
||||||
assert html =~ "Back to Map"
|
assert html =~ "Back to Map"
|
||||||
assert html =~ "Packets"
|
assert html =~ "Packets"
|
||||||
assert has_element?(view, "#aprs-map")
|
assert has_element?(view, "#aprs-map")
|
||||||
|
|
@ -16,7 +16,7 @@ defmodule AprsWeb.MapLive.CallsignViewTest do
|
||||||
test "normalizes callsign to uppercase", %{conn: conn} do
|
test "normalizes callsign to uppercase", %{conn: conn} do
|
||||||
{:ok, _view, html} = live(conn, "/w5isp-9")
|
{:ok, _view, html} = live(conn, "/w5isp-9")
|
||||||
|
|
||||||
assert html =~ "📡 W5ISP-9"
|
assert html =~ "W5ISP-9"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "shows loading state when no packets found", %{conn: conn} do
|
test "shows loading state when no packets found", %{conn: conn} do
|
||||||
|
|
@ -29,7 +29,7 @@ defmodule AprsWeb.MapLive.CallsignViewTest do
|
||||||
test "handles callsign without SSID", %{conn: conn} do
|
test "handles callsign without SSID", %{conn: conn} do
|
||||||
{:ok, _view, html} = live(conn, "/W5ISP")
|
{:ok, _view, html} = live(conn, "/W5ISP")
|
||||||
|
|
||||||
assert html =~ "📡 W5ISP"
|
assert html =~ "W5ISP"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "sets correct page title", %{conn: conn} do
|
test "sets correct page title", %{conn: conn} do
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ defmodule AprsWeb.UserConfirmationLiveTest do
|
||||||
describe "Confirm user" do
|
describe "Confirm user" do
|
||||||
test "renders confirmation page", %{conn: conn} do
|
test "renders confirmation page", %{conn: conn} do
|
||||||
{:ok, _lv, html} = live(conn, ~p"/users/confirm/some-token")
|
{:ok, _lv, html} = live(conn, ~p"/users/confirm/some-token")
|
||||||
assert html =~ "Confirm Account"
|
assert html =~ "Confirm my account"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "confirms the given token once", %{conn: conn, user: user} do
|
test "confirms the given token once", %{conn: conn, user: user} do
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
defmodule Aprs.Integration.HistoricalPacketsTest do
|
defmodule Aprs.Integration.HistoricalPacketsTest do
|
||||||
use AprsWeb.ConnCase
|
use AprsWeb.ConnCase
|
||||||
|
|
||||||
|
import Aprs.MockHelpers
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
@moduletag :skip_packets_mock
|
setup do
|
||||||
|
Mox.set_mox_global()
|
||||||
|
Application.put_env(:aprs, :packets_module, Aprs.PacketsMock)
|
||||||
|
on_exit(fn -> Mox.set_mox_private() end)
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
describe "historical packet loading" do
|
describe "historical packet loading" do
|
||||||
setup do
|
setup do
|
||||||
|
|
@ -78,7 +84,10 @@ defmodule Aprs.Integration.HistoricalPacketsTest do
|
||||||
{:ok, packets: [packet1, packet2, packet3, packet4]}
|
{:ok, packets: [packet1, packet2, packet3, packet4]}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "loads all historical packets at once when map is ready", %{conn: conn, packets: mock_packets} do
|
test "loads all historical packets at once when map is ready", %{
|
||||||
|
conn: conn,
|
||||||
|
packets: mock_packets
|
||||||
|
} do
|
||||||
# Mock the Packets.get_packets_for_replay function
|
# Mock the Packets.get_packets_for_replay function
|
||||||
expect_packets_for_replay_with_bounds(mock_packets)
|
expect_packets_for_replay_with_bounds(mock_packets)
|
||||||
|
|
||||||
|
|
@ -94,14 +103,29 @@ defmodule Aprs.Integration.HistoricalPacketsTest do
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Update bounds first
|
# Set historical_hours assign
|
||||||
|
render_hook(view, "update_historical_hours", %{"historical_hours" => "1"})
|
||||||
|
Process.sleep(100)
|
||||||
|
|
||||||
|
# Set bounds and wait for map_bounds to be set in assigns
|
||||||
render_hook(view, "bounds_changed", bounds_params)
|
render_hook(view, "bounds_changed", bounds_params)
|
||||||
|
|
||||||
# Trigger map ready event
|
send(
|
||||||
render_hook(view, "map_ready", %{})
|
view.pid,
|
||||||
|
{:process_bounds_update,
|
||||||
|
%{
|
||||||
|
north: bounds_params["bounds"]["north"],
|
||||||
|
south: bounds_params["bounds"]["south"],
|
||||||
|
east: bounds_params["bounds"]["east"],
|
||||||
|
west: bounds_params["bounds"]["west"]
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
|
||||||
# Wait for the historical packet loading to complete
|
Process.sleep(100)
|
||||||
Process.sleep(700)
|
|
||||||
|
# Now trigger map_ready
|
||||||
|
render_hook(view, "map_ready", %{})
|
||||||
|
Process.sleep(1200)
|
||||||
|
|
||||||
# The LiveView should have pushed an event with historical packets
|
# The LiveView should have pushed an event with historical packets
|
||||||
# Note: In real implementation, we'd need to verify the push_event was called
|
# Note: In real implementation, we'd need to verify the push_event was called
|
||||||
|
|
@ -127,14 +151,29 @@ defmodule Aprs.Integration.HistoricalPacketsTest do
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Update bounds first
|
# Set historical_hours assign
|
||||||
|
render_hook(view, "update_historical_hours", %{"historical_hours" => "1"})
|
||||||
|
Process.sleep(100)
|
||||||
|
|
||||||
|
# Set bounds and wait for map_bounds to be set in assigns
|
||||||
render_hook(view, "bounds_changed", bounds_params)
|
render_hook(view, "bounds_changed", bounds_params)
|
||||||
|
|
||||||
# Trigger map ready event
|
send(
|
||||||
render_hook(view, "map_ready", %{})
|
view.pid,
|
||||||
|
{:process_bounds_update,
|
||||||
|
%{
|
||||||
|
north: bounds_params["bounds"]["north"],
|
||||||
|
south: bounds_params["bounds"]["south"],
|
||||||
|
east: bounds_params["bounds"]["east"],
|
||||||
|
west: bounds_params["bounds"]["west"]
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
|
||||||
# Wait for the historical packet loading to complete
|
Process.sleep(100)
|
||||||
Process.sleep(700)
|
|
||||||
|
# Now trigger map_ready
|
||||||
|
render_hook(view, "map_ready", %{})
|
||||||
|
Process.sleep(1200)
|
||||||
|
|
||||||
verify!()
|
verify!()
|
||||||
end
|
end
|
||||||
|
|
@ -145,11 +184,32 @@ defmodule Aprs.Integration.HistoricalPacketsTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/")
|
{:ok, view, _html} = live(conn, "/")
|
||||||
|
|
||||||
# Trigger map ready event
|
# Set historical_hours assign
|
||||||
|
render_hook(view, "update_historical_hours", %{"historical_hours" => "1"})
|
||||||
|
|
||||||
|
# Set bounds and synchronously update map_bounds
|
||||||
|
bounds_params = %{
|
||||||
|
"bounds" => %{"north" => "45.0", "south" => "35.0", "east" => "-90.0", "west" => "-105.0"}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_hook(view, "bounds_changed", bounds_params)
|
||||||
|
|
||||||
|
send(
|
||||||
|
view.pid,
|
||||||
|
{:process_bounds_update,
|
||||||
|
%{
|
||||||
|
north: bounds_params["bounds"]["north"],
|
||||||
|
south: bounds_params["bounds"]["south"],
|
||||||
|
east: bounds_params["bounds"]["east"],
|
||||||
|
west: bounds_params["bounds"]["west"]
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now trigger map_ready
|
||||||
render_hook(view, "map_ready", %{})
|
render_hook(view, "map_ready", %{})
|
||||||
|
|
||||||
# Wait for the historical packet loading attempt
|
# Give a very short time for message processing, if needed
|
||||||
Process.sleep(700)
|
Process.sleep(20)
|
||||||
|
|
||||||
# Should not crash
|
# Should not crash
|
||||||
verify!()
|
verify!()
|
||||||
|
|
@ -170,7 +230,10 @@ defmodule Aprs.Integration.HistoricalPacketsTest do
|
||||||
verify!()
|
verify!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "handles locate_me event after historical packets are loaded", %{conn: conn, packets: mock_packets} do
|
test "handles locate_me event after historical packets are loaded", %{
|
||||||
|
conn: conn,
|
||||||
|
packets: mock_packets
|
||||||
|
} do
|
||||||
expect_packets_for_replay(mock_packets)
|
expect_packets_for_replay(mock_packets)
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/")
|
{:ok, view, _html} = live(conn, "/")
|
||||||
|
|
@ -210,7 +273,10 @@ defmodule Aprs.Integration.HistoricalPacketsTest do
|
||||||
{:ok, historical_packet: historical_packet}
|
{:ok, historical_packet: historical_packet}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "new live packet updates marker for same callsign", %{conn: conn, historical_packet: historical_packet} do
|
test "new live packet updates marker for same callsign", %{
|
||||||
|
conn: conn,
|
||||||
|
historical_packet: historical_packet
|
||||||
|
} do
|
||||||
expect_packets_for_replay([historical_packet])
|
expect_packets_for_replay([historical_packet])
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/")
|
{:ok, view, _html} = live(conn, "/")
|
||||||
|
|
@ -226,9 +292,26 @@ defmodule Aprs.Integration.HistoricalPacketsTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
render_hook(view, "bounds_changed", bounds_params)
|
render_hook(view, "bounds_changed", bounds_params)
|
||||||
|
|
||||||
|
send(
|
||||||
|
view.pid,
|
||||||
|
{:process_bounds_update,
|
||||||
|
%{
|
||||||
|
north: bounds_params["bounds"]["north"],
|
||||||
|
south: bounds_params["bounds"]["south"],
|
||||||
|
east: bounds_params["bounds"]["east"],
|
||||||
|
west: bounds_params["bounds"]["west"]
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
|
||||||
|
Process.sleep(100)
|
||||||
render_hook(view, "map_ready", %{})
|
render_hook(view, "map_ready", %{})
|
||||||
Process.sleep(700)
|
Process.sleep(700)
|
||||||
|
|
||||||
|
# Set historical_hours assign
|
||||||
|
render_hook(view, "update_historical_hours", %{"historical_hours" => "1"})
|
||||||
|
Process.sleep(100)
|
||||||
|
|
||||||
# Simulate a new live packet for the same callsign
|
# Simulate a new live packet for the same callsign
|
||||||
new_packet = %{
|
new_packet = %{
|
||||||
id: "LIVE1",
|
id: "LIVE1",
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@ defmodule AprsIsMock do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init(_opts) do
|
def init(_opts) do
|
||||||
Logger.info("Starting APRS.Is mock for testing")
|
|
||||||
|
|
||||||
# Mock connection state
|
# Mock connection state
|
||||||
initial_state = %{
|
initial_state = %{
|
||||||
connected: false,
|
connected: false,
|
||||||
|
|
@ -40,7 +38,6 @@ defmodule AprsIsMock do
|
||||||
# Client API - Mock implementations
|
# Client API - Mock implementations
|
||||||
|
|
||||||
def stop do
|
def stop do
|
||||||
Logger.info("Stopping APRS.Is mock")
|
|
||||||
GenServer.stop(__MODULE__, :normal)
|
GenServer.stop(__MODULE__, :normal)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -93,31 +90,26 @@ defmodule AprsIsMock do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_filter(filter_string) do
|
def set_filter(_filter_string) do
|
||||||
Logger.debug("Mock: Setting filter to #{filter_string}")
|
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_active_filters do
|
def list_active_filters do
|
||||||
Logger.debug("Mock: Listing active filters")
|
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_message(from, to, message) do
|
def send_message(_from, _to, _message) do
|
||||||
Logger.debug("Mock: Sending message from #{from} to #{to}: #{message}")
|
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_message(message) do
|
def send_message(message) do
|
||||||
Logger.debug("Mock: Sending message: #{message}")
|
|
||||||
GenServer.call(__MODULE__, {:send_message, message})
|
GenServer.call(__MODULE__, {:send_message, message})
|
||||||
end
|
end
|
||||||
|
|
||||||
# Server callbacks
|
# Server callbacks
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_call({:send_message, message}, _from, state) do
|
def handle_call({:send_message, _message}, _from, state) do
|
||||||
Logger.debug("Mock: Handling send message: #{message}")
|
|
||||||
{:reply, :ok, state}
|
{:reply, :ok, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -146,14 +138,12 @@ defmodule AprsIsMock do
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info(msg, state) do
|
def handle_info(_msg, state) do
|
||||||
Logger.debug("Mock: Received unexpected message: #{inspect(msg)}")
|
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def terminate(reason, _state) do
|
def terminate(_reason, _state) do
|
||||||
Logger.info("Mock APRS.Is terminating: #{inspect(reason)}")
|
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -163,7 +153,6 @@ defmodule AprsIsMock do
|
||||||
# Simulate receiving an APRS packet for testing purposes.
|
# Simulate receiving an APRS packet for testing purposes.
|
||||||
# This can be used in tests to trigger packet processing without
|
# This can be used in tests to trigger packet processing without
|
||||||
# connecting to external servers.
|
# connecting to external servers.
|
||||||
Logger.debug("Mock: Simulating packet: #{inspect(packet_data)}")
|
|
||||||
|
|
||||||
# Broadcast to live clients like the real implementation would
|
# Broadcast to live clients like the real implementation would
|
||||||
AprsWeb.Endpoint.broadcast("aprs_messages", "packet", packet_data)
|
AprsWeb.Endpoint.broadcast("aprs_messages", "packet", packet_data)
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,16 @@ defmodule Aprs.MockHelpers do
|
||||||
Sets up an expectation for PacketsMock with custom return value.
|
Sets up an expectation for PacketsMock with custom return value.
|
||||||
"""
|
"""
|
||||||
def expect_packets_for_replay(packets) do
|
def expect_packets_for_replay(packets) do
|
||||||
expect(Aprs.PacketsMock, :get_packets_for_replay, fn _opts -> packets end)
|
stub(Aprs.PacketsMock, :get_packets_for_replay, fn _opts ->
|
||||||
|
packets
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Sets up an expectation for PacketsMock with filtering based on bounds.
|
Sets up an expectation for PacketsMock with filtering based on bounds.
|
||||||
"""
|
"""
|
||||||
def expect_packets_for_replay_with_bounds(packets) do
|
def expect_packets_for_replay_with_bounds(packets) do
|
||||||
expect(Aprs.PacketsMock, :get_packets_for_replay, fn opts ->
|
stub(Aprs.PacketsMock, :get_packets_for_replay, fn opts ->
|
||||||
case opts[:bounds] do
|
case opts[:bounds] do
|
||||||
[west, south, east, north] ->
|
[west, south, east, north] ->
|
||||||
Enum.filter(packets, fn packet ->
|
Enum.filter(packets, fn packet ->
|
||||||
|
|
|
||||||
|
|
@ -12,5 +12,6 @@ Application.put_env(:aprs, :aprs_is_port, 14_580)
|
||||||
Application.put_env(:aprs, :aprs_is_login_id, "TEST")
|
Application.put_env(:aprs, :aprs_is_login_id, "TEST")
|
||||||
Application.put_env(:aprs, :aprs_is_password, "-1")
|
Application.put_env(:aprs, :aprs_is_password, "-1")
|
||||||
Application.put_env(:aprs, :aprs_is_default_filter, "r/0/0/1")
|
Application.put_env(:aprs, :aprs_is_default_filter, "r/0/0/1")
|
||||||
|
Application.put_env(:aprs, :packets_module, Aprs.PacketsMock)
|
||||||
|
|
||||||
# AprsIsMock is automatically loaded from test/support via elixirc_paths
|
# AprsIsMock is automatically loaded from test/support via elixirc_paths
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue