diff --git a/README.md b/README.md index 21f923c..2139d62 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,164 @@ -# aprs-collector +# heardlog + +Samlar APRS-data från två källor och lagrar i TimescaleDB + PostGIS som grund för en täckningskarta. ``` [Shack-LAN] [DMZ] Direwolf AGW:8000 - ??? -agw_forwarder.py ??????POST/Bearer????????? FastAPI :8080/ingest/rf ????????? TimescaleDB - ??? - rotate.aprs2.net??? (APRS-IS, outbound) + │ +agw_forwarder.py ──POST/Bearer──► FastAPI :8085 ──► TimescaleDB + PostGIS + │ + rotate.aprs2.net┘ (APRS-IS, outbound från DMZ) ``` +| Källa | Tabell | Nyckeldata | +|---|---|---| +| Direwolf AGW (RF) | `rf_frames` | `heard_direct` – hördes utan mellanliggande digi | +| APRS-IS regionalt | `is_frames` | Stationer inom 200 km som digipeatas till internet | + +`heard_direct = TRUE` är grundstenen för täckningskartan – ett bevis på att en station nådde vår mottagare direkt. + --- -## DMZ ??? snabbstart +## Krav + +- Docker + Docker Compose (DMZ-server) +- Direwolf med `AGWPORT 8000` i `direwolf.conf` (shack-dator) +- Python 3.10+ och `pip3 install requests` (shack-dator) + +--- + +## DMZ – snabbstart ```bash +git clone heardlog +cd heardlog + cp .env.example .env -# Generera en API-nyckel +# Generera API-nyckel python3 -c "import secrets; print(secrets.token_hex(32))" -# Klistra in i .env som API_KEY +# Klistra in som API_KEY i .env docker compose up -d docker compose logs -f collector ``` -S??tt en reverse proxy (Caddy/nginx) framf??r port 8080 om du vill ha TLS. +Förväntad output: +``` +INFO db Database schema ready +INFO main Startup complete – listening for RF frames +INFO aprs_is APRS-IS login: # logresp SA6ANW-1 unverified, server T2... +``` + +`unverified` på APRS-IS är förväntat – passcode `-1` ger receive-only vilket är tillräckligt. --- -## Shack ??? agw-forwarder - -Kopiera mappen `agw-forwarder/` till Direwolf-datorn. +## Shack – agw-forwarder ```bash pip3 install requests -export AGW_HOST=localhost -export AGW_PORT=8000 -export COLLECTOR_URL=http://:8080 -export API_KEY= -export STATION_CALL=SA6ANW-1 +# Skapa konfigurationsfil +cat > heardlog/agw-forwarder/.env << 'EOF' +AGW_HOST=localhost +AGW_PORT=8000 +COLLECTOR_URL=http://:8085 +API_KEY= +STATION_CALL=SA6ANW-1 +LOG_LEVEL=INFO +EOF +# Testa manuellt +cd heardlog/agw-forwarder python3 agw_forwarder.py ``` -### K??r som systemd-tj??nst +### Kör som systemd-tjänster ```bash -# Redigera agw-forwarder.service ??? fyll i COLLECTOR_URL och API_KEY -sudo cp agw-forwarder.service /etc/systemd/system/ +# Kopiera service-filer +sudo cp heardlog/agw-forwarder/direwolf.service /etc/systemd/system/ +sudo cp heardlog/agw-forwarder/agw-forwarder.service /etc/systemd/system/ + sudo systemctl daemon-reload +sudo systemctl enable --now direwolf sudo systemctl enable --now agw-forwarder -journalctl -u agw-forwarder -f + +# Verifiera +journalctl -u direwolf -u agw-forwarder -f ``` ---- - -## Resiliens - -Forwardern har en intern k?? (2000 frames). Om DMZ ??r tillf??lligt on??bar buffras -frames i minnet och skickas n??r anslutningen ??terkommer. Vid omstart av forwardern -f??rsvinner buffrade frames ??? tillr??ckligt f??r de flesta avbrott. +`agw-forwarder` har `Wants=direwolf.service` – systemd startar Direwolf automatiskt om den inte kör. --- -## API +## Konfiguration -| Endpoint | Metod | Auth | Beskrivning | -|---|---|---|---| -| `/ingest/rf` | POST | Bearer | RF-frame fr??n forwarder | -| `/health` | GET | ??? | Liveness check | +### DMZ (`.env`) -Swagger UI: `http://:8080/docs` +| Variabel | Standard | Beskrivning | +|---|---|---| +| `STATION_CALL` | `SA6ANW-1` | Din anropssignal | +| `DB_PASSWORD` | – | Postgres-lösenord | +| `API_KEY` | – | Delad hemlighet med forwardern | +| `APRS_IS_PASSCODE` | `-1` | `-1` för receive-only | +| `APRS_IS_FILTER` | `r/58.35/14.05/200` | lat/lon/km runt din QTH | +| `LOG_LEVEL` | `INFO` | `DEBUG` för varje frame | + +### Shack (`agw-forwarder/.env`) + +| Variabel | Standard | Beskrivning | +|---|---|---| +| `AGW_HOST` | `localhost` | Direwolf-host | +| `AGW_PORT` | `8000` | Direwolf AGW-port | +| `COLLECTOR_URL` | – | URL till DMZ-API:t | +| `API_KEY` | – | Samma som i DMZ `.env` | +| `STATION_CALL` | `SA6ANW-1` | Din anropssignal | + +--- + +## Implementationsnoteringar + +### AGW monitoring-format +Direwolf skickar `U`-frames i monitoring-mode med ett ledande mellanslag och full monitoring-header före `\r`: + +``` + 1:Fm SM6MJW-9 To UXRT8L Via WIDE1-1,WIDE2-1 [16:46:13]\r +``` + +Via-pathen extraheras med regex mellan `Via ` och ` NOW() - INTERVAL '7 days' ORDER BY ts DESC; --- Digis som h??rts senaste 30 min (= "online") +-- Digis/stationer hörda senaste 30 min SELECT src_call, MAX(ts) AS last_seen, COUNT(*) AS frames FROM rf_frames WHERE ts > NOW() - INTERVAL '30 minutes' GROUP BY src_call ORDER BY last_seen DESC; --- T??ckningspunkter per rutn??tscell +-- Täckningspunkter per rutnätscell (0.01°) SELECT ROUND(lat::numeric, 2) AS grid_lat, ROUND(lon::numeric, 2) AS grid_lon, @@ -101,4 +167,21 @@ FROM rf_frames WHERE heard_direct = TRUE AND lat IS NOT NULL GROUP BY grid_lat, grid_lon; + +-- APRS-IS senaste timmen +SELECT ts, src_call, lat, lon +FROM is_frames +WHERE ts > NOW() - INTERVAL '1 hour' +ORDER BY ts DESC; ``` + +--- + +## API + +| Endpoint | Metod | Auth | Beskrivning | +|---|---|---|---| +| `/ingest/rf` | POST | Bearer | RF-frame från forwarder | +| `/health` | GET | – | Liveness check | + +Swagger UI: `http://:8085/docs` diff --git a/README.md~ b/README.md~ new file mode 100644 index 0000000..21f923c --- /dev/null +++ b/README.md~ @@ -0,0 +1,104 @@ +# aprs-collector + +``` +[Shack-LAN] [DMZ] +Direwolf AGW:8000 + ??? +agw_forwarder.py ??????POST/Bearer????????? FastAPI :8080/ingest/rf ????????? TimescaleDB + ??? + rotate.aprs2.net??? (APRS-IS, outbound) +``` + +--- + +## DMZ ??? snabbstart + +```bash +cp .env.example .env + +# Generera en API-nyckel +python3 -c "import secrets; print(secrets.token_hex(32))" +# Klistra in i .env som API_KEY + +docker compose up -d +docker compose logs -f collector +``` + +S??tt en reverse proxy (Caddy/nginx) framf??r port 8080 om du vill ha TLS. + +--- + +## Shack ??? agw-forwarder + +Kopiera mappen `agw-forwarder/` till Direwolf-datorn. + +```bash +pip3 install requests + +export AGW_HOST=localhost +export AGW_PORT=8000 +export COLLECTOR_URL=http://:8080 +export API_KEY= +export STATION_CALL=SA6ANW-1 + +python3 agw_forwarder.py +``` + +### K??r som systemd-tj??nst + +```bash +# Redigera agw-forwarder.service ??? fyll i COLLECTOR_URL och API_KEY +sudo cp agw-forwarder.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now agw-forwarder +journalctl -u agw-forwarder -f +``` + +--- + +## Resiliens + +Forwardern har en intern k?? (2000 frames). Om DMZ ??r tillf??lligt on??bar buffras +frames i minnet och skickas n??r anslutningen ??terkommer. Vid omstart av forwardern +f??rsvinner buffrade frames ??? tillr??ckligt f??r de flesta avbrott. + +--- + +## API + +| Endpoint | Metod | Auth | Beskrivning | +|---|---|---|---| +| `/ingest/rf` | POST | Bearer | RF-frame fr??n forwarder | +| `/health` | GET | ??? | Liveness check | + +Swagger UI: `http://:8080/docs` + +--- + +## Nyttiga queries + +```sql +-- Direkt-h??rda stationer senaste 7 dagarna +SELECT ts, src_call, lat, lon, path +FROM rf_frames +WHERE heard_direct = TRUE + AND ts > NOW() - INTERVAL '7 days' +ORDER BY ts DESC; + +-- Digis som h??rts senaste 30 min (= "online") +SELECT src_call, MAX(ts) AS last_seen, COUNT(*) AS frames +FROM rf_frames +WHERE ts > NOW() - INTERVAL '30 minutes' +GROUP BY src_call +ORDER BY last_seen DESC; + +-- T??ckningspunkter per rutn??tscell +SELECT + ROUND(lat::numeric, 2) AS grid_lat, + ROUND(lon::numeric, 2) AS grid_lon, + COUNT(*) AS hits +FROM rf_frames +WHERE heard_direct = TRUE + AND lat IS NOT NULL +GROUP BY grid_lat, grid_lon; +```