RavitoBeta
Inloggen

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

Scopex
profile:readx
presencex
rides:write-ownx
offline_accessx

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

xx

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": "..." }

    x

    x