@fibril/can-core 0.1.2 → 0.3.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 +125 -460
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +89 -282
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -160,11 +160,7 @@ function extSeg(id) {
|
|
|
160
160
|
}
|
|
161
161
|
//#endregion
|
|
162
162
|
//#region src/dlc.ts
|
|
163
|
-
/**
|
|
164
|
-
* CAN FD valid payload sizes in ascending order (§4.3).
|
|
165
|
-
* Mirrors `fibril_can_core/include/fibril_can_core/packing.hpp::kCanFdDlcSteps`
|
|
166
|
-
* and `fibril_can_runtime/src/fcan_wire.c::fcan_quantize_dlc`.
|
|
167
|
-
*/
|
|
163
|
+
/** Valid CAN FD payload sizes, ascending. */
|
|
168
164
|
const FD_DLC_STEPS = Object.freeze([
|
|
169
165
|
0,
|
|
170
166
|
1,
|
|
@@ -183,21 +179,15 @@ const FD_DLC_STEPS = Object.freeze([
|
|
|
183
179
|
48,
|
|
184
180
|
64
|
|
185
181
|
]);
|
|
186
|
-
/**
|
|
187
|
-
* Round `bytes` up to the next valid CAN FD payload step. Returns 64 for any
|
|
188
|
-
* input ≥ 64.
|
|
189
|
-
*/
|
|
182
|
+
/** Round `bytes` up to the next valid CAN FD payload step (caps at 64). */
|
|
190
183
|
function quantizeDlc(bytes) {
|
|
191
184
|
if (bytes >= 64) return 64;
|
|
192
185
|
for (const step of FD_DLC_STEPS) if (step >= bytes) return step;
|
|
193
186
|
return 64;
|
|
194
187
|
}
|
|
195
188
|
/**
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* a self-inconsistent frame the slave would misparse), the original is
|
|
199
|
-
* returned unchanged so the backend rejects it loudly (parity with
|
|
200
|
-
* `pad_to_dlc` in `fibril_can_bridge/src/config_plane_codec.cpp`).
|
|
189
|
+
* Pad `data` to the next DLC step with zeros. Over-length input (>64 B, which
|
|
190
|
+
* the slave would misparse) is returned unchanged so the backend rejects it.
|
|
201
191
|
*/
|
|
202
192
|
function padToDlc(data) {
|
|
203
193
|
if (data.length >= 64) return data;
|
|
@@ -300,13 +290,9 @@ function encodeStop(nodeId, seq) {
|
|
|
300
290
|
return configFrame(nodeId, 33, seq, /* @__PURE__ */ new Uint8Array(0));
|
|
301
291
|
}
|
|
302
292
|
/**
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
* length to drop those padding bytes (parity with
|
|
307
|
-
* `append_schema_chunk` in `fibril_can_bridge/src/config_plane_codec.cpp`).
|
|
308
|
-
*
|
|
309
|
-
* Out-of-order chunks are rejected (returns 0 from {@link append}).
|
|
293
|
+
* SCHEMA_DATA reassembly. Clips each chunk against `expectedChunkLen` and
|
|
294
|
+
* the remaining blob length so DLC padding is discarded. Out-of-order
|
|
295
|
+
* chunks are rejected ({@link append} returns 0).
|
|
310
296
|
*/
|
|
311
297
|
var SchemaAccumulator = class {
|
|
312
298
|
#buf = [];
|
|
@@ -342,12 +328,8 @@ var SchemaAccumulator = class {
|
|
|
342
328
|
//#endregion
|
|
343
329
|
//#region src/scalar-codec.ts
|
|
344
330
|
/**
|
|
345
|
-
* Scalar type
|
|
346
|
-
*
|
|
347
|
-
* `fibril_can_core/src/scalar_type.cpp`.
|
|
348
|
-
*
|
|
349
|
-
* Type ordinal values are part of the protobuf schema blob's enum, so they
|
|
350
|
-
* must not be renumbered.
|
|
331
|
+
* Scalar type enum and little-endian wire codecs. The ordinal values are
|
|
332
|
+
* part of the protobuf schema blob and must not be renumbered.
|
|
351
333
|
*/
|
|
352
334
|
let ScalarType = /* @__PURE__ */ function(ScalarType) {
|
|
353
335
|
ScalarType[ScalarType["Bool"] = 0] = "Bool";
|
|
@@ -400,14 +382,9 @@ function scalarFromName(name) {
|
|
|
400
382
|
return idx >= 0 ? idx : void 0;
|
|
401
383
|
}
|
|
402
384
|
/**
|
|
403
|
-
* Pack an
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
* Pass `bigint` for 64-bit types (U64/I64). `number` is accepted for
|
|
407
|
-
* everything (it is internally widened to bigint so signed callers get the
|
|
408
|
-
* same sign-extension semantics as the C++ `pack_scalar_int_le(uint64_t)`).
|
|
409
|
-
* Signed callers pass the negative number directly; the function performs the
|
|
410
|
-
* 64-bit two's-complement cast.
|
|
385
|
+
* Pack an int/bool as little-endian into `out[offset..offset+scalarSize(type))`.
|
|
386
|
+
* `number` is widened to bigint so signed negatives sign-extend to 64-bit
|
|
387
|
+
* two's complement before truncation, matching C++ `pack_scalar_int_le`.
|
|
411
388
|
*/
|
|
412
389
|
function packScalarIntLe(type, value, out, offset = 0) {
|
|
413
390
|
const bytes = scalarSize(type);
|
|
@@ -463,7 +440,7 @@ var ProtoReader = class ProtoReader {
|
|
|
463
440
|
get position() {
|
|
464
441
|
return this.pos;
|
|
465
442
|
}
|
|
466
|
-
/** Read a base-128 varint as a JS number (
|
|
443
|
+
/** Read a base-128 varint as a JS number (schema uses uint32 only). */
|
|
467
444
|
readVarint() {
|
|
468
445
|
let result = 0;
|
|
469
446
|
let shift = 0;
|
|
@@ -501,11 +478,7 @@ var ProtoReader = class ProtoReader {
|
|
|
501
478
|
readMessage() {
|
|
502
479
|
return new ProtoReader(this.readBytes());
|
|
503
480
|
}
|
|
504
|
-
/**
|
|
505
|
-
* Read the next tag and return both the field number and wire type. The
|
|
506
|
-
* caller is expected to dispatch on field number; unknown fields can be
|
|
507
|
-
* passed to {@link skipValue}.
|
|
508
|
-
*/
|
|
481
|
+
/** Read a tag as `{field, wire}`. Unknown fields dispatch to {@link skipValue}. */
|
|
509
482
|
readTag() {
|
|
510
483
|
const tag = this.readVarint();
|
|
511
484
|
return {
|
|
@@ -543,23 +516,13 @@ var ProtoError = class extends Error {
|
|
|
543
516
|
//#endregion
|
|
544
517
|
//#region src/schema.ts
|
|
545
518
|
/**
|
|
546
|
-
* NodeSchema IR + protobuf decoder for the schema blob
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
* `
|
|
551
|
-
*
|
|
552
|
-
* Master-side notes:
|
|
553
|
-
* - The proto carries `topic_id` / `service_id` / `param_id` / BlockType
|
|
554
|
-
* `type_id` for codegen stability; the master derives equivalent indices
|
|
555
|
-
* from {@link computeNumbering} instead, so these are accepted but
|
|
556
|
-
* discarded on parse.
|
|
557
|
-
* - The proto wire field is `tx_disabled` while the IR holds `tx_enabled`
|
|
558
|
-
* (inverted, matches the C++ side).
|
|
519
|
+
* NodeSchema IR + protobuf decoder for the schema blob. Master-side notes:
|
|
520
|
+
* - Proto `topic_id` / `service_id` / `param_id` / BlockType `type_id`
|
|
521
|
+
* are accepted but discarded — the master derives indices from
|
|
522
|
+
* {@link computeNumbering}.
|
|
523
|
+
* - Proto wire `tx_disabled` inverts to IR `tx_enabled`.
|
|
559
524
|
* - `BlockArray.type_id` (proto) → `BlockArray.blockTypeIndex` (IR).
|
|
560
|
-
*
|
|
561
|
-
* The reader skips unknown fields silently so future schema additions do
|
|
562
|
-
* not break old masters.
|
|
525
|
+
* Unknown fields are skipped so future schema additions don't break masters.
|
|
563
526
|
*/
|
|
564
527
|
const FIELD = {
|
|
565
528
|
Field: {
|
|
@@ -896,20 +859,8 @@ function fieldWireSize(field) {
|
|
|
896
859
|
//#endregion
|
|
897
860
|
//#region src/dynamic-codec.ts
|
|
898
861
|
/**
|
|
899
|
-
* Field-driven dict ⇄ Uint8Array codec.
|
|
900
|
-
*
|
|
901
|
-
* function of the field list ({@link SchemaField}[]) and the JS dict.
|
|
902
|
-
*
|
|
903
|
-
* Conventions (matches CLAUDE.md "値の表現規約"):
|
|
904
|
-
* - dict keys are the schema field name verbatim
|
|
905
|
-
* - scalar ↔ JS `number`, except U64/I64 ↔ `bigint`
|
|
906
|
-
* - bool ↔ JS `boolean`
|
|
907
|
-
* - fixed-length array (`arrayLen > 0`) ↔ regular JS array
|
|
908
|
-
* (`number[]` / `bigint[]` / `boolean[]`) of exactly `arrayLen` elements
|
|
909
|
-
*
|
|
910
|
-
* Throws on missing / wrong-shape inputs. Decode tolerates extra trailing
|
|
911
|
-
* bytes (mirrors C++ side: the master ignores trailing slop so that future
|
|
912
|
-
* schema additions don't break older decoders).
|
|
862
|
+
* Field-driven dict ⇄ Uint8Array codec. Throws on shape mismatch; decode
|
|
863
|
+
* tolerates trailing bytes so future schema additions don't break decoders.
|
|
913
864
|
*/
|
|
914
865
|
var DynamicCodecError = class extends Error {
|
|
915
866
|
constructor(message) {
|
|
@@ -969,11 +920,7 @@ function fieldsWireSize(fields) {
|
|
|
969
920
|
for (const f of fields) n += fieldWireSize(f);
|
|
970
921
|
return n;
|
|
971
922
|
}
|
|
972
|
-
/**
|
|
973
|
-
* Encode a dict into the canonical little-endian wire bytes for `fields`.
|
|
974
|
-
* Returns a fresh `Uint8Array`. Throws {@link DynamicCodecError} on a
|
|
975
|
-
* missing or wrong-shape field.
|
|
976
|
-
*/
|
|
923
|
+
/** Encode a dict into little-endian wire bytes. Throws on missing / wrong-shape fields. */
|
|
977
924
|
function encodeFields(fields, value) {
|
|
978
925
|
const totalSize = fieldsWireSize(fields);
|
|
979
926
|
const out = new Uint8Array(totalSize);
|
|
@@ -1000,11 +947,7 @@ function encodeFields(fields, value) {
|
|
|
1000
947
|
}
|
|
1001
948
|
return out;
|
|
1002
949
|
}
|
|
1003
|
-
/**
|
|
1004
|
-
* Decode wire bytes into a dict keyed by field name. Extra trailing bytes
|
|
1005
|
-
* are ignored. Throws {@link DynamicCodecError} only if the payload is
|
|
1006
|
-
* shorter than the field list demands.
|
|
1007
|
-
*/
|
|
950
|
+
/** Decode wire bytes into a dict. Trailing bytes are ignored; throws only if too short. */
|
|
1008
951
|
function decodeFields(fields, bytes) {
|
|
1009
952
|
const expected = fieldsWireSize(fields);
|
|
1010
953
|
if (bytes.length < expected) throw new DynamicCodecError(`decodeFields: payload too short — need ${expected} bytes, got ${bytes.length}`);
|
|
@@ -1097,10 +1040,7 @@ var DataPlaneReceiver = class {
|
|
|
1097
1040
|
this.#now = opts.now ?? (() => Date.now());
|
|
1098
1041
|
this.#unsubFrame = opts.transport.onFrame((f) => this.#onFrame(f));
|
|
1099
1042
|
}
|
|
1100
|
-
/**
|
|
1101
|
-
* Claim a slave's S→M std_ids. Re-attaching the same nodeId atomically
|
|
1102
|
-
* detaches the prior plan first (re-provisioning case).
|
|
1103
|
-
*/
|
|
1043
|
+
/** Claim a slave's S→M std_ids. Re-attach atomically detaches the prior plan. */
|
|
1104
1044
|
attachSlave(nodeId, framePlan, index) {
|
|
1105
1045
|
this.detachSlave(nodeId);
|
|
1106
1046
|
const owned = /* @__PURE__ */ new Set();
|
|
@@ -1235,9 +1175,8 @@ var DataPlaneReceiver = class {
|
|
|
1235
1175
|
//#endregion
|
|
1236
1176
|
//#region src/frame-planner.ts
|
|
1237
1177
|
/**
|
|
1238
|
-
* 11-bit
|
|
1239
|
-
*
|
|
1240
|
-
* than wrapping — wrapping past 0x7FF would alias on the wire.
|
|
1178
|
+
* 11-bit std_id allocator with a delayed free pool. `next()` returns `null`
|
|
1179
|
+
* on exhaustion — wrapping past 0x7FF would alias on the wire.
|
|
1241
1180
|
*/
|
|
1242
1181
|
var StdIdAllocator = class {
|
|
1243
1182
|
#next = 256;
|
|
@@ -1329,9 +1268,8 @@ function emitOversizedTopic(fields, dir, periodUs, alloc, out) {
|
|
|
1329
1268
|
return flush();
|
|
1330
1269
|
}
|
|
1331
1270
|
/**
|
|
1332
|
-
* Plan a slave's frame layout. Returns `null`
|
|
1333
|
-
*
|
|
1334
|
-
* plan would silently drop fields.
|
|
1271
|
+
* Plan a slave's frame layout. Returns `null` on std_id exhaustion — a
|
|
1272
|
+
* partial plan would silently drop fields.
|
|
1335
1273
|
*/
|
|
1336
1274
|
function planFrames(requests, alloc) {
|
|
1337
1275
|
const buckets = /* @__PURE__ */ new Map();
|
|
@@ -1419,19 +1357,10 @@ function joinPath$1(ns, name) {
|
|
|
1419
1357
|
return `${ns}/${name}`;
|
|
1420
1358
|
}
|
|
1421
1359
|
/**
|
|
1422
|
-
*
|
|
1423
|
-
*
|
|
1424
|
-
*
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1427
|
-
* Pass `filter` to layer a runtime per-field mask on top (UI toggles,
|
|
1428
|
-
* per-slave overrides). Schema-disabled fields are unconditionally
|
|
1429
|
-
* dropped before `filter` is consulted, so callers don't have to repeat
|
|
1430
|
-
* the `txEnabled` check.
|
|
1431
|
-
*
|
|
1432
|
-
* This is the natural input to {@link planFrames} when the master just
|
|
1433
|
-
* wants to "observe everything the slave publishes" — i.e. the
|
|
1434
|
-
* sniffer-style auto-provision path.
|
|
1360
|
+
* Every schema-enabled S→M field at its declared period
|
|
1361
|
+
* (`field.periodUsOverride` ?? topic's `defaultPeriodUs`), optionally masked
|
|
1362
|
+
* further by `filter`. Feeds {@link planFrames} for the observe-everything
|
|
1363
|
+
* auto-provision path.
|
|
1435
1364
|
*/
|
|
1436
1365
|
function buildDefaultS2MRequests(schema, numbering, filter) {
|
|
1437
1366
|
const out = [];
|
|
@@ -1468,17 +1397,10 @@ function buildDefaultS2MRequests(schema, numbering, filter) {
|
|
|
1468
1397
|
return out;
|
|
1469
1398
|
}
|
|
1470
1399
|
/**
|
|
1471
|
-
*
|
|
1472
|
-
*
|
|
1473
|
-
*
|
|
1474
|
-
*
|
|
1475
|
-
* field-tx-enable map). With no filter, every schema-enabled S→M field is
|
|
1476
|
-
* subscribed.
|
|
1477
|
-
*
|
|
1478
|
-
* Returns `[]` when planning fails (e.g. std_id exhaustion); the caller's
|
|
1479
|
-
* NodeAgent will then leave the slave in `provisioning` → `awaiting-announce`
|
|
1480
|
-
* with no frames defined, which is the right outcome — partial plans would
|
|
1481
|
-
* silently drop fields.
|
|
1400
|
+
* FramePlanFactory subscribing to every schema-enabled S→M field (optionally
|
|
1401
|
+
* masked by `filter`) with a fresh {@link StdIdAllocator}. Returns `[]` on
|
|
1402
|
+
* planning failure so NodeAgent stalls in `awaiting-announce` — a partial
|
|
1403
|
+
* plan would silently drop fields.
|
|
1482
1404
|
*/
|
|
1483
1405
|
function defaultS2MFramePlan(schema, numbering, filter) {
|
|
1484
1406
|
const planned = planFrames(buildDefaultS2MRequests(schema, numbering, filter), new StdIdAllocator());
|
|
@@ -1529,19 +1451,13 @@ var MasterHeartbeatBroadcaster = class {
|
|
|
1529
1451
|
//#endregion
|
|
1530
1452
|
//#region src/numbering.ts
|
|
1531
1453
|
/**
|
|
1532
|
-
* Flat-numbering tables for topics, services, and parameters
|
|
1533
|
-
*
|
|
1534
|
-
*
|
|
1535
|
-
* of (schema, instance-count-vector). The same tables are derived
|
|
1536
|
-
* independently by codegen (with max_count for static checks), the slave
|
|
1537
|
-
* runtime (with actual counts at boot) and the master (with counts from the
|
|
1538
|
-
* ANNOUNCE frame) — keeping this in one place is what guarantees they agree.
|
|
1454
|
+
* Flat-numbering tables for topics, services, and parameters — a pure
|
|
1455
|
+
* function of (schema, instance-count-vector). Codegen, slave runtime, and
|
|
1456
|
+
* master all derive these tables through the same function.
|
|
1539
1457
|
*/
|
|
1540
1458
|
const SERVICE_INDEX_CANCEL = 255;
|
|
1541
1459
|
const SERVICE_INDEX_PARAM_SET = 253;
|
|
1542
|
-
/** Maximum first-class service_index produced by numbering (inclusive). */
|
|
1543
1460
|
const SERVICE_INDEX_MAX = 252;
|
|
1544
|
-
/** Number of available first-class service slots. */
|
|
1545
1461
|
const SERVICE_INDEX_CAPACITY = 253;
|
|
1546
1462
|
let NumberingError = /* @__PURE__ */ function(NumberingError) {
|
|
1547
1463
|
NumberingError["Ok"] = "ok";
|
|
@@ -1581,12 +1497,8 @@ function buildTable(schema, counts, pick) {
|
|
|
1581
1497
|
};
|
|
1582
1498
|
}
|
|
1583
1499
|
/**
|
|
1584
|
-
*
|
|
1585
|
-
*
|
|
1586
|
-
* malformed or service_index would overflow into the reserved range
|
|
1587
|
-
* (§4.2 §9.4).
|
|
1588
|
-
*
|
|
1589
|
-
* On error the returned tables are empty but still safe to inspect.
|
|
1500
|
+
* `actualCounts.length` must equal `schema.instances.length`. On any error
|
|
1501
|
+
* the returned tables are empty and `status !== Ok`.
|
|
1590
1502
|
*/
|
|
1591
1503
|
function computeNumbering(schema, actualCounts) {
|
|
1592
1504
|
const empty = {
|
|
@@ -1631,29 +1543,13 @@ function computeNumbering(schema, actualCounts) {
|
|
|
1631
1543
|
}
|
|
1632
1544
|
//#endregion
|
|
1633
1545
|
//#region src/schema-hash.ts
|
|
1634
|
-
/**
|
|
1635
|
-
* SHA-256 helpers for the schema blob (§5.4, §9.3).
|
|
1636
|
-
*
|
|
1637
|
-
* Both codegen (C++) and the bridge (C++) hash the entire schema blob with
|
|
1638
|
-
* SHA-256 and pack the first 8 bytes as little-endian u64 into the ANNOUNCE
|
|
1639
|
-
* frame. This module is the master-side equivalent: hash the blob we
|
|
1640
|
-
* received over SCHEMA_DATA and compare with the schema_hash the slave
|
|
1641
|
-
* advertised in ANNOUNCE.
|
|
1642
|
-
*
|
|
1643
|
-
* Uses Web Crypto (`crypto.subtle`) — available in modern browsers and Node
|
|
1644
|
-
* ≥ 19 / Deno / Bun, no polyfill needed for our target environments.
|
|
1645
|
-
*/
|
|
1646
1546
|
async function sha256(bytes) {
|
|
1647
1547
|
if (typeof crypto === "undefined" || !crypto.subtle) throw new Error("crypto.subtle is not available in this environment");
|
|
1648
1548
|
const owned = new Uint8Array(bytes);
|
|
1649
1549
|
const digest = await crypto.subtle.digest("SHA-256", owned);
|
|
1650
1550
|
return new Uint8Array(digest);
|
|
1651
1551
|
}
|
|
1652
|
-
/**
|
|
1653
|
-
* Compute the ANNOUNCE-shape schema hash: the first 8 bytes of SHA-256(blob),
|
|
1654
|
-
* interpreted as a little-endian u64. Mirrors
|
|
1655
|
-
* `fibril_can_core/sha256::hash_prefix_u64`.
|
|
1656
|
-
*/
|
|
1552
|
+
/** First 8 bytes of SHA-256(blob) as little-endian u64 — the ANNOUNCE shape. */
|
|
1657
1553
|
async function schemaHashPrefix(blob) {
|
|
1658
1554
|
const digest = await sha256(blob);
|
|
1659
1555
|
let result = 0n;
|
|
@@ -1663,18 +1559,9 @@ async function schemaHashPrefix(blob) {
|
|
|
1663
1559
|
//#endregion
|
|
1664
1560
|
//#region src/schema-index.ts
|
|
1665
1561
|
/**
|
|
1666
|
-
*
|
|
1667
|
-
*
|
|
1668
|
-
*
|
|
1669
|
-
*
|
|
1670
|
-
* Paths are local to a single slave (one {@link NodeAgent}). The block-array
|
|
1671
|
-
* `ros_namespace` is expanded by substituting `{i}` with the instance index
|
|
1672
|
-
* (mirrors `expand_i_token` in `fibril_can_bridge::node_agent_internal.hpp`).
|
|
1673
|
-
*
|
|
1674
|
-
* Two endpoints of the same kind sharing a path is a schema error — the
|
|
1675
|
-
* index throws on construction. Same path across different kinds (e.g. a
|
|
1676
|
-
* topic and a service named `motor0/foo`) is allowed because the caller
|
|
1677
|
-
* selects the kind explicitly via `topic()` / `service()` / `param()`.
|
|
1562
|
+
* Path (`<expanded_ns>/<endpoint_name>`) → descriptor lookup for one slave.
|
|
1563
|
+
* Duplicate path within the same kind throws; the same path across kinds
|
|
1564
|
+
* is allowed (caller selects via `topic()` / `service()` / `param()`).
|
|
1678
1565
|
*/
|
|
1679
1566
|
const I_TOKEN = "{i}";
|
|
1680
1567
|
function expandIToken(s, value) {
|
|
@@ -1691,10 +1578,6 @@ function sumFieldsWireSize(fields) {
|
|
|
1691
1578
|
for (const f of fields) n += fieldWireSize(f);
|
|
1692
1579
|
return n;
|
|
1693
1580
|
}
|
|
1694
|
-
/**
|
|
1695
|
-
* Path → descriptor index for one slave. Built once from
|
|
1696
|
-
* (schema, numbering) and reused for the lifetime of a snapshot.
|
|
1697
|
-
*/
|
|
1698
1581
|
var SchemaIndex = class {
|
|
1699
1582
|
#topicsByPath = /* @__PURE__ */ new Map();
|
|
1700
1583
|
#servicesByPath = /* @__PURE__ */ new Map();
|
|
@@ -1812,13 +1695,7 @@ var SchemaIndex = class {
|
|
|
1812
1695
|
};
|
|
1813
1696
|
//#endregion
|
|
1814
1697
|
//#region src/seq-allocator.ts
|
|
1815
|
-
/**
|
|
1816
|
-
* Sequential u8 allocator used for the config-plane `seq` field shared by
|
|
1817
|
-
* every command this master has issued to a given slave. Wraps at 256,
|
|
1818
|
-
* matching `BridgeNode::alloc_seq` in
|
|
1819
|
-
* `fibril_can_bridge/src/bridge_node.cpp` (the bridge wraps on the same
|
|
1820
|
-
* boundary; the AckTracker indexes by the wrapped value).
|
|
1821
|
-
*/
|
|
1698
|
+
/** Wrapping u8 counter for the config-plane `seq` byte. */
|
|
1822
1699
|
var SeqAllocator = class {
|
|
1823
1700
|
#next = 0;
|
|
1824
1701
|
next() {
|
|
@@ -1833,13 +1710,9 @@ var SeqAllocator = class {
|
|
|
1833
1710
|
//#endregion
|
|
1834
1711
|
//#region src/segmented-transfer.ts
|
|
1835
1712
|
/**
|
|
1836
|
-
*
|
|
1837
|
-
* `
|
|
1838
|
-
* `
|
|
1839
|
-
*
|
|
1840
|
-
* - ≤64 byte payload → one SINGLE frame (seg=0).
|
|
1841
|
-
* - Larger → seq=0 carries `[0]=0 [1..2]=total_len + ≤61B data`, subsequent
|
|
1842
|
-
* seqs carry `[0]=seq + ≤63B data`. Throws on payloads >1024B.
|
|
1713
|
+
* SVC_REQUEST frames for a master→slave call. ≤64 B → one SINGLE frame;
|
|
1714
|
+
* larger → seq 0 carries `[0]=0 [1..2]=total + ≤61 B data`, later seqs
|
|
1715
|
+
* carry `[0]=seq + ≤63 B data`. Throws on payloads >1024 B.
|
|
1843
1716
|
*/
|
|
1844
1717
|
function makeSvcRequestFrames(nodeId, svcIdx, callId, payload) {
|
|
1845
1718
|
if (payload.length > 1024) throw new RangeError(`service request payload ${payload.length} exceeds SEG_MAX_TOTAL (${SEG_MAX_TOTAL})`);
|
|
@@ -1880,9 +1753,8 @@ function makeSvcRequestFrames(nodeId, svcIdx, callId, payload) {
|
|
|
1880
1753
|
return out;
|
|
1881
1754
|
}
|
|
1882
1755
|
/**
|
|
1883
|
-
*
|
|
1884
|
-
* `channel` is
|
|
1885
|
-
* is `[status, ...response]` so the caller bakes status into the buffer.
|
|
1756
|
+
* Response segments (for mock slaves / TS slave runtime). `payload` must
|
|
1757
|
+
* already carry `[status, ...body]`; `channel` is typically CHAN_SVC_RESPONSE.
|
|
1886
1758
|
*/
|
|
1887
1759
|
function makeSvcSegmentedFrames(channel, nodeId, svcIdx, callId, payload) {
|
|
1888
1760
|
if (payload.length > 1024) throw new RangeError(`service response payload ${payload.length} exceeds SEG_MAX_TOTAL (${SEG_MAX_TOTAL})`);
|
|
@@ -1922,11 +1794,7 @@ function makeSvcSegmentedFrames(channel, nodeId, svcIdx, callId, payload) {
|
|
|
1922
1794
|
}
|
|
1923
1795
|
return out;
|
|
1924
1796
|
}
|
|
1925
|
-
/**
|
|
1926
|
-
* Bitmask of segment seqs required to reassemble a `totalLen`-byte payload.
|
|
1927
|
-
* Identical formula to `expected_mask` in
|
|
1928
|
-
* `fibril_can_bridge/src/node_agent.cpp` and the slave's `seg_expected_mask`.
|
|
1929
|
-
*/
|
|
1797
|
+
/** Bitmask of seg-seqs required to reassemble a `totalLen`-byte payload. */
|
|
1930
1798
|
function segExpectedMask(totalLen) {
|
|
1931
1799
|
if (totalLen <= 0) return 0;
|
|
1932
1800
|
if (totalLen <= 61) return 1;
|
|
@@ -1935,20 +1803,16 @@ function segExpectedMask(totalLen) {
|
|
|
1935
1803
|
if (nSegs >= 32) return 4294967295;
|
|
1936
1804
|
return (1 << nSegs) - 1;
|
|
1937
1805
|
}
|
|
1938
|
-
/**
|
|
1939
|
-
* Per-(svc, call_id) reassembly slot. The caller drives one of these per
|
|
1940
|
-
* outstanding incoming segmented transfer.
|
|
1941
|
-
*/
|
|
1806
|
+
/** Per-(svc, call_id) reassembly slot for one segmented transfer. */
|
|
1942
1807
|
var SegmentReassembler = class {
|
|
1943
1808
|
totalLen = 0;
|
|
1944
1809
|
rxBitmap = 0;
|
|
1945
1810
|
lastRxMs = 0;
|
|
1946
1811
|
#buffer = /* @__PURE__ */ new Uint8Array(0);
|
|
1947
1812
|
/**
|
|
1948
|
-
* Ingest one seg=1 frame
|
|
1949
|
-
*
|
|
1950
|
-
*
|
|
1951
|
-
* on protocol violations and resets the slot.
|
|
1813
|
+
* Ingest one seg=1 frame (payload with seq / total_len header attached).
|
|
1814
|
+
* Returns the assembled payload once complete, else null. Resets the slot
|
|
1815
|
+
* on protocol violation.
|
|
1952
1816
|
*/
|
|
1953
1817
|
ingest(data, nowMs) {
|
|
1954
1818
|
if (data.length === 0) return this.#reset();
|
|
@@ -2035,13 +1899,8 @@ const REASM_MAX_CONCURRENT = 16;
|
|
|
2035
1899
|
/** §7.4 reassembly TTL. */
|
|
2036
1900
|
const SEG_TIMEOUT_MS = 100;
|
|
2037
1901
|
/**
|
|
2038
|
-
*
|
|
2039
|
-
*
|
|
2040
|
-
* Promise-based and AbortSignal-aware.
|
|
2041
|
-
*
|
|
2042
|
-
* One {@link ServiceClient} attaches to the transport and demuxes incoming
|
|
2043
|
-
* SVC_RESPONSE frames for a single `nodeId`. NodeAgent owns one per slave
|
|
2044
|
-
* and exposes it on the running snapshot.
|
|
1902
|
+
* Promise-based, AbortSignal-aware service-call client for one slave.
|
|
1903
|
+
* NodeAgent owns one per slave and exposes it on the running snapshot.
|
|
2045
1904
|
*/
|
|
2046
1905
|
var ServiceClient = class {
|
|
2047
1906
|
nodeId;
|
|
@@ -2206,24 +2065,8 @@ var ServiceClient = class {
|
|
|
2206
2065
|
//#endregion
|
|
2207
2066
|
//#region src/typed-param.ts
|
|
2208
2067
|
/**
|
|
2209
|
-
*
|
|
2210
|
-
*
|
|
2211
|
-
*
|
|
2212
|
-
* Per SPEC §8.1 PARAM is M→S one-way: the master holds the source of truth
|
|
2213
|
-
* and re-injects defaults at provisioning time, so this wrapper exposes
|
|
2214
|
-
* `.set()` only — there's no `.get()` that queries the slave. The
|
|
2215
|
-
* descriptor's decoded default is available via `defaultValue()` for
|
|
2216
|
-
* reference / hydration of master-side state.
|
|
2217
|
-
*
|
|
2218
|
-
* The wire shape for one PARAM_SET request is:
|
|
2219
|
-
* [0] count = 1
|
|
2220
|
-
* [1..2] u16 param_index (LE)
|
|
2221
|
-
* [3..] value (scalar little-endian or fixed-array contiguous)
|
|
2222
|
-
*
|
|
2223
|
-
* Response is `[0] status [1..2] failed_param_index (when status != OK)`.
|
|
2224
|
-
* `set()` resolves with the status code; non-OK includes `failedIndex` so
|
|
2225
|
-
* the caller can correlate with a multi-param batch (single-param sets
|
|
2226
|
-
* always blame this param).
|
|
2068
|
+
* Typed wrapper around PARAM_SET (service_index 0xFD). PARAM is M→S
|
|
2069
|
+
* one-way, so this exposes `.set()` and `.defaultValue()` only.
|
|
2227
2070
|
*/
|
|
2228
2071
|
var ParamCodecError = class extends Error {
|
|
2229
2072
|
constructor(message) {
|
|
@@ -2306,11 +2149,7 @@ var TypedParam = class {
|
|
|
2306
2149
|
this.descriptor = descriptor;
|
|
2307
2150
|
this.path = descriptor.path;
|
|
2308
2151
|
}
|
|
2309
|
-
/**
|
|
2310
|
-
* Decode the schema-embedded default value (SPEC §9.1 `default`). Useful
|
|
2311
|
-
* to seed the master-side persistence store. Returns `null` if the
|
|
2312
|
-
* descriptor carries no default payload.
|
|
2313
|
-
*/
|
|
2152
|
+
/** Decoded schema default; `null` if the descriptor carries no default. */
|
|
2314
2153
|
defaultValue() {
|
|
2315
2154
|
if (this.descriptor.defaultValue.length === 0) return null;
|
|
2316
2155
|
return unpackValue(this.descriptor, this.descriptor.defaultValue);
|
|
@@ -2368,15 +2207,10 @@ const DEFAULT_OPTS = {
|
|
|
2368
2207
|
heartbeatTimeoutMs: 350
|
|
2369
2208
|
};
|
|
2370
2209
|
/**
|
|
2371
|
-
* Per-slave provisioning state machine.
|
|
2372
|
-
* `
|
|
2373
|
-
*
|
|
2374
|
-
*
|
|
2375
|
-
*
|
|
2376
|
-
* Lifecycle: `idle` → `awaiting-announce` → `schema-reading` (skipped if
|
|
2377
|
-
* blob already cached on this agent instance) → `numbering` →
|
|
2378
|
-
* `provisioning` → `running`. HEARTBEAT loss drops to `lost`; the next
|
|
2379
|
-
* ANNOUNCE restarts the flow.
|
|
2210
|
+
* Per-slave provisioning state machine.
|
|
2211
|
+
* `idle` → `awaiting-announce` → `schema-reading` (skipped on cache hit) →
|
|
2212
|
+
* `numbering` → `provisioning` → `running`. HEARTBEAT loss drops to `lost`;
|
|
2213
|
+
* the next ANNOUNCE restarts the flow.
|
|
2380
2214
|
*/
|
|
2381
2215
|
var NodeAgent = class {
|
|
2382
2216
|
nodeId;
|
|
@@ -2437,9 +2271,8 @@ var NodeAgent = class {
|
|
|
2437
2271
|
return this.#snapshot;
|
|
2438
2272
|
}
|
|
2439
2273
|
/**
|
|
2440
|
-
* Attach
|
|
2441
|
-
*
|
|
2442
|
-
* `stop()` to detach.
|
|
2274
|
+
* Attach and wait for ANNOUNCE. Resolves immediately; progress flows via
|
|
2275
|
+
* {@link state} / {@link snapshot} / `onState`.
|
|
2443
2276
|
*/
|
|
2444
2277
|
async start() {
|
|
2445
2278
|
if (this.#stopped) throw new Error("NodeAgent.start() after stop() is not supported — construct a new agent");
|
|
@@ -2450,17 +2283,11 @@ var NodeAgent = class {
|
|
|
2450
2283
|
this.#setState({ kind: "awaiting-announce" });
|
|
2451
2284
|
}
|
|
2452
2285
|
/**
|
|
2453
|
-
* Re-run
|
|
2454
|
-
*
|
|
2455
|
-
*
|
|
2456
|
-
*
|
|
2457
|
-
*
|
|
2458
|
-
* The agent must have observed an ANNOUNCE already; `reprovision()`
|
|
2459
|
-
* before that throws. RUNNING is required so that the slave is known to
|
|
2460
|
-
* be reachable — calling during `lost` would race the heartbeat
|
|
2461
|
-
* watchdog. Pending service calls are failed and the current snapshot is
|
|
2462
|
-
* torn down before the new bring-up begins, mirroring what happens on a
|
|
2463
|
-
* boot_id change.
|
|
2286
|
+
* Re-run FRAME_DEFINE / START against the live slave with the most recent
|
|
2287
|
+
* ANNOUNCE + cached schema. Invokes {@link FramePlanFactory} again so
|
|
2288
|
+
* per-slave overrides pick up their latest values. Requires a prior
|
|
2289
|
+
* ANNOUNCE. Tears down the current snapshot and fails pending service
|
|
2290
|
+
* calls, mirroring a boot_id change.
|
|
2464
2291
|
*/
|
|
2465
2292
|
async reprovision() {
|
|
2466
2293
|
if (this.#stopped) throw new Error("NodeAgent.reprovision() after stop()");
|
|
@@ -2762,10 +2589,8 @@ var NodeAgent = class {
|
|
|
2762
2589
|
this.#maybeIssueStart(a, schema, blob, numbering, gen, p);
|
|
2763
2590
|
}
|
|
2764
2591
|
/**
|
|
2765
|
-
*
|
|
2766
|
-
*
|
|
2767
|
-
* remembering enough context that `startNow()` can resume without
|
|
2768
|
-
* re-running FRAME_DEFINE.
|
|
2592
|
+
* Fire START if autoStart allows, else park in `awaiting-start` with
|
|
2593
|
+
* enough context that `startNow()` can resume without re-running FRAME_DEFINE.
|
|
2769
2594
|
*/
|
|
2770
2595
|
#maybeIssueStart(a, schema, blob, numbering, gen, progress) {
|
|
2771
2596
|
if (this.#autoStart()) {
|
|
@@ -2783,10 +2608,8 @@ var NodeAgent = class {
|
|
|
2783
2608
|
this.#setState({ kind: "awaiting-start" });
|
|
2784
2609
|
}
|
|
2785
2610
|
/**
|
|
2786
|
-
* Commit the deferred START
|
|
2787
|
-
*
|
|
2788
|
-
* other state throws so callers spot logic bugs instead of racing
|
|
2789
|
-
* with a fresh ANNOUNCE.
|
|
2611
|
+
* Commit the deferred START. Only legal in `awaiting-start`; throws
|
|
2612
|
+
* elsewhere so callers spot logic bugs instead of racing a fresh ANNOUNCE.
|
|
2790
2613
|
*/
|
|
2791
2614
|
async startNow() {
|
|
2792
2615
|
if (this.#stopped) throw new Error("NodeAgent.startNow() after stop()");
|
|
@@ -2910,19 +2733,12 @@ var MasterController = class {
|
|
|
2910
2733
|
this.#onError = opts.onError;
|
|
2911
2734
|
}
|
|
2912
2735
|
/**
|
|
2913
|
-
*
|
|
2914
|
-
*
|
|
2915
|
-
* `dataPlane.onTopic(nodeId, path, listener)`; read latest values with
|
|
2916
|
-
* `dataPlane.latest(nodeId, path)`.
|
|
2736
|
+
* S→M topic decoder. Auto-attaches / detaches on each slave's `running`
|
|
2737
|
+
* transition; subscribe via `dataPlane.onTopic` / read via `dataPlane.latest`.
|
|
2917
2738
|
*/
|
|
2918
2739
|
get dataPlane() {
|
|
2919
2740
|
return this.#dataPlane;
|
|
2920
2741
|
}
|
|
2921
|
-
/**
|
|
2922
|
-
* Attach to the transport, start MASTER_HEARTBEAT, fire one initial
|
|
2923
|
-
* broadcast DISCOVER to flush stale slave state from a previous master
|
|
2924
|
-
* session. New slaves auto-spawn a NodeAgent on first sight.
|
|
2925
|
-
*/
|
|
2926
2742
|
start() {
|
|
2927
2743
|
if (this.#started) return;
|
|
2928
2744
|
this.#started = true;
|
|
@@ -2963,7 +2779,7 @@ var MasterController = class {
|
|
|
2963
2779
|
schemaHash: r.schemaHash
|
|
2964
2780
|
})).sort((a, b) => a.nodeId - b.nodeId);
|
|
2965
2781
|
}
|
|
2966
|
-
/** Subscribe to slave-set / state-change updates. Fires on every change. */
|
|
2782
|
+
/** Subscribe to slave-set / state-change updates. Fires immediately with the current snapshot, then on every change. */
|
|
2967
2783
|
onChange(cb) {
|
|
2968
2784
|
this.#listeners.add(cb);
|
|
2969
2785
|
cb(this.snapshots());
|
|
@@ -2971,24 +2787,18 @@ var MasterController = class {
|
|
|
2971
2787
|
this.#listeners.delete(cb);
|
|
2972
2788
|
};
|
|
2973
2789
|
}
|
|
2974
|
-
/**
|
|
2975
|
-
* Access the NodeAgent for one slave, if it exists. Returns null until
|
|
2976
|
-
* the slave has been observed. Useful for service / param calls from
|
|
2977
|
-
* code that already knows the nodeId.
|
|
2978
|
-
*/
|
|
2790
|
+
/** NodeAgent for one slave, or null if not yet observed. */
|
|
2979
2791
|
agent(nodeId) {
|
|
2980
2792
|
return this.#slaves.get(nodeId)?.agent ?? null;
|
|
2981
2793
|
}
|
|
2982
|
-
/**
|
|
2794
|
+
/** Ad-hoc broadcast DISCOVER. */
|
|
2983
2795
|
async discoverNow() {
|
|
2984
2796
|
await this.#transport.send(encodeDiscover());
|
|
2985
2797
|
}
|
|
2986
2798
|
/**
|
|
2987
|
-
* Commit the deferred START on
|
|
2988
|
-
* `
|
|
2989
|
-
*
|
|
2990
|
-
* for a manual start; otherwise resolves once START has been queued
|
|
2991
|
-
* on the wire (the RUNNING transition arrives later via `onChange`).
|
|
2799
|
+
* Commit the deferred START on a slave parked in `awaiting-start`.
|
|
2800
|
+
* Returns `false` if the slave is unknown or not waiting; the RUNNING
|
|
2801
|
+
* transition arrives later via `onChange`.
|
|
2992
2802
|
*/
|
|
2993
2803
|
async startNode(nodeId) {
|
|
2994
2804
|
const rec = this.#slaves.get(nodeId);
|
|
@@ -3003,13 +2813,10 @@ var MasterController = class {
|
|
|
3003
2813
|
}
|
|
3004
2814
|
}
|
|
3005
2815
|
/**
|
|
3006
|
-
* Re-run
|
|
3007
|
-
*
|
|
3008
|
-
*
|
|
3009
|
-
*
|
|
3010
|
-
* ANNOUNCEd; otherwise resolves once the new bring-up has been kicked
|
|
3011
|
-
* off (it does not await the next RUNNING transition — listen via
|
|
3012
|
-
* {@link onChange} instead).
|
|
2816
|
+
* Re-run FRAME_DEFINE / START so the {@link FramePlanFactory} is consulted
|
|
2817
|
+
* again (e.g. after a UI-driven field-tx-enable change). Returns `false`
|
|
2818
|
+
* if unknown / never ANNOUNCEd; resolves once bring-up is kicked off, not
|
|
2819
|
+
* when RUNNING is reached — listen via {@link onChange}.
|
|
3013
2820
|
*/
|
|
3014
2821
|
async reprovision(nodeId) {
|
|
3015
2822
|
const rec = this.#slaves.get(nodeId);
|