@matter/nodejs-shell 0.17.9 → 0.18.0-alpha.0-20260812-4d7f2790e

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.
Files changed (52) hide show
  1. package/README.md +11 -1
  2. package/dist/esm/MatterNode.js +237 -134
  3. package/dist/esm/MatterNode.js.map +1 -1
  4. package/dist/esm/app.js +13 -7
  5. package/dist/esm/app.js.map +1 -1
  6. package/dist/esm/shell/cmd_cluster-attributes.js +64 -45
  7. package/dist/esm/shell/cmd_cluster-attributes.js.map +2 -2
  8. package/dist/esm/shell/cmd_cluster-commands.js +11 -9
  9. package/dist/esm/shell/cmd_cluster-commands.js.map +1 -1
  10. package/dist/esm/shell/cmd_cluster-events.js +36 -9
  11. package/dist/esm/shell/cmd_cluster-events.js.map +1 -1
  12. package/dist/esm/shell/cmd_commission.js +119 -93
  13. package/dist/esm/shell/cmd_commission.js.map +1 -1
  14. package/dist/esm/shell/cmd_discover.js +23 -21
  15. package/dist/esm/shell/cmd_discover.js.map +1 -1
  16. package/dist/esm/shell/cmd_icd.js +46 -30
  17. package/dist/esm/shell/cmd_icd.js.map +1 -1
  18. package/dist/esm/shell/cmd_identify.js +11 -7
  19. package/dist/esm/shell/cmd_identify.js.map +1 -1
  20. package/dist/esm/shell/cmd_nodes.js +189 -168
  21. package/dist/esm/shell/cmd_nodes.js.map +1 -1
  22. package/dist/esm/shell/cmd_session.js +3 -4
  23. package/dist/esm/shell/cmd_session.js.map +1 -1
  24. package/dist/esm/shell/cmd_subscribe.js +26 -12
  25. package/dist/esm/shell/cmd_subscribe.js.map +1 -1
  26. package/dist/esm/util/ClusterEndpoint.js +61 -0
  27. package/dist/esm/util/ClusterEndpoint.js.map +6 -0
  28. package/dist/esm/util/awaitSeeded.js +31 -0
  29. package/dist/esm/util/awaitSeeded.js.map +6 -0
  30. package/dist/esm/util/diagnosticLogging.js +99 -0
  31. package/dist/esm/util/diagnosticLogging.js.map +6 -0
  32. package/dist/esm/util/legacyStorageMigration.js +188 -0
  33. package/dist/esm/util/legacyStorageMigration.js.map +6 -0
  34. package/package.json +10 -11
  35. package/src/MatterNode.ts +298 -159
  36. package/src/app.ts +17 -9
  37. package/src/shell/cmd_cluster-attributes.ts +77 -53
  38. package/src/shell/cmd_cluster-commands.ts +12 -13
  39. package/src/shell/cmd_cluster-events.ts +38 -11
  40. package/src/shell/cmd_commission.ts +136 -113
  41. package/src/shell/cmd_discover.ts +36 -22
  42. package/src/shell/cmd_icd.ts +52 -32
  43. package/src/shell/cmd_identify.ts +11 -7
  44. package/src/shell/cmd_nodes.ts +226 -199
  45. package/src/shell/cmd_session.ts +3 -5
  46. package/src/shell/cmd_subscribe.ts +37 -14
  47. package/src/shell/webassets/index.html +1 -1
  48. package/src/tsconfig.json +0 -3
  49. package/src/util/ClusterEndpoint.ts +107 -0
  50. package/src/util/awaitSeeded.ts +43 -0
  51. package/src/util/diagnosticLogging.ts +117 -0
  52. package/src/util/legacyStorageMigration.ts +275 -0
@@ -4,20 +4,46 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { capitalize, ChannelType, decamelize, Diagnostic, ServerAddress } from "@matter/general";
8
- import { ClientNode, CommissioningClient, NetworkClient, SoftwareUpdateManager } from "@matter/node";
9
- import { PeerAddress, PeerSet } from "@matter/protocol";
7
+ import {
8
+ capitalize,
9
+ ChannelType,
10
+ decamelize,
11
+ Diagnostic,
12
+ ImplementationError,
13
+ InternalError,
14
+ Logger,
15
+ Millis,
16
+ ServerAddress,
17
+ } from "@matter/general";
18
+ import {
19
+ ClientNode,
20
+ CommissioningClient,
21
+ NetworkClient,
22
+ NodeConnectionState,
23
+ SoftwareUpdateManager,
24
+ } from "@matter/node";
25
+ import { BasicInformationClient } from "@matter/node/behaviors/basic-information";
26
+ import { FabricAuthority, PeerAddress, PeerSet } from "@matter/protocol";
10
27
  import { FabricIndex, NodeId, VendorId } from "@matter/types";
11
- import { NodeStateInformation } from "@project-chip/matter.js/device";
12
28
  import type { Argv } from "yargs";
13
- import { createDiagnosticCallbacks, MatterNode } from "../MatterNode.js";
29
+ import { MatterNode } from "../MatterNode.js";
30
+ import { awaitSeeded } from "../util/awaitSeeded.js";
14
31
  import { resolveDclMode, withDclModeOption } from "./ota-dcl-mode.js";
15
32
 
33
+ const logger = Logger.get("cmd_nodes");
34
+
35
+ /** Look up a commissioned peer by node id across the controller's default admin fabric. */
36
+ function findCommissionedNode(theNode: MatterNode, nodeId: NodeId): ClientNode | undefined {
37
+ return theNode.node.peers.commissioned.find(peer => peer.peerAddress?.nodeId === nodeId);
38
+ }
39
+
16
40
  /** Parse a `udp://host:port` / `tcp://host:port` URL (IPv6 host in brackets) into a {@link ServerAddress}. */
17
41
  function parseFallbackAddress(input: string): ServerAddress {
18
42
  const match = /^(udp|tcp):\/\/(.+)$/i.exec(input);
19
43
  if (!match) {
20
- throw new Error(`Invalid address "${input}". Expected udp://<host>:<port> or tcp://<host>:<port>`);
44
+ throw new ImplementationError(
45
+ `Invalid address "${input}". Expected udp://<host>:<port> or tcp://<host>:<port>`,
46
+ );
21
47
  }
22
48
  const type = match[1].toLowerCase() === "tcp" ? "tcp" : "udp";
23
49
  const rest = match[2];
@@ -27,14 +53,14 @@ function parseFallbackAddress(input: string): ServerAddress {
27
53
  if (rest.startsWith("[")) {
28
54
  const end = rest.indexOf("]");
29
55
  if (end === -1 || rest[end + 1] !== ":") {
30
- throw new Error(`Invalid IPv6 address "${input}". Expected ${type}://[<ipv6>]:<port>`);
56
+ throw new ImplementationError(`Invalid IPv6 address "${input}". Expected ${type}://[<ipv6>]:<port>`);
31
57
  }
32
58
  ip = rest.slice(1, end);
33
59
  portStr = rest.slice(end + 2);
34
60
  } else {
35
61
  const idx = rest.lastIndexOf(":");
36
62
  if (idx === -1) {
37
- throw new Error(`Missing port in "${input}". Expected ${type}://<host>:<port>`);
63
+ throw new ImplementationError(`Missing port in "${input}". Expected ${type}://<host>:<port>`);
38
64
  }
39
65
  ip = rest.slice(0, idx);
40
66
  portStr = rest.slice(idx + 1);
@@ -42,7 +68,9 @@ function parseFallbackAddress(input: string): ServerAddress {
42
68
 
43
69
  const port = Number(portStr);
44
70
  if (!ip.length || !Number.isInteger(port) || port < 1 || port > 65535) {
45
- throw new Error(`Invalid host/port in "${input}". Expected ${type}://<host>:<port> with port 1-65535`);
71
+ throw new ImplementationError(
72
+ `Invalid host/port in "${input}". Expected ${type}://<host>:<port> with port 1-65535`,
73
+ );
46
74
  }
47
75
 
48
76
  return { ip, port, type };
@@ -68,27 +96,27 @@ export default function commands(theNode: MatterNode) {
68
96
  async argv => {
69
97
  const { status } = argv;
70
98
  await theNode.start();
71
- if (theNode.commissioningController === undefined) {
72
- throw new Error("CommissioningController not initialized");
73
- }
99
+ const peers = theNode.node.peers.commissioned;
74
100
  switch (status) {
75
101
  case "commissioned": {
76
- const details = theNode.commissioningController.getCommissionedNodesDetails();
77
- details
78
- .map(detail => ({
79
- ...detail,
80
- nodeId: detail.nodeId.toString(),
81
- }))
82
- .forEach(detail => {
83
- console.log(detail);
102
+ for (const peer of peers) {
103
+ const { addresses, deviceName } = peer.state.commissioning;
104
+ console.log({
105
+ nodeId: peer.peerAddress?.nodeId.toString(),
106
+ operationalAddress: addresses?.length
107
+ ? ServerAddress.urlFor(addresses[0])
108
+ : undefined,
109
+ advertisedName: deviceName,
110
+ basicInformation: peer.maybeStateOf(BasicInformationClient),
84
111
  });
112
+ }
85
113
  break;
86
114
  }
87
115
  case "connected": {
88
- const nodeIds = theNode.commissioningController
89
- .getCommissionedNodes()
90
- .filter(nodeId => !!theNode.commissioningController?.getPairedNode(nodeId));
91
- console.log(nodeIds.map(nodeId => nodeId.toString()));
116
+ const nodeIds = peers
117
+ .filter(peer => peer.lifecycle.isConnected)
118
+ .map(peer => peer.peerAddress?.nodeId);
119
+ console.log(nodeIds.map(nodeId => nodeId?.toString()));
92
120
  break;
93
121
  }
94
122
  }
@@ -107,9 +135,12 @@ export default function commands(theNode: MatterNode) {
107
135
  async argv => {
108
136
  const { nodeId } = argv;
109
137
  const node = (await theNode.connectAndGetNodes(nodeId))[0];
138
+ if (!(await awaitSeeded(node))) {
139
+ return;
140
+ }
110
141
 
111
- console.log("Logging structure of Node ", node.nodeId.toString());
112
- node.logStructure();
142
+ console.log("Logging structure of Node ", node.peerAddress?.nodeId.toString());
143
+ logger.info(node);
113
144
  },
114
145
  )
115
146
  .command(
@@ -125,12 +156,13 @@ export default function commands(theNode: MatterNode) {
125
156
  async argv => {
126
157
  const { nodeId: nodeIdStr } = argv;
127
158
  await theNode.start();
128
- if (theNode.commissioningController === undefined) {
129
- throw new Error("CommissioningController not initialized");
130
- }
131
159
 
132
160
  const nodeId = NodeId(BigInt(nodeIdStr));
133
- const peerAddress = theNode.commissioningController.fabric.addressOf(nodeId);
161
+ const peerAddress = findCommissionedNode(theNode, nodeId)?.peerAddress;
162
+ if (peerAddress === undefined) {
163
+ console.log(`Node ${nodeIdStr} not commissioned`);
164
+ return;
165
+ }
134
166
  const peerSet = theNode.node.env.get(PeerSet);
135
167
  const peer = peerSet.for(peerAddress);
136
168
 
@@ -209,30 +241,13 @@ export default function commands(theNode: MatterNode) {
209
241
  },
210
242
  async argv => {
211
243
  const { nodeId: nodeIdStr, maxSubscriptionInterval, minSubscriptionInterval } = argv;
212
- await theNode.start();
213
- if (theNode.commissioningController === undefined) {
214
- throw new Error("CommissioningController not initialized");
215
- }
216
- let nodeIds = theNode.commissioningController.getCommissionedNodes();
217
- if (nodeIdStr !== "all") {
218
- const cmdNodeId = NodeId(BigInt(nodeIdStr));
219
- nodeIds = nodeIds.filter(nodeId => nodeId === cmdNodeId);
220
- if (!nodeIds.length) {
221
- throw new Error(`Node ${nodeIdStr} not commissioned`);
222
- }
223
- }
224
-
225
244
  const autoSubscribe = minSubscriptionInterval !== undefined;
226
245
 
227
- for (const nodeIdToProcess of nodeIds) {
228
- const node = await theNode.commissioningController.getNode(nodeIdToProcess);
229
- node.connect({
230
- autoSubscribe,
231
- subscribeMinIntervalFloorSeconds: autoSubscribe ? minSubscriptionInterval : undefined,
232
- subscribeMaxIntervalCeilingSeconds: autoSubscribe ? maxSubscriptionInterval : undefined,
233
- ...createDiagnosticCallbacks(),
234
- });
235
- }
246
+ await theNode.connectAndGetNodes(nodeIdStr !== "all" ? nodeIdStr : undefined, {
247
+ autoSubscribe,
248
+ subscribeMinIntervalFloorSeconds: autoSubscribe ? minSubscriptionInterval : undefined,
249
+ subscribeMaxIntervalCeilingSeconds: autoSubscribe ? maxSubscriptionInterval : undefined,
250
+ });
236
251
  },
237
252
  )
238
253
  .command(
@@ -247,27 +262,21 @@ export default function commands(theNode: MatterNode) {
247
262
  },
248
263
  async argv => {
249
264
  const { nodeId: nodeIdStr } = argv;
250
- if (theNode.commissioningController === undefined) {
251
- console.log("Controller not initialized, nothing to disconnect.");
252
- return;
253
- }
265
+ await theNode.start();
254
266
 
255
- let nodeIds = theNode.commissioningController.getCommissionedNodes();
267
+ let nodes = theNode.node.peers.commissioned;
256
268
  if (nodeIdStr !== "all") {
257
269
  const cmdNodeId = NodeId(BigInt(nodeIdStr));
258
- nodeIds = nodeIds.filter(nodeId => nodeId === cmdNodeId);
259
- if (!nodeIds.length) {
260
- throw new Error(`Node ${nodeIdStr} not commissioned`);
270
+ nodes = nodes.filter(node => node.peerAddress?.nodeId === cmdNodeId);
271
+ if (!nodes.length) {
272
+ throw new ImplementationError(`Node ${nodeIdStr} not commissioned`);
261
273
  }
262
274
  }
263
275
 
264
- for (const nodeIdToProcess of nodeIds) {
265
- const node = theNode.commissioningController.getPairedNode(nodeIdToProcess);
266
- if (node === undefined) {
267
- console.log(`Node ${nodeIdToProcess} not connected`);
268
- continue;
269
- }
270
- await node.disconnect();
276
+ for (const node of nodes) {
277
+ // Stop (not disable): peers stay enabled for on-demand reconnect, and the sweep being off
278
+ // keeps them disconnected across restarts anyway.
279
+ await node.stop();
271
280
  }
272
281
  },
273
282
  )
@@ -285,33 +294,25 @@ export default function commands(theNode: MatterNode) {
285
294
  async argv => {
286
295
  const { nodeIds: nodeIdStr } = argv;
287
296
  await theNode.start();
288
- if (theNode.commissioningController === undefined) {
289
- throw new Error("CommissioningController not initialized");
290
- }
291
- let nodeIds = theNode.commissioningController.getCommissionedNodes();
297
+ let nodes = theNode.node.peers.commissioned;
292
298
  if (nodeIdStr !== "all") {
293
299
  const nodeIdList = nodeIdStr.split(",").map(nodeId => NodeId(BigInt(nodeId)));
294
- nodeIds = nodeIds.filter(nodeId => nodeIdList.includes(nodeId));
295
- if (!nodeIds.length) {
296
- throw new Error(`Node ${nodeIdStr} not commissioned`);
300
+ nodes = nodes.filter(
301
+ node => node.peerAddress !== undefined && nodeIdList.includes(node.peerAddress.nodeId),
302
+ );
303
+ if (!nodes.length) {
304
+ throw new ImplementationError(`Node ${nodeIdStr} not commissioned`);
297
305
  }
298
306
  }
299
307
 
300
- const nodeDetails = theNode.commissioningController.getCommissionedNodesDetails();
301
-
302
- for (const nodeIdToProcess of nodeIds) {
303
- const node = theNode.commissioningController.getPairedNode(nodeIdToProcess);
304
- if (node === undefined) {
305
- const details = nodeDetails.find(nd => nd.nodeId === nodeIdToProcess);
306
- console.log(
307
- `Node ${nodeIdToProcess}: Not initialized${details?.deviceData?.basicInformation !== undefined ? ` (${details.deviceData.basicInformation.vendorName} ${details.deviceData.basicInformation.productName})` : ""}`,
308
- );
309
- } else {
310
- const basicInfo = node.basicInformation;
311
- console.log(
312
- `Node ${nodeIdToProcess}: Node Status: ${capitalize(decamelize(NodeStateInformation[node.connectionState], " "))}${basicInfo !== undefined ? ` (${basicInfo.vendorName} ${basicInfo.productName})` : ""}`,
313
- );
314
- }
308
+ for (const node of nodes) {
309
+ const basicInfo = node.maybeStateOf(BasicInformationClient);
310
+ const status = capitalize(
311
+ decamelize(NodeConnectionState[node.lifecycle.connectionState], " "),
312
+ );
313
+ console.log(
314
+ `Node ${node.peerAddress?.nodeId}: Node Status: ${status}${basicInfo !== undefined ? ` (${basicInfo.vendorName} ${basicInfo.productName})` : ""}`,
315
+ );
315
316
  }
316
317
  },
317
318
  )
@@ -336,20 +337,22 @@ export default function commands(theNode: MatterNode) {
336
337
  async argv => {
337
338
  const { nodeId: nodeIdStr, preference } = argv;
338
339
  await theNode.start();
339
- if (theNode.commissioningController === undefined) {
340
- throw new Error("CommissioningController not initialized");
341
- }
342
340
 
343
341
  const nodeId = NodeId(BigInt(nodeIdStr));
344
- const node = await theNode.commissioningController.getNode(nodeId);
342
+ const node = findCommissionedNode(theNode, nodeId);
343
+ if (node === undefined) {
344
+ throw new ImplementationError(`Node ${nodeIdStr} not commissioned`);
345
+ }
346
+ const peerAddress = node.peerAddress;
347
+ if (peerAddress === undefined) {
348
+ throw new ImplementationError(`Node ${nodeIdStr} has no peer address`);
349
+ }
345
350
 
346
351
  const pref = preference === "on" ? "tcp" : preference === "off" ? "udp" : undefined;
347
- await node.node.setStateOf(NetworkClient, { transportPreference: pref });
352
+ await node.setStateOf(NetworkClient, { transportPreference: pref });
348
353
 
349
354
  // Also update the protocol-level peer preference
350
- const peer = theNode.node.env
351
- .get(PeerSet)
352
- .for(theNode.commissioningController.fabric.addressOf(nodeId));
355
+ const peer = theNode.node.env.get(PeerSet).for(peerAddress);
353
356
  if (peer) {
354
357
  peer.transportPreference = pref === "tcp" ? ChannelType.TCP : undefined;
355
358
  }
@@ -377,13 +380,13 @@ export default function commands(theNode: MatterNode) {
377
380
  async argv => {
378
381
  const { nodeId: nodeIdStr } = argv;
379
382
  await theNode.start();
380
- if (theNode.commissioningController === undefined) {
381
- throw new Error("CommissioningController not initialized");
382
- }
383
383
 
384
384
  const nodeId = NodeId(BigInt(nodeIdStr));
385
- const node = await theNode.commissioningController.getNode(nodeId);
386
- const addresses = node.node.state.commissioning.addresses;
385
+ const node = findCommissionedNode(theNode, nodeId);
386
+ if (node === undefined) {
387
+ throw new ImplementationError(`Node ${nodeIdStr} not commissioned`);
388
+ }
389
+ const addresses = node.state.commissioning.addresses;
387
390
 
388
391
  if (!addresses?.length) {
389
392
  console.log(`Node ${nodeIdStr} has no fallback addresses stored`);
@@ -415,21 +418,21 @@ export default function commands(theNode: MatterNode) {
415
418
  async argv => {
416
419
  const { nodeId: nodeIdStr, address: addressStr } = argv;
417
420
  await theNode.start();
418
- if (theNode.commissioningController === undefined) {
419
- throw new Error("CommissioningController not initialized");
420
- }
421
421
 
422
422
  const nodeId = NodeId(BigInt(nodeIdStr));
423
- const node = await theNode.commissioningController.getNode(nodeId);
423
+ const node = findCommissionedNode(theNode, nodeId);
424
+ if (node === undefined) {
425
+ throw new ImplementationError(`Node ${nodeIdStr} not commissioned`);
426
+ }
424
427
 
425
428
  if (addressStr === "drop") {
426
- await node.node.setStateOf(CommissioningClient, { addresses: undefined });
429
+ await node.setStateOf(CommissioningClient, { addresses: undefined });
427
430
  console.log(`Fallback address for node ${nodeIdStr} dropped`);
428
431
  return;
429
432
  }
430
433
 
431
434
  const address = parseFallbackAddress(addressStr);
432
- await node.node.setStateOf(CommissioningClient, { addresses: [address] });
435
+ await node.setStateOf(CommissioningClient, { addresses: [address] });
433
436
  console.log(
434
437
  `Fallback address for node ${nodeIdStr} set to ${ServerAddress.urlFor(address)}`,
435
438
  );
@@ -465,29 +468,25 @@ export default function commands(theNode: MatterNode) {
465
468
  async argv => {
466
469
  const { nodeId: nodeIdStr, maxSubscriptionInterval, minSubscriptionInterval } = argv;
467
470
  await theNode.start();
468
- if (theNode.commissioningController === undefined) {
469
- throw new Error("CommissioningController not initialized");
470
- }
471
- let nodeIds = theNode.commissioningController.getCommissionedNodes();
472
471
 
473
472
  const cmdNodeId = NodeId(BigInt(nodeIdStr));
474
- nodeIds = nodeIds.filter(nodeId => nodeId === cmdNodeId);
475
- if (nodeIds.length) {
476
- throw new Error(`Node ${nodeIdStr} already known`);
473
+ if (findCommissionedNode(theNode, cmdNodeId) !== undefined) {
474
+ throw new ImplementationError(`Node ${nodeIdStr} already known`);
477
475
  }
478
476
 
479
- await theNode.commissioningController.node.peers.forAddress(
480
- theNode.commissioningController.fabric.addressOf(cmdNodeId),
481
- );
477
+ // Single-fabric shell: the controller's own (first-owned) fabric is the one to address peers on.
478
+ const fabric = theNode.node.env.get(FabricAuthority).fabrics[0];
479
+ if (fabric === undefined) {
480
+ throw new InternalError("No controller fabric present after start");
481
+ }
482
+ await theNode.node.peers.forAddress(fabric.addressOf(cmdNodeId));
482
483
 
483
484
  const autoSubscribe = minSubscriptionInterval !== undefined;
484
485
 
485
- const node = await theNode.commissioningController.getNode(cmdNodeId);
486
- node.connect({
486
+ await theNode.connectAndGetNodes(nodeIdStr, {
487
487
  autoSubscribe,
488
488
  subscribeMinIntervalFloorSeconds: autoSubscribe ? minSubscriptionInterval : undefined,
489
489
  subscribeMaxIntervalCeilingSeconds: autoSubscribe ? maxSubscriptionInterval : undefined,
490
- ...createDiagnosticCallbacks(),
491
490
  });
492
491
  },
493
492
  )
@@ -516,21 +515,20 @@ export default function commands(theNode: MatterNode) {
516
515
  const { nodeId: nodeIdStr, local } = argv;
517
516
 
518
517
  await theNode.start();
519
- if (theNode.commissioningController === undefined) {
520
- throw new Error("CommissioningController not initialized");
521
- }
522
518
 
523
519
  let peerToCheck: ClientNode | undefined = undefined;
524
520
  if (nodeIdStr !== undefined) {
525
521
  const nodeId = NodeId(BigInt(nodeIdStr));
526
- peerToCheck = (await theNode.commissioningController.getNode(nodeId))?.node;
522
+ peerToCheck = findCommissionedNode(theNode, nodeId);
523
+ if (peerToCheck === undefined) {
524
+ throw new ImplementationError(`Node ${nodeIdStr} not commissioned`);
525
+ }
527
526
  }
528
527
 
529
- const updatesAvailable = await theNode.commissioningController.otaProvider.act(
530
- agent =>
531
- agent
532
- .get(SoftwareUpdateManager)
533
- .queryUpdates({ peerToCheck, includeStoredUpdates: local }),
528
+ const updatesAvailable = await theNode.otaProviderEndpoint.act(agent =>
529
+ agent
530
+ .get(SoftwareUpdateManager)
531
+ .queryUpdates({ peerToCheck, includeStoredUpdates: local }),
534
532
  );
535
533
 
536
534
  if (updatesAvailable.length) {
@@ -567,34 +565,32 @@ export default function commands(theNode: MatterNode) {
567
565
  const { label: dclMode, isProduction } = resolveDclMode(theNode, mode);
568
566
 
569
567
  await theNode.start();
570
- if (theNode.commissioningController === undefined) {
571
- throw new Error("CommissioningController not initialized");
572
- }
573
568
 
574
569
  const nodeId = NodeId(BigInt(nodeIdStr));
575
- const nodeDetails = theNode.commissioningController
576
- .getCommissionedNodesDetails()
577
- .find(nd => nd.nodeId === nodeId);
578
- const basicInfo = nodeDetails?.deviceData?.basicInformation;
579
- if (!basicInfo) {
580
- throw new Error(`Node ${nodeIdStr} has no basic information available`);
570
+ const node = findCommissionedNode(theNode, nodeId);
571
+ if (node === undefined) {
572
+ throw new ImplementationError(`Node ${nodeIdStr} not commissioned`);
573
+ }
574
+ const basicInfo = node.maybeStateOf(BasicInformationClient);
575
+ if (basicInfo === undefined) {
576
+ throw new ImplementationError(
577
+ `Node ${nodeIdStr} has no basic information available`,
578
+ );
581
579
  }
582
580
  if (
583
581
  basicInfo.vendorId === undefined ||
584
582
  basicInfo.productId === undefined ||
585
583
  basicInfo.softwareVersion === undefined
586
584
  ) {
587
- throw new Error(
588
- `Node ${nodeIdStr} is missing required basic information for OTA check`,
585
+ throw new ImplementationError(
586
+ `Node ${nodeIdStr} BasicInformation is incomplete; connect the node first`,
589
587
  );
590
588
  }
591
589
 
592
590
  console.log(`Checking for OTA updates for node ${nodeIdStr}...`);
591
+ console.log(` Vendor ID: ${Diagnostic.hex(basicInfo.vendorId, 4).toUpperCase()}`);
593
592
  console.log(
594
- ` Vendor ID: ${Diagnostic.hex(basicInfo.vendorId as VendorId, 4).toUpperCase()}`,
595
- );
596
- console.log(
597
- ` Product ID: ${Diagnostic.hex(basicInfo.productId as number, 4).toUpperCase()}`,
593
+ ` Product ID: ${Diagnostic.hex(basicInfo.productId, 4).toUpperCase()}`,
598
594
  );
599
595
  console.log(
600
596
  ` Current Software Version: ${basicInfo.softwareVersion} (${basicInfo.softwareVersionString})`,
@@ -604,9 +600,9 @@ export default function commands(theNode: MatterNode) {
604
600
  const updateInfo = await (
605
601
  await theNode.otaService()
606
602
  ).checkForUpdate({
607
- vendorId: basicInfo.vendorId as VendorId,
608
- productId: basicInfo.productId as number,
609
- currentSoftwareVersion: basicInfo.softwareVersion as number,
603
+ vendorId: basicInfo.vendorId,
604
+ productId: basicInfo.productId,
605
+ currentSoftwareVersion: basicInfo.softwareVersion,
610
606
  includeStoredUpdates: local,
611
607
  isProduction,
612
608
  });
@@ -660,34 +656,32 @@ export default function commands(theNode: MatterNode) {
660
656
  const forceDownload = force === true;
661
657
 
662
658
  await theNode.start();
663
- if (theNode.commissioningController === undefined) {
664
- throw new Error("CommissioningController not initialized");
665
- }
666
659
 
667
660
  const nodeId = NodeId(BigInt(nodeIdStr));
668
- const nodeDetails = theNode.commissioningController
669
- .getCommissionedNodesDetails()
670
- .find(nd => nd.nodeId === nodeId);
671
- const basicInfo = nodeDetails?.deviceData?.basicInformation;
672
- if (!basicInfo) {
673
- throw new Error(`Node ${nodeIdStr} has no basic information available`);
661
+ const node = findCommissionedNode(theNode, nodeId);
662
+ if (node === undefined) {
663
+ throw new ImplementationError(`Node ${nodeIdStr} not commissioned`);
664
+ }
665
+ const basicInfo = node.maybeStateOf(BasicInformationClient);
666
+ if (basicInfo === undefined) {
667
+ throw new ImplementationError(
668
+ `Node ${nodeIdStr} has no basic information available`,
669
+ );
674
670
  }
675
671
  if (
676
672
  basicInfo.vendorId === undefined ||
677
673
  basicInfo.productId === undefined ||
678
674
  basicInfo.softwareVersion === undefined
679
675
  ) {
680
- throw new Error(
681
- `Node ${nodeIdStr} is missing required basic information for OTA check`,
676
+ throw new ImplementationError(
677
+ `Node ${nodeIdStr} BasicInformation is incomplete; connect the node first`,
682
678
  );
683
679
  }
684
680
 
685
681
  console.log(`Checking for OTA updates for node ${nodeIdStr}...`);
682
+ console.log(` Vendor ID: ${Diagnostic.hex(basicInfo.vendorId, 4).toUpperCase()}`);
686
683
  console.log(
687
- ` Vendor ID: ${Diagnostic.hex(basicInfo.vendorId as VendorId, 4).toUpperCase()}`,
688
- );
689
- console.log(
690
- ` Product ID: ${Diagnostic.hex(basicInfo.productId as number, 4).toUpperCase()}`,
684
+ ` Product ID: ${Diagnostic.hex(basicInfo.productId, 4).toUpperCase()}`,
691
685
  );
692
686
  console.log(
693
687
  ` Current Software Version: ${basicInfo.softwareVersion} (${basicInfo.softwareVersionString})`,
@@ -697,9 +691,9 @@ export default function commands(theNode: MatterNode) {
697
691
  const updateInfo = await (
698
692
  await theNode.otaService()
699
693
  ).checkForUpdate({
700
- vendorId: basicInfo.vendorId as VendorId,
701
- productId: basicInfo.productId as number,
702
- currentSoftwareVersion: basicInfo.softwareVersion as number,
694
+ vendorId: basicInfo.vendorId,
695
+ productId: basicInfo.productId,
696
+ currentSoftwareVersion: basicInfo.softwareVersion,
703
697
  includeStoredUpdates: local,
704
698
  isProduction,
705
699
  });
@@ -751,43 +745,64 @@ export default function commands(theNode: MatterNode) {
751
745
  describe: "Apply update from local file",
752
746
  type: "boolean",
753
747
  default: false,
748
+ })
749
+ .option("max-block-size", {
750
+ describe:
751
+ "Cap the BDX block size for this transfer, in bytes (default: whatever the device requests)",
752
+ type: "number",
753
+ })
754
+ .option("mrp-margin", {
755
+ describe:
756
+ "Additive MRP retransmission margin for this transfer, in milliseconds (default: derived from the device's medium)",
757
+ type: "number",
758
+ })
759
+ .check(argv => {
760
+ const blockSize = argv.maxBlockSize as number | undefined;
761
+ const margin = argv.mrpMargin as number | undefined;
762
+ if (
763
+ blockSize !== undefined &&
764
+ (!Number.isInteger(blockSize) || blockSize <= 0)
765
+ ) {
766
+ throw new Error("--max-block-size must be a positive integer");
767
+ }
768
+ if (margin !== undefined && (!Number.isFinite(margin) || margin < 0)) {
769
+ throw new Error("--mrp-margin must be a non-negative number of ms");
770
+ }
771
+ return true;
754
772
  });
755
773
  },
756
774
  async argv => {
757
- const { nodeId: nodeIdStr, mode, force, local } = argv;
775
+ const { nodeId: nodeIdStr, mode, force, local, maxBlockSize, mrpMargin } = argv;
758
776
  const { label: dclMode, isProduction } = resolveDclMode(theNode, mode);
759
777
  const forceDownload = force === true;
760
778
 
761
779
  await theNode.start();
762
780
 
763
- if (theNode.commissioningController === undefined) {
764
- throw new Error("CommissioningController not initialized");
765
- }
766
-
767
781
  const nodeId = NodeId(BigInt(nodeIdStr));
768
- const nodeDetails = theNode.commissioningController
769
- .getCommissionedNodesDetails()
770
- .find(nd => nd.nodeId === nodeId);
771
- const basicInfo = nodeDetails?.deviceData?.basicInformation;
772
- if (!basicInfo) {
773
- throw new Error(`Node ${nodeIdStr} has no basic information available`);
782
+ const node = findCommissionedNode(theNode, nodeId);
783
+ if (node === undefined) {
784
+ throw new ImplementationError(`Node ${nodeIdStr} not commissioned`);
785
+ }
786
+ const basicInfo = node.maybeStateOf(BasicInformationClient);
787
+ if (basicInfo === undefined) {
788
+ throw new ImplementationError(
789
+ `Node ${nodeIdStr} has no basic information available`,
790
+ );
774
791
  }
775
792
  if (
776
793
  basicInfo.vendorId === undefined ||
777
794
  basicInfo.productId === undefined ||
778
795
  basicInfo.softwareVersion === undefined
779
796
  ) {
780
- throw new Error(
781
- `Node ${nodeIdStr} is missing required basic information for OTA check`,
797
+ throw new ImplementationError(
798
+ `Node ${nodeIdStr} BasicInformation is incomplete; connect the node first`,
782
799
  );
783
800
  }
784
801
 
785
802
  console.log(`Checking for OTA updates for node ${nodeIdStr}...`);
803
+ console.log(` Vendor ID: ${Diagnostic.hex(basicInfo.vendorId, 4).toUpperCase()}`);
786
804
  console.log(
787
- ` Vendor ID: ${Diagnostic.hex(basicInfo.vendorId as VendorId, 4).toUpperCase()}`,
788
- );
789
- console.log(
790
- ` Product ID: ${Diagnostic.hex(basicInfo.productId as number, 4).toUpperCase()}`,
805
+ ` Product ID: ${Diagnostic.hex(basicInfo.productId, 4).toUpperCase()}`,
791
806
  );
792
807
  console.log(
793
808
  ` Current Software Version: ${basicInfo.softwareVersion} (${basicInfo.softwareVersionString})`,
@@ -797,9 +812,9 @@ export default function commands(theNode: MatterNode) {
797
812
  const localUpdates = await (
798
813
  await theNode.otaService()
799
814
  ).find({
800
- vendorId: basicInfo.vendorId as VendorId,
801
- productId: basicInfo.productId as number,
802
- currentVersion: basicInfo.softwareVersion as number,
815
+ vendorId: basicInfo.vendorId,
816
+ productId: basicInfo.productId,
817
+ currentVersion: basicInfo.softwareVersion,
803
818
  });
804
819
 
805
820
  if (local && !localUpdates.length) {
@@ -810,9 +825,9 @@ export default function commands(theNode: MatterNode) {
810
825
  const updateInfo = await (
811
826
  await theNode.otaService()
812
827
  ).checkForUpdate({
813
- vendorId: basicInfo.vendorId as VendorId,
814
- productId: basicInfo.productId as number,
815
- currentSoftwareVersion: basicInfo.softwareVersion as number,
828
+ vendorId: basicInfo.vendorId,
829
+ productId: basicInfo.productId,
830
+ currentSoftwareVersion: basicInfo.softwareVersion,
816
831
  includeStoredUpdates: local,
817
832
  isProduction,
818
833
  });
@@ -849,20 +864,32 @@ export default function commands(theNode: MatterNode) {
849
864
  );
850
865
  }
851
866
 
852
- const node = theNode.commissioningController.getPairedNode(nodeId);
853
- if (node === undefined) {
854
- throw new Error(`Node ${nodeIdStr} not connected`);
867
+ if (!node.lifecycle.isConnected) {
868
+ throw new ImplementationError(`Node ${nodeIdStr} not connected`);
869
+ }
870
+ const peerAddress = node.peerAddress;
871
+ if (peerAddress === undefined) {
872
+ throw new ImplementationError(`Node ${nodeIdStr} has no peer address`);
855
873
  }
856
874
 
857
- await theNode.commissioningController.otaProvider.act(agent => {
875
+ if (maxBlockSize !== undefined) {
876
+ console.log(`Capping BDX block size to ${maxBlockSize} bytes`);
877
+ }
878
+ if (mrpMargin !== undefined) {
879
+ console.log(`Using an MRP retransmission margin of ${mrpMargin}ms`);
880
+ }
881
+
882
+ await theNode.otaProviderEndpoint.act(agent => {
858
883
  return agent
859
884
  .get(SoftwareUpdateManager)
860
- .forceUpdate(
861
- PeerAddress({ nodeId, fabricIndex: FabricIndex(1) }),
862
- basicInfo.vendorId as VendorId,
863
- basicInfo.productId as number,
864
- updateVersion,
865
- );
885
+ .forceUpdate(PeerAddress({ nodeId, fabricIndex: FabricIndex(1) }), {
886
+ vendorId: basicInfo.vendorId as VendorId,
887
+ productId: basicInfo.productId as number,
888
+ targetSoftwareVersion: updateVersion,
889
+ maxBdxBlockSize: maxBlockSize,
890
+ bdxAdditionalMrpDelay:
891
+ mrpMargin === undefined ? undefined : Millis(mrpMargin),
892
+ });
866
893
  });
867
894
  },
868
895
  )