Open Challenge Protocol (OCP) | Ride Cave

Especificación

Open Challenge Protocol

Un formato JSON declarativo para expresar desafíos de entrenamiento entre plataformas.

BorradorVersión 0.1.0

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

CampoTipoDescripción
idstringIdentificador único de este desafío
versionstringVersión del protocolo (semver)
namestringNombre legible del desafío
phasesarrayLista ordenada de fases del desafío (O criteria para desafíos simples)

Campos opcionales

CampoTipoDescripción
descriptionstringDescripción más larga del desafío
sportstringTipo de deporte (cycling, running, rowing, any)
modestringrecord (mejor esfuerzo) o pass-fail (éxito binario). Predeterminado: pass-fail
configobjectConfiguración y valores calculados de todo el desafío
criteriaobjectCriterios de éxito (y de fallo, opcionales) para desafíos simples
loopsarrayBloques de evaluación que se repiten
directivesarrayComportamientos en tiempo de ejecución (notificaciones, sugerencias)
recordobjectQué métricas capturar como resultado del desafío
completionobjectCómo termina el desafío y qué mostrar
metadataobjectSugerencias 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

RutaTipoDescripción
time.elapsednumberSegundos desde que empezó el desafío
time.sessionnumberSegundos desde que empezó la rodada

Potencia

RutaTipoDescripción
power.instantnumberPotencia actual (vatios)
power.avgnumberPotencia promedio de la sesión
power.normalizednumberPotencia normalizada
power.wkgnumberVatios por kilogramo actuales
power.zonenumberZona de potencia actual (1-7)
power.percentFtpnumberPotencia actual como % del FTP

Velocidad, cadencia, frecuencia cardíaca

RutaTipoDescripción
speed.instantnumberVelocidad actual (m/s o km/h según el host)
cadence.instantnumberCadencia actual (RPM)
heartRate.instantnumberFrecuencia cardíaca actual (ppm)
heartRate.zonenumberZona de FC actual (1-5)

Distancia, vuelta, pista

RutaTipoDescripción
distance.totalnumberDistancia total (metros)
lap.indexnumberNúmero de vuelta actual
lap.countnumberTotal de vueltas completadas
track.lengthnumberLongitud de la pista (metros)

Métricas de competición/multijugador

Solo están disponibles en contextos de competición:

RutaTipoDescripción
position.currentnumberPosición actual en la competición (1 = primero)
gap.aheadnumberDiferencia con el ciclista de adelante
gap.behindnumberDiferencia con el ciclista de atrás
overtakes.countnumberNú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": [ ]
}
CampoTipoObligatorioDescripción
idstringSíIdentificador único de esta fase
namestringSíNombre visible
durationnumber/stringNoDuración de la fase en segundos, o "unlimited"
distancenumberNoDistancia de la fase en metros (alternativa a la duración)
targetsobjectNoMétricas objetivo de esta fase
exitConditionsobjectNoCondiciones 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"
}
CampoTipoObligatorioDescripción
metricstringSíRuta de la métrica a evaluar
operatorstringSíOperador de comparación
valuenumber/arraySíValor(es) objetivo
durationnumberNoRestricción de tiempo en segundos
windowstringNoCó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

  • between Rango inclusivo
  • outside Fuera de rango

Cambiar

  • increasedBy Aumento delta
  • decreasedBy Disminución delta

Ventanas de duración

VentanaDescripción
sustainedLa condición debe cumplirse de forma continua durante la duración (predeterminado)
cumulativeLa condición debe cumplirse durante la duración total (puede no ser continua)
rollingSe evalúa en una ventana móvil de la duración indicada
anyLa 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

PuertaDescripción
allTodas las condiciones deben cumplirse (AND)
anyAl menos una condición debe cumplirse (OR)
noneNinguna condición puede cumplirse (NOT ANY)
sequenceLas 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    │
                                         └─────────────┘
FaseDescripción
pendingDesafío cargado pero sin iniciar
armingEsperando las condiciones de inicio
activeEvaluando criterios de éxito/fallo
completeSe cumplieron los criterios de éxito
failedSe 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

VariableDescripció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íoMétrica principalEjemplo
Esfuerzo máximopower.maxPotencia pico
Contrarrelojduration por faseTiempo en sprint de 200 m
Mantener resistenciaduration por faseTiempo sostenido en el objetivo
Distanciadistance.totalMetros 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ónDescripción
phases-completeEl desafío termina cuando se completan todas las fases
exit-or-phases-completeEl desafío termina con la condición de salida de la fase O al completar todas las fases
criteria-metEl 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
    }
  }
}
CampoTipoDescripción
difficultynumberCalificación de dificultad del 1 al 5
estimatedDurationnumberDuración esperada en segundos
tagsarrayEtiquetas de categorización
authorstringCreador del desafío
iconstringIdentificador del ícono
colorstringColor de acento (hex)
requirementsobjectSensores/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 remo
  • running.* - Métricas específicas de carrera
  • strength.* - Métricas de entrenamiento de fuerza
  • custom.* - 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ónFechaCambios
0.1.0-draft2025-01Especificación inicial en borrador