BikeLog-Dienst¶
Holt die Sitzungen des Fahrradcomputers automatisch ab, sobald er im WLAN auftaucht, bewahrt sie unverändert auf und liefert GPX/CSV daraus.
Eine Sitzung ist alles, was ein Boot des BC schreibt: L_ (Binärlog),
I_ (Kurzstatistik), D_ (Debug-Log), S_/R_ (IMU-Rohdaten), T_
(Zeit-Hinweise), N_ (Forumslader-NMEA) -- alle mit demselben Stamm
(_143012), siehe Logformat und CLI. Die Dateien liegen im Dienst
in derselben Struktur wie auf der SD-Karte:
data/sessions/<gerät>/20260920/L_143012.bin (Dateien direkt in /BIKECOMP: Ordner "_")
data/index.sqlite3 Index (Sitzungen, Dateien, Hashes, Kennzahlen)
Uhr-Sprünge innerhalb einer Sitzung (die GPS-Zeit des Handys war beim Start des BC falsch,
2026-10-04: aus 2 h Fahrt wurden 17 h mit einer Stunde vom Vorabend) werden beim Lesen
rückgängig gemacht: die T_-Datei nennt die Sprünge, die Sprünge der Zeitstempel zeigen, wo
sie waren, die letzte Uhr gilt
(bikelog.timefix).
Die Kurzstatistik zeigt dann time=repaired.
GPX, CSV usw. werden bei Bedarf aus den Rohdaten erzeugt -- eine bessere Exportlogik verbessert damit rückwirkend alle alten Fahrten.
Abholen (Pull)¶
Der BC stellt seine SD-Karte ohnehin per HTTP bereit (/log/<tag>/<datei>).
Der Dienst holt ab, statt dass die Firmware hochlädt: keine Zugangsdaten auf dem
Gerät, keine Wiederholungslogik in einem Task, der mit BLE und Display um internen
Heap konkurriert. Für ein Rad, das nicht in Reichweite des heimischen WLANs steht,
gibt es zusätzlich den Push.
Wann (puller.py):
- mDNS-Probes (Haupt-Trigger): Nach jedem WLAN-Connect ruft die Firmware
MDNS.begin("TRGB-BC")auf, und der ESP32 probt dabei seinen Namen (RFC 6762 8.1: Anfragen nachTRGB-BC.localmit Authority-Sektion). Das tut nur ein Gerät, das gerade (neu) ins Netz kommt -- also nach Boot oder WLAN-Reconnect, genau dann, wenn eine Sitzung fertig geworden ist. Ein eigener Socket lauscht passiv darauf; 10 s später wird abgeholt (BOOT_SETTLE_S). Gemessen 2026-09-27: vier Probes binnen einer Sekunde nach dem Connect. - Der ServiceBrowser allein reicht nicht: Er meldet nur Dienste, die er noch nicht im Cache hat, und nach einem schnellen Neustart ist der Eintrag noch da (PTR-TTL 75 min). So ist der erste Versuch am 2026-09-27 gescheitert.
- Polling als Netz: alle
BIKELOG_PULL_INTERVAL_S(120 s) eine mDNS-Adressanfrage -- ist der BC weg, kostet das nichts, taucht er (wieder) auf, wird abgeholt. Zusätzlich spätestens alle 10 min, solange er online ist (der BC beendet eine Sitzung, wenn die Fahrt-Session beendet wird, s. u.). - Nachfassen: Direkt nach dem Boot verschiebt
LogSessionsdie beendete Sitzung erst im Hintergrund ausCUR/. Liegt dort beim Abruf mehr als die laufende Sitzung (oder schlägt der Abruf fehl), wird nach 60 s erneut abgeholt, höchstens 10-mal in Folge. - Von Hand:
POST /api/v1/pulloderbikelogservice pull.
Was: jede Datei der Liste, die noch fehlt oder deren Größe sich geändert
hat -- außer CUR/ (laufende Sitzung) und gelöschten Sitzungen. Neue
Sitzungen entstehen auf dem BC beim Booten, beim bewussten Beenden der Fahrt (langer Druck)
oder mit rotate auf der seriellen Konsole -- nicht beim WLAN-Connect, der auch per
Handy-Hotspot mitten in einer Tour vorkommt. Ein Abruf ohne Neues ist eine einzige Anfrage. Die Liste kommt aus /logfiles.json, falls die Firmware den
Endpunkt hat, sonst aus der HTML-Seite /logfiles/ (dort ohne Größen: eine
bekannte Datei wird dann nicht neu geholt).
Messwert 2026-09-27: Erstabruf 390 Dateien / 23 MB in 3,5 min (≈110 kB/s, der ESP32 bremst), Folgeabruf 2 s.
Löschen entfernt die Dateien, lässt aber einen Grabstein im Index -- sonst
käme die Sitzung beim nächsten Abruf von der SD-Karte zurück. Ein
ausdrücklicher PUT holt sie zurück.
Hochladen vom Fahrradcomputer (Push)¶
Der Pull braucht den Fahrradcomputer im heimischen WLAN. Ein Rad, das woanders steht (Arbeit, Keller ohne Empfang), kann stattdessen hochladen: Sobald es online ist, schickt es jede fertige Sitzungsdatei per WebDAV in einen Nextcloud-Ordner, und der Dienst holt sie dort ab. Push ergänzt den Pull, ersetzt ihn nicht -- beide können für dasselbe Gerät laufen:
- Die Dateien bleiben unverändert auf der SD-Karte, der Pull findet sie weiterhin.
- Der Dienst legt eine hochgeladene Datei über dasselbe
put_file()ab wie eine abgeholte: dieselben Bytes noch einmal sindunchanged, nichts liegt doppelt. - Eine im Dienst gelöschte oder archivierte Sitzung kommt durch einen Push nicht zurück (der
Eingang verwirft ihre Dateien; ein direkter Upload trägt den Header
X-BikeLog-Auto: 1, siehe API).
Fahrradcomputer (LogPush.cpp,
Seite /push, verlinkt von der Startseite und von /wifi):
| Ziel | URL | User / Password |
|---|---|---|
| Upload-Link (am einfachsten) | der Freigabe-Link eines als „Dateien ablegen“ (File drop) geteilten Ordners, https://<cloud>/s/<token> |
leer / das Freigabe-Passwort, falls eins gesetzt ist |
| WebDAV-Ordner mit Konto | https://<cloud>/remote.php/dav/files/<nutzer>/BikeLog-Inbox/<netzwerkname> |
Nextcloud-Nutzer / ein eigenes App-Passwort (Einstellungen -> Sicherheit -> „Geräte & Sitzungen“), allein widerrufbar |
Ein Upload-Link braucht kein Konto auf dem Fahrradcomputer. Die Firmware macht daraus die
WebDAV-Seite (/public.php/webdav, der Token als Nutzer). Ein File Drop nimmt nur PUT und behält
nur den Dateinamen (Nextcloud 31, FilesDropPlugin), deshalb kommen die Dateien flach als
<netzwerkname>~<tag>~<datei> an (TRGB-FL~20261018~L_143012.bin, eine Datei direkt in
/BIKECOMP: TRGB-FL~~L0042.bin); bei einer „Dateianfrage“ legt Nextcloud sie in einen Ordner je
Nickname (die Firmware schickt ihren Netzwerknamen). Ein schon vorhandener Name wird nicht ersetzt,
sondern als ... (2).bin abgelegt -- der Dienst nimmt solche Kopien älteste zuerst. Mit Konto
werden fehlende Ordner angelegt (MKCOL), und die Tagesordner der Karte bleiben erhalten. Test
target prüft URL und Zugang (bei einem Upload-Link schreibt er <netzwerkname>~push-test.txt, das
der Dienst löscht), Upload now
wartet nicht auf die nächste Runde, Mark all as uploaded trägt ein, was auf der Karte liegt,
ohne es hochzuladen -- für eine Karte, deren Sitzungen der Dienst schon per Pull hat. Das
Passwort kommt nur über das Formular herein, wird nie wieder angezeigt und nie geloggt.
Wann: 20 s nach jedem WLAN-Connect, danach alle 10 min, solange er online ist (eine mit langem
Druck beendete Fahrt erscheint ohne Reconnect), nach einem Fehler nach 5 min. Nur fertige
Sitzungen, nie CUR/. Was hochgeladen ist, steht in /bcpush.txt auf der SD-Karte
(<pfad> <größe>); eine Datei geht nur dann noch einmal hoch, wenn sich ihre Größe ändert (neues
Format der Kurzstatistik). Die Datei löschen lädt alles neu hoch. HTTPS wird gegen die gängigen
Stamm-Zertifizierungsstellen geprüft (Let's Encrypt und die großen CAs: ESP-IDFs eigenes
Bundle der gängigen CAs, CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN, erneuert sich mit jedem
IDF-Update); ein selbst signiertes Zertifikat geht nicht. Serielle Konsole:
push [status|now|test|mark|on|off].
Das WLAN selbst folgt seinen eigenen Regeln (WLAN): Das Netz muss eingetragen sein, und ohne Netz in Reichweite geht es 5 Minuten nach dem Start aus. Der Upload läuft auch über einen Handy-Hotspot mitten in der Tour; er schickt nur fertige Sitzungen, ein paar hundert KB pro Fahrt.
Dienst (inbox.py):
braucht den Nextcloud-Zugang des Syncs und den Eingangsordner:
BIKELOG_NEXTCLOUD_INBOX=BikeLog-Inbox
BIKELOG_NEXTCLOUD_INBOX ist der Ordner hinter dem Upload-Link (bzw. über den Geräteordnern),
vom Nextcloud-Konto des Dienstes aus gesehen. Er schaut alle BIKELOG_INBOX_INTERVAL_S (5 min) in den Eingang, legt jede Datei ab und löscht
sie dort; ein geleerter Tagesordner verschwindet, sobald eine Stunde nichts mehr hineingeschrieben
wurde. Der Ordner unter dem Eingang ist der Netzwerkname des Fahrradcomputers;
BIKELOG_PULL_TARGETS ordnet ihn dem Gerätenamen zu (pendler=TRGB-FL legt
BikeLog-Inbox/TRGB-FL/... unter pendler ab, dieselben Sitzungen wie der Pull von
TRGB-FL.local), ein Name ohne Ziel wird so übernommen. Von Hand: POST /api/v1/inbox oder
bikelogservice inbox.
Ohne Nextcloud kann der Fahrradcomputer auch direkt zum Dienst hochladen, wenn er ihn erreicht
(VPN, Portfreigabe): Folder URL http://<server>:8080/api/v1/devices/<gerät>/files, User leer,
als Passwort ein Token aus BIKELOG_TOKENS, falls Auth an ist.
GPX-Export¶
Zu jeder Sitzung mit Binärlog schreibt der Dienst automatisch eine Datei
unter data/export/gpx/ (lokale Startzeit + Gerät; mtime = Start der
Fahrt) -- als Übergabestelle für Nextcloud-Sync, Komoot/Strava usw.
(exporter.py):
Tours/-- echte Fahrten, mindestensMIN_EXPORT_DISTANCE_M(1 km) reale Bewegung (GPS-Sprünge rund um einen Standpunkt zählen nicht mit, siehebikelog.gpx.GpxStats.real_distance_m).Debug_Archive/-- alles andere mit verwertbarem GPS-Fix: kurze Testfahrten, ein am Rollentrainer stehendes Rad mit jitterndem Telefon-GPS, und jede Testfahrt unabhängig von der Länge (gpx_statustest). Nichts geht verloren, es landet nur nicht in Nextcloud/Strava/Komoot.
Sitzungen ganz ohne verwertbaren GPS-Fix bekommen gar keine Datei
(gpx_status no-gps), unlesbare Logs error: ….
Neu geschrieben (und ggf. zwischen den beiden Ordnern verschoben) wird, wenn
L_ oder I_ der Sitzung ankommt oder sich ändert, und für alle
Sitzungen, wenn EXPORT_VERSION erhöht wird -- das bei jeder inhaltlichen
Änderung am GPX tun, dann bekommen auch alte Fahrten die bessere Fassung.
Vor dem Export läuft bikelog/sanitize.py über
die Rohdaten (per Default an, GpxOptions.sanitize bzw. ?sanitize=false
am Download): der GPS-Fix wandert im Stand (Ampel, Pause, vor der ersten
Umdrehung) ein paar Meter um den Standpunkt -- sanitize erkennt das am
Radsensor (speed < MOVING_KMH) und lässt von jedem Stand nur den einen Fix
direkt an der Pause übrig, statt eines Gekritzels auf der Karte.
Inhalt (Details in bikelog/gpx.py):
| Wo | Was |
|---|---|
<metadata> |
Name, Beschreibung mit Kennzahlen (Strecke, Fahrzeit, Ø/max., Höhenmeter, Puls, Trittfrequenz, Temperatur, Stöße, Wegequalität, Labels), Link zur Sitzung (BIKELOG_PUBLIC_URL), Bounds |
<metadata><extensions><bc:Ride> |
dieselben Kennzahlen maschinenlesbar, Strecke je Wegeklasse und je Label, dazu die I_-Statistik des Geräts unverändert (bc:deviceSummary) |
| Trackpunkt, Garmin TPX v2 | atemp, hr, cad, speed, course -- das lesen Strava, Komoot, Garmin Connect |
Trackpunkt, bc:TrackPoint |
Trip-Distanz, Radgeschwindigkeit, Steigung (Anzeige/Baro/IMU), Baro- und GPS-Höhe, GPS-Genauigkeit und Fix-Alter, Wegeklasse + OSM-smoothness, Rauheit und RMS des Wegequalitäts-Intervalls, manuelles Label |
<wpt> |
Stöße (sym „Danger Area“, Details in bc:Shock) und Label-Wechsel (sym „Flag, Blue“) |
bc = https://github.com/euphi/TRGB-BikeComputer/gpx/v1. Viewer
überspringen unbekannte Erweiterungen; das GPX validiert gegen gpx.xsd,
die TPX-Elemente gegen TrackPointExtensionv2.xsd. Schlank ohne bc::
GET /api/v1/sessions/{id}.gpx?rich=false bzw. bikelog gpx --plain.
Von Hand: bikelogservice export (fehlende/veraltete), export --all (alle).
Komoot-Upload¶
Nie automatisch -- aber die Webseite fragt: Jede fertige, echte Fahrt, die weder
hochgeladen noch abgelehnt ist, erscheint oben in einem Kasten „Neue Fahrt bereit -- zu
Komoot hochladen?“ mit den Knöpfen Zu Komoot hochladen und Nicht hochladen. Die Frage
kommt bei jedem Aufruf wieder, bis einer der beiden gedrückt wird (Seite schließen
beantwortet nichts); die Ablehnung wird gespeichert (komoot_prompt = ignored, je
zusammengefasster Tour) und lässt sich mit
POST /api/v1/sessions/{id}/komoot/ignore?ask_again=true zurücknehmen. Ein fehlgeschlagener
Upload fragt weiter. Sitzungen, die bei der Einführung (Schema 7) schon da waren, werden
nicht gefragt; ihr „Komoot“-Knopf in der Tabelle bleibt.
Solange Dateien geholt werden, zeigt die Seite einen Fortschrittskasten (Datei n von m, MB bisher, danach „Verarbeite“: GPX-Export, Nextcloud) und lädt sich alle 3 s neu; das Journal nennt jede Datei, die Zahl der zu holenden Sitzungen und jeden GPX-Export.
POST /api/v1/sessions/{id}/komoot lädt die Fahrt über
kompy zu Komoot hoch (oder, in der
Web-Oberfläche, der „Komoot“-Knopf neben jeder echten Fahrt). Ein Reboot
mitten in einer Fahrt (kurzer BLE-Aussetzer, Pause mit ausgeschaltetem BC)
erzeugt mehrere Sitzungen für eine Tour -- Sitzungen desselben Geräts mit
höchstens BIKELOG_KOMOOT_MERGE_GAP_S Pause dazwischen werden deshalb zu
einer Fahrt zusammengefasst und als ein GPX hochgeladen
(komoot.py); alle beteiligten Sitzungen zeigen
danach denselben komoot_status. Ein zweiter Versuch ohne ?force=true
wird mit 409 abgelehnt.
Braucht pip install "bikelog[komoot]" (kompy + gpxpy, nicht Teil von
[service]) und BIKELOG_KOMOOT_EMAIL/BIKELOG_KOMOOT_PASSWORD in
bikelog.env -- ohne beides antwortet der Endpunkt mit 409
("not-configured"), sonst bleibt alles wie gehabt. kompy meldet nach dem
Upload keine Tour-ID zurück (Komoot-API-Limitation), daher kein direkter
Link auf die neue Tour -- nur der Status (uploaded/error).
Das hochgeladene GPX hat -- anders als der normale Export -- keine
<wpt>-Wegpunkte (Stöße, Label-Wechsel): Komoots Import-Endpunkt lehnt jede
Datei mit <wpt> ab (400 query is required for type=tour_planned, gegen
die echte API am 2026-09-27 verifiziert -- vermutlich hält deren Parser das
allein deswegen für eine geplante Route statt einer Aufzeichnung). Die
reichen bc:-Erweiterungen je Trackpunkt sind davon nicht betroffen und
bleiben drin.
Nextcloud-Sync¶
Anders als Komoot: läuft automatisch, kein Knopf, kein Endpunkt. Jede
Sitzung mit gpx_status ok (also in Tours/, siehe oben) wird per WebDAV
in ein konfigurierbares Verzeichnis auf einer Nextcloud-Instanz hochgeladen
(nextcloud.py); wird eine Sitzung nachträglich
als nicht mehr „echt“ eingestuft (Export-Logik verbessert, EXPORT_VERSION
erhöht), wird ihre Nextcloud-Kopie wieder gelöscht statt verwaist
liegenzubleiben. Debug_Archive-Sitzungen werden nie synchronisiert.
Braucht pip install "bikelog[nextcloud]" (requests, nicht Teil von
[service]) und in bikelog.env:
BIKELOG_NEXTCLOUD_URL=https://cloud.example.com
BIKELOG_NEXTCLOUD_USER=bikelog
BIKELOG_NEXTCLOUD_PASSWORD=<App-Passwort>
BIKELOG_NEXTCLOUD_DIR=BikeLog # Standard, frei wählbar
Das Passwort ist ein App-Passwort (Nextcloud: Einstellungen -> Sicherheit -> „Geräte & Sitzungen“ -> neues App-Passwort anlegen), nicht das normale Login-Passwort -- eigenständig widerrufbar, ohne den Hauptzugang zu berühren. Ohne URL/Nutzer/Passwort bleibt der Sync einfach aus, sonst wie gehabt.
Bekannte Lücke: Wird eine Sitzung gelöscht (DELETE /api/v1/sessions/{id}),
verschwindet nur die lokale Kopie -- die Nextcloud-Kopie bleibt liegen und
muss von Hand entfernt werden.
Testfahrten¶
Sitzungen mit emulierten Daten werden erkannt und von den Fahrten getrennt
(bikelog/testride.py):
| Markierung | Erkannt an |
|---|---|
| Simuliert | LOG_SIMULATED im Log: der Sensor-Simulator (sim auf der seriellen Konsole, Simulator-Build) oder eine TrailBridge-Testfahrt (SIM_FLAGS, Firmware seit 2.10.2026) |
| GPS-Wiedergabe? | ältere TrailBridge-Testfahrten ohne Flag: GPS legt mindestens 500 m zurück, der Radsensor weniger als 15 % davon. Eine echte Fahrt mit ausgefallenem Radsensor sieht genauso aus, daher das Fragezeichen |
Testfahrten sind in der Fahrtenliste (und in GET /api/v1/sessions) ausgeblendet, der
Filter sagt, wie viele; „Testfahrten zeigen" blendet sie mit ihrer Markierung ein. Sie zählen
nicht im Training, bei den Anstiegen und Zielen, landen in Debug_Archive/ und werden nie
synchronisiert oder für Komoot angeboten. Die Fahrtseite sagt, warum eine Sitzung als Test
gilt, und hat einen Knopf, um die Erkennung in beide Richtungen zu überstimmen („Doch eine
echte Fahrt", „Als Testfahrt markieren"; API: POST /api/v1/sessions/{id}/test?mark=test|real|auto).
Leerlauf-Sitzungen und Archiv¶
Jeder Start des Fahrradcomputers ist eine Sitzung, auch wenn er nur zu Hause an war. Eine
Sitzung, in der das Rad stand und die Position nirgendwohin kam (Rad < 50 m, alle GPS-Fixe
innerhalb von 300 m), oder die gar kein oder nur ein leeres Binärlog hat (nur Nullbytes),
ist Leerlauf
(bikelog/testride.py).
Ein Log in unbekanntem Format bleibt dagegen mit „Log unlesbar" in der Liste (der Hover-Text
nennt das gefundene Versions-Byte) und wird für den Bericht nicht erneut versucht.
Leerlauf-Sitzungen erscheinen nie in der Fahrtenliste (sie verweist darauf: „N
Leerlauf-Sitzungen im Archiv") oder in GET /api/v1/sessions (idle=true schließt sie ein),
bekommen kein GPX und zählen nirgends.
BIKELOG_IDLE_ARCHIVE_DAYS (Standard 7) Tage nach dem Abholen wandern ihre Dateien von
<data>/sessions/ nach <data>/archive/ (gleicher Baum) -- Debug-Logs bleiben mit der Shell
erreichbar
(archive.py).
Der Index behält einen Grabstein, solange die Dateien noch auf der SD-Karte liegen, sonst
holt der nächste Abruf sie wieder; der Abruf entfernt ihn, sobald sie aus der Dateiliste der
Karte verschwunden sind (das gilt auch für gelöschte Sitzungen).
Die Seite Archiv (/archive) zeigt die noch nicht archivierten Leerlauf-Sitzungen und
die archivierten, die noch auf der Karte liegen. „Auf dem BC löschen" (eine Sitzung oder alle)
löscht ihre Dateien sofort auf dem Fahrradcomputer über den /del/-Endpunkt der Firmware --
nur solange er erreichbar ist, es wird nichts vorgemerkt. Eine Sitzung, deren Dateien alle von
der Karte sind, wandert ins Archiv und verschwindet aus dem Index. Eine Datei, die nicht mehr
auf der Karte liegt, gilt als gelöscht: Die Firmware antwortet dafür mit 404, mit 403 nur
für Dateien der laufenden Sitzung oder eine gerade hochgeladene (ältere Firmware antwortet
auch für eine fehlende Datei mit 403 -- dann entscheidet die Dateiliste des Geräts).
Leerlauf-Sitzungen, die am Fahrradcomputer selbst gelöscht wurden (etwa mit Cleanup),
archiviert der nächste Abruf ohne Löschanfrage; Fahrten, die von der Karte verschwunden sind,
bleiben im Dienst. API:
POST /api/v1/archive/device-delete (session_id, ohne = alle; 409, wenn das Gerät nicht
erreichbar ist).
Räder¶
Die Seite Räder (/bikes) führt die Räder -- Name, Typ, Gewicht fahrbereit, CdA, Crr --
und welcher Fahrradcomputer ab welchem Tag an welchem Rad fährt
(bikelog/bikes.py,
gespeichert in <data>/bikes.json). Eine Sitzung bekommt das Rad, dem ihr Gerät am Fahrtag
zugeordnet war; auf der Fahrtseite kann eine einzelne Fahrt ein anderes Rad bekommen. Die
Leistungsschätzung des Berichts nimmt dann Körpergewicht (Seite Fahrer) + Radgewicht und
CdA/Crr des Rads; Fahrten ohne Rad behalten die Standardwerte der Fahrer-Seite. Eine Änderung an
Rad oder Zuordnung berechnet die betroffenen Berichte neu. Die Seite zeigt die Kilometer je Rad.
Fahrten importieren (GPX)¶
„GPX-Fahrten importieren" in der Fahrtenliste nimmt anderswo aufgezeichnete Fahrten (Garmin,
Strava, Komoot …): GPX mit Zeiten; Puls, Trittfrequenz und Temperatur aus Garmins
TrackPointExtension werden übernommen, die Steigung über ±50 m geglättet
(bikelog/gpximport.py).
Eine importierte Fahrt ist eine Sitzung des Geräts import (das daraus gebaute Log neben der
Original-GPX) und zählt wie jede andere Fahrt -- Bericht, Training, Anstiege --, wird aber weder
exportiert noch hochgeladen. Optional mit Rad und als frühere Teilnahme an einem Ziel: Das
Ziel zeigt dann „Deine bisherigen Teilnahmen" mit Zeit, Tempo, Puls, Leistung und TRIMP. Dieselbe
Datei noch einmal ersetzt die Sitzung. API: PUT /api/v1/import/gpx (bike_id, event_id,
Body = die Datei).
Geplante und regelmäßige Strecken¶
Die Seite Strecken (/routes) hält Strecken zum Fahren: geplante (eine anstehende
Tour) und regelmäßige (Pendelstrecke, Feierabendrunde). Eine oder mehrere GPX-Dateien
hochladen; jede wird darauf geprüft, was die Navigation von TrailBridge daraus macht, und zwar
so gelesen wie der GpxParser der App
(bikelog/routecheck.py):
| Prüfung | Was sie sagt |
|---|---|
| Geometrie | Track (trk) oder nur Routenpunkte (rte: dazwischen kennt TrailBridge den Weg nicht, Warnung); Punkte ohne gültige Koordinate, weniger als zwei Punkte, unter 100 m, kein XML/GPX, ein DOCTYPE oder über 20 MB = nicht nutzbar |
| Abbiegehinweise | aus OsmAnds Routensegmenten, aus den Routenpunkten (rtept mit Abbiegen) oder -- keine in der Datei oder nur Start/Zwischenziel/Ziel wie bei bikerouter.de -- von der App aus den Winkeln des Tracks abgeleitet (ohne Straßennamen) |
| Höhen | ohne <ele> an (fast) jedem Punkt kein Höhenprofil und keine Anstiege auf dem Fahrradcomputer (Warnung) |
| Lücken | Punkte mehr als 2 km auseinander (vereinfachter Track) oder Löcher über 200 m zwischen Track-Segmenten (Warnung) |
| Zeiten | Zeitstempel in der Datei: Die Restzeit kommt aus der geplanten Fahrzeit |
| Wegpunkte | wpt bis 100 m neben der Strecke erscheinen in der Streckenübersicht, die anderen werden gezählt |
Die Liste ist kurz: oben die aktuellen (geplanten) Strecken, darunter aufklappbar die
regelmäßigen (offen) und das Archiv (zu) -- je Zeile Name, Tags, Distanz, Höhenmeter und
ein kleines Höhenprofil (gleicher Maßstab in jeder Zeile bis 300 m Höhenunterschied, eine flache
Strecke bleibt flach, am Handy ausgeblendet) und ein Knopf In TrailBridge laden (bei der
bereitgestellten Strecke steht dort, ob sie schon auf dem Handy ist); das Prüfurteil steht nur
da, wenn etwas nicht stimmt. Darüber eine Zeile, welche Strecke TrailBridge hat und wann die
App zuletzt gefragt hat, aufklappbar mit der Einrichtung. Ein Klick auf die
Strecke öffnet ihre Seite (/routes/{id}) mit Distanz, Höhenmeter (mit Min./Max.), den Anstiegen
mit Kategorie (dieselben Kriterien wie im Fahrtbericht), geplanter Zeit und Geschwindigkeit, wenn
die Datei Zeiten hat, dem Höhenprofil (Höhenachse über mindestens 200 m, wie bei allen Höhenprofilen des Dienstes)
und den Befunden. Dort lässt sich eine Strecke umbenennen, anders einsortieren, mit den Tags
Pendel/Alltag und Training versehen, archivieren (das nimmt sie TrailBridge weg), durch
eine neue Version ihrer GPX ersetzen, herunterladen und löschen. Eine
Datei, die TrailBridge gar nicht lesen kann, wird abgelehnt und nicht aufbewahrt. Ablage in
<data>/routes.json, die Dateien unverändert in <data>/routes/
(routes.py); ändert sich die Prüfung selbst,
wird CHECK_VERSION erhöht, und der Dienst prüft die gespeicherten Strecken beim Start neu.
Wegpunkte (<wpt> in der GPX, z. B. in BRouter-Web per Rechtsklick benannt): Die Liste zeigt
die benannten nur als schmale senkrechte Striche im kleinen Höhenprofil, an ihrer Distanz ab
Start. Die Seite der Strecke zeigt eine Karte (die Strecke als Linie, ohne Kartenkacheln) und das
Höhenprofil mit jedem Wegpunkt markiert und benannt, darunter eine Tabelle mit Name, Distanz ab
Start und bis zum Ziel. Ein wpt mit type (from, via, to) liegt auf einem Routenpunkt;
einer ohne (ein Ort von Interesse) kann neben der Strecke liegen, seine Distanz ist deshalb die
seiner Projektion auf den Track (ein Hinweis nennt den Abstand; weiter als 1 km entfernt wird er
weggelassen). Die Maschinennamen von BRouter-Web (from, via1, to) sind Start, Ziel und
unbenannte Zwischenpunkte: in der Tabelle ausgegraut, ohne Strich in der Liste, ohne Namen in den
Diagrammen. Die OsmAnd-Abbiegehinweise im <rte> sind keine Wegpunkte. Namen werden für HTML
maskiert.
Zurück zum Planer: Die URL, die BRouter-Web oder bikerouter.de in die GPX schreibt (trk/link mit
lonlats= im Fragment, nur http(s)), wird auf der Seite der Strecke zum Knopf In BRouter bearbeiten.
Er hängt ?bikelog=<id>&bikelogname=<name> an: Ein entsprechend angepasstes BRouter-Web bietet dann im
Export-Dialog an, diese Strecke zu aktualisieren (PUT /api/v1/routes/{id}/gpx; Name, Tags und ID
bleiben, die alte Datei wird überschrieben, eine für TrailBridge bereitgestellte Strecke lädt die App neu).
Ein bikerouter.de-Link öffnet nur dort (In bikerouter.de öffnen).
Strecken für TrailBridge¶
„Für TrailBridge bereitstellen" markiert eine Strecke (eine andere zu markieren nimmt die Markierung weg). Die App holt sie sich selbst. Vertrag zwischen Dienst und App:
- Einrichten in der App: nur die Adresse des Dienstes (
BIKELOG_PUBLIC_URL, z. B.https://bikelog.example.org). Kein Passwort: Wie die Seiten begrenzt den Abruf das Netz -- der Dienst ist nur aus dem Heimnetz und dem VPN erreichbar (Reverse Proxy mitallow/deny). WirdBIKELOG_REQUIRE_AUTHeingeschaltet, braucht der Abruf wie jeder andere Pfad ein Token. - Wann die App fragt: beim Start, danach alle 15 Minuten -- aber nur, solange sie weder navigiert noch eine Testfahrt abspielt (eine geladene Route beendet beides).
GET /api/v1/trailbridge/route--204keine Strecke markiert;200JSON{id, name, kind, revision, marked_at, sha256, size_bytes, distance_km, ascent_m, verdict, gpx_url}mitETag: "<revision>"; mitIf-None-Match: "<revision>"304.gpx_urlist ein Pfad relativ zum Dienst.- Neu = andere Revision. Die Revision ändert sich mit der Datei (neue Version hochgeladen) und mit jeder Markierung -- dieselbe Strecke noch einmal zu markieren schickt sie noch einmal. Die App merkt sich die zuletzt geladene Revision und tut nichts, solange sie gleich bleibt.
GET /api/v1/trailbridge/routes/{id}.gpx-- die Datei unverändert (X-Bikelog-Revision). Die App lädt sie wie eine von Hand geöffnete GPX (startet die Navigation nicht).POST /api/v1/trailbridge/route/fetched?revision=<revision>-- nachdem die App sie fehlerfrei gelesen hat. Die Seite zeigt dann „Von TrailBridge geladen" mit Uhrzeit;409, wenn die Revision nicht mehr aktuell ist (die nächste Abfrage bringt die neue).
Die Seite zeigt außerdem, wann die App zuletzt gefragt hat (seit dem Start des Dienstes).
Sitzungsbericht¶
/ride/{id} (Klick auf eine Fahrt in der Liste) zeigt den Bericht einer Sitzung: Kennzahlen,
Höhenprofil mit den Anstiegen, Pulszonen, geschätzte Leistung, Wegequalität und Stöße,
technische Auffälligkeiten. GET /api/v1/sessions/{id}/report.md ist dasselbe als Markdown,
…/report.json die Zahlen dahinter. Alles kommt aus
bikelog/report.py
(Details: Werkzeuge).
Die Berichte liegen in der Tabelle reports des Index, unter einem Schlüssel aus
Berichtsversion, Hash der L_-Datei und Fahrerdaten. Nach dem Abholen (und beim Start)
berechnet der Dienst, was fehlt oder veraltet ist, mit niedrigster CPU-Priorität
(analysis.py).
Eine neue Berichtsversion oder geänderte Fahrerdaten erreichen so von selbst alle alten Fahrten.
Wind. Leistungsschätzung und aerobe Entkopplung rechnen den Wind der Fahrt ein
(Details). Der Dienst fragt bei Open-Meteo den Stundenwind für den
Fahrtag und die 0,1°-Zelle ab -- nichts sonst verlässt den Rechner, die Spur nie -- und hebt
jede Antwort unter <data>/weather/ auf (2026-10-24_49.4_10.9.json); ein neu gerechneter
Bericht braucht so kein Netz und liefert dasselbe Ergebnis. Die Fahrtseite zeigt Ø Wind, Böen,
den Anteil von Gegen- und Rückenwind und neben der Leistung den Wert ohne Wind. Wo die
Entkopplung nur den Wind widerspiegeln würde (hin dagegen, zurück damit), steht das da statt
einer Zahl. Ist Open-Meteo nicht erreichbar oder hat noch keine Daten, wird der Bericht ohne
Wind gerechnet (die Seite sagt warum) und eine Stunde später neu versucht; Tage aus der
„Historical Forecast“-API oder jünger als eine Woche werden erneut aus dem Archiv geholt,
sobald es endgültig ist (wind.retry_after im Berichts-JSON). BIKELOG_WIND=0 schaltet die
Abfrage ab; der Faktor vom 10-m-Wind zum Wind am Fahrer (Standard 0,55) ist wind_factor auf
der Seite Fahrer. Abschattung (Wald, Bebauung) wird nicht modelliert.
Leerlauf. Zeit vor der ersten und nach der letzten wirklichen Bewegung (Gerät in der Garage eingeschaltet, zu Hause weitergelaufen) ist kein Stopp und zählt weder zur Dauer noch zu Temperatur oder GPS-Statistik; Stopps mitten in der Fahrt bleiben. Höhenänderungen im Stand (Fahrstuhlfahrt mit dem Rad) zählen nicht als Steigen.
Fahrerdaten für Pulszonen, TRIMP und W/kg werden auf der Seite Fahrer (/athlete)
eingetragen und landen in <data>/athlete.json (anderer Pfad: BIKELOG_ATHLETE_FILE).
Jeder Schlüssel ist optional; ohne die Datei hat der Bericht einfach keine Zonen:
{"hr_max": 186, "hr_rest": 48, "mass_kg": 88, "rider_kg": 76, "cda": 0.38, "crr": 0.006}
Das JSON ist als Eingabe für einen Textgenerator gedacht (Vorlage oder lokales LLM): Alle Zahlen werden dort berechnet, ein Modell muss sie nur noch in Worte fassen -- siehe die Fahrt in Worten.
Die Fahrt in Worten (lokales LLM)¶
Mit BIKELOG_LLM_URL (ein Ollama-Server, z. B.
http://localhost:11434) bekommt die Fahrtseite einen Abschnitt In Worten: zwei, drei
Absätze dazu, was für eine Fahrt das war, wie Belastung und Puls zu den letzten Fahrten
stehen, ob das zur Trainingsphase des nächsten Ziels passt, und technische Warnungen. Alles
bleibt auf dem Heimserver.
- Das Modell fasst nur Zahlen in Worte. Der Prompt
(
bikelog/narrate.py) gibt ihm beschriftete Zeilen mit ausgeschriebenen Einheiten und ihrer Bedeutung („Höhenmeter bergauf: 480 m“, „Form (TSB): -12 (negativ = ermüdet)“), dazu den Vergleich mit den Fahrten der 42 Tage davor, die Belastung am Morgen der Fahrt und das nächste Ziel mit seiner Phase -- keine Abkürzungen, nichts zu rechnen. (Im ersten Test las qwen3:8b „480 hm“ als Hektometer und zählte sie zur Strecke.) - Jede Zahl der Antwort, die nicht im Prompt steht (Rundung erlaubt), steht unter dem Text: kein Beweis für einen Fehler, ein Grund hinzusehen.
- Das Denken ist abgeschaltet (
"think": false): qwen3 überlegt sonst erst mehrere hundert Tokens lang -- auf der CPU Minuten. - Die Texte entstehen im Hintergrund nach den Berichten, neueste Fahrt zuerst, einer nach dem
anderen (
llm.py), und liegen in der Tabellenarratives. Eine Fahrt ohne Text bekommt immer einen; ändern sich ihre Fakten (neues Ziel, anderes Rad, neuer Prompt), wird er nur für Fahrten der letztenBIKELOG_LLM_REFRESH_DAYSneu geschrieben. Neu schreiben auf der Seite schreibt ihn neu, vor allen anderen. Testfahrten und Fahrten unter 1 km bekommen keinen.
Auf dem Orange Pi 5 (16 GB, nur CPU) schreibt qwen3:8b etwa 2,7 Tokens/s, ein Text dauert
2--4 Minuten. Ein 14B-Modell braucht beim Laden etwa 9 GB (Ollama packt die Gewichte für die
ARM-CPU um) -- mit anderen Diensten auf der Maschine endet das beim OOM-Killer. Empfohlene
Ollama-Einstellungen (sudo systemctl edit ollama):
[Service]
Nice=10
OOMScoreAdjust=500
Environment=OLLAMA_NUM_PARALLEL=1
Environment=OLLAMA_CONTEXT_LENGTH=4096
Environment=OLLAMA_FLASH_ATTENTION=1
Environment=OLLAMA_KV_CACHE_TYPE=q8_0
Training, Ziele, Anstiege¶
Die Webseiten sind im Rim-&-Ridge-Design des Fahrradcomputers gestaltet (Design-System). Neben der Fahrtenliste und dem Fahrtbericht gibt es:
| Seite | Inhalt |
|---|---|
Training (/training) |
Fitness (CTL, 42-Tage-Mittel des TRIMP), Ermüdung (ATL, 7 Tage) und Form (TSB) im Verlauf; Kilometer und Stunden je Pulszone pro Woche; Wochentabelle. Braucht hr_max, Fahrten ohne Puls zählen als 0 |
Ziele (/goals) |
Zielrennen mit Datum und Priorität (A/B/C), Countdown und Trainingsphase (Grundlage, Aufbau, Spitze, Tapering); die Strecke als GPX hochladen (Knopf im Ziel, oder gleich im Formular für ein neues) für Distanz, Höhenmeter und Anstiege. Verglichen mit den letzten 6 Wochen: längste Fahrt gegen die Renndistanz, meiste Wochenhöhenmeter gegen die des Rennens; für jeden Anstieg der Strecke eine geschätzte Zeit aus der besten VAM auf vergleichbaren Anstiegen der letzten 90 Tage |
Anstiege (/climbs) |
mehrmals gefahrene Anstiege (Fuß und Gipfel höchstens 150 m auseinander, braucht GPS), jede Fahrt mit Zeit, Abstand zur besten, VAM, Puls, Leistung |
Fahrer (/athlete) |
Fahrerdaten, siehe oben |
Code: bikelog/training.py
(rein, arbeitet nur auf dem Bericht-JSON, Tests tests/test_training.py), Seiten in
webui.py
und charts.py
(SVG auf dem Server, keine JS-Bibliothek), Tests tests/test_pages.py. Die Zielrennen liegen in
<data>/events.json, ihre GPX-Dateien in <data>/events/.
Eine Fahrt, mehrere Sitzungen¶
Startet der Fahrradcomputer unterwegs neu, wird aus einer Fahrt mehrere Sitzungen.
Sitzungen desselben Geräts, deren Abstand (Ende der einen bis Start der nächsten) höchstens
BIKELOG_KOMOOT_MERGE_GAP_S (30 min) beträgt, sind eine Fahrt -- dieselbe Gruppierung wie
für den Komoot-Upload
(tours.py).
Training, Ziele und Anstiege zählen die Fahrt, ihr Bericht läuft über die verbundenen Daten
(die Strecke läuft weiter, der Neustart ist ein Stopp). Die Fahrtenliste zeigt eine Fahrt aus
mehreren Teilen als eine Zeile mit den Summen der ganzen Fahrt (Strecke, Fahrzeit, Ø) und den Teilen
kleiner darunter („Teil 1/2", ohne Trennlinie dazwischen); die Sitzungsseite verlinkt den Bericht der ganzen Fahrt (/tour/{id}). Das
hängt an den Uhrzeiten: Nach einem Reset ohne Uhr übernimmt die Firmware die GPS-Zeit von
TrailBridge und korrigiert die vorher geschriebenen Zeitstempel; eine Sitzung, die nie eine
Uhrzeit bekam (kein WLAN, kein TrailBridge), bleibt für sich. Test- und Leerlauf-Sitzungen
gehören nie zu einer Fahrt.
Die Seiten laden die Schriften von Google Fonts; ohne Internet nimmt der Browser Systemschriften.
Installation (Heimserver: ~/bikelog)¶
Tools/BikeLogService/install.sh # oder: install.sh /anderer/pfad
legt an: ~/bikelog/venv (eigene venv, der Code wird hineinkopiert --
Branch-Wechsel im Repo berühren den Dienst nicht), data/, bikelog.env
(Konfiguration, wird nie überschrieben) und bikelog.service. Einmalig als
root:
sudo install -m 644 ~/bikelog/bikelog.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now bikelog
journalctl -u bikelog -f
System-Unit mit User= statt User-Unit: braucht keine Login-Sitzung und
kein enable-linger.
Update: nach git pull wieder install.sh, dann sudo systemctl restart bikelog.
Oberfläche: http://<server>:8080/ (Fahrtenliste), API-Doku http://<server>:8080/docs.
Entwicklung¶
cd Tools
python3 -m venv .venv
.venv/bin/pip install -r BikeLogService/requirements.txt
.venv/bin/python -m pytest
PYTHONPATH=.:BikeLogService .venv/bin/python -m bikelogservice serve --port 8081
Kommandozeile¶
bikelogservice serve [--host 0.0.0.0] [--port 8080]
bikelogservice pull [TRGB-BC.local | 192.168.0.171] [--device trgb] # einmal abholen
bikelogservice import /media/sd/BIKECOMP [--device trgb] # SD-Karte im Kartenleser
bikelogservice export [--all] # GPX-Dateien schreiben
bikelogservice inbox # Push-Eingang einmal abholen
pull und import arbeiten auf demselben Datenverzeichnis
(BIKELOG_DATA_DIR) und dürfen parallel zum Dienst laufen.
Konfiguration¶
Umgebungsvariablen (im Dienst: ~/bikelog/bikelog.env):
| Variable | Default | Bedeutung |
|---|---|---|
BIKELOG_DATA_DIR |
$STATE_DIRECTORY bzw. ~/.local/state/bikelog |
Sitzungen + SQLite-Index |
BIKELOG_PORT |
-- | nur in der Unit: Port für serve |
BIKELOG_PULL |
0 |
Abholen an/aus |
BIKELOG_PULL_TARGETS |
trgb=TRGB-BC |
gerät=mdns-name, kommagetrennt |
BIKELOG_PULL_INTERVAL_S |
120 |
Polling-Intervall |
BIKELOG_PULL_MDNS |
1 |
auf mDNS-Ankündigungen hören |
BIKELOG_EXPORT_GPX |
1 |
GPX-Export an/aus |
BIKELOG_EXPORT_DIR |
<data>/export/gpx |
Zielverzeichnis (muss in der Unit beschreibbar sein: ReadWritePaths=) |
BIKELOG_PUBLIC_URL |
-- | z. B. http://server:8080, für den Link im GPX |
BIKELOG_REQUIRE_AUTH |
0 |
Auth an/aus |
BIKELOG_TOKENS |
-- | token:name, kommagetrennt |
BIKELOG_MAX_UPLOAD_BYTES |
64 MiB | Obergrenze pro PUT |
BIKELOG_KOMOOT_EMAIL / BIKELOG_KOMOOT_PASSWORD |
-- | Komoot-Login; ohne beide ist der Upload aus |
BIKELOG_KOMOOT_ACTIVITY |
touringbicycle |
eine SupportedActivities-Konstante aus kompy |
BIKELOG_KOMOOT_STATUS |
friends |
Sichtbarkeit der hochgeladenen Tour (public/private/friends) |
BIKELOG_KOMOOT_MERGE_GAP_S |
1800 |
Sitzungen desselben Geräts mit höchstens so viel Pause dazwischen gelten als eine unterbrochene Fahrt |
BIKELOG_NEXTCLOUD_URL / _USER / _PASSWORD |
-- | Nextcloud-Login (App-Passwort); ohne alle drei ist der Sync aus |
BIKELOG_NEXTCLOUD_DIR |
BikeLog |
Zielverzeichnis (WebDAV-Pfad, wird bei Bedarf angelegt) |
BIKELOG_NEXTCLOUD_INBOX |
-- | Ordner, in den die Fahrradcomputer hochladen (WebDAV-Pfad); leer = wird nicht abgeholt |
BIKELOG_INBOX_INTERVAL_S |
300 |
wie oft der Eingang angeschaut wird |
BIKELOG_ATHLETE_FILE |
<data>/athlete.json |
Fahrerdaten für den Sitzungsbericht |
BIKELOG_WIND |
1 |
Wind von Open-Meteo im Fahrtbericht; 0 = Berichte ohne Wind, kein Netzzugriff |
BIKELOG_IDLE_ARCHIVE_DAYS |
7 |
Leerlauf-Sitzungen nach so vielen Tagen nach archive/, 0 = nie |
BIKELOG_LLM_URL |
-- | Ollama-Server für die Fahrt in Worten; leer = aus |
BIKELOG_LLM_MODEL |
qwen3:8b |
Ollama-Modell |
BIKELOG_LLM_TIMEOUT_S |
1800 |
längste Wartezeit auf eine Antwort |
BIKELOG_LLM_REFRESH_DAYS |
14 |
Texte mit geänderten Fakten werden nur für Fahrten so vieler Tage neu geschrieben |
Mehrere Fahrradcomputer: Jede Build-Variante hat einen eigenen Netzwerknamen (mDNS-Host und
Standard-Hotspot-SSID, BC_HOSTNAME in platformio.ini): der Gravel-Build TRGB-BC, der
Forumslader-Build TRGB-FL; änderbar auf der Seite /wifi des Fahrradcomputers
(WLAN). Ein Abhol-Ziel je Fahrradcomputer, z. B.
BIKELOG_PULL_TARGETS=trgb=TRGB-BC,pendler=TRGB-FL; der Teil vor = ist der Gerätename, unter
dem die Sitzungen abgelegt und dem Räder zugeordnet werden (siehe Räder). Den Namen
eines bestehenden Ziels beibehalten, die gespeicherten Sitzungen liegen darunter.
API (v1)¶
| Methode | Pfad | Zweck |
|---|---|---|
GET |
/ |
Fahrtenliste (HTML) |
GET |
/api/v1/health |
Erreichbarkeit + Anzahl Sitzungen |
GET |
/api/v1/sessions |
Liste (limit, offset, tests: Testfahrten, idle: Leerlauf-Sitzungen einschließen), neueste zuerst |
GET |
/api/v1/sessions/{id} |
Sitzung mit Dateien, Kennzahlen, I_-Statistik |
GET |
/api/v1/sessions/{id}.gpx |
GPX wie im Export (max_fix_age_ms, segment_gap_s, ele, max_accuracy_m, shocks, labels, rich) |
GET |
/api/v1/sessions/{id}.csv |
CSV (with_gps) |
GET |
/api/v1/sessions/{id}/files/{name} |
eine Datei unverändert |
GET |
/api/v1/sessions/{id}/report.json |
Kennzahlen der Sitzung, siehe Sitzungsbericht |
GET |
/api/v1/sessions/{id}/report.md |
dasselbe als Markdown-Bericht |
POST |
/api/v1/sessions/{id}/test |
Test-Erkennung überstimmen (mark=test|real|auto) |
POST |
/api/v1/archive/device-delete |
Leerlauf-/archivierte Sitzungen sofort auf dem BC löschen (session_id) |
PUT |
/api/v1/import/gpx |
aufgezeichnete Fahrt importieren (bike_id, event_id; Body = GPX) |
GET |
/api/v1/training |
Tageslast (TRIMP, CTL, ATL, TSB), Wochen, wiederkehrende Anstiege (days, weeks) |
GET |
/api/v1/events |
Zielrennen mit Stand der Vorbereitung |
PUT |
/api/v1/events/{id}/gpx |
Strecken-GPX eines Zielrennens (Body = Datei) |
GET |
/routes |
geplante und regelmäßige Strecken (HTML) |
GET |
/routes/{id} |
eine Strecke mit Höhenprofil, Anstiegen und Befunden (HTML) |
GET/PUT |
/api/v1/routes |
Strecken mit Prüfung und Kennzahlen (tags, archived) / eine hochladen (kind=planned|regular, tags=commute,training, name, filename; Body = GPX) |
GET/DELETE |
/api/v1/routes/{id} |
eine Strecke / löschen |
GET |
/api/v1/routes/{id}.gpx |
ihre GPX unverändert |
PUT |
/api/v1/routes/{id}/gpx |
neue Version der Datei (filename) |
POST |
/api/v1/routes/{id}/trailbridge |
TrailBridge anbieten (on=false: zurücknehmen) |
GET |
/api/v1/trailbridge/route |
die für TrailBridge markierte Strecke, siehe oben |
GET |
/api/v1/trailbridge/routes/{id}.gpx |
ihre Datei, für die App |
POST |
/api/v1/trailbridge/route/fetched |
die App bestätigt eine Revision (revision) |
DELETE |
/api/v1/sessions/{id} |
Dateien löschen, Grabstein behalten |
POST |
/api/v1/sessions/{id}/komoot |
zur zusammengehörigen Tour hochladen (force), siehe unten |
POST |
/api/v1/sessions/{id}/komoot/ignore |
die Frage der Seite mit „nein“ beantworten (ask_again=true: wieder fragen) |
PUT |
/api/v1/devices/{gerät}/files/{tag}/{name} |
eine Datei einliefern (Body = Datei) |
GET/POST |
/api/v1/pull |
Status des Abholens / jetzt abholen (device) |
GET/POST |
/api/v1/inbox |
Status des Push-Eingangs / jetzt hineinschauen |
PUT ist idempotent: dieselben Bytes noch einmal ergeben unchanged, andere
Bytes unter demselben Namen replaced (der BC schreibt I_-Dateien neu,
wenn sich ihr Format ändert). Ein L_, das sich nicht lesen lässt, wird
trotzdem aufbewahrt und in log_error begründet -- es sind die Daten des
Geräts. CUR/ wird mit 409 abgelehnt. Eine gelöschte Sitzung kommt mit einem PUT
zurück -- außer mit dem Header X-BikeLog-Auto: 1 (der automatische Push des
Fahrradcomputers): Dann lautet die Antwort {"status": "deleted"}, und nichts wird abgelegt,
wie beim Pull.
curl -T L_143012.bin http://server:8080/api/v1/devices/trgb/files/20260920/L_143012.bin
Auth nachrüsten¶
Auth ist aus, aber vollständig verdrahtet. Jede Route hängt schon heute
an der Dependency require_principal (auth.py),
die aktuell einen anonymen Principal zurückgibt. Einschalten:
BIKELOG_REQUIRE_AUTH=1
BIKELOG_TOKENS=<token>:gravel
Keine Routenänderung, keine Migration; Clients schicken
Authorization: Bearer <token>. Die HTML-Seite ist dann im Browser nicht
mehr ohne Weiteres erreichbar -- für den Heimserver ohne Portfreigabe
bleibt Auth deshalb aus. Der Test
test_every_route_requires_a_token_once_auth_is_on hält fest, dass keine
Route an der Dependency vorbeigeht.