Specificatie
Open Challenge Protocol
Een declaratief JSON-formaat om fitnesschallenges over platforms heen te beschrijven.
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
| Veld | Type | Beschrijving |
|---|---|---|
id | string | Unieke ID voor deze challenge |
version | string | Protocolversie (semver) |
name | string | Leesbare naam van de challenge |
phases | array | Geordende lijst van challengefases (OF criteria voor eenvoudige challenges) |
Optionele velden
| Veld | Type | Beschrijving |
|---|---|---|
description | string | Langere beschrijving van de challenge |
sport | string | Type sport (cycling, running, rowing, any) |
mode | string | record (beste poging) of pass-fail (binair succes). Standaard: pass-fail |
config | object | Configuratie en berekende waarden voor de hele challenge |
criteria | object | Criteria voor succes (en optioneel falen) bij eenvoudige challenges |
loops | array | Herhalende evaluatieblokken |
directives | array | Runtimegedrag (meldingen, suggesties) |
record | object | Welke metrics als resultaat van de challenge worden vastgelegd |
completion | object | Hoe de challenge eindigt en wat er getoond wordt |
metadata | object | Toon 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
| Pad | Type | Beschrijving |
|---|---|---|
time.elapsed | number | Seconden sinds de start van de challenge |
time.session | number | Seconden sinds de start van de ritsessie |
Vermogen
| Pad | Type | Beschrijving |
|---|---|---|
power.instant | number | Huidig vermogen (watt) |
power.avg | number | Gemiddeld vermogen voor de sessie |
power.normalized | number | Genormaliseerd vermogen |
power.wkg | number | Huidige watt per kilogram |
power.zone | number | Huidige vermogenszone (1-7) |
power.percentFtp | number | Huidig vermogen als % van FTP |
Snelheid, cadans, hartslag
| Pad | Type | Beschrijving |
|---|---|---|
speed.instant | number | Huidige snelheid (m/s of km/h, afhankelijk van de host) |
cadence.instant | number | Huidige cadans (RPM) |
heartRate.instant | number | Huidige hartslag (BPM) |
heartRate.zone | number | Huidige hartslagzone (1-5) |
Afstand, ronde, baan
| Pad | Type | Beschrijving |
|---|---|---|
distance.total | number | Totale afstand (meter) |
lap.index | number | Huidig rondenummer |
lap.count | number | Totaal voltooide rondes |
track.length | number | Lengte van de baan (meter) |
Wedstrijd- en multiplayermetrics
Deze zijn alleen beschikbaar in een wedstrijdcontext:
| Pad | Type | Beschrijving |
|---|---|---|
position.current | number | Huidige positie in de wedstrijd (1 = eerste) |
gap.ahead | number | Verschil met renner voor je |
gap.behind | number | Verschil met renner achter je |
overtakes.count | number | Aantal 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": [ ]
}| Veld | Type | Verplicht | Beschrijving |
|---|---|---|---|
id | string | Ja | Unieke ID voor deze fase |
name | string | Ja | Weergavenaam |
duration | number/string | Nee | Duur van de fase in seconden, of "unlimited" |
distance | number | Nee | Afstand van de fase in meters (alternatief voor duur) |
targets | object | Nee | Doelmetrics voor deze fase |
exitConditions | object | Nee | Condities 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"
}| Veld | Type | Verplicht | Beschrijving |
|---|---|---|---|
metric | string | Ja | Metric-pad om te evalueren |
operator | string | Ja | Vergelijkingsoperator |
value | number/array | Ja | Doelwaarde(n) |
duration | number | Nee | Tijdslimiet in seconden |
window | string | Nee | Hoe 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
betweenInclusief bereikoutsideBuiten bereik
Wijzigen
increasedByDelta omhoogdecreasedByDelta omlaag
Duurvensters
| Venster | Beschrijving |
|---|---|
sustained | Conditie moet de hele duur onafgebroken waar zijn (standaard) |
cumulative | Conditie moet in totaal de hele duur waar zijn (mag onderbroken) |
rolling | Geëvalueerd over een voortschrijdend venster van de opgegeven duur |
any | Conditie moet op enig moment gedurende de duur waar zijn |
Logische poorten
Condities kunnen worden gecombineerd met logische poorten.
Typen poorten
| Poort | Beschrijving |
|---|---|
all | Alle condities moeten waar zijn (AND) |
any | Minstens één conditie moet waar zijn (OR) |
none | Geen enkele conditie mag waar zijn (NOT ANY) |
sequence | Condities 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 │
└─────────────┘| Fase | Beschrijving |
|---|---|
pending | Challenge geladen maar niet gestart |
arming | Wachten op startcondities |
active | Criteria voor slagen/falen evalueren |
complete | Aan succescriteria voldaan |
failed | Faalcriteria 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
| Wisselend | Beschrijving |
|---|---|
{{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 challenge | Primaire metric | Voorbeeld |
|---|---|---|
| Maximale inspanning | power.max | Piekvermogen |
| Tijdrit | duration voor fase | Sprinttijd 200 m |
| Duurinspanning vasthouden | duration voor fase | Tijd op doel gehouden |
| Afstand | distance.total | Afgelegde 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
| Conditie | Beschrijving |
|---|---|
phases-complete | Challenge eindigt als alle fases voltooid zijn |
exit-or-phases-complete | Challenge eindigt bij de exitconditie van een fase OF als alle fases voltooid zijn |
criteria-met | Challenge 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
}
}
}| Veld | Type | Beschrijving |
|---|---|---|
difficulty | number | Moeilijkheidsgraad van 1-5 |
estimatedDuration | number | Verwachte duur in seconden |
tags | array | Categorietags |
author | string | Maker van de challenge |
icon | string | Icoon-ID |
color | string | Accentkleur (hex) |
requirements | object | Benodigde 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 metricsrunning.*- Loopspecifieke metricsstrength.*- Metrics voor krachttrainingcustom.*- 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
| Versie | Datum | Wijzigingen |
|---|---|---|
| 0.1.0-draft | 2025-01 | Eerste conceptspecificatie |
