From b5d65c4a52ac3f03dfb8b3e62f5e9338f910d992 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 2 May 2026 18:37:10 +0000 Subject: [PATCH] N/A --- README.md | 221 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 135 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index 2139d62..f5d0a5a 100644 --- a/README.md +++ b/README.md @@ -1,187 +1,236 @@ # heardlog -Samlar APRS-data från två källor och lagrar i TimescaleDB + PostGIS som grund för en täckningskarta. +APRS RF coverage logger and map for amateur radio digipeaters. + +Collects every packet heard on RF by a Direwolf digipeater and builds an +interactive coverage map showing which Maidenhead subsquares have been reached, +which paths were used, and which stations have been heard. + +Live: [aprs.sa6anw.se](https://aprs.sa6anw.se) + +--- + +## Architecture ``` -[Shack-LAN] [DMZ] -Direwolf AGW:8000 - │ -agw_forwarder.py ──POST/Bearer──► FastAPI :8085 ──► TimescaleDB + PostGIS - │ - rotate.aprs2.net┘ (APRS-IS, outbound från DMZ) +[Shack LAN] [DMZ] +Direwolf AGW :8000 + | +agw_forwarder.py --POST/Bearer--> FastAPI :8085 --> TimescaleDB + PostGIS + | + rotate.aprs2.net| (APRS-IS regional feed, outbound) + | + nginx :80/443 + | + index.html (Leaflet map) ``` -| Källa | Tabell | Nyckeldata | +| Source | Table | Key data | |---|---|---| -| 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. +| Direwolf AGW (RF) | `rf_frames` | Every packet received over the air | +| APRS-IS regional | `is_frames` | Regional feed within 200 km radius | --- -## Krav +## Repository layout -- Docker + Docker Compose (DMZ-server) -- Direwolf med `AGWPORT 8000` i `direwolf.conf` (shack-dator) -- Python 3.10+ och `pip3 install requests` (shack-dator) +``` +heardlog/ +????????? .env.example +????????? README.md +????????? docker-compose.yml +????????? collector/ # DMZ - runs in Docker +??? ????????? Dockerfile +??? ????????? requirements.txt +??? ????????? config.py +??? ????????? db.py # TimescaleDB schema + insert helpers +??? ????????? maidenhead.py # Maidenhead subsquare calculations +??? ????????? aprs_is.py # APRS-IS client +??? ????????? main.py # FastAPI app + APRS-IS background task +????????? agw-forwarder/ # Shack - runs directly on the Direwolf machine +??? ????????? agw_forwarder.py +??? ????????? requirements.txt +??? ????????? agw-forwarder.service +????????? web/ + ????????? index.html # Leaflet map (served by nginx) + ????????? nginx.conf +``` --- -## DMZ – snabbstart +## Getting started + +### DMZ ```bash git clone heardlog cd heardlog cp .env.example .env +# Edit .env - set DB_PASSWORD and API_KEY -# Generera API-nyckel +# Generate API key python3 -c "import secrets; print(secrets.token_hex(32))" -# Klistra in som API_KEY i .env docker compose up -d docker compose logs -f collector ``` -Förväntad output: +Expected output: ``` INFO db Database schema ready -INFO main Startup complete – listening for RF frames +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 +### Shack (agw-forwarder) ```bash pip3 install requests -# Skapa konfigurationsfil cat > heardlog/agw-forwarder/.env << 'EOF' AGW_HOST=localhost AGW_PORT=8000 COLLECTOR_URL=http://:8085 -API_KEY= +API_KEY= STATION_CALL=SA6ANW-1 LOG_LEVEL=INFO EOF -# Testa manuellt +# Test manually first cd heardlog/agw-forwarder python3 agw_forwarder.py ``` -### Kör som systemd-tjänster +### Run as systemd services (shack) ```bash -# 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 - -# Verifiera journalctl -u direwolf -u agw-forwarder -f ``` -`agw-forwarder` har `Wants=direwolf.service` – systemd startar Direwolf automatiskt om den inte kör. +`agw-forwarder` has `Wants=direwolf.service` - systemd starts Direwolf automatically. --- -## Konfiguration +## Configuration ### DMZ (`.env`) -| Variabel | Standard | Beskrivning | +| Variable | Default | Description | |---|---|---| -| `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 | +| `STATION_CALL` | `SA6ANW-1` | Your callsign | +| `DB_PASSWORD` | - | Postgres password | +| `API_KEY` | - | Shared secret with forwarder | +| `APRS_IS_PASSCODE` | `-1` | `-1` for receive-only | +| `APRS_IS_FILTER` | `r/58.35/14.05/200` | lat/lon/km around your QTH | +| `LOG_LEVEL` | `INFO` | `DEBUG` for per-frame logging | ### Shack (`agw-forwarder/.env`) -| Variabel | Standard | Beskrivning | +| Variable | Default | Description | |---|---|---| -| `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 | +| `AGW_HOST` | `localhost` | Direwolf host | +| `AGW_PORT` | `8000` | Direwolf AGW port | +| `COLLECTOR_URL` | - | DMZ API URL | +| `API_KEY` | - | Same as DMZ `.env` | +| `STATION_CALL` | `SA6ANW-1` | Your callsign | --- -## Implementationsnoteringar +## Map -### AGW monitoring-format -Direwolf skickar `U`-frames i monitoring-mode med ett ledande mellanslag och full monitoring-header före `\r`: +The map has three zoom levels: + +| Zoom | Display | +|---|---| +| < 7 | One dot per Maidenhead subsquare | +| 7 - 13 | Transparent subsquare rectangles (~5 x 4.6 km each) | +| >= 14 | Individual observation points | + +Clicking a square opens a popup with two tabs: + +- **PATHS** - actual digipeater hops heard from that square, sorted by count. Digipeaters without a known position are shown in red. +- **STATIONS** - callsigns heard from that square. Clicking a station shows detailed statistics including distance, timestamps, and top paths. + +Hovering over a path row draws the route on the map: square center to each digipeater to RX station. The RX station marker (pulsing yellow dot) shows all paths heard aggregated across all squares. + +Clicking **SQUARES** in the header opens a feed of all squares sorted by most recently added, with distance from the RX station. + +Popups can be dragged to move them out of the way. + +--- + +## API + +| Endpoint | Auth | Description | +|---|---|---| +| `POST /ingest/rf` | Bearer | RF frame from forwarder | +| `GET /health` | - | Liveness check | +| `GET /coverage/squares` | - | Subsquares with path and station breakdown | +| `GET /coverage/points` | - | Individual observation positions | +| `GET /coverage/digis` | - | Latest known position per callsign | +| `GET /coverage/rx-stats` | - | All paths aggregated across all squares | +| `GET /coverage/station/{callsign}` | - | Detailed statistics for one station | + +Swagger UI: `http://:8085/docs` + +--- + +## Implementation notes + +### AGW monitoring format +Direwolf sends `U`-frames in monitoring mode with a leading space and full +monitoring header before `\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/stationer hörda senaste 30 min +-- Stations heard in the last 30 minutes 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 (0.01°) +-- Coverage grid SELECT ROUND(lat::numeric, 2) AS grid_lat, ROUND(lon::numeric, 2) AS grid_lon, - COUNT(*) AS hits + COUNT(*) AS hits FROM rf_frames -WHERE heard_direct = TRUE - AND lat IS NOT NULL +WHERE 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` +Access the database: +```bash +docker exec -it heardlog-db-1 psql -U aprs -d aprs +```