x
x
x
Beta x
x
import { Ravito } from 'https://ravito.online/platform/sdk/v1/ravito.js';
const ravito = new Ravito({
server: 'https://ravito.online', clientId: 'ravito-sandbox',
redirectUri: location.origin + '/callback', scope: 'presence profile:read rides:write-own',
});
if (!(await ravito.finishLogin())) await ravito.login();
else {
ravito.onRiders((riders) => console.log(riders));
await ravito.connectPresence({ place: 'mallorca-west' });
ravito.sendDistance({ distance: 1200, speed: 8.1 });
}
x
POST /connect/register
{ "client_name": "Gruvetto",
"redirect_uris": ["https://gruvetto.example/cb"],
"scope": "presence profile:read rides:write-own" }
x
| Scope | x |
|---|---|
profile:read | x |
presence | x |
rides:write-own | x |
offline_access | x |
x
-> { "type": "presence", "place": "mallorca-west", "distance": 1234.5, "dir": 1, "speed": 8.1 }
<- { "type": "riders", "mode": "on",
"riders": [ { "nick": "anna", "lat": 39.7612, "lon": 2.7023, "speed": 7.9, "app": "Gruvetto", "distance": 1210.4, "dir": 1, "routeId": "mallorca-west" } ] }
x
x
x
x
| x | x |
|---|---|
x
x
P = v * ( m*9.81*sin(a) + m*9.81*Crr*cos(a) + 0.5*rho*CdA*v^2 )
a = atan(grade% / 100) m = 85 kg CdA = 0.32 Crr = 0.0045 rho = 1.225
x
x
import { Ravito } from 'https://ravito.online/platform/sdk/v1/ravito.js';
const ravito = new Ravito({ server: 'https://ravito.online', clientId: 'rv_YOUR_CLIENT_ID',
redirectUri: location.origin + '/', scope: 'presence profile:read' });
if (!(await ravito.finishLogin())) await ravito.login();
else {
const ride = await ravito.joinRoute('mallorca-west'); // route, profile, socket
setInterval(() => ride.update({ power: currentWatts(), mass }), 1000); // mass: profile totalKg, 85 as fallback
ride.onRiders((riders) => draw(riders)); // { id, nick, lat, lon, speed, app, routeId, distance?, dir? }
// ride.position: { distance, lat, lon, alt, grade } ride.others: the last list of riders
// ride.pointAt(d), ride.geojson, ride.predict(rider, msSinceUpdate): see below
}
x
const ride = await ravito.joinRoute('mallorca-west');
workout.onTick(({ targetWatts, measuredWatts }) => { // once per second, in your workout loop
trainer.setErgTarget(targetWatts); // your plan, your trainer: nothing from Ravito goes in here
ride.update({ power: measuredWatts, mass }); // measured power becomes speed and distance
});
x
const seen = new Map(); // id -> { frame, at, d } (key on id, never on nick)
ride.onRiders((riders) => { const at = performance.now(); for (const r of riders) seen.set(r.id, { ...seen.get(r.id), frame: r, at }); });
const L = ride.lengthM, wrap = (a, b) => (((b - a + L / 2) % L) + L) % L - L / 2; // shortest way round the lap
let prev = performance.now();
function frame(now) { // requestAnimationFrame
const dt = (now - prev) / 1000; prev = now;
for (const [id, s] of seen) {
const age = now - s.at; // ms since this rider's last update
if (age > 6000) { seen.delete(id); hide(id); continue; } // nothing for 6 s: gone
const x = Math.max(0, age - 3000); // after 3 s brake to a stop (linear) and fade out
const p = ride.predict(s.frame, Math.min(age, 3000) + x - (x * x) / 6000); // frame + speed * dt, round the lap
if (s.d == null || Math.abs(wrap(s.d, p.distance)) > 25) s.d = p.distance; // first frame or teleport: no glide
else s.d = (s.d + wrap(s.d, p.distance) * (1 - Math.exp(-dt * 7)) + L) % L; // correct softly (about 0.15 s)
draw(id, ride.pointAt(s.d), Math.min(1, 1 - x / 3000)); // position and opacity
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
x
x
POST /api/v1/history/rides?dry_run=1
{ "start": "2026-11-07T09:00:00Z",
"points": [[0, 52.0100, 5.9800, 41, 180, 132], [60, 52.0110, 5.9810, 41, 185, 133]] }
x
{ "code": "scope_not_allowed", "message": "...", "hint": "..." }