Spécification
Open Challenge Protocol
Un format JSON déclaratif pour décrire des défis sportifs sur toutes les plateformes.
Aperçu
L'Open Challenge Protocol (OCP) est un format JSON déclaratif pour décrire des défis sportifs. Il définit ce qui constitue la réussite, pas comment une application hôte doit mettre en œuvre l'expérience.
Un défi est un objectif structuré qui évalue la performance du cycliste selon des critères définis, avec en option des phases de cycle de vie, des directives d'exécution et une logique d'itération/boucle.
Défis et séances
L'OCP est conçu spécifiquement pour les Défis, pas pour les séances. Il est essentiel de bien comprendre cette distinction.
Défis
- Dynamiques et orientés objectif : La réussite dépend du respect de critères précis
- Basé sur la logique : Construit autour de conditions, de seuils et de critères de sortie
- Durée souvent indéterminée : Beaucoup se terminent quand tu ne parviens plus à maintenir une condition
- Réussite/échec ou record à battre : Soit tu atteins l'objectif, soit tu te mesures à un record personnel
Exemples
- Tenir 5,0 w/kg pendant 10 secondes
- Sprinter 200 m le plus vite possible
- Rouler le plus loin possible en gardant la FC sous la zone 4
- Accélérer de l'arrêt à 40 km/h
Séances
- Structurées et prédéterminées : Suivre une séquence fixe d'intervalles
- Basé sur le temps/la durée : Les intervalles ont des durées précises
- Achèvement = réussite : Si tu termines tous les intervalles, tu as « terminé » la séance
- Orientées entraînement : Conçu pour l'adaptation physiologique
Inadapté à l'OCP
- 5x (30 secondes d'effort / 30 secondes de récup)
- Tabata : 8x (20 secondes / 10 secondes)
- Intervalles en pyramide de 1-2-3-2-1 minutes
- 2x20 minutes à la FTP
En cas de doute : Demande-toi : « La réussite dépend-elle de comment le cycliste performe, ou simplement du fait que le cycliste termine la structure ? » Si la réussite dépend du maintien de conditions → Défi. Si la réussite consiste juste à terminer les intervalles → Séance.
Structure du défi
Un défi est un objet JSON avec les champs de premier niveau suivants :
{
"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": { }
}Champs obligatoires
| Champ | Type | Description |
|---|---|---|
id | chaîne | Identifiant unique de ce défi |
version | chaîne | Version du protocole (semver) |
name | chaîne | Nom du défi lisible par un humain |
phases | tableau | Liste ordonnée des phases du défi (OU criteria pour les défis simples) |
Champs facultatifs
| Champ | Type | Description |
|---|---|---|
description | chaîne | Description détaillée du défi |
sport | chaîne | Type de sport (cycling, running, rowing, any) |
mode | chaîne | record (meilleur effort) ou pass-fail (réussite binaire). Par défaut : pass-fail |
config | objet | Configuration et valeurs calculées à l'échelle du défi |
criteria | objet | Critères de réussite (et d'échec en option) pour les défis simples |
loops | tableau | Blocs d'évaluation répétés |
directives | tableau | Comportements d'exécution (notifications, suggestions) |
record | objet | Métriques à enregistrer comme résultat du défi |
completion | objet | Comment le défi se termine et quoi afficher |
metadata | objet | Afficher des indices, la difficulté, des tags, etc. |
Modes de défi
pass-fail (par défaut) : défi classique avec critères de réussite/échec. Résultat binaire.
record : défi au meilleur effort, où le but est d'obtenir le meilleur résultat possible. Aucun état d'échec : le défi enregistre ta performance.
Mesures
Les métriques sont les points de données canoniques que les défis évaluent. Les applications hôtes relient leurs données brutes à ces chemins.
Format des chemins de métriques
Les métriques utilisent la notation pointée : 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)
Métriques de base
Temps
| Chemin | Type | Description |
|---|---|---|
time.elapsed | nombre | Secondes depuis le début du défi |
time.session | nombre | Secondes depuis le début de la sortie |
Puissance
| Chemin | Type | Description |
|---|---|---|
power.instant | nombre | Puissance actuelle (watts) |
power.avg | nombre | Puissance moyenne sur la session |
power.normalized | nombre | Puissance normalisée |
power.wkg | nombre | Watts par kilogramme actuels |
power.zone | nombre | Zone de puissance actuelle (1-7) |
power.percentFtp | nombre | Puissance actuelle en % de la FTP |
Vitesse, cadence, fréquence cardiaque
| Chemin | Type | Description |
|---|---|---|
speed.instant | nombre | Vitesse actuelle (m/s ou km/h selon l'hôte) |
cadence.instant | nombre | Cadence actuelle (RPM) |
heartRate.instant | nombre | Fréquence cardiaque actuelle (BPM) |
heartRate.zone | nombre | Zone FC actuelle (1-5) |
Distance, Lap, Track
| Chemin | Type | Description |
|---|---|---|
distance.total | nombre | Distance totale (mètres) |
lap.index | nombre | Numéro du tour actuel |
lap.count | nombre | Nombre total de tours effectués |
track.length | nombre | Longueur de la piste (mètres) |
Métriques compétition/multijoueur
Ces métriques ne sont disponibles qu'en contexte de compétition :
| Chemin | Type | Description |
|---|---|---|
position.current | nombre | Position actuelle en compétition (1 = premier) |
gap.ahead | nombre | Écart avec le coureur devant |
gap.behind | nombre | Écart avec le coureur derrière |
overtakes.count | nombre | Nombre de dépassements effectués |
Phases
Les phases définissent la structure d'un défi sous forme de séquence ordonnée de segments. Chaque phase a sa propre durée, ses cibles, ses conditions de sortie et ses directives.
Structure d'une phase
{
"id": "effort",
"name": "Max Effort",
"duration": 30,
"description": "Give everything for 30 seconds",
"targets": { },
"exitConditions": { },
"directives": [ ]
}| Champ | Type | Obligatoire | Description |
|---|---|---|---|
id | chaîne | Oui | Identifiant unique de cette phase |
name | chaîne | Oui | Nom affiché |
duration | nombre/chaîne | Non | Durée de la phase en secondes, ou "unlimited" |
distance | nombre | Non | Distance de la phase en mètres (alternative à la durée) |
targets | objet | Non | Métriques cibles pour cette phase |
exitConditions | objet | Non | Conditions qui terminent cette phase plus tôt |
Cibles
Les cibles définissent les métriques attendues ou requises pendant une phase :
{
"targets": {
"power": { "min": 80, "max": 150 },
"cadence": { "min": 90, "max": 100 }
}
}Cibles progressives
{
"power": {
"mode": "ramp",
"start": 100,
"increment": 1,
"per": "second"
}
}Conditions
Une condition est une expression testable unique qui renvoie vrai ou faux.
Structure d'une condition
{
"metric": "power.wkg",
"operator": ">=",
"value": 5.0,
"duration": 10,
"window": "sustained"
}| Champ | Type | Obligatoire | Description |
|---|---|---|---|
metric | chaîne | Oui | Chemin de la métrique à évaluer |
operator | chaîne | Oui | Opérateur de comparaison |
value | nombre/tableau | Oui | Valeur(s) cible(s) |
duration | nombre | Non | Contrainte de temps en secondes |
window | chaîne | Non | Comment la durée est évaluée |
Opérateurs
Comparaison
==Égal à!=Différent de>Supérieur à<Inférieur à>=Supérieur ou égal à<=Inférieur ou égal à
Plage
betweenPlage inclusiveoutsideHors plage
Changer
increasedByAugmentation par deltadecreasedByDiminution par delta
Fenêtres de durée
| Fenêtre | Description |
|---|---|
sustained | La condition doit être vraie en continu pendant la durée (par défaut) |
cumulative | La condition doit être vraie pendant la durée totale (pas forcément en continu) |
rolling | Évalué sur une fenêtre glissante de la durée indiquée |
any | La condition doit être vraie à un moment donné pendant la durée |
Portes logiques
Les conditions peuvent être combinées avec des portes logiques.
Types de portes
| Barrière | Description |
|---|---|
all | Toutes les conditions doivent être vraies (ET) |
any | Au moins une condition doit être vraie (OU) |
none | Aucune condition ne peut être vraie (AUCUNE) |
sequence | Les conditions doivent devenir vraies dans l'ordre |
Exemple : portes imbriquées
{
"all": [
{ "metric": "time.elapsed", "operator": ">=", "value": 60 },
{
"any": [
{ "metric": "power.wkg", "operator": ">=", "value": 5.0 },
{ "metric": "position.current", "operator": "==", "value": 1 }
]
}
]
}« Après 60 secondes, atteindre 5,0 w/kg OU être en tête »
Cycle de vie
Le cycle de vie définit quand l'évaluation du défi commence et se termine.
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ PENDING │ ──▶ │ ARMING │ ──▶ │ ACTIVE │ ──▶ │ COMPLETE │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│ │
│ ▼
│ ┌─────────────┐
└────────────▶ │ FAILED │
└─────────────┘| Phase | Description |
|---|---|
pending | Défi chargé mais pas démarré |
arming | En attente des conditions de départ |
active | Évaluation des critères de réussite/échec |
complete | Critères de réussite atteints |
failed | Critères d'échec atteints ou abandon |
Structure du cycle de vie
{
"lifecycle": {
"arming": {
"all": [
{ "metric": "time.elapsed", "operator": ">=", "value": 120 }
]
},
"timeout": 600,
"maxAttempts": 3
}
}Critères de réussite et d'échec
Structure des critères
{
"criteria": {
"success": {
"all": [
{ "metric": "power.wkg", "operator": ">=", "value": 5.0, "duration": 10 }
]
},
"failure": {
"any": [
{ "metric": "power.instant", "operator": "==", "value": 0, "duration": 5 }
]
}
}
}Défis à réussite seule
La plupart des défis ne définissent que des critères de réussite. Le défi continue jusqu'à la réussite ou l'abandon de l'utilisateur.
Défis avec conditions d'échec
{
"criteria": {
"success": {
"all": [
{ "metric": "lap.count", "operator": ">=", "value": 5 }
]
},
"failure": {
"any": [
{ "metric": "position.current", "operator": ">", "value": 3 }
]
}
}
}« Boucler 5 tours en restant dans le top 3. Tomber 4e ou pire = échec. »
Boucles
Les boucles permettent de répéter des blocs d'évaluation avec une logique tenant compte de l'itération. C'est indispensable pour les défis qui comportent plusieurs intervalles, une difficulté progressive ou des actions répétées.
Structure de boucle
{
"loops": [
{
"id": "sprint_intervals",
"count": 5,
"trigger": {
"metric": "lap.index",
"operator": "increasedBy",
"value": 1
},
"onIteration": {
"directives": [ ],
"criteria": { }
},
"onComplete": {
"directives": [ ]
}
}
]
}Variables de contexte d'itération
| Irrégulier | Description |
|---|---|
{{loopIndex}} | Itération actuelle (indexée à partir de 0) |
{{loopCount}} | Nombre total d'itérations configurées |
{{loopRemaining}} | Itérations restantes |
Exemple de difficulté progressive
{
"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 intervalles à puissance croissante : 3,0, 3,5, 4,0, 4,5, 5,0 w/kg »
Directives
Les directives sont des comportements d'exécution optionnels que l'application hôte peut appliquer. Elles sont indicatif : l'hôte décide de les suivre ou non.
Structure d'une 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
}
]
}Types de directives
notify
Afficher un message au cycliste
{
"type": "notify",
"params": {
"message": "Push harder!",
"priority": "normal | high | critical"
}
}sound
Jouer un signal sonore
{
"type": "sound",
"params": {
"sound": "bell | chime | warning | success"
}
}suggest
Suggérer une cible au cycliste (indicatif)
{
"type": "suggest",
"params": {
"metric": "power.instant",
"target": 250
}
}highlight
Mettre une métrique en évidence dans l'interface
{
"type": "highlight",
"params": {
"metric": "power.wkg",
"style": "pulse | glow | flash"
}
}Record (défis au meilleur effort)
Pour les défis mode: "record", l'objet record définit les métriques à enregistrer comme résultat du défi.
Structure du 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" }
]
}
}Métriques principales courantes
| Type de défi | Métrique principale | Exemple |
|---|---|---|
| Effort maximal | power.max | Puissance max |
| Contre-la-montre | duration pour la phase | Temps de sprint sur 200 m |
| Maintien d'endurance | duration pour la phase | Temps tenu à la cible |
| Distance | distance.total | Mètres parcourus |
Achèvement
L'objet completion définit comment un défi se termine et ce qu'il faut afficher dans le résumé.
Structure d'achèvement
{
"completion": {
"condition": "phases-complete | exit-or-phases-complete | criteria-met",
"summary": {
"headline": "{{record.primary.value}}W",
"subheadline": "Peak Power"
}
}
}Conditions d'achèvement
| Condition | Description |
|---|---|
phases-complete | Le défi se termine quand toutes les phases sont terminées |
exit-or-phases-complete | Le défi se termine sur la condition de sortie de phase OU quand toutes les phases sont terminées |
criteria-met | Le défi se termine quand les critères de réussite sont atteints (mode réussite-échec) |
Métadonnées
Les métadonnées fournissent des indications d'affichage et une catégorisation pour les applications hôtes.
{
"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
}
}
}| Champ | Type | Description |
|---|---|---|
difficulty | nombre | Note de difficulté de 1 à 5 |
estimatedDuration | nombre | Durée prévue en secondes |
tags | tableau | Tags de catégorisation |
author | chaîne | Créateur du défi |
icon | chaîne | Identifiant de l'icône |
color | chaîne | Couleur d'accent (hex) |
requirements | objet | Capteurs/équipement requis |
Exemple complet
{
"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"
}
}Modèle d'évaluation
Boucle d'évaluation
L'évaluateur de défis tourne à une fréquence fixe (recommandée : 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 │ └─────────────────────────────────────────────────────────────┘
Résultat de l'évaluation
{
"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
}Annexes
A. Environnement et portabilité
Les défis peuvent faire référence à des métriques d'environnement, des valeurs fournies par l'application hôte qui décrivent le contexte dans lequel le défi se déroule.
Spectre de portabilité
- Entièrement portable : Les défis qui ne font référence qu'aux métriques de base (puissance, cadence, fréquence cardiaque, temps, distance) fonctionnent sur n'importe quel hôte compatible
- Dépendant de l'environnement : Les défis qui font référence à des métriques d'environnement exigent que l'hôte fournisse ces valeurs
- Spécifique à la plateforme : Certains défis peuvent faire référence à des fonctionnalités propres à une plateforme
Valeurs de repli
{
"config": {
"approachDistance": {
"value": "{{track.length}}",
"fallback": 250
}
}
}B. Chemins de métriques réservés
Les chemins suivants sont réservés pour un usage futur :
rowing.*- Métriques propres à l'avironrunning.*- Métriques propres à la course à piedstrength.*- Métriques de renforcementcustom.*- Métriques personnalisées définies par l'hôte
C. Notes d'implémentation pour l'hôte
Les hôtes doivent implémenter un adaptateur de métriques qui relie leurs données internes aux chemins de métriques OCP :
interface MetricAdapter {
resolve(path: string): number | undefined;
getProfile(): { ftp?: number; weight?: number; maxHr?: number };
}D. Historique des versions
| Version | Date | Modifications |
|---|---|---|
| 0.1.0-draft | 2025-01 | Version initiale du projet de spécification |
