68bc7015fd
CI / Lint, typecheck, test, build (pull_request) Failing after 9s
- 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.
230 lines
9.5 KiB
Markdown
230 lines
9.5 KiB
Markdown
# 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:
|
||
```
|
||
<KSP>/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 <path>`.
|
||
|
||
#### 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: <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 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.
|