nendlabsExperiments

telemetry, relay, replay

nasetto

Remote race engineering becomes more useful when the shared object is structured telemetry instead of an expiring screen share.

The Shared Object Is State

The obvious primitive for remote race help is screen sharing. It is also the wrong primitive for the system being built here. Video shows the moment, then disappears. A race engineer needs speed, gear, pedal input, steering, lap timing, sector position, fuel, tire pressures, tire wear, track state, and callouts that can be reviewed against the actual session.

Nasetto splits those concerns. Voice can stay external. The system owns the state. A Windows agent runs beside Assetto Corsa, reads simulator telemetry, and streams typed messages to a relay. A browser dashboard subscribes to the room and sends structured callouts back to the driver-side app.

The result is not a better screen share. It is a session object that can be routed, persisted, and replayed.

typescript
export const telemetrySchema = z.object({
  type: z.literal("telemetry"),
  v: z.literal(1),
  ts: z.number(),
  seq: z.number(),
  car: z.object({
    speedKph: z.number(),
    gear: z.number(),
    rpm: z.number(),
    throttle: z.number(),
    brake: z.number(),
    steerAngleRad: z.number(),
  }).passthrough(),
  tires: z.object({
    pressures: z.tuple([z.number(), z.number(), z.number(), z.number()]),
    temps: z.tuple([z.number(), z.number(), z.number(), z.number()]),
    wear: z.tuple([z.number(), z.number(), z.number(), z.number()]),
  }).passthrough(),
}).passthrough();

The Simulator Boundary Is a Windows Agent

The hard integration is not the dashboard. It is the simulator boundary. The Tauri agent has to run on Windows, open Assetto Corsa shared-memory pages, read packed C-compatible structs, and convert them into the protocol the relay and browser understand.

The agent does not invent fake telemetry when the maps are unavailable. If the simulator is off or the shared-memory pages cannot be read, there is no sample. That absence is a better system behavior than a pretty dashboard fed by lies.

rust
const PHYSICS_MAP: &str = "acpmf_physics";
const GRAPHICS_MAP: &str = "acpmf_graphics";
const STATIC_MAP: &str = "acpmf_static";

impl TelemetryProvider for AcSharedMemProvider {
  fn sample(&mut self) -> Option<TelemetrySample> {
    self.try_open();
    let physics = self.physics.as_ref()?.read();
    let graphics = self.graphics.as_ref()?.read();
    let static_info = self.static_info.as_ref().map(|info| info.read());

    if graphics.status == AC_STATUS_OFF {
      return None;
    }

    Some(TelemetrySample {
      car: Car {
        speed_kph: physics.speed_kmh,
        gear: physics.gear,
        rpm: physics.rpms as f32,
        throttle: physics.gas,
        brake: physics.brake,
        steer_angle_rad: physics.steer_angle,
      },
      session: Session {
        track: static_info.map(|info| to_string(&info.track)).unwrap_or_default(),
        car_model: static_info.map(|info| to_string(&info.car_model)).unwrap_or_default(),
        status: map_flag_to_status(graphics.flag),
      },
    })
  }
}

One Room, One Agent, Many Viewers

The relay model is narrow on purpose. A room has one active agent and many viewers. Agent telemetry is the source of truth for live state. Viewers receive the latest telemetry immediately on connect, then receive each new sample. Viewer callouts are validated and routed back to the agent.

That simplicity matters because the v1 relay is a single instance. Live routing is in memory, while session history is persisted. Horizontal room coordination would be a different system. The current design keeps authority, routing, and persistence legible.

typescript
if (role === "agent") {
  if (room.agent) {
    ws.close(1008, "Agent already connected");
    return;
  }
  setAgent(roomId, ws);
  room.sessionId = startSession(roomId);
  return;
}

addViewer(roomId, ws);
if (room.lastTelemetry) {
  ws.send(JSON.stringify(room.lastTelemetry));
}

Replay Makes Advice Auditable

Replay is the difference between a live tool and an evidence tool. The relay stores rooms, sessions, telemetry samples, and callouts in SQLite. Agent connection starts a session. Agent disconnect ends it. Replay loads the session and reconstructs the timeline.

That gives technical feedback an object to inspect. A missed fuel read, late callout, or bad tire-pressure judgment can be reviewed against the data stream instead of remembered from a call.

sql
CREATE TABLE IF NOT EXISTS sessions (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  room_id TEXT NOT NULL,
  started_at INTEGER NOT NULL,
  ended_at INTEGER
);

CREATE TABLE IF NOT EXISTS telemetry (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  session_id INTEGER NOT NULL,
  ts INTEGER NOT NULL,
  seq INTEGER NOT NULL,
  payload TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS callouts (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  session_id INTEGER NOT NULL,
  ts INTEGER NOT NULL,
  text TEXT NOT NULL,
  priority TEXT NOT NULL
);

Auth and Updates Belong to the Boundary

The practical production work is part of the system, not deployment trivia. The relay moved viewer authentication from token-bearing URLs to room-scoped session cookies. The agent moved its WebSocket token into a bearer header. The relay also serves updater metadata for the Windows app, which matters because the agent is the piece that must sit next to the game.

The commit history shows the experiment tightening around operational reality: Bun-native relay serving with SQLite, Remix asset fixes, Windows shared-memory correction, richer telemetry payloads, room-scoped viewer sessions, bearer auth for the agent, and a version bump to 0.0.2.

The V1 Shape Is Deliberate

The boundary is honest. V1 assumes one relay instance. Room state is in memory. Auth is shared-token based, not an account system. Replay stores telemetry JSON without solving large-session export or pagination. The visible CI builds the relay, container, agent UI, and Rust core, but the repo does not contain a broad test suite.

Those constraints are acceptable because the project is testing the right thing: whether remote technical help becomes more valuable when the shared artifact is structured state. On that axis, the answer is clear. Screen sharing ends when the call ends. Telemetry can be replayed.