Open Challenge Protocol (OCP) | Ride Cave

Specificatie

Open Challenge Protocol

Een declaratief JSON-formaat om fitnesschallenges over platforms heen te beschrijven.

ConceptVersie 0.1.0

Overzicht

Het Open Challenge Protocol (OCP) is een declaratief JSON-formaat om fitnesschallenges te beschrijven. Het bepaalt wat succes is, niet hoe een hostapplicatie de ervaring moet uitvoeren.

Een challenge is een gestructureerd doel dat de prestatie van een renner toetst aan vastgelegde criteria, met optionele levenscyclusfases, runtime-directives en iteratie-/luslogica.

Challenges versus trainingen

OCP is specifiek ontworpen voor Challenges, niet voor trainingen. Dit onderscheid begrijpen is essentieel.

Challenges

  • Dynamisch en doelgericht: Succes wordt bepaald door aan specifieke criteria te voldoen
  • Op basis van logica: Opgebouwd rond condities, drempels en exitcriteria
  • Vaak onbepaalde duur: Veel eindigen als je een conditie niet volhoudt
  • Slagen/falen of records breken: Je haalt het doel of je strijdt tegen een persoonlijk record

Voorbeelden

  • Houd 5.0 w/kg vast voor 10 seconden
  • Sprint 200 m zo snel mogelijk
  • Rijd zo ver mogelijk terwijl je je hartslag onder zone 4 houdt
  • Versnel vanuit stilstand tot 40 km/h

Trainingen

  • Gestructureerd en vooraf bepaald: Volg een vaste reeks intervallen
  • Op basis van tijd/duur: Intervallen hebben een vaste duur
  • Voltooiing = succes: Als je alle intervallen afmaakt, heb je de training "voltooid"
  • Gericht op training: Ontworpen voor fysiologische aanpassing

Niet geschikt voor OCP

  • 5x (30 seconden aan / 30 seconden uit)
  • Tabata: 8x (20 seconden / 10 seconden)
  • Ladderintervallen van 1-2-3-2-1 minuut
  • 2x20 minuten op FTP

Bij twijfel: Vraag je af: "Hangt succes af van hoe de renner presteert, of alleen dat de renner de structuur afmaakt?" Hangt succes af van het vasthouden van voorwaarden → Challenge. Is succes alleen het afmaken van intervallen → Training.

Opbouw van een challenge

Een challenge is een JSON-object met de volgende velden op het hoogste niveau:

{
  "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": { }
}

Verplichte velden

VeldTypeBeschrijving
idstringUnieke ID voor deze challenge
versionstringProtocolversie (semver)
namestringLeesbare naam van de challenge
phasesarrayGeordende lijst van challengefases (OF criteria voor eenvoudige challenges)

Optionele velden

VeldTypeBeschrijving
descriptionstringLangere beschrijving van de challenge
sportstringType sport (cycling, running, rowing, any)
modestringrecord (beste poging) of pass-fail (binair succes). Standaard: pass-fail
configobjectConfiguratie en berekende waarden voor de hele challenge
criteriaobjectCriteria voor succes (en optioneel falen) bij eenvoudige challenges
loopsarrayHerhalende evaluatieblokken
directivesarrayRuntimegedrag (meldingen, suggesties)
recordobjectWelke metrics als resultaat van de challenge worden vastgelegd
completionobjectHoe de challenge eindigt en wat er getoond wordt
metadataobjectToon hints, moeilijkheid, tags, enz.

Challengemodi

pass-fail (standaard): klassieke challenge met criteria voor slagen of falen. Binaire uitkomst.

record: challenge waarbij je het best mogelijke resultaat probeert neer te zetten. Er is geen faalstatus: de challenge legt je prestatie vast.

Waarden

Metrics zijn de standaard datapunten die challenges evalueren. Hostapplicaties koppelen hun ruwe data aan deze paden.

Formaat van metric-paden

Metrics gebruiken puntnotatie: 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)

Kernmetrics

Tijd

PadTypeBeschrijving
time.elapsednumberSeconden sinds de start van de challenge
time.sessionnumberSeconden sinds de start van de ritsessie

Vermogen

PadTypeBeschrijving
power.instantnumberHuidig vermogen (watt)
power.avgnumberGemiddeld vermogen voor de sessie
power.normalizednumberGenormaliseerd vermogen
power.wkgnumberHuidige watt per kilogram
power.zonenumberHuidige vermogenszone (1-7)
power.percentFtpnumberHuidig vermogen als % van FTP

Snelheid, cadans, hartslag

PadTypeBeschrijving
speed.instantnumberHuidige snelheid (m/s of km/h, afhankelijk van de host)
cadence.instantnumberHuidige cadans (RPM)
heartRate.instantnumberHuidige hartslag (BPM)
heartRate.zonenumberHuidige hartslagzone (1-5)

Afstand, ronde, baan

PadTypeBeschrijving
distance.totalnumberTotale afstand (meter)
lap.indexnumberHuidig rondenummer
lap.countnumberTotaal voltooide rondes
track.lengthnumberLengte van de baan (meter)

Wedstrijd- en multiplayermetrics

Deze zijn alleen beschikbaar in een wedstrijdcontext:

PadTypeBeschrijving
position.currentnumberHuidige positie in de wedstrijd (1 = eerste)
gap.aheadnumberVerschil met renner voor je
gap.behindnumberVerschil met renner achter je
overtakes.countnumberAantal keer dat je inhaalde

Fases

Fases bepalen de opbouw van een challenge als een geordende reeks segmenten. Elke fase heeft een eigen duur, doelen, exitcondities en directives.

Opbouw van een fase

{
  "id": "effort",
  "name": "Max Effort",
  "duration": 30,
  "description": "Give everything for 30 seconds",
  "targets": { },
  "exitConditions": { },
  "directives": [ ]
}
VeldTypeVerplichtBeschrijving
idstringJaUnieke ID voor deze fase
namestringJaWeergavenaam
durationnumber/stringNeeDuur van de fase in seconden, of "unlimited"
distancenumberNeeAfstand van de fase in meters (alternatief voor duur)
targetsobjectNeeDoelmetrics voor deze fase
exitConditionsobjectNeeCondities die deze fase eerder beëindigen

Doelen

Doelen bepalen de verwachte of vereiste metrics tijdens een fase:

{
  "targets": {
    "power": { "min": 80, "max": 150 },
    "cadence": { "min": 90, "max": 100 }
  }
}

Oplopende doelen

{
  "power": {
    "mode": "ramp",
    "start": 100,
    "increment": 1,
    "per": "second"
  }
}

Omstandigheden

Een conditie is één toetsbare expressie die waar of onwaar oplevert.

Opbouw van een conditie

{
  "metric": "power.wkg",
  "operator": ">=",
  "value": 5.0,
  "duration": 10,
  "window": "sustained"
}
VeldTypeVerplichtBeschrijving
metricstringJaMetric-pad om te evalueren
operatorstringJaVergelijkingsoperator
valuenumber/arrayJaDoelwaarde(n)
durationnumberNeeTijdslimiet in seconden
windowstringNeeHoe de duur wordt geëvalueerd

Operatoren

Vergelijking

  • == Gelijk aan
  • != Niet gelijk aan
  • > Groter dan
  • < Kleiner dan
  • >= Groter dan of gelijk aan
  • <= Kleiner dan of gelijk aan

Bereik

  • between Inclusief bereik
  • outside Buiten bereik

Wijzigen

  • increasedBy Delta omhoog
  • decreasedBy Delta omlaag

Duurvensters

VensterBeschrijving
sustainedConditie moet de hele duur onafgebroken waar zijn (standaard)
cumulativeConditie moet in totaal de hele duur waar zijn (mag onderbroken)
rollingGeëvalueerd over een voortschrijdend venster van de opgegeven duur
anyConditie moet op enig moment gedurende de duur waar zijn

Logische poorten

Condities kunnen worden gecombineerd met logische poorten.

Typen poorten

PoortBeschrijving
allAlle condities moeten waar zijn (AND)
anyMinstens één conditie moet waar zijn (OR)
noneGeen enkele conditie mag waar zijn (NOT ANY)
sequenceCondities moeten op volgorde waar worden

Voorbeeld: geneste poorten

{
  "all": [
    { "metric": "time.elapsed", "operator": ">=", "value": 60 },
    {
      "any": [
        { "metric": "power.wkg", "operator": ">=", "value": 5.0 },
        { "metric": "position.current", "operator": "==", "value": 1 }
      ]
    }
  ]
}

"Haal na 60 seconden 5.0 w/kg OF lig op de eerste plaats"

Levenscyclus

De levenscyclus bepaalt wanneer de evaluatie van een challenge begint en eindigt.

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   PENDING   │ ──▶ │   ARMING    │ ──▶ │   ACTIVE    │ ──▶ │  COMPLETE   │
└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘
                          │                    │
                          │                    ▼
                          │              ┌─────────────┐
                          └────────────▶ │   FAILED    │
                                         └─────────────┘
FaseBeschrijving
pendingChallenge geladen maar niet gestart
armingWachten op startcondities
activeCriteria voor slagen/falen evalueren
completeAan succescriteria voldaan
failedFaalcriteria gehaald of afgebroken

Opbouw van de levenscyclus

{
  "lifecycle": {
    "arming": {
      "all": [
        { "metric": "time.elapsed", "operator": ">=", "value": 120 }
      ]
    },
    "timeout": 600,
    "maxAttempts": 3
  }
}

Criteria voor succes en falen

Opbouw van criteria

{
  "criteria": {
    "success": {
      "all": [
        { "metric": "power.wkg", "operator": ">=", "value": 5.0, "duration": 10 }
      ]
    },
    "failure": {
      "any": [
        { "metric": "power.instant", "operator": "==", "value": 0, "duration": 5 }
      ]
    }
  }
}

Challenges met alleen succes

De meeste challenges bepalen alleen succescriteria. De challenge loopt door tot succes of tot de gebruiker stopt.

Challenges met faalcondities

{
  "criteria": {
    "success": {
      "all": [
        { "metric": "lap.count", "operator": ">=", "value": 5 }
      ]
    },
    "failure": {
      "any": [
        { "metric": "position.current", "operator": ">", "value": 3 }
      ]
    }
  }
}

"Rijd 5 rondes en blijf in de top 3. Zak je naar de 4e plek of lager, dan heb je verloren."

Lussen

Lussen maken herhalende evaluatieblokken mogelijk met logica die rekening houdt met iteraties. Dat is essentieel voor challenges met meerdere intervallen, oplopende moeilijkheid of herhaalde acties.

Opbouw van een lus

{
  "loops": [
    {
      "id": "sprint_intervals",
      "count": 5,
      "trigger": {
        "metric": "lap.index",
        "operator": "increasedBy",
        "value": 1
      },
      "onIteration": {
        "directives": [ ],
        "criteria": { }
      },
      "onComplete": {
        "directives": [ ]
      }
    }
  ]
}

Contextvariabelen voor iteraties

WisselendBeschrijving
{{loopIndex}}Huidige iteratie (vanaf 0)
{{loopCount}}Totaal ingestelde iteraties
{{loopRemaining}}Resterende iteraties

Voorbeeld met oplopende moeilijkheid

{
  "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 intervallen met oplopend vermogen: 3.0, 3.5, 4.0, 4.5, 5.0 w/kg"

Directives

Directives zijn optionele runtimegedragingen die de hostapplicatie kan uitvoeren. Ze zijn advisory: de host beslist of hij ze opvolgt.

Opbouw van een directive

{
  "directives": [
    {
      "id": "gap_warning",
      "type": "notify",
      "when": {
        "metric": "gap.ahead",
        "operator": ">",
        "value": 3.0
      },
      "params": {
        "message": "Gap widening! Push harder.",
        "priority": "high"
      },
      "cooldown": 10
    }
  ]
}

Typen directives

notify

Toon een bericht aan de renner

{
  "type": "notify",
  "params": {
    "message": "Push harder!",
    "priority": "normal | high | critical"
  }
}

sound

Een geluidssignaal afspelen

{
  "type": "sound",
  "params": {
    "sound": "bell | chime | warning | success"
  }
}

suggest

Stel de renner een doel voor (advies)

{
  "type": "suggest",
  "params": {
    "metric": "power.instant",
    "target": 250
  }
}

highlight

Een metric in de interface uitlichten

{
  "type": "highlight",
  "params": {
    "metric": "power.wkg",
    "style": "pulse | glow | flash"
  }
}

Record (challenges met beste poging)

Bij mode: "record"-challenges bepaalt het record-object welke metrics als resultaat van de challenge worden vastgelegd.

Opbouw van een 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" }
    ]
  }
}

Veelgebruikte primaire metrics

Type challengePrimaire metricVoorbeeld
Maximale inspanningpower.maxPiekvermogen
Tijdritduration voor faseSprinttijd 200 m
Duurinspanning vasthoudenduration voor faseTijd op doel gehouden
Afstanddistance.totalAfgelegde meters

Voltooiing

Het completion-object bepaalt hoe een challenge eindigt en wat er in het overzicht wordt getoond.

Voltooiingsstructuur

{
  "completion": {
    "condition": "phases-complete | exit-or-phases-complete | criteria-met",
    "summary": {
      "headline": "{{record.primary.value}}W",
      "subheadline": "Peak Power"
    }
  }
}

Voltooiingscondities

ConditieBeschrijving
phases-completeChallenge eindigt als alle fases voltooid zijn
exit-or-phases-completeChallenge eindigt bij de exitconditie van een fase OF als alle fases voltooid zijn
criteria-metChallenge eindigt als aan de succescriteria is voldaan (pass-fail-modus)

Metadata

Metadata geeft hostapplicaties weergavehints en een indeling in categorieën.

{
  "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
    }
  }
}
VeldTypeBeschrijving
difficultynumberMoeilijkheidsgraad van 1-5
estimatedDurationnumberVerwachte duur in seconden
tagsarrayCategorietags
authorstringMaker van de challenge
iconstringIcoon-ID
colorstringAccentkleur (hex)
requirementsobjectBenodigde sensoren/apparatuur

Volledig voorbeeld

{
  "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"
  }
}

Evaluatiemodel

Evaluatielus

De challenge-evaluator draait op een vaste frequentie (aanbevolen: 10Hz).

┌─────────────────────────────────────────────────────────────┐
│                      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                                │
└─────────────────────────────────────────────────────────────┘

Evaluatieresultaat

{
  "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
}

Bijlagen

A. Omgeving en overdraagbaarheid

Challenges kunnen verwijzen naar omgevingsmetrics, waarden die de hostapplicatie aanlevert en die de context beschrijven waarin de challenge draait.

Spectrum van overdraagbaarheid

  • Volledig overdraagbaar: Challenges die alleen naar kernmetrics verwijzen (vermogen, cadans, hartslag, tijd, afstand) werken op elke compatibele host
  • Afhankelijk van de omgeving: Challenges die naar omgevingsmetrics verwijzen, vereisen dat de host die waarden aanlevert
  • Platformspecifiek: Sommige challenges verwijzen naar functies die uniek zijn voor een bepaald platform

Terugvalwaarden

{
  "config": {
    "approachDistance": {
      "value": "{{track.length}}",
      "fallback": 250
    }
  }
}

B. Gereserveerde metric-paden

De volgende paden zijn gereserveerd voor later gebruik:

  • rowing.* - Roeispecifieke metrics
  • running.* - Loopspecifieke metrics
  • strength.* - Metrics voor krachttraining
  • custom.* - Door de host bepaalde eigen metrics

C. Implementatienotities voor de host

Hosts moeten een metric-adapter implementeren die hun interne data koppelt aan OCP-metricpaden:

interface MetricAdapter {
  resolve(path: string): number | undefined;
  getProfile(): { ftp?: number; weight?: number; maxHr?: number };
}

D. Versiegeschiedenis

VersieDatumWijzigingen
0.1.0-draft2025-01Eerste conceptspecificatie