Spezifikation
Open Challenge Protocol
Ein deklaratives JSON-Format, um Fitness-Challenges plattformübergreifend zu beschreiben.
Überblick
Das Open Challenge Protocol (OCP) ist ein deklaratives JSON-Format zur Beschreibung von Fitness-Challenges. Es legt fest, was als Erfolg zählt, nicht wie eine Host-Anwendung das Erlebnis umsetzen soll.
Eine Challenge ist ein strukturiertes Ziel, das die Leistung anhand festgelegter Kriterien bewertet, mit optionalen Lebenszyklus-Phasen, Laufzeit-Direktiven und Iterations- bzw. Schleifenlogik.
Challenges vs. Trainings
OCP ist speziell für Challenges gemacht, nicht für Trainings. Diesen Unterschied zu verstehen ist entscheidend.
Challenges
- Dynamisch und zielorientiert: Erfolg bemisst sich daran, bestimmte Kriterien zu erfüllen
- Logikbasiert: Aufgebaut auf Bedingungen, Schwellen und Abbruchkriterien
- Oft unbestimmte Dauer: Viele enden, wenn du eine Bedingung nicht mehr halten kannst
- Bestanden/nicht bestanden oder Rekordjagd: Entweder du erreichst das Ziel, oder du trittst gegen eine persönliche Bestleistung an
Beispiele
- 5.0 w/kg für 10 Sekunden halten
- 200 m so schnell wie möglich sprinten
- So weit wie möglich fahren und die HF dabei unter Zone 4 halten
- Aus dem Stand auf 40 km/h beschleunigen
Trainings
- Strukturiert und vorab festgelegt: Einer festen Folge von Intervallen folgen
- Zeit- bzw. dauerbasiert: Intervalle haben feste Dauern
- Abschluss = Erfolg: Wenn du alle Intervalle schaffst, hast du das Training „abgeschlossen“
- Trainingsorientiert: Auf physiologische Anpassung ausgelegt
Nicht geeignet für OCP
- 5x (30 Sekunden Belastung / 30 Sekunden Pause)
- Tabata: 8x (20 Sekunden / 10 Sekunden)
- Leiter-Intervalle mit 1-2-3-2-1 Minuten
- 2x20 Minuten an der FTP
Im Zweifel: Frag dich: „Hängt der Erfolg davon ab, wie die Fahrerin oder der Fahrer fährt, oder nur dass die Struktur absolviert wird?“ Hängt der Erfolg davon ab, Bedingungen einzuhalten → Challenge. Geht es nur darum, Intervalle zu absolvieren → Training.
Aufbau einer Challenge
Eine Challenge ist ein JSON-Objekt mit den folgenden Feldern auf oberster Ebene:
{
"id": "string",
"version": "0.1.0",
"name": "string",
"description": "string (optional)",
"sport": "cycling | running | rowing | any (optional)",
"mode": "record | pass-fail",
"config": { },
"phases": [ ],
"criteria": { },
"loops": [ ],
"directives": [ ],
"record": { },
"completion": { },
"metadata": { }
}Pflichtfelder
| Feld | Typ | Beschreibung |
|---|---|---|
id | string | Eindeutige Kennung dieser Challenge |
version | string | Protokollversion (semver) |
name | string | Lesbarer Name der Challenge |
phases | array | Geordnete Liste der Challenge-Phasen (ODER criteria bei einfachen Challenges) |
Optionale Felder
| Feld | Typ | Beschreibung |
|---|---|---|
description | string | Ausführlichere Beschreibung der Challenge |
sport | string | Sportart (cycling, running, rowing, any) |
mode | string | record (Bestleistung) oder pass-fail (binärer Erfolg). Standard: pass-fail |
config | object | Konfiguration und berechnete Werte für die gesamte Challenge |
criteria | object | Erfolgs- (und optionale Misserfolgs-) Kriterien für einfache Challenges |
loops | array | Sich wiederholende Auswertungsblöcke |
directives | array | Laufzeitverhalten (Benachrichtigungen, Vorschläge) |
record | object | Welche Metriken als Ergebnis der Challenge erfasst werden |
completion | object | Wie die Challenge endet und was angezeigt wird |
metadata | object | Anzeigehinweise, Schwierigkeit, Tags usw. |
Challenge-Modi
pass-fail (Standard): Klassische Challenge mit Kriterien für Erfolg und Misserfolg. Binäres Ergebnis.
record: Challenge auf Bestleistung, bei der es um das bestmögliche Ergebnis geht. Kein Scheitern möglich: Die Challenge zeichnet deine Leistung auf.
Messwerte
Metriken sind die kanonischen Datenpunkte, die Challenges auswerten. Host-Anwendungen bilden ihre Rohdaten auf diese Pfade ab.
Format der Metrik-Pfade
Metriken nutzen Punktnotation: category.metric
power.instant → Current power in watts power.wkg → Current power in watts per kilogram lap.index → Current lap number (0-indexed or 1-indexed per host) position.current → Race position (1 = first place)
Kernmetriken
Zeit
| Pfad | Typ | Beschreibung |
|---|---|---|
time.elapsed | number | Sekunden seit Start der Challenge |
time.session | number | Sekunden seit Start der Fahrt |
Leistung
| Pfad | Typ | Beschreibung |
|---|---|---|
power.instant | number | Aktuelle Leistung (Watt) |
power.avg | number | Durchschnittsleistung der Einheit |
power.normalized | number | Normalized Power |
power.wkg | number | Aktuelle Watt pro Kilogramm |
power.zone | number | Aktuelle Leistungszone (1-7) |
power.percentFtp | number | Aktuelle Leistung in % der FTP |
Geschwindigkeit, Trittfrequenz, Herzfrequenz
| Pfad | Typ | Beschreibung |
|---|---|---|
speed.instant | number | Aktuelle Geschwindigkeit (m/s oder km/h, je nach Host) |
cadence.instant | number | Aktuelle Trittfrequenz (RPM) |
heartRate.instant | number | Aktuelle Herzfrequenz (S/min) |
heartRate.zone | number | Aktuelle HF-Zone (1-5) |
Distanz, Runde, Bahn
| Pfad | Typ | Beschreibung |
|---|---|---|
distance.total | number | Gesamtdistanz (Meter) |
lap.index | number | Aktuelle Rundennummer |
lap.count | number | Insgesamt gefahrene Runden |
track.length | number | Bahnlänge (Meter) |
Renn- und Multiplayer-Metriken
Diese sind nur im Rennkontext verfügbar:
| Pfad | Typ | Beschreibung |
|---|---|---|
position.current | number | Aktuelle Rennposition (1 = Erster) |
gap.ahead | number | Abstand zum Fahrer vor dir |
gap.behind | number | Abstand zum Fahrer hinter dir |
overtakes.count | number | Anzahl der Überholmanöver |
Phasen
Phasen legen den Aufbau einer Challenge als geordnete Abfolge von Abschnitten fest. Jede Phase hat ihre eigene Dauer, Ziele, Abbruchbedingungen und Direktiven.
Aufbau einer Phase
{
"id": "effort",
"name": "Max Effort",
"duration": 30,
"description": "Give everything for 30 seconds",
"targets": { },
"exitConditions": { },
"directives": [ ]
}| Feld | Typ | Erforderlich | Beschreibung |
|---|---|---|---|
id | string | Ja | Eindeutige Kennung dieser Phase |
name | string | Ja | Anzeigename |
duration | number/string | Nein | Dauer der Phase in Sekunden oder "unlimited" |
distance | number | Nein | Distanz der Phase in Metern (Alternative zur Dauer) |
targets | object | Nein | Zielmetriken für diese Phase |
exitConditions | object | Nein | Bedingungen, die diese Phase vorzeitig beenden |
Ziele
Ziele legen die erwarteten oder geforderten Metriken während einer Phase fest:
{
"targets": {
"power": { "min": 80, "max": 150 },
"cadence": { "min": 90, "max": 100 }
}
}Ansteigende Ziele
{
"power": {
"mode": "ramp",
"start": 100,
"increment": 1,
"per": "second"
}
}Bedingungen
Eine Bedingung ist ein einzelner prüfbarer Ausdruck, der wahr oder falsch ergibt.
Aufbau einer Bedingung
{
"metric": "power.wkg",
"operator": ">=",
"value": 5.0,
"duration": 10,
"window": "sustained"
}| Feld | Typ | Erforderlich | Beschreibung |
|---|---|---|---|
metric | string | Ja | Auszuwertender Metrik-Pfad |
operator | string | Ja | Vergleichsoperator |
value | number/array | Ja | Zielwert(e) |
duration | number | Nein | Zeitvorgabe in Sekunden |
window | string | Nein | Wie die Dauer ausgewertet wird |
Operatoren
Vergleich
==Gleich!=Ungleich>Größer als<Kleiner als>=Größer oder gleich<=Kleiner oder gleich
Bereich
betweenBereich inklusive GrenzenoutsideAußerhalb des Bereichs
Ändern
increasedByDelta-ZunahmedecreasedByDelta-Abnahme
Zeitfenster
| Fenster | Beschreibung |
|---|---|
sustained | Bedingung muss für die Dauer durchgehend wahr sein (Standard) |
cumulative | Bedingung muss insgesamt für die Dauer wahr sein (auch mit Unterbrechungen) |
rolling | Ausgewertet über ein gleitendes Fenster der angegebenen Dauer |
any | Bedingung muss zu irgendeinem Zeitpunkt für die Dauer wahr sein |
Logische Gatter
Bedingungen lassen sich mit logischen Gattern kombinieren.
Gattertypen
| Tor | Beschreibung |
|---|---|
all | Alle Bedingungen müssen wahr sein (AND) |
any | Mindestens eine Bedingung muss wahr sein (OR) |
none | Keine Bedingung darf wahr sein (NOT ANY) |
sequence | Bedingungen müssen der Reihe nach wahr werden |
Beispiel: Verschachtelte Gatter
{
"all": [
{ "metric": "time.elapsed", "operator": ">=", "value": 60 },
{
"any": [
{ "metric": "power.wkg", "operator": ">=", "value": 5.0 },
{ "metric": "position.current", "operator": "==", "value": 1 }
]
}
]
}„Nach 60 Sekunden entweder 5.0 w/kg erreichen ODER auf Platz eins liegen“
Lebenszyklus
Der Lebenszyklus legt fest, wann die Auswertung einer Challenge beginnt und endet.
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ PENDING │ ──▶ │ ARMING │ ──▶ │ ACTIVE │ ──▶ │ COMPLETE │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│ │
│ ▼
│ ┌─────────────┐
└────────────▶ │ FAILED │
└─────────────┘| Phase | Beschreibung |
|---|---|
pending | Challenge geladen, aber nicht gestartet |
arming | Warten auf Startbedingungen |
active | Erfolgs- und Misserfolgskriterien werden ausgewertet |
complete | Erfolgskriterien erfüllt |
failed | Misserfolgskriterien erfüllt oder abgebrochen |
Aufbau des Lebenszyklus
{
"lifecycle": {
"arming": {
"all": [
{ "metric": "time.elapsed", "operator": ">=", "value": 120 }
]
},
"timeout": 600,
"maxAttempts": 3
}
}Erfolgs- und Misserfolgskriterien
Aufbau der Kriterien
{
"criteria": {
"success": {
"all": [
{ "metric": "power.wkg", "operator": ">=", "value": 5.0, "duration": 10 }
]
},
"failure": {
"any": [
{ "metric": "power.instant", "operator": "==", "value": 0, "duration": 5 }
]
}
}
}Challenges nur mit Erfolgskriterien
Die meisten Challenges legen nur Erfolgskriterien fest. Die Challenge läuft, bis sie geschafft oder abgebrochen wird.
Challenges mit Misserfolgsbedingungen
{
"criteria": {
"success": {
"all": [
{ "metric": "lap.count", "operator": ">=", "value": 5 }
]
},
"failure": {
"any": [
{ "metric": "position.current", "operator": ">", "value": 3 }
]
}
}
}„Fahre 5 Runden und bleib dabei in den Top 3. Platz 4 oder schlechter = nicht bestanden.“
Schleifen
Schleifen ermöglichen sich wiederholende Auswertungsblöcke mit Logik, die die Iteration kennt. Das ist wichtig für Challenges mit mehreren Intervallen, steigender Schwierigkeit oder wiederholten Aktionen.
Aufbau einer Schleife
{
"loops": [
{
"id": "sprint_intervals",
"count": 5,
"trigger": {
"metric": "lap.index",
"operator": "increasedBy",
"value": 1
},
"onIteration": {
"directives": [ ],
"criteria": { }
},
"onComplete": {
"directives": [ ]
}
}
]
}Kontextvariablen der Iteration
| Wechselhaft | Beschreibung |
|---|---|
{{loopIndex}} | Aktuelle Iteration (ab 0 gezählt) |
{{loopCount}} | Insgesamt konfigurierte Iterationen |
{{loopRemaining}} | Verbleibende Iterationen |
Beispiel für steigende Schwierigkeit
{
"loops": [
{
"id": "progressive_power",
"count": 5,
"trigger": {
"metric": "time.elapsed",
"operator": ">=",
"value": "{{loopIndex * 60}}"
},
"onIteration": {
"directives": [
{
"type": "notify",
"message": "Interval {{loopIndex + 1}}/{{loopCount}}: Hold {{3.0 + loopIndex * 0.5}} w/kg"
}
]
}
}
]
}„5 Intervalle mit steigender Leistung: 3.0, 3.5, 4.0, 4.5, 5.0 w/kg“
Direktiven
Direktiven sind optionale Laufzeitverhalten, die die Host-Anwendung ausführen kann. Sie sind advisory: Der Host entscheidet, ob er sie umsetzt.
Aufbau einer Direktive
{
"directives": [
{
"id": "gap_warning",
"type": "notify",
"when": {
"metric": "gap.ahead",
"operator": ">",
"value": 3.0
},
"params": {
"message": "Gap widening! Push harder.",
"priority": "high"
},
"cooldown": 10
}
]
}Direktiventypen
notify
Dem Fahrer eine Nachricht anzeigen
{
"type": "notify",
"params": {
"message": "Push harder!",
"priority": "normal | high | critical"
}
}sound
Ein Audiosignal abspielen
{
"type": "sound",
"params": {
"sound": "bell | chime | warning | success"
}
}suggest
Dem Fahrer ein Ziel vorschlagen (advisory)
{
"type": "suggest",
"params": {
"metric": "power.instant",
"target": 250
}
}highlight
Eine Metrik in der Oberfläche hervorheben
{
"type": "highlight",
"params": {
"metric": "power.wkg",
"style": "pulse | glow | flash"
}
}Record (Challenges auf Bestleistung)
Bei mode: "record"-Challenges legt das record-Objekt fest, welche Metriken als Ergebnis der Challenge erfasst werden.
Aufbau von Record
{
"record": {
"primary": {
"metric": "power.max",
"label": "Peak Power",
"unit": "watts",
"phase": "effort"
},
"secondary": [
{ "metric": "power.peak", "label": "Peak Power", "unit": "watts" },
{ "metric": "cadence.max", "label": "Max Cadence", "unit": "rpm" }
]
}
}Gängige Hauptmetriken
| Challenge-Typ | Hauptmetrik | Beispiel |
|---|---|---|
| Maximale Anstrengung | power.max | Spitzenleistung |
| Zeitfahren | duration für die Phase | 200-m-Sprintzeit |
| Grundlage halten | duration für die Phase | Im Ziel gehaltene Zeit |
| Distanz | distance.total | Zurückgelegte Meter |
Abschluss
Das completion-Objekt legt fest, wie eine Challenge endet und was in der Zusammenfassung angezeigt wird.
Abschlussstruktur
{
"completion": {
"condition": "phases-complete | exit-or-phases-complete | criteria-met",
"summary": {
"headline": "{{record.primary.value}}W",
"subheadline": "Peak Power"
}
}
}Abschlussbedingungen
| Bedingung | Beschreibung |
|---|---|
phases-complete | Challenge endet, wenn alle Phasen abgeschlossen sind |
exit-or-phases-complete | Challenge endet bei der Abbruchbedingung einer Phase ODER wenn alle Phasen abgeschlossen sind |
criteria-met | Challenge endet, wenn die Erfolgskriterien erfüllt sind (Pass-Fail-Modus) |
Metadaten
Metadaten liefern Anzeigehinweise und Kategorisierung für Host-Anwendungen.
{
"metadata": {
"difficulty": 4,
"estimatedDuration": 120,
"tags": ["sprint", "power", "short"],
"author": "Ride Cave",
"icon": "bolt",
"color": "#FF6B00",
"shortDescription": "Hold 5.0 w/kg for 10 seconds",
"requirements": {
"powerMeter": true,
"heartRateMonitor": false
}
}
}| Feld | Typ | Beschreibung |
|---|---|---|
difficulty | number | Schwierigkeitsgrad 1-5 |
estimatedDuration | number | Erwartete Dauer in Sekunden |
tags | array | Kategorie-Tags |
author | string | Ersteller der Challenge |
icon | string | Icon-Kennung |
color | string | Akzentfarbe (Hex) |
requirements | object | Benötigte Sensoren/Ausrüstung |
Vollständiges Beispiel
{
"id": "power-surge",
"version": "0.1.0",
"name": "Power Surge",
"description": "After a 2-minute warmup, hold 5.0 w/kg for 10 continuous seconds.",
"sport": "cycling",
"lifecycle": {
"arming": {
"all": [
{ "metric": "time.elapsed", "operator": ">=", "value": 120 }
]
},
"timeout": 300
},
"criteria": {
"success": {
"all": [
{
"metric": "power.wkg",
"operator": ">=",
"value": 5.0,
"duration": 10,
"window": "sustained"
}
]
}
},
"directives": [
{
"type": "notify",
"params": {
"message": "Warmup complete. Go for 5.0 w/kg!"
},
"once": true
},
{
"type": "notify",
"when": {
"metric": "power.wkg",
"operator": ">=",
"value": 4.5
},
"params": {
"message": "Almost there! Push to 5.0!",
"priority": "high"
},
"cooldown": 5
}
],
"metadata": {
"difficulty": 4,
"estimatedDuration": 180,
"tags": ["sprint", "power", "short"],
"author": "Ride Cave",
"shortDescription": "Hold 5.0 w/kg for 10 seconds"
}
}Auswertungsmodell
Auswertungsschleife
Die Auswertung der Challenge läuft mit fester Frequenz (empfohlen: 10 Hz).
┌─────────────────────────────────────────────────────────────┐ │ EVALUATION TICK │ ├─────────────────────────────────────────────────────────────┤ │ 1. Collect current metrics from host │ │ 2. Check lifecycle phase │ │ - If PENDING: check arming conditions │ │ - If ARMING: check arming conditions → transition │ │ - If ACTIVE: evaluate criteria + loops + directives │ │ 3. Update loop states │ │ 4. Check success criteria │ │ 5. Check failure criteria (if defined) │ │ 6. Fire applicable directives │ │ 7. Return evaluation result │ └─────────────────────────────────────────────────────────────┘
Auswertungsergebnis
{
"phase": "active",
"success": false,
"failed": false,
"progress": {
"percent": 75,
"conditions": [
{
"metric": "power.wkg",
"target": 5.0,
"current": 5.2,
"durationElapsed": 7.5,
"durationRequired": 10,
"met": false
}
]
},
"loops": [
{
"id": "sprint_intervals",
"iteration": 2,
"count": 5,
"status": "active"
}
],
"firedDirectives": ["gap_warning"],
"elapsed": 145.3
}Anhänge
A. Umgebung & Portabilität
Challenges können Umgebungsmetriken referenzieren, also Werte der Host-Anwendung, die den Kontext beschreiben, in dem die Challenge läuft.
Spektrum der Portabilität
- Voll portabel: Challenges, die nur Kernmetriken referenzieren (Leistung, Trittfrequenz, Herzfrequenz, Zeit, Distanz), laufen auf jedem konformen Host
- Umgebungsabhängig: Challenges, die Umgebungsmetriken referenzieren, setzen voraus, dass der Host diese Werte liefert
- Plattformspezifisch: Manche Challenges können Features referenzieren, die es nur auf einer bestimmten Plattform gibt
Ersatzwerte
{
"config": {
"approachDistance": {
"value": "{{track.length}}",
"fallback": 250
}
}
}B. Reservierte Metrik-Pfade
Die folgenden Pfade sind für die zukünftige Nutzung reserviert:
rowing.*- Ruderspezifische Metrikenrunning.*- Laufspezifische Metrikenstrength.*- Metriken fürs Krafttrainingcustom.*- Vom Host definierte eigene Metriken
C. Hinweise zur Host-Implementierung
Hosts müssen einen Metrik-Adapter implementieren, der ihre internen Daten auf OCP-Metrik-Pfade abbildet:
interface MetricAdapter {
resolve(path: string): number | undefined;
getProfile(): { ftp?: number; weight?: number; maxHr?: number };
}D. Versionsverlauf
| Version | Datum | Änderungen |
|---|---|---|
| 0.1.0-draft | 2025-01 | Erster Entwurf der Spezifikation |
