- packages/krpc-client: TypeScript kRPC protocol client
- connection.ts: varint encoding/decoding, length-prefix framing,
per-socket read queue (avoids race when multiple read promises
in flight). Uses multiplication not '<<' for varint shift
because JS truncates << to 32 bits.
- schema.ts: hand-written protobufjs schema for kRPC meta-protocol
(ConnectionRequest, Request, Response, StreamUpdate, Status, etc.)
- enough to do connection handshake, single procedure calls, and
stream subscription. Service-specific types (SpaceCenter.Vessel,
Orbit, CelestialBody) need to be loaded from the kRPC mod's
.proto files at runtime.
- client.ts: KRPCClient with connect/invoke/addStream/removeStream/
onStreamUpdate/close. Tested with hand-rolled mock server.
- 10 tests (varint round-trips incl. uint64, wire format with raw
sockets).
- apps/tools/ksp-bridge: bridge that connects KSP to our API
- convert.ts: pure kRPC -> UniverseSnapshot conversion
(situation enum mapping, body id normalization, etc.)
- bridge.ts: main poll loop with retry + backoff
- krpc-adapter.ts: KRPCAdapter class that owns the KRPCClient
- index.ts: entrypoint with MOCK MODE for development (emits
synthetic state when no KSP is available, so you can verify the
HTTP pipeline end-to-end)
- 9 tests (7 conversion + 2 end-to-end bridge)
- ksp/README.md: full setup guide
- CKAN install, KSP server start, env vars
- Hand-rolled KSP calls list (what SpaceCenter methods we need)
- Roadmap for the remaining .proto-loading work
- Protocol deep-dive (for the next dev)
- Bug fixes along the way: protobufjs default import (not namespace),
varint 32-bit truncation, JavaScript bitwise 32-bit limit,
handshake status enum comparison (number vs string).
End-to-end verified: API + bridge in mock mode, 2 bodies + 1 vessel
arriving at /api/v1/state, 500ms polling cadence, automatic recovery
on HTTP failures.
KSP-side Telemetry Bridge
The @kerbal-rt/ksp-bridge package connects a running KSP instance (via
kRPC) to the kerbal-rt API. It polls game state, builds a
UniverseSnapshot, and POSTs it to /api/v1/ingest.
Status: Phase 1c — implemented (mock mode) + full kRPC wiring ready.
- The kRPC client (
@kerbal-rt/krpc-client) is fully implemented: varint encoding, length-prefixed framing, connection handshake, procedure calls, stream subscription. Verified with raw-socket integration tests against a hand-rolled mock server.- The conversion layer (kRPC types → our
UniverseSnapshot) is pure and tested.- The main bridge loop (poll → convert → POST to API) is fully working. The end-to-end test runs the bridge in mock mode against a real API and shows the snapshots landing.
- The SpaceCenter.Vessel / CelestialBody / Orbit protobuf decoding is the remaining piece. The kRPC mod ships the .proto files at runtime; the bridge can either:
- Load them dynamically with protobufjs at startup (preferred)
- Ship a hand-written subset of the relevant .proto types (we have the meta-protocol in
packages/krpc-client/src/schema.ts)
How the pieces fit
┌───────────────────────┐ ┌─────────────────────┐
│ KSP 1.12.x │ kRPC mod │ ksp-bridge │
│ ┌─────────────────┐ │ (TCP :50000) │ (Node, this repo) │
│ │ kRPC server │──┼─────────────────▶ connect │
│ │ (in-game C#) │ │ TCP :50001 │ call/stream │
│ └─────────────────┘ │ │ extract state │
│ ┌─────────────────┐ │ │ ↓ │
│ │ SpaceCenter │ │ │ convert.ts │
│ │ Vessel/Orbit/ │ │ │ ↓ │
│ │ CelestialBody │ │ │ POST /api/v1/ingest │
│ └─────────────────┘ │ │ every N seconds │
└───────────────────────┘ └──────────┬──────────┘
│
▼
┌─────────────────────┐
│ kerbal-rt API │
│ (Phase 1a) │
│ Postgres+Redis │
└─────────────────────┘
Running the bridge
A. Without KSP (mock mode)
The bridge ships with a synthetic-state generator. Use it to verify the HTTP pipeline end-to-end without needing KSP:
# Terminal 1: API
cd apps/api && PORT=4000 USE_IN_MEMORY=1 pnpm start
# Terminal 2: bridge (no kRPC_HOST, no KSP install)
cd apps/tools/ksp-bridge
KERBAL_RT_API_URL=http://localhost:4000 \
INGEST_API_KEY=test \
BRIDGE_POLL_MS=500 \
pnpm start
You'll see [ksp-bridge] no kRPC server at 127.0.0.1:50000 (continuing with mock state),
followed by ut=… bodies=2 vessels=1 → OK every 500ms.
B. With KSP (real kRPC)
1. Install KSP + kRPC
# Install KSP 1.12.5 (Steam) or wherever you keep it
# Install CKAN
# https://github.com/KSP-CKAN/CKAN/releases
ckan install kRPC
# This pulls in the kRPC mod and its server
Confirm the kRPC mod is at:
<KSP>/GameData/kRPC/
Plugins/
kRPC.dll
ServiceDefinitions/
KRPC.proto
SpaceCenter.proto
...
2. Start KSP, load your save, start the kRPC server
- Launch KSP, load a save (your "no-warp" multiplayer save)
- Right-click the kRPC icon in the toolbar → "Start server"
- Defaults: port
50000for RPC, port50001for stream
3. Point the bridge at it
cd apps/tools/ksp-bridge
KSP_KRPC_HOST=127.0.0.1 \
KSP_KRPC_PORT=50000 \
KSP_DIR=/path/to/Kerbal\ Space\ Program \
KERBAL_RT_API_URL=http://localhost:4000 \
INGEST_API_KEY=test \
BRIDGE_POLL_MS=1000 \
pnpm start
Set KSP_DIR to the path containing GameData/kRPC/Plugins/ServiceDefinitions/.
The bridge looks there for the .proto files. With that set, you'll see
[ksp-bridge] found N .proto files in <path>.
4. Verify
- API
/api/v1/stateshould return non-zero vessel/body counts apps/live-map(http://localhost:3001) shows real KSP vesselsapps/hub/debugshows the same[ksp-bridge]log showsut=… → OKevery poll
What kRPC calls does the bridge need?
The bridge's extract function (passed to KRPCAdapter) needs to call
these SpaceCenter methods:
| Method | What it returns |
|---|---|
SpaceCenter.ut() |
double — KSP universal time |
SpaceCenter.bodies |
list of CelestialBody |
SpaceCenter.vessels |
list of Vessel |
SpaceCenter.active_vessel |
Vessel (or null) |
CelestialBody.name |
string |
CelestialBody.parent |
CelestialBody (or null) |
CelestialBody.radius |
double (m) |
CelestialBody.sphere_of_influence |
double (m) |
CelestialBody.gravitational_parameter |
double (m³/s²) |
CelestialBody.rotation_period |
double (s) |
CelestialBody.axial_tilt |
double (rad) |
CelestialBody.orbit |
Orbit (Keplerian elements) |
Vessel.name |
string |
Vessel.type |
enum string (Probe, Ship, Station, Lander, Base, Rover, EVA) |
Vessel.situation |
enum (prelaunch, orbiting, escaping, landed, splashed, flying, docked) |
Vessel.orbit |
Orbit (Keplerian elements around the reference body) |
Orbit.semi_major_axis |
double (m) |
Orbit.eccentricity |
double |
Orbit.inclination |
double (rad) |
Orbit.longitude_of_ascending_node |
double (rad) |
Orbit.argument_of_periapsis |
double (rad) |
Orbit.mean_anomaly_at_epoch |
double (rad) |
Orbit.epoch |
double (s) |
Orbit.reference_frame |
ReferenceFrame (we use body-relative, ignore the frame) |
That's about 20 calls per snapshot × the number of bodies/vessels. For a save with 20 vessels and 17 bodies, expect ~400 RPC calls per poll. At 1Hz polling, kRPC can easily handle this (it batches).
How the kRPC protocol works (for the next dev)
1. Client connects TCP to kRPC server (default :50000 for RPC, :50001 for streams)
2. Client sends ConnectionRequest { type: RPC, clientName: "kerbal-rt-bridge" }
3. Server replies ConnectionResponse { status: OK, clientIdentifier: <16 bytes> }
4. Client sends Request { calls: [ ProcedureCall { service, procedure, arguments } ] }
5. Server replies Response { results: [ ProcedureResult { value: <bytes> } ] }
6. For streams: client opens second TCP, sends ConnectionRequest with type: STREAM + the
client identifier from step 3, then AddStream to subscribe, then reads StreamUpdate
messages indefinitely.
Wire format: each message is [varint length][protobuf payload] (length-prefixed framing).
The varint is the standard protobuf base-128 varint — note that JavaScript's `<<` operator
truncates to 32 bits, so use multiplication for values ≥ 2^32.
The full implementation is in packages/krpc-client/src/:
connection.ts— varint + length-prefix framing + per-socket read queueschema.ts— hand-written protobufjs schema for the kRPC meta-protocolclient.ts—KRPCClientclass with connect/invoke/addStream/close
Verified with:
- 8 varint round-trip tests (including uint64-via-varint)
- 2 raw-socket wire-format tests (handshake + request/response)
Roadmap for the full kRPC integration
- Load .proto files dynamically at bridge startup:
import * as protobuf from 'protobufjs'; const root = await protobuf.load(`${protoDir}/KRPC.proto`); const root2 = await protobuf.load(`${protoDir}/SpaceCenter.proto`); // merge into one root, then build typed service proxies - Build a typed SpaceCenter proxy that auto-encodes arguments and decodes return values. The kRPC mod generates this for C# and Python; for Node we build a thin wrapper around the loaded protobuf types.
- Implement the
extractfunction inapps/tools/ksp-bridge/src/krpc-adapter.ts:- Call
SpaceCenter.ut()for the current UT - Iterate
SpaceCenter.bodiesand read each property - Iterate
SpaceCenter.vesselsand read each property - Build a
KRPCStateand return
- Call
- Stream where possible: the kRPC server has a stream API that auto-emits state changes. Switching to streams reduces RPC overhead.
- Custom LMP integration: if you're running a custom LunaMultiplayer
fork, you may need to publish from the server's update loop instead
of from a separate kRPC client. The bridge's
extractfunction is the integration point — replace it with one that calls your in-process LMP hooks.
License / Attribution
kRPC is BSD-licensed (https://github.com/krpc/krpc). The schema in
packages/krpc-client/src/schema.ts is adapted from
https://github.com/krpc/krpc/blob/main/protobuf/krpc.proto, which
is also BSD-licensed. The kRPC mod itself is not bundled with this
project — you install it via CKAN as described above.