TypeScript contracts
On this page
rt-core/clients/ts/ holds two generated TypeScript files for applications that talk to rt-control from Node or from a web front end behind a server:
| File | Contents |
|---|---|
rt_control_api_generated.ts | An interface for every contract type, the Reason… and Operation… constants, the Capabilities list, ContractVersion, RequestSchema, ResponseSchema and CapabilitiesDigest. |
rt_protocol_generated.ts | Little-endian decoders for the binary telemetry stream: decodeTelemetryBatchHeaderV1, decodeCycleCaptureRecordV2, decodeCycleCaptureAxisV2, their byte sizes, CycleCaptureRecordV2LayoutDigest and TelemetryLayoutMismatchError. |
They have no imports and no runtime dependencies. Compile them with your own TypeScript toolchain, targeting ES2020 or later; nothing needs installing in rt-core. There is no HTTP client: rt-control listens on a Unix socket or on mutual TLS, which a browser cannot reach directly. Call it from Node, or through a server of your own.
Both files are regenerated from the contract by make generate-api and make generate-protocol, and make check-api and make check-protocol fail if they drift.
Example: typed calls from Node#
import http from "node:http";
import {
CapabilitiesDigest, RequestSchema, ReasonControlAlreadyOwned,
type Description, type Grant, type Response,
} from "./rt_control_api_generated.ts";
const socketPath = "/run/rosie-rt-core/control.sock";
function call(method: string, path: string, body?: object): Promise<Response> {
return new Promise((resolve, reject) => {
const req = http.request({ socketPath, method, path, headers: { "Content-Type": "application/json" } }, (res) => {
let text = "";
res.setEncoding("utf8");
res.on("data", (chunk) => (text += chunk));
res.on("end", () => resolve(JSON.parse(text) as Response));
});
req.on("error", reject);
if (body) req.write(JSON.stringify(body));
req.end();
});
}
const d = (await call("GET", "/v1/describe")).data as Description;
if (d.capabilities_digest !== CapabilitiesDigest) throw new Error("contract mismatch");
console.log(d.backend, d.axes.map((a) => `${a.id} [${a.min_position}, ${a.max_position}] ${a.position_unit}`));
const r = await call("POST", "/v1/control", {
schema: RequestSchema, operation: "acquire", controller: "ts-demo",
binding: { pair_id: "cell-a", revision: 1, configuration_sha256: d.configuration_sha256, machine_sha256: d.machine_sha256 },
});
if (r.error === ReasonControlAlreadyOwned) console.log("someone else has control");
else if (!r.error) console.log("generation", (r.data as Grant).generation);This sketch acquires but never renews or releases, so the grant simply expires after its lease. A real client must renew at a third of the lease and release when done; see Control authority.
uint64 precision#
JSON integer fields are typed number, which is exact only up to 2^53. Values such as deadline_host_ns, time_ns and event sequences can exceed that on a long-running host. When you need exact uint64 JSON values, parse with a lossless JSON parser. The binary telemetry decoders have no such problem: every 64-bit field decodes to bigint.
Request is declared with request_id?: string | null, but the server refuses null: omit the field, or send a string of 1..64 printable ASCII bytes.
Telemetry decoders#
The telemetry stream is a sequence of batches: a 312-byte header, then RecordCount records of CycleCaptureRecordV2Bytes (5392) bytes. Each decoder takes an exact-size Uint8Array and throws on a wrong length or a non-zero reserved field. decodeTelemetryBatchHeaderV1 also throws TelemetryLayoutMismatchError (with storedDigest and expectedDigest) when the batch uses another record layout.
import {
decodeTelemetryBatchHeaderV1, decodeCycleCaptureRecordV2,
TelemetryBatchHeaderV1Bytes, CycleCaptureRecordV2Bytes,
} from "./rt_protocol_generated.ts";
// Feed raw chunks from GET /v1/telemetry/stream?after=<cursor>. HTTP chunks need
// not line up with batches, so buffer until a whole batch is available.
let pending = new Uint8Array(0);
let cursor = 0n;
export function onChunk(chunk: Uint8Array) {
const joined = new Uint8Array(pending.length + chunk.length);
joined.set(pending);
joined.set(chunk, pending.length);
pending = joined;
for (;;) {
if (pending.length < TelemetryBatchHeaderV1Bytes) return;
const h = decodeTelemetryBatchHeaderV1(pending.subarray(0, TelemetryBatchHeaderV1Bytes));
if (h.Magic !== 0x31425452 || h.Version !== 1 || h.RecordBytes !== CycleCaptureRecordV2Bytes) throw new Error("bad header");
const size = TelemetryBatchHeaderV1Bytes + h.RecordCount * CycleCaptureRecordV2Bytes;
if (pending.length < size) return;
if (h.Dropped !== 0n) console.warn(`lost ${h.Dropped} records`);
for (let i = 0; i < h.RecordCount; i++) {
const at = TelemetryBatchHeaderV1Bytes + i * CycleCaptureRecordV2Bytes;
const rec = decodeCycleCaptureRecordV2(pending.subarray(at, at + CycleCaptureRecordV2Bytes));
console.log(rec.Seq, rec.Ax[0].Position); // Position is in drive counts
}
cursor = h.LastSequence; // resume point
pending = pending.slice(size);
}
}Before accepting a batch, also check AxisCount (1..16), RecordCount (at most 4096), that each record's Seq follows on from the last and that Axes matches the header. If the adapter or daemon incarnation changes, stop and reconcile before you reset the cursor. The layout, field by field, is in Events and telemetry.
What is exported#
| Export | Kind |
|---|---|
ContractVersion, RequestSchema, ResponseSchema, CapabilitiesDigest | Constants: 2, rosie.rt-control.request.v1, rosie.rt-control.response.v1 and the schema digest. |
Reason… (for example ReasonNotReady) | One string constant per reason code. |
Operation… (for example OperationStartProgram) | One string constant per operation. |
Capabilities | The 33 capabilities with name, state and transport, as const. |
Fence, Binding, Grant, Request, Response, Description, ProcessStatus, Event, EventBatch … | One interface per contract type. Native receipts (CommandResult, JogObservation, GrantObservation) keep their PascalCase field names. |
TelemetryBatchHeaderV1, CycleCaptureRecordV2, CycleCaptureAxisV2 | Decoded telemetry interfaces, with 64-bit fields as bigint. |