Especificación
Open Challenge Protocol
Un formato JSON declarativo para expresar desafíos de entrenamiento entre plataformas.
Resumen
El Open Challenge Protocol (OCP) es un formato JSON declarativo para expresar desafíos de entrenamiento. Define qué constituye el éxito, no cómo debe implementar la experiencia una aplicación host.
Un desafío es una meta estructurada que evalúa el rendimiento del ciclista frente a criterios definidos, con fases de ciclo de vida opcionales, directivas en tiempo de ejecución y lógica de iteración/bucles.
Desafíos vs entrenos
OCP está diseñado específicamente para Desafíos, no para entrenos. Entender esta diferencia es fundamental.
Desafíos
- Dinámico y orientado a metas: El éxito se determina al cumplir criterios específicos
- Basados en lógica: Construido en torno a condiciones, umbrales y criterios de salida
- Duración a menudo indeterminada: Muchos terminan cuando no logras mantener una condición
- Aprobado/no aprobado o romper récords: O logras la meta o compites contra una marca personal
Ejemplos
- Mantén 5.0 w/kg durante 10 segundos
- Esprinta 200 m lo más rápido posible
- Rueda lo más lejos posible manteniendo la FC por debajo de la zona 4
- Acelerar desde parado hasta 40 km/h
Entrenos
- Estructurado y predeterminado: Seguir una secuencia fija de intervalos
- Basados en tiempo/duración: Los intervalos tienen duraciones específicas
- Finalización = éxito: Si terminas todos los intervalos, "completaste" el entreno
- Enfocados en el entrenamiento: Diseñado para la adaptación fisiológica
No apto para OCP
- 5x (30 segundos fuerte / 30 segundos suave)
- Tabata: 8x (20 segundos / 10 segundos)
- Intervalos en escalera de 1-2-3-2-1 minutos
- 2x20 minutos al FTP
En caso de duda: Pregúntate: "¿El éxito depende de cómo rinde el ciclista, o solo de que complete la estructura?" Si el éxito depende de mantener condiciones → Desafío. Si el éxito es solo completar intervalos → Entreno.
Estructura del desafío
Un desafío es un objeto JSON con los siguientes campos de nivel superior:
{
"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": { }
}Campos obligatorios
| Campo | Tipo | Descripción |
|---|---|---|
id | string | Identificador único de este desafío |
version | string | Versión del protocolo (semver) |
name | string | Nombre legible del desafío |
phases | array | Lista ordenada de fases del desafío (O criteria para desafíos simples) |
Campos opcionales
| Campo | Tipo | Descripción |
|---|---|---|
description | string | Descripción más larga del desafío |
sport | string | Tipo de deporte (cycling, running, rowing, any) |
mode | string | record (mejor esfuerzo) o pass-fail (éxito binario). Predeterminado: pass-fail |
config | object | Configuración y valores calculados de todo el desafío |
criteria | object | Criterios de éxito (y de fallo, opcionales) para desafíos simples |
loops | array | Bloques de evaluación que se repiten |
directives | array | Comportamientos en tiempo de ejecución (notificaciones, sugerencias) |
record | object | Qué métricas capturar como resultado del desafío |
completion | object | Cómo termina el desafío y qué mostrar |
metadata | object | Sugerencias de visualización, dificultad, etiquetas, etc. |
Modos de desafío
pass-fail (predeterminado): desafío tradicional con criterios de éxito o fallo. Resultado binario.
record: desafío de mejor esfuerzo donde la meta es lograr el mejor resultado posible. No hay estado de fallo: el desafío registra tu rendimiento.
Métricas
Las métricas son los datos canónicos que evalúan los desafíos. Las aplicaciones host asignan sus datos en bruto a estas rutas.
Formato de ruta de métricas
Las métricas usan notación de puntos: 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étricas básicas
Tiempo
| Ruta | Tipo | Descripción |
|---|---|---|
time.elapsed | number | Segundos desde que empezó el desafío |
time.session | number | Segundos desde que empezó la rodada |
Potencia
| Ruta | Tipo | Descripción |
|---|---|---|
power.instant | number | Potencia actual (vatios) |
power.avg | number | Potencia promedio de la sesión |
power.normalized | number | Potencia normalizada |
power.wkg | number | Vatios por kilogramo actuales |
power.zone | number | Zona de potencia actual (1-7) |
power.percentFtp | number | Potencia actual como % del FTP |
Velocidad, cadencia, frecuencia cardíaca
| Ruta | Tipo | Descripción |
|---|---|---|
speed.instant | number | Velocidad actual (m/s o km/h según el host) |
cadence.instant | number | Cadencia actual (RPM) |
heartRate.instant | number | Frecuencia cardíaca actual (ppm) |
heartRate.zone | number | Zona de FC actual (1-5) |
Distancia, vuelta, pista
| Ruta | Tipo | Descripción |
|---|---|---|
distance.total | number | Distancia total (metros) |
lap.index | number | Número de vuelta actual |
lap.count | number | Total de vueltas completadas |
track.length | number | Longitud de la pista (metros) |
Métricas de competición/multijugador
Solo están disponibles en contextos de competición:
| Ruta | Tipo | Descripción |
|---|---|---|
position.current | number | Posición actual en la competición (1 = primero) |
gap.ahead | number | Diferencia con el ciclista de adelante |
gap.behind | number | Diferencia con el ciclista de atrás |
overtakes.count | number | Número de rebases realizados |
Fases
Las fases definen la estructura de un desafío como una secuencia ordenada de segmentos. Cada fase tiene su propia duración, objetivos, condiciones de salida y directivas.
Estructura de la fase
{
"id": "effort",
"name": "Max Effort",
"duration": 30,
"description": "Give everything for 30 seconds",
"targets": { },
"exitConditions": { },
"directives": [ ]
}| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | string | Sí | Identificador único de esta fase |
name | string | Sí | Nombre visible |
duration | number/string | No | Duración de la fase en segundos, o "unlimited" |
distance | number | No | Distancia de la fase en metros (alternativa a la duración) |
targets | object | No | Métricas objetivo de esta fase |
exitConditions | object | No | Condiciones que terminan esta fase antes de tiempo |
Objetivos
Los objetivos definen las métricas esperadas o requeridas durante una fase:
{
"targets": {
"power": { "min": 80, "max": 150 },
"cadence": { "min": 90, "max": 100 }
}
}Objetivos en rampa
{
"power": {
"mode": "ramp",
"start": 100,
"increment": 1,
"per": "second"
}
}Condiciones
Una condición es una única expresión evaluable que da verdadero o falso.
Estructura de la condición
{
"metric": "power.wkg",
"operator": ">=",
"value": 5.0,
"duration": 10,
"window": "sustained"
}| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
metric | string | Sí | Ruta de la métrica a evaluar |
operator | string | Sí | Operador de comparación |
value | number/array | Sí | Valor(es) objetivo |
duration | number | No | Restricción de tiempo en segundos |
window | string | No | Cómo se evalúa la duración |
Operadores
Comparación
==Igual a!=Distinto de>Mayor que<Menor que>=Mayor o igual que<=Menor o igual que
Rango
betweenRango inclusivooutsideFuera de rango
Cambiar
increasedByAumento deltadecreasedByDisminución delta
Ventanas de duración
| Ventana | Descripción |
|---|---|
sustained | La condición debe cumplirse de forma continua durante la duración (predeterminado) |
cumulative | La condición debe cumplirse durante la duración total (puede no ser continua) |
rolling | Se evalúa en una ventana móvil de la duración indicada |
any | La condición debe cumplirse en algún momento durante la duración |
Compuertas lógicas
Las condiciones se pueden combinar con compuertas lógicas.
Tipos de compuerta
| Puerta | Descripción |
|---|---|
all | Todas las condiciones deben cumplirse (AND) |
any | Al menos una condición debe cumplirse (OR) |
none | Ninguna condición puede cumplirse (NOT ANY) |
sequence | Las condiciones deben cumplirse en orden |
Ejemplo: compuertas anidadas
{
"all": [
{ "metric": "time.elapsed", "operator": ">=", "value": 60 },
{
"any": [
{ "metric": "power.wkg", "operator": ">=", "value": 5.0 },
{ "metric": "position.current", "operator": "==", "value": 1 }
]
}
]
}"Después de 60 segundos, alcanza 5.0 w/kg O ponte en primer lugar"
Ciclo de vida
El ciclo de vida define cuándo empieza y termina la evaluación del desafío.
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ PENDING │ ──▶ │ ARMING │ ──▶ │ ACTIVE │ ──▶ │ COMPLETE │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│ │
│ ▼
│ ┌─────────────┐
└────────────▶ │ FAILED │
└─────────────┘| Fase | Descripción |
|---|---|
pending | Desafío cargado pero sin iniciar |
arming | Esperando las condiciones de inicio |
active | Evaluando criterios de éxito/fallo |
complete | Se cumplieron los criterios de éxito |
failed | Se cumplieron los criterios de fallo o se abandonó |
Estructura del ciclo de vida
{
"lifecycle": {
"arming": {
"all": [
{ "metric": "time.elapsed", "operator": ">=", "value": 120 }
]
},
"timeout": 600,
"maxAttempts": 3
}
}Criterios de éxito y fallo
Estructura de criterios
{
"criteria": {
"success": {
"all": [
{ "metric": "power.wkg", "operator": ">=", "value": 5.0, "duration": 10 }
]
},
"failure": {
"any": [
{ "metric": "power.instant", "operator": "==", "value": 0, "duration": 5 }
]
}
}
}Desafíos solo con éxito
La mayoría de los desafíos solo definen criterios de éxito. El desafío continúa hasta el éxito o hasta que el usuario lo abandona.
Desafíos con condiciones de fallo
{
"criteria": {
"success": {
"all": [
{ "metric": "lap.count", "operator": ">=", "value": 5 }
]
},
"failure": {
"any": [
{ "metric": "position.current", "operator": ">", "value": 3 }
]
}
}
}"Completa 5 vueltas sin salir del top 3. Caer al 4.º puesto o peor = fallo."
Bucles
Los bucles permiten repetir bloques de evaluación con lógica que tiene en cuenta la iteración. Es esencial para desafíos con varios intervalos, dificultad progresiva o acciones repetidas.
Estructura del bucle
{
"loops": [
{
"id": "sprint_intervals",
"count": 5,
"trigger": {
"metric": "lap.index",
"operator": "increasedBy",
"value": 1
},
"onIteration": {
"directives": [ ],
"criteria": { }
},
"onComplete": {
"directives": [ ]
}
}
]
}Variables de contexto de la iteración
| Variable | Descripción |
|---|---|
{{loopIndex}} | Iteración actual (empieza en 0) |
{{loopCount}} | Total de iteraciones configuradas |
{{loopRemaining}} | Iteraciones restantes |
Ejemplo de dificultad progresiva
{
"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 intervalos con potencia creciente: 3.0, 3.5, 4.0, 4.5, 5.0 w/kg"
Directivas
Las directivas son comportamientos opcionales en tiempo de ejecución que la aplicación host puede ejecutar. Son de carácter orientativo: el host decide si las respeta.
Estructura de la directiva
{
"directives": [
{
"id": "gap_warning",
"type": "notify",
"when": {
"metric": "gap.ahead",
"operator": ">",
"value": 3.0
},
"params": {
"message": "Gap widening! Push harder.",
"priority": "high"
},
"cooldown": 10
}
]
}Tipos de directiva
notify
Mostrar un mensaje al ciclista
{
"type": "notify",
"params": {
"message": "Push harder!",
"priority": "normal | high | critical"
}
}sound
Reproducir un aviso sonoro
{
"type": "sound",
"params": {
"sound": "bell | chime | warning | success"
}
}suggest
Sugerir un objetivo al ciclista (orientativo)
{
"type": "suggest",
"params": {
"metric": "power.instant",
"target": 250
}
}highlight
Resaltar una métrica en la interfaz
{
"type": "highlight",
"params": {
"metric": "power.wkg",
"style": "pulse | glow | flash"
}
}Record (desafíos de mejor esfuerzo)
En los desafíos mode: "record", el objeto record define qué métricas se capturan como resultado del desafío.
Estructura de 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étricas principales comunes
| Tipo de desafío | Métrica principal | Ejemplo |
|---|---|---|
| Esfuerzo máximo | power.max | Potencia pico |
| Contrarreloj | duration por fase | Tiempo en sprint de 200 m |
| Mantener resistencia | duration por fase | Tiempo sostenido en el objetivo |
| Distancia | distance.total | Metros recorridos |
Finalización
El objeto completion define cómo termina un desafío y qué mostrar en el resumen.
Estructura de finalización
{
"completion": {
"condition": "phases-complete | exit-or-phases-complete | criteria-met",
"summary": {
"headline": "{{record.primary.value}}W",
"subheadline": "Peak Power"
}
}
}Condiciones de finalización
| Condición | Descripción |
|---|---|
phases-complete | El desafío termina cuando se completan todas las fases |
exit-or-phases-complete | El desafío termina con la condición de salida de la fase O al completar todas las fases |
criteria-met | El desafío termina cuando se cumplen los criterios de éxito (modo pass-fail) |
Metadatos
Los metadatos dan sugerencias de visualización y categorización a las aplicaciones host.
{
"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
}
}
}| Campo | Tipo | Descripción |
|---|---|---|
difficulty | number | Calificación de dificultad del 1 al 5 |
estimatedDuration | number | Duración esperada en segundos |
tags | array | Etiquetas de categorización |
author | string | Creador del desafío |
icon | string | Identificador del ícono |
color | string | Color de acento (hex) |
requirements | object | Sensores/equipo necesarios |
Ejemplo completo
{
"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"
}
}Modelo de evaluación
Bucle de evaluación
El evaluador de desafíos se ejecuta a una frecuencia fija (recomendado: 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 │ └─────────────────────────────────────────────────────────────┘
Resultado de la evaluación
{
"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
}Apéndices
A. Entorno y portabilidad
Los desafíos pueden hacer referencia a métricas de entorno, valores que proporciona la aplicación host y que describen el contexto en el que se ejecuta el desafío.
Espectro de portabilidad
- Totalmente portable: Los desafíos que solo usan métricas básicas (potencia, cadencia, frecuencia cardíaca, tiempo, distancia) funcionan en cualquier host compatible
- Dependientes del entorno: Los desafíos que usan métricas de entorno requieren que el host proporcione esos valores
- Específicos de la plataforma: Algunos desafíos pueden usar funciones exclusivas de una plataforma concreta
Valores de respaldo
{
"config": {
"approachDistance": {
"value": "{{track.length}}",
"fallback": 250
}
}
}B. Rutas de métricas reservadas
Las siguientes rutas están reservadas para uso futuro:
rowing.*- Métricas específicas de remorunning.*- Métricas específicas de carrerastrength.*- Métricas de entrenamiento de fuerzacustom.*- Métricas personalizadas definidas por el host
C. Notas de implementación para el host
Los hosts deben implementar un adaptador de métricas que asigne sus datos internos a las rutas de métricas de OCP:
interface MetricAdapter {
resolve(path: string): number | undefined;
getProfile(): { ftp?: number; weight?: number; maxHr?: number };
}D. Historial de versiones
| Versión | Fecha | Cambios |
|---|---|---|
| 0.1.0-draft | 2025-01 | Especificación inicial en borrador |
