@fibril/can-core 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1282 @@
1
+ import { CanFrameTx, ITransport, Unsubscribe } from "@fibril/can-transport";
2
+
3
+ //#region src/ack-tracker.d.ts
4
+ /**
5
+ * In-flight ACK/NAK ledger keyed by `seq`. Mirrors the per-node `AckEntry`
6
+ * map in `fibril_can_bridge/include/fibril_can_bridge/node_agent.hpp` but
7
+ * pared down for the single-threaded TS master — no deadlines list, the
8
+ * caller polls via {@link sweep}.
9
+ */
10
+ type AckOutcome = {
11
+ kind: 'acked';
12
+ } | {
13
+ kind: 'naked';
14
+ reason: number;
15
+ } | {
16
+ kind: 'timed-out';
17
+ };
18
+ type AckCallback = (outcome: AckOutcome) => void;
19
+ declare class AckTracker {
20
+ #private;
21
+ constructor(now?: () => number);
22
+ get pendingCount(): number;
23
+ /** Returns false if `seq` is already tracked; the existing entry is kept. */
24
+ track(seq: number, deadlineMs: number, cb: AckCallback): boolean;
25
+ /** Idempotent — fires `acked` if `seq` is pending. */
26
+ onAck(seq: number): void;
27
+ /** Idempotent — fires `naked` with the reason if `seq` is pending. */
28
+ onNak(seq: number, reason: number): void;
29
+ /** Fire `timed-out` for every entry whose deadline elapsed by `nowMs`. */
30
+ sweep(nowMs?: number): void;
31
+ /** Drop every entry without firing callbacks. Test-only. */
32
+ clear(): void;
33
+ }
34
+ //#endregion
35
+ //#region src/can-id.d.ts
36
+ /**
37
+ * CAN ID layout (Extended) and config-plane / NAK / svc-status / node-state
38
+ * constants. Mirrors SPEC §3, §5, §7 and `fibril_can_runtime/include/fibril_can/fcan_protocol.h`
39
+ * / `fibril_can_bridge/include/fibril_can_bridge/can_id.hpp`.
40
+ *
41
+ * Extended ID layout (29 bit, §3.2):
42
+ * [28:26] channel (3 bit)
43
+ * [25:19] node_id (7 bit, 0x7F = broadcast)
44
+ * [18:11] index (8 bit, service_index / cmd_code)
45
+ * [10:3] seq (8 bit, call_id / seq_num)
46
+ * [2] seg (1 bit, 0=SINGLE, 1=SEGMENT)
47
+ * [1:0] reserved (2 bit, 0)
48
+ *
49
+ * Standard ID (11 bit, §3.1) is dynamically allocated by the master and
50
+ * has no fixed bitfield meaning — kept here only as the wire mask.
51
+ */
52
+ declare const PROTOCOL_VERSION = 1;
53
+ declare const MAX_FRAME_PAYLOAD = 64;
54
+ declare const CHAN_CONFIG = 0;
55
+ declare const CHAN_SVC_REQUEST = 1;
56
+ declare const CHAN_SVC_RESPONSE = 2;
57
+ declare const NODE_ID_BROADCAST = 127;
58
+ declare const STD_ID_MASK = 2047;
59
+ declare const SVC_PARAM_SET = 253;
60
+ declare const SVC_CANCEL = 255;
61
+ declare const SVC_MAX_FIRST_CLASS = 252;
62
+ declare const CMD_DISCOVER = 1;
63
+ declare const CMD_ANNOUNCE = 2;
64
+ declare const CMD_SCHEMA_READ = 3;
65
+ declare const CMD_SCHEMA_DATA = 4;
66
+ declare const CMD_HEARTBEAT = 5;
67
+ declare const CMD_MASTER_HEARTBEAT = 7;
68
+ declare const CMD_FRAME_DEFINE = 16;
69
+ declare const CMD_HB_PERIOD = 19;
70
+ declare const CMD_START = 32;
71
+ declare const CMD_STOP = 33;
72
+ declare const CMD_ACK = 126;
73
+ declare const CMD_NAK = 127;
74
+ declare const NAK_UNKNOWN_CMD = 1;
75
+ declare const NAK_BAD_LENGTH = 2;
76
+ declare const NAK_BAD_PARAM = 3;
77
+ declare const NAK_NO_RESOURCE = 4;
78
+ declare const NAK_BAD_STATE = 5;
79
+ declare const NAK_ID_CONFLICT = 6;
80
+ declare const NAK_INDEX_RANGE = 7;
81
+ declare const NAK_DIR_MISMATCH = 8;
82
+ declare const NAK_SIZE_OVERFLOW = 9;
83
+ declare const STATE_UNPROVISIONED = 0;
84
+ declare const STATE_PROVISIONED = 1;
85
+ declare const STATE_RUNNING = 2;
86
+ declare const STATE_FAULT = 3;
87
+ declare const FAULT_NONE = 0;
88
+ declare const FAULT_NOMEM = 1;
89
+ declare const FAULT_INDEX_OVERFLOW = 2;
90
+ declare const FAULT_ID_CONFLICT = 3;
91
+ declare const DIR_TX_S2M = 0;
92
+ declare const DIR_RX_M2S = 1;
93
+ declare const SVC_OK = 0;
94
+ declare const SVC_ACCEPTED = 1;
95
+ declare const SVC_APP_ERROR = 2;
96
+ declare const SVC_BUSY = 3;
97
+ declare const SVC_UNAVAIL = 4;
98
+ declare const SVC_BAD_INDEX = 5;
99
+ declare const SVC_BAD_VALUE = 6;
100
+ declare const SVC_BAD_LENGTH = 8;
101
+ declare const SEG_FIRST_DATA = 61;
102
+ declare const SEG_REST_DATA = 63;
103
+ declare const SEG_MAX_TOTAL = 1024;
104
+ declare const SEG_MAX_SEQ = 31;
105
+ /** Byte offset of segment `seq` within the reassembled buffer (§7.4). */
106
+ declare function segOffset(seq: number): number;
107
+ /**
108
+ * Build a 29-bit Extended ID from its bitfields (§3.2). Inputs are masked
109
+ * down to their respective field widths.
110
+ */
111
+ declare function makeExtId(channel: number, nodeId: number, index: number, seq: number, seg?: number): number;
112
+ declare function extChannel(id: number): number;
113
+ declare function extNodeId(id: number): number;
114
+ declare function extIndex(id: number): number;
115
+ declare function extSeq(id: number): number;
116
+ declare function extSeg(id: number): number;
117
+ //#endregion
118
+ //#region src/config-plane.d.ts
119
+ /**
120
+ * Config-plane wire codec. Mirrors
121
+ * `fibril_can_bridge/include/fibril_can_bridge/config_plane_codec.hpp`
122
+ * and `fibril_can_bridge/src/config_plane_codec.cpp`.
123
+ *
124
+ * All integers are little-endian. Decoders return `null` on malformed input
125
+ * (wrong length etc.); encoders produce CAN frames padded to the next DLC
126
+ * step, matching what the slave sees on the bus.
127
+ */
128
+ /** ANNOUNCE payload (S→M, §5.4). */
129
+ interface AnnounceMsg {
130
+ schemaHash: bigint;
131
+ protocolVersion: number;
132
+ bootId: number;
133
+ blobLen: number;
134
+ /** Length K, indexed by block-type ordinal. */
135
+ instanceCounts: Uint8Array;
136
+ }
137
+ /** HEARTBEAT payload (S→M, §5.8). */
138
+ interface HeartbeatMsg {
139
+ bootId: number;
140
+ state: number;
141
+ faultSummary: number;
142
+ }
143
+ /** SCHEMA_DATA payload (S→M, §5.5). */
144
+ interface SchemaDataMsg {
145
+ offset: number;
146
+ bytes: Uint8Array;
147
+ }
148
+ /** NAK payload (S→M, §5.9). */
149
+ interface NakMsg {
150
+ cmd: number;
151
+ reason: number;
152
+ }
153
+ /** One entry inside a FRAME_DEFINE command (§5.6). */
154
+ interface FrameDefineEntry {
155
+ topicIndex: number;
156
+ fieldBitmask: number;
157
+ }
158
+ declare function decodeAnnounce(data: Uint8Array): AnnounceMsg | null;
159
+ declare function decodeHeartbeat(data: Uint8Array): HeartbeatMsg | null;
160
+ declare function decodeSchemaData(data: Uint8Array): SchemaDataMsg | null;
161
+ declare function decodeNak(data: Uint8Array): NakMsg | null;
162
+ /** Broadcast DISCOVER — all nodes re-announce and clear their frame tables. */
163
+ declare function encodeDiscover(): CanFrameTx;
164
+ /** Targeted DISCOVER for a single node (§5.11 reprovisioning). */
165
+ declare function encodeDiscoverTo(nodeId: number): CanFrameTx;
166
+ /** Broadcast MASTER_HEARTBEAT (§5.11). Refreshes slave master-lost watchdogs. */
167
+ declare function encodeMasterHeartbeat(): CanFrameTx;
168
+ declare function encodeSchemaRead(nodeId: number, offset: number, length: number, seq: number): CanFrameTx;
169
+ declare function encodeFrameDefine(nodeId: number, seq: number, stdId: number, periodUs: number, dir: number, entries: readonly FrameDefineEntry[]): CanFrameTx;
170
+ declare function encodeStart(nodeId: number, seq: number): CanFrameTx;
171
+ declare function encodeStop(nodeId: number, seq: number): CanFrameTx;
172
+ /**
173
+ * Accumulator for SCHEMA_DATA chunks. The slave DLC-pads each response (§4.3)
174
+ * so `chunk.bytes.length` is typically larger than what the master requested;
175
+ * the accumulator clips against `expectedChunkLen` and the remaining blob
176
+ * length to drop those padding bytes (parity with
177
+ * `append_schema_chunk` in `fibril_can_bridge/src/config_plane_codec.cpp`).
178
+ *
179
+ * Out-of-order chunks are rejected (returns 0 from {@link append}).
180
+ */
181
+ declare class SchemaAccumulator {
182
+ #private;
183
+ constructor(blobTotalLen: number);
184
+ get length(): number;
185
+ get blobTotalLen(): number;
186
+ get complete(): boolean;
187
+ bytes(): Uint8Array;
188
+ /**
189
+ * Append `chunk` if its offset matches the current accumulator length.
190
+ * Returns the number of bytes actually appended (0 on rejection).
191
+ */
192
+ append(chunk: SchemaDataMsg, expectedChunkLen: number): number;
193
+ }
194
+ //#endregion
195
+ //#region src/scalar-codec.d.ts
196
+ /**
197
+ * Scalar type enumeration and little-endian wire codecs. Mirrors
198
+ * `fibril_can_core/include/fibril_can_core/scalar_type.hpp` and
199
+ * `fibril_can_core/src/scalar_type.cpp`.
200
+ *
201
+ * Type ordinal values are part of the protobuf schema blob's enum, so they
202
+ * must not be renumbered.
203
+ */
204
+ declare enum ScalarType {
205
+ Bool = 0,
206
+ U8 = 1,
207
+ I8 = 2,
208
+ U16 = 3,
209
+ I16 = 4,
210
+ U32 = 5,
211
+ I32 = 6,
212
+ U64 = 7,
213
+ I64 = 8,
214
+ F32 = 9,
215
+ F64 = 10
216
+ }
217
+ declare const SCALAR_NAMES: readonly string[];
218
+ /** Wire size in bytes of one scalar element. bool is 1 byte. */
219
+ declare function scalarSize(type: ScalarType): number;
220
+ declare function scalarName(type: ScalarType): string | undefined;
221
+ declare function scalarFromName(name: string): ScalarType | undefined;
222
+ /**
223
+ * Pack an integer / bool value as little-endian bytes into `out` at `offset`.
224
+ * Writes scalarSize(type) bytes — pass a wide-enough `out`.
225
+ *
226
+ * Pass `bigint` for 64-bit types (U64/I64). `number` is accepted for
227
+ * everything (it is internally widened to bigint so signed callers get the
228
+ * same sign-extension semantics as the C++ `pack_scalar_int_le(uint64_t)`).
229
+ * Signed callers pass the negative number directly; the function performs the
230
+ * 64-bit two's-complement cast.
231
+ */
232
+ declare function packScalarIntLe(type: ScalarType, value: number | bigint, out: Uint8Array, offset?: number): void;
233
+ /** Bit-cast a float32 to its IEEE-754 little-endian representation. */
234
+ declare function packScalarF32Le(value: number, out: Uint8Array, offset?: number): void;
235
+ /** Bit-cast a float64 to its IEEE-754 little-endian representation. */
236
+ declare function packScalarF64Le(value: number, out: Uint8Array, offset?: number): void;
237
+ /**
238
+ * Inverse of {@link packScalarIntLe}. Returns the raw bit pattern as bigint;
239
+ * caller interprets sign via {@link sign-extend} helpers below.
240
+ */
241
+ declare function unpackScalarIntLe(type: ScalarType, src: Uint8Array, offset?: number): bigint;
242
+ declare function unpackScalarF32Le(src: Uint8Array, offset?: number): number;
243
+ declare function unpackScalarF64Le(src: Uint8Array, offset?: number): number;
244
+ /**
245
+ * Sign-extend a bigint that was read as an unsigned `bytes`-byte field to a
246
+ * signed bigint. Useful for I8/I16/I32/I64 after {@link unpackScalarIntLe}.
247
+ */
248
+ declare function signExtend(bits: bigint, bytes: number): bigint;
249
+ //#endregion
250
+ //#region src/schema.d.ts
251
+ declare enum Direction {
252
+ M2S = 0,
253
+ S2M = 1
254
+ }
255
+ interface SchemaField {
256
+ name: string;
257
+ type: ScalarType;
258
+ /** 0 = scalar; >0 = fixed-length array of `type`. */
259
+ arrayLen: number;
260
+ /** Per-field override of the owning topic's defaultPeriodUs; undefined = inherit. */
261
+ periodUsOverride?: number;
262
+ /** Master-side planner skips this field if false. Defaults to true. */
263
+ txEnabled: boolean;
264
+ }
265
+ interface RosMapping {
266
+ rosType: string;
267
+ rosName: string;
268
+ fieldMap: string[];
269
+ }
270
+ interface SchemaTopic {
271
+ name: string;
272
+ dir: Direction;
273
+ fields: SchemaField[];
274
+ defaultPeriodUs: number;
275
+ ros: RosMapping;
276
+ }
277
+ interface SchemaService {
278
+ name: string;
279
+ request: SchemaField[];
280
+ response: SchemaField[];
281
+ ros: RosMapping;
282
+ }
283
+ interface SchemaParam {
284
+ name: string;
285
+ type: ScalarType;
286
+ arrayLen: number;
287
+ /** Little-endian packed payload. Always present for default; min/max may be empty. */
288
+ defaultValue: Uint8Array;
289
+ minValue: Uint8Array;
290
+ maxValue: Uint8Array;
291
+ }
292
+ interface SchemaBlockType {
293
+ name: string;
294
+ topics: SchemaTopic[];
295
+ services: SchemaService[];
296
+ params: SchemaParam[];
297
+ }
298
+ interface SchemaBlockArray {
299
+ /** Index into NodeSchema.types (was `type_id` on the wire). */
300
+ blockTypeIndex: number;
301
+ rosNamespace: string;
302
+ }
303
+ interface WireLimits {
304
+ /** 0 = use bridge default. */
305
+ paramSetBatchBytes: number;
306
+ }
307
+ interface NodeSchema {
308
+ protocolVersion: number;
309
+ nodeName: string;
310
+ types: SchemaBlockType[];
311
+ instances: SchemaBlockArray[];
312
+ limits: WireLimits;
313
+ }
314
+ /**
315
+ * Parse a schema blob (proto3 binary) into the master-side IR.
316
+ *
317
+ * Throws {@link ProtoError} on malformed input or on
318
+ * `protocolVersion !== 1` (the only version this codec understands —
319
+ * mirrors C++ side's UnsupportedProtocolVersion behaviour).
320
+ */
321
+ declare function decodeSchema(blob: Uint8Array): NodeSchema;
322
+ /** Wire size in bytes for a single field (scalar or fixed array). */
323
+ declare function fieldWireSize(field: SchemaField): number;
324
+ //#endregion
325
+ //#region src/dynamic-codec.d.ts
326
+ /**
327
+ * Field-driven dict ⇄ Uint8Array codec. The runtime equivalent of what
328
+ * codegen would otherwise emit per message — encode/decode is a pure
329
+ * function of the field list ({@link SchemaField}[]) and the JS dict.
330
+ *
331
+ * Conventions (matches CLAUDE.md "値の表現規約"):
332
+ * - dict keys are the schema field name verbatim
333
+ * - scalar ↔ JS `number`, except U64/I64 ↔ `bigint`
334
+ * - bool ↔ JS `boolean`
335
+ * - fixed-length array (`arrayLen > 0`) ↔ regular JS array
336
+ * (`number[]` / `bigint[]` / `boolean[]`) of exactly `arrayLen` elements
337
+ *
338
+ * Throws on missing / wrong-shape inputs. Decode tolerates extra trailing
339
+ * bytes (mirrors C++ side: the master ignores trailing slop so that future
340
+ * schema additions don't break older decoders).
341
+ */
342
+ declare class DynamicCodecError extends Error {
343
+ constructor(message: string);
344
+ }
345
+ /** Tag union of accepted JS value shapes for one field. */
346
+ type FieldValue = number | bigint | boolean | number[] | bigint[] | boolean[];
347
+ /** Dict shape — string-keyed values, one entry per field. */
348
+ type Dict = Record<string, FieldValue>;
349
+ /**
350
+ * Sum the wire size of a field list. The result is the byte count that
351
+ * {@link encodeFields} will produce.
352
+ */
353
+ declare function fieldsWireSize(fields: readonly SchemaField[]): number;
354
+ /**
355
+ * Encode a dict into the canonical little-endian wire bytes for `fields`.
356
+ * Returns a fresh `Uint8Array`. Throws {@link DynamicCodecError} on a
357
+ * missing or wrong-shape field.
358
+ */
359
+ declare function encodeFields(fields: readonly SchemaField[], value: Dict): Uint8Array;
360
+ /**
361
+ * Decode wire bytes into a dict keyed by field name. Extra trailing bytes
362
+ * are ignored. Throws {@link DynamicCodecError} only if the payload is
363
+ * shorter than the field list demands.
364
+ */
365
+ declare function decodeFields(fields: readonly SchemaField[], bytes: Uint8Array): Dict;
366
+ //#endregion
367
+ //#region src/numbering.d.ts
368
+ /**
369
+ * Flat-numbering tables for topics, services, and parameters (§4.2).
370
+ *
371
+ * Port of `fibril_can_core/src/numbering.cpp`. Numbering is a pure function
372
+ * of (schema, instance-count-vector). The same tables are derived
373
+ * independently by codegen (with max_count for static checks), the slave
374
+ * runtime (with actual counts at boot) and the master (with counts from the
375
+ * ANNOUNCE frame) — keeping this in one place is what guarantees they agree.
376
+ */
377
+ declare const SERVICE_INDEX_CANCEL = 255;
378
+ declare const SERVICE_INDEX_PARAM_SET = 253;
379
+ /** Maximum first-class service_index produced by numbering (inclusive). */
380
+ declare const SERVICE_INDEX_MAX = 252;
381
+ /** Number of available first-class service slots. */
382
+ declare const SERVICE_INDEX_CAPACITY: number;
383
+ declare enum NumberingError {
384
+ Ok = "ok",
385
+ ServiceIndexOverflow = "service_index overflow (>253)",
386
+ TopicIndexOverflow = "topic_index overflow (>65535)",
387
+ ParamIndexOverflow = "param_index overflow (>65535)",
388
+ InstanceCountMismatch = "actual_counts length != #instances",
389
+ BlockTypeOutOfRange = "BlockArray::blockTypeIndex out of range"
390
+ }
391
+ /** Concrete location of one (instance, endpoint) within a NodeSchema. */
392
+ interface EndpointKey {
393
+ /** Index into NodeSchema.instances. */
394
+ blockArray: number;
395
+ /** 0..count-1 */
396
+ instance: number;
397
+ /** 0..nEndpoints(type)-1 */
398
+ endpoint: number;
399
+ }
400
+ /** Forward + inverse maps for a single endpoint kind. */
401
+ interface NumberingTable {
402
+ /**
403
+ * size = Σ_b(actualCounts[b] × nEndpoints(types[instances[b].blockTypeIndex]));
404
+ * the array index IS the flat numbering.
405
+ */
406
+ byIndex: EndpointKey[];
407
+ /** Per-block-array base offsets (size = #blockArrays + 1; last is total). */
408
+ bases: number[];
409
+ }
410
+ interface NumberingResult {
411
+ topics: NumberingTable;
412
+ services: NumberingTable;
413
+ params: NumberingTable;
414
+ status: NumberingError;
415
+ }
416
+ /**
417
+ * Compute the three numbering tables. `actualCounts` must have the same
418
+ * length as `schema.instances`. The result has `status !== Ok` if input is
419
+ * malformed or service_index would overflow into the reserved range
420
+ * (§4.2 §9.4).
421
+ *
422
+ * On error the returned tables are empty but still safe to inspect.
423
+ */
424
+ declare function computeNumbering(schema: NodeSchema, actualCounts: readonly number[]): NumberingResult;
425
+ //#endregion
426
+ //#region src/schema-index.d.ts
427
+ /** Resolved descriptor for one S→M or M→S topic. */
428
+ interface TopicDescriptor {
429
+ readonly kind: 'topic';
430
+ readonly path: string;
431
+ readonly topicIndex: number;
432
+ readonly dir: Direction;
433
+ readonly fields: readonly SchemaField[];
434
+ readonly defaultPeriodUs: number;
435
+ readonly wireSize: number;
436
+ }
437
+ /** Resolved descriptor for one Service. */
438
+ interface ServiceDescriptor {
439
+ readonly kind: 'service';
440
+ readonly path: string;
441
+ readonly serviceIndex: number;
442
+ readonly requestFields: readonly SchemaField[];
443
+ readonly responseFields: readonly SchemaField[];
444
+ readonly requestWireSize: number;
445
+ readonly responseWireSize: number;
446
+ }
447
+ /** Resolved descriptor for one Param (single-field). */
448
+ interface ParamDescriptor {
449
+ readonly kind: 'param';
450
+ readonly path: string;
451
+ readonly paramIndex: number;
452
+ readonly type: ScalarType;
453
+ /** 0 = scalar; >0 = fixed-length array of `type`. */
454
+ readonly arrayLen: number;
455
+ readonly wireSize: number;
456
+ readonly defaultValue: Uint8Array;
457
+ readonly minValue: Uint8Array;
458
+ readonly maxValue: Uint8Array;
459
+ }
460
+ type EndpointDescriptor = TopicDescriptor | ServiceDescriptor | ParamDescriptor;
461
+ /**
462
+ * Path → descriptor index for one slave. Built once from
463
+ * (schema, numbering) and reused for the lifetime of a snapshot.
464
+ */
465
+ declare class SchemaIndex {
466
+ #private;
467
+ constructor(schema: NodeSchema, numbering: NumberingResult, instanceCounts: readonly number[]);
468
+ topic(path: string): TopicDescriptor | undefined;
469
+ service(path: string): ServiceDescriptor | undefined;
470
+ param(path: string): ParamDescriptor | undefined;
471
+ topicByIndex(idx: number): TopicDescriptor | undefined;
472
+ serviceByIndex(idx: number): ServiceDescriptor | undefined;
473
+ paramByIndex(idx: number): ParamDescriptor | undefined;
474
+ /** All topic paths in numbering order. Useful for debugging / sniffer UI. */
475
+ topicPaths(): string[];
476
+ servicePaths(): string[];
477
+ paramPaths(): string[];
478
+ }
479
+ //#endregion
480
+ //#region src/service-client.d.ts
481
+ /** Per-call options. */
482
+ interface ServiceCallOptions {
483
+ /** Pre-encoded request payload (no status byte — that's response-only). */
484
+ request: Uint8Array;
485
+ /** Override default timeout. Default 250 ms (matches bridge `service_timeout_`). */
486
+ timeoutMs?: number;
487
+ /** Cancel via AbortSignal — fires SVC_CANCEL (§7.5) to the slave. */
488
+ signal?: AbortSignal;
489
+ }
490
+ /** Resolved service response. `status === 0` (SVC_OK) is the success path. */
491
+ interface ServiceResult {
492
+ status: number;
493
+ data: Uint8Array;
494
+ }
495
+ /** Throwable error union for `ServiceClient.call`. */
496
+ declare class ServiceCallError extends Error {
497
+ constructor(message: string, options?: ErrorOptions);
498
+ }
499
+ declare class ServiceTimeoutError extends ServiceCallError {
500
+ constructor(message?: string);
501
+ }
502
+ declare class ServiceUnavailableError extends ServiceCallError {
503
+ constructor(message?: string);
504
+ }
505
+ declare class ServiceCancelledError extends ServiceCallError {
506
+ constructor(message?: string);
507
+ }
508
+ declare class NoCallSlotError extends ServiceCallError {
509
+ constructor(serviceIndex: number);
510
+ }
511
+ /**
512
+ * Single-slave master service-call client. Port of NodeAgent's
513
+ * begin_service_call / on_service_response / on_service_segment flow but
514
+ * Promise-based and AbortSignal-aware.
515
+ *
516
+ * One {@link ServiceClient} attaches to the transport and demuxes incoming
517
+ * SVC_RESPONSE frames for a single `nodeId`. NodeAgent owns one per slave
518
+ * and exposes it on the running snapshot.
519
+ */
520
+ declare class ServiceClient {
521
+ #private;
522
+ readonly nodeId: number;
523
+ constructor(opts: {
524
+ nodeId: number;
525
+ transport: ITransport;
526
+ defaultTimeoutMs?: number;
527
+ now?: () => number;
528
+ });
529
+ attach(): void;
530
+ detach(): void;
531
+ /** Number of in-flight calls. Testing / introspection. */
532
+ get pendingCount(): number;
533
+ /**
534
+ * Sweep call deadlines and segment-reassembly TTLs. The owning agent
535
+ * should call this from its periodic sweep loop.
536
+ */
537
+ sweep(nowMs?: number): void;
538
+ /** Reject every in-flight call as Unavailable. NodeAgent calls this on lost / reprovision. */
539
+ failAll(reason?: string): void;
540
+ call(serviceIndex: number, opts: ServiceCallOptions): Promise<ServiceResult>;
541
+ }
542
+ //#endregion
543
+ //#region src/typed-param.d.ts
544
+ /**
545
+ * Schema-driven typed wrapper around PARAM_SET (SPEC §8.2, reserved
546
+ * service_index 0xFD).
547
+ *
548
+ * Per SPEC §8.1 PARAM is M→S one-way: the master holds the source of truth
549
+ * and re-injects defaults at provisioning time, so this wrapper exposes
550
+ * `.set()` only — there's no `.get()` that queries the slave. The
551
+ * descriptor's decoded default is available via `defaultValue()` for
552
+ * reference / hydration of master-side state.
553
+ *
554
+ * The wire shape for one PARAM_SET request is:
555
+ * [0] count = 1
556
+ * [1..2] u16 param_index (LE)
557
+ * [3..] value (scalar little-endian or fixed-array contiguous)
558
+ *
559
+ * Response is `[0] status [1..2] failed_param_index (when status != OK)`.
560
+ * `set()` resolves with the status code; non-OK includes `failedIndex` so
561
+ * the caller can correlate with a multi-param batch (single-param sets
562
+ * always blame this param).
563
+ */
564
+ declare class ParamCodecError extends Error {
565
+ constructor(message: string);
566
+ }
567
+ interface TypedParamSetOptions {
568
+ timeoutMs?: number;
569
+ signal?: AbortSignal;
570
+ }
571
+ interface TypedParamSetResult {
572
+ /** SPEC §7.2 status (0 = SVC_OK). */
573
+ readonly status: number;
574
+ /**
575
+ * On `status != SVC_OK`, the slave-reported param_index that failed.
576
+ * For a single-param call this is always `descriptor.paramIndex` if the
577
+ * slave fills the field, else `null` (slave omitted the field).
578
+ */
579
+ readonly failedIndex: number | null;
580
+ }
581
+ /** Build a PARAM_SET request payload for a single (descriptor, value) pair. */
582
+ declare function encodeParamSetSingle(desc: ParamDescriptor, value: unknown): Uint8Array;
583
+ declare class TypedParam<T> {
584
+ #private;
585
+ readonly path: string;
586
+ readonly descriptor: ParamDescriptor;
587
+ constructor(client: ServiceClient, descriptor: ParamDescriptor);
588
+ /**
589
+ * Decode the schema-embedded default value (SPEC §9.1 `default`). Useful
590
+ * to seed the master-side persistence store. Returns `null` if the
591
+ * descriptor carries no default payload.
592
+ */
593
+ defaultValue(): T | null;
594
+ /** Send a PARAM_SET with a single entry for this param. */
595
+ set(value: T, opts?: TypedParamSetOptions): Promise<TypedParamSetResult>;
596
+ }
597
+ //#endregion
598
+ //#region src/typed-service.d.ts
599
+ /**
600
+ * Schema-driven typed wrapper around {@link ServiceClient.call}.
601
+ *
602
+ * The generic parameters are compile-time only: at runtime, encoding is
603
+ * driven entirely by the {@link ServiceDescriptor}'s field list. Callers
604
+ * supply `Req` / `Resp` interfaces matching the schema fields by name —
605
+ * the runtime trusts the dict layout and packs by field iteration order.
606
+ *
607
+ * Returned on success: `{ status, data }` where `status` is the SPEC §7.2
608
+ * service status (0 = OK) and `data` is the decoded response dict typed as
609
+ * `Resp`. Non-OK responses still return a `data` object — the slave is
610
+ * required to fill the response payload even on failure (§7.2), and the
611
+ * decoder will best-effort attempt it; if the body is shorter than the
612
+ * schema demands, `data` is `null` instead of throwing (so callers can
613
+ * surface the status to the user without losing the whole response).
614
+ */
615
+ interface TypedServiceResult<Resp extends Dict> {
616
+ readonly status: number;
617
+ readonly data: Resp | null;
618
+ }
619
+ interface TypedServiceCallOptions {
620
+ timeoutMs?: number;
621
+ signal?: AbortSignal;
622
+ }
623
+ declare class TypedService<Req extends Dict, Resp extends Dict> {
624
+ #private;
625
+ readonly path: string;
626
+ readonly descriptor: ServiceDescriptor;
627
+ constructor(client: ServiceClient, descriptor: ServiceDescriptor);
628
+ /** Encode `request`, dispatch via the service plane, decode the response. */
629
+ call(request: Req, opts?: TypedServiceCallOptions): Promise<TypedServiceResult<Resp>>;
630
+ }
631
+ //#endregion
632
+ //#region src/node-agent.d.ts
633
+ /**
634
+ * One planned data-plane CAN frame, supplied by the caller (typically a
635
+ * frame planner — P6) and consumed by {@link NodeAgent} when it issues
636
+ * FRAME_DEFINE during bring-up. Mirrors `FrameSpec` in
637
+ * `fibril_can_bridge/include/fibril_can_bridge/frame_planner.hpp`.
638
+ */
639
+ interface FramePlan {
640
+ stdId: number;
641
+ periodUs: number;
642
+ dir: Direction;
643
+ entries: readonly FrameDefineEntry[];
644
+ }
645
+ /**
646
+ * Per-bring-up context handed to a {@link FramePlanFactory} callback so the
647
+ * caller can branch on the specific slave being provisioned (e.g. read an
648
+ * external per-nodeId field-enable override map). The same factory is
649
+ * shared across every {@link NodeAgent} the controller spawns, so this is
650
+ * the only way for the factory to discriminate slaves.
651
+ */
652
+ interface FramePlanContext {
653
+ /** The slave being provisioned (1..0x7E). */
654
+ readonly nodeId: number;
655
+ }
656
+ /**
657
+ * Builds the FRAME_DEFINE plan for a slave once its schema is decoded and
658
+ * numbered. The optional `ctx` argument carries the nodeId so callers that
659
+ * keep per-slave overrides (e.g. UI-controlled S→M field toggles) can fork
660
+ * on it without threading state through {@link NodeAgentOptions}. The
661
+ * 2-arg form is still accepted — extra parameters are simply ignored.
662
+ */
663
+ type FramePlanFactory = ((schema: NodeSchema, numbering: NumberingResult, ctx: FramePlanContext) => readonly FramePlan[]) | readonly FramePlan[];
664
+ /** Phase machine — public for observers. */
665
+ type NodeProvisionState = {
666
+ kind: 'idle';
667
+ } | {
668
+ kind: 'awaiting-announce';
669
+ } | {
670
+ kind: 'schema-reading';
671
+ received: number;
672
+ total: number;
673
+ } | {
674
+ kind: 'numbering';
675
+ } | {
676
+ kind: 'provisioning';
677
+ pendingDefines: number;
678
+ }
679
+ /**
680
+ * FRAME_DEFINE completed successfully but START has been withheld by
681
+ * the {@link NodeAgentOptions.autoStart} gate. `startNow()` moves the
682
+ * agent forward. `reprovision()` / a fresh ANNOUNCE re-enter the flow
683
+ * from bring-up.
684
+ */
685
+ | {
686
+ kind: 'awaiting-start';
687
+ } | {
688
+ kind: 'running';
689
+ } | {
690
+ kind: 'lost';
691
+ reason: string;
692
+ } | {
693
+ kind: 'blocked';
694
+ reason: string;
695
+ };
696
+ /** Snapshot available once the slave reaches RUNNING. */
697
+ interface NodeRunningSnapshot {
698
+ nodeId: number;
699
+ schemaHash: bigint;
700
+ bootId: number;
701
+ instanceCounts: Uint8Array;
702
+ blob: Uint8Array;
703
+ schema: NodeSchema;
704
+ numbering: NumberingResult;
705
+ framePlan: readonly FramePlan[];
706
+ /** std_id → topic_index map for the data plane (one entry per FRAME_DEFINE). */
707
+ stdIdToTopicIndex: Map<number, number>;
708
+ /** §7 master-side RPC client. Stays valid for the lifetime of the snapshot. */
709
+ services: ServiceClient;
710
+ /**
711
+ * Path → IR descriptor lookup. Drives the typed accessors below.
712
+ * Exposed for sniffer-style introspection (`index.topicPaths()` etc.).
713
+ */
714
+ index: SchemaIndex;
715
+ /**
716
+ * Schema-driven typed RPC wrapper. Throws if `path` is unknown. Generic
717
+ * `Req` / `Resp` are compile-time only — runtime encoding is driven by
718
+ * the schema's field list.
719
+ */
720
+ service<Req extends Dict, Resp extends Dict>(path: string): TypedService<Req, Resp>;
721
+ /**
722
+ * Schema-driven PARAM_SET wrapper. Throws if `path` is unknown.
723
+ * `T` is `number` for scalar params (except U64/I64 → `bigint`),
724
+ * `boolean` for bool, or the array form for fixed-length array params.
725
+ */
726
+ param<T = number | bigint | boolean | number[] | bigint[] | boolean[]>(path: string): TypedParam<T>;
727
+ }
728
+ interface NodeAgentOptions {
729
+ /** 1..0x7E — broadcast (0x7F) is rejected. */
730
+ nodeId: number;
731
+ /** Already open()'d transport. The agent attaches its own onFrame. */
732
+ transport: ITransport;
733
+ /** Strategy for building the FRAME_DEFINE plan. */
734
+ framePlan: FramePlanFactory;
735
+ /**
736
+ * Per-config-command ACK timeout in ms. Slaves must ACK within this
737
+ * window or the agent abandons the bring-up attempt and waits for the
738
+ * next ANNOUNCE. Default 250 ms (matches bridge's
739
+ * `bring_up_ack_timeout_`).
740
+ */
741
+ ackTimeoutMs?: number;
742
+ /**
743
+ * SCHEMA_READ chunk byte limit. SPEC §4.3 caps a single response at the
744
+ * SINGLE-frame payload (≤59 data bytes after the 4-byte offset header).
745
+ * Default 59.
746
+ */
747
+ schemaChunkSize?: number;
748
+ /**
749
+ * Multiplier on `ackTimeoutMs` used as the schema-read retry budget.
750
+ * Default 3 (one initial + two retries).
751
+ */
752
+ schemaReadRetries?: number;
753
+ /**
754
+ * Heartbeat-loss budget. The agent expects a HEARTBEAT every 100 ms
755
+ * (§14); after this much silence it transitions to `lost`. Default 350.
756
+ */
757
+ heartbeatTimeoutMs?: number;
758
+ /** Per-call default timeout for `services.call()`. Default `ackTimeoutMs`. */
759
+ serviceTimeoutMs?: number;
760
+ /**
761
+ * Gate consulted right before START is issued. When it returns `false`,
762
+ * the agent stops in {@link NodeProvisionState} `awaiting-start` after
763
+ * FRAME_DEFINE ACKs come back; the caller commits with `startNow()`.
764
+ * Read fresh at each decision point so callers can flip the setting at
765
+ * runtime without reconstructing the agent. Default `() => true`.
766
+ */
767
+ autoStart?: () => boolean;
768
+ /** Optional state-change observer. */
769
+ onState?: (state: NodeProvisionState) => void;
770
+ /** Optional non-fatal error observer (decode warnings, hash mismatch). */
771
+ onError?: (err: Error) => void;
772
+ /**
773
+ * Override for the wall clock. Tests pass a deterministic counter.
774
+ * Default `() => Date.now()`.
775
+ */
776
+ now?: () => number;
777
+ }
778
+ /**
779
+ * Per-slave provisioning state machine. Mirrors
780
+ * `fibril_can_bridge::NodeAgent` lifecycle entry points but trimmed for the
781
+ * single-threaded TS master — no ROS entity creation, no PARAM_SET
782
+ * re-injection (P5), no plan computation (P6).
783
+ *
784
+ * Lifecycle: `idle` → `awaiting-announce` → `schema-reading` (skipped if
785
+ * blob already cached on this agent instance) → `numbering` →
786
+ * `provisioning` → `running`. HEARTBEAT loss drops to `lost`; the next
787
+ * ANNOUNCE restarts the flow.
788
+ */
789
+ declare class NodeAgent {
790
+ #private;
791
+ readonly nodeId: number;
792
+ constructor(opts: NodeAgentOptions);
793
+ /** Service-plane RPC client for this slave. Always available. */
794
+ get services(): ServiceClient;
795
+ get state(): NodeProvisionState;
796
+ get snapshot(): NodeRunningSnapshot | null;
797
+ /**
798
+ * Attach to the transport and wait for ANNOUNCE. Resolves immediately —
799
+ * progress flows via {@link state} / {@link snapshot} / `onState`. Call
800
+ * `stop()` to detach.
801
+ */
802
+ start(): Promise<void>;
803
+ /**
804
+ * Re-run the FRAME_DEFINE / START handshake against the live slave using
805
+ * the most recent ANNOUNCE + cached schema. The {@link FramePlanFactory}
806
+ * is invoked again — callers that key off an external mutable state
807
+ * (e.g. per-field TX-enable overrides) will see the latest values.
808
+ *
809
+ * The agent must have observed an ANNOUNCE already; `reprovision()`
810
+ * before that throws. RUNNING is required so that the slave is known to
811
+ * be reachable — calling during `lost` would race the heartbeat
812
+ * watchdog. Pending service calls are failed and the current snapshot is
813
+ * torn down before the new bring-up begins, mirroring what happens on a
814
+ * boot_id change.
815
+ */
816
+ reprovision(): Promise<void>;
817
+ /** Detach, send STOP if currently RUNNING, clear all in-flight state. */
818
+ stop(): Promise<void>;
819
+ /**
820
+ * Commit the deferred START when {@link NodeAgentOptions.autoStart}
821
+ * gated the previous bring-up. Only legal in `awaiting-start`; any
822
+ * other state throws so callers spot logic bugs instead of racing
823
+ * with a fresh ANNOUNCE.
824
+ */
825
+ startNow(): Promise<void>;
826
+ }
827
+ //#endregion
828
+ //#region src/data-plane.d.ts
829
+ /**
830
+ * Master-side decoder for S→M data-plane frames.
831
+ *
832
+ * Listens on the transport for standard-ID frames whose IDs match one of
833
+ * the std_ids configured in any attached slave's {@link FramePlan}. For
834
+ * each match, unpacks the topic entries packed into the frame's payload
835
+ * (in FRAME_DEFINE order) using the schema field bitmask and publishes
836
+ * a decoded {@link Dict} per topic to subscribers.
837
+ *
838
+ * Lifecycle:
839
+ * - `new DataPlaneReceiver({ transport })` registers an `onFrame` tap.
840
+ * - `attachSlave(nodeId, framePlan, schemaIndex)` claims a slave's std_ids.
841
+ * - `detachSlave(nodeId)` releases them (call on RUNNING → not-RUNNING).
842
+ * - `dispose()` unsubscribes and clears all state.
843
+ *
844
+ * Two slaves cannot publish on the same std_id — attaching the second
845
+ * throws. This mirrors the bus-level reality: identical std_ids would
846
+ * collide on the wire.
847
+ *
848
+ * M→S frames (master output, dir `Direction.M2S`) are skipped during
849
+ * attach so the receiver doesn't try to decode echo or self-loopback
850
+ * frames as if they were slave-published.
851
+ */
852
+ interface DataPlaneReceiverOptions {
853
+ transport: ITransport;
854
+ /** Non-fatal error sink (decode errors, listener throws). */
855
+ onError?: (err: Error) => void;
856
+ /** Clock override for tests. Default `() => Date.now()`. */
857
+ now?: () => number;
858
+ }
859
+ type TopicListener = (value: Dict, tsMs: number) => void;
860
+ interface TopicSample {
861
+ readonly value: Dict;
862
+ readonly tsMs: number;
863
+ }
864
+ interface TopicStats {
865
+ /** Frames ever decoded into this topic since the slave attached. */
866
+ readonly count: number;
867
+ /** `now()` of the most recent frame. */
868
+ readonly lastRxMs: number;
869
+ /**
870
+ * Rolling rate over the last few samples (Hz). 0 until at least
871
+ * two frames have been seen.
872
+ */
873
+ readonly ratePerSec: number;
874
+ }
875
+ declare class DataPlaneReceiver {
876
+ #private;
877
+ constructor(opts: DataPlaneReceiverOptions);
878
+ /**
879
+ * Claim a slave's S→M std_ids. Re-attaching the same nodeId atomically
880
+ * detaches the prior plan first (re-provisioning case).
881
+ */
882
+ attachSlave(nodeId: number, framePlan: readonly FramePlan[], index: SchemaIndex): void;
883
+ /** Release a slave's std_ids and drop its cached topic samples. */
884
+ detachSlave(nodeId: number): void;
885
+ onTopic(nodeId: number, path: string, listener: TopicListener): Unsubscribe;
886
+ latest(nodeId: number, path: string): TopicSample | null;
887
+ stats(nodeId: number, path: string): TopicStats | null;
888
+ /** Paths attached for this slave (in plan order). */
889
+ topicPathsFor(nodeId: number): string[];
890
+ /** Bumps on every successfully decoded entry — cheap change detector. */
891
+ get version(): number;
892
+ dispose(): void;
893
+ }
894
+ //#endregion
895
+ //#region src/frame-planner.d.ts
896
+ /**
897
+ * Frame planner — turns a list of "I want field X at period Y" requests into
898
+ * the concrete FRAME_DEFINE bodies the master ships to the slave (§6.3).
899
+ *
900
+ * Port of `fibril_can_core/src/frame_planner.cpp` — same bucketing rules:
901
+ * - Group requests by (direction, period_us); never combine across either.
902
+ * - Inside a bucket, bin-pack whole topics into ≤64-byte frames.
903
+ * - A single topic whose declared fields exceed 64 bytes is the sole
904
+ * field-split exception (`emitOversizedTopic`).
905
+ *
906
+ * Determinism is load-bearing: codegen, bridge, and this TS master must
907
+ * produce identical std_id assignments for the same input. Both the
908
+ * bucket map and the per-bucket topic map iterate in ascending key order
909
+ * (matching `std::map` in the C++ reference).
910
+ */
911
+ interface FieldRequest {
912
+ topicIndex: number;
913
+ /** 0..15, bit position in `fieldBitmask`. */
914
+ fieldId: number;
915
+ /** Wire size of this field. */
916
+ sizeBytes: number;
917
+ /** 0 = on-trigger only. */
918
+ periodUs: number;
919
+ dir: Direction;
920
+ }
921
+ interface FrameSpec {
922
+ stdCanId: number;
923
+ periodUs: number;
924
+ dir: Direction;
925
+ logicalBytes: number;
926
+ entries: FrameDefineEntry[];
927
+ }
928
+ /**
929
+ * 11-bit Standard ID allocator with a free pool for IDs released after a
930
+ * grace window (§11.4.1). `next()` returns `null` on exhaustion rather
931
+ * than wrapping — wrapping past 0x7FF would alias on the wire.
932
+ */
933
+ declare class StdIdAllocator {
934
+ #private;
935
+ next(nowMs?: number): number | null;
936
+ release(id: number, availableAtMs: number): void;
937
+ reset(start?: number): void;
938
+ }
939
+ /**
940
+ * Plan a slave's frame layout. Returns `null` if the StdIdAllocator runs
941
+ * out of 11-bit IDs mid-plan — the caller MUST surface that; a partial
942
+ * plan would silently drop fields.
943
+ */
944
+ declare function planFrames(requests: readonly FieldRequest[], alloc: StdIdAllocator): FrameSpec[] | null;
945
+ //#endregion
946
+ //#region src/default-plan.d.ts
947
+ /**
948
+ * Per-field predicate handed to {@link buildDefaultS2MRequests} (and via
949
+ * it, {@link defaultS2MFramePlan}) so callers can apply a runtime mask on
950
+ * top of the schema-declared `txEnabled`.
951
+ *
952
+ * Contract:
953
+ * - Returning `false` skips the field entirely; the slave will not pack
954
+ * it into any S→M frame. Returning `true` keeps it.
955
+ * - Called only for S→M fields whose schema declares `txEnabled !==
956
+ * false` (schema-disabled fields are filtered out unconditionally).
957
+ * - `topicPath` is the same `'<expanded_ns>/<topic_name>'` string the
958
+ * master uses elsewhere (mirrors {@link SchemaIndex.topicPaths}), so
959
+ * UI override maps can key on the same identifier.
960
+ */
961
+ type S2MFieldFilter = (info: {
962
+ topicPath: string;
963
+ topicIndex: number;
964
+ fieldId: number;
965
+ fieldName: string;
966
+ field: SchemaField;
967
+ }) => boolean;
968
+ /**
969
+ * Synthesize the default {@link FieldRequest} list from a decoded schema +
970
+ * numbering table — every S2M topic field at its declared period
971
+ * (`field.periodUsOverride` if present, else the owning topic's
972
+ * `defaultPeriodUs`), skipping `txEnabled === false` fields.
973
+ *
974
+ * Pass `filter` to layer a runtime per-field mask on top (UI toggles,
975
+ * per-slave overrides). Schema-disabled fields are unconditionally
976
+ * dropped before `filter` is consulted, so callers don't have to repeat
977
+ * the `txEnabled` check.
978
+ *
979
+ * This is the natural input to {@link planFrames} when the master just
980
+ * wants to "observe everything the slave publishes" — i.e. the
981
+ * sniffer-style auto-provision path.
982
+ */
983
+ declare function buildDefaultS2MRequests(schema: NodeSchema, numbering: NumberingResult, filter?: S2MFieldFilter): FieldRequest[];
984
+ /**
985
+ * One-line FramePlanFactory that subscribes to every S2M field at its
986
+ * declared period using a freshly-allocated {@link StdIdAllocator}.
987
+ *
988
+ * Pass `filter` to apply a runtime per-field mask (e.g. a UI-controlled
989
+ * field-tx-enable map). With no filter, every schema-enabled S→M field is
990
+ * subscribed.
991
+ *
992
+ * Returns `[]` when planning fails (e.g. std_id exhaustion); the caller's
993
+ * NodeAgent will then leave the slave in `provisioning` → `awaiting-announce`
994
+ * with no frames defined, which is the right outcome — partial plans would
995
+ * silently drop fields.
996
+ */
997
+ declare function defaultS2MFramePlan(schema: NodeSchema, numbering: NumberingResult, filter?: S2MFieldFilter): FramePlan[];
998
+ //#endregion
999
+ //#region src/dlc.d.ts
1000
+ /**
1001
+ * CAN FD valid payload sizes in ascending order (§4.3).
1002
+ * Mirrors `fibril_can_core/include/fibril_can_core/packing.hpp::kCanFdDlcSteps`
1003
+ * and `fibril_can_runtime/src/fcan_wire.c::fcan_quantize_dlc`.
1004
+ */
1005
+ declare const FD_DLC_STEPS: readonly number[];
1006
+ /**
1007
+ * Round `bytes` up to the next valid CAN FD payload step. Returns 64 for any
1008
+ * input ≥ 64.
1009
+ */
1010
+ declare function quantizeDlc(bytes: number): number;
1011
+ /**
1012
+ * Grow `data` to the next DLC step by appending zero bytes. Returns a new
1013
+ * Uint8Array. Never shrinks: if `data` already exceeds 64 bytes (which is
1014
+ * a self-inconsistent frame the slave would misparse), the original is
1015
+ * returned unchanged so the backend rejects it loudly (parity with
1016
+ * `pad_to_dlc` in `fibril_can_bridge/src/config_plane_codec.cpp`).
1017
+ */
1018
+ declare function padToDlc(data: Uint8Array): Uint8Array;
1019
+ //#endregion
1020
+ //#region src/master-controller.d.ts
1021
+ /**
1022
+ * Multi-slave coordinator.
1023
+ *
1024
+ * One {@link MasterController} owns:
1025
+ * - a single transport (the bus)
1026
+ * - one {@link MasterHeartbeatBroadcaster} (shared across slaves)
1027
+ * - a single broadcast DISCOVER at start() to flush every slave's frame
1028
+ * table and pull fresh ANNOUNCEs onto the bus
1029
+ * - a `Map<nodeId, NodeAgent>` of provisioned slaves — auto-spawned on
1030
+ * the first ANNOUNCE / HEARTBEAT from a previously-unseen nodeId
1031
+ *
1032
+ * Periodic broadcast DISCOVER is intentionally NOT done. Broadcast
1033
+ * DISCOVER is destructive (§5.11): every receiving slave clears its frame
1034
+ * table and drops to UNPROVISIONED. Late-joining slaves are covered by
1035
+ * their own spontaneous ANNOUNCE × 3 at boot (§5.4); the user-initiated
1036
+ * "Discover" button (`discoverNow()`) is the escape hatch for the rare
1037
+ * case where the master came up first and missed the burst.
1038
+ *
1039
+ * Provisioning per slave still goes through {@link NodeAgent}. The
1040
+ * controller is the right place to add cross-slave concerns later (PARAM
1041
+ * persistence, name ownership, lab-mode multiplexers) without bloating
1042
+ * `NodeAgent`.
1043
+ *
1044
+ * The controller does NOT replace `NodeAgent` for single-slave embedded
1045
+ * use cases (e.g. tests, a fixed-topology setup); it sits one layer above
1046
+ * and is the natural entrypoint for the Web UI.
1047
+ */
1048
+ /** Default policy hooks applied to every spawned NodeAgent. */
1049
+ interface MasterControllerOptions {
1050
+ /** Already-open()'d transport. The controller attaches its own onFrame. */
1051
+ transport: ITransport;
1052
+ /** Strategy applied to every spawned NodeAgent. Required. */
1053
+ framePlan: FramePlanFactory;
1054
+ /** MASTER_HEARTBEAT broadcast period in ms. Default 100 (§5.11). */
1055
+ heartbeatPeriodMs?: number;
1056
+ /**
1057
+ * Broadcast DISCOVER period in ms. Default 0 (disabled). Setting >0 is
1058
+ * destructive — every periodic broadcast DISCOVER wipes every slave's
1059
+ * frame table and forces re-provisioning, breaking S→M topic flow until
1060
+ * each NodeAgent re-pushes its FRAME_DEFINEs. Useful only for very
1061
+ * niche reconnect-storm scenarios; prefer `discoverNow()` for on-demand
1062
+ * rediscovery and rely on slave spontaneous ANNOUNCE × 3 (§5.4) for
1063
+ * normal late-join.
1064
+ */
1065
+ discoverPeriodMs?: number;
1066
+ /**
1067
+ * Send a single broadcast DISCOVER inside `start()` to flush stale
1068
+ * slave state from a previous master session. Default true. Set false
1069
+ * in tests that drive ANNOUNCE manually and need a quiet bus.
1070
+ */
1071
+ initialDiscover?: boolean;
1072
+ /** Per-NodeAgent ACK timeout in ms. Default 250 (matches NodeAgent). */
1073
+ ackTimeoutMs?: number;
1074
+ /** Per-NodeAgent HEARTBEAT-loss budget in ms. Default 350. */
1075
+ heartbeatTimeoutMs?: number;
1076
+ /** Per-service-call default timeout in ms. Default `ackTimeoutMs`. */
1077
+ serviceTimeoutMs?: number;
1078
+ /**
1079
+ * Read-through gate applied to every spawned NodeAgent right before it
1080
+ * issues START. Callers pass a getter (not a boolean) so the flag can
1081
+ * be flipped at runtime — see `NodeAgentOptions.autoStart`. Default
1082
+ * `() => true`.
1083
+ */
1084
+ autoStart?: () => boolean;
1085
+ /** Optional clock override (tests). Default `() => Date.now()`. */
1086
+ now?: () => number;
1087
+ /** Optional non-fatal error sink. */
1088
+ onError?: (err: Error) => void;
1089
+ }
1090
+ /** Snapshot of one slave's state as observed by the controller. */
1091
+ interface ControllerSlaveSnapshot {
1092
+ readonly nodeId: number;
1093
+ readonly state: NodeProvisionState;
1094
+ /** Truthy once the slave reaches RUNNING for the first time. */
1095
+ readonly running: NodeRunningSnapshot | null;
1096
+ /** `now()` when HEARTBEAT was last seen. null if never. */
1097
+ readonly lastHeartbeatMs: number | null;
1098
+ /** boot_id observed (from ANNOUNCE or HEARTBEAT). null if neither yet. */
1099
+ readonly bootId: number | null;
1100
+ /** schema_hash observed in ANNOUNCE. null if not yet ANNOUNCEd. */
1101
+ readonly schemaHash: bigint | null;
1102
+ }
1103
+ type ControllerListener = (snapshots: readonly ControllerSlaveSnapshot[]) => void;
1104
+ declare class MasterController {
1105
+ #private;
1106
+ constructor(opts: MasterControllerOptions);
1107
+ /**
1108
+ * Master-side decoder for S→M topic frames. Auto-attached / detached as
1109
+ * each slave transitions in and out of `running`. Subscribe with
1110
+ * `dataPlane.onTopic(nodeId, path, listener)`; read latest values with
1111
+ * `dataPlane.latest(nodeId, path)`.
1112
+ */
1113
+ get dataPlane(): DataPlaneReceiver;
1114
+ /**
1115
+ * Attach to the transport, start MASTER_HEARTBEAT, fire one initial
1116
+ * broadcast DISCOVER to flush stale slave state from a previous master
1117
+ * session. New slaves auto-spawn a NodeAgent on first sight.
1118
+ */
1119
+ start(): void;
1120
+ /** Stop all internals, stop every NodeAgent, detach from the transport. */
1121
+ stop(): Promise<void>;
1122
+ /** Snapshot of all known slaves. Stable identity per call. */
1123
+ snapshots(): ControllerSlaveSnapshot[];
1124
+ /** Subscribe to slave-set / state-change updates. Fires on every change. */
1125
+ onChange(cb: ControllerListener): Unsubscribe;
1126
+ /**
1127
+ * Access the NodeAgent for one slave, if it exists. Returns null until
1128
+ * the slave has been observed. Useful for service / param calls from
1129
+ * code that already knows the nodeId.
1130
+ */
1131
+ agent(nodeId: number): NodeAgent | null;
1132
+ /** Issue an ad-hoc broadcast DISCOVER (between periodic firings). */
1133
+ discoverNow(): Promise<void>;
1134
+ /**
1135
+ * Commit the deferred START on one slave that is parked in
1136
+ * `awaiting-start` because the autoStart gate blocked its bring-up.
1137
+ * Returns `false` if the slave is unknown or not currently waiting
1138
+ * for a manual start; otherwise resolves once START has been queued
1139
+ * on the wire (the RUNNING transition arrives later via `onChange`).
1140
+ */
1141
+ startNode(nodeId: number): Promise<boolean>;
1142
+ /**
1143
+ * Re-run the FRAME_DEFINE / START handshake for one slave so the
1144
+ * {@link FramePlanFactory} is consulted again. Use when external state
1145
+ * the factory closes over has changed (e.g. per-field TX-enable
1146
+ * toggles). Returns `false` when the slave is unknown or has never
1147
+ * ANNOUNCEd; otherwise resolves once the new bring-up has been kicked
1148
+ * off (it does not await the next RUNNING transition — listen via
1149
+ * {@link onChange} instead).
1150
+ */
1151
+ reprovision(nodeId: number): Promise<boolean>;
1152
+ }
1153
+ //#endregion
1154
+ //#region src/master-heartbeat.d.ts
1155
+ /**
1156
+ * Broadcasts CMD_MASTER_HEARTBEAT (§5.11) on a fixed period so slaves with
1157
+ * a `master_lost_us` watchdog configured do not silence their S→M traffic.
1158
+ *
1159
+ * Independent of any single {@link NodeAgent} — the heartbeat is a single
1160
+ * bus-wide broadcast that refreshes every slave's watchdog. Construct one
1161
+ * per transport.
1162
+ */
1163
+ declare class MasterHeartbeatBroadcaster {
1164
+ #private;
1165
+ private readonly transport;
1166
+ private readonly periodMs;
1167
+ constructor(transport: ITransport, periodMs?: number);
1168
+ start(): void;
1169
+ stop(): void;
1170
+ get running(): boolean;
1171
+ }
1172
+ //#endregion
1173
+ //#region src/proto-reader.d.ts
1174
+ declare class ProtoReader {
1175
+ private readonly buf;
1176
+ private readonly end;
1177
+ private pos;
1178
+ constructor(buf: Uint8Array, end?: number);
1179
+ get done(): boolean;
1180
+ get position(): number;
1181
+ /** Read a base-128 varint as a JS number (overflows ≥ 2^53 wrap silently). */
1182
+ readVarint(): number;
1183
+ /** Read a length-delimited byte slice (no copy). */
1184
+ readBytes(): Uint8Array;
1185
+ /** Read a length-delimited UTF-8 string. */
1186
+ readString(): string;
1187
+ /** Read a length-delimited sub-message and return a new reader scoped to it. */
1188
+ readMessage(): ProtoReader;
1189
+ /**
1190
+ * Read the next tag and return both the field number and wire type. The
1191
+ * caller is expected to dispatch on field number; unknown fields can be
1192
+ * passed to {@link skipValue}.
1193
+ */
1194
+ readTag(): {
1195
+ field: number;
1196
+ wire: number;
1197
+ };
1198
+ /** Discard the value associated with `wire`. Used to tolerate unknown fields. */
1199
+ skipValue(wire: number): void;
1200
+ }
1201
+ declare class ProtoError extends Error {
1202
+ constructor(message: string);
1203
+ }
1204
+ //#endregion
1205
+ //#region src/schema-hash.d.ts
1206
+ /**
1207
+ * SHA-256 helpers for the schema blob (§5.4, §9.3).
1208
+ *
1209
+ * Both codegen (C++) and the bridge (C++) hash the entire schema blob with
1210
+ * SHA-256 and pack the first 8 bytes as little-endian u64 into the ANNOUNCE
1211
+ * frame. This module is the master-side equivalent: hash the blob we
1212
+ * received over SCHEMA_DATA and compare with the schema_hash the slave
1213
+ * advertised in ANNOUNCE.
1214
+ *
1215
+ * Uses Web Crypto (`crypto.subtle`) — available in modern browsers and Node
1216
+ * ≥ 19 / Deno / Bun, no polyfill needed for our target environments.
1217
+ */
1218
+ declare function sha256(bytes: Uint8Array): Promise<Uint8Array>;
1219
+ /**
1220
+ * Compute the ANNOUNCE-shape schema hash: the first 8 bytes of SHA-256(blob),
1221
+ * interpreted as a little-endian u64. Mirrors
1222
+ * `fibril_can_core/sha256::hash_prefix_u64`.
1223
+ */
1224
+ declare function schemaHashPrefix(blob: Uint8Array): Promise<bigint>;
1225
+ //#endregion
1226
+ //#region src/segmented-transfer.d.ts
1227
+ /**
1228
+ * Build the §7.4 SVC_REQUEST frame stream for a master→slave call. Mirrors
1229
+ * `make_svc_request_frames` in
1230
+ * `fibril_can_bridge/include/fibril_can_bridge/svc_request.hpp`.
1231
+ *
1232
+ * - ≤64 byte payload → one SINGLE frame (seg=0).
1233
+ * - Larger → seq=0 carries `[0]=0 [1..2]=total_len + ≤61B data`, subsequent
1234
+ * seqs carry `[0]=seq + ≤63B data`. Throws on payloads >1024B.
1235
+ */
1236
+ declare function makeSvcRequestFrames(nodeId: number, svcIdx: number, callId: number, payload: Uint8Array): CanFrameTx[];
1237
+ /**
1238
+ * Build §7.4 response segments. Used by mock slaves and any TS slave runtime.
1239
+ * `channel` is the channel field (typically `CHAN_SVC_RESPONSE`); the payload
1240
+ * is `[status, ...response]` so the caller bakes status into the buffer.
1241
+ */
1242
+ declare function makeSvcSegmentedFrames(channel: number, nodeId: number, svcIdx: number, callId: number, payload: Uint8Array): CanFrameTx[];
1243
+ /**
1244
+ * Bitmask of segment seqs required to reassemble a `totalLen`-byte payload.
1245
+ * Identical formula to `expected_mask` in
1246
+ * `fibril_can_bridge/src/node_agent.cpp` and the slave's `seg_expected_mask`.
1247
+ */
1248
+ declare function segExpectedMask(totalLen: number): number;
1249
+ /**
1250
+ * Per-(svc, call_id) reassembly slot. The caller drives one of these per
1251
+ * outstanding incoming segmented transfer.
1252
+ */
1253
+ declare class SegmentReassembler {
1254
+ #private;
1255
+ totalLen: number;
1256
+ rxBitmap: number;
1257
+ lastRxMs: number;
1258
+ /**
1259
+ * Ingest one seg=1 frame. `data` is the frame payload (with the seq /
1260
+ * total_len header still attached). Returns the fully-assembled payload
1261
+ * once every required segment has arrived; otherwise null. Returns null
1262
+ * on protocol violations and resets the slot.
1263
+ */
1264
+ ingest(data: Uint8Array, nowMs: number): Uint8Array | null;
1265
+ }
1266
+ //#endregion
1267
+ //#region src/seq-allocator.d.ts
1268
+ /**
1269
+ * Sequential u8 allocator used for the config-plane `seq` field shared by
1270
+ * every command this master has issued to a given slave. Wraps at 256,
1271
+ * matching `BridgeNode::alloc_seq` in
1272
+ * `fibril_can_bridge/src/bridge_node.cpp` (the bridge wraps on the same
1273
+ * boundary; the AckTracker indexes by the wrapped value).
1274
+ */
1275
+ declare class SeqAllocator {
1276
+ #private;
1277
+ next(): number;
1278
+ reset(): void;
1279
+ }
1280
+ //#endregion
1281
+ export { type AckCallback, type AckOutcome, AckTracker, type AnnounceMsg, CHAN_CONFIG, CHAN_SVC_REQUEST, CHAN_SVC_RESPONSE, CMD_ACK, CMD_ANNOUNCE, CMD_DISCOVER, CMD_FRAME_DEFINE, CMD_HB_PERIOD, CMD_HEARTBEAT, CMD_MASTER_HEARTBEAT, CMD_NAK, CMD_SCHEMA_DATA, CMD_SCHEMA_READ, CMD_START, CMD_STOP, type ControllerListener, type ControllerSlaveSnapshot, DIR_RX_M2S, DIR_TX_S2M, DataPlaneReceiver, type DataPlaneReceiverOptions, type Dict, Direction, DynamicCodecError, type EndpointDescriptor, type EndpointKey, FAULT_ID_CONFLICT, FAULT_INDEX_OVERFLOW, FAULT_NOMEM, FAULT_NONE, FD_DLC_STEPS, type FieldRequest, type FieldValue, type FrameDefineEntry, type FramePlan, type FramePlanContext, type FramePlanFactory, type FrameSpec, type HeartbeatMsg, MAX_FRAME_PAYLOAD, MasterController, type MasterControllerOptions, MasterHeartbeatBroadcaster, NAK_BAD_LENGTH, NAK_BAD_PARAM, NAK_BAD_STATE, NAK_DIR_MISMATCH, NAK_ID_CONFLICT, NAK_INDEX_RANGE, NAK_NO_RESOURCE, NAK_SIZE_OVERFLOW, NAK_UNKNOWN_CMD, NODE_ID_BROADCAST, type NakMsg, NoCallSlotError, NodeAgent, type NodeAgentOptions, type NodeProvisionState, type NodeRunningSnapshot, type NodeSchema, NumberingError, type NumberingResult, type NumberingTable, PROTOCOL_VERSION, ParamCodecError, type ParamDescriptor, ProtoError, ProtoReader, type RosMapping, type S2MFieldFilter, SCALAR_NAMES, SEG_FIRST_DATA, SEG_MAX_SEQ, SEG_MAX_TOTAL, SEG_REST_DATA, SERVICE_INDEX_CANCEL, SERVICE_INDEX_CAPACITY, SERVICE_INDEX_MAX, SERVICE_INDEX_PARAM_SET, STATE_FAULT, STATE_PROVISIONED, STATE_RUNNING, STATE_UNPROVISIONED, STD_ID_MASK, SVC_ACCEPTED, SVC_APP_ERROR, SVC_BAD_INDEX, SVC_BAD_LENGTH, SVC_BAD_VALUE, SVC_BUSY, SVC_CANCEL, SVC_MAX_FIRST_CLASS, SVC_OK, SVC_PARAM_SET, SVC_UNAVAIL, ScalarType, SchemaAccumulator, type SchemaBlockArray, type SchemaBlockType, type SchemaDataMsg, type SchemaField, SchemaIndex, type SchemaParam, type SchemaService, type SchemaTopic, SegmentReassembler, SeqAllocator, ServiceCallError, type ServiceCallOptions, ServiceCancelledError, ServiceClient, type ServiceDescriptor, type ServiceResult, ServiceTimeoutError, ServiceUnavailableError, StdIdAllocator, type TopicDescriptor, type TopicListener, type TopicSample, type TopicStats, TypedParam, type TypedParamSetOptions, type TypedParamSetResult, TypedService, type TypedServiceCallOptions, type TypedServiceResult, type WireLimits, buildDefaultS2MRequests, computeNumbering, decodeAnnounce, decodeFields, decodeHeartbeat, decodeNak, decodeSchema, decodeSchemaData, defaultS2MFramePlan, encodeDiscover, encodeDiscoverTo, encodeFields, encodeFrameDefine, encodeMasterHeartbeat, encodeParamSetSingle, encodeSchemaRead, encodeStart, encodeStop, extChannel, extIndex, extNodeId, extSeg, extSeq, fieldWireSize, fieldsWireSize, makeExtId, makeSvcRequestFrames, makeSvcSegmentedFrames, packScalarF32Le, packScalarF64Le, packScalarIntLe, padToDlc, planFrames, quantizeDlc, scalarFromName, scalarName, scalarSize, schemaHashPrefix, segExpectedMask, segOffset, sha256, signExtend, unpackScalarF32Le, unpackScalarF64Le, unpackScalarIntLe };
1282
+ //# sourceMappingURL=index.d.ts.map