Especificação
Open Challenge Protocol
Um formato JSON declarativo para descrever desafios de fitness entre plataformas.
Visão geral
O Open Challenge Protocol (OCP) é um formato JSON declarativo para descrever desafios de fitness. Ele define o que conta como sucesso, não como um aplicativo host deve implementar a experiência.
Um desafio é uma meta estruturada que avalia o desempenho do ciclista com base em critérios definidos, com fases de ciclo de vida, diretivas em tempo de execução e lógica de iteração/loop opcionais.
Desafios vs treinos
O OCP foi pensado especificamente para Desafios, não para treinos. Entender essa diferença é fundamental.
Desafios
- Dinâmico e orientado a metas: O sucesso é definido pelo cumprimento de critérios específicos
- Baseado em lógica: Baseado em condições, limites e critérios de saída
- Duração muitas vezes indeterminada: Muitos terminam quando você não consegue manter uma condição
- Pass/fail ou quebra de recorde: Ou você atinge a meta, ou disputa contra seu melhor resultado pessoal
Exemplos
- Manter 5.0 w/kg por 10 segundos
- Sprintar 200m o mais rápido possível
- Pedale o mais longe possível mantendo a FC abaixo da zona 4
- Acelerar do zero até 40 km/h
Treinos
- Estruturado e predeterminado: Seguir uma sequência fixa de intervalos
- Baseado em tempo/duração: Os intervalos têm durações específicas
- Conclusão = sucesso: Se você termina todos os intervalos, você "completou" o treino
- Focado em treino: Pensado para a adaptação fisiológica
Não adequado ao OCP
- 5x (30 segundos forte / 30 segundos leve)
- Tabata: 8x (20 segundos / 10 segundos)
- Intervalos em escada de 1-2-3-2-1 minutos
- 2x20 minutos no FTP
Na dúvida: Pergunte: "O sucesso depende de como o ciclista se sai, ou só que ele complete a estrutura?" Se o sucesso depende de manter condições → Desafio. Se o sucesso é só completar os intervalos → Treino.
Estrutura do desafio
Um desafio é um objeto JSON com os seguintes campos de nível 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 obrigatórios
| Campo | Tipo | Descrição |
|---|---|---|
id | string | Identificador único deste desafio |
version | string | Versão do protocolo (semver) |
name | string | Nome do desafio legível para pessoas |
phases | array | Lista ordenada de fases do desafio (OU criteria para desafios simples) |
Campos opcionais
| Campo | Tipo | Descrição |
|---|---|---|
description | string | Descrição mais longa do desafio |
sport | string | Tipo de esporte (cycling, running, rowing, any) |
mode | string | record (melhor esforço) ou pass-fail (sucesso binário). Padrão: pass-fail |
config | object | Configuração geral do desafio e valores calculados |
criteria | object | Critérios de sucesso (e de falha, opcionais) para desafios simples |
loops | array | Blocos de avaliação repetidos |
directives | array | Comportamentos em tempo de execução (notificações, sugestões) |
record | object | Quais métricas capturar como resultado do desafio |
completion | object | Como o desafio termina e o que exibir |
metadata | object | Exibir dicas, dificuldade, tags etc. |
Modos de desafio
pass-fail (padrão): desafio tradicional com critérios de sucesso/falha. Resultado binário.
record: desafio de melhor esforço, em que o objetivo é alcançar o melhor resultado possível. Não há estado de falha: o desafio registra o seu desempenho.
Métricas
As métricas são os dados canônicos que os desafios avaliam. Os aplicativos host mapeiam seus dados brutos para esses caminhos.
Formato do caminho da métrica
As métricas usam notação de ponto: 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
Tempo
| Caminho | Tipo | Descrição |
|---|---|---|
time.elapsed | number | Segundos desde o início do desafio |
time.session | number | Segundos desde o início da sessão de pedal |
Potência
| Caminho | Tipo | Descrição |
|---|---|---|
power.instant | number | Potência atual (watts) |
power.avg | number | Potência média da sessão |
power.normalized | number | Potência normalizada |
power.wkg | number | Watts por quilo atuais |
power.zone | number | Zona de potência atual (1-7) |
power.percentFtp | number | Potência atual em % do FTP |
Velocidade, cadência, frequência cardíaca
| Caminho | Tipo | Descrição |
|---|---|---|
speed.instant | number | Velocidade atual (m/s ou km/h, conforme o host) |
cadence.instant | number | Cadência atual (RPM) |
heartRate.instant | number | Frequência cardíaca atual (BPM) |
heartRate.zone | number | Zona de FC atual (1-5) |
Distância, volta, pista
| Caminho | Tipo | Descrição |
|---|---|---|
distance.total | number | Distância total (metros) |
lap.index | number | Número da volta atual |
lap.count | number | Total de voltas completadas |
track.length | number | Comprimento da pista (metros) |
Métricas de prova/multijogador
Estes só estão disponíveis em contextos de prova:
| Caminho | Tipo | Descrição |
|---|---|---|
position.current | number | Posição atual na prova (1 = primeiro) |
gap.ahead | number | Diferença para o ciclista à frente |
gap.behind | number | Diferença para o ciclista atrás |
overtakes.count | number | Número de ultrapassagens feitas |
Fases
As fases definem a estrutura de um desafio como uma sequência ordenada de segmentos. Cada fase tem sua própria duração, metas, condições de saída e diretivas.
Estrutura da fase
{
"id": "effort",
"name": "Max Effort",
"duration": 30,
"description": "Give everything for 30 seconds",
"targets": { },
"exitConditions": { },
"directives": [ ]
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id | string | Sim | Identificador único desta fase |
name | string | Sim | Nome de exibição |
duration | number/string | Não | Duração da fase em segundos, ou "unlimited" |
distance | number | Não | Distância da fase em metros (alternativa à duração) |
targets | object | Não | Métricas-alvo desta fase |
exitConditions | object | Não | Condições que encerram esta fase antes da hora |
Metas
As metas definem as métricas esperadas ou exigidas durante uma fase:
{
"targets": {
"power": { "min": 80, "max": 150 },
"cadence": { "min": 90, "max": 100 }
}
}Metas em rampa
{
"power": {
"mode": "ramp",
"start": 100,
"increment": 1,
"per": "second"
}
}Condições
Uma condição é uma única expressão testável que resulta em verdadeiro ou falso.
Estrutura da condição
{
"metric": "power.wkg",
"operator": ">=",
"value": 5.0,
"duration": 10,
"window": "sustained"
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
metric | string | Sim | Caminho da métrica a avaliar |
operator | string | Sim | Operador de comparação |
value | number/array | Sim | Valor(es)-alvo |
duration | number | Não | Restrição de tempo em segundos |
window | string | Não | Como a duração é avaliada |
Operadores
Comparação
==Igual a!=Diferente de>Maior que<Menor que>=Maior ou igual a<=Menor ou igual a
Faixa
betweenIntervalo inclusivooutsideFora do intervalo
Alterar
increasedByAumento deltadecreasedByRedução delta
Janelas de duração
| Janela | Descrição |
|---|---|
sustained | A condição precisa ser verdadeira continuamente durante a duração (padrão) |
cumulative | A condição precisa ser verdadeira pelo tempo total (pode ser descontínuo) |
rolling | Avaliado numa janela móvel com a duração especificada |
any | A condição precisa ser verdadeira em algum momento durante a duração |
Portas lógicas
As condições podem ser combinadas com portas lógicas.
Tipos de porta
| Portão | Descrição |
|---|---|
all | Todas as condições precisam ser verdadeiras (AND) |
any | Pelo menos uma condição precisa ser verdadeira (OR) |
none | Nenhuma condição pode ser verdadeira (NOT ANY) |
sequence | As condições precisam se tornar verdadeiras em ordem |
Exemplo: portas aninhadas
{
"all": [
{ "metric": "time.elapsed", "operator": ">=", "value": 60 },
{
"any": [
{ "metric": "power.wkg", "operator": ">=", "value": 5.0 },
{ "metric": "position.current", "operator": "==", "value": 1 }
]
}
]
}"Depois de 60 segundos, atinja 5.0 w/kg OU esteja em primeiro lugar"
Ciclo de vida
O ciclo de vida define quando a avaliação do desafio começa e termina.
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ PENDING │ ──▶ │ ARMING │ ──▶ │ ACTIVE │ ──▶ │ COMPLETE │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│ │
│ ▼
│ ┌─────────────┐
└────────────▶ │ FAILED │
└─────────────┘| Fase | Descrição |
|---|---|
pending | Desafio carregado, mas não iniciado |
arming | Aguardando condições de início |
active | Avaliando os critérios de sucesso/falha |
complete | Critérios de sucesso atingidos |
failed | Critérios de falha atingidos ou desafio abandonado |
Estrutura do ciclo de vida
{
"lifecycle": {
"arming": {
"all": [
{ "metric": "time.elapsed", "operator": ">=", "value": 120 }
]
},
"timeout": 600,
"maxAttempts": 3
}
}Critérios de sucesso e falha
Estrutura dos critérios
{
"criteria": {
"success": {
"all": [
{ "metric": "power.wkg", "operator": ">=", "value": 5.0, "duration": 10 }
]
},
"failure": {
"any": [
{ "metric": "power.instant", "operator": "==", "value": 0, "duration": 5 }
]
}
}
}Desafios só com sucesso
A maioria dos desafios só define critérios de sucesso. O desafio continua até o sucesso ou até o usuário desistir.
Desafios com condições de falha
{
"criteria": {
"success": {
"all": [
{ "metric": "lap.count", "operator": ">=", "value": 5 }
]
},
"failure": {
"any": [
{ "metric": "position.current", "operator": ">", "value": 3 }
]
}
}
}"Complete 5 voltas sem sair do top 3. Cair para 4º ou pior = falha."
Loops
Os loops permitem repetir blocos de avaliação com lógica que leva a iteração em conta. Isso é essencial para desafios que envolvem vários intervalos, dificuldade progressiva ou ações repetidas.
Estrutura do loop
{
"loops": [
{
"id": "sprint_intervals",
"count": 5,
"trigger": {
"metric": "lap.index",
"operator": "increasedBy",
"value": 1
},
"onIteration": {
"directives": [ ],
"criteria": { }
},
"onComplete": {
"directives": [ ]
}
}
]
}Variáveis de contexto da iteração
| Variável | Descrição |
|---|---|
{{loopIndex}} | Iteração atual (começando em 0) |
{{loopCount}} | Total de iterações configuradas |
{{loopRemaining}} | Iterações restantes |
Exemplo de dificuldade progressiva
{
"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 com potência crescente: 3.0, 3.5, 4.0, 4.5, 5.0 w/kg"
Diretivas
Diretivas são comportamentos opcionais em tempo de execução que o aplicativo host pode executar. Elas são advisory: o host decide se vai segui-las.
Estrutura da diretiva
{
"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 diretiva
notify
Exibir uma mensagem para o ciclista
{
"type": "notify",
"params": {
"message": "Push harder!",
"priority": "normal | high | critical"
}
}sound
Tocar um aviso sonoro
{
"type": "sound",
"params": {
"sound": "bell | chime | warning | success"
}
}suggest
Sugerir uma meta ao ciclista (advisory)
{
"type": "suggest",
"params": {
"metric": "power.instant",
"target": 250
}
}highlight
Destacar uma métrica na interface
{
"type": "highlight",
"params": {
"metric": "power.wkg",
"style": "pulse | glow | flash"
}
}Record (desafios de melhor esforço)
Para desafios mode: "record", o objeto record define quais métricas capturar como resultado do desafio.
Estrutura do 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 primárias comuns
| Tipo de desafio | Métrica principal | Exemplo |
|---|---|---|
| Esforço máximo | power.max | Potência de pico |
| Contrarrelógio | duration para a fase | Tempo de sprint de 200m |
| Manutenção em endurance | duration para a fase | Tempo mantido na meta |
| Distância | distance.total | Metros percorridos |
Conclusão
O objeto completion define como um desafio termina e o que exibir no resumo.
Estrutura de conclusão
{
"completion": {
"condition": "phases-complete | exit-or-phases-complete | criteria-met",
"summary": {
"headline": "{{record.primary.value}}W",
"subheadline": "Peak Power"
}
}
}Condições de conclusão
| Condição | Descrição |
|---|---|
phases-complete | O desafio termina quando todas as fases terminam |
exit-or-phases-complete | O desafio termina na condição de saída da fase OU quando todas as fases terminam |
criteria-met | O desafio termina quando os critérios de sucesso são atingidos (modo pass-fail) |
Metadados
Os metadados fornecem dicas de exibição e categorização para os aplicativos 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 | Descrição |
|---|---|---|
difficulty | number | Nota de dificuldade de 1 a 5 |
estimatedDuration | number | Duração esperada em segundos |
tags | array | Tags de categorização |
author | string | Criador do desafio |
icon | string | Identificador do ícone |
color | string | Cor de destaque (hex) |
requirements | object | Sensores/equipamentos necessários |
Exemplo 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 avaliação
Loop de avaliação
O avaliador do desafio roda numa frequência fixa (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 da avaliação
{
"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. Ambiente e portabilidade
Os desafios podem referenciar métricas de ambiente, valores fornecidos pelo aplicativo host que descrevem o contexto em que o desafio roda.
Espectro de portabilidade
- Totalmente portátil: Desafios que só referenciam métricas básicas (potência, cadência, frequência cardíaca, tempo, distância) funcionam em qualquer host compatível
- Dependente do ambiente: Desafios que referenciam métricas de ambiente exigem que o host forneça esses valores
- Específico da plataforma: Alguns desafios podem referenciar recursos exclusivos de uma plataforma específica
Valores de reserva
{
"config": {
"approachDistance": {
"value": "{{track.length}}",
"fallback": 250
}
}
}B. Caminhos de métricas reservados
Os seguintes caminhos estão reservados para uso futuro:
rowing.*- Métricas específicas de remorunning.*- Métricas específicas de corridastrength.*- Métricas de treino de forçacustom.*- Métricas personalizadas definidas pelo host
C. Notas de implementação para o host
Os hosts precisam implementar um adaptador de métricas que mapeie seus dados internos para os caminhos de métricas do OCP:
interface MetricAdapter {
resolve(path: string): number | undefined;
getProfile(): { ftp?: number; weight?: number; maxHr?: number };
}D. Histórico de versões
| Versão | Data | Alterações |
|---|---|---|
| 0.1.0-draft | 2025-01 | Especificação inicial em rascunho |
