refactor to use external parser
This commit is contained in:
parent
2ba9f11656
commit
ce7c50caa1
202 changed files with 794 additions and 5100 deletions
6
.github/workflows/dependency-updates.yml
vendored
6
.github/workflows/dependency-updates.yml
vendored
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
tags: aprs:latest
|
||||
tags: aprsme:latest
|
||||
load: true
|
||||
no-cache: true
|
||||
pull: true
|
||||
|
|
@ -36,7 +36,7 @@ jobs:
|
|||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
image-ref: aprs:latest
|
||||
image-ref: aprsme:latest
|
||||
format: "sarif"
|
||||
output: "trivy-results.sarif"
|
||||
severity: "CRITICAL,HIGH"
|
||||
|
|
@ -59,7 +59,7 @@ jobs:
|
|||
|
||||
echo "### OS Package Vulnerabilities" >> 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
|
||||
|
||||
- name: Upload vulnerability summary
|
||||
|
|
|
|||
12
.github/workflows/docker-security-scan.yml
vendored
12
.github/workflows/docker-security-scan.yml
vendored
|
|
@ -45,7 +45,7 @@ jobs:
|
|||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
tags: aprs:${{ github.sha }}
|
||||
tags: aprsme:${{ github.sha }}
|
||||
load: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
|
@ -53,7 +53,7 @@ jobs:
|
|||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
image-ref: aprs:${{ github.sha }}
|
||||
image-ref: aprsme:${{ github.sha }}
|
||||
format: "sarif"
|
||||
output: "trivy-results.sarif"
|
||||
severity: "CRITICAL,HIGH"
|
||||
|
|
@ -70,7 +70,7 @@ jobs:
|
|||
- name: Run Trivy for outdated OS packages
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
image-ref: aprs:${{ github.sha }}
|
||||
image-ref: aprsme:${{ github.sha }}
|
||||
format: "table"
|
||||
output: "os-packages.txt"
|
||||
severity: "HIGH,CRITICAL"
|
||||
|
|
@ -79,7 +79,7 @@ jobs:
|
|||
- name: Run Trivy for outdated application dependencies
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
image-ref: aprs:${{ github.sha }}
|
||||
image-ref: aprsme:${{ github.sha }}
|
||||
format: "table"
|
||||
output: "app-packages.txt"
|
||||
severity: "HIGH,CRITICAL"
|
||||
|
|
@ -99,7 +99,7 @@ jobs:
|
|||
uses: docker/scout-action@v1
|
||||
with:
|
||||
command: quickview,cves
|
||||
image: aprs:${{ github.sha }}
|
||||
image: aprsme:${{ github.sha }}
|
||||
output-format: sarif
|
||||
only-severities: critical,high
|
||||
sarif-file: scout-results.sarif
|
||||
|
|
@ -116,7 +116,7 @@ jobs:
|
|||
if: always()
|
||||
run: |
|
||||
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 "### OS Package Vulnerabilities" >> security-summary.md
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -20,7 +20,7 @@ erl_crash.dump
|
|||
*.ez
|
||||
|
||||
# Ignore package tarball (built via "mix hex.build").
|
||||
aprs-*.tar
|
||||
aprsme-*.tar
|
||||
|
||||
# Ignore assets that are produced by build tools.
|
||||
/priv/static/assets/
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ WORKDIR /app
|
|||
|
||||
# Copy the Elixir release from builder stage
|
||||
# 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
|
||||
USER 0
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
* **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
|
||||
scope "/", AprsWeb do
|
||||
scope "/", AprsmeWeb do
|
||||
pipe_through :browser
|
||||
# ... other routes
|
||||
get "/health", PageController, :health
|
||||
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
|
||||
def health(conn, _params) do
|
||||
# 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
|
||||
```
|
||||
* **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>`).
|
||||
|
|
|
|||
|
|
@ -14,36 +14,36 @@ import Config
|
|||
#
|
||||
# For production it's recommended to configure a different adapter
|
||||
# at the `config/runtime.exs`.
|
||||
config :aprs, Aprs.Mailer, adapter: Swoosh.Adapters.Local
|
||||
config :aprsme, Aprsme.Mailer, adapter: Swoosh.Adapters.Local
|
||||
|
||||
# Configures the endpoint
|
||||
config :aprs, AprsWeb.Endpoint,
|
||||
config :aprsme, AprsmeWeb.Endpoint,
|
||||
url: [host: "localhost"],
|
||||
render_errors: [
|
||||
formats: [html: AprsWeb.ErrorHTML, json: AprsWeb.ErrorJSON],
|
||||
formats: [html: AprsmeWeb.ErrorHTML, json: AprsmeWeb.ErrorJSON],
|
||||
layout: false
|
||||
],
|
||||
pubsub_server: Aprs.PubSub,
|
||||
pubsub_server: Aprsme.PubSub,
|
||||
live_view: [signing_salt: "ees098qG"]
|
||||
|
||||
# Configure Oban for background jobs
|
||||
config :aprs, Oban,
|
||||
repo: Aprs.Repo,
|
||||
config :aprsme, Oban,
|
||||
repo: Aprsme.Repo,
|
||||
plugins: [
|
||||
{Oban.Plugins.Pruner, max_age: 60 * 60 * 24 * 7},
|
||||
{Oban.Plugins.Cron,
|
||||
crontab: [
|
||||
{"0 0 * * *", Aprs.Workers.PacketCleanupWorker},
|
||||
{"0 3 * * 1", Aprs.DeviceIdentification.Worker}
|
||||
{"0 0 * * *", Aprsme.Workers.PacketCleanupWorker},
|
||||
{"0 3 * * 1", Aprsme.DeviceIdentification.Worker}
|
||||
]}
|
||||
],
|
||||
queues: [default: 10, maintenance: 2]
|
||||
|
||||
config :aprs,
|
||||
ecto_repos: [Aprs.Repo]
|
||||
config :aprsme,
|
||||
ecto_repos: [Aprsme.Repo]
|
||||
|
||||
config :aprs,
|
||||
ecto_repos: [Aprs.Repo],
|
||||
config :aprsme,
|
||||
ecto_repos: [Aprsme.Repo],
|
||||
aprs_is_server: System.get_env("APRS_SERVER", "dallas.aprs2.net"),
|
||||
aprs_is_port: 10_152,
|
||||
aprs_is_default_filter: System.get_env("APRS_FILTER"),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import Config
|
||||
|
||||
# Configure your database
|
||||
config :aprs, Aprs.Repo,
|
||||
config :aprsme, Aprsme.Repo,
|
||||
# For development, we disable any cache and enable
|
||||
# debugging and code reloading.
|
||||
#
|
||||
|
|
@ -11,14 +11,14 @@ config :aprs, Aprs.Repo,
|
|||
username: "postgres",
|
||||
password: "postgres",
|
||||
hostname: "localhost",
|
||||
database: "aprs_dev",
|
||||
database: "aprsme_dev",
|
||||
stacktrace: true,
|
||||
show_sensitive_data_on_connection_error: true,
|
||||
pool_size: 10,
|
||||
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.
|
||||
# Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
|
||||
http: [ip: {127, 0, 0, 1}, port: 4000],
|
||||
|
|
@ -37,13 +37,13 @@ config :aprs, AprsWeb.Endpoint,
|
|||
]
|
||||
|
||||
# Watch static and templates for browser reloading.
|
||||
config :aprs, AprsWeb.Endpoint,
|
||||
config :aprsme, AprsmeWeb.Endpoint,
|
||||
live_reload: [
|
||||
patterns: [
|
||||
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
|
||||
~r"priv/gettext/.*(po)$",
|
||||
~r"lib/aprs_web/(live|views)/.*(ex)$",
|
||||
~r"lib/aprs_web/templates/.*(eex)$",
|
||||
~r"lib/aprsme_web/(live|views)/.*(ex)$",
|
||||
~r"lib/aprsme_web/templates/.*(eex)$",
|
||||
~r"assets/vendor/.*(js|css)$"
|
||||
]
|
||||
]
|
||||
|
|
@ -53,7 +53,7 @@ config :aprs, AprsWeb.Endpoint,
|
|||
# mix phx.gen.cert
|
||||
#
|
||||
# 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
|
||||
#
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import Config
|
|||
# manifest is generated by the `mix phx.digest` task,
|
||||
# which you should run after static files are built and
|
||||
# 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
|
||||
|
||||
|
|
@ -26,4 +26,4 @@ config :esbuild,
|
|||
config :logger, level: :info
|
||||
|
||||
# Configures Swoosh API Client
|
||||
config :swoosh, :api_client, Aprs.Finch
|
||||
config :swoosh, :api_client, Aprsme.Finch
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ import Config
|
|||
# If you use `mix release`, you need to explicitly enable the server
|
||||
# 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`
|
||||
# script that automatically sets the env var above.
|
||||
# Always start the server in production/docker environments
|
||||
if System.get_env("PHX_SERVER") || config_env() == :prod do
|
||||
config :aprs, AprsWeb.Endpoint, server: true
|
||||
config :aprsme, AprsmeWeb.Endpoint, server: true
|
||||
end
|
||||
|
||||
if config_env() == :prod do
|
||||
|
|
@ -46,14 +46,14 @@ if config_env() == :prod do
|
|||
host = System.get_env("PHX_HOST") || "example.com"
|
||||
port = String.to_integer(System.get_env("PORT") || "4000")
|
||||
|
||||
config :aprs, Aprs.Repo,
|
||||
config :aprsme, Aprsme.Repo,
|
||||
# ssl: true,
|
||||
url: database_url,
|
||||
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
|
||||
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"],
|
||||
http: [
|
||||
# Bind on all IPv4 interfaces for better container compatibility
|
||||
|
|
@ -66,10 +66,10 @@ if config_env() == :prod do
|
|||
secret_key_base: secret_key_base,
|
||||
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,
|
||||
ecto_repos: [Aprs.Repo],
|
||||
config :aprsme,
|
||||
ecto_repos: [Aprsme.Repo],
|
||||
aprs_is_server: System.get_env("APRS_SERVER", "dallas.aprs2.net"),
|
||||
aprs_is_port: 14_580,
|
||||
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 your endpoint configuration:
|
||||
#
|
||||
# config :aprs, AprsWeb.Endpoint,
|
||||
# config :aprsme, AprsmeWeb.Endpoint,
|
||||
# https: [
|
||||
# ...,
|
||||
# port: 443,
|
||||
|
|
@ -116,7 +116,7 @@ if config_env() == :prod do
|
|||
# We also recommend setting `force_ssl` in your endpoint, ensuring
|
||||
# no data is ever sent via http, always redirecting to https:
|
||||
#
|
||||
# config :aprs, AprsWeb.Endpoint,
|
||||
# config :aprsme, AprsmeWeb.Endpoint,
|
||||
# force_ssl: [hsts: true]
|
||||
#
|
||||
# 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
|
||||
# are not using SMTP. Here is an example of the configuration:
|
||||
#
|
||||
# config :aprs, Aprs.Mailer,
|
||||
# config :aprsme, Aprsme.Mailer,
|
||||
# adapter: Swoosh.Adapters.Mailgun,
|
||||
# api_key: System.get_env("MAILGUN_API_KEY"),
|
||||
# domain: System.get_env("MAILGUN_DOMAIN")
|
||||
|
|
|
|||
|
|
@ -1,40 +1,40 @@
|
|||
import Config
|
||||
|
||||
# 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
|
||||
#
|
||||
# The MIX_TEST_PARTITION environment variable can be used
|
||||
# to provide built-in test partitioning in CI environment.
|
||||
# Run `mix help test` for more information.
|
||||
config :aprs, Aprs.Repo,
|
||||
config :aprsme, Aprsme.Repo,
|
||||
username: "postgres",
|
||||
password: "postgres",
|
||||
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_size: System.schedulers_online() * 2,
|
||||
types: Aprs.PostgresTypes
|
||||
types: Aprsme.PostgresTypes
|
||||
|
||||
# We don't run a server during test. If one is required,
|
||||
# you can enable the server option below.
|
||||
config :aprs, AprsWeb.Endpoint,
|
||||
config :aprsme, AprsmeWeb.Endpoint,
|
||||
http: [ip: {127, 0, 0, 1}, port: 4002],
|
||||
secret_key_base: "IV9+ENaw9i8xjReRk4sULRvRgsmFVTGQwQGGrf4G+Q/SFMeHBCNWRlPXQ2YvT36R",
|
||||
server: false
|
||||
|
||||
# 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
|
||||
config :aprs, :initialize_replay_delay, 0
|
||||
config :aprsme, :initialize_replay_delay, 0
|
||||
|
||||
# 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
|
||||
config :aprs,
|
||||
config :aprsme,
|
||||
aprs_is_server: "mock.aprs.test",
|
||||
aprs_is_port: 14_580,
|
||||
aprs_is_default_filter: "r/33/-96/100",
|
||||
|
|
@ -43,7 +43,7 @@ config :aprs,
|
|||
disable_aprs_connection: true
|
||||
|
||||
# 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
|
||||
config :bcrypt_elixir, :log_rounds, 1
|
||||
|
|
|
|||
|
|
@ -49,5 +49,5 @@ spec:
|
|||
initialDelaySeconds: 30
|
||||
periodSeconds: 15
|
||||
# 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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
defmodule Aprs.Mailer do
|
||||
@moduledoc false
|
||||
use Swoosh.Mailer, otp_app: :aprs
|
||||
end
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
defmodule Aprs.Presence do
|
||||
@moduledoc false
|
||||
use Phoenix.Presence, otp_app: :aprs, pubsub_server: Aprs.PubSub
|
||||
end
|
||||
|
|
@ -1,20 +1,20 @@
|
|||
defmodule AprsWeb do
|
||||
defmodule AprsmeWeb do
|
||||
@moduledoc """
|
||||
The entrypoint for defining your web interface, such
|
||||
as controllers, components, channels, and so on.
|
||||
|
||||
This can be used in your application as:
|
||||
|
||||
use AprsWeb, :controller
|
||||
use AprsWeb, :html
|
||||
use AprsmeWeb, :controller
|
||||
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
|
||||
on imports, uses and aliases.
|
||||
|
||||
Do NOT define functions inside the quoted expressions
|
||||
below. Instead, define additional modules and import
|
||||
those modules here.
|
||||
below. Instead, define additional functions and do the
|
||||
imports in your modules.
|
||||
"""
|
||||
|
||||
def static_paths, do: ~w(assets fonts images aprs-symbols favicon.ico robots.txt)
|
||||
|
|
@ -23,9 +23,10 @@ defmodule AprsWeb do
|
|||
quote do
|
||||
use Phoenix.Router, helpers: false
|
||||
|
||||
# Import common connection and controller functions to use in pipelines
|
||||
import Phoenix.Controller
|
||||
import Phoenix.LiveView.Router
|
||||
|
||||
# Import common connection and controller functions to use in pipelines
|
||||
import Plug.Conn
|
||||
end
|
||||
end
|
||||
|
|
@ -33,17 +34,18 @@ defmodule AprsWeb do
|
|||
def channel do
|
||||
quote do
|
||||
use Phoenix.Channel
|
||||
|
||||
import AprsmeWeb.Gettext
|
||||
end
|
||||
end
|
||||
|
||||
def controller do
|
||||
quote do
|
||||
use Phoenix.Controller,
|
||||
namespace: AprsWeb,
|
||||
formats: [:html, :json],
|
||||
layouts: [html: AprsWeb.Layouts]
|
||||
layouts: [html: AprsmeWeb.Layouts]
|
||||
|
||||
import AprsWeb.Gettext
|
||||
import AprsmeWeb.Gettext
|
||||
import Plug.Conn
|
||||
|
||||
unquote(verified_routes())
|
||||
|
|
@ -53,7 +55,7 @@ defmodule AprsWeb do
|
|||
def live_view do
|
||||
quote do
|
||||
use Phoenix.LiveView,
|
||||
layout: {AprsWeb.Layouts, :app}
|
||||
layout: {AprsmeWeb.Layouts, :app}
|
||||
|
||||
unquote(html_helpers())
|
||||
end
|
||||
|
|
@ -82,11 +84,11 @@ defmodule AprsWeb do
|
|||
|
||||
defp html_helpers do
|
||||
quote do
|
||||
import AprsmeWeb.CoreComponents
|
||||
import AprsmeWeb.Gettext
|
||||
# HTML escaping functionality
|
||||
# Core UI components and translation
|
||||
import AprsWeb.CoreComponents
|
||||
import AprsWeb.Gettext
|
||||
import Phoenix.HTML
|
||||
# Core UI components and translation
|
||||
|
||||
# Shortcut for generating JS commands
|
||||
alias Phoenix.LiveView.JS
|
||||
|
|
@ -99,9 +101,9 @@ defmodule AprsWeb do
|
|||
def verified_routes do
|
||||
quote do
|
||||
use Phoenix.VerifiedRoutes,
|
||||
endpoint: AprsWeb.Endpoint,
|
||||
router: AprsWeb.Router,
|
||||
statics: AprsWeb.static_paths()
|
||||
endpoint: AprsmeWeb.Endpoint,
|
||||
router: AprsmeWeb.Router,
|
||||
statics: AprsmeWeb.static_paths()
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
5
lib/aprsme.ex
Normal file
5
lib/aprsme.ex
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
defmodule Aprsme do
|
||||
@moduledoc """
|
||||
The main application module for Aprsme.
|
||||
"""
|
||||
end
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
defmodule Aprs.Accounts do
|
||||
defmodule Aprsme.Accounts do
|
||||
@moduledoc """
|
||||
The Accounts context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
|
||||
alias Aprs.Accounts.User
|
||||
alias Aprs.Accounts.UserNotifier
|
||||
alias Aprs.Accounts.UserToken
|
||||
alias Aprs.Repo
|
||||
alias Aprsme.Accounts.User
|
||||
alias Aprsme.Accounts.UserNotifier
|
||||
alias Aprsme.Accounts.UserToken
|
||||
alias Aprsme.Repo
|
||||
|
||||
## Database getters
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Aprs.Accounts.User do
|
||||
defmodule Aprsme.Accounts.User do
|
||||
@moduledoc false
|
||||
use Ecto.Schema
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ defmodule Aprs.Accounts.User do
|
|||
|
||||
defp do_validate_unique_email(changeset, true) do
|
||||
changeset
|
||||
|> unsafe_validate_unique(:email, Aprs.Repo)
|
||||
|> unsafe_validate_unique(:email, Aprsme.Repo)
|
||||
|> unique_constraint(:email)
|
||||
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
|
||||
`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
|
||||
Bcrypt.verify_pass(password, hashed_password)
|
||||
end
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
defmodule Aprs.Accounts.UserNotifier do
|
||||
defmodule Aprsme.Accounts.UserNotifier do
|
||||
@moduledoc false
|
||||
import Swoosh.Email
|
||||
|
||||
alias Aprs.Mailer
|
||||
alias Aprsme.Mailer
|
||||
|
||||
# Delivers the email using the application mailer.
|
||||
defp deliver(recipient, subject, body) do
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
defmodule Aprs.Accounts.UserToken do
|
||||
defmodule Aprsme.Accounts.UserToken do
|
||||
@moduledoc false
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Aprs.Accounts.UserToken
|
||||
alias Aprsme.Accounts.UserToken
|
||||
|
||||
@hash_algorithm :sha256
|
||||
@rand_size 32
|
||||
|
|
@ -20,7 +20,7 @@ defmodule Aprs.Accounts.UserToken do
|
|||
field(:token, :binary)
|
||||
field(:context, :string)
|
||||
field(:sent_to, :string)
|
||||
belongs_to(:user, Aprs.Accounts.User)
|
||||
belongs_to(:user, Aprsme.Accounts.User)
|
||||
|
||||
timestamps(updated_at: false)
|
||||
end
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Aprs.Application do
|
||||
defmodule Aprsme.Application do
|
||||
# See https://hexdocs.pm/elixir/Application.html
|
||||
# for more information on OTP Applications
|
||||
@moduledoc false
|
||||
|
|
@ -16,35 +16,40 @@ defmodule Aprs.Application do
|
|||
|
||||
children = [
|
||||
# Start the Telemetry supervisor
|
||||
AprsWeb.Telemetry,
|
||||
AprsmeWeb.Telemetry,
|
||||
# Start the Ecto repository
|
||||
Aprs.Repo,
|
||||
Aprsme.Repo,
|
||||
# Start the PubSub system
|
||||
{Phoenix.PubSub, name: Aprs.PubSub},
|
||||
{Phoenix.PubSub, name: Aprsme.PubSub},
|
||||
# Start Finch
|
||||
{Finch, name: Aprs.Finch},
|
||||
{Finch, name: Aprsme.Finch},
|
||||
# Start the Endpoint (http/https)
|
||||
AprsWeb.Endpoint,
|
||||
# Start a worker by calling: Aprs.Worker.start_link(arg)
|
||||
# {Aprs.Worker, arg}
|
||||
AprsmeWeb.Endpoint,
|
||||
# Start a worker by calling: Aprsme.Worker.start_link(arg)
|
||||
# {Aprsme.Worker, arg}
|
||||
{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
|
||||
{Oban, :aprs |> Application.get_env(Oban, []) |> Keyword.put(:queues, default: 10, maintenance: 2)},
|
||||
Aprs.Presence,
|
||||
Aprs.AprsIsConnection,
|
||||
Aprs.PostgresNotifier
|
||||
{Oban, :aprsme |> Application.get_env(Oban, []) |> Keyword.put(:queues, default: 10, maintenance: 2)},
|
||||
Aprsme.Presence,
|
||||
Aprsme.AprsIsConnection,
|
||||
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
|
||||
# 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)
|
||||
|
||||
# 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}
|
||||
end
|
||||
|
|
@ -53,12 +58,12 @@ defmodule Aprs.Application do
|
|||
# whenever the application is updated.
|
||||
@impl true
|
||||
def config_change(changed, _new, removed) do
|
||||
AprsWeb.Endpoint.config_change(changed, removed)
|
||||
AprsmeWeb.Endpoint.config_change(changed, removed)
|
||||
:ok
|
||||
end
|
||||
|
||||
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)
|
||||
rescue
|
||||
error ->
|
||||
|
|
@ -70,7 +75,7 @@ defmodule Aprs.Application do
|
|||
end
|
||||
|
||||
defp maybe_add_is_supervisor(children, env) when env in [:prod, :dev] do
|
||||
children ++ [Aprs.Is.IsSupervisor]
|
||||
children ++ [Aprsme.Is.IsSupervisor]
|
||||
end
|
||||
|
||||
defp maybe_add_is_supervisor(children, _env), do: children
|
||||
|
|
@ -79,7 +84,7 @@ defmodule Aprs.Application do
|
|||
require Logger
|
||||
|
||||
Logger.info("Running database migrations...")
|
||||
Aprs.Release.migrate()
|
||||
Aprsme.Release.migrate()
|
||||
Logger.info("Database migrations completed")
|
||||
end
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Aprs.AprsIsConnection do
|
||||
defmodule Aprsme.AprsIsConnection do
|
||||
@moduledoc """
|
||||
Maintains a supervised TCP connection to APRS-IS, with reconnection logic and
|
||||
exponential backoff. Broadcasts each received line via Phoenix.PubSub and emits
|
||||
|
|
@ -43,13 +43,13 @@ defmodule Aprs.AprsIsConnection do
|
|||
case connect_aprs_is() do
|
||||
{:ok, socket} ->
|
||||
Logger.info("Connected to APRS-IS")
|
||||
:telemetry.execute([:aprs, :is, :connected], %{}, %{})
|
||||
:telemetry.execute([:aprsme, :is, :connected], %{}, %{})
|
||||
{:noreply, %{state | socket: socket, backoff: @reconnect_initial}}
|
||||
|
||||
{:error, reason} ->
|
||||
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)
|
||||
{:noreply, %{state | socket: nil, backoff: min(state.backoff * 2, @reconnect_max)}}
|
||||
end
|
||||
|
|
@ -58,15 +58,15 @@ defmodule Aprs.AprsIsConnection do
|
|||
@impl true
|
||||
def handle_info({:tcp, _socket, data}, state) do
|
||||
# Each line received from APRS-IS
|
||||
Phoenix.PubSub.broadcast(Aprs.PubSub, @pubsub_topic, {:aprs_is_line, data})
|
||||
:telemetry.execute([:aprs, :is, :packet], %{size: byte_size(data)}, %{data: data})
|
||||
Phoenix.PubSub.broadcast(Aprsme.PubSub, @pubsub_topic, {:aprsme_is_line, data})
|
||||
:telemetry.execute([:aprsme, :is, :packet], %{size: byte_size(data)}, %{data: data})
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:tcp_closed, _socket}, state) do
|
||||
Logger.warning("APRS-IS connection closed. Reconnecting...")
|
||||
:telemetry.execute([:aprs, :is, :disconnected], %{}, %{})
|
||||
:telemetry.execute([:aprsme, :is, :disconnected], %{}, %{})
|
||||
schedule_connect(@reconnect_initial)
|
||||
{:noreply, %{state | socket: nil, backoff: @reconnect_initial}}
|
||||
end
|
||||
|
|
@ -74,7 +74,7 @@ defmodule Aprs.AprsIsConnection do
|
|||
@impl true
|
||||
def handle_info({:tcp_error, _socket, reason}, state) do
|
||||
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)
|
||||
{:noreply, %{state | socket: nil, backoff: @reconnect_initial}}
|
||||
end
|
||||
|
|
@ -102,11 +102,11 @@ defmodule Aprs.AprsIsConnection do
|
|||
end
|
||||
|
||||
defp connect_aprs_is do
|
||||
host = Application.get_env(:aprs, :aprs_is_host, ~c"rotate.aprs2.net")
|
||||
port = Application.get_env(:aprs, :aprs_is_port, 14_580)
|
||||
callsign = Application.get_env(:aprs, :aprs_is_callsign, "N0CALL")
|
||||
passcode = Application.get_env(:aprs, :aprs_is_passcode, "00000")
|
||||
filter = Application.get_env(:aprs, :aprs_is_filter, "")
|
||||
host = Application.get_env(:aprsme, :aprsme_is_host, ~c"rotate.aprs2.net")
|
||||
port = Application.get_env(:aprsme, :aprsme_is_port, 14_580)
|
||||
callsign = Application.get_env(:aprsme, :aprsme_is_callsign, "N0CALL")
|
||||
passcode = Application.get_env(:aprsme, :aprsme_is_passcode, "00000")
|
||||
filter = Application.get_env(:aprsme, :aprsme_is_filter, "")
|
||||
|
||||
opts = [:binary, active: true, packet: :line, keepalive: true]
|
||||
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
defmodule Aprs.Archiver do
|
||||
defmodule Aprsme.Archiver do
|
||||
@moduledoc false
|
||||
use GenServer
|
||||
|
||||
alias AprsWeb.Endpoint
|
||||
alias AprsmeWeb.Endpoint
|
||||
|
||||
require Jason
|
||||
require Logger
|
||||
|
||||
# alias Aprs.{Packet, Repo}
|
||||
# alias Aprsme.{Packet, Repo}
|
||||
@topic "call"
|
||||
|
||||
# API
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Aprs.BadPacket do
|
||||
defmodule Aprsme.BadPacket do
|
||||
@moduledoc false
|
||||
use Ecto.Schema
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ defmodule Aprs.BadPacket do
|
|||
@type t :: %__MODULE__{}
|
||||
|
||||
@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
|
||||
bad_packet
|
||||
|> cast(attrs, [:raw_packet, :error_message, :error_type, :attempted_at])
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Aprs.Convert do
|
||||
defmodule Aprsme.Convert do
|
||||
@moduledoc false
|
||||
|
||||
@spec wind(number(), :ultimeter, :mph) :: float()
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
defmodule Aprs.DataExtended do
|
||||
defmodule Aprsme.DataExtended do
|
||||
@moduledoc false
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Aprs.DataExtended
|
||||
alias Aprsme.DataExtended
|
||||
|
||||
embedded_schema do
|
||||
field :aprs_messaging, :boolean, default: false
|
||||
field :aprsme_messaging, :boolean, default: false
|
||||
field :comment, :string
|
||||
field :data_type, :string
|
||||
field :latitude, :decimal
|
||||
|
|
@ -19,11 +19,11 @@ defmodule Aprs.DataExtended do
|
|||
@type t :: %__MODULE__{}
|
||||
|
||||
@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
|
||||
data_extended
|
||||
|> cast(attrs, [
|
||||
:aprs_messaging,
|
||||
:aprsme_messaging,
|
||||
:comment,
|
||||
:data_type,
|
||||
:latitude,
|
||||
|
|
@ -32,7 +32,7 @@ defmodule Aprs.DataExtended do
|
|||
:symbol_table_id
|
||||
])
|
||||
|> validate_required([
|
||||
:aprs_messaging,
|
||||
:aprsme_messaging,
|
||||
:comment,
|
||||
:data_type,
|
||||
:symbol_code,
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
defmodule Aprs.DeviceIdentification do
|
||||
defmodule Aprsme.DeviceIdentification do
|
||||
@moduledoc """
|
||||
Handles APRS device identification based on the APRS device identification database.
|
||||
"""
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Aprs.Devices
|
||||
alias Aprs.Repo
|
||||
alias Aprsme.Devices
|
||||
alias Aprsme.Repo
|
||||
|
||||
@device_patterns [
|
||||
{~r/^ \x00\x00$/, "Original MIC-E"},
|
||||
|
|
@ -41,13 +41,13 @@ defmodule Aprs.DeviceIdentification do
|
|||
|
||||
## Examples
|
||||
|
||||
iex> Aprs.DeviceIdentification.identify_device(">" <> <<0>> <> "^")
|
||||
iex> Aprsme.DeviceIdentification.identify_device(">" <> <<0>> <> "^")
|
||||
"Kenwood TH-D74"
|
||||
|
||||
iex> Aprs.DeviceIdentification.identify_device("`_#")
|
||||
iex> Aprsme.DeviceIdentification.identify_device("`_#")
|
||||
"Yaesu VX-8G"
|
||||
|
||||
iex> Aprs.DeviceIdentification.identify_device("not-a-match")
|
||||
iex> Aprsme.DeviceIdentification.identify_device("not-a-match")
|
||||
"Unknown"
|
||||
"""
|
||||
@spec identify_device(String.t()) :: String.t()
|
||||
|
|
@ -64,10 +64,10 @@ defmodule Aprs.DeviceIdentification do
|
|||
|
||||
## Examples
|
||||
|
||||
iex> "Kenwood" in Aprs.DeviceIdentification.known_manufacturers()
|
||||
iex> "Kenwood" in Aprsme.DeviceIdentification.known_manufacturers()
|
||||
true
|
||||
|
||||
iex> Enum.member?(Aprs.DeviceIdentification.known_manufacturers(), "AP510")
|
||||
iex> Enum.member?(Aprsme.DeviceIdentification.known_manufacturers(), "AP510")
|
||||
true
|
||||
"""
|
||||
@spec known_manufacturers() :: [String.t()]
|
||||
|
|
@ -93,10 +93,10 @@ defmodule Aprs.DeviceIdentification do
|
|||
|
||||
## Examples
|
||||
|
||||
iex> Aprs.DeviceIdentification.known_models("Kenwood")
|
||||
iex> Aprsme.DeviceIdentification.known_models("Kenwood")
|
||||
["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()]
|
||||
|
|
@ -131,7 +131,7 @@ defmodule Aprs.DeviceIdentification do
|
|||
def fetch_and_upsert_devices do
|
||||
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, json} = Jason.decode(body)
|
||||
upsert_devices(json)
|
||||
|
|
@ -170,7 +170,7 @@ defmodule Aprs.DeviceIdentification do
|
|||
|
||||
# Helper to enqueue the job
|
||||
def enqueue_refresh_job do
|
||||
Oban.insert!(Aprs.DeviceIdentification.Worker.new(%{}))
|
||||
Oban.insert!(Aprsme.DeviceIdentification.Worker.new(%{}))
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -179,6 +179,8 @@ defmodule Aprs.DeviceIdentification do
|
|||
|
||||
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
|
||||
# Fetch all device patterns from DB
|
||||
devices = Repo.all(Devices)
|
||||
|
|
@ -218,13 +220,13 @@ defmodule Aprs.DeviceIdentification do
|
|||
end
|
||||
end
|
||||
|
||||
defmodule Aprs.DeviceIdentification.Worker do
|
||||
defmodule Aprsme.DeviceIdentification.Worker do
|
||||
@moduledoc false
|
||||
use Oban.Worker, queue: :default, max_attempts: 1
|
||||
|
||||
@impl true
|
||||
def perform(_job) do
|
||||
Aprs.DeviceIdentification.maybe_refresh_devices()
|
||||
Aprsme.DeviceIdentification.maybe_refresh_devices()
|
||||
:ok
|
||||
end
|
||||
end
|
||||
32
lib/aprsme/device_parser.ex
Normal file
32
lib/aprsme/device_parser.ex
Normal 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
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Aprs.Devices do
|
||||
defmodule Aprsme.Devices do
|
||||
@moduledoc false
|
||||
use Ecto.Schema
|
||||
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
# test/support/devices_seeder.exs
|
||||
alias Aprs.Devices
|
||||
alias Aprs.Repo
|
||||
|
||||
defmodule DevicesSeeder do
|
||||
defmodule Aprsme.DevicesSeeder do
|
||||
@moduledoc false
|
||||
|
||||
alias Aprsme.Devices
|
||||
alias Aprsme.Repo
|
||||
|
||||
def seed_from_json(path \\ "test/support/test_devices.json") do
|
||||
{:ok, body} = File.read(path)
|
||||
{:ok, json} = Jason.decode(body)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Aprs.EncodingUtils do
|
||||
defmodule Aprsme.EncodingUtils do
|
||||
@moduledoc """
|
||||
Utilities for handling encoding issues in APRS packet data.
|
||||
|
||||
|
|
@ -7,7 +7,7 @@ defmodule Aprs.EncodingUtils do
|
|||
to web clients.
|
||||
"""
|
||||
|
||||
alias Parser.Types.MicE
|
||||
alias Aprs.Types.MicE
|
||||
|
||||
@doc """
|
||||
Sanitizes a binary to ensure it can be safely JSON encoded.
|
||||
|
|
@ -18,10 +18,10 @@ defmodule Aprs.EncodingUtils do
|
|||
|
||||
## Examples
|
||||
|
||||
iex> Aprs.EncodingUtils.sanitize_string("Hello World")
|
||||
iex> Aprsme.EncodingUtils.sanitize_string("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"
|
||||
"""
|
||||
@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.
|
||||
"""
|
||||
@spec sanitize_packet(struct() | map()) :: struct() | map()
|
||||
def sanitize_packet(%Aprs.Packet{} = packet) do
|
||||
def sanitize_packet(%Aprsme.Packet{} = packet) do
|
||||
%{
|
||||
packet
|
||||
| information_field: sanitize_string(packet.information_field),
|
||||
|
|
@ -97,7 +97,7 @@ defmodule Aprs.EncodingUtils do
|
|||
|
||||
## Examples
|
||||
|
||||
iex> Aprs.EncodingUtils.to_hex(<<72, 101, 108, 108, 111>>)
|
||||
iex> Aprsme.EncodingUtils.to_hex(<<72, 101, 108, 108, 111>>)
|
||||
"48656C6C6F"
|
||||
"""
|
||||
@spec to_hex(binary()) :: String.t()
|
||||
|
|
@ -113,10 +113,10 @@ defmodule Aprs.EncodingUtils do
|
|||
|
||||
## Examples
|
||||
|
||||
iex> Aprs.EncodingUtils.encoding_info("Hello")
|
||||
iex> Aprsme.EncodingUtils.encoding_info("Hello")
|
||||
%{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}
|
||||
"""
|
||||
@spec encoding_info(binary()) :: map()
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Aprs.Is do
|
||||
defmodule Aprsme.Is do
|
||||
@moduledoc false
|
||||
use GenServer
|
||||
|
||||
|
|
@ -29,8 +29,8 @@ defmodule Aprs.Is do
|
|||
|
||||
@impl true
|
||||
def init(_opts) do
|
||||
env = Application.get_env(:aprs, :env)
|
||||
disable_connection = Application.get_env(:aprs, :disable_aprs_connection, false)
|
||||
env = Application.get_env(:aprsme, :env)
|
||||
disable_connection = Application.get_env(:aprsme, :disable_aprs_connection, false)
|
||||
|
||||
do_init(env, disable_connection)
|
||||
end
|
||||
|
|
@ -53,11 +53,11 @@ defmodule Aprs.Is do
|
|||
Process.sleep(2000)
|
||||
|
||||
# Get startup parameters
|
||||
server = Application.get_env(:aprs, :aprs_is_server, ~c"rotate.aprs2.net")
|
||||
port = Application.get_env(:aprs, :aprs_is_port, 14_580)
|
||||
default_filter = Application.get_env(:aprs, :aprs_is_default_filter, "r/33/-96/100")
|
||||
aprs_user_id = Application.get_env(:aprs, :aprs_is_login_id, "W5ISP")
|
||||
aprs_passcode = Application.get_env(:aprs, :aprs_is_password, "-1")
|
||||
server = Application.get_env(:aprsme, :aprsme_is_server, ~c"rotate.aprs2.net")
|
||||
port = Application.get_env(:aprsme, :aprsme_is_port, 14_580)
|
||||
default_filter = Application.get_env(:aprsme, :aprsme_is_default_filter, "r/33/-96/100")
|
||||
aprs_user_id = Application.get_env(:aprsme, :aprsme_is_login_id, "W5ISP")
|
||||
aprs_passcode = Application.get_env(:aprsme, :aprsme_is_password, "-1")
|
||||
|
||||
# Record connection start time
|
||||
connected_at = DateTime.utc_now()
|
||||
|
|
@ -95,7 +95,7 @@ defmodule Aprs.Is do
|
|||
else
|
||||
_ ->
|
||||
Logger.error("Unable to establish connection or log in to APRS-IS")
|
||||
{:stop, :aprs_connection_failed}
|
||||
{:stop, :aprsme_connection_failed}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -110,8 +110,8 @@ defmodule Aprs.Is do
|
|||
case Process.whereis(__MODULE__) do
|
||||
nil ->
|
||||
# GenServer is not running (disconnected)
|
||||
server = Application.get_env(:aprs, :aprs_is_server, ~c"rotate.aprs2.net")
|
||||
port = Application.get_env(:aprs, :aprs_is_port, 14_580)
|
||||
server = Application.get_env(:aprsme, :aprsme_is_server, nil)
|
||||
port = Application.get_env(:aprsme, :aprsme_is_port, 14_580)
|
||||
|
||||
%{
|
||||
connected: false,
|
||||
|
|
@ -119,10 +119,10 @@ defmodule Aprs.Is do
|
|||
port: port,
|
||||
connected_at: nil,
|
||||
uptime_seconds: 0,
|
||||
login_id: Application.get_env(:aprs, :aprs_is_login_id, "W5ISP"),
|
||||
filter: Application.get_env(:aprs, :aprs_is_default_filter, "r/33/-96/100"),
|
||||
login_id: Application.get_env(:aprsme, :aprsme_is_login_id, "W5ISP"),
|
||||
filter: Application.get_env(:aprsme, :aprsme_is_default_filter, "r/33/-96/100"),
|
||||
packet_stats: default_packet_stats(),
|
||||
stored_packet_count: Aprs.Packets.get_total_packet_count()
|
||||
stored_packet_count: Aprsme.Packets.get_total_packet_count()
|
||||
}
|
||||
|
||||
_pid ->
|
||||
|
|
@ -131,8 +131,8 @@ defmodule Aprs.Is do
|
|||
catch
|
||||
:exit, _ ->
|
||||
# GenServer exists but not responding
|
||||
server = Application.get_env(:aprs, :aprs_is_server, ~c"rotate.aprs2.net")
|
||||
port = Application.get_env(:aprs, :aprs_is_port, 14_580)
|
||||
server = Application.get_env(:aprsme, :aprsme_is_server, ~c"rotate.aprs2.net")
|
||||
port = Application.get_env(:aprsme, :aprsme_is_port, 14_580)
|
||||
|
||||
%{
|
||||
connected: false,
|
||||
|
|
@ -140,10 +140,10 @@ defmodule Aprs.Is do
|
|||
port: port,
|
||||
connected_at: nil,
|
||||
uptime_seconds: 0,
|
||||
login_id: Application.get_env(:aprs, :aprs_is_login_id, "W5ISP"),
|
||||
filter: Application.get_env(:aprs, :aprs_is_default_filter, "r/33/-96/100"),
|
||||
login_id: Application.get_env(:aprsme, :aprsme_is_login_id, "W5ISP"),
|
||||
filter: Application.get_env(:aprsme, :aprsme_is_default_filter, "r/33/-96/100"),
|
||||
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
|
||||
|
|
@ -167,8 +167,8 @@ defmodule Aprs.Is do
|
|||
{:ok, :ssl.sslsocket()} | {:error, any()}
|
||||
defp connect_to_aprs_is(server, port) do
|
||||
# Additional safeguard: prevent connections in test environment
|
||||
env = Application.get_env(:aprs, :env)
|
||||
disable_connection = Application.get_env(:aprs, :disable_aprs_connection, false)
|
||||
env = Application.get_env(:aprsme, :env)
|
||||
disable_connection = Application.get_env(:aprsme, :disable_aprs_connection, false)
|
||||
|
||||
do_connect_to_aprs_is(server, port, env, disable_connection)
|
||||
end
|
||||
|
|
@ -186,7 +186,8 @@ defmodule Aprs.Is do
|
|||
defp do_connect_to_aprs_is(server, port, _env, false) do
|
||||
Logger.debug("Connecting to: #{server}:#{port}")
|
||||
opts = [:binary, active: true]
|
||||
:gen_tcp.connect(String.to_charlist(server), port, opts)
|
||||
server_string = if is_list(server), do: List.to_string(server), else: server
|
||||
:gen_tcp.connect(String.to_charlist(server_string), port, opts)
|
||||
end
|
||||
|
||||
@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()
|
||||
defp create_timer(timeout) do
|
||||
Process.send_after(self(), :aprs_no_message_timeout, timeout)
|
||||
Process.send_after(self(), :aprsme_no_message_timeout, timeout)
|
||||
end
|
||||
|
||||
@spec create_keepalive_timer(non_neg_integer()) :: reference()
|
||||
|
|
@ -212,7 +213,7 @@ defmodule Aprs.Is do
|
|||
|
||||
@impl true
|
||||
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
|
||||
message = message <> "{" <> to_string(next_ack_number) <> "\r"
|
||||
|
||||
|
|
@ -231,16 +232,16 @@ defmodule Aprs.Is do
|
|||
login_id: state.login_params.user_id,
|
||||
filter: state.login_params.filter,
|
||||
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}
|
||||
end
|
||||
|
||||
@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.")
|
||||
{:stop, :aprs_timeout, state}
|
||||
{:stop, :aprsme_timeout, state}
|
||||
end
|
||||
|
||||
def handle_info(:send_keepalive, state) do
|
||||
|
|
@ -283,7 +284,7 @@ defmodule Aprs.Is do
|
|||
end)
|
||||
|
||||
# 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
|
||||
|
|
@ -318,7 +319,7 @@ defmodule Aprs.Is do
|
|||
end)
|
||||
|
||||
# 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
|
||||
|
|
@ -408,7 +409,7 @@ defmodule Aprs.Is do
|
|||
def dispatch(""), do: nil
|
||||
|
||||
def dispatch(message) do
|
||||
case Parser.parse(message) do
|
||||
case Aprs.parse(message) do
|
||||
{:ok, parsed_message} ->
|
||||
# Store the packet in the database for future replay
|
||||
# Use Task to avoid slowing down the main process
|
||||
|
|
@ -424,13 +425,13 @@ defmodule Aprs.Is do
|
|||
attrs = struct_to_map(packet_data)
|
||||
|
||||
# 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
|
||||
attrs = normalize_data_type(attrs)
|
||||
|
||||
# Store in database through the Packets context
|
||||
case Aprs.Packets.store_packet(attrs) do
|
||||
case Aprsme.Packets.store_packet(attrs) do
|
||||
{:ok, _packet} ->
|
||||
# Packet stored successfully
|
||||
:ok
|
||||
|
|
@ -455,19 +456,19 @@ defmodule Aprs.Is do
|
|||
end)
|
||||
|
||||
# Broadcast to live clients
|
||||
AprsWeb.Endpoint.broadcast("aprs_messages", "packet", parsed_message)
|
||||
AprsmeWeb.Endpoint.broadcast("aprs_messages", "packet", parsed_message)
|
||||
|
||||
{: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",
|
||||
type: "ParseError"
|
||||
})
|
||||
|
||||
{: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
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Aprs.Is.IsSupervisor do
|
||||
defmodule Aprsme.Is.IsSupervisor do
|
||||
@moduledoc false
|
||||
use Supervisor
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ defmodule Aprs.Is.IsSupervisor do
|
|||
@impl true
|
||||
def init(:ok) do
|
||||
children = [
|
||||
{Aprs.Is, []}
|
||||
{Aprsme.Is, []}
|
||||
]
|
||||
|
||||
# Supervisor will restart children max 3 times in 5 seconds
|
||||
4
lib/aprsme/mailer.ex
Normal file
4
lib/aprsme/mailer.ex
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
defmodule Aprsme.Mailer do
|
||||
@moduledoc false
|
||||
use Swoosh.Mailer, otp_app: :aprsme
|
||||
end
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
defmodule Aprs.Packet do
|
||||
defmodule Aprsme.Packet do
|
||||
@moduledoc false
|
||||
use Aprs.Schema
|
||||
use Aprsme.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Aprs.DataExtended
|
||||
alias Parser.Types.MicE
|
||||
alias Aprs.Types.MicE
|
||||
alias Aprsme.DataExtended
|
||||
|
||||
schema "packets" do
|
||||
field(:base_callsign, :string)
|
||||
|
|
@ -30,7 +30,7 @@ defmodule Aprs.Packet do
|
|||
# Additional packet data
|
||||
field(:comment, :string)
|
||||
field(:timestamp, :string)
|
||||
field(:aprs_messaging, :boolean, default: false)
|
||||
field(:aprsme_messaging, :boolean, default: false)
|
||||
|
||||
# Weather data
|
||||
field(:temperature, :float)
|
||||
|
|
@ -65,7 +65,7 @@ defmodule Aprs.Packet do
|
|||
@type t :: %__MODULE__{}
|
||||
|
||||
@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
|
||||
# Convert atom data_type to string
|
||||
attrs = normalize_data_type(attrs)
|
||||
|
|
@ -90,7 +90,7 @@ defmodule Aprs.Packet do
|
|||
:symbol_table_id,
|
||||
:comment,
|
||||
:timestamp,
|
||||
:aprs_messaging,
|
||||
:aprsme_messaging,
|
||||
:temperature,
|
||||
:humidity,
|
||||
:wind_speed,
|
||||
|
|
@ -277,8 +277,8 @@ defmodule Aprs.Packet do
|
|||
|> maybe_put(:comment, data_extended[:comment] || data_extended["comment"])
|
||||
|> maybe_put(:timestamp, data_extended[:timestamp] || data_extended["timestamp"])
|
||||
|> maybe_put(
|
||||
:aprs_messaging,
|
||||
data_extended[:aprs_messaging?] || data_extended["aprs_messaging?"]
|
||||
:aprsme_messaging,
|
||||
data_extended[:aprsme_messaging?] || data_extended["aprs_messaging?"]
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -519,14 +519,14 @@ defmodule Aprs.Packet do
|
|||
@doc """
|
||||
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(_), do: nil
|
||||
|
||||
@doc """
|
||||
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(_), do: nil
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Aprs.PacketReplay do
|
||||
defmodule Aprsme.PacketReplay do
|
||||
@moduledoc """
|
||||
Handles replaying of historical APRS packets alongside live packets.
|
||||
|
||||
|
|
@ -11,8 +11,8 @@ defmodule Aprs.PacketReplay do
|
|||
|
||||
use GenServer
|
||||
|
||||
alias Aprs.Packets
|
||||
alias AprsWeb.Endpoint
|
||||
alias Aprsme.Packets
|
||||
alias AprsmeWeb.Endpoint
|
||||
|
||||
require Logger
|
||||
|
||||
|
|
@ -393,7 +393,7 @@ defmodule Aprs.PacketReplay do
|
|||
|
||||
@spec via_tuple(String.t()) :: {:via, Registry, {atom(), String.t()}}
|
||||
defp via_tuple(user_id) do
|
||||
{:via, Registry, {Aprs.ReplayRegistry, "replay:#{user_id}"}}
|
||||
{:via, Registry, {Aprsme.ReplayRegistry, "replay:#{user_id}"}}
|
||||
end
|
||||
|
||||
@spec sanitize_packet_for_transport(any()) :: map()
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
defmodule Aprs.PacketReplayBehaviour do
|
||||
defmodule Aprsme.PacketReplayBehaviour do
|
||||
@moduledoc """
|
||||
Behavior definition for the PacketReplay module.
|
||||
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 pause_replay(reference()) :: :ok | {:error, :not_found}
|
||||
@callback resume_replay(reference()) :: :ok | {:error, :not_found}
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
defmodule Aprs.Packets do
|
||||
defmodule Aprsme.Packets do
|
||||
@moduledoc """
|
||||
The Packets context.
|
||||
"""
|
||||
|
||||
@behaviour Aprs.PacketsBehaviour
|
||||
@behaviour Aprsme.PacketsBehaviour
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
|
||||
alias Aprs.BadPacket
|
||||
alias Aprs.Packet
|
||||
alias Aprs.Repo
|
||||
alias Parser.Types.MicE
|
||||
alias Aprs.Types.MicE
|
||||
alias Aprsme.BadPacket
|
||||
alias Aprsme.Packet
|
||||
alias Aprsme.Repo
|
||||
|
||||
@doc """
|
||||
Stores a packet in the database.
|
||||
|
|
@ -34,8 +34,8 @@ defmodule Aprs.Packets do
|
|||
end)
|
||||
|> normalize_ssid()
|
||||
|> then(fn attrs ->
|
||||
device_identifier = Parser.DeviceParser.extract_device_identifier(packet_data)
|
||||
matched_device = Aprs.DeviceIdentification.lookup_device_by_identifier(device_identifier)
|
||||
device_identifier = Aprsme.DeviceParser.extract_device_identifier(packet_data)
|
||||
matched_device = Aprsme.DeviceIdentification.lookup_device_by_identifier(device_identifier)
|
||||
canonical_identifier = if matched_device, do: matched_device.identifier, else: device_identifier
|
||||
Map.put(attrs, :device_identifier, canonical_identifier)
|
||||
end)
|
||||
|
|
@ -460,7 +460,7 @@ defmodule Aprs.Packets do
|
|||
"""
|
||||
@impl true
|
||||
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)
|
||||
|
||||
{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(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: ""
|
||||
end
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Aprs.PacketsBehaviour do
|
||||
defmodule Aprsme.PacketsBehaviour do
|
||||
@moduledoc """
|
||||
Behaviour for Packets module to allow mocking in tests
|
||||
"""
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Aprs.Passcode do
|
||||
defmodule Aprsme.Passcode do
|
||||
@moduledoc """
|
||||
Module for generating APRS passcodes from callsigns.
|
||||
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
|
||||
|
||||
## Examples
|
||||
iex> Aprs.Passcode.generate("W5ISP")
|
||||
iex> Aprsme.Passcode.generate("W5ISP")
|
||||
15748
|
||||
"""
|
||||
@spec generate(String.t()) :: non_neg_integer()
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Aprs.PostgresNotifier do
|
||||
defmodule Aprsme.PostgresNotifier do
|
||||
@moduledoc """
|
||||
Listens to PostgreSQL NOTIFY events on the "aprs_events" and "aprs_packets" channels and broadcasts
|
||||
them via Phoenix.PubSub for reactive, event-driven updates.
|
||||
|
|
@ -6,9 +6,9 @@ defmodule Aprs.PostgresNotifier do
|
|||
use GenServer
|
||||
|
||||
@event_channel "aprs_events"
|
||||
@event_topic "postgres:aprs_events"
|
||||
@event_topic "postgres:aprsme_events"
|
||||
@packet_channel "aprs_packets"
|
||||
@packet_topic "postgres:aprs_packets"
|
||||
@packet_topic "postgres:aprsme_packets"
|
||||
|
||||
def start_link(_opts) do
|
||||
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
|
||||
|
|
@ -16,7 +16,7 @@ defmodule Aprs.PostgresNotifier do
|
|||
|
||||
@impl true
|
||||
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, _ref2} = Postgrex.Notifications.listen(conn, @packet_channel)
|
||||
{:ok, %{conn: conn}}
|
||||
|
|
@ -24,14 +24,14 @@ defmodule Aprs.PostgresNotifier do
|
|||
|
||||
@impl true
|
||||
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}
|
||||
end
|
||||
|
||||
def handle_info({:notification, _conn, _pid, @packet_channel, payload}, state) do
|
||||
case Jason.decode(payload) do
|
||||
{:ok, packet} ->
|
||||
Phoenix.PubSub.broadcast(Aprs.PubSub, @packet_topic, {:postgres_packet, packet})
|
||||
Phoenix.PubSub.broadcast(Aprsme.PubSub, @packet_topic, {:postgres_packet, packet})
|
||||
|
||||
_ ->
|
||||
:noop
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
Postgrex.Types.define(
|
||||
Aprs.PostgresTypes,
|
||||
Aprsme.PostgresTypes,
|
||||
[Geo.PostGIS.Extension] ++ Ecto.Adapters.Postgres.extensions(),
|
||||
json: Jason
|
||||
)
|
||||
4
lib/aprsme/presence.ex
Normal file
4
lib/aprsme/presence.ex
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
defmodule Aprsme.Presence do
|
||||
@moduledoc false
|
||||
use Phoenix.Presence, otp_app: :aprsme, pubsub_server: Aprsme.PubSub
|
||||
end
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
defmodule Aprs.Release do
|
||||
defmodule Aprsme.Release do
|
||||
@moduledoc """
|
||||
Used for executing DB release tasks when run in production without Mix
|
||||
installed.
|
||||
"""
|
||||
@app :aprs
|
||||
@app :aprsme
|
||||
|
||||
def migrate do
|
||||
require Logger
|
||||
|
|
@ -12,7 +12,7 @@ defmodule Aprs.Release do
|
|||
Logger.info("Deployment timestamp: #{deployed_at}")
|
||||
|
||||
# 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
|
||||
|
||||
def rollback(repo, version) do
|
||||
|
|
@ -33,7 +33,7 @@ defmodule Aprs.Release do
|
|||
deployed_at = read_deployment_timestamp()
|
||||
|
||||
# Add to application config
|
||||
Application.put_env(:aprs, :deployed_at, deployed_at)
|
||||
Application.put_env(:aprsme, :deployed_at, deployed_at)
|
||||
|
||||
deployed_at
|
||||
end
|
||||
|
|
@ -42,7 +42,7 @@ defmodule Aprs.Release do
|
|||
Get the deployment timestamp.
|
||||
"""
|
||||
def deployed_at do
|
||||
Application.get_env(:aprs, :deployed_at) || DateTime.utc_now()
|
||||
Application.get_env(:aprsme, :deployed_at) || DateTime.utc_now()
|
||||
end
|
||||
|
||||
defp read_deployment_timestamp do
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
defmodule Aprs.Repo do
|
||||
defmodule Aprsme.Repo do
|
||||
use Ecto.Repo,
|
||||
otp_app: :aprs,
|
||||
otp_app: :aprsme,
|
||||
adapter: Ecto.Adapters.Postgres
|
||||
end
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Aprs.Schema do
|
||||
defmodule Aprsme.Schema do
|
||||
@moduledoc false
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Aprs.Workers.PacketCleanupWorker do
|
||||
defmodule Aprsme.Workers.PacketCleanupWorker do
|
||||
@moduledoc """
|
||||
Oban worker for cleaning up old APRS packet data.
|
||||
|
||||
|
|
@ -18,8 +18,8 @@ defmodule Aprs.Workers.PacketCleanupWorker do
|
|||
|
||||
# Import modules needed for database operations
|
||||
|
||||
alias Aprs.Packet
|
||||
alias Aprs.Repo
|
||||
alias Aprsme.Packet
|
||||
alias Aprsme.Repo
|
||||
|
||||
require Logger
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ defmodule Aprs.Workers.PacketCleanupWorker do
|
|||
deleted_count = cleanup_old_packets()
|
||||
|
||||
# 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")
|
||||
|
||||
|
|
@ -92,6 +92,6 @@ defmodule Aprs.Workers.PacketCleanupWorker do
|
|||
end
|
||||
|
||||
defp packets_module do
|
||||
Application.get_env(:aprs, :packets_module, Aprs.Packets)
|
||||
Application.get_env(:aprsme, :packets_module, Aprsme.Packets)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule AprsWeb.CoreComponents do
|
||||
defmodule AprsmeWeb.CoreComponents do
|
||||
@moduledoc """
|
||||
Provides core UI components.
|
||||
|
||||
|
|
@ -10,7 +10,7 @@ defmodule AprsWeb.CoreComponents do
|
|||
[heroicons_elixir](https://github.com/mveytsman/heroicons_elixir) project.
|
||||
"""
|
||||
use Phoenix.Component
|
||||
use Gettext, backend: AprsWeb.Gettext
|
||||
use Gettext, backend: AprsmeWeb.Gettext
|
||||
|
||||
alias Phoenix.HTML.Form
|
||||
alias Phoenix.LiveView.JS
|
||||
|
|
@ -620,9 +620,9 @@ defmodule AprsWeb.CoreComponents do
|
|||
# should be written to the errors.po file. The :count option is
|
||||
# set by Ecto and indicates we should also apply plural rules.
|
||||
if count = opts[:count] do
|
||||
Gettext.dngettext(AprsWeb.Gettext, "errors", msg, msg, count, opts)
|
||||
Gettext.dngettext(AprsmeWeb.Gettext, "errors", msg, msg, count, opts)
|
||||
else
|
||||
Gettext.dgettext(AprsWeb.Gettext, "errors", msg, opts)
|
||||
Gettext.dgettext(AprsmeWeb.Gettext, "errors", msg, opts)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
defmodule AprsWeb.Layouts do
|
||||
defmodule AprsmeWeb.Layouts do
|
||||
@moduledoc false
|
||||
use AprsWeb, :html
|
||||
use AprsmeWeb, :html
|
||||
|
||||
embed_templates "layouts/*"
|
||||
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
defmodule AprsWeb.Api.V1.CallsignController do
|
||||
defmodule AprsmeWeb.Api.V1.CallsignController do
|
||||
@moduledoc """
|
||||
API v1 controller for callsign-related endpoints.
|
||||
"""
|
||||
use AprsWeb, :controller
|
||||
use AprsmeWeb, :controller
|
||||
|
||||
alias Aprs.Packets
|
||||
alias AprsWeb.Api.V1.CallsignJSON
|
||||
alias Aprsme.Packets
|
||||
alias AprsmeWeb.Api.V1.CallsignJSON
|
||||
|
||||
action_fallback AprsWeb.Api.V1.FallbackController
|
||||
action_fallback AprsmeWeb.Api.V1.FallbackController
|
||||
|
||||
@doc """
|
||||
Get the most recent packet for a given callsign.
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
defmodule AprsWeb.Api.V1.FallbackController do
|
||||
defmodule AprsmeWeb.Api.V1.FallbackController do
|
||||
@moduledoc """
|
||||
Translates controller action results into valid `Plug.Conn` responses.
|
||||
|
||||
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.
|
||||
def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> put_view(json: AprsWeb.Api.V1.ChangesetJSON)
|
||||
|> put_view(json: AprsmeWeb.Api.V1.ChangesetJSON)
|
||||
|> render(:error, changeset: changeset)
|
||||
end
|
||||
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
defmodule AprsWeb.Api.V1.CallsignJSON do
|
||||
defmodule AprsmeWeb.Api.V1.CallsignJSON do
|
||||
@moduledoc """
|
||||
Renders callsign and packet data for API v1.
|
||||
"""
|
||||
|
||||
alias Aprs.Packet
|
||||
alias Aprsme.Packet
|
||||
|
||||
def render("show.json", %{packet: packet}) do
|
||||
%{data: packet_json(packet)}
|
||||
|
|
@ -38,7 +38,7 @@ defmodule AprsWeb.Api.V1.CallsignJSON do
|
|||
symbol: symbol_json(packet),
|
||||
comment: packet.comment,
|
||||
timestamp: packet.timestamp,
|
||||
aprs_messaging: packet.aprs_messaging,
|
||||
aprs_messaging: packet.aprsme_messaging,
|
||||
weather: weather_json(packet),
|
||||
equipment: equipment_json(packet),
|
||||
message: message_json(packet),
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule AprsWeb.Api.V1.ChangesetJSON do
|
||||
defmodule AprsmeWeb.Api.V1.ChangesetJSON do
|
||||
@moduledoc """
|
||||
Renders changeset errors for API v1.
|
||||
"""
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule AprsWeb.Api.V1.ErrorJSON do
|
||||
defmodule AprsmeWeb.Api.V1.ErrorJSON do
|
||||
@moduledoc """
|
||||
Renders error responses for API v1.
|
||||
"""
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
defmodule AprsWeb.ErrorHTML do
|
||||
defmodule AprsmeWeb.ErrorHTML do
|
||||
@moduledoc false
|
||||
use AprsWeb, :html
|
||||
use AprsmeWeb, :html
|
||||
|
||||
# If you want to customize your error pages,
|
||||
# uncomment the embed_templates/1 call below
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule AprsWeb.ErrorJSON do
|
||||
defmodule AprsmeWeb.ErrorJSON do
|
||||
@moduledoc false
|
||||
# If you want to customize a particular status code,
|
||||
# you may add your own clauses, such as:
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
defmodule AprsWeb.PageController do
|
||||
defmodule AprsmeWeb.PageController do
|
||||
@moduledoc false
|
||||
use AprsWeb, :controller
|
||||
use AprsmeWeb, :controller
|
||||
|
||||
def home(conn, _params) do
|
||||
render(conn, :home)
|
||||
|
|
@ -14,14 +14,14 @@ defmodule AprsWeb.PageController do
|
|||
# Check database connection
|
||||
db_healthy =
|
||||
try do
|
||||
Aprs.Repo.query!("SELECT 1")
|
||||
Aprsme.Repo.query!("SELECT 1")
|
||||
true
|
||||
rescue
|
||||
_ -> false
|
||||
end
|
||||
|
||||
# Get application version
|
||||
version = :aprs |> Application.spec(:vsn) |> List.to_string()
|
||||
version = :aprsme |> Application.spec(:vsn) |> List.to_string()
|
||||
|
||||
if db_healthy do
|
||||
json(conn, %{
|
||||
|
|
@ -45,7 +45,7 @@ defmodule AprsWeb.PageController do
|
|||
def ready(conn, _params) do
|
||||
# Simple readiness check without database dependency
|
||||
# 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, %{
|
||||
status: "ready",
|
||||
|
|
@ -56,10 +56,10 @@ defmodule AprsWeb.PageController do
|
|||
|
||||
def status_json(conn, _params) do
|
||||
# Get APRS-IS connection status
|
||||
aprs_status = Aprs.Is.get_status()
|
||||
aprs_status = Aprsme.Is.get_status()
|
||||
|
||||
# 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
|
||||
uptime_display = format_uptime(aprs_status.uptime_seconds)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
defmodule AprsWeb.PageHTML do
|
||||
defmodule AprsmeWeb.PageHTML do
|
||||
@moduledoc false
|
||||
use AprsWeb, :html
|
||||
use AprsmeWeb, :html
|
||||
|
||||
embed_templates "page_html/*"
|
||||
end
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
defmodule AprsWeb.UserSessionController do
|
||||
defmodule AprsmeWeb.UserSessionController do
|
||||
@moduledoc false
|
||||
use AprsWeb, :controller
|
||||
use AprsmeWeb, :controller
|
||||
|
||||
alias Aprs.Accounts
|
||||
alias AprsWeb.UserAuth
|
||||
alias Aprsme.Accounts
|
||||
alias AprsmeWeb.UserAuth
|
||||
|
||||
def create(conn, %{"_action" => "registered"} = params) do
|
||||
create(conn, params, "Account created successfully!")
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
defmodule AprsWeb.Endpoint do
|
||||
defmodule AprsmeWeb.Endpoint do
|
||||
@moduledoc false
|
||||
|
||||
use Phoenix.Endpoint, otp_app: :aprs
|
||||
use Phoenix.Endpoint, otp_app: :aprsme
|
||||
|
||||
# The session will be stored in the cookie and signed,
|
||||
# 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.
|
||||
plug Plug.Static,
|
||||
at: "/",
|
||||
from: :aprs,
|
||||
from: :aprsme,
|
||||
gzip: false,
|
||||
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
|
||||
plug Phoenix.LiveReloader
|
||||
plug Phoenix.CodeReloader
|
||||
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :aprs
|
||||
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :aprsme
|
||||
end
|
||||
|
||||
plug Phoenix.LiveDashboard.RequestLogger,
|
||||
|
|
@ -49,5 +49,5 @@ defmodule AprsWeb.Endpoint do
|
|||
plug Plug.MethodOverride
|
||||
plug Plug.Head
|
||||
plug Plug.Session, @session_options
|
||||
plug AprsWeb.Router
|
||||
plug AprsmeWeb.Router
|
||||
end
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
defmodule AprsWeb.Gettext do
|
||||
defmodule AprsmeWeb.Gettext do
|
||||
@moduledoc """
|
||||
A module providing Internationalization with a gettext-based API.
|
||||
|
||||
By using [Gettext](https://hexdocs.pm/gettext),
|
||||
your module gains a set of macros for translations, for example:
|
||||
|
||||
import AprsWeb.Gettext
|
||||
import AprsmeWeb.Gettext
|
||||
|
||||
# Simple translation
|
||||
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.
|
||||
"""
|
||||
use Gettext.Backend, otp_app: :aprs
|
||||
use Gettext.Backend, otp_app: :aprsme
|
||||
end
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
defmodule AprsWeb.AboutLive do
|
||||
defmodule AprsmeWeb.AboutLive do
|
||||
@moduledoc false
|
||||
use AprsWeb, :live_view
|
||||
use AprsmeWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
defmodule AprsWeb.ApiDocsLive do
|
||||
defmodule AprsmeWeb.ApiDocsLive do
|
||||
@moduledoc """
|
||||
LiveView for API documentation page.
|
||||
"""
|
||||
use AprsWeb, :live_view
|
||||
use AprsmeWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
|
|
@ -60,7 +60,7 @@ defmodule AprsWeb.ApiDocsLive do
|
|||
end
|
||||
|
||||
defp make_http_request(callsign) do
|
||||
alias Aprs.Packets
|
||||
alias Aprsme.Packets
|
||||
|
||||
# Validate callsign format (same as controller)
|
||||
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),
|
||||
"comment" => packet.comment,
|
||||
"timestamp" => packet.timestamp,
|
||||
"aprs_messaging" => packet.aprs_messaging,
|
||||
"aprsme_messaging" => packet.aprsme_messaging,
|
||||
"weather" => format_weather(packet),
|
||||
"equipment" => format_equipment(packet),
|
||||
"message" => format_message(packet),
|
||||
|
|
@ -373,7 +373,7 @@ defmodule AprsWeb.ApiDocsLive do
|
|||
},
|
||||
"comment": "Mobile Station",
|
||||
"timestamp": null,
|
||||
"aprs_messaging": false,
|
||||
"aprsme_messaging": false,
|
||||
"weather": null,
|
||||
"equipment": {
|
||||
"manufacturer": "Kenwood",
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
defmodule AprsWeb.BadPacketsLive.Index do
|
||||
defmodule AprsmeWeb.BadPacketsLive.Index do
|
||||
@moduledoc false
|
||||
use AprsWeb, :live_view
|
||||
use AprsmeWeb, :live_view
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Aprs.BadPacket
|
||||
alias Aprs.Repo
|
||||
alias Aprsme.BadPacket
|
||||
alias Aprsme.Repo
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
if connected?(socket) do
|
||||
# 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
|
||||
bad_packets = fetch_bad_packets()
|
||||
# Extra safeguard to ensure we never show more than 100
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
defmodule AprsWeb.InfoLive.Show do
|
||||
defmodule AprsmeWeb.InfoLive.Show do
|
||||
@moduledoc false
|
||||
use AprsWeb, :live_view
|
||||
use AprsmeWeb, :live_view
|
||||
|
||||
alias Aprs.Packets
|
||||
alias AprsWeb.MapLive.PacketUtils
|
||||
alias Aprsme.Packets
|
||||
alias AprsmeWeb.MapLive.PacketUtils
|
||||
|
||||
@neighbor_radius_km 10
|
||||
@neighbor_limit 10
|
||||
|
|
@ -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_code = symbol_code || ">" %>
|
||||
<% symbol_table_num =
|
||||
|
|
@ -13,14 +13,14 @@
|
|||
|> String.to_charlist()
|
||||
|> List.first()
|
||||
|> (fn c -> if is_integer(c), do: c, else: 63 end).() %>
|
||||
<% symbol_img = "/aprs-symbols/aprs-symbols-24-#{symbol_table_num}@2x.png" %>
|
||||
<% symbol_index = symbol_code_ord - 33 %>
|
||||
<% _symbol_img = "/aprs-symbols/aprs-symbols-24-#{symbol_table_num}@2x.png" %>
|
||||
<% _symbol_index = symbol_code_ord - 33 %>
|
||||
|
||||
<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">
|
||||
APRS station <span class="text-blue-700">{@callsign}</span>
|
||||
<%= 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_code = symbol_code || ">" %>
|
||||
<% symbol_table_num =
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
<span class="font-semibold">Location:</span> {@packet.lat}, {@packet.lon}
|
||||
</div>
|
||||
<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
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -92,7 +92,7 @@
|
|||
</.link>
|
||||
<%= if neighbor.packet do %>
|
||||
<% {symbol_table_id, symbol_code} =
|
||||
AprsWeb.MapLive.PacketUtils.get_symbol_info(neighbor.packet) %>
|
||||
AprsmeWeb.MapLive.PacketUtils.get_symbol_info(neighbor.packet) %>
|
||||
<% symbol_table =
|
||||
if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %>
|
||||
<% symbol_code = symbol_code || ">" %>
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
defmodule AprsWeb.MapLive.CallsignView do
|
||||
defmodule AprsmeWeb.MapLive.CallsignView do
|
||||
@moduledoc false
|
||||
use AprsWeb, :live_view
|
||||
use AprsmeWeb, :live_view
|
||||
|
||||
alias Aprs.Packets
|
||||
alias AprsWeb.Endpoint
|
||||
alias AprsWeb.MapLive.MapHelpers
|
||||
alias AprsWeb.MapLive.PacketUtils
|
||||
alias Aprsme.Packets
|
||||
alias AprsmeWeb.Endpoint
|
||||
alias AprsmeWeb.MapLive.MapHelpers
|
||||
alias AprsmeWeb.MapLive.PacketUtils
|
||||
|
||||
@default_center %{lat: 39.0, lng: -98.0}
|
||||
@default_zoom 4
|
||||
|
|
@ -1,34 +1,34 @@
|
|||
defmodule AprsWeb.MapLive.Index do
|
||||
defmodule AprsmeWeb.MapLive.Index do
|
||||
@moduledoc """
|
||||
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 AprsWeb.MapLive.MapHelpers
|
||||
alias AprsWeb.MapLive.PacketUtils
|
||||
alias AprsmeWeb.Endpoint
|
||||
alias AprsmeWeb.MapLive.MapHelpers
|
||||
alias AprsmeWeb.MapLive.PacketUtils
|
||||
alias Phoenix.LiveView.Socket
|
||||
|
||||
@default_center %{lat: 39.8283, lng: -98.5795}
|
||||
@default_zoom 5
|
||||
@finch_name Aprs.Finch
|
||||
@initialize_replay_delay Application.compile_env(:aprs, :initialize_replay_delay, 500)
|
||||
@finch_name Aprsme.Finch
|
||||
@initialize_replay_delay Application.compile_env(:aprsme, :initialize_replay_delay, 500)
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
if connected?(socket) do
|
||||
# Subscribe to packet updates
|
||||
Phoenix.PubSub.subscribe(Aprs.PubSub, "packets")
|
||||
Phoenix.PubSub.subscribe(Aprs.PubSub, "bad_packets")
|
||||
Phoenix.PubSub.subscribe(Aprsme.PubSub, "packets")
|
||||
Phoenix.PubSub.subscribe(Aprsme.PubSub, "bad_packets")
|
||||
|
||||
# Schedule periodic cleanup of old packets
|
||||
Process.send_after(self(), :cleanup_old_packets, 60_000)
|
||||
end
|
||||
|
||||
# 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)
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ defmodule AprsWeb.MapLive.Index do
|
|||
|
||||
if connected?(socket) do
|
||||
Endpoint.subscribe("aprs_messages")
|
||||
Phoenix.PubSub.subscribe(Aprs.PubSub, "postgres:aprs_packets")
|
||||
Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_packets")
|
||||
maybe_start_geolocation(socket)
|
||||
end
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ defmodule AprsWeb.MapLive.Index do
|
|||
defp maybe_start_geolocation(socket) do
|
||||
if geolocation_enabled?() do
|
||||
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.
|
||||
# This will be geolocated to Mountain View, CA.
|
||||
"8.8.8.8"
|
||||
|
|
@ -116,7 +116,7 @@ defmodule AprsWeb.MapLive.Index do
|
|||
end
|
||||
|
||||
defp geolocation_enabled? do
|
||||
Application.get_env(:aprs, :disable_aprs_connection, false) != true
|
||||
Application.get_env(:aprsme, :disable_aprs_connection, false) != true
|
||||
end
|
||||
|
||||
defp extract_ip(socket) do
|
||||
|
|
@ -1175,7 +1175,7 @@ defmodule AprsWeb.MapLive.Index do
|
|||
}
|
||||
|
||||
# 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)
|
||||
|
||||
# Sort packets by received_at timestamp to ensure chronological replay
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
defmodule AprsWeb.MapLive.MapHelpers do
|
||||
defmodule AprsmeWeb.MapLive.MapHelpers do
|
||||
@moduledoc false
|
||||
|
||||
alias Parser.Types.MicE
|
||||
alias Aprs.Types.MicE
|
||||
|
||||
@spec get_coordinates(map() | struct()) :: {number() | nil, number() | nil, map() | nil}
|
||||
def get_coordinates(%{data_extended: %MicE{} = mic_e}) do
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
defmodule AprsWeb.MapLive.PacketUtils do
|
||||
defmodule AprsmeWeb.MapLive.PacketUtils do
|
||||
@moduledoc """
|
||||
Shared utilities for extracting and processing packet data in map views.
|
||||
"""
|
||||
|
||||
alias AprsWeb.MapLive.MapHelpers
|
||||
alias AprsWeb.TimeHelpers
|
||||
alias AprsmeWeb.MapLive.MapHelpers
|
||||
alias AprsmeWeb.TimeHelpers
|
||||
|
||||
@doc """
|
||||
Safely extracts a value from a packet or data_extended map with fallback support.
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
defmodule AprsWeb.PacketsLive.CallsignView do
|
||||
defmodule AprsmeWeb.PacketsLive.CallsignView do
|
||||
@moduledoc false
|
||||
use AprsWeb, :live_view
|
||||
use AprsmeWeb, :live_view
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Aprs.EncodingUtils
|
||||
alias Aprs.Packet
|
||||
alias Aprs.Repo
|
||||
alias AprsWeb.Endpoint
|
||||
alias Parser.DeviceParser
|
||||
alias Aprsme.DeviceParser
|
||||
alias Aprsme.EncodingUtils
|
||||
alias Aprsme.Packet
|
||||
alias Aprsme.Repo
|
||||
alias AprsmeWeb.Endpoint
|
||||
|
||||
@impl true
|
||||
def mount(%{"callsign" => callsign}, _session, socket) do
|
||||
|
|
@ -66,7 +66,7 @@ defmodule AprsWeb.PacketsLive.CallsignView do
|
|||
|
||||
canonical_identifier =
|
||||
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
|
||||
else
|
||||
device_identifier
|
||||
|
|
@ -208,7 +208,10 @@ defmodule AprsWeb.PacketsLive.CallsignView do
|
|||
|
||||
defp enrich_packet_with_device_info(packet) do
|
||||
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
|
||||
vendor = if device, do: device.vendor
|
||||
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
defmodule AprsWeb.PacketsLive.Index do
|
||||
defmodule AprsmeWeb.PacketsLive.Index do
|
||||
@moduledoc false
|
||||
use AprsWeb, :live_view
|
||||
use AprsmeWeb, :live_view
|
||||
|
||||
alias Aprs.EncodingUtils
|
||||
alias AprsWeb.Endpoint
|
||||
alias Aprsme.EncodingUtils
|
||||
alias AprsmeWeb.Endpoint
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
if connected?(socket) do
|
||||
Endpoint.subscribe("aprs_messages")
|
||||
Phoenix.PubSub.subscribe(Aprs.PubSub, "postgres:aprs_packets")
|
||||
Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_packets")
|
||||
end
|
||||
|
||||
{:ok, assign(socket, :packets, [])}
|
||||
|
|
@ -26,7 +26,7 @@ defmodule AprsWeb.PacketsLive.Index do
|
|||
|
||||
@impl true
|
||||
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)
|
||||
socket = assign(socket, :packets, packets)
|
||||
{:noreply, socket}
|
||||
|
|
@ -51,8 +51,8 @@
|
|||
Map.get(packet, :lat) ||
|
||||
if(
|
||||
Map.has_key?(packet, :location) and not is_nil(packet.location) and
|
||||
function_exported?(Aprs.Packet, :lat, 1),
|
||||
do: Aprs.Packet.lat(packet),
|
||||
function_exported?(Aprsme.Packet, :lat, 1),
|
||||
do: Aprsme.Packet.lat(packet),
|
||||
else: nil
|
||||
) ||
|
||||
case Map.get(packet, :data_extended) do
|
||||
|
|
@ -86,8 +86,8 @@
|
|||
Map.get(packet, :lon) ||
|
||||
if(
|
||||
Map.has_key?(packet, :location) and not is_nil(packet.location) and
|
||||
function_exported?(Aprs.Packet, :lon, 1),
|
||||
do: Aprs.Packet.lon(packet),
|
||||
function_exported?(Aprsme.Packet, :lon, 1),
|
||||
do: Aprsme.Packet.lon(packet),
|
||||
else: nil
|
||||
) ||
|
||||
case Map.get(packet, :data_extended) do
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
defmodule AprsWeb.StatusLive.Index do
|
||||
defmodule AprsmeWeb.StatusLive.Index do
|
||||
@moduledoc """
|
||||
LiveView for displaying real-time APRS-IS connection status
|
||||
"""
|
||||
use AprsWeb, :live_view
|
||||
use AprsmeWeb, :live_view
|
||||
|
||||
# 30 seconds
|
||||
@refresh_interval 1_000
|
||||
|
|
@ -327,11 +327,11 @@ defmodule AprsWeb.StatusLive.Index do
|
|||
# Private functions
|
||||
|
||||
defp get_aprs_status do
|
||||
Aprs.Is.get_status()
|
||||
Aprsme.Is.get_status()
|
||||
end
|
||||
|
||||
defp get_app_version do
|
||||
:aprs |> Application.spec(:vsn) |> List.to_string()
|
||||
:aprsme |> Application.spec(:vsn) |> List.to_string()
|
||||
end
|
||||
|
||||
defp refresh_status(socket) do
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
defmodule AprsWeb.UserConfirmationInstructionsLive do
|
||||
defmodule AprsmeWeb.UserConfirmationInstructionsLive do
|
||||
@moduledoc false
|
||||
use AprsWeb, :live_view
|
||||
use AprsmeWeb, :live_view
|
||||
|
||||
alias Aprs.Accounts
|
||||
alias Aprsme.Accounts
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
defmodule AprsWeb.UserConfirmationLive do
|
||||
defmodule AprsmeWeb.UserConfirmationLive do
|
||||
@moduledoc false
|
||||
use AprsWeb, :live_view
|
||||
use AprsmeWeb, :live_view
|
||||
|
||||
alias Aprs.Accounts
|
||||
alias Aprsme.Accounts
|
||||
|
||||
def render(%{live_action: :edit} = assigns) do
|
||||
~H"""
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
defmodule AprsWeb.UserForgotPasswordLive do
|
||||
defmodule AprsmeWeb.UserForgotPasswordLive do
|
||||
@moduledoc false
|
||||
use AprsWeb, :live_view
|
||||
use AprsmeWeb, :live_view
|
||||
|
||||
alias Aprs.Accounts
|
||||
alias Aprsme.Accounts
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
defmodule AprsWeb.UserLoginLive do
|
||||
defmodule AprsmeWeb.UserLoginLive do
|
||||
@moduledoc false
|
||||
use AprsWeb, :live_view
|
||||
use AprsmeWeb, :live_view
|
||||
|
||||
alias Aprs.Accounts
|
||||
alias Aprs.Accounts.User
|
||||
alias Aprsme.Accounts
|
||||
alias Aprsme.Accounts.User
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
defmodule AprsWeb.UserRegistrationLive do
|
||||
defmodule AprsmeWeb.UserRegistrationLive do
|
||||
@moduledoc false
|
||||
use AprsWeb, :live_view
|
||||
use AprsmeWeb, :live_view
|
||||
|
||||
alias Aprs.Accounts
|
||||
alias Aprs.Accounts.User
|
||||
alias Aprsme.Accounts
|
||||
alias Aprsme.Accounts.User
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
defmodule AprsWeb.UserResetPasswordLive do
|
||||
defmodule AprsmeWeb.UserResetPasswordLive do
|
||||
@moduledoc false
|
||||
use AprsWeb, :live_view
|
||||
use AprsmeWeb, :live_view
|
||||
|
||||
alias Aprs.Accounts
|
||||
alias Aprsme.Accounts
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
defmodule AprsWeb.UserSettingsLive do
|
||||
defmodule AprsmeWeb.UserSettingsLive do
|
||||
@moduledoc false
|
||||
use AprsWeb, :live_view
|
||||
use AprsmeWeb, :live_view
|
||||
|
||||
alias Aprs.Accounts
|
||||
alias Aprsme.Accounts
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
defmodule AprsWeb.WeatherLive.CallsignView do
|
||||
defmodule AprsmeWeb.WeatherLive.CallsignView do
|
||||
@moduledoc false
|
||||
use AprsWeb, :live_view
|
||||
use AprsmeWeb, :live_view
|
||||
|
||||
alias Aprs.Packets
|
||||
alias AprsWeb.MapLive.PacketUtils
|
||||
alias Aprsme.Packets
|
||||
alias AprsmeWeb.MapLive.PacketUtils
|
||||
|
||||
@impl true
|
||||
def mount(%{"callsign" => callsign}, _session, socket) do
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
defmodule AprsWeb.Router do
|
||||
use AprsWeb, :router
|
||||
defmodule AprsmeWeb.Router do
|
||||
use AprsmeWeb, :router
|
||||
|
||||
import AprsWeb.UserAuth
|
||||
import AprsmeWeb.UserAuth
|
||||
import Phoenix.LiveDashboard.Router
|
||||
|
||||
pipeline :browser do
|
||||
plug :accepts, ["html"]
|
||||
plug :fetch_session
|
||||
plug :fetch_live_flash
|
||||
plug :put_root_layout, {AprsWeb.Layouts, :root}
|
||||
plug :put_root_layout, {AprsmeWeb.Layouts, :root}
|
||||
plug :protect_from_forgery
|
||||
plug :put_secure_browser_headers
|
||||
plug :fetch_current_user
|
||||
|
|
@ -18,17 +18,17 @@ defmodule AprsWeb.Router do
|
|||
plug :accepts, ["json"]
|
||||
end
|
||||
|
||||
scope "/", AprsWeb do
|
||||
scope "/", AprsmeWeb do
|
||||
pipe_through :api
|
||||
get "/health", PageController, :health
|
||||
get "/ready", PageController, :ready
|
||||
get "/status.json", PageController, :status_json
|
||||
end
|
||||
|
||||
scope "/", AprsWeb do
|
||||
scope "/", AprsmeWeb do
|
||||
pipe_through :browser
|
||||
|
||||
live_dashboard "/dashboard", metrics: AprsWeb.Telemetry
|
||||
live_dashboard "/dashboard", metrics: AprsmeWeb.Telemetry
|
||||
|
||||
live_session :regular_pages do
|
||||
live "/status", StatusLive.Index, :index
|
||||
|
|
@ -41,21 +41,21 @@ defmodule AprsWeb.Router do
|
|||
live "/info/:callsign", InfoLive.Show, :show
|
||||
end
|
||||
|
||||
live_session :map_pages, layout: {AprsWeb.Layouts, :map} do
|
||||
live_session :map_pages, layout: {AprsmeWeb.Layouts, :map} do
|
||||
live "/", MapLive.Index, :index
|
||||
live "/:callsign", MapLive.CallsignView, :index
|
||||
end
|
||||
end
|
||||
|
||||
# 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
|
||||
|
||||
get "/callsign/:callsign", CallsignController, :show
|
||||
end
|
||||
|
||||
# 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
|
||||
# it behind authentication and allow only admins to access it.
|
||||
# If your application does not have an admins-only section yet,
|
||||
|
|
@ -65,18 +65,18 @@ defmodule AprsWeb.Router do
|
|||
scope "/dev" do
|
||||
pipe_through :browser
|
||||
|
||||
# live_dashboard "/dashboard", metrics: AprsWeb.Telemetry
|
||||
# live_dashboard "/dashboard", metrics: AprsmeWeb.Telemetry
|
||||
forward "/mailbox", Plug.Swoosh.MailboxPreview
|
||||
end
|
||||
end
|
||||
|
||||
## Authentication routes
|
||||
|
||||
scope "/", AprsWeb do
|
||||
scope "/", AprsmeWeb do
|
||||
pipe_through [:browser, :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/log_in", UserLoginLive, :new
|
||||
live "/users/reset_password", UserForgotPasswordLive, :new
|
||||
|
|
@ -86,23 +86,23 @@ defmodule AprsWeb.Router do
|
|||
post "/users/log_in", UserSessionController, :create
|
||||
end
|
||||
|
||||
scope "/", AprsWeb do
|
||||
scope "/", AprsmeWeb do
|
||||
pipe_through [:browser, :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/confirm_email/:token", UserSettingsLive, :confirm_email
|
||||
end
|
||||
end
|
||||
|
||||
scope "/", AprsWeb do
|
||||
scope "/", AprsmeWeb do
|
||||
pipe_through [:browser]
|
||||
|
||||
delete "/users/log_out", UserSessionController, :delete
|
||||
|
||||
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", UserConfirmationInstructionsLive, :new
|
||||
end
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule AprsWeb.Telemetry do
|
||||
defmodule AprsmeWeb.Telemetry do
|
||||
@moduledoc false
|
||||
use Supervisor
|
||||
|
||||
|
|
@ -54,23 +54,23 @@ defmodule AprsWeb.Telemetry do
|
|||
),
|
||||
|
||||
# Database Metrics
|
||||
summary("aprs.repo.query.total_time",
|
||||
summary("aprsme.repo.query.total_time",
|
||||
unit: {:native, :millisecond},
|
||||
description: "The sum of the other measurements"
|
||||
),
|
||||
summary("aprs.repo.query.decode_time",
|
||||
summary("aprsme.repo.query.decode_time",
|
||||
unit: {:native, :millisecond},
|
||||
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},
|
||||
description: "The time spent executing the query"
|
||||
),
|
||||
summary("aprs.repo.query.queue_time",
|
||||
summary("aprsme.repo.query.queue_time",
|
||||
unit: {:native, :millisecond},
|
||||
description: "The time spent waiting for a database connection"
|
||||
),
|
||||
summary("aprs.repo.query.idle_time",
|
||||
summary("aprsme.repo.query.idle_time",
|
||||
unit: {:native, :millisecond},
|
||||
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.
|
||||
# This function must call :telemetry.execute/3 and a metric must be added above.
|
||||
# {AprsWeb, :count_users, []}
|
||||
# {AprsmeWeb, :count_users, []}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule AprsWeb.TimeHelpers do
|
||||
defmodule AprsmeWeb.TimeHelpers do
|
||||
@moduledoc """
|
||||
Shared helpers for formatting time and dates in the web layer.
|
||||
"""
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
defmodule AprsWeb.UserAuth do
|
||||
defmodule AprsmeWeb.UserAuth do
|
||||
@moduledoc false
|
||||
use AprsWeb, :verified_routes
|
||||
use AprsmeWeb, :verified_routes
|
||||
|
||||
import Phoenix.Controller
|
||||
import Plug.Conn
|
||||
|
||||
alias Aprs.Accounts
|
||||
alias Aprsme.Accounts
|
||||
|
||||
# Make the remember me cookie valid for 60 days.
|
||||
# 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)
|
||||
|
||||
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
|
||||
|
||||
conn
|
||||
|
|
@ -131,16 +131,16 @@ defmodule AprsWeb.UserAuth do
|
|||
Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate
|
||||
the current_user:
|
||||
|
||||
defmodule AprsWeb.PageLive do
|
||||
use AprsWeb, :live_view
|
||||
defmodule AprsmeWeb.PageLive do
|
||||
use AprsmeWeb, :live_view
|
||||
|
||||
on_mount {AprsWeb.UserAuth, :mount_current_user}
|
||||
on_mount {AprsmeWeb.UserAuth, :mount_current_user}
|
||||
...
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
"""
|
||||
970
lib/parser.ex
970
lib/parser.ex
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
Loading…
Add table
Reference in a new issue