Compare commits
2
Commits
0cd2c3951a
...
30d3127b21
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30d3127b21 | ||
|
|
88fd24c22b |
@@ -0,0 +1,5 @@
|
||||
.venv
|
||||
.git
|
||||
data
|
||||
.env
|
||||
**/__pycache__
|
||||
@@ -0,0 +1,3 @@
|
||||
# API-Key von https://api-manager.opentransportdata.swiss (Produkt "GTFS-RT").
|
||||
# Leer lassen = Karte läuft nur nach Fahrplan, ohne Verspätungen.
|
||||
TRAINPULSE_RT_KEY=
|
||||
@@ -1,3 +1,5 @@
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
data/
|
||||
.env
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
# pfaedle: rechnet aus OpenStreetMap die echten Streckenverläufe
|
||||
FROM debian:trixie AS pfaedle
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates git cmake g++ make libzip-dev zlib1g-dev libbz2-dev \
|
||||
&& git clone --depth 1 --recurse-submodules --shallow-submodules https://github.com/ad-freiburg/pfaedle /src \
|
||||
&& cmake -S /src -B /build -DCMAKE_BUILD_TYPE=Release \
|
||||
&& make -C /build -j"$(nproc)" pfaedle \
|
||||
&& make -C /build install
|
||||
|
||||
# pmtiles: schneidet die Kartenkacheln für die Region aus
|
||||
FROM golang:1.25-trixie AS pmtiles
|
||||
RUN go install github.com/protomaps/go-pmtiles@v1.31.2
|
||||
|
||||
FROM python:3.13-slim-trixie
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends libzip5 zlib1g libbz2-1.0 libgomp1 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=pfaedle /usr/local/bin/pfaedle /usr/local/bin/
|
||||
COPY --from=pfaedle /usr/local/etc/pfaedle /usr/local/etc/pfaedle
|
||||
COPY --from=pmtiles /go/bin/go-pmtiles /usr/local/bin/pmtiles
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY trainpulse trainpulse
|
||||
COPY web web
|
||||
COPY tests tests
|
||||
ENV TRAINPULSE_DATA=/data PYTHONUNBUFFERED=1
|
||||
EXPOSE 8080
|
||||
CMD ["uvicorn", "trainpulse.server:app", "--host", "0.0.0.0", "--port", "8080", "--proxy-headers"]
|
||||
@@ -7,11 +7,11 @@ Ziel, Verspätung und Fahrtverlauf mit Gleis. Hintergrund umschaltbar zwischen K
|
||||
## Wie es funktioniert
|
||||
|
||||
```
|
||||
opentransportdata.swiss ──GTFS (Mo+Do)──▶ trainpulse-update (stündlich)
|
||||
opentransportdata.swiss ──GTFS (Mo+Do)──▶ updater-Container (stündlich)
|
||||
├─ Filter: nur Bahn (route_type 100–199)
|
||||
OpenStreetMap (Geofabrik) ─────────────────▶├─ pfaedle: echte Streckenverläufe
|
||||
└─▶ /var/lib/trainpulse/feeds/<version>/
|
||||
opentransportdata.swiss ──GTFS-RT (35 s)──▶ trainpulse (Webserver, Port 8080)
|
||||
└─▶ data/feeds/<version>/
|
||||
opentransportdata.swiss ──GTFS-RT (35 s)──▶ web-Container (Port 8080)
|
||||
├─ Position = Fahrplan + Verspätung, entlang Strecke
|
||||
└─ /api/vehicles, /api/trip/…, /api/status, Weboberfläche
|
||||
```
|
||||
@@ -23,33 +23,43 @@ Wechsel des Fahrplans: Neue Fahrpläne erscheinen Montag und Donnerstag zwischen
|
||||
die Echtzeitdaten stellen erst um 15 Uhr um. Der Server lädt deshalb immer den Fahrplan, auf den
|
||||
sich die Echtzeitdaten gerade beziehen (`feed_version`), und schaltet automatisch mit um.
|
||||
|
||||
## Installation auf Proxmox
|
||||
## Installation auf Proxmox (Docker)
|
||||
|
||||
TrainPulse läuft als zwei Docker-Container aus demselben Image:
|
||||
`web` (Webserver, Port 8080) und `updater` (holt stündlich neue Fahrpläne und beim ersten Start
|
||||
die Kartenkacheln). Alle Daten liegen im Ordner `data/` neben der `compose.yaml`.
|
||||
|
||||
1. **Container anlegen** (Proxmox-Oberfläche → *Create CT*):
|
||||
Debian 12 oder 13 Template, unprivileged, 2 Kerne, **4 GB RAM** (pfaedle braucht beim Aufbereiten bis ~2 GB, der Server ~0,5 GB), 20 GB Disk, Netz per DHCP.
|
||||
2. **Projekt in den Container kopieren**, z. B. `scp trainpulse.zip root@<ip>:` und dort
|
||||
`apt install -y unzip && unzip trainpulse.zip`.
|
||||
3. **Installieren:** `bash trainpulse/deploy/install.sh`
|
||||
Baut pfaedle, lädt die Kartenkacheln für die Schweiz, richtet die Dienste ein und startet
|
||||
die erste Fahrplan-Aufbereitung im Hintergrund (`journalctl -fu trainpulse-update`).
|
||||
4. **Echtzeit freischalten:** Auf <https://api-manager.opentransportdata.swiss> registrieren,
|
||||
Debian 12 oder 13 Template, unprivileged, 2 Kerne, **4 GB RAM** (pfaedle braucht beim
|
||||
Aufbereiten bis ~2 GB, der Server ~0,5 GB), **20 GB Disk**, Netz per DHCP.
|
||||
Unter *Options → Features* **Nesting** und **keyctl** aktivieren – ohne das läuft Docker
|
||||
im LXC nicht.
|
||||
2. **Installieren** (im Container als root):
|
||||
```bash
|
||||
apt install -y git
|
||||
git clone https://git.scarriffle.com/Scarriffle/Trainpulse.git
|
||||
bash Trainpulse/deploy/install.sh
|
||||
```
|
||||
Das Skript installiert Docker, falls es fehlt, baut das Image und startet beide Container.
|
||||
3. **Echtzeit freischalten:** Auf <https://api-manager.opentransportdata.swiss> registrieren,
|
||||
eine Applikation anlegen, das Produkt *GTFS-RT* abonnieren und den API-Key in
|
||||
`/etc/trainpulse.env` bei `TRAINPULSE_RT_KEY=` eintragen. Dann `systemctl restart trainpulse`.
|
||||
`Trainpulse/.env` bei `TRAINPULSE_RT_KEY=` eintragen. Dann `docker compose up -d`.
|
||||
|
||||
Danach ist die Karte unter `http://<ip>:8080` erreichbar. Für den Zugriff von aussen einfach
|
||||
hinter deinen bestehenden Reverse Proxy hängen.
|
||||
|
||||
Ein Update des Programms: neue Version in den Container kopieren, `install.sh` nochmals ausführen.
|
||||
**Update:** `git pull && bash deploy/install.sh` – baut das Image neu, Daten bleiben.
|
||||
|
||||
## Nützliche Befehle
|
||||
## Nützliche Befehle (im Ordner `Trainpulse`)
|
||||
|
||||
| Was | Befehl |
|
||||
|---|---|
|
||||
| Status | `curl localhost:8080/api/status` |
|
||||
| Logs Webserver | `journalctl -fu trainpulse` |
|
||||
| Logs Fahrplan-Update | `journalctl -fu trainpulse-update` |
|
||||
| Fahrplan sofort neu aufbereiten | `sudo -u trainpulse env $(cat /etc/trainpulse.env) /opt/trainpulse/.venv/bin/python -m trainpulse.prepare --force` |
|
||||
| Selbsttest | `/opt/trainpulse/.venv/bin/python tests/test_core.py` (im Projektordner) |
|
||||
| Logs Webserver | `docker compose logs -f web` |
|
||||
| Logs Fahrplan-Update | `docker compose logs -f updater` |
|
||||
| Fahrplan sofort neu aufbereiten | `docker compose exec updater python -m trainpulse.prepare --force` |
|
||||
| Selbsttest | `docker compose exec web python tests/test_core.py` |
|
||||
| Stoppen / Starten | `docker compose down` / `docker compose up -d` |
|
||||
|
||||
## Datenquellen und Lizenzen
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
image: trainpulse
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./data:/data
|
||||
|
||||
# holt stündlich neue Fahrpläne (und beim ersten Start die Kartenkacheln)
|
||||
updater:
|
||||
build: .
|
||||
image: trainpulse
|
||||
depends_on: [web]
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./data:/data
|
||||
command: ["sh", "-c", "while true; do python -m trainpulse.prepare; sleep 3600; done"]
|
||||
+27
-64
@@ -1,79 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# TrainPulse – Installation in einem Debian-LXC (Proxmox). Im Container als root:
|
||||
# TrainPulse – Installation mit Docker (z. B. in einem Debian-LXC auf Proxmox). Als root:
|
||||
# bash deploy/install.sh
|
||||
# Erneut ausführen = Update des Programmcodes (Daten, Karte und Einstellungen bleiben).
|
||||
# Erneut ausführen = Update (Image neu bauen, Daten und Einstellungen bleiben).
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
APP=/opt/trainpulse
|
||||
DATA=/var/lib/trainpulse
|
||||
SRC=$(cd "$(dirname "$0")/.." && pwd)
|
||||
BBOX=5.8,45.7,10.6,47.9 # Schweiz mit etwas Rand
|
||||
[ "$(id -u)" = 0 ] || { echo "Bitte als root ausführen" >&2; exit 1; }
|
||||
command -v apt-get >/dev/null || { echo "Nur für Debian/Ubuntu (apt) gedacht" >&2; exit 1; }
|
||||
mem=$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo)
|
||||
[ "$mem" -ge 3500 ] || echo "WARNUNG: nur ${mem} MB RAM – für die Fahrplan-Aufbereitung werden ~4 GB empfohlen" >&2
|
||||
|
||||
echo "==> Pakete"
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq python3-venv git cmake g++ make libzip-dev zlib1g-dev libbz2-dev curl ca-certificates >/dev/null
|
||||
|
||||
echo "==> Programm nach $APP"
|
||||
id trainpulse &>/dev/null || useradd --system --home-dir "$DATA" --shell /usr/sbin/nologin trainpulse
|
||||
mkdir -p "$APP" "$DATA/tiles"
|
||||
rm -rf "$APP/trainpulse" "$APP/web"
|
||||
cp -r "$SRC/trainpulse" "$SRC/web" "$SRC/requirements.txt" "$APP/"
|
||||
[ -d "$APP/.venv" ] || python3 -m venv "$APP/.venv"
|
||||
"$APP/.venv/bin/pip" install -q --upgrade -r "$APP/requirements.txt"
|
||||
|
||||
if ! command -v pfaedle >/dev/null; then
|
||||
echo "==> pfaedle bauen (Streckenverläufe aus OpenStreetMap, dauert ein paar Minuten)"
|
||||
tmp=$(mktemp -d)
|
||||
git clone -q --depth 1 --recurse-submodules --shallow-submodules https://github.com/ad-freiburg/pfaedle "$tmp/src"
|
||||
cmake -S "$tmp/src" -B "$tmp/build" -DCMAKE_BUILD_TYPE=Release >/dev/null
|
||||
make -C "$tmp/build" -j"$(nproc)" >/dev/null
|
||||
make -C "$tmp/build" install >/dev/null
|
||||
rm -rf "$tmp"
|
||||
if ! command -v docker >/dev/null || ! docker compose version >/dev/null 2>&1; then
|
||||
echo "==> Docker installieren (offizielles Skript von get.docker.com)"
|
||||
apt-get update -qq
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl ca-certificates >/dev/null
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
fi
|
||||
|
||||
if ! command -v pmtiles >/dev/null; then
|
||||
echo "==> pmtiles-Werkzeug"
|
||||
url=$(curl -fsSL https://api.github.com/repos/protomaps/go-pmtiles/releases/latest | python3 -c \
|
||||
'import json,sys; print(next(a["browser_download_url"] for a in json.load(sys.stdin)["assets"] if a["name"].endswith("Linux_x86_64.tar.gz")))')
|
||||
curl -fsSL "$url" | tar xz -C /usr/local/bin pmtiles
|
||||
fi
|
||||
|
||||
if [ ! -s "$DATA/tiles/region.pmtiles" ]; then
|
||||
echo "==> Kartenkacheln (OpenStreetMap via Protomaps) für $BBOX"
|
||||
for i in 1 2 3 4 5 6 7; do
|
||||
d=$(date -d "-$i day" +%Y%m%d)
|
||||
if pmtiles extract "https://build.protomaps.com/$d.pmtiles" "$DATA/tiles/region.tmp.pmtiles" --bbox=$BBOX --maxzoom=15; then
|
||||
mv "$DATA/tiles/region.tmp.pmtiles" "$DATA/tiles/region.pmtiles"
|
||||
break
|
||||
fi
|
||||
done
|
||||
[ -s "$DATA/tiles/region.pmtiles" ] || { echo "Kartenkacheln konnten nicht geladen werden" >&2; exit 1; }
|
||||
fi
|
||||
|
||||
if [ ! -f /etc/trainpulse.env ]; then
|
||||
cat > /etc/trainpulse.env <<EOF
|
||||
TRAINPULSE_DATA=$DATA
|
||||
# API-Key von https://api-manager.opentransportdata.swiss (Produkt "GTFS-RT")
|
||||
TRAINPULSE_RT_KEY=
|
||||
if ! docker run --rm hello-world >/dev/null 2>&1; then
|
||||
cat >&2 <<'EOF'
|
||||
Docker ist installiert, kann aber keine Container starten.
|
||||
In einem Proxmox-LXC: Container herunterfahren, unter Optionen → Features
|
||||
"Nesting" und "keyctl" aktivieren, Container starten, Skript nochmals ausführen.
|
||||
EOF
|
||||
chmod 640 /etc/trainpulse.env
|
||||
chgrp trainpulse /etc/trainpulse.env
|
||||
exit 1
|
||||
fi
|
||||
chown -R trainpulse:trainpulse "$DATA"
|
||||
|
||||
echo "==> Dienste"
|
||||
cp "$SRC"/deploy/trainpulse.service "$SRC"/deploy/trainpulse-update.service "$SRC"/deploy/trainpulse-update.timer /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now trainpulse-update.timer
|
||||
systemctl enable trainpulse.service
|
||||
systemctl restart trainpulse.service
|
||||
systemctl start --no-block trainpulse-update.service
|
||||
[ -f .env ] || cp .env.example .env
|
||||
mkdir -p data
|
||||
|
||||
echo "==> Image bauen und starten (das erste Mal dauert der Bau von pfaedle ein paar Minuten)"
|
||||
docker compose up -d --build
|
||||
|
||||
ip=$(hostname -I | awk '{print $1}')
|
||||
cat <<EOF
|
||||
|
||||
Fertig. Die Karte läuft unter http://$ip:8080
|
||||
Der erste Fahrplan wird jetzt im Hintergrund geladen und aufbereitet (ca. 10–15 min).
|
||||
Fortschritt: journalctl -fu trainpulse-update
|
||||
Echtzeit: API-Key in /etc/trainpulse.env eintragen, dann systemctl restart trainpulse
|
||||
Der Updater lädt jetzt Kartenkacheln (~1,2 GB) und den Fahrplan und berechnet die
|
||||
Streckenverläufe. Bis die ersten Züge erscheinen, dauert es ca. 15 Minuten.
|
||||
Fortschritt: docker compose logs -f updater
|
||||
Echtzeit: API-Key in $(pwd)/.env eintragen, dann docker compose up -d
|
||||
EOF
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
[Unit]
|
||||
Description=TrainPulse: neuen Fahrplan holen und Streckenverläufe berechnen
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=trainpulse
|
||||
EnvironmentFile=/etc/trainpulse.env
|
||||
WorkingDirectory=/opt/trainpulse
|
||||
ExecStart=/opt/trainpulse/.venv/bin/python -m trainpulse.prepare
|
||||
Nice=10
|
||||
@@ -1,10 +0,0 @@
|
||||
[Unit]
|
||||
Description=TrainPulse: stündlich auf neuen Fahrplan prüfen
|
||||
|
||||
[Timer]
|
||||
OnBootSec=2min
|
||||
OnCalendar=*-*-* *:20:00
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -1,15 +0,0 @@
|
||||
[Unit]
|
||||
Description=TrainPulse Webserver
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
User=trainpulse
|
||||
EnvironmentFile=/etc/trainpulse.env
|
||||
WorkingDirectory=/opt/trainpulse
|
||||
ExecStart=/opt/trainpulse/.venv/bin/uvicorn trainpulse.server:app --host 0.0.0.0 --port 8080 --proxy-headers
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -13,4 +13,6 @@ RT_INTERVAL = 35 # Sekunden; Limit sind 2 Abfragen/Minute (Sliding Window)
|
||||
|
||||
OSM_URL = os.environ.get("TRAINPULSE_OSM_URL", "https://download.geofabrik.de/europe/switzerland-latest.osm.pbf")
|
||||
PFAEDLE = os.environ.get("TRAINPULSE_PFAEDLE", "pfaedle")
|
||||
PMTILES = os.environ.get("TRAINPULSE_PMTILES", "pmtiles")
|
||||
TILES_BBOX = "5.8,45.7,10.6,47.9" # Schweiz mit etwas Rand
|
||||
USER_AGENT = "trainpulse/0.1 (+https://scarriffle.com)"
|
||||
|
||||
@@ -201,6 +201,24 @@ def pack_shapes(d: Path) -> None:
|
||||
src.unlink()
|
||||
|
||||
|
||||
def ensure_tiles() -> None:
|
||||
"""Kartenkacheln (OpenStreetMap, von Protomaps) einmalig für die Region herunterladen."""
|
||||
dest = C.DATA_DIR / "tiles" / "region.pmtiles"
|
||||
if dest.exists() and dest.stat().st_size:
|
||||
return
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = dest.with_suffix(".tmp")
|
||||
for days in range(1, 8): # Protomaps baut täglich, ältere Builds werden gelöscht
|
||||
d = (dt.date.today() - dt.timedelta(days=days)).strftime("%Y%m%d")
|
||||
log.info("Lade Kartenkacheln vom %s …", d)
|
||||
r = subprocess.run([C.PMTILES, "extract", f"https://build.protomaps.com/{d}.pmtiles", str(tmp),
|
||||
f"--bbox={C.TILES_BBOX}", "--maxzoom=15"])
|
||||
if r.returncode == 0 and tmp.exists() and tmp.stat().st_size:
|
||||
tmp.rename(dest)
|
||||
return
|
||||
log.error("Kartenkacheln konnten nicht geladen werden – Karte bleibt ohne Hintergrund")
|
||||
|
||||
|
||||
def publish(src: Path, meta: dict) -> None:
|
||||
(src / "meta.json").write_text(json.dumps(meta))
|
||||
dest = C.FEEDS_DIR / meta["feed_version"]
|
||||
@@ -219,6 +237,8 @@ def main() -> None:
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||
|
||||
C.FEEDS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
if not args.zip:
|
||||
ensure_tiles()
|
||||
state_file = C.DATA_DIR / "state.json"
|
||||
state = json.loads(state_file.read_text()) if state_file.exists() else {}
|
||||
work = C.DATA_DIR / "work"
|
||||
|
||||
Reference in New Issue
Block a user