Open Challenge Protocol (OCP) | Ride Cave

Spezifikation

Open Challenge Protocol

Ein deklaratives JSON-Format, um Fitness-Challenges plattformübergreifend zu beschreiben.

EntwurfVersion 0.1.0

Ü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

FeldTypBeschreibung
idstringEindeutige Kennung dieser Challenge
versionstringProtokollversion (semver)
namestringLesbarer Name der Challenge
phasesarrayGeordnete Liste der Challenge-Phasen (ODER criteria bei einfachen Challenges)

Optionale Felder

FeldTypBeschreibung
descriptionstringAusführlichere Beschreibung der Challenge
sportstringSportart (cycling, running, rowing, any)
modestringrecord (Bestleistung) oder pass-fail (binärer Erfolg). Standard: pass-fail
configobjectKonfiguration und berechnete Werte für die gesamte Challenge
criteriaobjectErfolgs- (und optionale Misserfolgs-) Kriterien für einfache Challenges
loopsarraySich wiederholende Auswertungsblöcke
directivesarrayLaufzeitverhalten (Benachrichtigungen, Vorschläge)
recordobjectWelche Metriken als Ergebnis der Challenge erfasst werden
completionobjectWie die Challenge endet und was angezeigt wird
metadataobjectAnzeigehinweise, 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

PfadTypBeschreibung
time.elapsednumberSekunden seit Start der Challenge
time.sessionnumberSekunden seit Start der Fahrt

Leistung

PfadTypBeschreibung
power.instantnumberAktuelle Leistung (Watt)
power.avgnumberDurchschnittsleistung der Einheit
power.normalizednumberNormalized Power
power.wkgnumberAktuelle Watt pro Kilogramm
power.zonenumberAktuelle Leistungszone (1-7)
power.percentFtpnumberAktuelle Leistung in % der FTP

Geschwindigkeit, Trittfrequenz, Herzfrequenz

PfadTypBeschreibung
speed.instantnumberAktuelle Geschwindigkeit (m/s oder km/h, je nach Host)
cadence.instantnumberAktuelle Trittfrequenz (RPM)
heartRate.instantnumberAktuelle Herzfrequenz (S/min)
heartRate.zonenumberAktuelle HF-Zone (1-5)

Distanz, Runde, Bahn

PfadTypBeschreibung
distance.totalnumberGesamtdistanz (Meter)
lap.indexnumberAktuelle Rundennummer
lap.countnumberInsgesamt gefahrene Runden
track.lengthnumberBahnlänge (Meter)

Renn- und Multiplayer-Metriken

Diese sind nur im Rennkontext verfügbar:

PfadTypBeschreibung
position.currentnumberAktuelle Rennposition (1 = Erster)
gap.aheadnumberAbstand zum Fahrer vor dir
gap.behindnumberAbstand zum Fahrer hinter dir
overtakes.countnumberAnzahl 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": [ ]
}
FeldTypErforderlichBeschreibung
idstringJaEindeutige Kennung dieser Phase
namestringJaAnzeigename
durationnumber/stringNeinDauer der Phase in Sekunden oder "unlimited"
distancenumberNeinDistanz der Phase in Metern (Alternative zur Dauer)
targetsobjectNeinZielmetriken für diese Phase
exitConditionsobjectNeinBedingungen, 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"
}
FeldTypErforderlichBeschreibung
metricstringJaAuszuwertender Metrik-Pfad
operatorstringJaVergleichsoperator
valuenumber/arrayJaZielwert(e)
durationnumberNeinZeitvorgabe in Sekunden
windowstringNeinWie die Dauer ausgewertet wird

Operatoren

Vergleich

  • == Gleich
  • != Ungleich
  • > Größer als
  • < Kleiner als
  • >= Größer oder gleich
  • <= Kleiner oder gleich

Bereich

  • between Bereich inklusive Grenzen
  • outside Außerhalb des Bereichs

Ändern

  • increasedBy Delta-Zunahme
  • decreasedBy Delta-Abnahme

Zeitfenster

FensterBeschreibung
sustainedBedingung muss für die Dauer durchgehend wahr sein (Standard)
cumulativeBedingung muss insgesamt für die Dauer wahr sein (auch mit Unterbrechungen)
rollingAusgewertet über ein gleitendes Fenster der angegebenen Dauer
anyBedingung muss zu irgendeinem Zeitpunkt für die Dauer wahr sein

Logische Gatter

Bedingungen lassen sich mit logischen Gattern kombinieren.

Gattertypen

TorBeschreibung
allAlle Bedingungen müssen wahr sein (AND)
anyMindestens eine Bedingung muss wahr sein (OR)
noneKeine Bedingung darf wahr sein (NOT ANY)
sequenceBedingungen 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    │
                                         └─────────────┘
PhaseBeschreibung
pendingChallenge geladen, aber nicht gestartet
armingWarten auf Startbedingungen
activeErfolgs- und Misserfolgskriterien werden ausgewertet
completeErfolgskriterien erfüllt
failedMisserfolgskriterien 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

WechselhaftBeschreibung
{{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-TypHauptmetrikBeispiel
Maximale Anstrengungpower.maxSpitzenleistung
Zeitfahrenduration für die Phase200-m-Sprintzeit
Grundlage haltenduration für die PhaseIm Ziel gehaltene Zeit
Distanzdistance.totalZurü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

BedingungBeschreibung
phases-completeChallenge endet, wenn alle Phasen abgeschlossen sind
exit-or-phases-completeChallenge endet bei der Abbruchbedingung einer Phase ODER wenn alle Phasen abgeschlossen sind
criteria-metChallenge 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
    }
  }
}
FeldTypBeschreibung
difficultynumberSchwierigkeitsgrad 1-5
estimatedDurationnumberErwartete Dauer in Sekunden
tagsarrayKategorie-Tags
authorstringErsteller der Challenge
iconstringIcon-Kennung
colorstringAkzentfarbe (Hex)
requirementsobjectBenö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 Metriken
  • running.* - Laufspezifische Metriken
  • strength.* - Metriken fürs Krafttraining
  • custom.* - 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

VersionDatumÄnderungen
0.1.0-draft2025-01Erster Entwurf der Spezifikation