first commit
This commit is contained in:
157
README.md
157
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]
|
[Shack-LAN] [DMZ]
|
||||||
Direwolf AGW:8000
|
Direwolf AGW:8000
|
||||||
???
|
│
|
||||||
agw_forwarder.py ??????POST/Bearer????????? FastAPI :8080/ingest/rf ????????? TimescaleDB
|
agw_forwarder.py ──POST/Bearer──► FastAPI :8085 ──► TimescaleDB + PostGIS
|
||||||
???
|
│
|
||||||
rotate.aprs2.net??? (APRS-IS, outbound)
|
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
|
```bash
|
||||||
|
git clone <repo> heardlog
|
||||||
|
cd heardlog
|
||||||
|
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
|
|
||||||
# Generera en API-nyckel
|
# Generera API-nyckel
|
||||||
python3 -c "import secrets; print(secrets.token_hex(32))"
|
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 up -d
|
||||||
docker compose logs -f collector
|
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
|
## Shack – agw-forwarder
|
||||||
|
|
||||||
Kopiera mappen `agw-forwarder/` till Direwolf-datorn.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip3 install requests
|
pip3 install requests
|
||||||
|
|
||||||
export AGW_HOST=localhost
|
# Skapa konfigurationsfil
|
||||||
export AGW_PORT=8000
|
cat > heardlog/agw-forwarder/.env << 'EOF'
|
||||||
export COLLECTOR_URL=http://<dmz-ip>:8080
|
AGW_HOST=localhost
|
||||||
export API_KEY=<samma nyckel som i DMZ .env>
|
AGW_PORT=8000
|
||||||
export STATION_CALL=SA6ANW-1
|
COLLECTOR_URL=http://<dmz-ip>:8085
|
||||||
|
API_KEY=<samma nyckel som i DMZ .env>
|
||||||
|
STATION_CALL=SA6ANW-1
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Testa manuellt
|
||||||
|
cd heardlog/agw-forwarder
|
||||||
python3 agw_forwarder.py
|
python3 agw_forwarder.py
|
||||||
```
|
```
|
||||||
|
|
||||||
### K??r som systemd-tj??nst
|
### Kör som systemd-tjänster
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Redigera agw-forwarder.service ??? fyll i COLLECTOR_URL och API_KEY
|
# Kopiera service-filer
|
||||||
sudo cp agw-forwarder.service /etc/systemd/system/
|
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 daemon-reload
|
||||||
|
sudo systemctl enable --now direwolf
|
||||||
sudo systemctl enable --now agw-forwarder
|
sudo systemctl enable --now agw-forwarder
|
||||||
journalctl -u agw-forwarder -f
|
|
||||||
|
# Verifiera
|
||||||
|
journalctl -u direwolf -u agw-forwarder -f
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
`agw-forwarder` har `Wants=direwolf.service` – systemd startar Direwolf automatiskt om den inte kör.
|
||||||
|
|
||||||
## 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
|
## Konfiguration
|
||||||
|
|
||||||
| Endpoint | Metod | Auth | Beskrivning |
|
### DMZ (`.env`)
|
||||||
|---|---|---|---|
|
|
||||||
| `/ingest/rf` | POST | Bearer | RF-frame fr??n forwarder |
|
|
||||||
| `/health` | GET | ??? | Liveness check |
|
|
||||||
|
|
||||||
Swagger UI: `http://<dmz-ip>: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 <UI pid=F0 Len=17 F=0 >[16:46:13]\r<info>
|
||||||
|
```
|
||||||
|
|
||||||
|
Via-pathen extraheras med regex mellan `Via ` och ` <UI`. `heard_direct` avgörs av om någon hop i pathen har `*` (har-repeaterats-flaggan).
|
||||||
|
|
||||||
|
### Null-bytes
|
||||||
|
APRS-frames kan innehålla `0x00`-bytes i info-fältet. Dessa stoppas ut innan DB-insert eftersom Postgres UTF8 inte accepterar null-bytes.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Nyttiga queries
|
## Nyttiga queries
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- Direkt-h??rda stationer senaste 7 dagarna
|
-- Senaste RF-frames
|
||||||
|
SELECT ts, src_call, lat, lon, heard_direct, path
|
||||||
|
FROM rf_frames
|
||||||
|
ORDER BY ts DESC
|
||||||
|
LIMIT 20;
|
||||||
|
|
||||||
|
-- Direkt-hörda stationer senaste 7 dagarna
|
||||||
SELECT ts, src_call, lat, lon, path
|
SELECT ts, src_call, lat, lon, path
|
||||||
FROM rf_frames
|
FROM rf_frames
|
||||||
WHERE heard_direct = TRUE
|
WHERE heard_direct = TRUE
|
||||||
AND ts > NOW() - INTERVAL '7 days'
|
AND ts > NOW() - INTERVAL '7 days'
|
||||||
ORDER BY ts DESC;
|
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
|
SELECT src_call, MAX(ts) AS last_seen, COUNT(*) AS frames
|
||||||
FROM rf_frames
|
FROM rf_frames
|
||||||
WHERE ts > NOW() - INTERVAL '30 minutes'
|
WHERE ts > NOW() - INTERVAL '30 minutes'
|
||||||
GROUP BY src_call
|
GROUP BY src_call
|
||||||
ORDER BY last_seen DESC;
|
ORDER BY last_seen DESC;
|
||||||
|
|
||||||
-- T??ckningspunkter per rutn??tscell
|
-- Täckningspunkter per rutnätscell (0.01°)
|
||||||
SELECT
|
SELECT
|
||||||
ROUND(lat::numeric, 2) AS grid_lat,
|
ROUND(lat::numeric, 2) AS grid_lat,
|
||||||
ROUND(lon::numeric, 2) AS grid_lon,
|
ROUND(lon::numeric, 2) AS grid_lon,
|
||||||
@@ -101,4 +167,21 @@ FROM rf_frames
|
|||||||
WHERE heard_direct = TRUE
|
WHERE heard_direct = TRUE
|
||||||
AND lat IS NOT NULL
|
AND lat IS NOT NULL
|
||||||
GROUP BY grid_lat, grid_lon;
|
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://<dmz-ip>:8085/docs`
|
||||||
|
|||||||
104
README.md~
Normal file
104
README.md~
Normal file
@@ -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://<dmz-ip>:8080
|
||||||
|
export API_KEY=<samma nyckel som i DMZ .env>
|
||||||
|
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://<dmz-ip>: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;
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user