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
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
tags: aprs:latest
|
tags: aprsme:latest
|
||||||
load: true
|
load: true
|
||||||
no-cache: true
|
no-cache: true
|
||||||
pull: true
|
pull: true
|
||||||
|
|
@ -36,7 +36,7 @@ jobs:
|
||||||
- name: Run Trivy vulnerability scanner
|
- name: Run Trivy vulnerability scanner
|
||||||
uses: aquasecurity/trivy-action@master
|
uses: aquasecurity/trivy-action@master
|
||||||
with:
|
with:
|
||||||
image-ref: aprs:latest
|
image-ref: aprsme:latest
|
||||||
format: "sarif"
|
format: "sarif"
|
||||||
output: "trivy-results.sarif"
|
output: "trivy-results.sarif"
|
||||||
severity: "CRITICAL,HIGH"
|
severity: "CRITICAL,HIGH"
|
||||||
|
|
@ -59,7 +59,7 @@ jobs:
|
||||||
|
|
||||||
echo "### OS Package Vulnerabilities" >> dep-update-report.md
|
echo "### OS Package Vulnerabilities" >> dep-update-report.md
|
||||||
echo "```" >> dep-update-report.md
|
echo "```" >> dep-update-report.md
|
||||||
trivy image --severity HIGH,CRITICAL --no-progress aprs:latest >> dep-update-report.md
|
trivy image --severity HIGH,CRITICAL --no-progress aprsme:latest >> dep-update-report.md
|
||||||
echo "```" >> dep-update-report.md
|
echo "```" >> dep-update-report.md
|
||||||
|
|
||||||
- name: Upload vulnerability summary
|
- name: Upload vulnerability summary
|
||||||
|
|
|
||||||
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
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
tags: aprs:${{ github.sha }}
|
tags: aprsme:${{ github.sha }}
|
||||||
load: true
|
load: true
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
|
@ -53,7 +53,7 @@ jobs:
|
||||||
- name: Run Trivy vulnerability scanner
|
- name: Run Trivy vulnerability scanner
|
||||||
uses: aquasecurity/trivy-action@master
|
uses: aquasecurity/trivy-action@master
|
||||||
with:
|
with:
|
||||||
image-ref: aprs:${{ github.sha }}
|
image-ref: aprsme:${{ github.sha }}
|
||||||
format: "sarif"
|
format: "sarif"
|
||||||
output: "trivy-results.sarif"
|
output: "trivy-results.sarif"
|
||||||
severity: "CRITICAL,HIGH"
|
severity: "CRITICAL,HIGH"
|
||||||
|
|
@ -70,7 +70,7 @@ jobs:
|
||||||
- name: Run Trivy for outdated OS packages
|
- name: Run Trivy for outdated OS packages
|
||||||
uses: aquasecurity/trivy-action@master
|
uses: aquasecurity/trivy-action@master
|
||||||
with:
|
with:
|
||||||
image-ref: aprs:${{ github.sha }}
|
image-ref: aprsme:${{ github.sha }}
|
||||||
format: "table"
|
format: "table"
|
||||||
output: "os-packages.txt"
|
output: "os-packages.txt"
|
||||||
severity: "HIGH,CRITICAL"
|
severity: "HIGH,CRITICAL"
|
||||||
|
|
@ -79,7 +79,7 @@ jobs:
|
||||||
- name: Run Trivy for outdated application dependencies
|
- name: Run Trivy for outdated application dependencies
|
||||||
uses: aquasecurity/trivy-action@master
|
uses: aquasecurity/trivy-action@master
|
||||||
with:
|
with:
|
||||||
image-ref: aprs:${{ github.sha }}
|
image-ref: aprsme:${{ github.sha }}
|
||||||
format: "table"
|
format: "table"
|
||||||
output: "app-packages.txt"
|
output: "app-packages.txt"
|
||||||
severity: "HIGH,CRITICAL"
|
severity: "HIGH,CRITICAL"
|
||||||
|
|
@ -99,7 +99,7 @@ jobs:
|
||||||
uses: docker/scout-action@v1
|
uses: docker/scout-action@v1
|
||||||
with:
|
with:
|
||||||
command: quickview,cves
|
command: quickview,cves
|
||||||
image: aprs:${{ github.sha }}
|
image: aprsme:${{ github.sha }}
|
||||||
output-format: sarif
|
output-format: sarif
|
||||||
only-severities: critical,high
|
only-severities: critical,high
|
||||||
sarif-file: scout-results.sarif
|
sarif-file: scout-results.sarif
|
||||||
|
|
@ -116,7 +116,7 @@ jobs:
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
echo "# Docker Image Security Report" > security-summary.md
|
echo "# Docker Image Security Report" > security-summary.md
|
||||||
echo "## Image: aprs:${{ github.sha }}" >> security-summary.md
|
echo "## Image: aprsme:${{ github.sha }}" >> security-summary.md
|
||||||
echo "## Date: $(date)" >> security-summary.md
|
echo "## Date: $(date)" >> security-summary.md
|
||||||
|
|
||||||
echo "### OS Package Vulnerabilities" >> security-summary.md
|
echo "### OS Package Vulnerabilities" >> security-summary.md
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -20,7 +20,7 @@ erl_crash.dump
|
||||||
*.ez
|
*.ez
|
||||||
|
|
||||||
# Ignore package tarball (built via "mix hex.build").
|
# Ignore package tarball (built via "mix hex.build").
|
||||||
aprs-*.tar
|
aprsme-*.tar
|
||||||
|
|
||||||
# Ignore assets that are produced by build tools.
|
# Ignore assets that are produced by build tools.
|
||||||
/priv/static/assets/
|
/priv/static/assets/
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ WORKDIR /app
|
||||||
|
|
||||||
# Copy the Elixir release from builder stage
|
# Copy the Elixir release from builder stage
|
||||||
# The distroless nonroot user has UID 65532, so we use that
|
# The distroless nonroot user has UID 65532, so we use that
|
||||||
COPY --from=builder --chown=65532:65532 /app/_build/prod/rel/aprs ./
|
COPY --from=builder --chown=65532:65532 /app/_build/prod/rel/aprsme ./
|
||||||
|
|
||||||
# Create required directories with proper permissions
|
# Create required directories with proper permissions
|
||||||
USER 0
|
USER 0
|
||||||
|
|
|
||||||
|
|
@ -83,19 +83,19 @@ The workflow is defined in `.github/workflows/deploy-k3s.yaml`. It will:
|
||||||
|
|
||||||
* **Database Migrations:** The Kubernetes deployment for the application includes an init container that automatically runs database migrations (`/app/bin/migrate`) before the main application container starts.
|
* **Database Migrations:** The Kubernetes deployment for the application includes an init container that automatically runs database migrations (`/app/bin/migrate`) before the main application container starts.
|
||||||
* **Health Check Endpoint:** The application deployment (`k8s/app-deployment.yaml`) is configured with readiness and liveness probes that expect a health check endpoint at `/health` (returning HTTP 200). You may need to add this to your Phoenix application's router and controller:
|
* **Health Check Endpoint:** The application deployment (`k8s/app-deployment.yaml`) is configured with readiness and liveness probes that expect a health check endpoint at `/health` (returning HTTP 200). You may need to add this to your Phoenix application's router and controller:
|
||||||
* In `lib/aprs_web/router.ex`:
|
* In `lib/aprsme_web/router.ex`:
|
||||||
```elixir
|
```elixir
|
||||||
scope "/", AprsWeb do
|
scope "/", AprsmeWeb do
|
||||||
pipe_through :browser
|
pipe_through :browser
|
||||||
# ... other routes
|
# ... other routes
|
||||||
get "/health", PageController, :health
|
get "/health", PageController, :health
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
* In `lib/aprs_web/controllers/page_controller.ex` (or another suitable controller):
|
* In `lib/aprsme_web/controllers/page_controller.ex` (or another suitable controller):
|
||||||
```elixir
|
```elixir
|
||||||
def health(conn, _params) do
|
def health(conn, _params) do
|
||||||
# You can add more sophisticated checks here if needed
|
# You can add more sophisticated checks here if needed
|
||||||
json(conn, %{status: "ok", version: Application.spec(:aprs, :vsn)})
|
json(conn, %{status: "ok", version: Application.spec(:aprsme, :vsn)})
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
* **Image Name:** The application deployment manifest (`k8s/app-deployment.yaml`) uses a placeholder image name `your-ghcr-username/aprs-app:latest`. The CI/CD pipeline automatically replaces this with the correct image name from GHCR (e.g., `ghcr.io/your-github-owner/your-repo-name:<git-sha>`).
|
* **Image Name:** The application deployment manifest (`k8s/app-deployment.yaml`) uses a placeholder image name `your-ghcr-username/aprs-app:latest`. The CI/CD pipeline automatically replaces this with the correct image name from GHCR (e.g., `ghcr.io/your-github-owner/your-repo-name:<git-sha>`).
|
||||||
|
|
|
||||||
|
|
@ -14,36 +14,36 @@ import Config
|
||||||
#
|
#
|
||||||
# For production it's recommended to configure a different adapter
|
# For production it's recommended to configure a different adapter
|
||||||
# at the `config/runtime.exs`.
|
# at the `config/runtime.exs`.
|
||||||
config :aprs, Aprs.Mailer, adapter: Swoosh.Adapters.Local
|
config :aprsme, Aprsme.Mailer, adapter: Swoosh.Adapters.Local
|
||||||
|
|
||||||
# Configures the endpoint
|
# Configures the endpoint
|
||||||
config :aprs, AprsWeb.Endpoint,
|
config :aprsme, AprsmeWeb.Endpoint,
|
||||||
url: [host: "localhost"],
|
url: [host: "localhost"],
|
||||||
render_errors: [
|
render_errors: [
|
||||||
formats: [html: AprsWeb.ErrorHTML, json: AprsWeb.ErrorJSON],
|
formats: [html: AprsmeWeb.ErrorHTML, json: AprsmeWeb.ErrorJSON],
|
||||||
layout: false
|
layout: false
|
||||||
],
|
],
|
||||||
pubsub_server: Aprs.PubSub,
|
pubsub_server: Aprsme.PubSub,
|
||||||
live_view: [signing_salt: "ees098qG"]
|
live_view: [signing_salt: "ees098qG"]
|
||||||
|
|
||||||
# Configure Oban for background jobs
|
# Configure Oban for background jobs
|
||||||
config :aprs, Oban,
|
config :aprsme, Oban,
|
||||||
repo: Aprs.Repo,
|
repo: Aprsme.Repo,
|
||||||
plugins: [
|
plugins: [
|
||||||
{Oban.Plugins.Pruner, max_age: 60 * 60 * 24 * 7},
|
{Oban.Plugins.Pruner, max_age: 60 * 60 * 24 * 7},
|
||||||
{Oban.Plugins.Cron,
|
{Oban.Plugins.Cron,
|
||||||
crontab: [
|
crontab: [
|
||||||
{"0 0 * * *", Aprs.Workers.PacketCleanupWorker},
|
{"0 0 * * *", Aprsme.Workers.PacketCleanupWorker},
|
||||||
{"0 3 * * 1", Aprs.DeviceIdentification.Worker}
|
{"0 3 * * 1", Aprsme.DeviceIdentification.Worker}
|
||||||
]}
|
]}
|
||||||
],
|
],
|
||||||
queues: [default: 10, maintenance: 2]
|
queues: [default: 10, maintenance: 2]
|
||||||
|
|
||||||
config :aprs,
|
config :aprsme,
|
||||||
ecto_repos: [Aprs.Repo]
|
ecto_repos: [Aprsme.Repo]
|
||||||
|
|
||||||
config :aprs,
|
config :aprsme,
|
||||||
ecto_repos: [Aprs.Repo],
|
ecto_repos: [Aprsme.Repo],
|
||||||
aprs_is_server: System.get_env("APRS_SERVER", "dallas.aprs2.net"),
|
aprs_is_server: System.get_env("APRS_SERVER", "dallas.aprs2.net"),
|
||||||
aprs_is_port: 10_152,
|
aprs_is_port: 10_152,
|
||||||
aprs_is_default_filter: System.get_env("APRS_FILTER"),
|
aprs_is_default_filter: System.get_env("APRS_FILTER"),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import Config
|
import Config
|
||||||
|
|
||||||
# Configure your database
|
# Configure your database
|
||||||
config :aprs, Aprs.Repo,
|
config :aprsme, Aprsme.Repo,
|
||||||
# For development, we disable any cache and enable
|
# For development, we disable any cache and enable
|
||||||
# debugging and code reloading.
|
# debugging and code reloading.
|
||||||
#
|
#
|
||||||
|
|
@ -11,14 +11,14 @@ config :aprs, Aprs.Repo,
|
||||||
username: "postgres",
|
username: "postgres",
|
||||||
password: "postgres",
|
password: "postgres",
|
||||||
hostname: "localhost",
|
hostname: "localhost",
|
||||||
database: "aprs_dev",
|
database: "aprsme_dev",
|
||||||
stacktrace: true,
|
stacktrace: true,
|
||||||
show_sensitive_data_on_connection_error: true,
|
show_sensitive_data_on_connection_error: true,
|
||||||
pool_size: 10,
|
pool_size: 10,
|
||||||
log: false,
|
log: false,
|
||||||
types: Aprs.PostgresTypes
|
types: Aprsme.PostgresTypes
|
||||||
|
|
||||||
config :aprs, AprsWeb.Endpoint,
|
config :aprsme, AprsmeWeb.Endpoint,
|
||||||
# Binding to loopback ipv4 address prevents access from other machines.
|
# Binding to loopback ipv4 address prevents access from other machines.
|
||||||
# Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
|
# Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
|
||||||
http: [ip: {127, 0, 0, 1}, port: 4000],
|
http: [ip: {127, 0, 0, 1}, port: 4000],
|
||||||
|
|
@ -37,13 +37,13 @@ config :aprs, AprsWeb.Endpoint,
|
||||||
]
|
]
|
||||||
|
|
||||||
# Watch static and templates for browser reloading.
|
# Watch static and templates for browser reloading.
|
||||||
config :aprs, AprsWeb.Endpoint,
|
config :aprsme, AprsmeWeb.Endpoint,
|
||||||
live_reload: [
|
live_reload: [
|
||||||
patterns: [
|
patterns: [
|
||||||
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
|
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
|
||||||
~r"priv/gettext/.*(po)$",
|
~r"priv/gettext/.*(po)$",
|
||||||
~r"lib/aprs_web/(live|views)/.*(ex)$",
|
~r"lib/aprsme_web/(live|views)/.*(ex)$",
|
||||||
~r"lib/aprs_web/templates/.*(eex)$",
|
~r"lib/aprsme_web/templates/.*(eex)$",
|
||||||
~r"assets/vendor/.*(js|css)$"
|
~r"assets/vendor/.*(js|css)$"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
@ -53,7 +53,7 @@ config :aprs, AprsWeb.Endpoint,
|
||||||
# mix phx.gen.cert
|
# mix phx.gen.cert
|
||||||
#
|
#
|
||||||
# Run `mix help phx.gen.cert` for more information.
|
# Run `mix help phx.gen.cert` for more information.
|
||||||
config :aprs, dev_routes: true
|
config :aprsme, dev_routes: true
|
||||||
|
|
||||||
# Do not include metadata nor timestamps in development logs
|
# Do not include metadata nor timestamps in development logs
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import Config
|
||||||
# manifest is generated by the `mix phx.digest` task,
|
# manifest is generated by the `mix phx.digest` task,
|
||||||
# which you should run after static files are built and
|
# which you should run after static files are built and
|
||||||
# before starting your production server.
|
# before starting your production server.
|
||||||
config :aprs, AprsWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
|
config :aprsme, AprsmeWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
|
||||||
|
|
||||||
# Runtime production configuration, including reading
|
# Runtime production configuration, including reading
|
||||||
|
|
||||||
|
|
@ -26,4 +26,4 @@ config :esbuild,
|
||||||
config :logger, level: :info
|
config :logger, level: :info
|
||||||
|
|
||||||
# Configures Swoosh API Client
|
# Configures Swoosh API Client
|
||||||
config :swoosh, :api_client, Aprs.Finch
|
config :swoosh, :api_client, Aprsme.Finch
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,13 @@ import Config
|
||||||
# If you use `mix release`, you need to explicitly enable the server
|
# If you use `mix release`, you need to explicitly enable the server
|
||||||
# by passing the PHX_SERVER=true when you start it:
|
# by passing the PHX_SERVER=true when you start it:
|
||||||
#
|
#
|
||||||
# PHX_SERVER=true bin/aprs start
|
# PHX_SERVER=true bin/aprsme start
|
||||||
#
|
#
|
||||||
# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
|
# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
|
||||||
# script that automatically sets the env var above.
|
# script that automatically sets the env var above.
|
||||||
# Always start the server in production/docker environments
|
# Always start the server in production/docker environments
|
||||||
if System.get_env("PHX_SERVER") || config_env() == :prod do
|
if System.get_env("PHX_SERVER") || config_env() == :prod do
|
||||||
config :aprs, AprsWeb.Endpoint, server: true
|
config :aprsme, AprsmeWeb.Endpoint, server: true
|
||||||
end
|
end
|
||||||
|
|
||||||
if config_env() == :prod do
|
if config_env() == :prod do
|
||||||
|
|
@ -46,14 +46,14 @@ if config_env() == :prod do
|
||||||
host = System.get_env("PHX_HOST") || "example.com"
|
host = System.get_env("PHX_HOST") || "example.com"
|
||||||
port = String.to_integer(System.get_env("PORT") || "4000")
|
port = String.to_integer(System.get_env("PORT") || "4000")
|
||||||
|
|
||||||
config :aprs, Aprs.Repo,
|
config :aprsme, Aprsme.Repo,
|
||||||
# ssl: true,
|
# ssl: true,
|
||||||
url: database_url,
|
url: database_url,
|
||||||
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
|
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
|
||||||
socket_options: maybe_ipv6,
|
socket_options: maybe_ipv6,
|
||||||
types: Aprs.PostgresTypes
|
types: Aprsme.PostgresTypes
|
||||||
|
|
||||||
config :aprs, AprsWeb.Endpoint,
|
config :aprsme, AprsmeWeb.Endpoint,
|
||||||
url: [host: host, port: 443, scheme: "https"],
|
url: [host: host, port: 443, scheme: "https"],
|
||||||
http: [
|
http: [
|
||||||
# Bind on all IPv4 interfaces for better container compatibility
|
# Bind on all IPv4 interfaces for better container compatibility
|
||||||
|
|
@ -66,10 +66,10 @@ if config_env() == :prod do
|
||||||
secret_key_base: secret_key_base,
|
secret_key_base: secret_key_base,
|
||||||
server: true
|
server: true
|
||||||
|
|
||||||
# config :aprs, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
|
# config :aprsme, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
|
||||||
|
|
||||||
config :aprs,
|
config :aprsme,
|
||||||
ecto_repos: [Aprs.Repo],
|
ecto_repos: [Aprsme.Repo],
|
||||||
aprs_is_server: System.get_env("APRS_SERVER", "dallas.aprs2.net"),
|
aprs_is_server: System.get_env("APRS_SERVER", "dallas.aprs2.net"),
|
||||||
aprs_is_port: 14_580,
|
aprs_is_port: 14_580,
|
||||||
aprs_is_default_filter: System.get_env("APRS_FILTER"),
|
aprs_is_default_filter: System.get_env("APRS_FILTER"),
|
||||||
|
|
@ -94,7 +94,7 @@ if config_env() == :prod do
|
||||||
# To get SSL working, you will need to add the `https` key
|
# To get SSL working, you will need to add the `https` key
|
||||||
# to your endpoint configuration:
|
# to your endpoint configuration:
|
||||||
#
|
#
|
||||||
# config :aprs, AprsWeb.Endpoint,
|
# config :aprsme, AprsmeWeb.Endpoint,
|
||||||
# https: [
|
# https: [
|
||||||
# ...,
|
# ...,
|
||||||
# port: 443,
|
# port: 443,
|
||||||
|
|
@ -116,7 +116,7 @@ if config_env() == :prod do
|
||||||
# We also recommend setting `force_ssl` in your endpoint, ensuring
|
# We also recommend setting `force_ssl` in your endpoint, ensuring
|
||||||
# no data is ever sent via http, always redirecting to https:
|
# no data is ever sent via http, always redirecting to https:
|
||||||
#
|
#
|
||||||
# config :aprs, AprsWeb.Endpoint,
|
# config :aprsme, AprsmeWeb.Endpoint,
|
||||||
# force_ssl: [hsts: true]
|
# force_ssl: [hsts: true]
|
||||||
#
|
#
|
||||||
# Check `Plug.SSL` for all available options in `force_ssl`.
|
# Check `Plug.SSL` for all available options in `force_ssl`.
|
||||||
|
|
@ -127,7 +127,7 @@ if config_env() == :prod do
|
||||||
# Also, you may need to configure the Swoosh API client of your choice if you
|
# Also, you may need to configure the Swoosh API client of your choice if you
|
||||||
# are not using SMTP. Here is an example of the configuration:
|
# are not using SMTP. Here is an example of the configuration:
|
||||||
#
|
#
|
||||||
# config :aprs, Aprs.Mailer,
|
# config :aprsme, Aprsme.Mailer,
|
||||||
# adapter: Swoosh.Adapters.Mailgun,
|
# adapter: Swoosh.Adapters.Mailgun,
|
||||||
# api_key: System.get_env("MAILGUN_API_KEY"),
|
# api_key: System.get_env("MAILGUN_API_KEY"),
|
||||||
# domain: System.get_env("MAILGUN_DOMAIN")
|
# domain: System.get_env("MAILGUN_DOMAIN")
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,40 @@
|
||||||
import Config
|
import Config
|
||||||
|
|
||||||
# In test we don't send emails.
|
# In test we don't send emails.
|
||||||
config :aprs, Aprs.Mailer, adapter: Swoosh.Adapters.Test
|
config :aprsme, Aprsme.Mailer, adapter: Swoosh.Adapters.Test
|
||||||
|
|
||||||
# Configure your database
|
# Configure your database
|
||||||
#
|
#
|
||||||
# The MIX_TEST_PARTITION environment variable can be used
|
# The MIX_TEST_PARTITION environment variable can be used
|
||||||
# to provide built-in test partitioning in CI environment.
|
# to provide built-in test partitioning in CI environment.
|
||||||
# Run `mix help test` for more information.
|
# Run `mix help test` for more information.
|
||||||
config :aprs, Aprs.Repo,
|
config :aprsme, Aprsme.Repo,
|
||||||
username: "postgres",
|
username: "postgres",
|
||||||
password: "postgres",
|
password: "postgres",
|
||||||
hostname: "localhost",
|
hostname: "localhost",
|
||||||
database: "aprs_test#{System.get_env("MIX_TEST_PARTITION")}",
|
database: "aprsme_test#{System.get_env("MIX_TEST_PARTITION")}",
|
||||||
pool: Ecto.Adapters.SQL.Sandbox,
|
pool: Ecto.Adapters.SQL.Sandbox,
|
||||||
pool_size: System.schedulers_online() * 2,
|
pool_size: System.schedulers_online() * 2,
|
||||||
types: Aprs.PostgresTypes
|
types: Aprsme.PostgresTypes
|
||||||
|
|
||||||
# We don't run a server during test. If one is required,
|
# We don't run a server during test. If one is required,
|
||||||
# you can enable the server option below.
|
# you can enable the server option below.
|
||||||
config :aprs, AprsWeb.Endpoint,
|
config :aprsme, AprsmeWeb.Endpoint,
|
||||||
http: [ip: {127, 0, 0, 1}, port: 4002],
|
http: [ip: {127, 0, 0, 1}, port: 4002],
|
||||||
secret_key_base: "IV9+ENaw9i8xjReRk4sULRvRgsmFVTGQwQGGrf4G+Q/SFMeHBCNWRlPXQ2YvT36R",
|
secret_key_base: "IV9+ENaw9i8xjReRk4sULRvRgsmFVTGQwQGGrf4G+Q/SFMeHBCNWRlPXQ2YvT36R",
|
||||||
server: false
|
server: false
|
||||||
|
|
||||||
# Disable Oban during tests to prevent background job execution
|
# Disable Oban during tests to prevent background job execution
|
||||||
config :aprs, Oban, testing: :inline
|
config :aprsme, Oban, testing: :inline
|
||||||
|
|
||||||
# Disable initialize replay delay in test environment
|
# Disable initialize replay delay in test environment
|
||||||
config :aprs, :initialize_replay_delay, 0
|
config :aprsme, :initialize_replay_delay, 0
|
||||||
|
|
||||||
# Configure the packets module to use the mock in tests
|
# Configure the packets module to use the mock in tests
|
||||||
config :aprs, :packets_module, Aprs.PacketsMock
|
config :aprsme, :packets_module, Aprsme.PacketsMock
|
||||||
|
|
||||||
# Disable APRS-IS external connections in test environment
|
# Disable APRS-IS external connections in test environment
|
||||||
config :aprs,
|
config :aprsme,
|
||||||
aprs_is_server: "mock.aprs.test",
|
aprs_is_server: "mock.aprs.test",
|
||||||
aprs_is_port: 14_580,
|
aprs_is_port: 14_580,
|
||||||
aprs_is_default_filter: "r/33/-96/100",
|
aprs_is_default_filter: "r/33/-96/100",
|
||||||
|
|
@ -43,7 +43,7 @@ config :aprs,
|
||||||
disable_aprs_connection: true
|
disable_aprs_connection: true
|
||||||
|
|
||||||
# Disable automatic migrations during tests
|
# Disable automatic migrations during tests
|
||||||
config :aprs, auto_migrate: false
|
config :aprsme, auto_migrate: false
|
||||||
|
|
||||||
# Only in tests, remove the complexity from the password hashing algorithm
|
# Only in tests, remove the complexity from the password hashing algorithm
|
||||||
config :bcrypt_elixir, :log_rounds, 1
|
config :bcrypt_elixir, :log_rounds, 1
|
||||||
|
|
|
||||||
|
|
@ -49,5 +49,5 @@ spec:
|
||||||
initialDelaySeconds: 30
|
initialDelaySeconds: 30
|
||||||
periodSeconds: 15
|
periodSeconds: 15
|
||||||
# Note: For the health endpoint, you might need to create a simple one in your Phoenix router
|
# Note: For the health endpoint, you might need to create a simple one in your Phoenix router
|
||||||
# e.g., scope "/", AprsWeb do; get "/health", PageController, :health; end
|
# e.g., scope "/", AprsmeWeb do; get "/health", PageController, :health; end
|
||||||
# and a `def health(conn, _params), do: json(conn, %{status: "ok"})` in PageController.
|
# and a `def health(conn, _params), do: json(conn, %{status: "ok"})` in PageController.
|
||||||
|
|
|
||||||
|
|
@ -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 """
|
@moduledoc """
|
||||||
The entrypoint for defining your web interface, such
|
The entrypoint for defining your web interface, such
|
||||||
as controllers, components, channels, and so on.
|
as controllers, components, channels, and so on.
|
||||||
|
|
||||||
This can be used in your application as:
|
This can be used in your application as:
|
||||||
|
|
||||||
use AprsWeb, :controller
|
use AprsmeWeb, :controller
|
||||||
use AprsWeb, :html
|
use AprsmeWeb, :html
|
||||||
|
|
||||||
The definitions below will be executed for every controller,
|
The definitions below will be executed for every view,
|
||||||
component, etc, so keep them short and clean, focused
|
component, etc, so keep them short and clean, focused
|
||||||
on imports, uses and aliases.
|
on imports, uses and aliases.
|
||||||
|
|
||||||
Do NOT define functions inside the quoted expressions
|
Do NOT define functions inside the quoted expressions
|
||||||
below. Instead, define additional modules and import
|
below. Instead, define additional functions and do the
|
||||||
those modules here.
|
imports in your modules.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def static_paths, do: ~w(assets fonts images aprs-symbols favicon.ico robots.txt)
|
def static_paths, do: ~w(assets fonts images aprs-symbols favicon.ico robots.txt)
|
||||||
|
|
@ -23,9 +23,10 @@ defmodule AprsWeb do
|
||||||
quote do
|
quote do
|
||||||
use Phoenix.Router, helpers: false
|
use Phoenix.Router, helpers: false
|
||||||
|
|
||||||
# Import common connection and controller functions to use in pipelines
|
|
||||||
import Phoenix.Controller
|
import Phoenix.Controller
|
||||||
import Phoenix.LiveView.Router
|
import Phoenix.LiveView.Router
|
||||||
|
|
||||||
|
# Import common connection and controller functions to use in pipelines
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -33,17 +34,18 @@ defmodule AprsWeb do
|
||||||
def channel do
|
def channel do
|
||||||
quote do
|
quote do
|
||||||
use Phoenix.Channel
|
use Phoenix.Channel
|
||||||
|
|
||||||
|
import AprsmeWeb.Gettext
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def controller do
|
def controller do
|
||||||
quote do
|
quote do
|
||||||
use Phoenix.Controller,
|
use Phoenix.Controller,
|
||||||
namespace: AprsWeb,
|
|
||||||
formats: [:html, :json],
|
formats: [:html, :json],
|
||||||
layouts: [html: AprsWeb.Layouts]
|
layouts: [html: AprsmeWeb.Layouts]
|
||||||
|
|
||||||
import AprsWeb.Gettext
|
import AprsmeWeb.Gettext
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
|
|
||||||
unquote(verified_routes())
|
unquote(verified_routes())
|
||||||
|
|
@ -53,7 +55,7 @@ defmodule AprsWeb do
|
||||||
def live_view do
|
def live_view do
|
||||||
quote do
|
quote do
|
||||||
use Phoenix.LiveView,
|
use Phoenix.LiveView,
|
||||||
layout: {AprsWeb.Layouts, :app}
|
layout: {AprsmeWeb.Layouts, :app}
|
||||||
|
|
||||||
unquote(html_helpers())
|
unquote(html_helpers())
|
||||||
end
|
end
|
||||||
|
|
@ -82,11 +84,11 @@ defmodule AprsWeb do
|
||||||
|
|
||||||
defp html_helpers do
|
defp html_helpers do
|
||||||
quote do
|
quote do
|
||||||
|
import AprsmeWeb.CoreComponents
|
||||||
|
import AprsmeWeb.Gettext
|
||||||
# HTML escaping functionality
|
# HTML escaping functionality
|
||||||
# Core UI components and translation
|
|
||||||
import AprsWeb.CoreComponents
|
|
||||||
import AprsWeb.Gettext
|
|
||||||
import Phoenix.HTML
|
import Phoenix.HTML
|
||||||
|
# Core UI components and translation
|
||||||
|
|
||||||
# Shortcut for generating JS commands
|
# Shortcut for generating JS commands
|
||||||
alias Phoenix.LiveView.JS
|
alias Phoenix.LiveView.JS
|
||||||
|
|
@ -99,9 +101,9 @@ defmodule AprsWeb do
|
||||||
def verified_routes do
|
def verified_routes do
|
||||||
quote do
|
quote do
|
||||||
use Phoenix.VerifiedRoutes,
|
use Phoenix.VerifiedRoutes,
|
||||||
endpoint: AprsWeb.Endpoint,
|
endpoint: AprsmeWeb.Endpoint,
|
||||||
router: AprsWeb.Router,
|
router: AprsmeWeb.Router,
|
||||||
statics: AprsWeb.static_paths()
|
statics: AprsmeWeb.static_paths()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
5
lib/aprsme.ex
Normal file
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 """
|
@moduledoc """
|
||||||
The Accounts context.
|
The Accounts context.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import Ecto.Query, warn: false
|
import Ecto.Query, warn: false
|
||||||
|
|
||||||
alias Aprs.Accounts.User
|
alias Aprsme.Accounts.User
|
||||||
alias Aprs.Accounts.UserNotifier
|
alias Aprsme.Accounts.UserNotifier
|
||||||
alias Aprs.Accounts.UserToken
|
alias Aprsme.Accounts.UserToken
|
||||||
alias Aprs.Repo
|
alias Aprsme.Repo
|
||||||
|
|
||||||
## Database getters
|
## Database getters
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Aprs.Accounts.User do
|
defmodule Aprsme.Accounts.User do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
|
|
||||||
|
|
@ -89,7 +89,7 @@ defmodule Aprs.Accounts.User do
|
||||||
|
|
||||||
defp do_validate_unique_email(changeset, true) do
|
defp do_validate_unique_email(changeset, true) do
|
||||||
changeset
|
changeset
|
||||||
|> unsafe_validate_unique(:email, Aprs.Repo)
|
|> unsafe_validate_unique(:email, Aprsme.Repo)
|
||||||
|> unique_constraint(:email)
|
|> unique_constraint(:email)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -143,7 +143,7 @@ defmodule Aprs.Accounts.User do
|
||||||
If there is no user or the user doesn't have a password, we call
|
If there is no user or the user doesn't have a password, we call
|
||||||
`Bcrypt.no_user_verify/0` to avoid timing attacks.
|
`Bcrypt.no_user_verify/0` to avoid timing attacks.
|
||||||
"""
|
"""
|
||||||
def valid_password?(%Aprs.Accounts.User{hashed_password: hashed_password}, password)
|
def valid_password?(%Aprsme.Accounts.User{hashed_password: hashed_password}, password)
|
||||||
when is_binary(hashed_password) and byte_size(password) > 0 do
|
when is_binary(hashed_password) and byte_size(password) > 0 do
|
||||||
Bcrypt.verify_pass(password, hashed_password)
|
Bcrypt.verify_pass(password, hashed_password)
|
||||||
end
|
end
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
defmodule Aprs.Accounts.UserNotifier do
|
defmodule Aprsme.Accounts.UserNotifier do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
import Swoosh.Email
|
import Swoosh.Email
|
||||||
|
|
||||||
alias Aprs.Mailer
|
alias Aprsme.Mailer
|
||||||
|
|
||||||
# Delivers the email using the application mailer.
|
# Delivers the email using the application mailer.
|
||||||
defp deliver(recipient, subject, body) do
|
defp deliver(recipient, subject, body) do
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
defmodule Aprs.Accounts.UserToken do
|
defmodule Aprsme.Accounts.UserToken do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
alias Aprs.Accounts.UserToken
|
alias Aprsme.Accounts.UserToken
|
||||||
|
|
||||||
@hash_algorithm :sha256
|
@hash_algorithm :sha256
|
||||||
@rand_size 32
|
@rand_size 32
|
||||||
|
|
@ -20,7 +20,7 @@ defmodule Aprs.Accounts.UserToken do
|
||||||
field(:token, :binary)
|
field(:token, :binary)
|
||||||
field(:context, :string)
|
field(:context, :string)
|
||||||
field(:sent_to, :string)
|
field(:sent_to, :string)
|
||||||
belongs_to(:user, Aprs.Accounts.User)
|
belongs_to(:user, Aprsme.Accounts.User)
|
||||||
|
|
||||||
timestamps(updated_at: false)
|
timestamps(updated_at: false)
|
||||||
end
|
end
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Aprs.Application do
|
defmodule Aprsme.Application do
|
||||||
# See https://hexdocs.pm/elixir/Application.html
|
# See https://hexdocs.pm/elixir/Application.html
|
||||||
# for more information on OTP Applications
|
# for more information on OTP Applications
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
@ -16,35 +16,40 @@ defmodule Aprs.Application do
|
||||||
|
|
||||||
children = [
|
children = [
|
||||||
# Start the Telemetry supervisor
|
# Start the Telemetry supervisor
|
||||||
AprsWeb.Telemetry,
|
AprsmeWeb.Telemetry,
|
||||||
# Start the Ecto repository
|
# Start the Ecto repository
|
||||||
Aprs.Repo,
|
Aprsme.Repo,
|
||||||
# Start the PubSub system
|
# Start the PubSub system
|
||||||
{Phoenix.PubSub, name: Aprs.PubSub},
|
{Phoenix.PubSub, name: Aprsme.PubSub},
|
||||||
# Start Finch
|
# Start Finch
|
||||||
{Finch, name: Aprs.Finch},
|
{Finch, name: Aprsme.Finch},
|
||||||
# Start the Endpoint (http/https)
|
# Start the Endpoint (http/https)
|
||||||
AprsWeb.Endpoint,
|
AprsmeWeb.Endpoint,
|
||||||
# Start a worker by calling: Aprs.Worker.start_link(arg)
|
# Start a worker by calling: Aprsme.Worker.start_link(arg)
|
||||||
# {Aprs.Worker, arg}
|
# {Aprsme.Worker, arg}
|
||||||
{Registry, keys: :duplicate, name: Registry.PubSub, partitions: System.schedulers_online()},
|
{Registry, keys: :duplicate, name: Registry.PubSub, partitions: System.schedulers_online()},
|
||||||
{Cluster.Supervisor, [topologies, [name: Aprs.ClusterSupervisor]]},
|
{Cluster.Supervisor, [topologies, [name: Aprsme.ClusterSupervisor]]},
|
||||||
# Start Oban for background jobs
|
# Start Oban for background jobs
|
||||||
{Oban, :aprs |> Application.get_env(Oban, []) |> Keyword.put(:queues, default: 10, maintenance: 2)},
|
{Oban, :aprsme |> Application.get_env(Oban, []) |> Keyword.put(:queues, default: 10, maintenance: 2)},
|
||||||
Aprs.Presence,
|
Aprsme.Presence,
|
||||||
Aprs.AprsIsConnection,
|
Aprsme.AprsIsConnection,
|
||||||
Aprs.PostgresNotifier
|
Aprsme.PostgresNotifier
|
||||||
]
|
]
|
||||||
|
|
||||||
children = maybe_add_is_supervisor(children, Application.get_env(:aprs, :env))
|
children = maybe_add_is_supervisor(children, Application.get_env(:aprsme, :env))
|
||||||
|
|
||||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||||
# for other strategies and supported options
|
# for other strategies and supported options
|
||||||
opts = [strategy: :one_for_one, name: Aprs.Supervisor]
|
opts = [strategy: :one_for_one, name: Aprsme.Supervisor]
|
||||||
{:ok, sup} = Supervisor.start_link(children, opts)
|
{:ok, sup} = Supervisor.start_link(children, opts)
|
||||||
|
|
||||||
# Now that the Repo is started, run the refresh in a background task
|
# Now that the Repo is started, run the refresh in a background task
|
||||||
Task.start(fn -> Aprs.DeviceIdentification.maybe_refresh_devices() end)
|
# Skip in test environment to avoid DBConnection.OwnershipError
|
||||||
|
env = Application.get_env(:aprsme, :env)
|
||||||
|
|
||||||
|
if env != :test do
|
||||||
|
Task.start(fn -> Aprsme.DeviceIdentification.maybe_refresh_devices() end)
|
||||||
|
end
|
||||||
|
|
||||||
{:ok, sup}
|
{:ok, sup}
|
||||||
end
|
end
|
||||||
|
|
@ -53,12 +58,12 @@ defmodule Aprs.Application do
|
||||||
# whenever the application is updated.
|
# whenever the application is updated.
|
||||||
@impl true
|
@impl true
|
||||||
def config_change(changed, _new, removed) do
|
def config_change(changed, _new, removed) do
|
||||||
AprsWeb.Endpoint.config_change(changed, removed)
|
AprsmeWeb.Endpoint.config_change(changed, removed)
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
defp migrate do
|
defp migrate do
|
||||||
auto_migrate = Application.get_env(:aprs, :auto_migrate, true)
|
auto_migrate = Application.get_env(:aprsme, :auto_migrate, true)
|
||||||
do_migrate(auto_migrate)
|
do_migrate(auto_migrate)
|
||||||
rescue
|
rescue
|
||||||
error ->
|
error ->
|
||||||
|
|
@ -70,7 +75,7 @@ defmodule Aprs.Application do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_add_is_supervisor(children, env) when env in [:prod, :dev] do
|
defp maybe_add_is_supervisor(children, env) when env in [:prod, :dev] do
|
||||||
children ++ [Aprs.Is.IsSupervisor]
|
children ++ [Aprsme.Is.IsSupervisor]
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_add_is_supervisor(children, _env), do: children
|
defp maybe_add_is_supervisor(children, _env), do: children
|
||||||
|
|
@ -79,7 +84,7 @@ defmodule Aprs.Application do
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
Logger.info("Running database migrations...")
|
Logger.info("Running database migrations...")
|
||||||
Aprs.Release.migrate()
|
Aprsme.Release.migrate()
|
||||||
Logger.info("Database migrations completed")
|
Logger.info("Database migrations completed")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Aprs.AprsIsConnection do
|
defmodule Aprsme.AprsIsConnection do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Maintains a supervised TCP connection to APRS-IS, with reconnection logic and
|
Maintains a supervised TCP connection to APRS-IS, with reconnection logic and
|
||||||
exponential backoff. Broadcasts each received line via Phoenix.PubSub and emits
|
exponential backoff. Broadcasts each received line via Phoenix.PubSub and emits
|
||||||
|
|
@ -43,13 +43,13 @@ defmodule Aprs.AprsIsConnection do
|
||||||
case connect_aprs_is() do
|
case connect_aprs_is() do
|
||||||
{:ok, socket} ->
|
{:ok, socket} ->
|
||||||
Logger.info("Connected to APRS-IS")
|
Logger.info("Connected to APRS-IS")
|
||||||
:telemetry.execute([:aprs, :is, :connected], %{}, %{})
|
:telemetry.execute([:aprsme, :is, :connected], %{}, %{})
|
||||||
{:noreply, %{state | socket: socket, backoff: @reconnect_initial}}
|
{:noreply, %{state | socket: socket, backoff: @reconnect_initial}}
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
Logger.error("APRS-IS connection failed: #{inspect(reason)}. Retrying in #{state.backoff} ms.")
|
Logger.error("APRS-IS connection failed: #{inspect(reason)}. Retrying in #{state.backoff} ms.")
|
||||||
|
|
||||||
:telemetry.execute([:aprs, :is, :connect_error], %{}, %{reason: reason})
|
:telemetry.execute([:aprsme, :is, :connect_error], %{}, %{reason: reason})
|
||||||
schedule_connect(state.backoff)
|
schedule_connect(state.backoff)
|
||||||
{:noreply, %{state | socket: nil, backoff: min(state.backoff * 2, @reconnect_max)}}
|
{:noreply, %{state | socket: nil, backoff: min(state.backoff * 2, @reconnect_max)}}
|
||||||
end
|
end
|
||||||
|
|
@ -58,15 +58,15 @@ defmodule Aprs.AprsIsConnection do
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:tcp, _socket, data}, state) do
|
def handle_info({:tcp, _socket, data}, state) do
|
||||||
# Each line received from APRS-IS
|
# Each line received from APRS-IS
|
||||||
Phoenix.PubSub.broadcast(Aprs.PubSub, @pubsub_topic, {:aprs_is_line, data})
|
Phoenix.PubSub.broadcast(Aprsme.PubSub, @pubsub_topic, {:aprsme_is_line, data})
|
||||||
:telemetry.execute([:aprs, :is, :packet], %{size: byte_size(data)}, %{data: data})
|
:telemetry.execute([:aprsme, :is, :packet], %{size: byte_size(data)}, %{data: data})
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:tcp_closed, _socket}, state) do
|
def handle_info({:tcp_closed, _socket}, state) do
|
||||||
Logger.warning("APRS-IS connection closed. Reconnecting...")
|
Logger.warning("APRS-IS connection closed. Reconnecting...")
|
||||||
:telemetry.execute([:aprs, :is, :disconnected], %{}, %{})
|
:telemetry.execute([:aprsme, :is, :disconnected], %{}, %{})
|
||||||
schedule_connect(@reconnect_initial)
|
schedule_connect(@reconnect_initial)
|
||||||
{:noreply, %{state | socket: nil, backoff: @reconnect_initial}}
|
{:noreply, %{state | socket: nil, backoff: @reconnect_initial}}
|
||||||
end
|
end
|
||||||
|
|
@ -74,7 +74,7 @@ defmodule Aprs.AprsIsConnection do
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:tcp_error, _socket, reason}, state) do
|
def handle_info({:tcp_error, _socket, reason}, state) do
|
||||||
Logger.error("APRS-IS TCP error: #{inspect(reason)}. Reconnecting...")
|
Logger.error("APRS-IS TCP error: #{inspect(reason)}. Reconnecting...")
|
||||||
:telemetry.execute([:aprs, :is, :tcp_error], %{}, %{reason: reason})
|
:telemetry.execute([:aprsme, :is, :tcp_error], %{}, %{reason: reason})
|
||||||
schedule_connect(@reconnect_initial)
|
schedule_connect(@reconnect_initial)
|
||||||
{:noreply, %{state | socket: nil, backoff: @reconnect_initial}}
|
{:noreply, %{state | socket: nil, backoff: @reconnect_initial}}
|
||||||
end
|
end
|
||||||
|
|
@ -102,11 +102,11 @@ defmodule Aprs.AprsIsConnection do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp connect_aprs_is do
|
defp connect_aprs_is do
|
||||||
host = Application.get_env(:aprs, :aprs_is_host, ~c"rotate.aprs2.net")
|
host = Application.get_env(:aprsme, :aprsme_is_host, ~c"rotate.aprs2.net")
|
||||||
port = Application.get_env(:aprs, :aprs_is_port, 14_580)
|
port = Application.get_env(:aprsme, :aprsme_is_port, 14_580)
|
||||||
callsign = Application.get_env(:aprs, :aprs_is_callsign, "N0CALL")
|
callsign = Application.get_env(:aprsme, :aprsme_is_callsign, "N0CALL")
|
||||||
passcode = Application.get_env(:aprs, :aprs_is_passcode, "00000")
|
passcode = Application.get_env(:aprsme, :aprsme_is_passcode, "00000")
|
||||||
filter = Application.get_env(:aprs, :aprs_is_filter, "")
|
filter = Application.get_env(:aprsme, :aprsme_is_filter, "")
|
||||||
|
|
||||||
opts = [:binary, active: true, packet: :line, keepalive: true]
|
opts = [:binary, active: true, packet: :line, keepalive: true]
|
||||||
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
defmodule Aprs.Archiver do
|
defmodule Aprsme.Archiver do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use GenServer
|
use GenServer
|
||||||
|
|
||||||
alias AprsWeb.Endpoint
|
alias AprsmeWeb.Endpoint
|
||||||
|
|
||||||
require Jason
|
require Jason
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
# alias Aprs.{Packet, Repo}
|
# alias Aprsme.{Packet, Repo}
|
||||||
@topic "call"
|
@topic "call"
|
||||||
|
|
||||||
# API
|
# API
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Aprs.BadPacket do
|
defmodule Aprsme.BadPacket do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
|
|
||||||
|
|
@ -17,7 +17,7 @@ defmodule Aprs.BadPacket do
|
||||||
@type t :: %__MODULE__{}
|
@type t :: %__MODULE__{}
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@spec changeset(Aprs.BadPacket.t(), map()) :: Ecto.Changeset.t()
|
@spec changeset(Aprsme.BadPacket.t(), map()) :: Ecto.Changeset.t()
|
||||||
def changeset(bad_packet, attrs) do
|
def changeset(bad_packet, attrs) do
|
||||||
bad_packet
|
bad_packet
|
||||||
|> cast(attrs, [:raw_packet, :error_message, :error_type, :attempted_at])
|
|> cast(attrs, [:raw_packet, :error_message, :error_type, :attempted_at])
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Aprs.Convert do
|
defmodule Aprsme.Convert do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
@spec wind(number(), :ultimeter, :mph) :: float()
|
@spec wind(number(), :ultimeter, :mph) :: float()
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
defmodule Aprs.DataExtended do
|
defmodule Aprsme.DataExtended do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
|
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
alias Aprs.DataExtended
|
alias Aprsme.DataExtended
|
||||||
|
|
||||||
embedded_schema do
|
embedded_schema do
|
||||||
field :aprs_messaging, :boolean, default: false
|
field :aprsme_messaging, :boolean, default: false
|
||||||
field :comment, :string
|
field :comment, :string
|
||||||
field :data_type, :string
|
field :data_type, :string
|
||||||
field :latitude, :decimal
|
field :latitude, :decimal
|
||||||
|
|
@ -19,11 +19,11 @@ defmodule Aprs.DataExtended do
|
||||||
@type t :: %__MODULE__{}
|
@type t :: %__MODULE__{}
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@spec changeset(Aprs.DataExtended.t(), map()) :: Ecto.Changeset.t()
|
@spec changeset(Aprsme.DataExtended.t(), map()) :: Ecto.Changeset.t()
|
||||||
def changeset(%DataExtended{} = data_extended, attrs) do
|
def changeset(%DataExtended{} = data_extended, attrs) do
|
||||||
data_extended
|
data_extended
|
||||||
|> cast(attrs, [
|
|> cast(attrs, [
|
||||||
:aprs_messaging,
|
:aprsme_messaging,
|
||||||
:comment,
|
:comment,
|
||||||
:data_type,
|
:data_type,
|
||||||
:latitude,
|
:latitude,
|
||||||
|
|
@ -32,7 +32,7 @@ defmodule Aprs.DataExtended do
|
||||||
:symbol_table_id
|
:symbol_table_id
|
||||||
])
|
])
|
||||||
|> validate_required([
|
|> validate_required([
|
||||||
:aprs_messaging,
|
:aprsme_messaging,
|
||||||
:comment,
|
:comment,
|
||||||
:data_type,
|
:data_type,
|
||||||
:symbol_code,
|
:symbol_code,
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
defmodule Aprs.DeviceIdentification do
|
defmodule Aprsme.DeviceIdentification do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Handles APRS device identification based on the APRS device identification database.
|
Handles APRS device identification based on the APRS device identification database.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
alias Aprs.Devices
|
alias Aprsme.Devices
|
||||||
alias Aprs.Repo
|
alias Aprsme.Repo
|
||||||
|
|
||||||
@device_patterns [
|
@device_patterns [
|
||||||
{~r/^ \x00\x00$/, "Original MIC-E"},
|
{~r/^ \x00\x00$/, "Original MIC-E"},
|
||||||
|
|
@ -41,13 +41,13 @@ defmodule Aprs.DeviceIdentification do
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> Aprs.DeviceIdentification.identify_device(">" <> <<0>> <> "^")
|
iex> Aprsme.DeviceIdentification.identify_device(">" <> <<0>> <> "^")
|
||||||
"Kenwood TH-D74"
|
"Kenwood TH-D74"
|
||||||
|
|
||||||
iex> Aprs.DeviceIdentification.identify_device("`_#")
|
iex> Aprsme.DeviceIdentification.identify_device("`_#")
|
||||||
"Yaesu VX-8G"
|
"Yaesu VX-8G"
|
||||||
|
|
||||||
iex> Aprs.DeviceIdentification.identify_device("not-a-match")
|
iex> Aprsme.DeviceIdentification.identify_device("not-a-match")
|
||||||
"Unknown"
|
"Unknown"
|
||||||
"""
|
"""
|
||||||
@spec identify_device(String.t()) :: String.t()
|
@spec identify_device(String.t()) :: String.t()
|
||||||
|
|
@ -64,10 +64,10 @@ defmodule Aprs.DeviceIdentification do
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> "Kenwood" in Aprs.DeviceIdentification.known_manufacturers()
|
iex> "Kenwood" in Aprsme.DeviceIdentification.known_manufacturers()
|
||||||
true
|
true
|
||||||
|
|
||||||
iex> Enum.member?(Aprs.DeviceIdentification.known_manufacturers(), "AP510")
|
iex> Enum.member?(Aprsme.DeviceIdentification.known_manufacturers(), "AP510")
|
||||||
true
|
true
|
||||||
"""
|
"""
|
||||||
@spec known_manufacturers() :: [String.t()]
|
@spec known_manufacturers() :: [String.t()]
|
||||||
|
|
@ -93,10 +93,10 @@ defmodule Aprs.DeviceIdentification do
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> Aprs.DeviceIdentification.known_models("Kenwood")
|
iex> Aprsme.DeviceIdentification.known_models("Kenwood")
|
||||||
["TH-D74", "TH-D74A", "DM-710", "DM-700"]
|
["TH-D74", "TH-D74A", "DM-710", "DM-700"]
|
||||||
|
|
||||||
iex> Aprs.DeviceIdentification.known_models("Unknown")
|
iex> Aprsme.DeviceIdentification.known_models("Unknown")
|
||||||
[]
|
[]
|
||||||
"""
|
"""
|
||||||
@spec known_models(String.t()) :: [String.t()]
|
@spec known_models(String.t()) :: [String.t()]
|
||||||
|
|
@ -131,7 +131,7 @@ defmodule Aprs.DeviceIdentification do
|
||||||
def fetch_and_upsert_devices do
|
def fetch_and_upsert_devices do
|
||||||
req = Finch.build(:get, @url)
|
req = Finch.build(:get, @url)
|
||||||
|
|
||||||
case Finch.request(req, Aprs.Finch) do
|
case Finch.request(req, Aprsme.Finch) do
|
||||||
{:ok, %Finch.Response{status: 200, body: body}} ->
|
{:ok, %Finch.Response{status: 200, body: body}} ->
|
||||||
{:ok, json} = Jason.decode(body)
|
{:ok, json} = Jason.decode(body)
|
||||||
upsert_devices(json)
|
upsert_devices(json)
|
||||||
|
|
@ -170,7 +170,7 @@ defmodule Aprs.DeviceIdentification do
|
||||||
|
|
||||||
# Helper to enqueue the job
|
# Helper to enqueue the job
|
||||||
def enqueue_refresh_job do
|
def enqueue_refresh_job do
|
||||||
Oban.insert!(Aprs.DeviceIdentification.Worker.new(%{}))
|
Oban.insert!(Aprsme.DeviceIdentification.Worker.new(%{}))
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
@ -179,6 +179,8 @@ defmodule Aprs.DeviceIdentification do
|
||||||
|
|
||||||
Note: This function requires the devices table to be seeded and a running Repo context.
|
Note: This function requires the devices table to be seeded and a running Repo context.
|
||||||
"""
|
"""
|
||||||
|
def lookup_device_by_identifier(nil), do: nil
|
||||||
|
|
||||||
def lookup_device_by_identifier(identifier) when is_binary(identifier) do
|
def lookup_device_by_identifier(identifier) when is_binary(identifier) do
|
||||||
# Fetch all device patterns from DB
|
# Fetch all device patterns from DB
|
||||||
devices = Repo.all(Devices)
|
devices = Repo.all(Devices)
|
||||||
|
|
@ -218,13 +220,13 @@ defmodule Aprs.DeviceIdentification do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defmodule Aprs.DeviceIdentification.Worker do
|
defmodule Aprsme.DeviceIdentification.Worker do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Oban.Worker, queue: :default, max_attempts: 1
|
use Oban.Worker, queue: :default, max_attempts: 1
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def perform(_job) do
|
def perform(_job) do
|
||||||
Aprs.DeviceIdentification.maybe_refresh_devices()
|
Aprsme.DeviceIdentification.maybe_refresh_devices()
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
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
|
@moduledoc false
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
# test/support/devices_seeder.exs
|
defmodule Aprsme.DevicesSeeder do
|
||||||
alias Aprs.Devices
|
|
||||||
alias Aprs.Repo
|
|
||||||
|
|
||||||
defmodule DevicesSeeder do
|
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
|
alias Aprsme.Devices
|
||||||
|
alias Aprsme.Repo
|
||||||
|
|
||||||
def seed_from_json(path \\ "test/support/test_devices.json") do
|
def seed_from_json(path \\ "test/support/test_devices.json") do
|
||||||
{:ok, body} = File.read(path)
|
{:ok, body} = File.read(path)
|
||||||
{:ok, json} = Jason.decode(body)
|
{:ok, json} = Jason.decode(body)
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Aprs.EncodingUtils do
|
defmodule Aprsme.EncodingUtils do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Utilities for handling encoding issues in APRS packet data.
|
Utilities for handling encoding issues in APRS packet data.
|
||||||
|
|
||||||
|
|
@ -7,7 +7,7 @@ defmodule Aprs.EncodingUtils do
|
||||||
to web clients.
|
to web clients.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
alias Parser.Types.MicE
|
alias Aprs.Types.MicE
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Sanitizes a binary to ensure it can be safely JSON encoded.
|
Sanitizes a binary to ensure it can be safely JSON encoded.
|
||||||
|
|
@ -18,10 +18,10 @@ defmodule Aprs.EncodingUtils do
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> Aprs.EncodingUtils.sanitize_string("Hello World")
|
iex> Aprsme.EncodingUtils.sanitize_string("Hello World")
|
||||||
"Hello World"
|
"Hello World"
|
||||||
|
|
||||||
iex> Aprs.EncodingUtils.sanitize_string(<<72, 101, 108, 108, 111, 211, 87, 111, 114, 108, 100>>)
|
iex> Aprsme.EncodingUtils.sanitize_string(<<72, 101, 108, 108, 111, 211, 87, 111, 114, 108, 100>>)
|
||||||
"HelloÓWorld"
|
"HelloÓWorld"
|
||||||
"""
|
"""
|
||||||
@spec sanitize_string(binary() | nil | any()) :: binary() | nil | any()
|
@spec sanitize_string(binary() | nil | any()) :: binary() | nil | any()
|
||||||
|
|
@ -49,7 +49,7 @@ defmodule Aprs.EncodingUtils do
|
||||||
Sanitizes all string fields in an APRS packet to ensure safe JSON encoding.
|
Sanitizes all string fields in an APRS packet to ensure safe JSON encoding.
|
||||||
"""
|
"""
|
||||||
@spec sanitize_packet(struct() | map()) :: struct() | map()
|
@spec sanitize_packet(struct() | map()) :: struct() | map()
|
||||||
def sanitize_packet(%Aprs.Packet{} = packet) do
|
def sanitize_packet(%Aprsme.Packet{} = packet) do
|
||||||
%{
|
%{
|
||||||
packet
|
packet
|
||||||
| information_field: sanitize_string(packet.information_field),
|
| information_field: sanitize_string(packet.information_field),
|
||||||
|
|
@ -97,7 +97,7 @@ defmodule Aprs.EncodingUtils do
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> Aprs.EncodingUtils.to_hex(<<72, 101, 108, 108, 111>>)
|
iex> Aprsme.EncodingUtils.to_hex(<<72, 101, 108, 108, 111>>)
|
||||||
"48656C6C6F"
|
"48656C6C6F"
|
||||||
"""
|
"""
|
||||||
@spec to_hex(binary()) :: String.t()
|
@spec to_hex(binary()) :: String.t()
|
||||||
|
|
@ -113,10 +113,10 @@ defmodule Aprs.EncodingUtils do
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> Aprs.EncodingUtils.encoding_info("Hello")
|
iex> Aprsme.EncodingUtils.encoding_info("Hello")
|
||||||
%{valid_utf8: true, byte_count: 5, char_count: 5}
|
%{valid_utf8: true, byte_count: 5, char_count: 5}
|
||||||
|
|
||||||
iex> Aprs.EncodingUtils.encoding_info(<<72, 101, 211, 108, 111>>)
|
iex> Aprsme.EncodingUtils.encoding_info(<<72, 101, 211, 108, 111>>)
|
||||||
%{valid_utf8: false, byte_count: 5, invalid_at: 2}
|
%{valid_utf8: false, byte_count: 5, invalid_at: 2}
|
||||||
"""
|
"""
|
||||||
@spec encoding_info(binary()) :: map()
|
@spec encoding_info(binary()) :: map()
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Aprs.Is do
|
defmodule Aprsme.Is do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use GenServer
|
use GenServer
|
||||||
|
|
||||||
|
|
@ -29,8 +29,8 @@ defmodule Aprs.Is do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init(_opts) do
|
def init(_opts) do
|
||||||
env = Application.get_env(:aprs, :env)
|
env = Application.get_env(:aprsme, :env)
|
||||||
disable_connection = Application.get_env(:aprs, :disable_aprs_connection, false)
|
disable_connection = Application.get_env(:aprsme, :disable_aprs_connection, false)
|
||||||
|
|
||||||
do_init(env, disable_connection)
|
do_init(env, disable_connection)
|
||||||
end
|
end
|
||||||
|
|
@ -53,11 +53,11 @@ defmodule Aprs.Is do
|
||||||
Process.sleep(2000)
|
Process.sleep(2000)
|
||||||
|
|
||||||
# Get startup parameters
|
# Get startup parameters
|
||||||
server = Application.get_env(:aprs, :aprs_is_server, ~c"rotate.aprs2.net")
|
server = Application.get_env(:aprsme, :aprsme_is_server, ~c"rotate.aprs2.net")
|
||||||
port = Application.get_env(:aprs, :aprs_is_port, 14_580)
|
port = Application.get_env(:aprsme, :aprsme_is_port, 14_580)
|
||||||
default_filter = Application.get_env(:aprs, :aprs_is_default_filter, "r/33/-96/100")
|
default_filter = Application.get_env(:aprsme, :aprsme_is_default_filter, "r/33/-96/100")
|
||||||
aprs_user_id = Application.get_env(:aprs, :aprs_is_login_id, "W5ISP")
|
aprs_user_id = Application.get_env(:aprsme, :aprsme_is_login_id, "W5ISP")
|
||||||
aprs_passcode = Application.get_env(:aprs, :aprs_is_password, "-1")
|
aprs_passcode = Application.get_env(:aprsme, :aprsme_is_password, "-1")
|
||||||
|
|
||||||
# Record connection start time
|
# Record connection start time
|
||||||
connected_at = DateTime.utc_now()
|
connected_at = DateTime.utc_now()
|
||||||
|
|
@ -95,7 +95,7 @@ defmodule Aprs.Is do
|
||||||
else
|
else
|
||||||
_ ->
|
_ ->
|
||||||
Logger.error("Unable to establish connection or log in to APRS-IS")
|
Logger.error("Unable to establish connection or log in to APRS-IS")
|
||||||
{:stop, :aprs_connection_failed}
|
{:stop, :aprsme_connection_failed}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -110,8 +110,8 @@ defmodule Aprs.Is do
|
||||||
case Process.whereis(__MODULE__) do
|
case Process.whereis(__MODULE__) do
|
||||||
nil ->
|
nil ->
|
||||||
# GenServer is not running (disconnected)
|
# GenServer is not running (disconnected)
|
||||||
server = Application.get_env(:aprs, :aprs_is_server, ~c"rotate.aprs2.net")
|
server = Application.get_env(:aprsme, :aprsme_is_server, nil)
|
||||||
port = Application.get_env(:aprs, :aprs_is_port, 14_580)
|
port = Application.get_env(:aprsme, :aprsme_is_port, 14_580)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
connected: false,
|
connected: false,
|
||||||
|
|
@ -119,10 +119,10 @@ defmodule Aprs.Is do
|
||||||
port: port,
|
port: port,
|
||||||
connected_at: nil,
|
connected_at: nil,
|
||||||
uptime_seconds: 0,
|
uptime_seconds: 0,
|
||||||
login_id: Application.get_env(:aprs, :aprs_is_login_id, "W5ISP"),
|
login_id: Application.get_env(:aprsme, :aprsme_is_login_id, "W5ISP"),
|
||||||
filter: Application.get_env(:aprs, :aprs_is_default_filter, "r/33/-96/100"),
|
filter: Application.get_env(:aprsme, :aprsme_is_default_filter, "r/33/-96/100"),
|
||||||
packet_stats: default_packet_stats(),
|
packet_stats: default_packet_stats(),
|
||||||
stored_packet_count: Aprs.Packets.get_total_packet_count()
|
stored_packet_count: Aprsme.Packets.get_total_packet_count()
|
||||||
}
|
}
|
||||||
|
|
||||||
_pid ->
|
_pid ->
|
||||||
|
|
@ -131,8 +131,8 @@ defmodule Aprs.Is do
|
||||||
catch
|
catch
|
||||||
:exit, _ ->
|
:exit, _ ->
|
||||||
# GenServer exists but not responding
|
# GenServer exists but not responding
|
||||||
server = Application.get_env(:aprs, :aprs_is_server, ~c"rotate.aprs2.net")
|
server = Application.get_env(:aprsme, :aprsme_is_server, ~c"rotate.aprs2.net")
|
||||||
port = Application.get_env(:aprs, :aprs_is_port, 14_580)
|
port = Application.get_env(:aprsme, :aprsme_is_port, 14_580)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
connected: false,
|
connected: false,
|
||||||
|
|
@ -140,10 +140,10 @@ defmodule Aprs.Is do
|
||||||
port: port,
|
port: port,
|
||||||
connected_at: nil,
|
connected_at: nil,
|
||||||
uptime_seconds: 0,
|
uptime_seconds: 0,
|
||||||
login_id: Application.get_env(:aprs, :aprs_is_login_id, "W5ISP"),
|
login_id: Application.get_env(:aprsme, :aprsme_is_login_id, "W5ISP"),
|
||||||
filter: Application.get_env(:aprs, :aprs_is_default_filter, "r/33/-96/100"),
|
filter: Application.get_env(:aprsme, :aprsme_is_default_filter, "r/33/-96/100"),
|
||||||
packet_stats: default_packet_stats(),
|
packet_stats: default_packet_stats(),
|
||||||
stored_packet_count: Aprs.Packets.get_total_packet_count()
|
stored_packet_count: Aprsme.Packets.get_total_packet_count()
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -167,8 +167,8 @@ defmodule Aprs.Is do
|
||||||
{:ok, :ssl.sslsocket()} | {:error, any()}
|
{:ok, :ssl.sslsocket()} | {:error, any()}
|
||||||
defp connect_to_aprs_is(server, port) do
|
defp connect_to_aprs_is(server, port) do
|
||||||
# Additional safeguard: prevent connections in test environment
|
# Additional safeguard: prevent connections in test environment
|
||||||
env = Application.get_env(:aprs, :env)
|
env = Application.get_env(:aprsme, :env)
|
||||||
disable_connection = Application.get_env(:aprs, :disable_aprs_connection, false)
|
disable_connection = Application.get_env(:aprsme, :disable_aprs_connection, false)
|
||||||
|
|
||||||
do_connect_to_aprs_is(server, port, env, disable_connection)
|
do_connect_to_aprs_is(server, port, env, disable_connection)
|
||||||
end
|
end
|
||||||
|
|
@ -186,7 +186,8 @@ defmodule Aprs.Is do
|
||||||
defp do_connect_to_aprs_is(server, port, _env, false) do
|
defp do_connect_to_aprs_is(server, port, _env, false) do
|
||||||
Logger.debug("Connecting to: #{server}:#{port}")
|
Logger.debug("Connecting to: #{server}:#{port}")
|
||||||
opts = [:binary, active: true]
|
opts = [:binary, active: true]
|
||||||
:gen_tcp.connect(String.to_charlist(server), port, opts)
|
server_string = if is_list(server), do: List.to_string(server), else: server
|
||||||
|
:gen_tcp.connect(String.to_charlist(server_string), port, opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec send_login_string(:ssl.sslsocket(), String.t(), String.t(), String.t()) ::
|
@spec send_login_string(:ssl.sslsocket(), String.t(), String.t(), String.t()) ::
|
||||||
|
|
@ -202,7 +203,7 @@ defmodule Aprs.Is do
|
||||||
|
|
||||||
@spec create_timer(non_neg_integer()) :: reference()
|
@spec create_timer(non_neg_integer()) :: reference()
|
||||||
defp create_timer(timeout) do
|
defp create_timer(timeout) do
|
||||||
Process.send_after(self(), :aprs_no_message_timeout, timeout)
|
Process.send_after(self(), :aprsme_no_message_timeout, timeout)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec create_keepalive_timer(non_neg_integer()) :: reference()
|
@spec create_keepalive_timer(non_neg_integer()) :: reference()
|
||||||
|
|
@ -212,7 +213,7 @@ defmodule Aprs.Is do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_call({:send_message, message}, _from, state) do
|
def handle_call({:send_message, message}, _from, state) do
|
||||||
next_ack_number = :ets.update_counter(:aprs, :message_number, 1)
|
next_ack_number = :ets.update_counter(:aprsme, :message_number, 1)
|
||||||
# Append ack number
|
# Append ack number
|
||||||
message = message <> "{" <> to_string(next_ack_number) <> "\r"
|
message = message <> "{" <> to_string(next_ack_number) <> "\r"
|
||||||
|
|
||||||
|
|
@ -231,16 +232,16 @@ defmodule Aprs.Is do
|
||||||
login_id: state.login_params.user_id,
|
login_id: state.login_params.user_id,
|
||||||
filter: state.login_params.filter,
|
filter: state.login_params.filter,
|
||||||
packet_stats: state.packet_stats,
|
packet_stats: state.packet_stats,
|
||||||
stored_packet_count: Aprs.Packets.get_total_packet_count()
|
stored_packet_count: Aprsme.Packets.get_total_packet_count()
|
||||||
}
|
}
|
||||||
|
|
||||||
{:reply, status, state}
|
{:reply, status, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info(:aprs_no_message_timeout, state) do
|
def handle_info(:aprsme_no_message_timeout, state) do
|
||||||
Logger.error("Socket timeout detected. Killing genserver.")
|
Logger.error("Socket timeout detected. Killing genserver.")
|
||||||
{:stop, :aprs_timeout, state}
|
{:stop, :aprsme_timeout, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info(:send_keepalive, state) do
|
def handle_info(:send_keepalive, state) do
|
||||||
|
|
@ -283,7 +284,7 @@ defmodule Aprs.Is do
|
||||||
end)
|
end)
|
||||||
|
|
||||||
# Start a new timer
|
# Start a new timer
|
||||||
timer = Process.send_after(self(), :aprs_no_message_timeout, @aprs_timeout)
|
timer = Process.send_after(self(), :aprsme_no_message_timeout, @aprs_timeout)
|
||||||
|
|
||||||
state =
|
state =
|
||||||
state
|
state
|
||||||
|
|
@ -318,7 +319,7 @@ defmodule Aprs.Is do
|
||||||
end)
|
end)
|
||||||
|
|
||||||
# Start a new timer
|
# Start a new timer
|
||||||
timer = Process.send_after(self(), :aprs_no_message_timeout, @aprs_timeout)
|
timer = Process.send_after(self(), :aprsme_no_message_timeout, @aprs_timeout)
|
||||||
|
|
||||||
state =
|
state =
|
||||||
state
|
state
|
||||||
|
|
@ -408,7 +409,7 @@ defmodule Aprs.Is do
|
||||||
def dispatch(""), do: nil
|
def dispatch(""), do: nil
|
||||||
|
|
||||||
def dispatch(message) do
|
def dispatch(message) do
|
||||||
case Parser.parse(message) do
|
case Aprs.parse(message) do
|
||||||
{:ok, parsed_message} ->
|
{:ok, parsed_message} ->
|
||||||
# Store the packet in the database for future replay
|
# Store the packet in the database for future replay
|
||||||
# Use Task to avoid slowing down the main process
|
# Use Task to avoid slowing down the main process
|
||||||
|
|
@ -424,13 +425,13 @@ defmodule Aprs.Is do
|
||||||
attrs = struct_to_map(packet_data)
|
attrs = struct_to_map(packet_data)
|
||||||
|
|
||||||
# Extract additional data from the parsed packet including raw packet
|
# Extract additional data from the parsed packet including raw packet
|
||||||
attrs = Aprs.Packet.extract_additional_data(attrs, message)
|
attrs = Aprsme.Packet.extract_additional_data(attrs, message)
|
||||||
|
|
||||||
# Normalize data_type to string if it's an atom
|
# Normalize data_type to string if it's an atom
|
||||||
attrs = normalize_data_type(attrs)
|
attrs = normalize_data_type(attrs)
|
||||||
|
|
||||||
# Store in database through the Packets context
|
# Store in database through the Packets context
|
||||||
case Aprs.Packets.store_packet(attrs) do
|
case Aprsme.Packets.store_packet(attrs) do
|
||||||
{:ok, _packet} ->
|
{:ok, _packet} ->
|
||||||
# Packet stored successfully
|
# Packet stored successfully
|
||||||
:ok
|
:ok
|
||||||
|
|
@ -455,19 +456,19 @@ defmodule Aprs.Is do
|
||||||
end)
|
end)
|
||||||
|
|
||||||
# Broadcast to live clients
|
# Broadcast to live clients
|
||||||
AprsWeb.Endpoint.broadcast("aprs_messages", "packet", parsed_message)
|
AprsmeWeb.Endpoint.broadcast("aprs_messages", "packet", parsed_message)
|
||||||
|
|
||||||
{:error, :invalid_packet} ->
|
{:error, :invalid_packet} ->
|
||||||
Logger.debug("PARSE ERROR: invalid packet")
|
Logger.debug("PARSE ERROR: invalid packet")
|
||||||
|
|
||||||
Aprs.Packets.store_bad_packet(message, %{
|
Aprsme.Packets.store_bad_packet(message, %{
|
||||||
message: "Invalid packet format",
|
message: "Invalid packet format",
|
||||||
type: "ParseError"
|
type: "ParseError"
|
||||||
})
|
})
|
||||||
|
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
Logger.debug("PARSE ERROR: " <> error)
|
Logger.debug("PARSE ERROR: " <> error)
|
||||||
Aprs.Packets.store_bad_packet(message, %{message: error, type: "ParseError"})
|
Aprsme.Packets.store_bad_packet(message, %{message: error, type: "ParseError"})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Aprs.Is.IsSupervisor do
|
defmodule Aprsme.Is.IsSupervisor do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Supervisor
|
use Supervisor
|
||||||
|
|
||||||
|
|
@ -9,7 +9,7 @@ defmodule Aprs.Is.IsSupervisor do
|
||||||
@impl true
|
@impl true
|
||||||
def init(:ok) do
|
def init(:ok) do
|
||||||
children = [
|
children = [
|
||||||
{Aprs.Is, []}
|
{Aprsme.Is, []}
|
||||||
]
|
]
|
||||||
|
|
||||||
# Supervisor will restart children max 3 times in 5 seconds
|
# Supervisor will restart children max 3 times in 5 seconds
|
||||||
4
lib/aprsme/mailer.ex
Normal file
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
|
@moduledoc false
|
||||||
use Aprs.Schema
|
use Aprsme.Schema
|
||||||
|
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
alias Aprs.DataExtended
|
alias Aprs.Types.MicE
|
||||||
alias Parser.Types.MicE
|
alias Aprsme.DataExtended
|
||||||
|
|
||||||
schema "packets" do
|
schema "packets" do
|
||||||
field(:base_callsign, :string)
|
field(:base_callsign, :string)
|
||||||
|
|
@ -30,7 +30,7 @@ defmodule Aprs.Packet do
|
||||||
# Additional packet data
|
# Additional packet data
|
||||||
field(:comment, :string)
|
field(:comment, :string)
|
||||||
field(:timestamp, :string)
|
field(:timestamp, :string)
|
||||||
field(:aprs_messaging, :boolean, default: false)
|
field(:aprsme_messaging, :boolean, default: false)
|
||||||
|
|
||||||
# Weather data
|
# Weather data
|
||||||
field(:temperature, :float)
|
field(:temperature, :float)
|
||||||
|
|
@ -65,7 +65,7 @@ defmodule Aprs.Packet do
|
||||||
@type t :: %__MODULE__{}
|
@type t :: %__MODULE__{}
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@spec changeset(Aprs.Packet.t(), map()) :: Ecto.Changeset.t()
|
@spec changeset(Aprsme.Packet.t(), map()) :: Ecto.Changeset.t()
|
||||||
def changeset(packet, attrs) do
|
def changeset(packet, attrs) do
|
||||||
# Convert atom data_type to string
|
# Convert atom data_type to string
|
||||||
attrs = normalize_data_type(attrs)
|
attrs = normalize_data_type(attrs)
|
||||||
|
|
@ -90,7 +90,7 @@ defmodule Aprs.Packet do
|
||||||
:symbol_table_id,
|
:symbol_table_id,
|
||||||
:comment,
|
:comment,
|
||||||
:timestamp,
|
:timestamp,
|
||||||
:aprs_messaging,
|
:aprsme_messaging,
|
||||||
:temperature,
|
:temperature,
|
||||||
:humidity,
|
:humidity,
|
||||||
:wind_speed,
|
:wind_speed,
|
||||||
|
|
@ -277,8 +277,8 @@ defmodule Aprs.Packet do
|
||||||
|> maybe_put(:comment, data_extended[:comment] || data_extended["comment"])
|
|> maybe_put(:comment, data_extended[:comment] || data_extended["comment"])
|
||||||
|> maybe_put(:timestamp, data_extended[:timestamp] || data_extended["timestamp"])
|
|> maybe_put(:timestamp, data_extended[:timestamp] || data_extended["timestamp"])
|
||||||
|> maybe_put(
|
|> maybe_put(
|
||||||
:aprs_messaging,
|
:aprsme_messaging,
|
||||||
data_extended[:aprs_messaging?] || data_extended["aprs_messaging?"]
|
data_extended[:aprsme_messaging?] || data_extended["aprs_messaging?"]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -519,14 +519,14 @@ defmodule Aprs.Packet do
|
||||||
@doc """
|
@doc """
|
||||||
Get latitude from a packet's location geometry.
|
Get latitude from a packet's location geometry.
|
||||||
"""
|
"""
|
||||||
@spec lat(Aprs.Packet.t()) :: number() | nil
|
@spec lat(Aprsme.Packet.t()) :: number() | nil
|
||||||
def lat(%__MODULE__{location: %Geo.Point{coordinates: {_lon, lat}}}), do: lat
|
def lat(%__MODULE__{location: %Geo.Point{coordinates: {_lon, lat}}}), do: lat
|
||||||
def lat(_), do: nil
|
def lat(_), do: nil
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Get longitude from a packet's location geometry.
|
Get longitude from a packet's location geometry.
|
||||||
"""
|
"""
|
||||||
@spec lon(Aprs.Packet.t()) :: number() | nil
|
@spec lon(Aprsme.Packet.t()) :: number() | nil
|
||||||
def lon(%__MODULE__{location: %Geo.Point{coordinates: {lon, _lat}}}), do: lon
|
def lon(%__MODULE__{location: %Geo.Point{coordinates: {lon, _lat}}}), do: lon
|
||||||
def lon(_), do: nil
|
def lon(_), do: nil
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Aprs.PacketReplay do
|
defmodule Aprsme.PacketReplay do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Handles replaying of historical APRS packets alongside live packets.
|
Handles replaying of historical APRS packets alongside live packets.
|
||||||
|
|
||||||
|
|
@ -11,8 +11,8 @@ defmodule Aprs.PacketReplay do
|
||||||
|
|
||||||
use GenServer
|
use GenServer
|
||||||
|
|
||||||
alias Aprs.Packets
|
alias Aprsme.Packets
|
||||||
alias AprsWeb.Endpoint
|
alias AprsmeWeb.Endpoint
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
|
|
@ -393,7 +393,7 @@ defmodule Aprs.PacketReplay do
|
||||||
|
|
||||||
@spec via_tuple(String.t()) :: {:via, Registry, {atom(), String.t()}}
|
@spec via_tuple(String.t()) :: {:via, Registry, {atom(), String.t()}}
|
||||||
defp via_tuple(user_id) do
|
defp via_tuple(user_id) do
|
||||||
{:via, Registry, {Aprs.ReplayRegistry, "replay:#{user_id}"}}
|
{:via, Registry, {Aprsme.ReplayRegistry, "replay:#{user_id}"}}
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec sanitize_packet_for_transport(any()) :: map()
|
@spec sanitize_packet_for_transport(any()) :: map()
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
defmodule Aprs.PacketReplayBehaviour do
|
defmodule Aprsme.PacketReplayBehaviour do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Behavior definition for the PacketReplay module.
|
Behavior definition for the PacketReplay module.
|
||||||
This allows us to mock the PacketReplay module in tests.
|
This allows us to mock the PacketReplay module in tests.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@callback start_replay(pid(), list(Aprs.Packet.t()), keyword()) :: {:ok, reference()} | {:error, term()}
|
@callback start_replay(pid(), list(Aprsme.Packet.t()), keyword()) :: {:ok, reference()} | {:error, term()}
|
||||||
@callback stop_replay(reference()) :: :ok
|
@callback stop_replay(reference()) :: :ok
|
||||||
@callback pause_replay(reference()) :: :ok | {:error, :not_found}
|
@callback pause_replay(reference()) :: :ok | {:error, :not_found}
|
||||||
@callback resume_replay(reference()) :: :ok | {:error, :not_found}
|
@callback resume_replay(reference()) :: :ok | {:error, :not_found}
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
defmodule Aprs.Packets do
|
defmodule Aprsme.Packets do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
The Packets context.
|
The Packets context.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@behaviour Aprs.PacketsBehaviour
|
@behaviour Aprsme.PacketsBehaviour
|
||||||
|
|
||||||
import Ecto.Query, warn: false
|
import Ecto.Query, warn: false
|
||||||
|
|
||||||
alias Aprs.BadPacket
|
alias Aprs.Types.MicE
|
||||||
alias Aprs.Packet
|
alias Aprsme.BadPacket
|
||||||
alias Aprs.Repo
|
alias Aprsme.Packet
|
||||||
alias Parser.Types.MicE
|
alias Aprsme.Repo
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Stores a packet in the database.
|
Stores a packet in the database.
|
||||||
|
|
@ -34,8 +34,8 @@ defmodule Aprs.Packets do
|
||||||
end)
|
end)
|
||||||
|> normalize_ssid()
|
|> normalize_ssid()
|
||||||
|> then(fn attrs ->
|
|> then(fn attrs ->
|
||||||
device_identifier = Parser.DeviceParser.extract_device_identifier(packet_data)
|
device_identifier = Aprsme.DeviceParser.extract_device_identifier(packet_data)
|
||||||
matched_device = Aprs.DeviceIdentification.lookup_device_by_identifier(device_identifier)
|
matched_device = Aprsme.DeviceIdentification.lookup_device_by_identifier(device_identifier)
|
||||||
canonical_identifier = if matched_device, do: matched_device.identifier, else: device_identifier
|
canonical_identifier = if matched_device, do: matched_device.identifier, else: device_identifier
|
||||||
Map.put(attrs, :device_identifier, canonical_identifier)
|
Map.put(attrs, :device_identifier, canonical_identifier)
|
||||||
end)
|
end)
|
||||||
|
|
@ -460,7 +460,7 @@ defmodule Aprs.Packets do
|
||||||
"""
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
def clean_old_packets do
|
def clean_old_packets do
|
||||||
retention_days = Application.get_env(:aprs, :packet_retention_days, 365)
|
retention_days = Application.get_env(:aprsme, :packet_retention_days, 365)
|
||||||
cutoff_time = DateTime.add(DateTime.utc_now(), -retention_days * 86_400, :second)
|
cutoff_time = DateTime.add(DateTime.utc_now(), -retention_days * 86_400, :second)
|
||||||
|
|
||||||
{deleted_count, _} = Repo.delete_all(from(p in Packet, where: p.received_at < ^cutoff_time))
|
{deleted_count, _} = Repo.delete_all(from(p in Packet, where: p.received_at < ^cutoff_time))
|
||||||
|
|
@ -533,7 +533,7 @@ defmodule Aprs.Packets do
|
||||||
defp sanitize_packet_strings(list) when is_list(list), do: Enum.map(list, &sanitize_packet_strings/1)
|
defp sanitize_packet_strings(list) when is_list(list), do: Enum.map(list, &sanitize_packet_strings/1)
|
||||||
|
|
||||||
defp sanitize_packet_strings(binary) when is_binary(binary) do
|
defp sanitize_packet_strings(binary) when is_binary(binary) do
|
||||||
s = Aprs.EncodingUtils.sanitize_string(binary)
|
s = Aprsme.EncodingUtils.sanitize_string(binary)
|
||||||
if is_binary(s), do: s, else: ""
|
if is_binary(s), do: s, else: ""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Aprs.PacketsBehaviour do
|
defmodule Aprsme.PacketsBehaviour do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Behaviour for Packets module to allow mocking in tests
|
Behaviour for Packets module to allow mocking in tests
|
||||||
"""
|
"""
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Aprs.Passcode do
|
defmodule Aprsme.Passcode do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Module for generating APRS passcodes from callsigns.
|
Module for generating APRS passcodes from callsigns.
|
||||||
The passcode is a hash of the callsign used for authentication with APRS-IS servers.
|
The passcode is a hash of the callsign used for authentication with APRS-IS servers.
|
||||||
|
|
@ -14,7 +14,7 @@ defmodule Aprs.Passcode do
|
||||||
- The generated passcode as an integer
|
- The generated passcode as an integer
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
iex> Aprs.Passcode.generate("W5ISP")
|
iex> Aprsme.Passcode.generate("W5ISP")
|
||||||
15748
|
15748
|
||||||
"""
|
"""
|
||||||
@spec generate(String.t()) :: non_neg_integer()
|
@spec generate(String.t()) :: non_neg_integer()
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Aprs.PostgresNotifier do
|
defmodule Aprsme.PostgresNotifier do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Listens to PostgreSQL NOTIFY events on the "aprs_events" and "aprs_packets" channels and broadcasts
|
Listens to PostgreSQL NOTIFY events on the "aprs_events" and "aprs_packets" channels and broadcasts
|
||||||
them via Phoenix.PubSub for reactive, event-driven updates.
|
them via Phoenix.PubSub for reactive, event-driven updates.
|
||||||
|
|
@ -6,9 +6,9 @@ defmodule Aprs.PostgresNotifier do
|
||||||
use GenServer
|
use GenServer
|
||||||
|
|
||||||
@event_channel "aprs_events"
|
@event_channel "aprs_events"
|
||||||
@event_topic "postgres:aprs_events"
|
@event_topic "postgres:aprsme_events"
|
||||||
@packet_channel "aprs_packets"
|
@packet_channel "aprs_packets"
|
||||||
@packet_topic "postgres:aprs_packets"
|
@packet_topic "postgres:aprsme_packets"
|
||||||
|
|
||||||
def start_link(_opts) do
|
def start_link(_opts) do
|
||||||
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
|
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
|
||||||
|
|
@ -16,7 +16,7 @@ defmodule Aprs.PostgresNotifier do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init(_) do
|
def init(_) do
|
||||||
{:ok, conn} = Postgrex.Notifications.start_link(Aprs.Repo.config())
|
{:ok, conn} = Postgrex.Notifications.start_link(Aprsme.Repo.config())
|
||||||
{:ok, _ref1} = Postgrex.Notifications.listen(conn, @event_channel)
|
{:ok, _ref1} = Postgrex.Notifications.listen(conn, @event_channel)
|
||||||
{:ok, _ref2} = Postgrex.Notifications.listen(conn, @packet_channel)
|
{:ok, _ref2} = Postgrex.Notifications.listen(conn, @packet_channel)
|
||||||
{:ok, %{conn: conn}}
|
{:ok, %{conn: conn}}
|
||||||
|
|
@ -24,14 +24,14 @@ defmodule Aprs.PostgresNotifier do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:notification, _conn, _pid, @event_channel, payload}, state) do
|
def handle_info({:notification, _conn, _pid, @event_channel, payload}, state) do
|
||||||
Phoenix.PubSub.broadcast(Aprs.PubSub, @event_topic, {:postgres_notify, payload})
|
Phoenix.PubSub.broadcast(Aprsme.PubSub, @event_topic, {:postgres_notify, payload})
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info({:notification, _conn, _pid, @packet_channel, payload}, state) do
|
def handle_info({:notification, _conn, _pid, @packet_channel, payload}, state) do
|
||||||
case Jason.decode(payload) do
|
case Jason.decode(payload) do
|
||||||
{:ok, packet} ->
|
{:ok, packet} ->
|
||||||
Phoenix.PubSub.broadcast(Aprs.PubSub, @packet_topic, {:postgres_packet, packet})
|
Phoenix.PubSub.broadcast(Aprsme.PubSub, @packet_topic, {:postgres_packet, packet})
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
:noop
|
:noop
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
Postgrex.Types.define(
|
Postgrex.Types.define(
|
||||||
Aprs.PostgresTypes,
|
Aprsme.PostgresTypes,
|
||||||
[Geo.PostGIS.Extension] ++ Ecto.Adapters.Postgres.extensions(),
|
[Geo.PostGIS.Extension] ++ Ecto.Adapters.Postgres.extensions(),
|
||||||
json: Jason
|
json: Jason
|
||||||
)
|
)
|
||||||
4
lib/aprsme/presence.ex
Normal file
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 """
|
@moduledoc """
|
||||||
Used for executing DB release tasks when run in production without Mix
|
Used for executing DB release tasks when run in production without Mix
|
||||||
installed.
|
installed.
|
||||||
"""
|
"""
|
||||||
@app :aprs
|
@app :aprsme
|
||||||
|
|
||||||
def migrate do
|
def migrate do
|
||||||
require Logger
|
require Logger
|
||||||
|
|
@ -12,7 +12,7 @@ defmodule Aprs.Release do
|
||||||
Logger.info("Deployment timestamp: #{deployed_at}")
|
Logger.info("Deployment timestamp: #{deployed_at}")
|
||||||
|
|
||||||
# Run migrations
|
# Run migrations
|
||||||
{:ok, _, _} = Ecto.Migrator.with_repo(Aprs.Repo, &Ecto.Migrator.run(&1, :up, all: true))
|
{:ok, _, _} = Ecto.Migrator.with_repo(Aprsme.Repo, &Ecto.Migrator.run(&1, :up, all: true))
|
||||||
end
|
end
|
||||||
|
|
||||||
def rollback(repo, version) do
|
def rollback(repo, version) do
|
||||||
|
|
@ -33,7 +33,7 @@ defmodule Aprs.Release do
|
||||||
deployed_at = read_deployment_timestamp()
|
deployed_at = read_deployment_timestamp()
|
||||||
|
|
||||||
# Add to application config
|
# Add to application config
|
||||||
Application.put_env(:aprs, :deployed_at, deployed_at)
|
Application.put_env(:aprsme, :deployed_at, deployed_at)
|
||||||
|
|
||||||
deployed_at
|
deployed_at
|
||||||
end
|
end
|
||||||
|
|
@ -42,7 +42,7 @@ defmodule Aprs.Release do
|
||||||
Get the deployment timestamp.
|
Get the deployment timestamp.
|
||||||
"""
|
"""
|
||||||
def deployed_at do
|
def deployed_at do
|
||||||
Application.get_env(:aprs, :deployed_at) || DateTime.utc_now()
|
Application.get_env(:aprsme, :deployed_at) || DateTime.utc_now()
|
||||||
end
|
end
|
||||||
|
|
||||||
defp read_deployment_timestamp do
|
defp read_deployment_timestamp do
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule Aprs.Repo do
|
defmodule Aprsme.Repo do
|
||||||
use Ecto.Repo,
|
use Ecto.Repo,
|
||||||
otp_app: :aprs,
|
otp_app: :aprsme,
|
||||||
adapter: Ecto.Adapters.Postgres
|
adapter: Ecto.Adapters.Postgres
|
||||||
end
|
end
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Aprs.Schema do
|
defmodule Aprsme.Schema do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
defmacro __using__(_) do
|
defmacro __using__(_) do
|
||||||
quote do
|
quote do
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Aprs.Workers.PacketCleanupWorker do
|
defmodule Aprsme.Workers.PacketCleanupWorker do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Oban worker for cleaning up old APRS packet data.
|
Oban worker for cleaning up old APRS packet data.
|
||||||
|
|
||||||
|
|
@ -18,8 +18,8 @@ defmodule Aprs.Workers.PacketCleanupWorker do
|
||||||
|
|
||||||
# Import modules needed for database operations
|
# Import modules needed for database operations
|
||||||
|
|
||||||
alias Aprs.Packet
|
alias Aprsme.Packet
|
||||||
alias Aprs.Repo
|
alias Aprsme.Repo
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
|
|
@ -52,7 +52,7 @@ defmodule Aprs.Workers.PacketCleanupWorker do
|
||||||
deleted_count = cleanup_old_packets()
|
deleted_count = cleanup_old_packets()
|
||||||
|
|
||||||
# Log results
|
# Log results
|
||||||
retention_days = Application.get_env(:aprs, :packet_retention_days, 365)
|
retention_days = Application.get_env(:aprsme, :packet_retention_days, 365)
|
||||||
|
|
||||||
Logger.info("APRS packet cleanup complete: removed #{deleted_count} packets older than #{retention_days} days")
|
Logger.info("APRS packet cleanup complete: removed #{deleted_count} packets older than #{retention_days} days")
|
||||||
|
|
||||||
|
|
@ -92,6 +92,6 @@ defmodule Aprs.Workers.PacketCleanupWorker do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp packets_module do
|
defp packets_module do
|
||||||
Application.get_env(:aprs, :packets_module, Aprs.Packets)
|
Application.get_env(:aprsme, :packets_module, Aprsme.Packets)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule AprsWeb.CoreComponents do
|
defmodule AprsmeWeb.CoreComponents do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Provides core UI components.
|
Provides core UI components.
|
||||||
|
|
||||||
|
|
@ -10,7 +10,7 @@ defmodule AprsWeb.CoreComponents do
|
||||||
[heroicons_elixir](https://github.com/mveytsman/heroicons_elixir) project.
|
[heroicons_elixir](https://github.com/mveytsman/heroicons_elixir) project.
|
||||||
"""
|
"""
|
||||||
use Phoenix.Component
|
use Phoenix.Component
|
||||||
use Gettext, backend: AprsWeb.Gettext
|
use Gettext, backend: AprsmeWeb.Gettext
|
||||||
|
|
||||||
alias Phoenix.HTML.Form
|
alias Phoenix.HTML.Form
|
||||||
alias Phoenix.LiveView.JS
|
alias Phoenix.LiveView.JS
|
||||||
|
|
@ -620,9 +620,9 @@ defmodule AprsWeb.CoreComponents do
|
||||||
# should be written to the errors.po file. The :count option is
|
# should be written to the errors.po file. The :count option is
|
||||||
# set by Ecto and indicates we should also apply plural rules.
|
# set by Ecto and indicates we should also apply plural rules.
|
||||||
if count = opts[:count] do
|
if count = opts[:count] do
|
||||||
Gettext.dngettext(AprsWeb.Gettext, "errors", msg, msg, count, opts)
|
Gettext.dngettext(AprsmeWeb.Gettext, "errors", msg, msg, count, opts)
|
||||||
else
|
else
|
||||||
Gettext.dgettext(AprsWeb.Gettext, "errors", msg, opts)
|
Gettext.dgettext(AprsmeWeb.Gettext, "errors", msg, opts)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule AprsWeb.Layouts do
|
defmodule AprsmeWeb.Layouts do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AprsWeb, :html
|
use AprsmeWeb, :html
|
||||||
|
|
||||||
embed_templates "layouts/*"
|
embed_templates "layouts/*"
|
||||||
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
defmodule AprsWeb.Api.V1.CallsignController do
|
defmodule AprsmeWeb.Api.V1.CallsignController do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
API v1 controller for callsign-related endpoints.
|
API v1 controller for callsign-related endpoints.
|
||||||
"""
|
"""
|
||||||
use AprsWeb, :controller
|
use AprsmeWeb, :controller
|
||||||
|
|
||||||
alias Aprs.Packets
|
alias Aprsme.Packets
|
||||||
alias AprsWeb.Api.V1.CallsignJSON
|
alias AprsmeWeb.Api.V1.CallsignJSON
|
||||||
|
|
||||||
action_fallback AprsWeb.Api.V1.FallbackController
|
action_fallback AprsmeWeb.Api.V1.FallbackController
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Get the most recent packet for a given callsign.
|
Get the most recent packet for a given callsign.
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
defmodule AprsWeb.Api.V1.FallbackController do
|
defmodule AprsmeWeb.Api.V1.FallbackController do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Translates controller action results into valid `Plug.Conn` responses.
|
Translates controller action results into valid `Plug.Conn` responses.
|
||||||
|
|
||||||
See `Phoenix.Controller.action_fallback/1` for more details.
|
See `Phoenix.Controller.action_fallback/1` for more details.
|
||||||
"""
|
"""
|
||||||
use AprsWeb, :controller
|
use AprsmeWeb, :controller
|
||||||
|
|
||||||
alias AprsWeb.Api.V1.ErrorJSON
|
alias AprsmeWeb.Api.V1.ErrorJSON
|
||||||
|
|
||||||
# This clause handles errors returned by Ecto's insert/update/delete.
|
# This clause handles errors returned by Ecto's insert/update/delete.
|
||||||
def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
|
def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
|
||||||
conn
|
conn
|
||||||
|> put_status(:unprocessable_entity)
|
|> put_status(:unprocessable_entity)
|
||||||
|> put_view(json: AprsWeb.Api.V1.ChangesetJSON)
|
|> put_view(json: AprsmeWeb.Api.V1.ChangesetJSON)
|
||||||
|> render(:error, changeset: changeset)
|
|> render(:error, changeset: changeset)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
defmodule AprsWeb.Api.V1.CallsignJSON do
|
defmodule AprsmeWeb.Api.V1.CallsignJSON do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Renders callsign and packet data for API v1.
|
Renders callsign and packet data for API v1.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
alias Aprs.Packet
|
alias Aprsme.Packet
|
||||||
|
|
||||||
def render("show.json", %{packet: packet}) do
|
def render("show.json", %{packet: packet}) do
|
||||||
%{data: packet_json(packet)}
|
%{data: packet_json(packet)}
|
||||||
|
|
@ -38,7 +38,7 @@ defmodule AprsWeb.Api.V1.CallsignJSON do
|
||||||
symbol: symbol_json(packet),
|
symbol: symbol_json(packet),
|
||||||
comment: packet.comment,
|
comment: packet.comment,
|
||||||
timestamp: packet.timestamp,
|
timestamp: packet.timestamp,
|
||||||
aprs_messaging: packet.aprs_messaging,
|
aprs_messaging: packet.aprsme_messaging,
|
||||||
weather: weather_json(packet),
|
weather: weather_json(packet),
|
||||||
equipment: equipment_json(packet),
|
equipment: equipment_json(packet),
|
||||||
message: message_json(packet),
|
message: message_json(packet),
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule AprsWeb.Api.V1.ChangesetJSON do
|
defmodule AprsmeWeb.Api.V1.ChangesetJSON do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Renders changeset errors for API v1.
|
Renders changeset errors for API v1.
|
||||||
"""
|
"""
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule AprsWeb.Api.V1.ErrorJSON do
|
defmodule AprsmeWeb.Api.V1.ErrorJSON do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Renders error responses for API v1.
|
Renders error responses for API v1.
|
||||||
"""
|
"""
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule AprsWeb.ErrorHTML do
|
defmodule AprsmeWeb.ErrorHTML do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AprsWeb, :html
|
use AprsmeWeb, :html
|
||||||
|
|
||||||
# If you want to customize your error pages,
|
# If you want to customize your error pages,
|
||||||
# uncomment the embed_templates/1 call below
|
# uncomment the embed_templates/1 call below
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule AprsWeb.ErrorJSON do
|
defmodule AprsmeWeb.ErrorJSON do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
# If you want to customize a particular status code,
|
# If you want to customize a particular status code,
|
||||||
# you may add your own clauses, such as:
|
# you may add your own clauses, such as:
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule AprsWeb.PageController do
|
defmodule AprsmeWeb.PageController do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AprsWeb, :controller
|
use AprsmeWeb, :controller
|
||||||
|
|
||||||
def home(conn, _params) do
|
def home(conn, _params) do
|
||||||
render(conn, :home)
|
render(conn, :home)
|
||||||
|
|
@ -14,14 +14,14 @@ defmodule AprsWeb.PageController do
|
||||||
# Check database connection
|
# Check database connection
|
||||||
db_healthy =
|
db_healthy =
|
||||||
try do
|
try do
|
||||||
Aprs.Repo.query!("SELECT 1")
|
Aprsme.Repo.query!("SELECT 1")
|
||||||
true
|
true
|
||||||
rescue
|
rescue
|
||||||
_ -> false
|
_ -> false
|
||||||
end
|
end
|
||||||
|
|
||||||
# Get application version
|
# Get application version
|
||||||
version = :aprs |> Application.spec(:vsn) |> List.to_string()
|
version = :aprsme |> Application.spec(:vsn) |> List.to_string()
|
||||||
|
|
||||||
if db_healthy do
|
if db_healthy do
|
||||||
json(conn, %{
|
json(conn, %{
|
||||||
|
|
@ -45,7 +45,7 @@ defmodule AprsWeb.PageController do
|
||||||
def ready(conn, _params) do
|
def ready(conn, _params) do
|
||||||
# Simple readiness check without database dependency
|
# Simple readiness check without database dependency
|
||||||
# This is faster for startup health checks
|
# This is faster for startup health checks
|
||||||
version = :aprs |> Application.spec(:vsn) |> List.to_string()
|
version = :aprsme |> Application.spec(:vsn) |> List.to_string()
|
||||||
|
|
||||||
json(conn, %{
|
json(conn, %{
|
||||||
status: "ready",
|
status: "ready",
|
||||||
|
|
@ -56,10 +56,10 @@ defmodule AprsWeb.PageController do
|
||||||
|
|
||||||
def status_json(conn, _params) do
|
def status_json(conn, _params) do
|
||||||
# Get APRS-IS connection status
|
# Get APRS-IS connection status
|
||||||
aprs_status = Aprs.Is.get_status()
|
aprs_status = Aprsme.Is.get_status()
|
||||||
|
|
||||||
# Get application version
|
# Get application version
|
||||||
version = :aprs |> Application.spec(:vsn) |> List.to_string()
|
version = :aprsme |> Application.spec(:vsn) |> List.to_string()
|
||||||
|
|
||||||
# Calculate uptime in a human-readable format
|
# Calculate uptime in a human-readable format
|
||||||
uptime_display = format_uptime(aprs_status.uptime_seconds)
|
uptime_display = format_uptime(aprs_status.uptime_seconds)
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule AprsWeb.PageHTML do
|
defmodule AprsmeWeb.PageHTML do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AprsWeb, :html
|
use AprsmeWeb, :html
|
||||||
|
|
||||||
embed_templates "page_html/*"
|
embed_templates "page_html/*"
|
||||||
end
|
end
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
defmodule AprsWeb.UserSessionController do
|
defmodule AprsmeWeb.UserSessionController do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AprsWeb, :controller
|
use AprsmeWeb, :controller
|
||||||
|
|
||||||
alias Aprs.Accounts
|
alias Aprsme.Accounts
|
||||||
alias AprsWeb.UserAuth
|
alias AprsmeWeb.UserAuth
|
||||||
|
|
||||||
def create(conn, %{"_action" => "registered"} = params) do
|
def create(conn, %{"_action" => "registered"} = params) do
|
||||||
create(conn, params, "Account created successfully!")
|
create(conn, params, "Account created successfully!")
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
defmodule AprsWeb.Endpoint do
|
defmodule AprsmeWeb.Endpoint do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
use Phoenix.Endpoint, otp_app: :aprs
|
use Phoenix.Endpoint, otp_app: :aprsme
|
||||||
|
|
||||||
# The session will be stored in the cookie and signed,
|
# The session will be stored in the cookie and signed,
|
||||||
# this means its contents can be read but not tampered with.
|
# this means its contents can be read but not tampered with.
|
||||||
|
|
@ -21,7 +21,7 @@ defmodule AprsWeb.Endpoint do
|
||||||
# when deploying your static files in production.
|
# when deploying your static files in production.
|
||||||
plug Plug.Static,
|
plug Plug.Static,
|
||||||
at: "/",
|
at: "/",
|
||||||
from: :aprs,
|
from: :aprsme,
|
||||||
gzip: false,
|
gzip: false,
|
||||||
only: ~w(assets fonts images aprs-symbols favicon.ico robots.txt)
|
only: ~w(assets fonts images aprs-symbols favicon.ico robots.txt)
|
||||||
|
|
||||||
|
|
@ -31,7 +31,7 @@ defmodule AprsWeb.Endpoint do
|
||||||
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
|
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
|
||||||
plug Phoenix.LiveReloader
|
plug Phoenix.LiveReloader
|
||||||
plug Phoenix.CodeReloader
|
plug Phoenix.CodeReloader
|
||||||
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :aprs
|
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :aprsme
|
||||||
end
|
end
|
||||||
|
|
||||||
plug Phoenix.LiveDashboard.RequestLogger,
|
plug Phoenix.LiveDashboard.RequestLogger,
|
||||||
|
|
@ -49,5 +49,5 @@ defmodule AprsWeb.Endpoint do
|
||||||
plug Plug.MethodOverride
|
plug Plug.MethodOverride
|
||||||
plug Plug.Head
|
plug Plug.Head
|
||||||
plug Plug.Session, @session_options
|
plug Plug.Session, @session_options
|
||||||
plug AprsWeb.Router
|
plug AprsmeWeb.Router
|
||||||
end
|
end
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
defmodule AprsWeb.Gettext do
|
defmodule AprsmeWeb.Gettext do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A module providing Internationalization with a gettext-based API.
|
A module providing Internationalization with a gettext-based API.
|
||||||
|
|
||||||
By using [Gettext](https://hexdocs.pm/gettext),
|
By using [Gettext](https://hexdocs.pm/gettext),
|
||||||
your module gains a set of macros for translations, for example:
|
your module gains a set of macros for translations, for example:
|
||||||
|
|
||||||
import AprsWeb.Gettext
|
import AprsmeWeb.Gettext
|
||||||
|
|
||||||
# Simple translation
|
# Simple translation
|
||||||
gettext("Here is the string to translate")
|
gettext("Here is the string to translate")
|
||||||
|
|
@ -20,5 +20,5 @@ defmodule AprsWeb.Gettext do
|
||||||
|
|
||||||
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
|
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
|
||||||
"""
|
"""
|
||||||
use Gettext.Backend, otp_app: :aprs
|
use Gettext.Backend, otp_app: :aprsme
|
||||||
end
|
end
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule AprsWeb.AboutLive do
|
defmodule AprsmeWeb.AboutLive do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AprsWeb, :live_view
|
use AprsmeWeb, :live_view
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
defmodule AprsWeb.ApiDocsLive do
|
defmodule AprsmeWeb.ApiDocsLive do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
LiveView for API documentation page.
|
LiveView for API documentation page.
|
||||||
"""
|
"""
|
||||||
use AprsWeb, :live_view
|
use AprsmeWeb, :live_view
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
|
|
@ -60,7 +60,7 @@ defmodule AprsWeb.ApiDocsLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp make_http_request(callsign) do
|
defp make_http_request(callsign) do
|
||||||
alias Aprs.Packets
|
alias Aprsme.Packets
|
||||||
|
|
||||||
# Validate callsign format (same as controller)
|
# Validate callsign format (same as controller)
|
||||||
callsign_regex = ~r/^[A-Z0-9]{1,3}[0-9][A-Z]{1,4}(-[0-9]{1,2})?$/
|
callsign_regex = ~r/^[A-Z0-9]{1,3}[0-9][A-Z]{1,4}(-[0-9]{1,2})?$/
|
||||||
|
|
@ -112,7 +112,7 @@ defmodule AprsWeb.ApiDocsLive do
|
||||||
"symbol" => format_symbol(packet),
|
"symbol" => format_symbol(packet),
|
||||||
"comment" => packet.comment,
|
"comment" => packet.comment,
|
||||||
"timestamp" => packet.timestamp,
|
"timestamp" => packet.timestamp,
|
||||||
"aprs_messaging" => packet.aprs_messaging,
|
"aprsme_messaging" => packet.aprsme_messaging,
|
||||||
"weather" => format_weather(packet),
|
"weather" => format_weather(packet),
|
||||||
"equipment" => format_equipment(packet),
|
"equipment" => format_equipment(packet),
|
||||||
"message" => format_message(packet),
|
"message" => format_message(packet),
|
||||||
|
|
@ -373,7 +373,7 @@ defmodule AprsWeb.ApiDocsLive do
|
||||||
},
|
},
|
||||||
"comment": "Mobile Station",
|
"comment": "Mobile Station",
|
||||||
"timestamp": null,
|
"timestamp": null,
|
||||||
"aprs_messaging": false,
|
"aprsme_messaging": false,
|
||||||
"weather": null,
|
"weather": null,
|
||||||
"equipment": {
|
"equipment": {
|
||||||
"manufacturer": "Kenwood",
|
"manufacturer": "Kenwood",
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
defmodule AprsWeb.BadPacketsLive.Index do
|
defmodule AprsmeWeb.BadPacketsLive.Index do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AprsWeb, :live_view
|
use AprsmeWeb, :live_view
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
alias Aprs.BadPacket
|
alias Aprsme.BadPacket
|
||||||
alias Aprs.Repo
|
alias Aprsme.Repo
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
if connected?(socket) do
|
if connected?(socket) do
|
||||||
# Subscribe to Postgres notifications for bad packets
|
# Subscribe to Postgres notifications for bad packets
|
||||||
Phoenix.PubSub.subscribe(Aprs.PubSub, "postgres:aprs_events")
|
Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_events")
|
||||||
# Load initial bad packets
|
# Load initial bad packets
|
||||||
bad_packets = fetch_bad_packets()
|
bad_packets = fetch_bad_packets()
|
||||||
# Extra safeguard to ensure we never show more than 100
|
# Extra safeguard to ensure we never show more than 100
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
defmodule AprsWeb.InfoLive.Show do
|
defmodule AprsmeWeb.InfoLive.Show do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AprsWeb, :live_view
|
use AprsmeWeb, :live_view
|
||||||
|
|
||||||
alias Aprs.Packets
|
alias Aprsme.Packets
|
||||||
alias AprsWeb.MapLive.PacketUtils
|
alias AprsmeWeb.MapLive.PacketUtils
|
||||||
|
|
||||||
@neighbor_radius_km 10
|
@neighbor_radius_km 10
|
||||||
@neighbor_limit 10
|
@neighbor_limit 10
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<% {symbol_table_id, symbol_code} = AprsWeb.MapLive.PacketUtils.get_symbol_info(@packet || %{}) %>
|
<% {symbol_table_id, symbol_code} = AprsmeWeb.MapLive.PacketUtils.get_symbol_info(@packet || %{}) %>
|
||||||
<% symbol_table = if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %>
|
<% symbol_table = if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %>
|
||||||
<% symbol_code = symbol_code || ">" %>
|
<% symbol_code = symbol_code || ">" %>
|
||||||
<% symbol_table_num =
|
<% symbol_table_num =
|
||||||
|
|
@ -13,14 +13,14 @@
|
||||||
|> String.to_charlist()
|
|> String.to_charlist()
|
||||||
|> List.first()
|
|> List.first()
|
||||||
|> (fn c -> if is_integer(c), do: c, else: 63 end).() %>
|
|> (fn c -> if is_integer(c), do: c, else: 63 end).() %>
|
||||||
<% symbol_img = "/aprs-symbols/aprs-symbols-24-#{symbol_table_num}@2x.png" %>
|
<% _symbol_img = "/aprs-symbols/aprs-symbols-24-#{symbol_table_num}@2x.png" %>
|
||||||
<% symbol_index = symbol_code_ord - 33 %>
|
<% _symbol_index = symbol_code_ord - 33 %>
|
||||||
|
|
||||||
<div class="max-w-3xl mx-auto mt-10 p-8 bg-white rounded-xl shadow-lg">
|
<div class="max-w-3xl mx-auto mt-10 p-8 bg-white rounded-xl shadow-lg">
|
||||||
<h1 class="text-3xl font-bold mb-2 flex items-center gap-2">
|
<h1 class="text-3xl font-bold mb-2 flex items-center gap-2">
|
||||||
APRS station <span class="text-blue-700">{@callsign}</span>
|
APRS station <span class="text-blue-700">{@callsign}</span>
|
||||||
<%= if @packet do %>
|
<%= if @packet do %>
|
||||||
<% {symbol_table_id, symbol_code} = AprsWeb.MapLive.PacketUtils.get_symbol_info(@packet) %>
|
<% {symbol_table_id, symbol_code} = AprsmeWeb.MapLive.PacketUtils.get_symbol_info(@packet) %>
|
||||||
<% symbol_table = if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %>
|
<% symbol_table = if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %>
|
||||||
<% symbol_code = symbol_code || ">" %>
|
<% symbol_code = symbol_code || ">" %>
|
||||||
<% symbol_table_num =
|
<% symbol_table_num =
|
||||||
|
|
@ -57,7 +57,7 @@
|
||||||
<span class="font-semibold">Location:</span> {@packet.lat}, {@packet.lon}
|
<span class="font-semibold">Location:</span> {@packet.lat}, {@packet.lon}
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<span class="font-semibold">Last position:</span> {AprsWeb.MapLive.PacketUtils.get_timestamp(
|
<span class="font-semibold">Last position:</span> {AprsmeWeb.MapLive.PacketUtils.get_timestamp(
|
||||||
@packet
|
@packet
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -92,7 +92,7 @@
|
||||||
</.link>
|
</.link>
|
||||||
<%= if neighbor.packet do %>
|
<%= if neighbor.packet do %>
|
||||||
<% {symbol_table_id, symbol_code} =
|
<% {symbol_table_id, symbol_code} =
|
||||||
AprsWeb.MapLive.PacketUtils.get_symbol_info(neighbor.packet) %>
|
AprsmeWeb.MapLive.PacketUtils.get_symbol_info(neighbor.packet) %>
|
||||||
<% symbol_table =
|
<% symbol_table =
|
||||||
if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %>
|
if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %>
|
||||||
<% symbol_code = symbol_code || ">" %>
|
<% symbol_code = symbol_code || ">" %>
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
defmodule AprsWeb.MapLive.CallsignView do
|
defmodule AprsmeWeb.MapLive.CallsignView do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AprsWeb, :live_view
|
use AprsmeWeb, :live_view
|
||||||
|
|
||||||
alias Aprs.Packets
|
alias Aprsme.Packets
|
||||||
alias AprsWeb.Endpoint
|
alias AprsmeWeb.Endpoint
|
||||||
alias AprsWeb.MapLive.MapHelpers
|
alias AprsmeWeb.MapLive.MapHelpers
|
||||||
alias AprsWeb.MapLive.PacketUtils
|
alias AprsmeWeb.MapLive.PacketUtils
|
||||||
|
|
||||||
@default_center %{lat: 39.0, lng: -98.0}
|
@default_center %{lat: 39.0, lng: -98.0}
|
||||||
@default_zoom 4
|
@default_zoom 4
|
||||||
|
|
@ -1,34 +1,34 @@
|
||||||
defmodule AprsWeb.MapLive.Index do
|
defmodule AprsmeWeb.MapLive.Index do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
LiveView for displaying real-time APRS packets on a map
|
LiveView for displaying real-time APRS packets on a map
|
||||||
"""
|
"""
|
||||||
use AprsWeb, :live_view
|
use AprsmeWeb, :live_view
|
||||||
|
|
||||||
import AprsWeb.TimeHelpers, only: [time_ago_in_words: 1]
|
import AprsmeWeb.TimeHelpers, only: [time_ago_in_words: 1]
|
||||||
|
|
||||||
alias AprsWeb.Endpoint
|
alias AprsmeWeb.Endpoint
|
||||||
alias AprsWeb.MapLive.MapHelpers
|
alias AprsmeWeb.MapLive.MapHelpers
|
||||||
alias AprsWeb.MapLive.PacketUtils
|
alias AprsmeWeb.MapLive.PacketUtils
|
||||||
alias Phoenix.LiveView.Socket
|
alias Phoenix.LiveView.Socket
|
||||||
|
|
||||||
@default_center %{lat: 39.8283, lng: -98.5795}
|
@default_center %{lat: 39.8283, lng: -98.5795}
|
||||||
@default_zoom 5
|
@default_zoom 5
|
||||||
@finch_name Aprs.Finch
|
@finch_name Aprsme.Finch
|
||||||
@initialize_replay_delay Application.compile_env(:aprs, :initialize_replay_delay, 500)
|
@initialize_replay_delay Application.compile_env(:aprsme, :initialize_replay_delay, 500)
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
if connected?(socket) do
|
if connected?(socket) do
|
||||||
# Subscribe to packet updates
|
# Subscribe to packet updates
|
||||||
Phoenix.PubSub.subscribe(Aprs.PubSub, "packets")
|
Phoenix.PubSub.subscribe(Aprsme.PubSub, "packets")
|
||||||
Phoenix.PubSub.subscribe(Aprs.PubSub, "bad_packets")
|
Phoenix.PubSub.subscribe(Aprsme.PubSub, "bad_packets")
|
||||||
|
|
||||||
# Schedule periodic cleanup of old packets
|
# Schedule periodic cleanup of old packets
|
||||||
Process.send_after(self(), :cleanup_old_packets, 60_000)
|
Process.send_after(self(), :cleanup_old_packets, 60_000)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Get deployment timestamp from config (set during application startup)
|
# Get deployment timestamp from config (set during application startup)
|
||||||
deployed_at = Aprs.Release.deployed_at()
|
deployed_at = Aprsme.Release.deployed_at()
|
||||||
|
|
||||||
one_hour_ago = DateTime.add(DateTime.utc_now(), -3600, :second)
|
one_hour_ago = DateTime.add(DateTime.utc_now(), -3600, :second)
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
|
|
||||||
if connected?(socket) do
|
if connected?(socket) do
|
||||||
Endpoint.subscribe("aprs_messages")
|
Endpoint.subscribe("aprs_messages")
|
||||||
Phoenix.PubSub.subscribe(Aprs.PubSub, "postgres:aprs_packets")
|
Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_packets")
|
||||||
maybe_start_geolocation(socket)
|
maybe_start_geolocation(socket)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -99,7 +99,7 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
defp maybe_start_geolocation(socket) do
|
defp maybe_start_geolocation(socket) do
|
||||||
if geolocation_enabled?() do
|
if geolocation_enabled?() do
|
||||||
ip_for_geolocation =
|
ip_for_geolocation =
|
||||||
if Application.get_env(:aprs, AprsWeb.Endpoint)[:code_reloader] do
|
if Application.get_env(:aprsme, AprsmeWeb.Endpoint)[:code_reloader] do
|
||||||
# For testing geolocation in dev environment, use a public IP address.
|
# For testing geolocation in dev environment, use a public IP address.
|
||||||
# This will be geolocated to Mountain View, CA.
|
# This will be geolocated to Mountain View, CA.
|
||||||
"8.8.8.8"
|
"8.8.8.8"
|
||||||
|
|
@ -116,7 +116,7 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp geolocation_enabled? do
|
defp geolocation_enabled? do
|
||||||
Application.get_env(:aprs, :disable_aprs_connection, false) != true
|
Application.get_env(:aprsme, :disable_aprs_connection, false) != true
|
||||||
end
|
end
|
||||||
|
|
||||||
defp extract_ip(socket) do
|
defp extract_ip(socket) do
|
||||||
|
|
@ -1175,7 +1175,7 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
}
|
}
|
||||||
|
|
||||||
# Call the database through the Packets context
|
# Call the database through the Packets context
|
||||||
packets_module = Application.get_env(:aprs, :packets_module, Aprs.Packets)
|
packets_module = Application.get_env(:aprsme, :packets_module, Aprsme.Packets)
|
||||||
packets = packets_module.get_packets_for_replay(packets_params)
|
packets = packets_module.get_packets_for_replay(packets_params)
|
||||||
|
|
||||||
# Sort packets by received_at timestamp to ensure chronological replay
|
# Sort packets by received_at timestamp to ensure chronological replay
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
defmodule AprsWeb.MapLive.MapHelpers do
|
defmodule AprsmeWeb.MapLive.MapHelpers do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
alias Parser.Types.MicE
|
alias Aprs.Types.MicE
|
||||||
|
|
||||||
@spec get_coordinates(map() | struct()) :: {number() | nil, number() | nil, map() | nil}
|
@spec get_coordinates(map() | struct()) :: {number() | nil, number() | nil, map() | nil}
|
||||||
def get_coordinates(%{data_extended: %MicE{} = mic_e}) do
|
def get_coordinates(%{data_extended: %MicE{} = mic_e}) do
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
defmodule AprsWeb.MapLive.PacketUtils do
|
defmodule AprsmeWeb.MapLive.PacketUtils do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Shared utilities for extracting and processing packet data in map views.
|
Shared utilities for extracting and processing packet data in map views.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
alias AprsWeb.MapLive.MapHelpers
|
alias AprsmeWeb.MapLive.MapHelpers
|
||||||
alias AprsWeb.TimeHelpers
|
alias AprsmeWeb.TimeHelpers
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Safely extracts a value from a packet or data_extended map with fallback support.
|
Safely extracts a value from a packet or data_extended map with fallback support.
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
defmodule AprsWeb.PacketsLive.CallsignView do
|
defmodule AprsmeWeb.PacketsLive.CallsignView do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AprsWeb, :live_view
|
use AprsmeWeb, :live_view
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
alias Aprs.EncodingUtils
|
alias Aprsme.DeviceParser
|
||||||
alias Aprs.Packet
|
alias Aprsme.EncodingUtils
|
||||||
alias Aprs.Repo
|
alias Aprsme.Packet
|
||||||
alias AprsWeb.Endpoint
|
alias Aprsme.Repo
|
||||||
alias Parser.DeviceParser
|
alias AprsmeWeb.Endpoint
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(%{"callsign" => callsign}, _session, socket) do
|
def mount(%{"callsign" => callsign}, _session, socket) do
|
||||||
|
|
@ -66,7 +66,7 @@ defmodule AprsWeb.PacketsLive.CallsignView do
|
||||||
|
|
||||||
canonical_identifier =
|
canonical_identifier =
|
||||||
if is_binary(device_identifier) do
|
if is_binary(device_identifier) do
|
||||||
matched_device = Aprs.DeviceIdentification.lookup_device_by_identifier(device_identifier)
|
matched_device = Aprsme.DeviceIdentification.lookup_device_by_identifier(device_identifier)
|
||||||
if matched_device, do: matched_device.identifier, else: device_identifier
|
if matched_device, do: matched_device.identifier, else: device_identifier
|
||||||
else
|
else
|
||||||
device_identifier
|
device_identifier
|
||||||
|
|
@ -208,7 +208,10 @@ defmodule AprsWeb.PacketsLive.CallsignView do
|
||||||
|
|
||||||
defp enrich_packet_with_device_info(packet) do
|
defp enrich_packet_with_device_info(packet) do
|
||||||
device_identifier = Map.get(packet, :device_identifier) || Map.get(packet, "device_identifier")
|
device_identifier = Map.get(packet, :device_identifier) || Map.get(packet, "device_identifier")
|
||||||
device = if is_binary(device_identifier), do: Aprs.DeviceIdentification.lookup_device_by_identifier(device_identifier)
|
|
||||||
|
device =
|
||||||
|
if is_binary(device_identifier), do: Aprsme.DeviceIdentification.lookup_device_by_identifier(device_identifier)
|
||||||
|
|
||||||
model = if device, do: device.model
|
model = if device, do: device.model
|
||||||
vendor = if device, do: device.vendor
|
vendor = if device, do: device.vendor
|
||||||
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
defmodule AprsWeb.PacketsLive.Index do
|
defmodule AprsmeWeb.PacketsLive.Index do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AprsWeb, :live_view
|
use AprsmeWeb, :live_view
|
||||||
|
|
||||||
alias Aprs.EncodingUtils
|
alias Aprsme.EncodingUtils
|
||||||
alias AprsWeb.Endpoint
|
alias AprsmeWeb.Endpoint
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
if connected?(socket) do
|
if connected?(socket) do
|
||||||
Endpoint.subscribe("aprs_messages")
|
Endpoint.subscribe("aprs_messages")
|
||||||
Phoenix.PubSub.subscribe(Aprs.PubSub, "postgres:aprs_packets")
|
Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_packets")
|
||||||
end
|
end
|
||||||
|
|
||||||
{:ok, assign(socket, :packets, [])}
|
{:ok, assign(socket, :packets, [])}
|
||||||
|
|
@ -26,7 +26,7 @@ defmodule AprsWeb.PacketsLive.Index do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:postgres_packet, payload}, socket) do
|
def handle_info({:postgres_packet, payload}, socket) do
|
||||||
sanitized_payload = Aprs.EncodingUtils.sanitize_packet(payload)
|
sanitized_payload = Aprsme.EncodingUtils.sanitize_packet(payload)
|
||||||
packets = Enum.take([sanitized_payload | socket.assigns.packets], 100)
|
packets = Enum.take([sanitized_payload | socket.assigns.packets], 100)
|
||||||
socket = assign(socket, :packets, packets)
|
socket = assign(socket, :packets, packets)
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
|
|
@ -51,8 +51,8 @@
|
||||||
Map.get(packet, :lat) ||
|
Map.get(packet, :lat) ||
|
||||||
if(
|
if(
|
||||||
Map.has_key?(packet, :location) and not is_nil(packet.location) and
|
Map.has_key?(packet, :location) and not is_nil(packet.location) and
|
||||||
function_exported?(Aprs.Packet, :lat, 1),
|
function_exported?(Aprsme.Packet, :lat, 1),
|
||||||
do: Aprs.Packet.lat(packet),
|
do: Aprsme.Packet.lat(packet),
|
||||||
else: nil
|
else: nil
|
||||||
) ||
|
) ||
|
||||||
case Map.get(packet, :data_extended) do
|
case Map.get(packet, :data_extended) do
|
||||||
|
|
@ -86,8 +86,8 @@
|
||||||
Map.get(packet, :lon) ||
|
Map.get(packet, :lon) ||
|
||||||
if(
|
if(
|
||||||
Map.has_key?(packet, :location) and not is_nil(packet.location) and
|
Map.has_key?(packet, :location) and not is_nil(packet.location) and
|
||||||
function_exported?(Aprs.Packet, :lon, 1),
|
function_exported?(Aprsme.Packet, :lon, 1),
|
||||||
do: Aprs.Packet.lon(packet),
|
do: Aprsme.Packet.lon(packet),
|
||||||
else: nil
|
else: nil
|
||||||
) ||
|
) ||
|
||||||
case Map.get(packet, :data_extended) do
|
case Map.get(packet, :data_extended) do
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
defmodule AprsWeb.StatusLive.Index do
|
defmodule AprsmeWeb.StatusLive.Index do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
LiveView for displaying real-time APRS-IS connection status
|
LiveView for displaying real-time APRS-IS connection status
|
||||||
"""
|
"""
|
||||||
use AprsWeb, :live_view
|
use AprsmeWeb, :live_view
|
||||||
|
|
||||||
# 30 seconds
|
# 30 seconds
|
||||||
@refresh_interval 1_000
|
@refresh_interval 1_000
|
||||||
|
|
@ -327,11 +327,11 @@ defmodule AprsWeb.StatusLive.Index do
|
||||||
# Private functions
|
# Private functions
|
||||||
|
|
||||||
defp get_aprs_status do
|
defp get_aprs_status do
|
||||||
Aprs.Is.get_status()
|
Aprsme.Is.get_status()
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_app_version do
|
defp get_app_version do
|
||||||
:aprs |> Application.spec(:vsn) |> List.to_string()
|
:aprsme |> Application.spec(:vsn) |> List.to_string()
|
||||||
end
|
end
|
||||||
|
|
||||||
defp refresh_status(socket) do
|
defp refresh_status(socket) do
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
defmodule AprsWeb.UserConfirmationInstructionsLive do
|
defmodule AprsmeWeb.UserConfirmationInstructionsLive do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AprsWeb, :live_view
|
use AprsmeWeb, :live_view
|
||||||
|
|
||||||
alias Aprs.Accounts
|
alias Aprsme.Accounts
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
defmodule AprsWeb.UserConfirmationLive do
|
defmodule AprsmeWeb.UserConfirmationLive do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AprsWeb, :live_view
|
use AprsmeWeb, :live_view
|
||||||
|
|
||||||
alias Aprs.Accounts
|
alias Aprsme.Accounts
|
||||||
|
|
||||||
def render(%{live_action: :edit} = assigns) do
|
def render(%{live_action: :edit} = assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
defmodule AprsWeb.UserForgotPasswordLive do
|
defmodule AprsmeWeb.UserForgotPasswordLive do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AprsWeb, :live_view
|
use AprsmeWeb, :live_view
|
||||||
|
|
||||||
alias Aprs.Accounts
|
alias Aprsme.Accounts
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
defmodule AprsWeb.UserLoginLive do
|
defmodule AprsmeWeb.UserLoginLive do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AprsWeb, :live_view
|
use AprsmeWeb, :live_view
|
||||||
|
|
||||||
alias Aprs.Accounts
|
alias Aprsme.Accounts
|
||||||
alias Aprs.Accounts.User
|
alias Aprsme.Accounts.User
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
defmodule AprsWeb.UserRegistrationLive do
|
defmodule AprsmeWeb.UserRegistrationLive do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AprsWeb, :live_view
|
use AprsmeWeb, :live_view
|
||||||
|
|
||||||
alias Aprs.Accounts
|
alias Aprsme.Accounts
|
||||||
alias Aprs.Accounts.User
|
alias Aprsme.Accounts.User
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
defmodule AprsWeb.UserResetPasswordLive do
|
defmodule AprsmeWeb.UserResetPasswordLive do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AprsWeb, :live_view
|
use AprsmeWeb, :live_view
|
||||||
|
|
||||||
alias Aprs.Accounts
|
alias Aprsme.Accounts
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
defmodule AprsWeb.UserSettingsLive do
|
defmodule AprsmeWeb.UserSettingsLive do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AprsWeb, :live_view
|
use AprsmeWeb, :live_view
|
||||||
|
|
||||||
alias Aprs.Accounts
|
alias Aprsme.Accounts
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
defmodule AprsWeb.WeatherLive.CallsignView do
|
defmodule AprsmeWeb.WeatherLive.CallsignView do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AprsWeb, :live_view
|
use AprsmeWeb, :live_view
|
||||||
|
|
||||||
alias Aprs.Packets
|
alias Aprsme.Packets
|
||||||
alias AprsWeb.MapLive.PacketUtils
|
alias AprsmeWeb.MapLive.PacketUtils
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(%{"callsign" => callsign}, _session, socket) do
|
def mount(%{"callsign" => callsign}, _session, socket) do
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
defmodule AprsWeb.Router do
|
defmodule AprsmeWeb.Router do
|
||||||
use AprsWeb, :router
|
use AprsmeWeb, :router
|
||||||
|
|
||||||
import AprsWeb.UserAuth
|
import AprsmeWeb.UserAuth
|
||||||
import Phoenix.LiveDashboard.Router
|
import Phoenix.LiveDashboard.Router
|
||||||
|
|
||||||
pipeline :browser do
|
pipeline :browser do
|
||||||
plug :accepts, ["html"]
|
plug :accepts, ["html"]
|
||||||
plug :fetch_session
|
plug :fetch_session
|
||||||
plug :fetch_live_flash
|
plug :fetch_live_flash
|
||||||
plug :put_root_layout, {AprsWeb.Layouts, :root}
|
plug :put_root_layout, {AprsmeWeb.Layouts, :root}
|
||||||
plug :protect_from_forgery
|
plug :protect_from_forgery
|
||||||
plug :put_secure_browser_headers
|
plug :put_secure_browser_headers
|
||||||
plug :fetch_current_user
|
plug :fetch_current_user
|
||||||
|
|
@ -18,17 +18,17 @@ defmodule AprsWeb.Router do
|
||||||
plug :accepts, ["json"]
|
plug :accepts, ["json"]
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", AprsWeb do
|
scope "/", AprsmeWeb do
|
||||||
pipe_through :api
|
pipe_through :api
|
||||||
get "/health", PageController, :health
|
get "/health", PageController, :health
|
||||||
get "/ready", PageController, :ready
|
get "/ready", PageController, :ready
|
||||||
get "/status.json", PageController, :status_json
|
get "/status.json", PageController, :status_json
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", AprsWeb do
|
scope "/", AprsmeWeb do
|
||||||
pipe_through :browser
|
pipe_through :browser
|
||||||
|
|
||||||
live_dashboard "/dashboard", metrics: AprsWeb.Telemetry
|
live_dashboard "/dashboard", metrics: AprsmeWeb.Telemetry
|
||||||
|
|
||||||
live_session :regular_pages do
|
live_session :regular_pages do
|
||||||
live "/status", StatusLive.Index, :index
|
live "/status", StatusLive.Index, :index
|
||||||
|
|
@ -41,21 +41,21 @@ defmodule AprsWeb.Router do
|
||||||
live "/info/:callsign", InfoLive.Show, :show
|
live "/info/:callsign", InfoLive.Show, :show
|
||||||
end
|
end
|
||||||
|
|
||||||
live_session :map_pages, layout: {AprsWeb.Layouts, :map} do
|
live_session :map_pages, layout: {AprsmeWeb.Layouts, :map} do
|
||||||
live "/", MapLive.Index, :index
|
live "/", MapLive.Index, :index
|
||||||
live "/:callsign", MapLive.CallsignView, :index
|
live "/:callsign", MapLive.CallsignView, :index
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# API v1 routes
|
# API v1 routes
|
||||||
scope "/api/v1", AprsWeb.Api.V1, as: :api_v1 do
|
scope "/api/v1", AprsmeWeb.Api.V1, as: :api_v1 do
|
||||||
pipe_through :api
|
pipe_through :api
|
||||||
|
|
||||||
get "/callsign/:callsign", CallsignController, :show
|
get "/callsign/:callsign", CallsignController, :show
|
||||||
end
|
end
|
||||||
|
|
||||||
# Enable LiveDashboard and Swoosh mailbox preview in development
|
# Enable LiveDashboard and Swoosh mailbox preview in development
|
||||||
if Application.compile_env(:aprs, :dev_routes) do
|
if Application.compile_env(:aprsme, :dev_routes) do
|
||||||
# If you want to use the LiveDashboard in production, you should put
|
# If you want to use the LiveDashboard in production, you should put
|
||||||
# it behind authentication and allow only admins to access it.
|
# it behind authentication and allow only admins to access it.
|
||||||
# If your application does not have an admins-only section yet,
|
# If your application does not have an admins-only section yet,
|
||||||
|
|
@ -65,18 +65,18 @@ defmodule AprsWeb.Router do
|
||||||
scope "/dev" do
|
scope "/dev" do
|
||||||
pipe_through :browser
|
pipe_through :browser
|
||||||
|
|
||||||
# live_dashboard "/dashboard", metrics: AprsWeb.Telemetry
|
# live_dashboard "/dashboard", metrics: AprsmeWeb.Telemetry
|
||||||
forward "/mailbox", Plug.Swoosh.MailboxPreview
|
forward "/mailbox", Plug.Swoosh.MailboxPreview
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
## Authentication routes
|
## Authentication routes
|
||||||
|
|
||||||
scope "/", AprsWeb do
|
scope "/", AprsmeWeb do
|
||||||
pipe_through [:browser, :redirect_if_user_is_authenticated]
|
pipe_through [:browser, :redirect_if_user_is_authenticated]
|
||||||
|
|
||||||
live_session :redirect_if_user_is_authenticated,
|
live_session :redirect_if_user_is_authenticated,
|
||||||
on_mount: [{AprsWeb.UserAuth, :redirect_if_user_is_authenticated}] do
|
on_mount: [{AprsmeWeb.UserAuth, :redirect_if_user_is_authenticated}] do
|
||||||
live "/users/register", UserRegistrationLive, :new
|
live "/users/register", UserRegistrationLive, :new
|
||||||
live "/users/log_in", UserLoginLive, :new
|
live "/users/log_in", UserLoginLive, :new
|
||||||
live "/users/reset_password", UserForgotPasswordLive, :new
|
live "/users/reset_password", UserForgotPasswordLive, :new
|
||||||
|
|
@ -86,23 +86,23 @@ defmodule AprsWeb.Router do
|
||||||
post "/users/log_in", UserSessionController, :create
|
post "/users/log_in", UserSessionController, :create
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", AprsWeb do
|
scope "/", AprsmeWeb do
|
||||||
pipe_through [:browser, :require_authenticated_user]
|
pipe_through [:browser, :require_authenticated_user]
|
||||||
|
|
||||||
live_session :require_authenticated_user,
|
live_session :require_authenticated_user,
|
||||||
on_mount: [{AprsWeb.UserAuth, :ensure_authenticated}] do
|
on_mount: [{AprsmeWeb.UserAuth, :ensure_authenticated}] do
|
||||||
live "/users/settings", UserSettingsLive, :edit
|
live "/users/settings", UserSettingsLive, :edit
|
||||||
live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
|
live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", AprsWeb do
|
scope "/", AprsmeWeb do
|
||||||
pipe_through [:browser]
|
pipe_through [:browser]
|
||||||
|
|
||||||
delete "/users/log_out", UserSessionController, :delete
|
delete "/users/log_out", UserSessionController, :delete
|
||||||
|
|
||||||
live_session :current_user,
|
live_session :current_user,
|
||||||
on_mount: [{AprsWeb.UserAuth, :mount_current_user}] do
|
on_mount: [{AprsmeWeb.UserAuth, :mount_current_user}] do
|
||||||
live "/users/confirm/:token", UserConfirmationLive, :edit
|
live "/users/confirm/:token", UserConfirmationLive, :edit
|
||||||
live "/users/confirm", UserConfirmationInstructionsLive, :new
|
live "/users/confirm", UserConfirmationInstructionsLive, :new
|
||||||
end
|
end
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule AprsWeb.Telemetry do
|
defmodule AprsmeWeb.Telemetry do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Supervisor
|
use Supervisor
|
||||||
|
|
||||||
|
|
@ -54,23 +54,23 @@ defmodule AprsWeb.Telemetry do
|
||||||
),
|
),
|
||||||
|
|
||||||
# Database Metrics
|
# Database Metrics
|
||||||
summary("aprs.repo.query.total_time",
|
summary("aprsme.repo.query.total_time",
|
||||||
unit: {:native, :millisecond},
|
unit: {:native, :millisecond},
|
||||||
description: "The sum of the other measurements"
|
description: "The sum of the other measurements"
|
||||||
),
|
),
|
||||||
summary("aprs.repo.query.decode_time",
|
summary("aprsme.repo.query.decode_time",
|
||||||
unit: {:native, :millisecond},
|
unit: {:native, :millisecond},
|
||||||
description: "The time spent decoding the data received from the database"
|
description: "The time spent decoding the data received from the database"
|
||||||
),
|
),
|
||||||
summary("aprs.repo.query.query_time",
|
summary("aprsme.repo.query.query_time",
|
||||||
unit: {:native, :millisecond},
|
unit: {:native, :millisecond},
|
||||||
description: "The time spent executing the query"
|
description: "The time spent executing the query"
|
||||||
),
|
),
|
||||||
summary("aprs.repo.query.queue_time",
|
summary("aprsme.repo.query.queue_time",
|
||||||
unit: {:native, :millisecond},
|
unit: {:native, :millisecond},
|
||||||
description: "The time spent waiting for a database connection"
|
description: "The time spent waiting for a database connection"
|
||||||
),
|
),
|
||||||
summary("aprs.repo.query.idle_time",
|
summary("aprsme.repo.query.idle_time",
|
||||||
unit: {:native, :millisecond},
|
unit: {:native, :millisecond},
|
||||||
description: "The time the connection spent waiting before being checked out for the query"
|
description: "The time the connection spent waiting before being checked out for the query"
|
||||||
),
|
),
|
||||||
|
|
@ -87,7 +87,7 @@ defmodule AprsWeb.Telemetry do
|
||||||
[
|
[
|
||||||
# A module, function and arguments to be invoked periodically.
|
# A module, function and arguments to be invoked periodically.
|
||||||
# This function must call :telemetry.execute/3 and a metric must be added above.
|
# This function must call :telemetry.execute/3 and a metric must be added above.
|
||||||
# {AprsWeb, :count_users, []}
|
# {AprsmeWeb, :count_users, []}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule AprsWeb.TimeHelpers do
|
defmodule AprsmeWeb.TimeHelpers do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Shared helpers for formatting time and dates in the web layer.
|
Shared helpers for formatting time and dates in the web layer.
|
||||||
"""
|
"""
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
defmodule AprsWeb.UserAuth do
|
defmodule AprsmeWeb.UserAuth do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AprsWeb, :verified_routes
|
use AprsmeWeb, :verified_routes
|
||||||
|
|
||||||
import Phoenix.Controller
|
import Phoenix.Controller
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
|
|
||||||
alias Aprs.Accounts
|
alias Aprsme.Accounts
|
||||||
|
|
||||||
# Make the remember me cookie valid for 60 days.
|
# Make the remember me cookie valid for 60 days.
|
||||||
# If you want bump or reduce this value, also change
|
# If you want bump or reduce this value, also change
|
||||||
|
|
@ -74,7 +74,7 @@ defmodule AprsWeb.UserAuth do
|
||||||
user_token && Accounts.delete_user_session_token(user_token)
|
user_token && Accounts.delete_user_session_token(user_token)
|
||||||
|
|
||||||
if live_socket_id = get_session(conn, :live_socket_id) do
|
if live_socket_id = get_session(conn, :live_socket_id) do
|
||||||
AprsWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
|
AprsmeWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
|
||||||
end
|
end
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|
|
@ -131,16 +131,16 @@ defmodule AprsWeb.UserAuth do
|
||||||
Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate
|
Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate
|
||||||
the current_user:
|
the current_user:
|
||||||
|
|
||||||
defmodule AprsWeb.PageLive do
|
defmodule AprsmeWeb.PageLive do
|
||||||
use AprsWeb, :live_view
|
use AprsmeWeb, :live_view
|
||||||
|
|
||||||
on_mount {AprsWeb.UserAuth, :mount_current_user}
|
on_mount {AprsmeWeb.UserAuth, :mount_current_user}
|
||||||
...
|
...
|
||||||
end
|
end
|
||||||
|
|
||||||
Or use the `live_session` of your router to invoke the on_mount callback:
|
Or use the `live_session` of your router to invoke the on_mount callback:
|
||||||
|
|
||||||
live_session :authenticated, on_mount: [{AprsWeb.UserAuth, :ensure_authenticated}] do
|
live_session :authenticated, on_mount: [{AprsmeWeb.UserAuth, :ensure_authenticated}] do
|
||||||
live "/profile", ProfileLive, :index
|
live "/profile", ProfileLive, :index
|
||||||
end
|
end
|
||||||
"""
|
"""
|
||||||
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