@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.
- package/dist/index.d.ts +1282 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3077 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
package/dist/index.d.ts
ADDED
|
@@ -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
|