refactor to use external parser

This commit is contained in:
Graham McIntire 2025-06-24 14:22:09 -05:00
parent 2ba9f11656
commit ce7c50caa1
No known key found for this signature in database
202 changed files with 794 additions and 5100 deletions

View file

@ -26,7 +26,7 @@ jobs:
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . context: .
tags: aprs:latest tags: aprsme:latest
load: true load: true
no-cache: true no-cache: true
pull: true pull: true
@ -36,7 +36,7 @@ jobs:
- name: Run Trivy vulnerability scanner - name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master uses: aquasecurity/trivy-action@master
with: with:
image-ref: aprs:latest image-ref: aprsme:latest
format: "sarif" format: "sarif"
output: "trivy-results.sarif" output: "trivy-results.sarif"
severity: "CRITICAL,HIGH" severity: "CRITICAL,HIGH"
@ -59,7 +59,7 @@ jobs:
echo "### OS Package Vulnerabilities" >> dep-update-report.md echo "### OS Package Vulnerabilities" >> dep-update-report.md
echo "```" >> dep-update-report.md echo "```" >> dep-update-report.md
trivy image --severity HIGH,CRITICAL --no-progress aprs:latest >> dep-update-report.md trivy image --severity HIGH,CRITICAL --no-progress aprsme:latest >> dep-update-report.md
echo "```" >> dep-update-report.md echo "```" >> dep-update-report.md
- name: Upload vulnerability summary - name: Upload vulnerability summary

View file

@ -45,7 +45,7 @@ jobs:
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . context: .
tags: aprs:${{ github.sha }} tags: aprsme:${{ github.sha }}
load: true load: true
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
@ -53,7 +53,7 @@ jobs:
- name: Run Trivy vulnerability scanner - name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master uses: aquasecurity/trivy-action@master
with: with:
image-ref: aprs:${{ github.sha }} image-ref: aprsme:${{ github.sha }}
format: "sarif" format: "sarif"
output: "trivy-results.sarif" output: "trivy-results.sarif"
severity: "CRITICAL,HIGH" severity: "CRITICAL,HIGH"
@ -70,7 +70,7 @@ jobs:
- name: Run Trivy for outdated OS packages - name: Run Trivy for outdated OS packages
uses: aquasecurity/trivy-action@master uses: aquasecurity/trivy-action@master
with: with:
image-ref: aprs:${{ github.sha }} image-ref: aprsme:${{ github.sha }}
format: "table" format: "table"
output: "os-packages.txt" output: "os-packages.txt"
severity: "HIGH,CRITICAL" severity: "HIGH,CRITICAL"
@ -79,7 +79,7 @@ jobs:
- name: Run Trivy for outdated application dependencies - name: Run Trivy for outdated application dependencies
uses: aquasecurity/trivy-action@master uses: aquasecurity/trivy-action@master
with: with:
image-ref: aprs:${{ github.sha }} image-ref: aprsme:${{ github.sha }}
format: "table" format: "table"
output: "app-packages.txt" output: "app-packages.txt"
severity: "HIGH,CRITICAL" severity: "HIGH,CRITICAL"
@ -99,7 +99,7 @@ jobs:
uses: docker/scout-action@v1 uses: docker/scout-action@v1
with: with:
command: quickview,cves command: quickview,cves
image: aprs:${{ github.sha }} image: aprsme:${{ github.sha }}
output-format: sarif output-format: sarif
only-severities: critical,high only-severities: critical,high
sarif-file: scout-results.sarif sarif-file: scout-results.sarif
@ -116,7 +116,7 @@ jobs:
if: always() if: always()
run: | run: |
echo "# Docker Image Security Report" > security-summary.md echo "# Docker Image Security Report" > security-summary.md
echo "## Image: aprs:${{ github.sha }}" >> security-summary.md echo "## Image: aprsme:${{ github.sha }}" >> security-summary.md
echo "## Date: $(date)" >> security-summary.md echo "## Date: $(date)" >> security-summary.md
echo "### OS Package Vulnerabilities" >> security-summary.md echo "### OS Package Vulnerabilities" >> security-summary.md

2
.gitignore vendored
View file

@ -20,7 +20,7 @@ erl_crash.dump
*.ez *.ez
# Ignore package tarball (built via "mix hex.build"). # Ignore package tarball (built via "mix hex.build").
aprs-*.tar aprsme-*.tar
# Ignore assets that are produced by build tools. # Ignore assets that are produced by build tools.
/priv/static/assets/ /priv/static/assets/

View file

@ -121,7 +121,7 @@ WORKDIR /app
# Copy the Elixir release from builder stage # Copy the Elixir release from builder stage
# The distroless nonroot user has UID 65532, so we use that # The distroless nonroot user has UID 65532, so we use that
COPY --from=builder --chown=65532:65532 /app/_build/prod/rel/aprs ./ COPY --from=builder --chown=65532:65532 /app/_build/prod/rel/aprsme ./
# Create required directories with proper permissions # Create required directories with proper permissions
USER 0 USER 0

View file

@ -83,19 +83,19 @@ The workflow is defined in `.github/workflows/deploy-k3s.yaml`. It will:
* **Database Migrations:** The Kubernetes deployment for the application includes an init container that automatically runs database migrations (`/app/bin/migrate`) before the main application container starts. * **Database Migrations:** The Kubernetes deployment for the application includes an init container that automatically runs database migrations (`/app/bin/migrate`) before the main application container starts.
* **Health Check Endpoint:** The application deployment (`k8s/app-deployment.yaml`) is configured with readiness and liveness probes that expect a health check endpoint at `/health` (returning HTTP 200). You may need to add this to your Phoenix application's router and controller: * **Health Check Endpoint:** The application deployment (`k8s/app-deployment.yaml`) is configured with readiness and liveness probes that expect a health check endpoint at `/health` (returning HTTP 200). You may need to add this to your Phoenix application's router and controller:
* In `lib/aprs_web/router.ex`: * In `lib/aprsme_web/router.ex`:
```elixir ```elixir
scope "/", AprsWeb do scope "/", AprsmeWeb do
pipe_through :browser pipe_through :browser
# ... other routes # ... other routes
get "/health", PageController, :health get "/health", PageController, :health
end end
``` ```
* In `lib/aprs_web/controllers/page_controller.ex` (or another suitable controller): * In `lib/aprsme_web/controllers/page_controller.ex` (or another suitable controller):
```elixir ```elixir
def health(conn, _params) do def health(conn, _params) do
# You can add more sophisticated checks here if needed # You can add more sophisticated checks here if needed
json(conn, %{status: "ok", version: Application.spec(:aprs, :vsn)}) json(conn, %{status: "ok", version: Application.spec(:aprsme, :vsn)})
end end
``` ```
* **Image Name:** The application deployment manifest (`k8s/app-deployment.yaml`) uses a placeholder image name `your-ghcr-username/aprs-app:latest`. The CI/CD pipeline automatically replaces this with the correct image name from GHCR (e.g., `ghcr.io/your-github-owner/your-repo-name:<git-sha>`). * **Image Name:** The application deployment manifest (`k8s/app-deployment.yaml`) uses a placeholder image name `your-ghcr-username/aprs-app:latest`. The CI/CD pipeline automatically replaces this with the correct image name from GHCR (e.g., `ghcr.io/your-github-owner/your-repo-name:<git-sha>`).

View file

@ -14,36 +14,36 @@ import Config
# #
# For production it's recommended to configure a different adapter # For production it's recommended to configure a different adapter
# at the `config/runtime.exs`. # at the `config/runtime.exs`.
config :aprs, Aprs.Mailer, adapter: Swoosh.Adapters.Local config :aprsme, Aprsme.Mailer, adapter: Swoosh.Adapters.Local
# Configures the endpoint # Configures the endpoint
config :aprs, AprsWeb.Endpoint, config :aprsme, AprsmeWeb.Endpoint,
url: [host: "localhost"], url: [host: "localhost"],
render_errors: [ render_errors: [
formats: [html: AprsWeb.ErrorHTML, json: AprsWeb.ErrorJSON], formats: [html: AprsmeWeb.ErrorHTML, json: AprsmeWeb.ErrorJSON],
layout: false layout: false
], ],
pubsub_server: Aprs.PubSub, pubsub_server: Aprsme.PubSub,
live_view: [signing_salt: "ees098qG"] live_view: [signing_salt: "ees098qG"]
# Configure Oban for background jobs # Configure Oban for background jobs
config :aprs, Oban, config :aprsme, Oban,
repo: Aprs.Repo, repo: Aprsme.Repo,
plugins: [ plugins: [
{Oban.Plugins.Pruner, max_age: 60 * 60 * 24 * 7}, {Oban.Plugins.Pruner, max_age: 60 * 60 * 24 * 7},
{Oban.Plugins.Cron, {Oban.Plugins.Cron,
crontab: [ crontab: [
{"0 0 * * *", Aprs.Workers.PacketCleanupWorker}, {"0 0 * * *", Aprsme.Workers.PacketCleanupWorker},
{"0 3 * * 1", Aprs.DeviceIdentification.Worker} {"0 3 * * 1", Aprsme.DeviceIdentification.Worker}
]} ]}
], ],
queues: [default: 10, maintenance: 2] queues: [default: 10, maintenance: 2]
config :aprs, config :aprsme,
ecto_repos: [Aprs.Repo] ecto_repos: [Aprsme.Repo]
config :aprs, config :aprsme,
ecto_repos: [Aprs.Repo], ecto_repos: [Aprsme.Repo],
aprs_is_server: System.get_env("APRS_SERVER", "dallas.aprs2.net"), aprs_is_server: System.get_env("APRS_SERVER", "dallas.aprs2.net"),
aprs_is_port: 10_152, aprs_is_port: 10_152,
aprs_is_default_filter: System.get_env("APRS_FILTER"), aprs_is_default_filter: System.get_env("APRS_FILTER"),

View file

@ -1,7 +1,7 @@
import Config import Config
# Configure your database # Configure your database
config :aprs, Aprs.Repo, config :aprsme, Aprsme.Repo,
# For development, we disable any cache and enable # For development, we disable any cache and enable
# debugging and code reloading. # debugging and code reloading.
# #
@ -11,14 +11,14 @@ config :aprs, Aprs.Repo,
username: "postgres", username: "postgres",
password: "postgres", password: "postgres",
hostname: "localhost", hostname: "localhost",
database: "aprs_dev", database: "aprsme_dev",
stacktrace: true, stacktrace: true,
show_sensitive_data_on_connection_error: true, show_sensitive_data_on_connection_error: true,
pool_size: 10, pool_size: 10,
log: false, log: false,
types: Aprs.PostgresTypes types: Aprsme.PostgresTypes
config :aprs, AprsWeb.Endpoint, config :aprsme, AprsmeWeb.Endpoint,
# Binding to loopback ipv4 address prevents access from other machines. # Binding to loopback ipv4 address prevents access from other machines.
# Change to `ip: {0, 0, 0, 0}` to allow access from other machines. # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
http: [ip: {127, 0, 0, 1}, port: 4000], http: [ip: {127, 0, 0, 1}, port: 4000],
@ -37,13 +37,13 @@ config :aprs, AprsWeb.Endpoint,
] ]
# Watch static and templates for browser reloading. # Watch static and templates for browser reloading.
config :aprs, AprsWeb.Endpoint, config :aprsme, AprsmeWeb.Endpoint,
live_reload: [ live_reload: [
patterns: [ patterns: [
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
~r"priv/gettext/.*(po)$", ~r"priv/gettext/.*(po)$",
~r"lib/aprs_web/(live|views)/.*(ex)$", ~r"lib/aprsme_web/(live|views)/.*(ex)$",
~r"lib/aprs_web/templates/.*(eex)$", ~r"lib/aprsme_web/templates/.*(eex)$",
~r"assets/vendor/.*(js|css)$" ~r"assets/vendor/.*(js|css)$"
] ]
] ]
@ -53,7 +53,7 @@ config :aprs, AprsWeb.Endpoint,
# mix phx.gen.cert # mix phx.gen.cert
# #
# Run `mix help phx.gen.cert` for more information. # Run `mix help phx.gen.cert` for more information.
config :aprs, dev_routes: true config :aprsme, dev_routes: true
# Do not include metadata nor timestamps in development logs # Do not include metadata nor timestamps in development logs
# #

View file

@ -9,7 +9,7 @@ import Config
# manifest is generated by the `mix phx.digest` task, # manifest is generated by the `mix phx.digest` task,
# which you should run after static files are built and # which you should run after static files are built and
# before starting your production server. # before starting your production server.
config :aprs, AprsWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" config :aprsme, AprsmeWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
# Runtime production configuration, including reading # Runtime production configuration, including reading
@ -26,4 +26,4 @@ config :esbuild,
config :logger, level: :info config :logger, level: :info
# Configures Swoosh API Client # Configures Swoosh API Client
config :swoosh, :api_client, Aprs.Finch config :swoosh, :api_client, Aprsme.Finch

View file

@ -12,13 +12,13 @@ import Config
# If you use `mix release`, you need to explicitly enable the server # If you use `mix release`, you need to explicitly enable the server
# by passing the PHX_SERVER=true when you start it: # by passing the PHX_SERVER=true when you start it:
# #
# PHX_SERVER=true bin/aprs start # PHX_SERVER=true bin/aprsme start
# #
# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
# script that automatically sets the env var above. # script that automatically sets the env var above.
# Always start the server in production/docker environments # Always start the server in production/docker environments
if System.get_env("PHX_SERVER") || config_env() == :prod do if System.get_env("PHX_SERVER") || config_env() == :prod do
config :aprs, AprsWeb.Endpoint, server: true config :aprsme, AprsmeWeb.Endpoint, server: true
end end
if config_env() == :prod do if config_env() == :prod do
@ -46,14 +46,14 @@ if config_env() == :prod do
host = System.get_env("PHX_HOST") || "example.com" host = System.get_env("PHX_HOST") || "example.com"
port = String.to_integer(System.get_env("PORT") || "4000") port = String.to_integer(System.get_env("PORT") || "4000")
config :aprs, Aprs.Repo, config :aprsme, Aprsme.Repo,
# ssl: true, # ssl: true,
url: database_url, url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
socket_options: maybe_ipv6, socket_options: maybe_ipv6,
types: Aprs.PostgresTypes types: Aprsme.PostgresTypes
config :aprs, AprsWeb.Endpoint, config :aprsme, AprsmeWeb.Endpoint,
url: [host: host, port: 443, scheme: "https"], url: [host: host, port: 443, scheme: "https"],
http: [ http: [
# Bind on all IPv4 interfaces for better container compatibility # Bind on all IPv4 interfaces for better container compatibility
@ -66,10 +66,10 @@ if config_env() == :prod do
secret_key_base: secret_key_base, secret_key_base: secret_key_base,
server: true server: true
# config :aprs, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") # config :aprsme, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
config :aprs, config :aprsme,
ecto_repos: [Aprs.Repo], ecto_repos: [Aprsme.Repo],
aprs_is_server: System.get_env("APRS_SERVER", "dallas.aprs2.net"), aprs_is_server: System.get_env("APRS_SERVER", "dallas.aprs2.net"),
aprs_is_port: 14_580, aprs_is_port: 14_580,
aprs_is_default_filter: System.get_env("APRS_FILTER"), aprs_is_default_filter: System.get_env("APRS_FILTER"),
@ -94,7 +94,7 @@ if config_env() == :prod do
# To get SSL working, you will need to add the `https` key # To get SSL working, you will need to add the `https` key
# to your endpoint configuration: # to your endpoint configuration:
# #
# config :aprs, AprsWeb.Endpoint, # config :aprsme, AprsmeWeb.Endpoint,
# https: [ # https: [
# ..., # ...,
# port: 443, # port: 443,
@ -116,7 +116,7 @@ if config_env() == :prod do
# We also recommend setting `force_ssl` in your endpoint, ensuring # We also recommend setting `force_ssl` in your endpoint, ensuring
# no data is ever sent via http, always redirecting to https: # no data is ever sent via http, always redirecting to https:
# #
# config :aprs, AprsWeb.Endpoint, # config :aprsme, AprsmeWeb.Endpoint,
# force_ssl: [hsts: true] # force_ssl: [hsts: true]
# #
# Check `Plug.SSL` for all available options in `force_ssl`. # Check `Plug.SSL` for all available options in `force_ssl`.
@ -127,7 +127,7 @@ if config_env() == :prod do
# Also, you may need to configure the Swoosh API client of your choice if you # Also, you may need to configure the Swoosh API client of your choice if you
# are not using SMTP. Here is an example of the configuration: # are not using SMTP. Here is an example of the configuration:
# #
# config :aprs, Aprs.Mailer, # config :aprsme, Aprsme.Mailer,
# adapter: Swoosh.Adapters.Mailgun, # adapter: Swoosh.Adapters.Mailgun,
# api_key: System.get_env("MAILGUN_API_KEY"), # api_key: System.get_env("MAILGUN_API_KEY"),
# domain: System.get_env("MAILGUN_DOMAIN") # domain: System.get_env("MAILGUN_DOMAIN")

View file

@ -1,40 +1,40 @@
import Config import Config
# In test we don't send emails. # In test we don't send emails.
config :aprs, Aprs.Mailer, adapter: Swoosh.Adapters.Test config :aprsme, Aprsme.Mailer, adapter: Swoosh.Adapters.Test
# Configure your database # Configure your database
# #
# The MIX_TEST_PARTITION environment variable can be used # The MIX_TEST_PARTITION environment variable can be used
# to provide built-in test partitioning in CI environment. # to provide built-in test partitioning in CI environment.
# Run `mix help test` for more information. # Run `mix help test` for more information.
config :aprs, Aprs.Repo, config :aprsme, Aprsme.Repo,
username: "postgres", username: "postgres",
password: "postgres", password: "postgres",
hostname: "localhost", hostname: "localhost",
database: "aprs_test#{System.get_env("MIX_TEST_PARTITION")}", database: "aprsme_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox, pool: Ecto.Adapters.SQL.Sandbox,
pool_size: System.schedulers_online() * 2, pool_size: System.schedulers_online() * 2,
types: Aprs.PostgresTypes types: Aprsme.PostgresTypes
# We don't run a server during test. If one is required, # We don't run a server during test. If one is required,
# you can enable the server option below. # you can enable the server option below.
config :aprs, AprsWeb.Endpoint, config :aprsme, AprsmeWeb.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 4002], http: [ip: {127, 0, 0, 1}, port: 4002],
secret_key_base: "IV9+ENaw9i8xjReRk4sULRvRgsmFVTGQwQGGrf4G+Q/SFMeHBCNWRlPXQ2YvT36R", secret_key_base: "IV9+ENaw9i8xjReRk4sULRvRgsmFVTGQwQGGrf4G+Q/SFMeHBCNWRlPXQ2YvT36R",
server: false server: false
# Disable Oban during tests to prevent background job execution # Disable Oban during tests to prevent background job execution
config :aprs, Oban, testing: :inline config :aprsme, Oban, testing: :inline
# Disable initialize replay delay in test environment # Disable initialize replay delay in test environment
config :aprs, :initialize_replay_delay, 0 config :aprsme, :initialize_replay_delay, 0
# Configure the packets module to use the mock in tests # Configure the packets module to use the mock in tests
config :aprs, :packets_module, Aprs.PacketsMock config :aprsme, :packets_module, Aprsme.PacketsMock
# Disable APRS-IS external connections in test environment # Disable APRS-IS external connections in test environment
config :aprs, config :aprsme,
aprs_is_server: "mock.aprs.test", aprs_is_server: "mock.aprs.test",
aprs_is_port: 14_580, aprs_is_port: 14_580,
aprs_is_default_filter: "r/33/-96/100", aprs_is_default_filter: "r/33/-96/100",
@ -43,7 +43,7 @@ config :aprs,
disable_aprs_connection: true disable_aprs_connection: true
# Disable automatic migrations during tests # Disable automatic migrations during tests
config :aprs, auto_migrate: false config :aprsme, auto_migrate: false
# Only in tests, remove the complexity from the password hashing algorithm # Only in tests, remove the complexity from the password hashing algorithm
config :bcrypt_elixir, :log_rounds, 1 config :bcrypt_elixir, :log_rounds, 1

View file

@ -49,5 +49,5 @@ spec:
initialDelaySeconds: 30 initialDelaySeconds: 30
periodSeconds: 15 periodSeconds: 15
# Note: For the health endpoint, you might need to create a simple one in your Phoenix router # Note: For the health endpoint, you might need to create a simple one in your Phoenix router
# e.g., scope "/", AprsWeb do; get "/health", PageController, :health; end # e.g., scope "/", AprsmeWeb do; get "/health", PageController, :health; end
# and a `def health(conn, _params), do: json(conn, %{status: "ok"})` in PageController. # and a `def health(conn, _params), do: json(conn, %{status: "ok"})` in PageController.

View file

@ -1,9 +0,0 @@
defmodule Aprs do
@moduledoc """
Aprs keeps the contexts that define your domain
and business logic.
Contexts are also responsible for managing your data, regardless
if it comes from the database, an external API or others.
"""
end

View file

@ -1,4 +0,0 @@
defmodule Aprs.Mailer do
@moduledoc false
use Swoosh.Mailer, otp_app: :aprs
end

View file

@ -1,4 +0,0 @@
defmodule Aprs.Presence do
@moduledoc false
use Phoenix.Presence, otp_app: :aprs, pubsub_server: Aprs.PubSub
end

View file

@ -1,20 +1,20 @@
defmodule AprsWeb do defmodule AprsmeWeb do
@moduledoc """ @moduledoc """
The entrypoint for defining your web interface, such The entrypoint for defining your web interface, such
as controllers, components, channels, and so on. as controllers, components, channels, and so on.
This can be used in your application as: This can be used in your application as:
use AprsWeb, :controller use AprsmeWeb, :controller
use AprsWeb, :html use AprsmeWeb, :html
The definitions below will be executed for every controller, The definitions below will be executed for every view,
component, etc, so keep them short and clean, focused component, etc, so keep them short and clean, focused
on imports, uses and aliases. on imports, uses and aliases.
Do NOT define functions inside the quoted expressions Do NOT define functions inside the quoted expressions
below. Instead, define additional modules and import below. Instead, define additional functions and do the
those modules here. imports in your modules.
""" """
def static_paths, do: ~w(assets fonts images aprs-symbols favicon.ico robots.txt) def static_paths, do: ~w(assets fonts images aprs-symbols favicon.ico robots.txt)
@ -23,9 +23,10 @@ defmodule AprsWeb do
quote do quote do
use Phoenix.Router, helpers: false use Phoenix.Router, helpers: false
# Import common connection and controller functions to use in pipelines
import Phoenix.Controller import Phoenix.Controller
import Phoenix.LiveView.Router import Phoenix.LiveView.Router
# Import common connection and controller functions to use in pipelines
import Plug.Conn import Plug.Conn
end end
end end
@ -33,17 +34,18 @@ defmodule AprsWeb do
def channel do def channel do
quote do quote do
use Phoenix.Channel use Phoenix.Channel
import AprsmeWeb.Gettext
end end
end end
def controller do def controller do
quote do quote do
use Phoenix.Controller, use Phoenix.Controller,
namespace: AprsWeb,
formats: [:html, :json], formats: [:html, :json],
layouts: [html: AprsWeb.Layouts] layouts: [html: AprsmeWeb.Layouts]
import AprsWeb.Gettext import AprsmeWeb.Gettext
import Plug.Conn import Plug.Conn
unquote(verified_routes()) unquote(verified_routes())
@ -53,7 +55,7 @@ defmodule AprsWeb do
def live_view do def live_view do
quote do quote do
use Phoenix.LiveView, use Phoenix.LiveView,
layout: {AprsWeb.Layouts, :app} layout: {AprsmeWeb.Layouts, :app}
unquote(html_helpers()) unquote(html_helpers())
end end
@ -82,11 +84,11 @@ defmodule AprsWeb do
defp html_helpers do defp html_helpers do
quote do quote do
import AprsmeWeb.CoreComponents
import AprsmeWeb.Gettext
# HTML escaping functionality # HTML escaping functionality
# Core UI components and translation
import AprsWeb.CoreComponents
import AprsWeb.Gettext
import Phoenix.HTML import Phoenix.HTML
# Core UI components and translation
# Shortcut for generating JS commands # Shortcut for generating JS commands
alias Phoenix.LiveView.JS alias Phoenix.LiveView.JS
@ -99,9 +101,9 @@ defmodule AprsWeb do
def verified_routes do def verified_routes do
quote do quote do
use Phoenix.VerifiedRoutes, use Phoenix.VerifiedRoutes,
endpoint: AprsWeb.Endpoint, endpoint: AprsmeWeb.Endpoint,
router: AprsWeb.Router, router: AprsmeWeb.Router,
statics: AprsWeb.static_paths() statics: AprsmeWeb.static_paths()
end end
end end

5
lib/aprsme.ex Normal file
View file

@ -0,0 +1,5 @@
defmodule Aprsme do
@moduledoc """
The main application module for Aprsme.
"""
end

View file

@ -1,14 +1,14 @@
defmodule Aprs.Accounts do defmodule Aprsme.Accounts do
@moduledoc """ @moduledoc """
The Accounts context. The Accounts context.
""" """
import Ecto.Query, warn: false import Ecto.Query, warn: false
alias Aprs.Accounts.User alias Aprsme.Accounts.User
alias Aprs.Accounts.UserNotifier alias Aprsme.Accounts.UserNotifier
alias Aprs.Accounts.UserToken alias Aprsme.Accounts.UserToken
alias Aprs.Repo alias Aprsme.Repo
## Database getters ## Database getters

View file

@ -1,4 +1,4 @@
defmodule Aprs.Accounts.User do defmodule Aprsme.Accounts.User do
@moduledoc false @moduledoc false
use Ecto.Schema use Ecto.Schema
@ -89,7 +89,7 @@ defmodule Aprs.Accounts.User do
defp do_validate_unique_email(changeset, true) do defp do_validate_unique_email(changeset, true) do
changeset changeset
|> unsafe_validate_unique(:email, Aprs.Repo) |> unsafe_validate_unique(:email, Aprsme.Repo)
|> unique_constraint(:email) |> unique_constraint(:email)
end end
@ -143,7 +143,7 @@ defmodule Aprs.Accounts.User do
If there is no user or the user doesn't have a password, we call If there is no user or the user doesn't have a password, we call
`Bcrypt.no_user_verify/0` to avoid timing attacks. `Bcrypt.no_user_verify/0` to avoid timing attacks.
""" """
def valid_password?(%Aprs.Accounts.User{hashed_password: hashed_password}, password) def valid_password?(%Aprsme.Accounts.User{hashed_password: hashed_password}, password)
when is_binary(hashed_password) and byte_size(password) > 0 do when is_binary(hashed_password) and byte_size(password) > 0 do
Bcrypt.verify_pass(password, hashed_password) Bcrypt.verify_pass(password, hashed_password)
end end

View file

@ -1,8 +1,8 @@
defmodule Aprs.Accounts.UserNotifier do defmodule Aprsme.Accounts.UserNotifier do
@moduledoc false @moduledoc false
import Swoosh.Email import Swoosh.Email
alias Aprs.Mailer alias Aprsme.Mailer
# Delivers the email using the application mailer. # Delivers the email using the application mailer.
defp deliver(recipient, subject, body) do defp deliver(recipient, subject, body) do

View file

@ -1,10 +1,10 @@
defmodule Aprs.Accounts.UserToken do defmodule Aprsme.Accounts.UserToken do
@moduledoc false @moduledoc false
use Ecto.Schema use Ecto.Schema
import Ecto.Query import Ecto.Query
alias Aprs.Accounts.UserToken alias Aprsme.Accounts.UserToken
@hash_algorithm :sha256 @hash_algorithm :sha256
@rand_size 32 @rand_size 32
@ -20,7 +20,7 @@ defmodule Aprs.Accounts.UserToken do
field(:token, :binary) field(:token, :binary)
field(:context, :string) field(:context, :string)
field(:sent_to, :string) field(:sent_to, :string)
belongs_to(:user, Aprs.Accounts.User) belongs_to(:user, Aprsme.Accounts.User)
timestamps(updated_at: false) timestamps(updated_at: false)
end end

View file

@ -1,4 +1,4 @@
defmodule Aprs.Application do defmodule Aprsme.Application do
# See https://hexdocs.pm/elixir/Application.html # See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications # for more information on OTP Applications
@moduledoc false @moduledoc false
@ -16,35 +16,40 @@ defmodule Aprs.Application do
children = [ children = [
# Start the Telemetry supervisor # Start the Telemetry supervisor
AprsWeb.Telemetry, AprsmeWeb.Telemetry,
# Start the Ecto repository # Start the Ecto repository
Aprs.Repo, Aprsme.Repo,
# Start the PubSub system # Start the PubSub system
{Phoenix.PubSub, name: Aprs.PubSub}, {Phoenix.PubSub, name: Aprsme.PubSub},
# Start Finch # Start Finch
{Finch, name: Aprs.Finch}, {Finch, name: Aprsme.Finch},
# Start the Endpoint (http/https) # Start the Endpoint (http/https)
AprsWeb.Endpoint, AprsmeWeb.Endpoint,
# Start a worker by calling: Aprs.Worker.start_link(arg) # Start a worker by calling: Aprsme.Worker.start_link(arg)
# {Aprs.Worker, arg} # {Aprsme.Worker, arg}
{Registry, keys: :duplicate, name: Registry.PubSub, partitions: System.schedulers_online()}, {Registry, keys: :duplicate, name: Registry.PubSub, partitions: System.schedulers_online()},
{Cluster.Supervisor, [topologies, [name: Aprs.ClusterSupervisor]]}, {Cluster.Supervisor, [topologies, [name: Aprsme.ClusterSupervisor]]},
# Start Oban for background jobs # Start Oban for background jobs
{Oban, :aprs |> Application.get_env(Oban, []) |> Keyword.put(:queues, default: 10, maintenance: 2)}, {Oban, :aprsme |> Application.get_env(Oban, []) |> Keyword.put(:queues, default: 10, maintenance: 2)},
Aprs.Presence, Aprsme.Presence,
Aprs.AprsIsConnection, Aprsme.AprsIsConnection,
Aprs.PostgresNotifier Aprsme.PostgresNotifier
] ]
children = maybe_add_is_supervisor(children, Application.get_env(:aprs, :env)) children = maybe_add_is_supervisor(children, Application.get_env(:aprsme, :env))
# 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
opts = [strategy: :one_for_one, name: Aprs.Supervisor] opts = [strategy: :one_for_one, name: Aprsme.Supervisor]
{:ok, sup} = Supervisor.start_link(children, opts) {:ok, sup} = Supervisor.start_link(children, opts)
# Now that the Repo is started, run the refresh in a background task # Now that the Repo is started, run the refresh in a background task
Task.start(fn -> Aprs.DeviceIdentification.maybe_refresh_devices() end) # Skip in test environment to avoid DBConnection.OwnershipError
env = Application.get_env(:aprsme, :env)
if env != :test do
Task.start(fn -> Aprsme.DeviceIdentification.maybe_refresh_devices() end)
end
{:ok, sup} {:ok, sup}
end end
@ -53,12 +58,12 @@ defmodule Aprs.Application do
# whenever the application is updated. # whenever the application is updated.
@impl true @impl true
def config_change(changed, _new, removed) do def config_change(changed, _new, removed) do
AprsWeb.Endpoint.config_change(changed, removed) AprsmeWeb.Endpoint.config_change(changed, removed)
:ok :ok
end end
defp migrate do defp migrate do
auto_migrate = Application.get_env(:aprs, :auto_migrate, true) auto_migrate = Application.get_env(:aprsme, :auto_migrate, true)
do_migrate(auto_migrate) do_migrate(auto_migrate)
rescue rescue
error -> error ->
@ -70,7 +75,7 @@ defmodule Aprs.Application do
end end
defp maybe_add_is_supervisor(children, env) when env in [:prod, :dev] do defp maybe_add_is_supervisor(children, env) when env in [:prod, :dev] do
children ++ [Aprs.Is.IsSupervisor] children ++ [Aprsme.Is.IsSupervisor]
end end
defp maybe_add_is_supervisor(children, _env), do: children defp maybe_add_is_supervisor(children, _env), do: children
@ -79,7 +84,7 @@ defmodule Aprs.Application do
require Logger require Logger
Logger.info("Running database migrations...") Logger.info("Running database migrations...")
Aprs.Release.migrate() Aprsme.Release.migrate()
Logger.info("Database migrations completed") Logger.info("Database migrations completed")
end end

View file

@ -1,4 +1,4 @@
defmodule Aprs.AprsIsConnection do defmodule Aprsme.AprsIsConnection do
@moduledoc """ @moduledoc """
Maintains a supervised TCP connection to APRS-IS, with reconnection logic and Maintains a supervised TCP connection to APRS-IS, with reconnection logic and
exponential backoff. Broadcasts each received line via Phoenix.PubSub and emits exponential backoff. Broadcasts each received line via Phoenix.PubSub and emits
@ -43,13 +43,13 @@ defmodule Aprs.AprsIsConnection do
case connect_aprs_is() do case connect_aprs_is() do
{:ok, socket} -> {:ok, socket} ->
Logger.info("Connected to APRS-IS") Logger.info("Connected to APRS-IS")
:telemetry.execute([:aprs, :is, :connected], %{}, %{}) :telemetry.execute([:aprsme, :is, :connected], %{}, %{})
{:noreply, %{state | socket: socket, backoff: @reconnect_initial}} {:noreply, %{state | socket: socket, backoff: @reconnect_initial}}
{:error, reason} -> {:error, reason} ->
Logger.error("APRS-IS connection failed: #{inspect(reason)}. Retrying in #{state.backoff} ms.") Logger.error("APRS-IS connection failed: #{inspect(reason)}. Retrying in #{state.backoff} ms.")
:telemetry.execute([:aprs, :is, :connect_error], %{}, %{reason: reason}) :telemetry.execute([:aprsme, :is, :connect_error], %{}, %{reason: reason})
schedule_connect(state.backoff) schedule_connect(state.backoff)
{:noreply, %{state | socket: nil, backoff: min(state.backoff * 2, @reconnect_max)}} {:noreply, %{state | socket: nil, backoff: min(state.backoff * 2, @reconnect_max)}}
end end
@ -58,15 +58,15 @@ defmodule Aprs.AprsIsConnection do
@impl true @impl true
def handle_info({:tcp, _socket, data}, state) do def handle_info({:tcp, _socket, data}, state) do
# Each line received from APRS-IS # Each line received from APRS-IS
Phoenix.PubSub.broadcast(Aprs.PubSub, @pubsub_topic, {:aprs_is_line, data}) Phoenix.PubSub.broadcast(Aprsme.PubSub, @pubsub_topic, {:aprsme_is_line, data})
:telemetry.execute([:aprs, :is, :packet], %{size: byte_size(data)}, %{data: data}) :telemetry.execute([:aprsme, :is, :packet], %{size: byte_size(data)}, %{data: data})
{:noreply, state} {:noreply, state}
end end
@impl true @impl true
def handle_info({:tcp_closed, _socket}, state) do def handle_info({:tcp_closed, _socket}, state) do
Logger.warning("APRS-IS connection closed. Reconnecting...") Logger.warning("APRS-IS connection closed. Reconnecting...")
:telemetry.execute([:aprs, :is, :disconnected], %{}, %{}) :telemetry.execute([:aprsme, :is, :disconnected], %{}, %{})
schedule_connect(@reconnect_initial) schedule_connect(@reconnect_initial)
{:noreply, %{state | socket: nil, backoff: @reconnect_initial}} {:noreply, %{state | socket: nil, backoff: @reconnect_initial}}
end end
@ -74,7 +74,7 @@ defmodule Aprs.AprsIsConnection do
@impl true @impl true
def handle_info({:tcp_error, _socket, reason}, state) do def handle_info({:tcp_error, _socket, reason}, state) do
Logger.error("APRS-IS TCP error: #{inspect(reason)}. Reconnecting...") Logger.error("APRS-IS TCP error: #{inspect(reason)}. Reconnecting...")
:telemetry.execute([:aprs, :is, :tcp_error], %{}, %{reason: reason}) :telemetry.execute([:aprsme, :is, :tcp_error], %{}, %{reason: reason})
schedule_connect(@reconnect_initial) schedule_connect(@reconnect_initial)
{:noreply, %{state | socket: nil, backoff: @reconnect_initial}} {:noreply, %{state | socket: nil, backoff: @reconnect_initial}}
end end
@ -102,11 +102,11 @@ defmodule Aprs.AprsIsConnection do
end end
defp connect_aprs_is do defp connect_aprs_is do
host = Application.get_env(:aprs, :aprs_is_host, ~c"rotate.aprs2.net") host = Application.get_env(:aprsme, :aprsme_is_host, ~c"rotate.aprs2.net")
port = Application.get_env(:aprs, :aprs_is_port, 14_580) port = Application.get_env(:aprsme, :aprsme_is_port, 14_580)
callsign = Application.get_env(:aprs, :aprs_is_callsign, "N0CALL") callsign = Application.get_env(:aprsme, :aprsme_is_callsign, "N0CALL")
passcode = Application.get_env(:aprs, :aprs_is_passcode, "00000") passcode = Application.get_env(:aprsme, :aprsme_is_passcode, "00000")
filter = Application.get_env(:aprs, :aprs_is_filter, "") filter = Application.get_env(:aprsme, :aprsme_is_filter, "")
opts = [:binary, active: true, packet: :line, keepalive: true] opts = [:binary, active: true, packet: :line, keepalive: true]

View file

@ -1,13 +1,13 @@
defmodule Aprs.Archiver do defmodule Aprsme.Archiver do
@moduledoc false @moduledoc false
use GenServer use GenServer
alias AprsWeb.Endpoint alias AprsmeWeb.Endpoint
require Jason require Jason
require Logger require Logger
# alias Aprs.{Packet, Repo} # alias Aprsme.{Packet, Repo}
@topic "call" @topic "call"
# API # API

View file

@ -1,4 +1,4 @@
defmodule Aprs.BadPacket do defmodule Aprsme.BadPacket do
@moduledoc false @moduledoc false
use Ecto.Schema use Ecto.Schema
@ -17,7 +17,7 @@ defmodule Aprs.BadPacket do
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@doc false @doc false
@spec changeset(Aprs.BadPacket.t(), map()) :: Ecto.Changeset.t() @spec changeset(Aprsme.BadPacket.t(), map()) :: Ecto.Changeset.t()
def changeset(bad_packet, attrs) do def changeset(bad_packet, attrs) do
bad_packet bad_packet
|> cast(attrs, [:raw_packet, :error_message, :error_type, :attempted_at]) |> cast(attrs, [:raw_packet, :error_message, :error_type, :attempted_at])

View file

@ -1,4 +1,4 @@
defmodule Aprs.Convert do defmodule Aprsme.Convert do
@moduledoc false @moduledoc false
@spec wind(number(), :ultimeter, :mph) :: float() @spec wind(number(), :ultimeter, :mph) :: float()

View file

@ -1,13 +1,13 @@
defmodule Aprs.DataExtended do defmodule Aprsme.DataExtended do
@moduledoc false @moduledoc false
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias Aprs.DataExtended alias Aprsme.DataExtended
embedded_schema do embedded_schema do
field :aprs_messaging, :boolean, default: false field :aprsme_messaging, :boolean, default: false
field :comment, :string field :comment, :string
field :data_type, :string field :data_type, :string
field :latitude, :decimal field :latitude, :decimal
@ -19,11 +19,11 @@ defmodule Aprs.DataExtended do
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@doc false @doc false
@spec changeset(Aprs.DataExtended.t(), map()) :: Ecto.Changeset.t() @spec changeset(Aprsme.DataExtended.t(), map()) :: Ecto.Changeset.t()
def changeset(%DataExtended{} = data_extended, attrs) do def changeset(%DataExtended{} = data_extended, attrs) do
data_extended data_extended
|> cast(attrs, [ |> cast(attrs, [
:aprs_messaging, :aprsme_messaging,
:comment, :comment,
:data_type, :data_type,
:latitude, :latitude,
@ -32,7 +32,7 @@ defmodule Aprs.DataExtended do
:symbol_table_id :symbol_table_id
]) ])
|> validate_required([ |> validate_required([
:aprs_messaging, :aprsme_messaging,
:comment, :comment,
:data_type, :data_type,
:symbol_code, :symbol_code,

View file

@ -1,12 +1,12 @@
defmodule Aprs.DeviceIdentification do defmodule Aprsme.DeviceIdentification do
@moduledoc """ @moduledoc """
Handles APRS device identification based on the APRS device identification database. Handles APRS device identification based on the APRS device identification database.
""" """
import Ecto.Query import Ecto.Query
alias Aprs.Devices alias Aprsme.Devices
alias Aprs.Repo alias Aprsme.Repo
@device_patterns [ @device_patterns [
{~r/^ \x00\x00$/, "Original MIC-E"}, {~r/^ \x00\x00$/, "Original MIC-E"},
@ -41,13 +41,13 @@ defmodule Aprs.DeviceIdentification do
## Examples ## Examples
iex> Aprs.DeviceIdentification.identify_device(">" <> <<0>> <> "^") iex> Aprsme.DeviceIdentification.identify_device(">" <> <<0>> <> "^")
"Kenwood TH-D74" "Kenwood TH-D74"
iex> Aprs.DeviceIdentification.identify_device("`_#") iex> Aprsme.DeviceIdentification.identify_device("`_#")
"Yaesu VX-8G" "Yaesu VX-8G"
iex> Aprs.DeviceIdentification.identify_device("not-a-match") iex> Aprsme.DeviceIdentification.identify_device("not-a-match")
"Unknown" "Unknown"
""" """
@spec identify_device(String.t()) :: String.t() @spec identify_device(String.t()) :: String.t()
@ -64,10 +64,10 @@ defmodule Aprs.DeviceIdentification do
## Examples ## Examples
iex> "Kenwood" in Aprs.DeviceIdentification.known_manufacturers() iex> "Kenwood" in Aprsme.DeviceIdentification.known_manufacturers()
true true
iex> Enum.member?(Aprs.DeviceIdentification.known_manufacturers(), "AP510") iex> Enum.member?(Aprsme.DeviceIdentification.known_manufacturers(), "AP510")
true true
""" """
@spec known_manufacturers() :: [String.t()] @spec known_manufacturers() :: [String.t()]
@ -93,10 +93,10 @@ defmodule Aprs.DeviceIdentification do
## Examples ## Examples
iex> Aprs.DeviceIdentification.known_models("Kenwood") iex> Aprsme.DeviceIdentification.known_models("Kenwood")
["TH-D74", "TH-D74A", "DM-710", "DM-700"] ["TH-D74", "TH-D74A", "DM-710", "DM-700"]
iex> Aprs.DeviceIdentification.known_models("Unknown") iex> Aprsme.DeviceIdentification.known_models("Unknown")
[] []
""" """
@spec known_models(String.t()) :: [String.t()] @spec known_models(String.t()) :: [String.t()]
@ -131,7 +131,7 @@ defmodule Aprs.DeviceIdentification do
def fetch_and_upsert_devices do def fetch_and_upsert_devices do
req = Finch.build(:get, @url) req = Finch.build(:get, @url)
case Finch.request(req, Aprs.Finch) do case Finch.request(req, Aprsme.Finch) do
{:ok, %Finch.Response{status: 200, body: body}} -> {:ok, %Finch.Response{status: 200, body: body}} ->
{:ok, json} = Jason.decode(body) {:ok, json} = Jason.decode(body)
upsert_devices(json) upsert_devices(json)
@ -170,7 +170,7 @@ defmodule Aprs.DeviceIdentification do
# Helper to enqueue the job # Helper to enqueue the job
def enqueue_refresh_job do def enqueue_refresh_job do
Oban.insert!(Aprs.DeviceIdentification.Worker.new(%{})) Oban.insert!(Aprsme.DeviceIdentification.Worker.new(%{}))
end end
@doc """ @doc """
@ -179,6 +179,8 @@ defmodule Aprs.DeviceIdentification do
Note: This function requires the devices table to be seeded and a running Repo context. Note: This function requires the devices table to be seeded and a running Repo context.
""" """
def lookup_device_by_identifier(nil), do: nil
def lookup_device_by_identifier(identifier) when is_binary(identifier) do def lookup_device_by_identifier(identifier) when is_binary(identifier) do
# Fetch all device patterns from DB # Fetch all device patterns from DB
devices = Repo.all(Devices) devices = Repo.all(Devices)
@ -218,13 +220,13 @@ defmodule Aprs.DeviceIdentification do
end end
end end
defmodule Aprs.DeviceIdentification.Worker do defmodule Aprsme.DeviceIdentification.Worker do
@moduledoc false @moduledoc false
use Oban.Worker, queue: :default, max_attempts: 1 use Oban.Worker, queue: :default, max_attempts: 1
@impl true @impl true
def perform(_job) do def perform(_job) do
Aprs.DeviceIdentification.maybe_refresh_devices() Aprsme.DeviceIdentification.maybe_refresh_devices()
:ok :ok
end end
end end

View file

@ -0,0 +1,32 @@
defmodule Aprsme.DeviceParser do
@moduledoc """
Extracts device identifiers from APRS packet data.
"""
@doc """
Extracts a device identifier from packet data.
Returns the device identifier string or nil if not found.
"""
@spec extract_device_identifier(map()) :: String.t() | nil
def extract_device_identifier(packet_data) do
# Try to extract from various possible locations
packet_data
|> Map.get(:device_identifier)
|> case do
nil -> extract_from_data_extended(packet_data)
identifier -> identifier
end
end
defp extract_from_data_extended(packet_data) do
data_extended = Map.get(packet_data, :data_extended) || %{}
# Try to extract from symbol information
symbol_table_id = Map.get(data_extended, :symbol_table_id) || Map.get(data_extended, "symbol_table_id")
symbol_code = Map.get(data_extended, :symbol_code) || Map.get(data_extended, "symbol_code")
if symbol_table_id && symbol_code do
"#{symbol_table_id}#{symbol_code}"
end
end
end

View file

@ -1,4 +1,4 @@
defmodule Aprs.Devices do defmodule Aprsme.Devices do
@moduledoc false @moduledoc false
use Ecto.Schema use Ecto.Schema

View file

@ -1,9 +1,9 @@
# test/support/devices_seeder.exs defmodule Aprsme.DevicesSeeder do
alias Aprs.Devices
alias Aprs.Repo
defmodule DevicesSeeder do
@moduledoc false @moduledoc false
alias Aprsme.Devices
alias Aprsme.Repo
def seed_from_json(path \\ "test/support/test_devices.json") do def seed_from_json(path \\ "test/support/test_devices.json") do
{:ok, body} = File.read(path) {:ok, body} = File.read(path)
{:ok, json} = Jason.decode(body) {:ok, json} = Jason.decode(body)

View file

@ -1,4 +1,4 @@
defmodule Aprs.EncodingUtils do defmodule Aprsme.EncodingUtils do
@moduledoc """ @moduledoc """
Utilities for handling encoding issues in APRS packet data. Utilities for handling encoding issues in APRS packet data.
@ -7,7 +7,7 @@ defmodule Aprs.EncodingUtils do
to web clients. to web clients.
""" """
alias Parser.Types.MicE alias Aprs.Types.MicE
@doc """ @doc """
Sanitizes a binary to ensure it can be safely JSON encoded. Sanitizes a binary to ensure it can be safely JSON encoded.
@ -18,10 +18,10 @@ defmodule Aprs.EncodingUtils do
## Examples ## Examples
iex> Aprs.EncodingUtils.sanitize_string("Hello World") iex> Aprsme.EncodingUtils.sanitize_string("Hello World")
"Hello World" "Hello World"
iex> Aprs.EncodingUtils.sanitize_string(<<72, 101, 108, 108, 111, 211, 87, 111, 114, 108, 100>>) iex> Aprsme.EncodingUtils.sanitize_string(<<72, 101, 108, 108, 111, 211, 87, 111, 114, 108, 100>>)
"HelloÓWorld" "HelloÓWorld"
""" """
@spec sanitize_string(binary() | nil | any()) :: binary() | nil | any() @spec sanitize_string(binary() | nil | any()) :: binary() | nil | any()
@ -49,7 +49,7 @@ defmodule Aprs.EncodingUtils do
Sanitizes all string fields in an APRS packet to ensure safe JSON encoding. Sanitizes all string fields in an APRS packet to ensure safe JSON encoding.
""" """
@spec sanitize_packet(struct() | map()) :: struct() | map() @spec sanitize_packet(struct() | map()) :: struct() | map()
def sanitize_packet(%Aprs.Packet{} = packet) do def sanitize_packet(%Aprsme.Packet{} = packet) do
%{ %{
packet packet
| information_field: sanitize_string(packet.information_field), | information_field: sanitize_string(packet.information_field),
@ -97,7 +97,7 @@ defmodule Aprs.EncodingUtils do
## Examples ## Examples
iex> Aprs.EncodingUtils.to_hex(<<72, 101, 108, 108, 111>>) iex> Aprsme.EncodingUtils.to_hex(<<72, 101, 108, 108, 111>>)
"48656C6C6F" "48656C6C6F"
""" """
@spec to_hex(binary()) :: String.t() @spec to_hex(binary()) :: String.t()
@ -113,10 +113,10 @@ defmodule Aprs.EncodingUtils do
## Examples ## Examples
iex> Aprs.EncodingUtils.encoding_info("Hello") iex> Aprsme.EncodingUtils.encoding_info("Hello")
%{valid_utf8: true, byte_count: 5, char_count: 5} %{valid_utf8: true, byte_count: 5, char_count: 5}
iex> Aprs.EncodingUtils.encoding_info(<<72, 101, 211, 108, 111>>) iex> Aprsme.EncodingUtils.encoding_info(<<72, 101, 211, 108, 111>>)
%{valid_utf8: false, byte_count: 5, invalid_at: 2} %{valid_utf8: false, byte_count: 5, invalid_at: 2}
""" """
@spec encoding_info(binary()) :: map() @spec encoding_info(binary()) :: map()

View file

@ -1,4 +1,4 @@
defmodule Aprs.Is do defmodule Aprsme.Is do
@moduledoc false @moduledoc false
use GenServer use GenServer
@ -29,8 +29,8 @@ defmodule Aprs.Is do
@impl true @impl true
def init(_opts) do def init(_opts) do
env = Application.get_env(:aprs, :env) env = Application.get_env(:aprsme, :env)
disable_connection = Application.get_env(:aprs, :disable_aprs_connection, false) disable_connection = Application.get_env(:aprsme, :disable_aprs_connection, false)
do_init(env, disable_connection) do_init(env, disable_connection)
end end
@ -53,11 +53,11 @@ defmodule Aprs.Is do
Process.sleep(2000) Process.sleep(2000)
# Get startup parameters # Get startup parameters
server = Application.get_env(:aprs, :aprs_is_server, ~c"rotate.aprs2.net") server = Application.get_env(:aprsme, :aprsme_is_server, ~c"rotate.aprs2.net")
port = Application.get_env(:aprs, :aprs_is_port, 14_580) port = Application.get_env(:aprsme, :aprsme_is_port, 14_580)
default_filter = Application.get_env(:aprs, :aprs_is_default_filter, "r/33/-96/100") default_filter = Application.get_env(:aprsme, :aprsme_is_default_filter, "r/33/-96/100")
aprs_user_id = Application.get_env(:aprs, :aprs_is_login_id, "W5ISP") aprs_user_id = Application.get_env(:aprsme, :aprsme_is_login_id, "W5ISP")
aprs_passcode = Application.get_env(:aprs, :aprs_is_password, "-1") aprs_passcode = Application.get_env(:aprsme, :aprsme_is_password, "-1")
# Record connection start time # Record connection start time
connected_at = DateTime.utc_now() connected_at = DateTime.utc_now()
@ -95,7 +95,7 @@ defmodule Aprs.Is do
else else
_ -> _ ->
Logger.error("Unable to establish connection or log in to APRS-IS") Logger.error("Unable to establish connection or log in to APRS-IS")
{:stop, :aprs_connection_failed} {:stop, :aprsme_connection_failed}
end end
end end
@ -110,8 +110,8 @@ defmodule Aprs.Is do
case Process.whereis(__MODULE__) do case Process.whereis(__MODULE__) do
nil -> nil ->
# GenServer is not running (disconnected) # GenServer is not running (disconnected)
server = Application.get_env(:aprs, :aprs_is_server, ~c"rotate.aprs2.net") server = Application.get_env(:aprsme, :aprsme_is_server, nil)
port = Application.get_env(:aprs, :aprs_is_port, 14_580) port = Application.get_env(:aprsme, :aprsme_is_port, 14_580)
%{ %{
connected: false, connected: false,
@ -119,10 +119,10 @@ defmodule Aprs.Is do
port: port, port: port,
connected_at: nil, connected_at: nil,
uptime_seconds: 0, uptime_seconds: 0,
login_id: Application.get_env(:aprs, :aprs_is_login_id, "W5ISP"), login_id: Application.get_env(:aprsme, :aprsme_is_login_id, "W5ISP"),
filter: Application.get_env(:aprs, :aprs_is_default_filter, "r/33/-96/100"), filter: Application.get_env(:aprsme, :aprsme_is_default_filter, "r/33/-96/100"),
packet_stats: default_packet_stats(), packet_stats: default_packet_stats(),
stored_packet_count: Aprs.Packets.get_total_packet_count() stored_packet_count: Aprsme.Packets.get_total_packet_count()
} }
_pid -> _pid ->
@ -131,8 +131,8 @@ defmodule Aprs.Is do
catch catch
:exit, _ -> :exit, _ ->
# GenServer exists but not responding # GenServer exists but not responding
server = Application.get_env(:aprs, :aprs_is_server, ~c"rotate.aprs2.net") server = Application.get_env(:aprsme, :aprsme_is_server, ~c"rotate.aprs2.net")
port = Application.get_env(:aprs, :aprs_is_port, 14_580) port = Application.get_env(:aprsme, :aprsme_is_port, 14_580)
%{ %{
connected: false, connected: false,
@ -140,10 +140,10 @@ defmodule Aprs.Is do
port: port, port: port,
connected_at: nil, connected_at: nil,
uptime_seconds: 0, uptime_seconds: 0,
login_id: Application.get_env(:aprs, :aprs_is_login_id, "W5ISP"), login_id: Application.get_env(:aprsme, :aprsme_is_login_id, "W5ISP"),
filter: Application.get_env(:aprs, :aprs_is_default_filter, "r/33/-96/100"), filter: Application.get_env(:aprsme, :aprsme_is_default_filter, "r/33/-96/100"),
packet_stats: default_packet_stats(), packet_stats: default_packet_stats(),
stored_packet_count: Aprs.Packets.get_total_packet_count() stored_packet_count: Aprsme.Packets.get_total_packet_count()
} }
end end
end end
@ -167,8 +167,8 @@ 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
env = Application.get_env(:aprs, :env) env = Application.get_env(:aprsme, :env)
disable_connection = Application.get_env(:aprs, :disable_aprs_connection, false) disable_connection = Application.get_env(:aprsme, :disable_aprs_connection, false)
do_connect_to_aprs_is(server, port, env, disable_connection) do_connect_to_aprs_is(server, port, env, disable_connection)
end end
@ -186,7 +186,8 @@ defmodule Aprs.Is do
defp do_connect_to_aprs_is(server, port, _env, false) do defp do_connect_to_aprs_is(server, port, _env, false) do
Logger.debug("Connecting to: #{server}:#{port}") Logger.debug("Connecting to: #{server}:#{port}")
opts = [:binary, active: true] opts = [:binary, active: true]
:gen_tcp.connect(String.to_charlist(server), port, opts) server_string = if is_list(server), do: List.to_string(server), else: server
:gen_tcp.connect(String.to_charlist(server_string), 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()) ::
@ -202,7 +203,7 @@ defmodule Aprs.Is do
@spec create_timer(non_neg_integer()) :: reference() @spec create_timer(non_neg_integer()) :: reference()
defp create_timer(timeout) do defp create_timer(timeout) do
Process.send_after(self(), :aprs_no_message_timeout, timeout) Process.send_after(self(), :aprsme_no_message_timeout, timeout)
end end
@spec create_keepalive_timer(non_neg_integer()) :: reference() @spec create_keepalive_timer(non_neg_integer()) :: reference()
@ -212,7 +213,7 @@ defmodule Aprs.Is do
@impl true @impl true
def handle_call({:send_message, message}, _from, state) do def handle_call({:send_message, message}, _from, state) do
next_ack_number = :ets.update_counter(:aprs, :message_number, 1) next_ack_number = :ets.update_counter(:aprsme, :message_number, 1)
# Append ack number # Append ack number
message = message <> "{" <> to_string(next_ack_number) <> "\r" message = message <> "{" <> to_string(next_ack_number) <> "\r"
@ -231,16 +232,16 @@ defmodule Aprs.Is do
login_id: state.login_params.user_id, login_id: state.login_params.user_id,
filter: state.login_params.filter, filter: state.login_params.filter,
packet_stats: state.packet_stats, packet_stats: state.packet_stats,
stored_packet_count: Aprs.Packets.get_total_packet_count() stored_packet_count: Aprsme.Packets.get_total_packet_count()
} }
{:reply, status, state} {:reply, status, state}
end end
@impl true @impl true
def handle_info(:aprs_no_message_timeout, state) do def handle_info(:aprsme_no_message_timeout, state) do
Logger.error("Socket timeout detected. Killing genserver.") Logger.error("Socket timeout detected. Killing genserver.")
{:stop, :aprs_timeout, state} {:stop, :aprsme_timeout, state}
end end
def handle_info(:send_keepalive, state) do def handle_info(:send_keepalive, state) do
@ -283,7 +284,7 @@ defmodule Aprs.Is do
end) end)
# Start a new timer # Start a new timer
timer = Process.send_after(self(), :aprs_no_message_timeout, @aprs_timeout) timer = Process.send_after(self(), :aprsme_no_message_timeout, @aprs_timeout)
state = state =
state state
@ -318,7 +319,7 @@ defmodule Aprs.Is do
end) end)
# Start a new timer # Start a new timer
timer = Process.send_after(self(), :aprs_no_message_timeout, @aprs_timeout) timer = Process.send_after(self(), :aprsme_no_message_timeout, @aprs_timeout)
state = state =
state state
@ -408,7 +409,7 @@ defmodule Aprs.Is do
def dispatch(""), do: nil def dispatch(""), do: nil
def dispatch(message) do def dispatch(message) do
case Parser.parse(message) do case Aprs.parse(message) do
{:ok, parsed_message} -> {:ok, parsed_message} ->
# Store the packet in the database for future replay # Store the packet in the database for future replay
# Use Task to avoid slowing down the main process # Use Task to avoid slowing down the main process
@ -424,13 +425,13 @@ defmodule Aprs.Is do
attrs = struct_to_map(packet_data) attrs = struct_to_map(packet_data)
# Extract additional data from the parsed packet including raw packet # Extract additional data from the parsed packet including raw packet
attrs = Aprs.Packet.extract_additional_data(attrs, message) attrs = Aprsme.Packet.extract_additional_data(attrs, message)
# Normalize data_type to string if it's an atom # Normalize data_type to string if it's an atom
attrs = normalize_data_type(attrs) attrs = normalize_data_type(attrs)
# Store in database through the Packets context # Store in database through the Packets context
case Aprs.Packets.store_packet(attrs) do case Aprsme.Packets.store_packet(attrs) do
{:ok, _packet} -> {:ok, _packet} ->
# Packet stored successfully # Packet stored successfully
:ok :ok
@ -455,19 +456,19 @@ defmodule Aprs.Is do
end) end)
# Broadcast to live clients # Broadcast to live clients
AprsWeb.Endpoint.broadcast("aprs_messages", "packet", parsed_message) AprsmeWeb.Endpoint.broadcast("aprs_messages", "packet", parsed_message)
{:error, :invalid_packet} -> {:error, :invalid_packet} ->
Logger.debug("PARSE ERROR: invalid packet") Logger.debug("PARSE ERROR: invalid packet")
Aprs.Packets.store_bad_packet(message, %{ Aprsme.Packets.store_bad_packet(message, %{
message: "Invalid packet format", message: "Invalid packet format",
type: "ParseError" type: "ParseError"
}) })
{:error, error} -> {:error, error} ->
Logger.debug("PARSE ERROR: " <> error) Logger.debug("PARSE ERROR: " <> error)
Aprs.Packets.store_bad_packet(message, %{message: error, type: "ParseError"}) Aprsme.Packets.store_bad_packet(message, %{message: error, type: "ParseError"})
end end
end end

View file

@ -1,4 +1,4 @@
defmodule Aprs.Is.IsSupervisor do defmodule Aprsme.Is.IsSupervisor do
@moduledoc false @moduledoc false
use Supervisor use Supervisor
@ -9,7 +9,7 @@ defmodule Aprs.Is.IsSupervisor do
@impl true @impl true
def init(:ok) do def init(:ok) do
children = [ children = [
{Aprs.Is, []} {Aprsme.Is, []}
] ]
# Supervisor will restart children max 3 times in 5 seconds # Supervisor will restart children max 3 times in 5 seconds

4
lib/aprsme/mailer.ex Normal file
View file

@ -0,0 +1,4 @@
defmodule Aprsme.Mailer do
@moduledoc false
use Swoosh.Mailer, otp_app: :aprsme
end

View file

@ -1,11 +1,11 @@
defmodule Aprs.Packet do defmodule Aprsme.Packet do
@moduledoc false @moduledoc false
use Aprs.Schema use Aprsme.Schema
import Ecto.Changeset import Ecto.Changeset
alias Aprs.DataExtended alias Aprs.Types.MicE
alias Parser.Types.MicE alias Aprsme.DataExtended
schema "packets" do schema "packets" do
field(:base_callsign, :string) field(:base_callsign, :string)
@ -30,7 +30,7 @@ defmodule Aprs.Packet do
# Additional packet data # Additional packet data
field(:comment, :string) field(:comment, :string)
field(:timestamp, :string) field(:timestamp, :string)
field(:aprs_messaging, :boolean, default: false) field(:aprsme_messaging, :boolean, default: false)
# Weather data # Weather data
field(:temperature, :float) field(:temperature, :float)
@ -65,7 +65,7 @@ defmodule Aprs.Packet do
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@doc false @doc false
@spec changeset(Aprs.Packet.t(), map()) :: Ecto.Changeset.t() @spec changeset(Aprsme.Packet.t(), map()) :: Ecto.Changeset.t()
def changeset(packet, attrs) do def changeset(packet, attrs) do
# Convert atom data_type to string # Convert atom data_type to string
attrs = normalize_data_type(attrs) attrs = normalize_data_type(attrs)
@ -90,7 +90,7 @@ defmodule Aprs.Packet do
:symbol_table_id, :symbol_table_id,
:comment, :comment,
:timestamp, :timestamp,
:aprs_messaging, :aprsme_messaging,
:temperature, :temperature,
:humidity, :humidity,
:wind_speed, :wind_speed,
@ -277,8 +277,8 @@ defmodule Aprs.Packet do
|> maybe_put(:comment, data_extended[:comment] || data_extended["comment"]) |> maybe_put(:comment, data_extended[:comment] || data_extended["comment"])
|> maybe_put(:timestamp, data_extended[:timestamp] || data_extended["timestamp"]) |> maybe_put(:timestamp, data_extended[:timestamp] || data_extended["timestamp"])
|> maybe_put( |> maybe_put(
:aprs_messaging, :aprsme_messaging,
data_extended[:aprs_messaging?] || data_extended["aprs_messaging?"] data_extended[:aprsme_messaging?] || data_extended["aprs_messaging?"]
) )
end end
@ -519,14 +519,14 @@ defmodule Aprs.Packet do
@doc """ @doc """
Get latitude from a packet's location geometry. Get latitude from a packet's location geometry.
""" """
@spec lat(Aprs.Packet.t()) :: number() | nil @spec lat(Aprsme.Packet.t()) :: number() | nil
def lat(%__MODULE__{location: %Geo.Point{coordinates: {_lon, lat}}}), do: lat def lat(%__MODULE__{location: %Geo.Point{coordinates: {_lon, lat}}}), do: lat
def lat(_), do: nil def lat(_), do: nil
@doc """ @doc """
Get longitude from a packet's location geometry. Get longitude from a packet's location geometry.
""" """
@spec lon(Aprs.Packet.t()) :: number() | nil @spec lon(Aprsme.Packet.t()) :: number() | nil
def lon(%__MODULE__{location: %Geo.Point{coordinates: {lon, _lat}}}), do: lon def lon(%__MODULE__{location: %Geo.Point{coordinates: {lon, _lat}}}), do: lon
def lon(_), do: nil def lon(_), do: nil

View file

@ -1,4 +1,4 @@
defmodule Aprs.PacketReplay do defmodule Aprsme.PacketReplay do
@moduledoc """ @moduledoc """
Handles replaying of historical APRS packets alongside live packets. Handles replaying of historical APRS packets alongside live packets.
@ -11,8 +11,8 @@ defmodule Aprs.PacketReplay do
use GenServer use GenServer
alias Aprs.Packets alias Aprsme.Packets
alias AprsWeb.Endpoint alias AprsmeWeb.Endpoint
require Logger require Logger
@ -393,7 +393,7 @@ defmodule Aprs.PacketReplay do
@spec via_tuple(String.t()) :: {:via, Registry, {atom(), String.t()}} @spec via_tuple(String.t()) :: {:via, Registry, {atom(), String.t()}}
defp via_tuple(user_id) do defp via_tuple(user_id) do
{:via, Registry, {Aprs.ReplayRegistry, "replay:#{user_id}"}} {:via, Registry, {Aprsme.ReplayRegistry, "replay:#{user_id}"}}
end end
@spec sanitize_packet_for_transport(any()) :: map() @spec sanitize_packet_for_transport(any()) :: map()

View file

@ -1,10 +1,10 @@
defmodule Aprs.PacketReplayBehaviour do defmodule Aprsme.PacketReplayBehaviour do
@moduledoc """ @moduledoc """
Behavior definition for the PacketReplay module. Behavior definition for the PacketReplay module.
This allows us to mock the PacketReplay module in tests. This allows us to mock the PacketReplay module in tests.
""" """
@callback start_replay(pid(), list(Aprs.Packet.t()), keyword()) :: {:ok, reference()} | {:error, term()} @callback start_replay(pid(), list(Aprsme.Packet.t()), keyword()) :: {:ok, reference()} | {:error, term()}
@callback stop_replay(reference()) :: :ok @callback stop_replay(reference()) :: :ok
@callback pause_replay(reference()) :: :ok | {:error, :not_found} @callback pause_replay(reference()) :: :ok | {:error, :not_found}
@callback resume_replay(reference()) :: :ok | {:error, :not_found} @callback resume_replay(reference()) :: :ok | {:error, :not_found}

View file

@ -1,16 +1,16 @@
defmodule Aprs.Packets do defmodule Aprsme.Packets do
@moduledoc """ @moduledoc """
The Packets context. The Packets context.
""" """
@behaviour Aprs.PacketsBehaviour @behaviour Aprsme.PacketsBehaviour
import Ecto.Query, warn: false import Ecto.Query, warn: false
alias Aprs.BadPacket alias Aprs.Types.MicE
alias Aprs.Packet alias Aprsme.BadPacket
alias Aprs.Repo alias Aprsme.Packet
alias Parser.Types.MicE alias Aprsme.Repo
@doc """ @doc """
Stores a packet in the database. Stores a packet in the database.
@ -34,8 +34,8 @@ defmodule Aprs.Packets do
end) end)
|> normalize_ssid() |> normalize_ssid()
|> then(fn attrs -> |> then(fn attrs ->
device_identifier = Parser.DeviceParser.extract_device_identifier(packet_data) device_identifier = Aprsme.DeviceParser.extract_device_identifier(packet_data)
matched_device = Aprs.DeviceIdentification.lookup_device_by_identifier(device_identifier) matched_device = Aprsme.DeviceIdentification.lookup_device_by_identifier(device_identifier)
canonical_identifier = if matched_device, do: matched_device.identifier, else: device_identifier canonical_identifier = if matched_device, do: matched_device.identifier, else: device_identifier
Map.put(attrs, :device_identifier, canonical_identifier) Map.put(attrs, :device_identifier, canonical_identifier)
end) end)
@ -460,7 +460,7 @@ defmodule Aprs.Packets do
""" """
@impl true @impl true
def clean_old_packets do def clean_old_packets do
retention_days = Application.get_env(:aprs, :packet_retention_days, 365) retention_days = Application.get_env(:aprsme, :packet_retention_days, 365)
cutoff_time = DateTime.add(DateTime.utc_now(), -retention_days * 86_400, :second) cutoff_time = DateTime.add(DateTime.utc_now(), -retention_days * 86_400, :second)
{deleted_count, _} = Repo.delete_all(from(p in Packet, where: p.received_at < ^cutoff_time)) {deleted_count, _} = Repo.delete_all(from(p in Packet, where: p.received_at < ^cutoff_time))
@ -533,7 +533,7 @@ defmodule Aprs.Packets do
defp sanitize_packet_strings(list) when is_list(list), do: Enum.map(list, &sanitize_packet_strings/1) defp sanitize_packet_strings(list) when is_list(list), do: Enum.map(list, &sanitize_packet_strings/1)
defp sanitize_packet_strings(binary) when is_binary(binary) do defp sanitize_packet_strings(binary) when is_binary(binary) do
s = Aprs.EncodingUtils.sanitize_string(binary) s = Aprsme.EncodingUtils.sanitize_string(binary)
if is_binary(s), do: s, else: "" if is_binary(s), do: s, else: ""
end end

View file

@ -1,4 +1,4 @@
defmodule Aprs.PacketsBehaviour do defmodule Aprsme.PacketsBehaviour do
@moduledoc """ @moduledoc """
Behaviour for Packets module to allow mocking in tests Behaviour for Packets module to allow mocking in tests
""" """

View file

@ -1,4 +1,4 @@
defmodule Aprs.Passcode do defmodule Aprsme.Passcode do
@moduledoc """ @moduledoc """
Module for generating APRS passcodes from callsigns. Module for generating APRS passcodes from callsigns.
The passcode is a hash of the callsign used for authentication with APRS-IS servers. The passcode is a hash of the callsign used for authentication with APRS-IS servers.
@ -14,7 +14,7 @@ defmodule Aprs.Passcode do
- The generated passcode as an integer - The generated passcode as an integer
## Examples ## Examples
iex> Aprs.Passcode.generate("W5ISP") iex> Aprsme.Passcode.generate("W5ISP")
15748 15748
""" """
@spec generate(String.t()) :: non_neg_integer() @spec generate(String.t()) :: non_neg_integer()

View file

@ -1,4 +1,4 @@
defmodule Aprs.PostgresNotifier do defmodule Aprsme.PostgresNotifier do
@moduledoc """ @moduledoc """
Listens to PostgreSQL NOTIFY events on the "aprs_events" and "aprs_packets" channels and broadcasts Listens to PostgreSQL NOTIFY events on the "aprs_events" and "aprs_packets" channels and broadcasts
them via Phoenix.PubSub for reactive, event-driven updates. them via Phoenix.PubSub for reactive, event-driven updates.
@ -6,9 +6,9 @@ defmodule Aprs.PostgresNotifier do
use GenServer use GenServer
@event_channel "aprs_events" @event_channel "aprs_events"
@event_topic "postgres:aprs_events" @event_topic "postgres:aprsme_events"
@packet_channel "aprs_packets" @packet_channel "aprs_packets"
@packet_topic "postgres:aprs_packets" @packet_topic "postgres:aprsme_packets"
def start_link(_opts) do def start_link(_opts) do
GenServer.start_link(__MODULE__, nil, name: __MODULE__) GenServer.start_link(__MODULE__, nil, name: __MODULE__)
@ -16,7 +16,7 @@ defmodule Aprs.PostgresNotifier do
@impl true @impl true
def init(_) do def init(_) do
{:ok, conn} = Postgrex.Notifications.start_link(Aprs.Repo.config()) {:ok, conn} = Postgrex.Notifications.start_link(Aprsme.Repo.config())
{:ok, _ref1} = Postgrex.Notifications.listen(conn, @event_channel) {:ok, _ref1} = Postgrex.Notifications.listen(conn, @event_channel)
{:ok, _ref2} = Postgrex.Notifications.listen(conn, @packet_channel) {:ok, _ref2} = Postgrex.Notifications.listen(conn, @packet_channel)
{:ok, %{conn: conn}} {:ok, %{conn: conn}}
@ -24,14 +24,14 @@ defmodule Aprs.PostgresNotifier do
@impl true @impl true
def handle_info({:notification, _conn, _pid, @event_channel, payload}, state) do def handle_info({:notification, _conn, _pid, @event_channel, payload}, state) do
Phoenix.PubSub.broadcast(Aprs.PubSub, @event_topic, {:postgres_notify, payload}) Phoenix.PubSub.broadcast(Aprsme.PubSub, @event_topic, {:postgres_notify, payload})
{:noreply, state} {:noreply, state}
end end
def handle_info({:notification, _conn, _pid, @packet_channel, payload}, state) do def handle_info({:notification, _conn, _pid, @packet_channel, payload}, state) do
case Jason.decode(payload) do case Jason.decode(payload) do
{:ok, packet} -> {:ok, packet} ->
Phoenix.PubSub.broadcast(Aprs.PubSub, @packet_topic, {:postgres_packet, packet}) Phoenix.PubSub.broadcast(Aprsme.PubSub, @packet_topic, {:postgres_packet, packet})
_ -> _ ->
:noop :noop

View file

@ -1,5 +1,5 @@
Postgrex.Types.define( Postgrex.Types.define(
Aprs.PostgresTypes, Aprsme.PostgresTypes,
[Geo.PostGIS.Extension] ++ Ecto.Adapters.Postgres.extensions(), [Geo.PostGIS.Extension] ++ Ecto.Adapters.Postgres.extensions(),
json: Jason json: Jason
) )

4
lib/aprsme/presence.ex Normal file
View file

@ -0,0 +1,4 @@
defmodule Aprsme.Presence do
@moduledoc false
use Phoenix.Presence, otp_app: :aprsme, pubsub_server: Aprsme.PubSub
end

View file

@ -1,9 +1,9 @@
defmodule Aprs.Release do defmodule Aprsme.Release do
@moduledoc """ @moduledoc """
Used for executing DB release tasks when run in production without Mix Used for executing DB release tasks when run in production without Mix
installed. installed.
""" """
@app :aprs @app :aprsme
def migrate do def migrate do
require Logger require Logger
@ -12,7 +12,7 @@ defmodule Aprs.Release do
Logger.info("Deployment timestamp: #{deployed_at}") Logger.info("Deployment timestamp: #{deployed_at}")
# Run migrations # Run migrations
{:ok, _, _} = Ecto.Migrator.with_repo(Aprs.Repo, &Ecto.Migrator.run(&1, :up, all: true)) {:ok, _, _} = Ecto.Migrator.with_repo(Aprsme.Repo, &Ecto.Migrator.run(&1, :up, all: true))
end end
def rollback(repo, version) do def rollback(repo, version) do
@ -33,7 +33,7 @@ defmodule Aprs.Release do
deployed_at = read_deployment_timestamp() deployed_at = read_deployment_timestamp()
# Add to application config # Add to application config
Application.put_env(:aprs, :deployed_at, deployed_at) Application.put_env(:aprsme, :deployed_at, deployed_at)
deployed_at deployed_at
end end
@ -42,7 +42,7 @@ defmodule Aprs.Release do
Get the deployment timestamp. Get the deployment timestamp.
""" """
def deployed_at do def deployed_at do
Application.get_env(:aprs, :deployed_at) || DateTime.utc_now() Application.get_env(:aprsme, :deployed_at) || DateTime.utc_now()
end end
defp read_deployment_timestamp do defp read_deployment_timestamp do

View file

@ -1,5 +1,5 @@
defmodule Aprs.Repo do defmodule Aprsme.Repo do
use Ecto.Repo, use Ecto.Repo,
otp_app: :aprs, otp_app: :aprsme,
adapter: Ecto.Adapters.Postgres adapter: Ecto.Adapters.Postgres
end end

View file

@ -1,4 +1,4 @@
defmodule Aprs.Schema do defmodule Aprsme.Schema do
@moduledoc false @moduledoc false
defmacro __using__(_) do defmacro __using__(_) do
quote do quote do

View file

@ -1,4 +1,4 @@
defmodule Aprs.Workers.PacketCleanupWorker do defmodule Aprsme.Workers.PacketCleanupWorker do
@moduledoc """ @moduledoc """
Oban worker for cleaning up old APRS packet data. Oban worker for cleaning up old APRS packet data.
@ -18,8 +18,8 @@ defmodule Aprs.Workers.PacketCleanupWorker do
# Import modules needed for database operations # Import modules needed for database operations
alias Aprs.Packet alias Aprsme.Packet
alias Aprs.Repo alias Aprsme.Repo
require Logger require Logger
@ -52,7 +52,7 @@ defmodule Aprs.Workers.PacketCleanupWorker do
deleted_count = cleanup_old_packets() deleted_count = cleanup_old_packets()
# Log results # Log results
retention_days = Application.get_env(:aprs, :packet_retention_days, 365) retention_days = Application.get_env(:aprsme, :packet_retention_days, 365)
Logger.info("APRS packet cleanup complete: removed #{deleted_count} packets older than #{retention_days} days") Logger.info("APRS packet cleanup complete: removed #{deleted_count} packets older than #{retention_days} days")
@ -92,6 +92,6 @@ defmodule Aprs.Workers.PacketCleanupWorker do
end end
defp packets_module do defp packets_module do
Application.get_env(:aprs, :packets_module, Aprs.Packets) Application.get_env(:aprsme, :packets_module, Aprsme.Packets)
end end
end end

View file

@ -1,4 +1,4 @@
defmodule AprsWeb.CoreComponents do defmodule AprsmeWeb.CoreComponents do
@moduledoc """ @moduledoc """
Provides core UI components. Provides core UI components.
@ -10,7 +10,7 @@ defmodule AprsWeb.CoreComponents do
[heroicons_elixir](https://github.com/mveytsman/heroicons_elixir) project. [heroicons_elixir](https://github.com/mveytsman/heroicons_elixir) project.
""" """
use Phoenix.Component use Phoenix.Component
use Gettext, backend: AprsWeb.Gettext use Gettext, backend: AprsmeWeb.Gettext
alias Phoenix.HTML.Form alias Phoenix.HTML.Form
alias Phoenix.LiveView.JS alias Phoenix.LiveView.JS
@ -620,9 +620,9 @@ defmodule AprsWeb.CoreComponents do
# should be written to the errors.po file. The :count option is # should be written to the errors.po file. The :count option is
# set by Ecto and indicates we should also apply plural rules. # set by Ecto and indicates we should also apply plural rules.
if count = opts[:count] do if count = opts[:count] do
Gettext.dngettext(AprsWeb.Gettext, "errors", msg, msg, count, opts) Gettext.dngettext(AprsmeWeb.Gettext, "errors", msg, msg, count, opts)
else else
Gettext.dgettext(AprsWeb.Gettext, "errors", msg, opts) Gettext.dgettext(AprsmeWeb.Gettext, "errors", msg, opts)
end end
end end

View file

@ -1,6 +1,6 @@
defmodule AprsWeb.Layouts do defmodule AprsmeWeb.Layouts do
@moduledoc false @moduledoc false
use AprsWeb, :html use AprsmeWeb, :html
embed_templates "layouts/*" embed_templates "layouts/*"

View file

@ -1,13 +1,13 @@
defmodule AprsWeb.Api.V1.CallsignController do defmodule AprsmeWeb.Api.V1.CallsignController do
@moduledoc """ @moduledoc """
API v1 controller for callsign-related endpoints. API v1 controller for callsign-related endpoints.
""" """
use AprsWeb, :controller use AprsmeWeb, :controller
alias Aprs.Packets alias Aprsme.Packets
alias AprsWeb.Api.V1.CallsignJSON alias AprsmeWeb.Api.V1.CallsignJSON
action_fallback AprsWeb.Api.V1.FallbackController action_fallback AprsmeWeb.Api.V1.FallbackController
@doc """ @doc """
Get the most recent packet for a given callsign. Get the most recent packet for a given callsign.

View file

@ -1,18 +1,18 @@
defmodule AprsWeb.Api.V1.FallbackController do defmodule AprsmeWeb.Api.V1.FallbackController do
@moduledoc """ @moduledoc """
Translates controller action results into valid `Plug.Conn` responses. Translates controller action results into valid `Plug.Conn` responses.
See `Phoenix.Controller.action_fallback/1` for more details. See `Phoenix.Controller.action_fallback/1` for more details.
""" """
use AprsWeb, :controller use AprsmeWeb, :controller
alias AprsWeb.Api.V1.ErrorJSON alias AprsmeWeb.Api.V1.ErrorJSON
# This clause handles errors returned by Ecto's insert/update/delete. # This clause handles errors returned by Ecto's insert/update/delete.
def call(conn, {:error, %Ecto.Changeset{} = changeset}) do def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
conn conn
|> put_status(:unprocessable_entity) |> put_status(:unprocessable_entity)
|> put_view(json: AprsWeb.Api.V1.ChangesetJSON) |> put_view(json: AprsmeWeb.Api.V1.ChangesetJSON)
|> render(:error, changeset: changeset) |> render(:error, changeset: changeset)
end end

View file

@ -1,9 +1,9 @@
defmodule AprsWeb.Api.V1.CallsignJSON do defmodule AprsmeWeb.Api.V1.CallsignJSON do
@moduledoc """ @moduledoc """
Renders callsign and packet data for API v1. Renders callsign and packet data for API v1.
""" """
alias Aprs.Packet alias Aprsme.Packet
def render("show.json", %{packet: packet}) do def render("show.json", %{packet: packet}) do
%{data: packet_json(packet)} %{data: packet_json(packet)}
@ -38,7 +38,7 @@ defmodule AprsWeb.Api.V1.CallsignJSON do
symbol: symbol_json(packet), symbol: symbol_json(packet),
comment: packet.comment, comment: packet.comment,
timestamp: packet.timestamp, timestamp: packet.timestamp,
aprs_messaging: packet.aprs_messaging, aprs_messaging: packet.aprsme_messaging,
weather: weather_json(packet), weather: weather_json(packet),
equipment: equipment_json(packet), equipment: equipment_json(packet),
message: message_json(packet), message: message_json(packet),

View file

@ -1,4 +1,4 @@
defmodule AprsWeb.Api.V1.ChangesetJSON do defmodule AprsmeWeb.Api.V1.ChangesetJSON do
@moduledoc """ @moduledoc """
Renders changeset errors for API v1. Renders changeset errors for API v1.
""" """

View file

@ -1,4 +1,4 @@
defmodule AprsWeb.Api.V1.ErrorJSON do defmodule AprsmeWeb.Api.V1.ErrorJSON do
@moduledoc """ @moduledoc """
Renders error responses for API v1. Renders error responses for API v1.
""" """

View file

@ -1,6 +1,6 @@
defmodule AprsWeb.ErrorHTML do defmodule AprsmeWeb.ErrorHTML do
@moduledoc false @moduledoc false
use AprsWeb, :html use AprsmeWeb, :html
# If you want to customize your error pages, # If you want to customize your error pages,
# uncomment the embed_templates/1 call below # uncomment the embed_templates/1 call below

View file

@ -1,4 +1,4 @@
defmodule AprsWeb.ErrorJSON do defmodule AprsmeWeb.ErrorJSON do
@moduledoc false @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:

View file

@ -1,6 +1,6 @@
defmodule AprsWeb.PageController do defmodule AprsmeWeb.PageController do
@moduledoc false @moduledoc false
use AprsWeb, :controller use AprsmeWeb, :controller
def home(conn, _params) do def home(conn, _params) do
render(conn, :home) render(conn, :home)
@ -14,14 +14,14 @@ defmodule AprsWeb.PageController do
# Check database connection # Check database connection
db_healthy = db_healthy =
try do try do
Aprs.Repo.query!("SELECT 1") Aprsme.Repo.query!("SELECT 1")
true true
rescue rescue
_ -> false _ -> false
end end
# Get application version # Get application version
version = :aprs |> Application.spec(:vsn) |> List.to_string() version = :aprsme |> Application.spec(:vsn) |> List.to_string()
if db_healthy do if db_healthy do
json(conn, %{ json(conn, %{
@ -45,7 +45,7 @@ defmodule AprsWeb.PageController do
def ready(conn, _params) do def ready(conn, _params) do
# Simple readiness check without database dependency # Simple readiness check without database dependency
# This is faster for startup health checks # This is faster for startup health checks
version = :aprs |> Application.spec(:vsn) |> List.to_string() version = :aprsme |> Application.spec(:vsn) |> List.to_string()
json(conn, %{ json(conn, %{
status: "ready", status: "ready",
@ -56,10 +56,10 @@ defmodule AprsWeb.PageController do
def status_json(conn, _params) do def status_json(conn, _params) do
# Get APRS-IS connection status # Get APRS-IS connection status
aprs_status = Aprs.Is.get_status() aprs_status = Aprsme.Is.get_status()
# Get application version # Get application version
version = :aprs |> Application.spec(:vsn) |> List.to_string() version = :aprsme |> Application.spec(:vsn) |> List.to_string()
# Calculate uptime in a human-readable format # Calculate uptime in a human-readable format
uptime_display = format_uptime(aprs_status.uptime_seconds) uptime_display = format_uptime(aprs_status.uptime_seconds)

View file

@ -1,6 +1,6 @@
defmodule AprsWeb.PageHTML do defmodule AprsmeWeb.PageHTML do
@moduledoc false @moduledoc false
use AprsWeb, :html use AprsmeWeb, :html
embed_templates "page_html/*" embed_templates "page_html/*"
end end

View file

@ -1,9 +1,9 @@
defmodule AprsWeb.UserSessionController do defmodule AprsmeWeb.UserSessionController do
@moduledoc false @moduledoc false
use AprsWeb, :controller use AprsmeWeb, :controller
alias Aprs.Accounts alias Aprsme.Accounts
alias AprsWeb.UserAuth alias AprsmeWeb.UserAuth
def create(conn, %{"_action" => "registered"} = params) do def create(conn, %{"_action" => "registered"} = params) do
create(conn, params, "Account created successfully!") create(conn, params, "Account created successfully!")

View file

@ -1,7 +1,7 @@
defmodule AprsWeb.Endpoint do defmodule AprsmeWeb.Endpoint do
@moduledoc false @moduledoc false
use Phoenix.Endpoint, otp_app: :aprs use Phoenix.Endpoint, otp_app: :aprsme
# The session will be stored in the cookie and signed, # The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with. # this means its contents can be read but not tampered with.
@ -21,7 +21,7 @@ defmodule AprsWeb.Endpoint do
# when deploying your static files in production. # when deploying your static files in production.
plug Plug.Static, plug Plug.Static,
at: "/", at: "/",
from: :aprs, from: :aprsme,
gzip: false, gzip: false,
only: ~w(assets fonts images aprs-symbols favicon.ico robots.txt) only: ~w(assets fonts images aprs-symbols favicon.ico robots.txt)
@ -31,7 +31,7 @@ defmodule AprsWeb.Endpoint do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader plug Phoenix.LiveReloader
plug Phoenix.CodeReloader plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :aprs plug Phoenix.Ecto.CheckRepoStatus, otp_app: :aprsme
end end
plug Phoenix.LiveDashboard.RequestLogger, plug Phoenix.LiveDashboard.RequestLogger,
@ -49,5 +49,5 @@ defmodule AprsWeb.Endpoint do
plug Plug.MethodOverride plug Plug.MethodOverride
plug Plug.Head plug Plug.Head
plug Plug.Session, @session_options plug Plug.Session, @session_options
plug AprsWeb.Router plug AprsmeWeb.Router
end end

View file

@ -1,11 +1,11 @@
defmodule AprsWeb.Gettext do defmodule AprsmeWeb.Gettext do
@moduledoc """ @moduledoc """
A module providing Internationalization with a gettext-based API. A module providing Internationalization with a gettext-based API.
By using [Gettext](https://hexdocs.pm/gettext), By using [Gettext](https://hexdocs.pm/gettext),
your module gains a set of macros for translations, for example: your module gains a set of macros for translations, for example:
import AprsWeb.Gettext import AprsmeWeb.Gettext
# Simple translation # Simple translation
gettext("Here is the string to translate") gettext("Here is the string to translate")
@ -20,5 +20,5 @@ defmodule AprsWeb.Gettext do
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
""" """
use Gettext.Backend, otp_app: :aprs use Gettext.Backend, otp_app: :aprsme
end end

View file

@ -1,6 +1,6 @@
defmodule AprsWeb.AboutLive do defmodule AprsmeWeb.AboutLive do
@moduledoc false @moduledoc false
use AprsWeb, :live_view use AprsmeWeb, :live_view
@impl true @impl true
def mount(_params, _session, socket) do def mount(_params, _session, socket) do

View file

@ -1,8 +1,8 @@
defmodule AprsWeb.ApiDocsLive do defmodule AprsmeWeb.ApiDocsLive do
@moduledoc """ @moduledoc """
LiveView for API documentation page. LiveView for API documentation page.
""" """
use AprsWeb, :live_view use AprsmeWeb, :live_view
@impl true @impl true
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
@ -60,7 +60,7 @@ defmodule AprsWeb.ApiDocsLive do
end end
defp make_http_request(callsign) do defp make_http_request(callsign) do
alias Aprs.Packets alias Aprsme.Packets
# Validate callsign format (same as controller) # Validate callsign format (same as controller)
callsign_regex = ~r/^[A-Z0-9]{1,3}[0-9][A-Z]{1,4}(-[0-9]{1,2})?$/ callsign_regex = ~r/^[A-Z0-9]{1,3}[0-9][A-Z]{1,4}(-[0-9]{1,2})?$/
@ -112,7 +112,7 @@ defmodule AprsWeb.ApiDocsLive do
"symbol" => format_symbol(packet), "symbol" => format_symbol(packet),
"comment" => packet.comment, "comment" => packet.comment,
"timestamp" => packet.timestamp, "timestamp" => packet.timestamp,
"aprs_messaging" => packet.aprs_messaging, "aprsme_messaging" => packet.aprsme_messaging,
"weather" => format_weather(packet), "weather" => format_weather(packet),
"equipment" => format_equipment(packet), "equipment" => format_equipment(packet),
"message" => format_message(packet), "message" => format_message(packet),
@ -373,7 +373,7 @@ defmodule AprsWeb.ApiDocsLive do
}, },
"comment": "Mobile Station", "comment": "Mobile Station",
"timestamp": null, "timestamp": null,
"aprs_messaging": false, "aprsme_messaging": false,
"weather": null, "weather": null,
"equipment": { "equipment": {
"manufacturer": "Kenwood", "manufacturer": "Kenwood",

View file

@ -1,17 +1,17 @@
defmodule AprsWeb.BadPacketsLive.Index do defmodule AprsmeWeb.BadPacketsLive.Index do
@moduledoc false @moduledoc false
use AprsWeb, :live_view use AprsmeWeb, :live_view
import Ecto.Query import Ecto.Query
alias Aprs.BadPacket alias Aprsme.BadPacket
alias Aprs.Repo alias Aprsme.Repo
@impl true @impl true
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
if connected?(socket) do if connected?(socket) do
# Subscribe to Postgres notifications for bad packets # Subscribe to Postgres notifications for bad packets
Phoenix.PubSub.subscribe(Aprs.PubSub, "postgres:aprs_events") Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_events")
# Load initial bad packets # Load initial bad packets
bad_packets = fetch_bad_packets() bad_packets = fetch_bad_packets()
# Extra safeguard to ensure we never show more than 100 # Extra safeguard to ensure we never show more than 100

View file

@ -1,9 +1,9 @@
defmodule AprsWeb.InfoLive.Show do defmodule AprsmeWeb.InfoLive.Show do
@moduledoc false @moduledoc false
use AprsWeb, :live_view use AprsmeWeb, :live_view
alias Aprs.Packets alias Aprsme.Packets
alias AprsWeb.MapLive.PacketUtils alias AprsmeWeb.MapLive.PacketUtils
@neighbor_radius_km 10 @neighbor_radius_km 10
@neighbor_limit 10 @neighbor_limit 10

View file

@ -1,4 +1,4 @@
<% {symbol_table_id, symbol_code} = AprsWeb.MapLive.PacketUtils.get_symbol_info(@packet || %{}) %> <% {symbol_table_id, symbol_code} = AprsmeWeb.MapLive.PacketUtils.get_symbol_info(@packet || %{}) %>
<% symbol_table = if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %> <% symbol_table = if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %>
<% symbol_code = symbol_code || ">" %> <% symbol_code = symbol_code || ">" %>
<% symbol_table_num = <% symbol_table_num =
@ -13,14 +13,14 @@
|> String.to_charlist() |> String.to_charlist()
|> List.first() |> List.first()
|> (fn c -> if is_integer(c), do: c, else: 63 end).() %> |> (fn c -> if is_integer(c), do: c, else: 63 end).() %>
<% symbol_img = "/aprs-symbols/aprs-symbols-24-#{symbol_table_num}@2x.png" %> <% _symbol_img = "/aprs-symbols/aprs-symbols-24-#{symbol_table_num}@2x.png" %>
<% symbol_index = symbol_code_ord - 33 %> <% _symbol_index = symbol_code_ord - 33 %>
<div class="max-w-3xl mx-auto mt-10 p-8 bg-white rounded-xl shadow-lg"> <div class="max-w-3xl mx-auto mt-10 p-8 bg-white rounded-xl shadow-lg">
<h1 class="text-3xl font-bold mb-2 flex items-center gap-2"> <h1 class="text-3xl font-bold mb-2 flex items-center gap-2">
APRS station <span class="text-blue-700">{@callsign}</span> APRS station <span class="text-blue-700">{@callsign}</span>
<%= if @packet do %> <%= if @packet do %>
<% {symbol_table_id, symbol_code} = AprsWeb.MapLive.PacketUtils.get_symbol_info(@packet) %> <% {symbol_table_id, symbol_code} = AprsmeWeb.MapLive.PacketUtils.get_symbol_info(@packet) %>
<% symbol_table = if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %> <% symbol_table = if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %>
<% symbol_code = symbol_code || ">" %> <% symbol_code = symbol_code || ">" %>
<% symbol_table_num = <% symbol_table_num =
@ -57,7 +57,7 @@
<span class="font-semibold">Location:</span> {@packet.lat}, {@packet.lon} <span class="font-semibold">Location:</span> {@packet.lat}, {@packet.lon}
</div> </div>
<div class="mb-2"> <div class="mb-2">
<span class="font-semibold">Last position:</span> {AprsWeb.MapLive.PacketUtils.get_timestamp( <span class="font-semibold">Last position:</span> {AprsmeWeb.MapLive.PacketUtils.get_timestamp(
@packet @packet
)} )}
</div> </div>
@ -92,7 +92,7 @@
</.link> </.link>
<%= if neighbor.packet do %> <%= if neighbor.packet do %>
<% {symbol_table_id, symbol_code} = <% {symbol_table_id, symbol_code} =
AprsWeb.MapLive.PacketUtils.get_symbol_info(neighbor.packet) %> AprsmeWeb.MapLive.PacketUtils.get_symbol_info(neighbor.packet) %>
<% symbol_table = <% symbol_table =
if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %> if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %>
<% symbol_code = symbol_code || ">" %> <% symbol_code = symbol_code || ">" %>

View file

@ -1,11 +1,11 @@
defmodule AprsWeb.MapLive.CallsignView do defmodule AprsmeWeb.MapLive.CallsignView do
@moduledoc false @moduledoc false
use AprsWeb, :live_view use AprsmeWeb, :live_view
alias Aprs.Packets alias Aprsme.Packets
alias AprsWeb.Endpoint alias AprsmeWeb.Endpoint
alias AprsWeb.MapLive.MapHelpers alias AprsmeWeb.MapLive.MapHelpers
alias AprsWeb.MapLive.PacketUtils alias AprsmeWeb.MapLive.PacketUtils
@default_center %{lat: 39.0, lng: -98.0} @default_center %{lat: 39.0, lng: -98.0}
@default_zoom 4 @default_zoom 4

View file

@ -1,34 +1,34 @@
defmodule AprsWeb.MapLive.Index do defmodule AprsmeWeb.MapLive.Index do
@moduledoc """ @moduledoc """
LiveView for displaying real-time APRS packets on a map LiveView for displaying real-time APRS packets on a map
""" """
use AprsWeb, :live_view use AprsmeWeb, :live_view
import AprsWeb.TimeHelpers, only: [time_ago_in_words: 1] import AprsmeWeb.TimeHelpers, only: [time_ago_in_words: 1]
alias AprsWeb.Endpoint alias AprsmeWeb.Endpoint
alias AprsWeb.MapLive.MapHelpers alias AprsmeWeb.MapLive.MapHelpers
alias AprsWeb.MapLive.PacketUtils alias AprsmeWeb.MapLive.PacketUtils
alias Phoenix.LiveView.Socket alias Phoenix.LiveView.Socket
@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 Aprsme.Finch
@initialize_replay_delay Application.compile_env(:aprs, :initialize_replay_delay, 500) @initialize_replay_delay Application.compile_env(:aprsme, :initialize_replay_delay, 500)
@impl true @impl true
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
if connected?(socket) do if connected?(socket) do
# Subscribe to packet updates # Subscribe to packet updates
Phoenix.PubSub.subscribe(Aprs.PubSub, "packets") Phoenix.PubSub.subscribe(Aprsme.PubSub, "packets")
Phoenix.PubSub.subscribe(Aprs.PubSub, "bad_packets") Phoenix.PubSub.subscribe(Aprsme.PubSub, "bad_packets")
# Schedule periodic cleanup of old packets # Schedule periodic cleanup of old packets
Process.send_after(self(), :cleanup_old_packets, 60_000) Process.send_after(self(), :cleanup_old_packets, 60_000)
end end
# Get deployment timestamp from config (set during application startup) # Get deployment timestamp from config (set during application startup)
deployed_at = Aprs.Release.deployed_at() deployed_at = Aprsme.Release.deployed_at()
one_hour_ago = DateTime.add(DateTime.utc_now(), -3600, :second) one_hour_ago = DateTime.add(DateTime.utc_now(), -3600, :second)
@ -38,7 +38,7 @@ defmodule AprsWeb.MapLive.Index do
if connected?(socket) do if connected?(socket) do
Endpoint.subscribe("aprs_messages") Endpoint.subscribe("aprs_messages")
Phoenix.PubSub.subscribe(Aprs.PubSub, "postgres:aprs_packets") Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_packets")
maybe_start_geolocation(socket) maybe_start_geolocation(socket)
end end
@ -99,7 +99,7 @@ defmodule AprsWeb.MapLive.Index do
defp maybe_start_geolocation(socket) do defp maybe_start_geolocation(socket) do
if geolocation_enabled?() do if geolocation_enabled?() do
ip_for_geolocation = ip_for_geolocation =
if Application.get_env(:aprs, AprsWeb.Endpoint)[:code_reloader] do if Application.get_env(:aprsme, AprsmeWeb.Endpoint)[:code_reloader] do
# For testing geolocation in dev environment, use a public IP address. # For testing geolocation in dev environment, use a public IP address.
# This will be geolocated to Mountain View, CA. # This will be geolocated to Mountain View, CA.
"8.8.8.8" "8.8.8.8"
@ -116,7 +116,7 @@ defmodule AprsWeb.MapLive.Index do
end end
defp geolocation_enabled? do defp geolocation_enabled? do
Application.get_env(:aprs, :disable_aprs_connection, false) != true Application.get_env(:aprsme, :disable_aprs_connection, false) != true
end end
defp extract_ip(socket) do defp extract_ip(socket) do
@ -1175,7 +1175,7 @@ defmodule AprsWeb.MapLive.Index do
} }
# Call the database through the Packets context # Call the database through the Packets context
packets_module = Application.get_env(:aprs, :packets_module, Aprs.Packets) packets_module = Application.get_env(:aprsme, :packets_module, Aprsme.Packets)
packets = packets_module.get_packets_for_replay(packets_params) packets = packets_module.get_packets_for_replay(packets_params)
# Sort packets by received_at timestamp to ensure chronological replay # Sort packets by received_at timestamp to ensure chronological replay

View file

@ -1,7 +1,7 @@
defmodule AprsWeb.MapLive.MapHelpers do defmodule AprsmeWeb.MapLive.MapHelpers do
@moduledoc false @moduledoc false
alias Parser.Types.MicE alias Aprs.Types.MicE
@spec get_coordinates(map() | struct()) :: {number() | nil, number() | nil, map() | nil} @spec get_coordinates(map() | struct()) :: {number() | nil, number() | nil, map() | nil}
def get_coordinates(%{data_extended: %MicE{} = mic_e}) do def get_coordinates(%{data_extended: %MicE{} = mic_e}) do

View file

@ -1,10 +1,10 @@
defmodule AprsWeb.MapLive.PacketUtils do defmodule AprsmeWeb.MapLive.PacketUtils do
@moduledoc """ @moduledoc """
Shared utilities for extracting and processing packet data in map views. Shared utilities for extracting and processing packet data in map views.
""" """
alias AprsWeb.MapLive.MapHelpers alias AprsmeWeb.MapLive.MapHelpers
alias AprsWeb.TimeHelpers alias AprsmeWeb.TimeHelpers
@doc """ @doc """
Safely extracts a value from a packet or data_extended map with fallback support. Safely extracts a value from a packet or data_extended map with fallback support.

View file

@ -1,14 +1,14 @@
defmodule AprsWeb.PacketsLive.CallsignView do defmodule AprsmeWeb.PacketsLive.CallsignView do
@moduledoc false @moduledoc false
use AprsWeb, :live_view use AprsmeWeb, :live_view
import Ecto.Query import Ecto.Query
alias Aprs.EncodingUtils alias Aprsme.DeviceParser
alias Aprs.Packet alias Aprsme.EncodingUtils
alias Aprs.Repo alias Aprsme.Packet
alias AprsWeb.Endpoint alias Aprsme.Repo
alias Parser.DeviceParser alias AprsmeWeb.Endpoint
@impl true @impl true
def mount(%{"callsign" => callsign}, _session, socket) do def mount(%{"callsign" => callsign}, _session, socket) do
@ -66,7 +66,7 @@ defmodule AprsWeb.PacketsLive.CallsignView do
canonical_identifier = canonical_identifier =
if is_binary(device_identifier) do if is_binary(device_identifier) do
matched_device = Aprs.DeviceIdentification.lookup_device_by_identifier(device_identifier) matched_device = Aprsme.DeviceIdentification.lookup_device_by_identifier(device_identifier)
if matched_device, do: matched_device.identifier, else: device_identifier if matched_device, do: matched_device.identifier, else: device_identifier
else else
device_identifier device_identifier
@ -208,7 +208,10 @@ defmodule AprsWeb.PacketsLive.CallsignView do
defp enrich_packet_with_device_info(packet) do defp enrich_packet_with_device_info(packet) do
device_identifier = Map.get(packet, :device_identifier) || Map.get(packet, "device_identifier") device_identifier = Map.get(packet, :device_identifier) || Map.get(packet, "device_identifier")
device = if is_binary(device_identifier), do: Aprs.DeviceIdentification.lookup_device_by_identifier(device_identifier)
device =
if is_binary(device_identifier), do: Aprsme.DeviceIdentification.lookup_device_by_identifier(device_identifier)
model = if device, do: device.model model = if device, do: device.model
vendor = if device, do: device.vendor vendor = if device, do: device.vendor

View file

@ -1,15 +1,15 @@
defmodule AprsWeb.PacketsLive.Index do defmodule AprsmeWeb.PacketsLive.Index do
@moduledoc false @moduledoc false
use AprsWeb, :live_view use AprsmeWeb, :live_view
alias Aprs.EncodingUtils alias Aprsme.EncodingUtils
alias AprsWeb.Endpoint alias AprsmeWeb.Endpoint
@impl true @impl true
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
if connected?(socket) do if connected?(socket) do
Endpoint.subscribe("aprs_messages") Endpoint.subscribe("aprs_messages")
Phoenix.PubSub.subscribe(Aprs.PubSub, "postgres:aprs_packets") Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_packets")
end end
{:ok, assign(socket, :packets, [])} {:ok, assign(socket, :packets, [])}
@ -26,7 +26,7 @@ defmodule AprsWeb.PacketsLive.Index do
@impl true @impl true
def handle_info({:postgres_packet, payload}, socket) do def handle_info({:postgres_packet, payload}, socket) do
sanitized_payload = Aprs.EncodingUtils.sanitize_packet(payload) sanitized_payload = Aprsme.EncodingUtils.sanitize_packet(payload)
packets = Enum.take([sanitized_payload | socket.assigns.packets], 100) packets = Enum.take([sanitized_payload | socket.assigns.packets], 100)
socket = assign(socket, :packets, packets) socket = assign(socket, :packets, packets)
{:noreply, socket} {:noreply, socket}

View file

@ -51,8 +51,8 @@
Map.get(packet, :lat) || Map.get(packet, :lat) ||
if( if(
Map.has_key?(packet, :location) and not is_nil(packet.location) and Map.has_key?(packet, :location) and not is_nil(packet.location) and
function_exported?(Aprs.Packet, :lat, 1), function_exported?(Aprsme.Packet, :lat, 1),
do: Aprs.Packet.lat(packet), do: Aprsme.Packet.lat(packet),
else: nil else: nil
) || ) ||
case Map.get(packet, :data_extended) do case Map.get(packet, :data_extended) do
@ -86,8 +86,8 @@
Map.get(packet, :lon) || Map.get(packet, :lon) ||
if( if(
Map.has_key?(packet, :location) and not is_nil(packet.location) and Map.has_key?(packet, :location) and not is_nil(packet.location) and
function_exported?(Aprs.Packet, :lon, 1), function_exported?(Aprsme.Packet, :lon, 1),
do: Aprs.Packet.lon(packet), do: Aprsme.Packet.lon(packet),
else: nil else: nil
) || ) ||
case Map.get(packet, :data_extended) do case Map.get(packet, :data_extended) do

View file

@ -1,8 +1,8 @@
defmodule AprsWeb.StatusLive.Index do defmodule AprsmeWeb.StatusLive.Index do
@moduledoc """ @moduledoc """
LiveView for displaying real-time APRS-IS connection status LiveView for displaying real-time APRS-IS connection status
""" """
use AprsWeb, :live_view use AprsmeWeb, :live_view
# 30 seconds # 30 seconds
@refresh_interval 1_000 @refresh_interval 1_000
@ -327,11 +327,11 @@ defmodule AprsWeb.StatusLive.Index do
# Private functions # Private functions
defp get_aprs_status do defp get_aprs_status do
Aprs.Is.get_status() Aprsme.Is.get_status()
end end
defp get_app_version do defp get_app_version do
:aprs |> Application.spec(:vsn) |> List.to_string() :aprsme |> Application.spec(:vsn) |> List.to_string()
end end
defp refresh_status(socket) do defp refresh_status(socket) do

View file

@ -1,8 +1,8 @@
defmodule AprsWeb.UserConfirmationInstructionsLive do defmodule AprsmeWeb.UserConfirmationInstructionsLive do
@moduledoc false @moduledoc false
use AprsWeb, :live_view use AprsmeWeb, :live_view
alias Aprs.Accounts alias Aprsme.Accounts
def render(assigns) do def render(assigns) do
~H""" ~H"""

View file

@ -1,8 +1,8 @@
defmodule AprsWeb.UserConfirmationLive do defmodule AprsmeWeb.UserConfirmationLive do
@moduledoc false @moduledoc false
use AprsWeb, :live_view use AprsmeWeb, :live_view
alias Aprs.Accounts alias Aprsme.Accounts
def render(%{live_action: :edit} = assigns) do def render(%{live_action: :edit} = assigns) do
~H""" ~H"""

View file

@ -1,8 +1,8 @@
defmodule AprsWeb.UserForgotPasswordLive do defmodule AprsmeWeb.UserForgotPasswordLive do
@moduledoc false @moduledoc false
use AprsWeb, :live_view use AprsmeWeb, :live_view
alias Aprs.Accounts alias Aprsme.Accounts
def render(assigns) do def render(assigns) do
~H""" ~H"""

View file

@ -1,9 +1,9 @@
defmodule AprsWeb.UserLoginLive do defmodule AprsmeWeb.UserLoginLive do
@moduledoc false @moduledoc false
use AprsWeb, :live_view use AprsmeWeb, :live_view
alias Aprs.Accounts alias Aprsme.Accounts
alias Aprs.Accounts.User alias Aprsme.Accounts.User
def render(assigns) do def render(assigns) do
~H""" ~H"""

View file

@ -1,9 +1,9 @@
defmodule AprsWeb.UserRegistrationLive do defmodule AprsmeWeb.UserRegistrationLive do
@moduledoc false @moduledoc false
use AprsWeb, :live_view use AprsmeWeb, :live_view
alias Aprs.Accounts alias Aprsme.Accounts
alias Aprs.Accounts.User alias Aprsme.Accounts.User
def render(assigns) do def render(assigns) do
~H""" ~H"""

View file

@ -1,8 +1,8 @@
defmodule AprsWeb.UserResetPasswordLive do defmodule AprsmeWeb.UserResetPasswordLive do
@moduledoc false @moduledoc false
use AprsWeb, :live_view use AprsmeWeb, :live_view
alias Aprs.Accounts alias Aprsme.Accounts
def render(assigns) do def render(assigns) do
~H""" ~H"""

View file

@ -1,8 +1,8 @@
defmodule AprsWeb.UserSettingsLive do defmodule AprsmeWeb.UserSettingsLive do
@moduledoc false @moduledoc false
use AprsWeb, :live_view use AprsmeWeb, :live_view
alias Aprs.Accounts alias Aprsme.Accounts
def render(assigns) do def render(assigns) do
~H""" ~H"""

View file

@ -1,9 +1,9 @@
defmodule AprsWeb.WeatherLive.CallsignView do defmodule AprsmeWeb.WeatherLive.CallsignView do
@moduledoc false @moduledoc false
use AprsWeb, :live_view use AprsmeWeb, :live_view
alias Aprs.Packets alias Aprsme.Packets
alias AprsWeb.MapLive.PacketUtils alias AprsmeWeb.MapLive.PacketUtils
@impl true @impl true
def mount(%{"callsign" => callsign}, _session, socket) do def mount(%{"callsign" => callsign}, _session, socket) do

View file

@ -1,14 +1,14 @@
defmodule AprsWeb.Router do defmodule AprsmeWeb.Router do
use AprsWeb, :router use AprsmeWeb, :router
import AprsWeb.UserAuth import AprsmeWeb.UserAuth
import Phoenix.LiveDashboard.Router import Phoenix.LiveDashboard.Router
pipeline :browser do pipeline :browser do
plug :accepts, ["html"] plug :accepts, ["html"]
plug :fetch_session plug :fetch_session
plug :fetch_live_flash plug :fetch_live_flash
plug :put_root_layout, {AprsWeb.Layouts, :root} plug :put_root_layout, {AprsmeWeb.Layouts, :root}
plug :protect_from_forgery plug :protect_from_forgery
plug :put_secure_browser_headers plug :put_secure_browser_headers
plug :fetch_current_user plug :fetch_current_user
@ -18,17 +18,17 @@ defmodule AprsWeb.Router do
plug :accepts, ["json"] plug :accepts, ["json"]
end end
scope "/", AprsWeb do scope "/", AprsmeWeb do
pipe_through :api pipe_through :api
get "/health", PageController, :health get "/health", PageController, :health
get "/ready", PageController, :ready get "/ready", PageController, :ready
get "/status.json", PageController, :status_json get "/status.json", PageController, :status_json
end end
scope "/", AprsWeb do scope "/", AprsmeWeb do
pipe_through :browser pipe_through :browser
live_dashboard "/dashboard", metrics: AprsWeb.Telemetry live_dashboard "/dashboard", metrics: AprsmeWeb.Telemetry
live_session :regular_pages do live_session :regular_pages do
live "/status", StatusLive.Index, :index live "/status", StatusLive.Index, :index
@ -41,21 +41,21 @@ defmodule AprsWeb.Router do
live "/info/:callsign", InfoLive.Show, :show live "/info/:callsign", InfoLive.Show, :show
end end
live_session :map_pages, layout: {AprsWeb.Layouts, :map} do live_session :map_pages, layout: {AprsmeWeb.Layouts, :map} do
live "/", MapLive.Index, :index live "/", MapLive.Index, :index
live "/:callsign", MapLive.CallsignView, :index live "/:callsign", MapLive.CallsignView, :index
end end
end end
# API v1 routes # API v1 routes
scope "/api/v1", AprsWeb.Api.V1, as: :api_v1 do scope "/api/v1", AprsmeWeb.Api.V1, as: :api_v1 do
pipe_through :api pipe_through :api
get "/callsign/:callsign", CallsignController, :show get "/callsign/:callsign", CallsignController, :show
end end
# Enable LiveDashboard and Swoosh mailbox preview in development # Enable LiveDashboard and Swoosh mailbox preview in development
if Application.compile_env(:aprs, :dev_routes) do if Application.compile_env(:aprsme, :dev_routes) do
# If you want to use the LiveDashboard in production, you should put # If you want to use the LiveDashboard in production, you should put
# it behind authentication and allow only admins to access it. # it behind authentication and allow only admins to access it.
# If your application does not have an admins-only section yet, # If your application does not have an admins-only section yet,
@ -65,18 +65,18 @@ defmodule AprsWeb.Router do
scope "/dev" do scope "/dev" do
pipe_through :browser pipe_through :browser
# live_dashboard "/dashboard", metrics: AprsWeb.Telemetry # live_dashboard "/dashboard", metrics: AprsmeWeb.Telemetry
forward "/mailbox", Plug.Swoosh.MailboxPreview forward "/mailbox", Plug.Swoosh.MailboxPreview
end end
end end
## Authentication routes ## Authentication routes
scope "/", AprsWeb do scope "/", AprsmeWeb do
pipe_through [:browser, :redirect_if_user_is_authenticated] pipe_through [:browser, :redirect_if_user_is_authenticated]
live_session :redirect_if_user_is_authenticated, live_session :redirect_if_user_is_authenticated,
on_mount: [{AprsWeb.UserAuth, :redirect_if_user_is_authenticated}] do on_mount: [{AprsmeWeb.UserAuth, :redirect_if_user_is_authenticated}] do
live "/users/register", UserRegistrationLive, :new live "/users/register", UserRegistrationLive, :new
live "/users/log_in", UserLoginLive, :new live "/users/log_in", UserLoginLive, :new
live "/users/reset_password", UserForgotPasswordLive, :new live "/users/reset_password", UserForgotPasswordLive, :new
@ -86,23 +86,23 @@ defmodule AprsWeb.Router do
post "/users/log_in", UserSessionController, :create post "/users/log_in", UserSessionController, :create
end end
scope "/", AprsWeb do scope "/", AprsmeWeb do
pipe_through [:browser, :require_authenticated_user] pipe_through [:browser, :require_authenticated_user]
live_session :require_authenticated_user, live_session :require_authenticated_user,
on_mount: [{AprsWeb.UserAuth, :ensure_authenticated}] do on_mount: [{AprsmeWeb.UserAuth, :ensure_authenticated}] do
live "/users/settings", UserSettingsLive, :edit live "/users/settings", UserSettingsLive, :edit
live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
end end
end end
scope "/", AprsWeb do scope "/", AprsmeWeb do
pipe_through [:browser] pipe_through [:browser]
delete "/users/log_out", UserSessionController, :delete delete "/users/log_out", UserSessionController, :delete
live_session :current_user, live_session :current_user,
on_mount: [{AprsWeb.UserAuth, :mount_current_user}] do on_mount: [{AprsmeWeb.UserAuth, :mount_current_user}] do
live "/users/confirm/:token", UserConfirmationLive, :edit live "/users/confirm/:token", UserConfirmationLive, :edit
live "/users/confirm", UserConfirmationInstructionsLive, :new live "/users/confirm", UserConfirmationInstructionsLive, :new
end end

View file

@ -1,4 +1,4 @@
defmodule AprsWeb.Telemetry do defmodule AprsmeWeb.Telemetry do
@moduledoc false @moduledoc false
use Supervisor use Supervisor
@ -54,23 +54,23 @@ defmodule AprsWeb.Telemetry do
), ),
# Database Metrics # Database Metrics
summary("aprs.repo.query.total_time", summary("aprsme.repo.query.total_time",
unit: {:native, :millisecond}, unit: {:native, :millisecond},
description: "The sum of the other measurements" description: "The sum of the other measurements"
), ),
summary("aprs.repo.query.decode_time", summary("aprsme.repo.query.decode_time",
unit: {:native, :millisecond}, unit: {:native, :millisecond},
description: "The time spent decoding the data received from the database" description: "The time spent decoding the data received from the database"
), ),
summary("aprs.repo.query.query_time", summary("aprsme.repo.query.query_time",
unit: {:native, :millisecond}, unit: {:native, :millisecond},
description: "The time spent executing the query" description: "The time spent executing the query"
), ),
summary("aprs.repo.query.queue_time", summary("aprsme.repo.query.queue_time",
unit: {:native, :millisecond}, unit: {:native, :millisecond},
description: "The time spent waiting for a database connection" description: "The time spent waiting for a database connection"
), ),
summary("aprs.repo.query.idle_time", summary("aprsme.repo.query.idle_time",
unit: {:native, :millisecond}, unit: {:native, :millisecond},
description: "The time the connection spent waiting before being checked out for the query" description: "The time the connection spent waiting before being checked out for the query"
), ),
@ -87,7 +87,7 @@ defmodule AprsWeb.Telemetry do
[ [
# A module, function and arguments to be invoked periodically. # A module, function and arguments to be invoked periodically.
# This function must call :telemetry.execute/3 and a metric must be added above. # This function must call :telemetry.execute/3 and a metric must be added above.
# {AprsWeb, :count_users, []} # {AprsmeWeb, :count_users, []}
] ]
end end
end end

View file

@ -1,4 +1,4 @@
defmodule AprsWeb.TimeHelpers do defmodule AprsmeWeb.TimeHelpers do
@moduledoc """ @moduledoc """
Shared helpers for formatting time and dates in the web layer. Shared helpers for formatting time and dates in the web layer.
""" """

View file

@ -1,11 +1,11 @@
defmodule AprsWeb.UserAuth do defmodule AprsmeWeb.UserAuth do
@moduledoc false @moduledoc false
use AprsWeb, :verified_routes use AprsmeWeb, :verified_routes
import Phoenix.Controller import Phoenix.Controller
import Plug.Conn import Plug.Conn
alias Aprs.Accounts alias Aprsme.Accounts
# Make the remember me cookie valid for 60 days. # Make the remember me cookie valid for 60 days.
# If you want bump or reduce this value, also change # If you want bump or reduce this value, also change
@ -74,7 +74,7 @@ defmodule AprsWeb.UserAuth do
user_token && Accounts.delete_user_session_token(user_token) user_token && Accounts.delete_user_session_token(user_token)
if live_socket_id = get_session(conn, :live_socket_id) do if live_socket_id = get_session(conn, :live_socket_id) do
AprsWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{}) AprsmeWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
end end
conn conn
@ -131,16 +131,16 @@ defmodule AprsWeb.UserAuth do
Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate
the current_user: the current_user:
defmodule AprsWeb.PageLive do defmodule AprsmeWeb.PageLive do
use AprsWeb, :live_view use AprsmeWeb, :live_view
on_mount {AprsWeb.UserAuth, :mount_current_user} on_mount {AprsmeWeb.UserAuth, :mount_current_user}
... ...
end end
Or use the `live_session` of your router to invoke the on_mount callback: Or use the `live_session` of your router to invoke the on_mount callback:
live_session :authenticated, on_mount: [{AprsWeb.UserAuth, :ensure_authenticated}] do live_session :authenticated, on_mount: [{AprsmeWeb.UserAuth, :ensure_authenticated}] do
live "/profile", ProfileLive, :index live "/profile", ProfileLive, :index
end end
""" """

View file

@ -1,970 +0,0 @@
defmodule Parser do
@moduledoc """
Main parsing library
"""
alias Aprs.Convert
alias Parser.Item
alias Parser.MicE
alias Parser.Object
alias Parser.PHG
alias Parser.Status
alias Parser.Telemetry
alias Parser.Weather
# Simple APRS position parsing to replace parse_aprs_position
defp parse_aprs_position(lat, lon) do
# Regex for latitude: 2 deg, 2+ min, 1 dir (N/S)
# Regex for longitude: 3 deg, 2+ min, 1 dir (E/W)
lat_re = ~r/^(\d{2})(\d{2}\.\d+)([NS])$/
lon_re = ~r/^(\d{3})(\d{2}\.\d+)([EW])$/
with [_, lat_deg, lat_min, lat_dir] <- Regex.run(lat_re, lat),
[_, lon_deg, lon_min, lon_dir] <- Regex.run(lon_re, lon) do
lat_val =
Decimal.add(Decimal.new(lat_deg), Decimal.div(Decimal.new(lat_min), Decimal.new("60")))
lon_val =
Decimal.add(Decimal.new(lon_deg), Decimal.div(Decimal.new(lon_min), Decimal.new("60")))
lat = if lat_dir == "S", do: Decimal.negate(lat_val), else: lat_val
lon = if lon_dir == "W", do: Decimal.negate(lon_val), else: lon_val
%{latitude: lat, longitude: lon}
else
_ -> %{latitude: nil, longitude: nil}
end
end
@type packet :: %{
id: String.t(),
sender: String.t(),
path: String.t(),
destination: String.t(),
information_field: String.t(),
data_type: atom(),
base_callsign: String.t(),
ssid: String.t(),
data_extended: map() | nil,
received_at: DateTime.t()
}
@type parse_result :: {:ok, packet()} | {:error, atom() | String.t()}
@type position_ambiguity :: 0..4
@spec parse(String.t()) :: parse_result()
def parse(message) when is_binary(message) do
do_parse(message)
rescue
_ ->
{:error, :invalid_packet}
end
def parse(_), do: {:error, :invalid_packet}
@spec do_parse(String.t()) :: parse_result()
defp do_parse(message) do
with {:ok, [sender, path, data]} <- split_packet(message),
{:ok, callsign_parts} <- parse_callsign(sender),
{:ok, data_type} <- parse_datatype_safe(data),
{:ok, [destination, path]} <- split_path(path),
:ok <- validate_path(path) do
data_trimmed = String.trim(data)
data_without_type = String.slice(data_trimmed, 1..-1//1)
data_extended = parse_data(data_type, destination, data_without_type)
base_callsign = List.first(callsign_parts)
ssid =
case List.last(callsign_parts) do
nil ->
nil
s when is_binary(s) ->
s
i when is_integer(i) ->
to_string(i)
_ ->
nil
end
{:ok,
%{
id: 16 |> :crypto.strong_rand_bytes() |> Base.encode16(case: :lower),
sender: sender,
path: path,
destination: destination,
information_field: data_trimmed,
data_type: data_type,
base_callsign: base_callsign,
ssid: ssid,
data_extended: data_extended,
received_at: DateTime.truncate(DateTime.utc_now(), :microsecond)
}}
end
rescue
_ ->
{:error, :invalid_packet}
end
# Validate path for too many components
def validate_path(path) when is_binary(path) and path != "" do
if length(String.split(path, ",")) > 8 do
{:error, "Too many path components"}
else
:ok
end
end
def validate_path(_), do: :ok
# Safely split packet into components
@spec split_packet(String.t()) :: {:ok, [String.t()]} | {:error, String.t()}
def split_packet(message) do
split_packet_parts(String.split(message, [">", ":"], parts: 3))
end
@spec split_packet_parts([String.t()]) :: {:ok, [String.t()]} | {:error, String.t()}
defp split_packet_parts([sender, path, data]) when byte_size(sender) > 0 and byte_size(path) > 0 do
{:ok, [sender, path, data]}
end
@spec split_packet_parts(list()) :: {:error, String.t()}
defp split_packet_parts(_), do: {:error, "Invalid packet format"}
# Safely split path into destination and digipeater path
@spec split_path(String.t()) :: {:ok, [String.t()]} | {:error, String.t()}
def split_path(path) when is_binary(path) do
split = String.split(path, ",", parts: 2)
split_path_parts(split)
end
defp split_path_parts([destination, digi_path]), do: {:ok, [destination, digi_path]}
defp split_path_parts([destination]), do: {:ok, [destination, ""]}
defp split_path_parts(_), do: {:error, "Invalid path format"}
# Safe version of parse_datatype that returns {:ok, type} or {:error, reason}
@spec parse_datatype_safe(String.t()) :: {:ok, atom()} | {:error, String.t()}
def parse_datatype_safe(""), do: {:error, "Empty data"}
def parse_datatype_safe(data), do: {:ok, parse_datatype(data)}
@spec parse_callsign(String.t()) :: {:ok, [String.t()]} | {:error, String.t()}
def parse_callsign(callsign) do
case Parser.AX25.parse_callsign(callsign) do
{:ok, {base, ssid}} -> {:ok, [base, ssid]}
{:error, reason} -> {:error, reason}
end
end
# One of the nutty exceptions in the APRS protocol has to do with this
# data type indicator. It's usually the first character of the message.
# However, in some rare cases, the ! indicator can be anywhere in the
# first 40 characters of the message. I'm not going to deal with that
# weird case right now. It seems like its for a specific type of old
# TNC hardware that probably doesn't even exist anymore.
@spec parse_datatype(String.t()) :: atom()
def parse_datatype(<<":", _::binary>>), do: :message
def parse_datatype(<<">", _::binary>>), do: :status
def parse_datatype("!" <> _), do: :position
def parse_datatype("/" <> _), do: :timestamped_position
def parse_datatype("=" <> _), do: :position_with_message
def parse_datatype("@" <> _), do: :timestamped_position_with_message
def parse_datatype(";" <> _), do: :object
def parse_datatype("`" <> _), do: :mic_e_old
def parse_datatype("'" <> _), do: :mic_e_old
def parse_datatype("_" <> _), do: :weather
def parse_datatype("T" <> _), do: :telemetry
def parse_datatype("$" <> _), do: :raw_gps_ultimeter
def parse_datatype("<" <> _), do: :station_capabilities
def parse_datatype("?" <> _), do: :query
def parse_datatype("{" <> _), do: :user_defined
def parse_datatype("}" <> _), do: :third_party_traffic
def parse_datatype("%" <> _), do: :item
def parse_datatype(")" <> _), do: :item
def parse_datatype("*" <> _), do: :peet_logging
def parse_datatype("," <> _), do: :invalid_test_data
def parse_datatype("#DFS" <> _), do: :df_report
def parse_datatype("#PHG" <> _), do: :phg_data
def parse_datatype("#" <> _), do: :phg_data
def parse_datatype(_), do: :unknown_datatype
@spec parse_data(atom(), String.t(), String.t()) :: map() | nil
def parse_data(:mic_e, destination, data), do: MicE.parse(data, destination)
def parse_data(:mic_e_old, destination, data), do: MicE.parse(data, destination)
def parse_data(:object, _destination, data), do: Object.parse(data)
def parse_data(:item, _destination, data), do: Item.parse(data)
def parse_data(:weather, _destination, data), do: Weather.parse(data)
def parse_data(:telemetry, _destination, data), do: Telemetry.parse(data)
def parse_data(:status, _destination, data), do: Status.parse(data)
def parse_data(:phg_data, _destination, data), do: PHG.parse(data)
def parse_data(:peet_logging, _destination, data), do: Parser.SpecialDataHelpers.parse_peet_logging(data)
def parse_data(:invalid_test_data, _destination, data), do: Parser.SpecialDataHelpers.parse_invalid_test_data(data)
def parse_data(:raw_gps_ultimeter, _destination, data) do
case Parser.NMEAHelpers.parse_nmea_sentence(data) do
{:error, error} ->
%{
data_type: :raw_gps_ultimeter,
error: error,
nmea_type: nil,
raw_data: data,
latitude: nil,
longitude: nil
}
end
end
def parse_data(:df_report, _destination, data) do
if String.starts_with?(data, "DFS") and byte_size(data) >= 7 do
<<"DFS", s, h, g, d, rest::binary>> = data
%{
df_strength: Parser.PHGHelpers.parse_df_strength(s),
height: Parser.PHGHelpers.parse_phg_height(h),
gain: Parser.PHGHelpers.parse_phg_gain(g),
directivity: Parser.PHGHelpers.parse_phg_directivity(d),
comment: rest,
data_type: :df_report
}
else
%{
df_data: data,
data_type: :df_report
}
end
end
def parse_data(:user_defined, _destination, data), do: parse_user_defined(data)
def parse_data(:third_party_traffic, _destination, data), do: parse_third_party_traffic(data)
def parse_data(:message, _destination, data) do
case Regex.run(~r/^:([^:]+):(.+?)(\{(\d+)\})?$/s, data) do
[_, addressee, message_text, _full_ack, message_number] ->
%{
data_type: :message,
addressee: String.trim(addressee),
message_text: String.trim(message_text),
message_number: message_number
}
[_, addressee, message_text] ->
%{
data_type: :message,
addressee: String.trim(addressee),
message_text: String.trim(message_text)
}
_ ->
nil
end
end
def parse_data(:position, destination, <<"!", rest::binary>>) do
parse_data(:position, destination, rest)
end
def parse_data(:position, _destination, <<"/", _::binary>> = data) do
result = parse_position_without_timestamp(data)
if result.data_type == :malformed_position, do: result, else: %{result | data_type: :position}
end
def parse_data(:position, _destination, data) do
result = parse_position_without_timestamp(data)
if result.data_type == :malformed_position, do: result, else: %{result | data_type: :position}
end
def parse_data(:position_with_message, _destination, data) do
result = parse_position_with_message_without_timestamp(data)
%{result | data_type: :position}
end
def parse_data(:timestamped_position, _destination, data) do
parse_position_with_timestamp(false, data, :timestamped_position)
end
def parse_data(:timestamped_position_with_message, _destination, data) do
case data do
<<time::binary-size(7), latitude::binary-size(8), sym_table_id::binary-size(1), longitude::binary-size(9),
symbol_code::binary-size(1), rest::binary>> ->
weather_start = String.starts_with?(rest, "_")
if weather_start do
result =
Parser.parse_position_with_datetime_and_weather(
true,
time,
latitude,
sym_table_id,
longitude,
symbol_code,
rest
)
add_has_location(result)
else
result = parse_position_with_timestamp(true, data, :timestamped_position_with_message)
add_has_location(result)
end
_ ->
result = parse_position_with_timestamp(true, data, :timestamped_position_with_message)
add_has_location(result)
end
end
def parse_data(:station_capabilities, _destination, data), do: parse_station_capabilities(data)
def parse_data(:query, _destination, data), do: parse_query(data)
# Catch-all for unknown or unsupported types
def parse_data(_type, _destination, _data), do: nil
defp add_has_location(result) do
Map.put(
result,
:has_location,
(is_number(result[:latitude]) or is_struct(result[:latitude], Decimal)) and
(is_number(result[:longitude]) or is_struct(result[:longitude], Decimal))
)
end
@spec parse_position_with_datetime_and_weather(
boolean(),
binary(),
binary(),
binary(),
binary(),
binary(),
binary()
) :: map()
def parse_position_with_datetime_and_weather(
aprs_messaging?,
time,
latitude,
sym_table_id,
longitude,
symbol_code,
weather_report
) do
pos = parse_aprs_position(latitude, longitude)
weather_data = Parser.Weather.parse_weather_data(weather_report)
%{
latitude: pos.latitude,
longitude: pos.longitude,
timestamp: time,
symbol_table_id: sym_table_id,
symbol_code: symbol_code,
weather: weather_data,
data_type: :position_with_datetime_and_weather,
aprs_messaging?: aprs_messaging?
}
end
@spec decode_compressed_position(binary()) :: map()
def decode_compressed_position(
<<"/", latitude::binary-size(4), longitude::binary-size(4), symbol_code::binary-size(1), _cs::binary-size(2),
_compression_type::binary-size(2), _rest::binary>>
) do
lat = Parser.CompressedPositionHelpers.convert_to_base91(latitude)
lon = Parser.CompressedPositionHelpers.convert_to_base91(longitude)
%{
latitude: lat,
longitude: lon,
symbol_code: symbol_code
}
end
@spec convert_to_base91(binary()) :: integer()
def convert_to_base91(<<value::binary-size(4)>>) do
[v1, v2, v3, v4] = to_charlist(value)
(v1 - 33) * 91 * 91 * 91 + (v2 - 33) * 91 * 91 + (v3 - 33) * 91 + v4
end
# Helper to extract course and speed from comment field (e.g., "/123/045" or "123/045")
@spec extract_course_and_speed(String.t()) :: {integer() | nil, float() | nil}
defp extract_course_and_speed(comment) do
# Match "/123/045" or "123/045" at the start of the comment
case Regex.run(~r"/?(\d{3})/(\d{3})", comment) do
[_, course, speed] -> {String.to_integer(course), String.to_integer(speed) * 1.0}
_ -> {nil, nil}
end
end
# Patch parse_position_without_timestamp to include course/speed
@spec parse_position_without_timestamp(String.t()) :: map()
def parse_position_without_timestamp(position_data) do
case position_data do
<<latitude::binary-size(8), sym_table_id::binary-size(1), longitude::binary-size(9), symbol_code::binary-size(1),
comment::binary>> ->
parse_position_uncompressed(latitude, sym_table_id, longitude, symbol_code, comment)
<<latitude::binary-size(8), sym_table_id::binary-size(1), longitude::binary-size(9)>> ->
parse_position_short_uncompressed(latitude, sym_table_id, longitude)
<<"/", 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>> ->
parse_position_compressed(
latitude_compressed,
longitude_compressed,
symbol_code,
cs,
compression_type,
comment
)
_ ->
parse_position_malformed(position_data)
end
end
defp parse_position_uncompressed(latitude, sym_table_id, longitude, symbol_code, comment) do
%{latitude: lat, longitude: lon} = parse_aprs_position(latitude, longitude)
ambiguity = Parser.UtilityHelpers.calculate_position_ambiguity(latitude, longitude)
dao_data = parse_dao_extension(comment)
{course, speed} = extract_course_and_speed(comment)
has_position =
(is_number(lat) or is_struct(lat, Decimal)) and (is_number(lon) or is_struct(lon, Decimal))
base_map = %{
latitude: lat,
longitude: lon,
timestamp: nil,
symbol_table_id: sym_table_id,
symbol_code: symbol_code,
comment: comment,
data_type: :position,
aprs_messaging?: false,
compressed?: false,
position_ambiguity: ambiguity,
dao: dao_data,
course: course,
speed: speed,
has_position: has_position
}
if sym_table_id == "/" and symbol_code == "_" do
weather_map = Parser.Weather.parse_weather_data(comment)
Map.merge(base_map, weather_map)
else
base_map
end
end
defp parse_position_short_uncompressed(latitude, sym_table_id, longitude) do
%{latitude: lat, longitude: lon} = parse_aprs_position(latitude, longitude)
ambiguity = Parser.UtilityHelpers.calculate_position_ambiguity(latitude, longitude)
has_position =
(is_number(lat) or is_struct(lat, Decimal)) and (is_number(lon) or is_struct(lon, Decimal))
%{
latitude: lat,
longitude: lon,
timestamp: nil,
symbol_table_id: sym_table_id,
symbol_code: "_",
data_type: :position,
aprs_messaging?: false,
compressed?: false,
position_ambiguity: ambiguity,
dao: nil,
course: nil,
speed: nil,
has_position: has_position
}
end
defp parse_position_compressed(latitude_compressed, longitude_compressed, symbol_code, cs, compression_type, comment) do
converted_lat = Parser.CompressedPositionHelpers.convert_compressed_lat(latitude_compressed)
converted_lon = Parser.CompressedPositionHelpers.convert_compressed_lon(longitude_compressed)
compressed_cs = Parser.CompressedPositionHelpers.convert_compressed_cs(cs)
ambiguity = Parser.CompressedPositionHelpers.calculate_compressed_ambiguity(compression_type)
has_position =
(is_number(converted_lat) or is_struct(converted_lat, Decimal)) and
(is_number(converted_lon) or is_struct(converted_lon, Decimal))
base_data = %{
latitude: converted_lat,
longitude: converted_lon,
symbol_table_id: "/",
symbol_code: symbol_code,
comment: comment,
position_format: :compressed,
compression_type: compression_type,
data_type: :position,
compressed?: true,
position_ambiguity: ambiguity,
dao: nil,
has_position: has_position
}
Map.merge(base_data, compressed_cs)
rescue
_e ->
%{
latitude: nil,
longitude: nil,
symbol_table_id: "/",
symbol_code: symbol_code,
comment: comment,
position_format: :compressed,
compression_type: compression_type,
data_type: :position,
compressed?: true,
position_ambiguity: Parser.CompressedPositionHelpers.calculate_compressed_ambiguity(compression_type),
dao: nil,
course: nil,
speed: nil,
has_position: false
}
end
defp parse_position_malformed(position_data) do
%{
latitude: nil,
longitude: nil,
timestamp: nil,
symbol_table_id: nil,
symbol_code: nil,
data_type: :malformed_position,
aprs_messaging?: false,
compressed?: false,
comment: String.trim(position_data),
dao: nil,
course: nil,
speed: nil,
has_position: false
}
end
# Patch parse_position_with_message_without_timestamp to propagate course/speed
@spec parse_position_with_message_without_timestamp(String.t()) :: map()
def parse_position_with_message_without_timestamp(position_data) do
result = parse_position_without_timestamp(position_data)
Map.put(result, :aprs_messaging?, true)
end
# Patch parse_position_with_timestamp to extract course/speed from comment
@spec parse_position_with_timestamp(boolean(), binary(), atom()) :: map()
def parse_position_with_timestamp(
aprs_messaging?,
<<time::binary-size(7), latitude::binary-size(8), sym_table_id::binary-size(1), longitude::binary-size(9),
symbol_code::binary-size(1), comment::binary>>,
_data_type
) do
case Parser.UtilityHelpers.validate_position_data(latitude, longitude) do
{:ok, {lat, lon}} ->
position = parse_aprs_position(latitude, longitude)
{course, speed} = extract_course_and_speed(comment)
%{
latitude: lat,
longitude: lon,
position: position,
time: Parser.UtilityHelpers.validate_timestamp(time),
symbol_table_id: sym_table_id,
symbol_code: symbol_code,
comment: comment,
data_type: :position,
aprs_messaging?: aprs_messaging?,
compressed?: false,
course: course,
speed: speed
}
_ ->
# Fallback: try to extract lat/lon using regex if binary pattern match fails
regex =
~r/^(?<time>\w{7})(?<lat>\d{4,5}\.\d+[NS])(?<sym_table>.)(?<lon>\d{5,6}\.\d+[EW])(?<sym_code>.)(?<comment>.*)$/
case Regex.named_captures(
regex,
time <> latitude <> sym_table_id <> longitude <> symbol_code <> comment
) do
%{
"lat" => lat,
"lon" => lon,
"time" => time,
"sym_table" => sym_table,
"sym_code" => sym_code,
"comment" => comment
} ->
pos = parse_aprs_position(lat, lon)
%{
latitude: pos.latitude,
longitude: pos.longitude,
time: time,
symbol_table_id: sym_table,
symbol_code: sym_code,
comment: comment,
data_type: :position,
aprs_messaging?: aprs_messaging?,
compressed?: false
}
_ ->
%{
data_type: :timestamped_position_error,
error: "Invalid timestamped position format",
raw_data: time <> latitude <> sym_table_id <> longitude <> symbol_code <> comment
}
end
end
end
def parse_position_with_timestamp(aprs_messaging?, data, _data_type) do
# Fallback: try to extract lat/lon using regex if binary pattern match fails
regex =
~r/^(?<time>\w{7})(?<lat>\d{4,5}\.\d+[NS])(?<sym_table>.)(?<lon>\d{5,6}\.\d+[EW])(?<sym_code>.)(?<comment>.*)$/
case Regex.named_captures(regex, data) do
%{
"lat" => lat,
"lon" => lon,
"time" => time,
"sym_table" => sym_table,
"sym_code" => sym_code,
"comment" => comment
} ->
pos = parse_aprs_position(lat, lon)
%{
latitude: pos.latitude,
longitude: pos.longitude,
time: time,
symbol_table_id: sym_table,
symbol_code: sym_code,
comment: comment,
data_type: :position,
aprs_messaging?: aprs_messaging?,
compressed?: false
}
_ ->
%{
data_type: :timestamped_position_error,
error: "Invalid timestamped position format",
raw_data: data
}
end
end
@spec parse_manufacturer(binary()) :: String.t()
def parse_manufacturer(symbols) do
Aprs.DeviceIdentification.identify_device(symbols)
end
@spec convert_compressed_lat(binary()) :: float()
def convert_compressed_lat(lat) do
[l1, l2, l3, l4] = to_charlist(lat)
90 - ((l1 - 33) * 91 ** 3 + (l2 - 33) * 91 ** 2 + (l3 - 33) * 91 + l4 - 33) / 380_926
end
@spec convert_compressed_lon(binary()) :: float()
def convert_compressed_lon(lon) do
[l1, l2, l3, l4] = to_charlist(lon)
-180 + ((l1 - 33) * 91 ** 3 + (l2 - 33) * 91 ** 2 + (l3 - 33) * 91 + l4 - 33) / 190_463
end
@spec convert_compressed_cs(binary()) :: map()
def convert_compressed_cs(cs) do
[c, s] = to_charlist(cs)
c = c - 33
s = s - 33
case c do
x when x in ?!..?z ->
# compressed speed/course value
# speed is returned in knots
%{
course: s * 4,
speed: Convert.speed(1.08 ** s - 1, :knots, :mph)
}
?Z ->
# pre-calculated radio range
%{
range: 2 * 1.08 ** s
}
_ ->
%{}
end
end
# Status Report parsing
def parse_status(<<">", status_text::binary>>) do
%{
status_text: status_text,
data_type: :status
}
end
@spec parse_status(String.t()) :: map()
def parse_status(data) do
%{
status_text: data,
data_type: :status
}
end
# Station Capabilities parsing
def parse_station_capabilities(<<"<", capabilities::binary>>) do
%{
capabilities: capabilities,
data_type: :station_capabilities
}
end
@spec parse_station_capabilities(String.t()) :: map()
def parse_station_capabilities(data) do
%{
capabilities: data,
data_type: :station_capabilities
}
end
# Query parsing
def parse_query(<<"?", query_type::binary-size(1), query_data::binary>>) do
%{
query_type: query_type,
query_data: query_data,
data_type: :query
}
end
@spec parse_query(String.t()) :: map()
def parse_query(data) do
%{
query_data: data,
data_type: :query
}
end
# User Defined parsing
def parse_user_defined(<<"{", user_id::binary-size(1), user_data::binary>>) do
parsed_data = parse_user_defined_format(user_id, user_data)
Map.merge(
%{
user_id: user_id,
data_type: :user_defined,
raw_data: user_data
},
parsed_data
)
end
@spec parse_user_defined(String.t()) :: map()
def parse_user_defined(data) do
%{
user_data: data,
data_type: :user_defined
}
end
# Parse specific user-defined formats
defp parse_user_defined_format("A", user_data), do: %{format: :experimental_a, content: user_data}
defp parse_user_defined_format("B", user_data), do: %{format: :experimental_b, content: user_data}
defp parse_user_defined_format("C", user_data), do: %{format: :custom_c, content: user_data}
defp parse_user_defined_format(_, user_data), do: %{format: :unknown, content: user_data}
# Third Party Traffic parsing
def parse_third_party_traffic(packet) do
if Parser.UtilityHelpers.count_leading_braces(packet) + 1 > 3 do
%{
error: "Maximum tunnel depth exceeded"
}
else
case parse_tunneled_packet(packet) do
{:ok, parsed_packet} ->
build_third_party_traffic_result(packet, parsed_packet)
{:error, reason} ->
%{
error: reason
}
end
end
end
defp build_third_party_traffic_result(packet, parsed_packet) do
case parse_nested_tunnel(packet) do
{:ok, nested_packet} ->
%{
third_party_packet: nested_packet,
data_type: :third_party_traffic,
raw_data: packet
}
{:error, _} ->
%{
third_party_packet: parsed_packet,
data_type: :third_party_traffic,
raw_data: packet
}
end
end
@spec parse_tunneled_packet(String.t()) :: {:ok, map()} | {:error, String.t()}
defp parse_tunneled_packet(packet) do
case String.split(packet, ":", parts: 2) do
[header, information] ->
parse_tunneled_packet_with_header(header, information)
_ ->
{:error, "Invalid tunneled packet format"}
end
end
defp parse_tunneled_packet_with_header(header, information) do
case parse_tunneled_header(header) do
{:ok, header_data} ->
parse_tunneled_packet_with_information(header_data, information)
{:error, reason} ->
{:error, "Invalid header: #{reason}"}
end
end
defp parse_tunneled_packet_with_information(header_data, information) do
case parse_datatype_safe(information) do
{:ok, data_type} ->
data_without_type = String.slice(information, 1..-1//1)
data_extended = parse_data(data_type, header_data.destination, data_without_type)
{:ok,
Map.merge(header_data, %{
information_field: information,
data_type: data_type,
data_extended: data_extended
})}
{:error, reason} ->
{:error, "Invalid data type: #{reason}"}
end
end
@spec parse_tunneled_header(String.t()) :: {:ok, map()} | {:error, String.t()}
defp parse_tunneled_header(header) do
case String.split(header, ">", parts: 2) do
[sender, path] ->
parse_sender_and_path(sender, path)
_ ->
{:error, "Invalid header format"}
end
end
defp parse_sender_and_path(sender, path) do
case parse_callsign(sender) do
{:ok, callsign_parts} ->
base_callsign = List.first(callsign_parts)
ssid = extract_ssid(List.last(callsign_parts))
case split_path_for_tunnel(path) do
{:ok, [destination, digi_path]} ->
{:ok,
%{
sender: sender,
base_callsign: base_callsign,
ssid: ssid,
destination: destination,
digi_path: digi_path
}}
{:error, reason} ->
{:error, "Invalid path: #{reason}"}
end
{:error, reason} ->
{:error, "Invalid callsign: #{reason}"}
end
end
defp extract_ssid(nil), do: nil
defp extract_ssid(s) when is_binary(s), do: s
defp extract_ssid(i) when is_integer(i), do: to_string(i)
defp extract_ssid(_), do: nil
defp split_path_for_tunnel(path) do
split_path(path)
end
# Add network tunneling support
@spec parse_network_tunnel(String.t()) :: {:ok, map()} | {:error, String.t()}
defp parse_network_tunnel(packet) do
# Network tunneling packets start with "}" and contain a complete APRS packet
case String.slice(packet, 1..-1//1) do
tunneled_packet ->
case parse_tunneled_packet(tunneled_packet) do
{:ok, parsed_packet} ->
{:ok,
Map.merge(parsed_packet, %{
tunnel_type: :network,
raw_data: packet
})}
{:error, reason} ->
{:error, "Invalid tunneled packet: #{reason}"}
end
end
end
# Add support for multiple levels of tunneling
defp parse_nested_tunnel(packet, depth \\ 0) do
cond do
depth > 3 ->
{:error, "Maximum tunnel depth exceeded"}
String.starts_with?(packet, "}") ->
case parse_network_tunnel(packet) do
{:ok, parsed_packet} -> handle_parsed_network_tunnel(parsed_packet, depth)
{:error, reason} -> {:error, reason}
end
true ->
{:error, "Not a tunneled packet"}
end
end
defp handle_parsed_network_tunnel(parsed_packet, depth) do
case Map.get(parsed_packet, :data_extended) do
%{raw_data: nested_data} when is_binary(nested_data) ->
case parse_nested_tunnel(nested_data, depth + 1) do
{:ok, nested_packet} -> {:ok, Map.put(parsed_packet, :nested_packet, nested_packet)}
{:error, _} -> {:ok, parsed_packet}
end
_ ->
{:ok, parsed_packet}
end
end
# Add DAO (Datum) extension support
@spec parse_dao_extension(String.t()) :: map() | nil
defp parse_dao_extension(comment) do
case Regex.run(~r/!([A-Za-z])([A-Za-z])([A-Za-z])!/, comment) do
[_, lat_dao, lon_dao, _] ->
%{
lat_dao: lat_dao,
lon_dao: lon_dao,
datum: "WGS84"
}
_ ->
nil
end
end
end

View file

@ -1,37 +0,0 @@
defmodule Parser.AX25 do
@moduledoc """
AX.25 callsign and path parsing/validation for APRS packets.
"""
@doc """
Parse and validate an AX.25 callsign. Returns {:ok, {base, ssid}} or {:error, reason}.
"""
@spec parse_callsign(String.t()) :: {:ok, {String.t(), String.t()}} | {:error, String.t()}
def parse_callsign(callsign) do
cond do
not is_binary(callsign) ->
{:error, "Invalid callsign format"}
byte_size(callsign) == 0 ->
{:error, "Empty callsign"}
String.contains?(callsign, "-") ->
case String.split(callsign, "-") do
[base, ssid] -> {:ok, {base, ssid}}
_ -> {:ok, {callsign, "0"}}
end
true ->
{:ok, {callsign, "0"}}
end
end
@doc """
Parse and validate an AX.25 path. Returns {:ok, [String.t()]} or {:error, reason}.
"""
@spec parse_path(String.t()) :: {:ok, [String.t()]} | {:error, String.t()}
def parse_path(_path) do
# Stub: actual logic to be implemented
{:error, "Not yet implemented"}
end
end

View file

@ -1,20 +0,0 @@
defmodule Parser.Compressed do
@moduledoc """
Compressed position parsing for APRS packets.
"""
alias Parser.Types.ParseError
@doc """
Parse a compressed position string. Returns a struct or ParseError.
"""
@spec parse(String.t()) :: map() | ParseError.t()
def parse(_compressed_str) do
# Stub: actual logic to be implemented
%ParseError{
error_code: :not_implemented,
error_message: "Compressed position parsing not yet implemented",
raw_data: nil
}
end
end

View file

@ -1,57 +0,0 @@
defmodule Parser.CompressedPositionHelpers do
@moduledoc """
Compressed position helpers for APRS.
"""
@spec convert_compressed_lat(binary()) :: {:ok, float()} | {:error, String.t()}
def convert_compressed_lat(lat) when is_binary(lat) and byte_size(lat) == 4 do
[l1, l2, l3, l4] = to_charlist(lat)
{:ok, 90 - ((l1 - 33) * 91 ** 3 + (l2 - 33) * 91 ** 2 + (l3 - 33) * 91 + l4 - 33) / 380_926}
end
def convert_compressed_lat(_), do: {:error, "Invalid compressed latitude"}
@spec convert_compressed_lon(binary()) :: {:ok, float()} | {:error, String.t()}
def convert_compressed_lon(lon) when is_binary(lon) and byte_size(lon) == 4 do
[l1, l2, l3, l4] = to_charlist(lon)
{:ok, -180 + ((l1 - 33) * 91 ** 3 + (l2 - 33) * 91 ** 2 + (l3 - 33) * 91 + l4 - 33) / 190_463}
end
def convert_compressed_lon(_), do: {:error, "Invalid compressed longitude"}
@spec calculate_compressed_ambiguity(String.t()) :: integer()
def calculate_compressed_ambiguity(compression_type) do
case compression_type do
" " -> 0
"!" -> 1
"\"" -> 2
"#" -> 3
"$" -> 4
_ -> 0
end
end
@doc false
def convert_to_base91(<<value::binary-size(4)>>) do
[v1, v2, v3, v4] = to_charlist(value)
(v1 - 33) * 91 * 91 * 91 + (v2 - 33) * 91 * 91 + (v3 - 33) * 91 + v4
end
@spec convert_compressed_cs(binary()) :: map()
def convert_compressed_cs(cs) do
[c, s] = to_charlist(cs)
c = c - 33
s = s - 33
case c do
x when x in ?!..?z ->
%{course: s * 4, speed: Aprs.Convert.speed(1.08 ** s - 1, :knots, :mph)}
?Z ->
%{range: 2 * 1.08 ** s}
_ ->
%{}
end
end
end

View file

@ -1,22 +0,0 @@
defmodule Parser.Core do
@moduledoc """
Main entry point for APRS packet parsing. Delegates to submodules for specific formats.
"""
alias Parser.Types.Packet
alias Parser.Types.ParseError
@doc """
Parse an APRS packet string into a Packet struct or return a ParseError struct.
"""
@spec parse(String.t()) :: {:ok, Packet.t()} | {:error, ParseError.t()}
def parse(_packet) do
# Stub: actual logic will delegate to submodules
{:error,
%ParseError{
error_code: :not_implemented,
error_message: "Not yet implemented",
raw_data: nil
}}
end
end

View file

@ -1,28 +0,0 @@
defmodule Parser.DeviceParser do
@moduledoc """
Extracts device identifier (TOCALL or Mic-E) from APRS packets.
"""
@doc """
Extract the device identifier from a packet map or raw packet string.
"""
def extract_device_identifier(%{destination: dest}) when is_binary(dest) do
# TOCALL is usually the first 6 chars of destination
String.slice(dest, 0, 6)
end
def extract_device_identifier(%{data_type: :mic_e, destination: dest}) when is_binary(dest) do
# Mic-E uses destination for device ID
String.slice(dest, 0, 6)
end
def extract_device_identifier(packet) when is_binary(packet) do
# Try to parse out the destination field from raw packet
case Regex.run(~r/^[^>]+>([^,]+),/, packet) do
[_, dest] -> String.slice(dest, 0, 6)
_ -> nil
end
end
def extract_device_identifier(_), do: nil
end

Some files were not shown because too many files have changed in this diff Show more