# 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: > 1. **Load them dynamically** with protobufjs at startup (preferred) > 2. **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: ```bash # 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 ```bash # 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: ``` /GameData/kRPC/ Plugins/ kRPC.dll ServiceDefinitions/ KRPC.proto SpaceCenter.proto ... ``` #### 2. Start KSP, load your save, start the kRPC server 1. Launch KSP, load a save (your "no-warp" multiplayer save) 2. Right-click the kRPC icon in the toolbar → "Start server" 3. Defaults: port `50000` for RPC, port `50001` for stream #### 3. Point the bridge at it ```bash 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 `. #### 4. Verify - API `/api/v1/state` should return non-zero vessel/body counts - `apps/live-map` (http://localhost:3001) shows real KSP vessels - `apps/hub/debug` shows the same - `[ksp-bridge]` log shows `ut=… → OK` every 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: } ] } 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 queue - `schema.ts` — hand-written protobufjs schema for the kRPC meta-protocol - `client.ts` — `KRPCClient` class 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 1. **Load .proto files dynamically** at bridge startup: ```ts 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 ``` 2. **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. 3. **Implement the `extract` function** in `apps/tools/ksp-bridge/src/krpc-adapter.ts`: - Call `SpaceCenter.ut()` for the current UT - Iterate `SpaceCenter.bodies` and read each property - Iterate `SpaceCenter.vessels` and read each property - Build a `KRPCState` and return 4. **Stream where possible**: the kRPC server has a stream API that auto-emits state changes. Switching to streams reduces RPC overhead. 5. **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 `extract` function 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.