Specifica
Open Challenge Protocol
Un formato JSON dichiarativo per descrivere sfide di fitness tra piattaforme diverse.
Panoramica
L'Open Challenge Protocol (OCP) è un formato JSON dichiarativo per descrivere sfide di fitness. Definisce cosa costituisce il successo, non come un'applicazione host debba implementare l'esperienza.
Una sfida è un obiettivo strutturato che valuta la prestazione del ciclista rispetto a criteri definiti, con fasi del ciclo di vita, direttive di runtime e logica di iterazione/loop opzionali.
Sfide e allenamenti a confronto
OCP è pensato specificamente per le Sfide, non per gli allenamenti. Capire questa distinzione è fondamentale.
Sfide
- Dinamico e orientato all'obiettivo: Il successo si ottiene soddisfacendo criteri specifici
- Basati sulla logica: Costruito su condizioni, soglie e criteri di uscita
- Durata spesso indeterminata: Molte terminano quando non riesci a mantenere una condizione
- Pass/fail o record: O raggiungi l'obiettivo o gareggi contro il tuo record personale
Esempi
- Tieni 5.0 w/kg per 10 secondi
- Sprinta sui 200 m il più veloce possibile
- Percorri la distanza più lunga possibile tenendo la FC sotto la zona 4
- Accelera da fermo a 40 km/h
Allenamenti
- Strutturati e predeterminati: Segui una sequenza fissa di intervalli
- Basati su tempo/durata: Gli intervalli hanno durate precise
- Completamento = successo: Se completi tutti gli intervalli, hai "completato" l'allenamento
- Orientati all'allenamento: Pensato per l'adattamento fisiologico
Non adatto a OCP
- 5x (30 secondi on / 30 secondi off)
- Tabata: 8x (20 secondi / 10 secondi)
- Intervalli a scala 1-2-3-2-1 minuti
- 2x20 minuti a FTP
Nel dubbio: Chiediti: "Il successo dipende da come si comporta il ciclista, o solo dal fatto che completi la struttura?" Se il successo dipende dal mantenere delle condizioni → Sfida. Se il successo è solo completare gli intervalli → Allenamento.
Struttura della sfida
Una sfida è un oggetto JSON con i seguenti campi di primo livello:
{
"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": { }
}Campi obbligatori
| Campo | Tipo | Descrizione |
|---|---|---|
id | string | Identificatore univoco di questa sfida |
version | string | Versione del protocollo (semver) |
name | string | Nome della sfida leggibile |
phases | array | Elenco ordinato delle fasi della sfida (OPPURE criteria per le sfide semplici) |
Campi facoltativi
| Campo | Tipo | Descrizione |
|---|---|---|
description | string | Descrizione più lunga della sfida |
sport | string | Tipo di sport (cycling, running, rowing, any) |
mode | string | record (miglior risultato) oppure pass-fail (successo binario). Predefinito: pass-fail |
config | object | Configurazione e valori calcolati a livello di sfida |
criteria | object | Criteri di successo (e facoltativamente di fallimento) per le sfide semplici |
loops | array | Blocchi di valutazione ripetuti |
directives | array | Comportamenti di runtime (notifiche, suggerimenti) |
record | object | Quali metriche registrare come risultato della sfida |
completion | object | Come termina la sfida e cosa mostrare |
metadata | object | Suggerimenti di visualizzazione, difficoltà, tag, ecc. |
Modalità della sfida
pass-fail (predefinito): sfida tradizionale con criteri di successo/fallimento. Esito binario.
record: sfida in cui l'obiettivo è ottenere il miglior risultato possibile. Nessuno stato di fallimento: la sfida registra la tua prestazione.
Metriche
Le metriche sono i dati canonici che le sfide valutano. Le applicazioni host mappano i propri dati grezzi su questi percorsi.
Formato del percorso metrico
Le metriche usano la notazione a punti: 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)
Metriche di base
Tempo
| Percorso | Tipo | Descrizione |
|---|---|---|
time.elapsed | number | Secondi dall'inizio della sfida |
time.session | number | Secondi dall'inizio della sessione |
Potenza
| Percorso | Tipo | Descrizione |
|---|---|---|
power.instant | number | Potenza attuale (watt) |
power.avg | number | Potenza media della sessione |
power.normalized | number | Normalized Power |
power.wkg | number | Watt per chilo attuali |
power.zone | number | Zona di potenza attuale (1-7) |
power.percentFtp | number | Potenza attuale in % della FTP |
Velocità, cadenza, frequenza cardiaca
| Percorso | Tipo | Descrizione |
|---|---|---|
speed.instant | number | Velocità attuale (m/s o km/h a seconda dell'host) |
cadence.instant | number | Cadenza attuale (RPM) |
heartRate.instant | number | Frequenza cardiaca attuale (BPM) |
heartRate.zone | number | Zona FC attuale (1-5) |
Distanza, giro, pista
| Percorso | Tipo | Descrizione |
|---|---|---|
distance.total | number | Distanza totale (metri) |
lap.index | number | Numero del giro attuale |
lap.count | number | Giri totali completati |
track.length | number | Lunghezza della pista (metri) |
Metriche di gara/multiplayer
Sono disponibili solo nei contesti di gara:
| Percorso | Tipo | Descrizione |
|---|---|---|
position.current | number | Posizione attuale in gara (1 = primo) |
gap.ahead | number | Distacco dal ciclista davanti |
gap.behind | number | Distacco dal ciclista dietro |
overtakes.count | number | Numero di sorpassi effettuati |
Fasi
Le fasi definiscono la struttura di una sfida come sequenza ordinata di segmenti. Ogni fase ha la sua durata, i suoi obiettivi, le sue condizioni di uscita e le sue direttive.
Struttura della fase
{
"id": "effort",
"name": "Max Effort",
"duration": 30,
"description": "Give everything for 30 seconds",
"targets": { },
"exitConditions": { },
"directives": [ ]
}| Campo | Tipo | Obbligatorio | Descrizione |
|---|---|---|---|
id | string | Sì | Identificatore univoco di questa fase |
name | string | Sì | Nome visualizzato |
duration | number/string | No | Durata della fase in secondi, oppure "unlimited" |
distance | number | No | Distanza della fase in metri (alternativa alla durata) |
targets | object | No | Metriche obiettivo per questa fase |
exitConditions | object | No | Condizioni che chiudono prima questa fase |
Obiettivi
Gli obiettivi definiscono le metriche previste o richieste durante una fase:
{
"targets": {
"power": { "min": 80, "max": 150 },
"cadence": { "min": 90, "max": 100 }
}
}Obiettivi a rampa
{
"power": {
"mode": "ramp",
"start": 100,
"increment": 1,
"per": "second"
}
}Condizioni
Una condizione è una singola espressione verificabile che restituisce vero o falso.
Struttura della condizione
{
"metric": "power.wkg",
"operator": ">=",
"value": 5.0,
"duration": 10,
"window": "sustained"
}| Campo | Tipo | Obbligatorio | Descrizione |
|---|---|---|---|
metric | string | Sì | Percorso metrico da valutare |
operator | string | Sì | Operatore di confronto |
value | number/array | Sì | Valore/i obiettivo |
duration | number | No | Vincolo di tempo in secondi |
window | string | No | Come viene valutata la durata |
Operatori
Confronto
==Uguale a!=Diverso da>Maggiore di<Minore di>=Maggiore o uguale a<=Minore o uguale a
Intervallo
betweenIntervallo inclusivooutsideFuori intervallo
Cambia
increasedByAumento deltadecreasedByDiminuzione delta
Finestre di durata
| Finestra | Descrizione |
|---|---|
sustained | La condizione deve essere vera in modo continuo per la durata (predefinito) |
cumulative | La condizione deve essere vera per la durata totale (anche non continua) |
rolling | Valutato su una finestra mobile della durata indicata |
any | La condizione deve essere vera in un momento qualsiasi per la durata |
Porte logiche
Le condizioni si possono combinare con porte logiche.
Tipi di porta
| Cancello | Descrizione |
|---|---|
all | Tutte le condizioni devono essere vere (AND) |
any | Almeno una condizione deve essere vera (OR) |
none | Nessuna condizione può essere vera (NOT ANY) |
sequence | Le condizioni devono diventare vere in ordine |
Esempio: porte annidate
{
"all": [
{ "metric": "time.elapsed", "operator": ">=", "value": 60 },
{
"any": [
{ "metric": "power.wkg", "operator": ">=", "value": 5.0 },
{ "metric": "position.current", "operator": "==", "value": 1 }
]
}
]
}"Dopo 60 secondi, raggiungi 5.0 w/kg OPPURE sii in prima posizione"
Ciclo di vita
Il ciclo di vita definisce quando inizia e quando finisce la valutazione della sfida.
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ PENDING │ ──▶ │ ARMING │ ──▶ │ ACTIVE │ ──▶ │ COMPLETE │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│ │
│ ▼
│ ┌─────────────┐
└────────────▶ │ FAILED │
└─────────────┘| Fase | Descrizione |
|---|---|
pending | Sfida caricata ma non avviata |
arming | In attesa delle condizioni di partenza |
active | Valutazione dei criteri di successo/fallimento |
complete | Criteri di successo raggiunti |
failed | Criteri di fallimento raggiunti o sfida abbandonata |
Struttura del ciclo di vita
{
"lifecycle": {
"arming": {
"all": [
{ "metric": "time.elapsed", "operator": ">=", "value": 120 }
]
},
"timeout": 600,
"maxAttempts": 3
}
}Criteri di successo e fallimento
Struttura dei criteri
{
"criteria": {
"success": {
"all": [
{ "metric": "power.wkg", "operator": ">=", "value": 5.0, "duration": 10 }
]
},
"failure": {
"any": [
{ "metric": "power.instant", "operator": "==", "value": 0, "duration": 5 }
]
}
}
}Sfide con solo criteri di successo
La maggior parte delle sfide definisce solo criteri di successo. La sfida continua fino al successo o finché l'utente non la abbandona.
Sfide con condizioni di fallimento
{
"criteria": {
"success": {
"all": [
{ "metric": "lap.count", "operator": ">=", "value": 5 }
]
},
"failure": {
"any": [
{ "metric": "position.current", "operator": ">", "value": 3 }
]
}
}
}"Completa 5 giri restando nei primi 3. Scendi al 4° posto o peggio = fallito."
Loop
I loop permettono di ripetere blocchi di valutazione con una logica che tiene conto dell'iterazione. Sono essenziali per sfide con più intervalli, difficoltà progressiva o azioni ripetute.
Struttura del loop
{
"loops": [
{
"id": "sprint_intervals",
"count": 5,
"trigger": {
"metric": "lap.index",
"operator": "increasedBy",
"value": 1
},
"onIteration": {
"directives": [ ],
"criteria": { }
},
"onComplete": {
"directives": [ ]
}
}
]
}Variabili di contesto dell'iterazione
| Variabile | Descrizione |
|---|---|
{{loopIndex}} | Iterazione attuale (a partire da 0) |
{{loopCount}} | Iterazioni totali configurate |
{{loopRemaining}} | Iterazioni rimanenti |
Esempio di difficoltà 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 intervalli a potenza crescente: 3.0, 3.5, 4.0, 4.5, 5.0 w/kg"
Direttive
Le direttive sono comportamenti di runtime facoltativi che l'applicazione host può eseguire. Sono consultivo: è l'host a decidere se applicarle.
Struttura delle direttive
{
"directives": [
{
"id": "gap_warning",
"type": "notify",
"when": {
"metric": "gap.ahead",
"operator": ">",
"value": 3.0
},
"params": {
"message": "Gap widening! Push harder.",
"priority": "high"
},
"cooldown": 10
}
]
}Tipi di direttiva
notify
Mostra un messaggio al ciclista
{
"type": "notify",
"params": {
"message": "Push harder!",
"priority": "normal | high | critical"
}
}sound
Riproduci un segnale audio
{
"type": "sound",
"params": {
"sound": "bell | chime | warning | success"
}
}suggest
Suggerisci un obiettivo al ciclista (consultivo)
{
"type": "suggest",
"params": {
"metric": "power.instant",
"target": 250
}
}highlight
Evidenzia una metrica nell'interfaccia
{
"type": "highlight",
"params": {
"metric": "power.wkg",
"style": "pulse | glow | flash"
}
}Record (sfide al miglior risultato)
Per le sfide mode: "record", l'oggetto record definisce quali metriche registrare come risultato della sfida.
Struttura del 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" }
]
}
}Metriche principali comuni
| Tipo di sfida | Metrica principale | Esempio |
|---|---|---|
| Sforzo massimo | power.max | Potenza di picco |
| Cronometro | duration per la fase | Tempo sui 200 m lanciati |
| Tenuta di fondo | duration per la fase | Tempo tenuto in target |
| Distanza | distance.total | Metri percorsi |
Completamento
L'oggetto completion definisce come termina una sfida e cosa mostrare nel riepilogo.
Struttura di completamento
{
"completion": {
"condition": "phases-complete | exit-or-phases-complete | criteria-met",
"summary": {
"headline": "{{record.primary.value}}W",
"subheadline": "Peak Power"
}
}
}Condizioni di completamento
| Condizione | Descrizione |
|---|---|
phases-complete | La sfida termina quando tutte le fasi sono completate |
exit-or-phases-complete | La sfida termina alla condizione di uscita di una fase OPPURE quando tutte le fasi sono completate |
criteria-met | La sfida termina quando i criteri di successo sono soddisfatti (modalità pass-fail) |
Metadati
I metadati forniscono suggerimenti di visualizzazione e categorie per le applicazioni 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 | Descrizione |
|---|---|---|
difficulty | number | Valutazione di difficoltà da 1 a 5 |
estimatedDuration | number | Durata prevista in secondi |
tags | array | Tag di categoria |
author | string | Autore della sfida |
icon | string | Identificatore dell'icona |
color | string | Colore di accento (hex) |
requirements | object | Sensori/attrezzatura necessari |
Esempio 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"
}
}Modello di valutazione
Ciclo di valutazione
Il valutatore delle sfide gira a frequenza fissa (consigliata: 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 │ └─────────────────────────────────────────────────────────────┘
Risultato della valutazione
{
"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
}Appendici
A. Ambiente e portabilità
Le sfide possono fare riferimento a metriche di ambiente, valori forniti dall'applicazione host che descrivono il contesto in cui si svolge la sfida.
Spettro di portabilità
- Completamente portabile: Le sfide che usano solo metriche di base (potenza, cadenza, frequenza cardiaca, tempo, distanza) funzionano su qualsiasi host conforme
- Dipendenti dall'ambiente: Le sfide che usano metriche di ambiente richiedono che l'host fornisca quei valori
- Specifici della piattaforma: Alcune sfide possono fare riferimento a funzioni esclusive di una piattaforma specifica
Valori di riserva
{
"config": {
"approachDistance": {
"value": "{{track.length}}",
"fallback": 250
}
}
}B. Percorsi metrici riservati
I seguenti percorsi sono riservati per usi futuri:
rowing.*- Metriche specifiche del canottaggiorunning.*- Metriche specifiche della corsastrength.*- Metriche dell'allenamento di forzacustom.*- Metriche personalizzate definite dall'host
C. Note di implementazione per l'host
Gli host devono implementare un adattatore di metriche che mappa i propri dati interni sui percorsi metrici OCP:
interface MetricAdapter {
resolve(path: string): number | undefined;
getProfile(): { ftp?: number; weight?: number; maxHr?: number };
}D. Cronologia delle versioni
| Versione | Data | Modifiche |
|---|---|---|
| 0.1.0-draft | 2025-01 | Bozza iniziale della specifica |
