@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.
- package/README.md +11 -1
- package/dist/esm/MatterNode.js +237 -134
- package/dist/esm/MatterNode.js.map +1 -1
- package/dist/esm/app.js +13 -7
- package/dist/esm/app.js.map +1 -1
- package/dist/esm/shell/cmd_cluster-attributes.js +64 -45
- package/dist/esm/shell/cmd_cluster-attributes.js.map +2 -2
- package/dist/esm/shell/cmd_cluster-commands.js +11 -9
- package/dist/esm/shell/cmd_cluster-commands.js.map +1 -1
- package/dist/esm/shell/cmd_cluster-events.js +36 -9
- package/dist/esm/shell/cmd_cluster-events.js.map +1 -1
- package/dist/esm/shell/cmd_commission.js +119 -93
- package/dist/esm/shell/cmd_commission.js.map +1 -1
- package/dist/esm/shell/cmd_discover.js +23 -21
- package/dist/esm/shell/cmd_discover.js.map +1 -1
- package/dist/esm/shell/cmd_icd.js +46 -30
- package/dist/esm/shell/cmd_icd.js.map +1 -1
- package/dist/esm/shell/cmd_identify.js +11 -7
- package/dist/esm/shell/cmd_identify.js.map +1 -1
- package/dist/esm/shell/cmd_nodes.js +189 -168
- package/dist/esm/shell/cmd_nodes.js.map +1 -1
- package/dist/esm/shell/cmd_session.js +3 -4
- package/dist/esm/shell/cmd_session.js.map +1 -1
- package/dist/esm/shell/cmd_subscribe.js +26 -12
- package/dist/esm/shell/cmd_subscribe.js.map +1 -1
- package/dist/esm/util/ClusterEndpoint.js +61 -0
- package/dist/esm/util/ClusterEndpoint.js.map +6 -0
- package/dist/esm/util/awaitSeeded.js +31 -0
- package/dist/esm/util/awaitSeeded.js.map +6 -0
- package/dist/esm/util/diagnosticLogging.js +99 -0
- package/dist/esm/util/diagnosticLogging.js.map +6 -0
- package/dist/esm/util/legacyStorageMigration.js +188 -0
- package/dist/esm/util/legacyStorageMigration.js.map +6 -0
- package/package.json +10 -11
- package/src/MatterNode.ts +298 -159
- package/src/app.ts +17 -9
- package/src/shell/cmd_cluster-attributes.ts +77 -53
- package/src/shell/cmd_cluster-commands.ts +12 -13
- package/src/shell/cmd_cluster-events.ts +38 -11
- package/src/shell/cmd_commission.ts +136 -113
- package/src/shell/cmd_discover.ts +36 -22
- package/src/shell/cmd_icd.ts +52 -32
- package/src/shell/cmd_identify.ts +11 -7
- package/src/shell/cmd_nodes.ts +226 -199
- package/src/shell/cmd_session.ts +3 -5
- package/src/shell/cmd_subscribe.ts +37 -14
- package/src/shell/webassets/index.html +1 -1
- package/src/tsconfig.json +0 -3
- package/src/util/ClusterEndpoint.ts +107 -0
- package/src/util/awaitSeeded.ts +43 -0
- package/src/util/diagnosticLogging.ts +117 -0
- package/src/util/legacyStorageMigration.ts +275 -0
|
@@ -4,15 +4,23 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { Diagnostic, Logger,
|
|
7
|
+
import { Diagnostic, ImplementationError, Logger, Seconds } from "@matter/general";
|
|
8
|
+
import { ClientNode, CommissioningClient } from "@matter/node";
|
|
9
|
+
import { BasicInformationClient } from "@matter/node/behaviors/basic-information";
|
|
10
|
+
import { DescriptorClient } from "@matter/node/behaviors/descriptor";
|
|
8
11
|
import { DiscoveryCapabilitiesSchema, ManualPairingCodeCodec, NodeId, QrCode, QrPairingCodeCodec } from "@matter/types";
|
|
9
|
-
import {
|
|
10
|
-
import { NodeCommissioningOptions } from "@project-chip/matter.js";
|
|
12
|
+
import { GeneralCommissioning } from "@matter/types/clusters";
|
|
11
13
|
import type { Argv } from "yargs";
|
|
12
|
-
import {
|
|
14
|
+
import { MatterNode } from "../MatterNode.js";
|
|
15
|
+
import { awaitSeeded } from "../util/awaitSeeded.js";
|
|
13
16
|
|
|
14
17
|
const logger = Logger.get("Commission");
|
|
15
18
|
|
|
19
|
+
/** Look up a commissioned peer by node id across the controller's default admin fabric. */
|
|
20
|
+
function findCommissionedNode(theNode: MatterNode, nodeId: NodeId): ClientNode | undefined {
|
|
21
|
+
return theNode.node.peers.commissioned.find(peer => peer.peerAddress?.nodeId === nodeId);
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
export default function commands(theNode: MatterNode) {
|
|
17
25
|
return {
|
|
18
26
|
command: "commission",
|
|
@@ -23,9 +31,53 @@ export default function commands(theNode: MatterNode) {
|
|
|
23
31
|
.command("pair", "Pair with a matter device", yargs => {
|
|
24
32
|
return (
|
|
25
33
|
yargs
|
|
34
|
+
// Options are registered before the command below so their types flow into its handler's argv.
|
|
35
|
+
.options({
|
|
36
|
+
pairingCode: {
|
|
37
|
+
describe: "pairing code",
|
|
38
|
+
default: undefined,
|
|
39
|
+
type: "string",
|
|
40
|
+
},
|
|
41
|
+
qrCode: {
|
|
42
|
+
describe: "QR code string (MT:...)",
|
|
43
|
+
default: undefined,
|
|
44
|
+
type: "string",
|
|
45
|
+
},
|
|
46
|
+
qrCodeIndex: {
|
|
47
|
+
describe: "Index of QR code entry if multiple (1..n)",
|
|
48
|
+
default: 1,
|
|
49
|
+
type: "number",
|
|
50
|
+
},
|
|
51
|
+
setupPinCode: {
|
|
52
|
+
describe: "setup pin code",
|
|
53
|
+
default: 20202021,
|
|
54
|
+
type: "number",
|
|
55
|
+
},
|
|
56
|
+
instanceId: {
|
|
57
|
+
alias: "i",
|
|
58
|
+
describe: "instance id",
|
|
59
|
+
type: "string",
|
|
60
|
+
},
|
|
61
|
+
discriminator: {
|
|
62
|
+
alias: "d",
|
|
63
|
+
description: "Long discriminator",
|
|
64
|
+
type: "number",
|
|
65
|
+
},
|
|
66
|
+
shortDiscriminator: {
|
|
67
|
+
alias: "s",
|
|
68
|
+
description: "Short discriminator",
|
|
69
|
+
type: "number",
|
|
70
|
+
},
|
|
71
|
+
ble: {
|
|
72
|
+
alias: "b",
|
|
73
|
+
description: "Also discover over BLE",
|
|
74
|
+
type: "boolean",
|
|
75
|
+
default: false,
|
|
76
|
+
},
|
|
77
|
+
})
|
|
26
78
|
// Pair
|
|
27
79
|
.command(
|
|
28
|
-
"* [node-id] [ip
|
|
80
|
+
"* [node-id] [ip] [port]",
|
|
29
81
|
"Commission a matter device",
|
|
30
82
|
yargs => {
|
|
31
83
|
return yargs
|
|
@@ -46,17 +98,16 @@ export default function commands(theNode: MatterNode) {
|
|
|
46
98
|
});
|
|
47
99
|
},
|
|
48
100
|
async argv => {
|
|
49
|
-
const {
|
|
50
|
-
pairingCode,
|
|
51
|
-
qrCode,
|
|
52
|
-
nodeId: nodeIdStr,
|
|
53
|
-
ipPort,
|
|
54
|
-
ip,
|
|
55
|
-
ble = false,
|
|
56
|
-
instanceId,
|
|
57
|
-
} = argv;
|
|
101
|
+
const { pairingCode, qrCode, nodeId: nodeIdStr, port, ip, ble, instanceId } = argv;
|
|
58
102
|
let { setupPinCode, discriminator, shortDiscriminator, qrCodeIndex } = argv;
|
|
59
103
|
|
|
104
|
+
if ((ip === undefined) !== (port === undefined)) {
|
|
105
|
+
console.log(
|
|
106
|
+
"Known-address commissioning requires both <ip> and <port>; provide both or neither (to use discovery).",
|
|
107
|
+
);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
60
111
|
if (typeof pairingCode === "string" && pairingCode.length > 0) {
|
|
61
112
|
const { shortDiscriminator: pairingCodeShortDiscriminator, passcode } =
|
|
62
113
|
ManualPairingCodeCodec.decode(pairingCode);
|
|
@@ -104,32 +155,6 @@ export default function commands(theNode: MatterNode) {
|
|
|
104
155
|
|
|
105
156
|
const nodeId = nodeIdStr !== undefined ? NodeId(BigInt(nodeIdStr)) : undefined;
|
|
106
157
|
await theNode.start();
|
|
107
|
-
if (theNode.commissioningController === undefined) {
|
|
108
|
-
throw new Error("CommissioningController not initialized");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const options = {
|
|
112
|
-
discovery: {
|
|
113
|
-
knownAddress:
|
|
114
|
-
ip !== undefined && ipPort !== undefined
|
|
115
|
-
? { ip, port: ipPort, type: "udp" }
|
|
116
|
-
: undefined,
|
|
117
|
-
identifierData:
|
|
118
|
-
instanceId !== undefined
|
|
119
|
-
? { instanceId }
|
|
120
|
-
: discriminator !== undefined
|
|
121
|
-
? { longDiscriminator: discriminator }
|
|
122
|
-
: shortDiscriminator !== undefined
|
|
123
|
-
? { shortDiscriminator }
|
|
124
|
-
: {},
|
|
125
|
-
discoveryCapabilities: {
|
|
126
|
-
ble,
|
|
127
|
-
onIpNetwork: true,
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
passcode: setupPinCode,
|
|
131
|
-
...createDiagnosticCallbacks(),
|
|
132
|
-
} as NodeCommissioningOptions;
|
|
133
158
|
|
|
134
159
|
// Attestation policy: strict rejects errors but allows warnings/info
|
|
135
160
|
const strictAttestation = await theNode.Store.get<boolean>(
|
|
@@ -137,8 +162,12 @@ export default function commands(theNode: MatterNode) {
|
|
|
137
162
|
false,
|
|
138
163
|
);
|
|
139
164
|
|
|
140
|
-
|
|
141
|
-
|
|
165
|
+
const commissioningOptions: CommissioningClient.CommissioningOptions = {
|
|
166
|
+
passcode: setupPinCode,
|
|
167
|
+
discriminator,
|
|
168
|
+
nodeId,
|
|
169
|
+
// The shell subscribes on demand via the `subscribe` command, not at commission.
|
|
170
|
+
autoSubscribe: false,
|
|
142
171
|
regulatoryLocation: GeneralCommissioning.RegulatoryLocationType.Outdoor, // Set to the most restrictive if relevant
|
|
143
172
|
regulatoryCountryCode: "XX",
|
|
144
173
|
onAttestationFailure: findings => {
|
|
@@ -156,103 +185,82 @@ export default function commands(theNode: MatterNode) {
|
|
|
156
185
|
},
|
|
157
186
|
};
|
|
158
187
|
|
|
159
|
-
|
|
188
|
+
// Keep secrets out of the debug dump (CodeQL: clear-text logging of sensitive
|
|
189
|
+
// info). Omit the passcode entirely — spreading then overriding still flows it
|
|
190
|
+
// into the logged object — and mask network credentials.
|
|
191
|
+
const { passcode: _passcode, wifiNetwork, ...loggable } = commissioningOptions;
|
|
192
|
+
console.log(
|
|
193
|
+
Diagnostic.json({
|
|
194
|
+
...loggable,
|
|
195
|
+
...(wifiNetwork && {
|
|
196
|
+
wifiNetwork: { ...wifiNetwork, wifiCredentials: "<redacted>" },
|
|
197
|
+
}),
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
160
200
|
|
|
161
201
|
await theNode.certificateService();
|
|
162
202
|
|
|
163
203
|
const wifiSsid = await theNode.Store.get<string>("WiFiSsid", "");
|
|
164
204
|
const wifiCredentials = await theNode.Store.get<string>("WiFiPassword", "");
|
|
165
205
|
if (wifiSsid.length > 0 && wifiCredentials.length > 0) {
|
|
166
|
-
|
|
206
|
+
commissioningOptions.wifiNetwork = { wifiSsid, wifiCredentials };
|
|
167
207
|
}
|
|
168
208
|
const threadOperationalDataset = await theNode.Store.get<string>(
|
|
169
209
|
"ThreadOperationalDataset",
|
|
170
210
|
"",
|
|
171
211
|
);
|
|
172
212
|
if (threadOperationalDataset.length > 0) {
|
|
173
|
-
|
|
213
|
+
commissioningOptions.threadNetwork = {
|
|
174
214
|
networkName: await theNode.Store.get<string>("ThreadName", ""),
|
|
175
215
|
operationalDataset: threadOperationalDataset,
|
|
176
216
|
};
|
|
177
217
|
}
|
|
178
218
|
|
|
179
|
-
|
|
180
|
-
|
|
219
|
+
let node: ClientNode;
|
|
220
|
+
if (ip !== undefined && port !== undefined) {
|
|
221
|
+
// Known address: locate/create the peer node directly instead of running mDNS
|
|
222
|
+
// discovery. Mirrors MatterController's own known-address commissioning path.
|
|
223
|
+
node = await theNode.node.peers.forDescriptor({
|
|
224
|
+
addresses: [{ ip, port, type: "udp" }],
|
|
225
|
+
});
|
|
226
|
+
await node.commission(commissioningOptions);
|
|
227
|
+
} else {
|
|
228
|
+
const identifierData =
|
|
229
|
+
instanceId !== undefined
|
|
230
|
+
? { instanceId }
|
|
231
|
+
: shortDiscriminator !== undefined
|
|
232
|
+
? { shortDiscriminator }
|
|
233
|
+
: {};
|
|
234
|
+
node = await theNode.node.peers.commission({
|
|
235
|
+
...identifierData,
|
|
236
|
+
...commissioningOptions,
|
|
237
|
+
discoveryCapabilities: { ble, onIpNetwork: true },
|
|
238
|
+
});
|
|
239
|
+
}
|
|
181
240
|
|
|
182
|
-
console.log("Commissioned Node:",
|
|
241
|
+
console.log("Commissioned Node:", node.peerAddress?.nodeId.toString());
|
|
183
242
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
// Should not happen
|
|
187
|
-
throw new MatterError("Node not found after commissioning.");
|
|
243
|
+
if (!(await awaitSeeded(node))) {
|
|
244
|
+
return;
|
|
188
245
|
}
|
|
189
246
|
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
// Example to initialize a ClusterClient and access concrete fields as API methods
|
|
194
|
-
const descriptor = node.getRootClusterClient(Descriptor);
|
|
247
|
+
// Example to read cluster state directly instead of via a ClusterClient
|
|
248
|
+
const descriptor = node.maybeStateOf(DescriptorClient);
|
|
195
249
|
if (descriptor !== undefined) {
|
|
196
|
-
console.log(
|
|
197
|
-
console.log(
|
|
250
|
+
console.log(descriptor.deviceTypeList);
|
|
251
|
+
console.log(descriptor.serverList);
|
|
198
252
|
} else {
|
|
199
253
|
console.log("No Descriptor Cluster found. This should never happen!");
|
|
200
254
|
}
|
|
201
255
|
|
|
202
|
-
|
|
203
|
-
const info = node.getRootClusterClient(BasicInformation);
|
|
256
|
+
const info = node.maybeStateOf(BasicInformationClient);
|
|
204
257
|
if (info !== undefined) {
|
|
205
|
-
console.log(
|
|
206
|
-
//console.log(await info.subscribeProductNameAttribute(value => console.log("productName", value), 5, 30));
|
|
207
|
-
//console.log(await info.getProductNameAttribute()); // This call is resolved locally because we have subscribed to the value!
|
|
258
|
+
console.log(info.productName);
|
|
208
259
|
} else {
|
|
209
260
|
console.log("No BasicInformation Cluster found. This should never happen!");
|
|
210
261
|
}
|
|
211
262
|
},
|
|
212
263
|
)
|
|
213
|
-
.options({
|
|
214
|
-
pairingCode: {
|
|
215
|
-
describe: "pairing code",
|
|
216
|
-
default: undefined,
|
|
217
|
-
type: "string",
|
|
218
|
-
},
|
|
219
|
-
qrCode: {
|
|
220
|
-
describe: "QR code string (MT:...)",
|
|
221
|
-
default: undefined,
|
|
222
|
-
type: "string",
|
|
223
|
-
},
|
|
224
|
-
qrCodeIndex: {
|
|
225
|
-
describe: "Index of QR code entry if multiple (1..n)",
|
|
226
|
-
default: 1,
|
|
227
|
-
type: "number",
|
|
228
|
-
},
|
|
229
|
-
setupPinCode: {
|
|
230
|
-
describe: "setup pin code",
|
|
231
|
-
default: 20202021,
|
|
232
|
-
type: "number",
|
|
233
|
-
},
|
|
234
|
-
instanceId: {
|
|
235
|
-
alias: "i",
|
|
236
|
-
describe: "instance id",
|
|
237
|
-
type: "string",
|
|
238
|
-
},
|
|
239
|
-
discriminator: {
|
|
240
|
-
alias: "d",
|
|
241
|
-
description: "Long discriminator",
|
|
242
|
-
type: "number",
|
|
243
|
-
},
|
|
244
|
-
shortDiscriminator: {
|
|
245
|
-
alias: "s",
|
|
246
|
-
description: "Short discriminator",
|
|
247
|
-
type: "number",
|
|
248
|
-
},
|
|
249
|
-
ble: {
|
|
250
|
-
alias: "b",
|
|
251
|
-
description: "Also discover over BLE",
|
|
252
|
-
type: "boolean",
|
|
253
|
-
default: false,
|
|
254
|
-
},
|
|
255
|
-
})
|
|
256
264
|
);
|
|
257
265
|
})
|
|
258
266
|
.command(
|
|
@@ -275,8 +283,11 @@ export default function commands(theNode: MatterNode) {
|
|
|
275
283
|
const { nodeId, timeout } = argv;
|
|
276
284
|
await theNode.start();
|
|
277
285
|
const node = (await theNode.connectAndGetNodes(nodeId, { autoSubscribe: false }))[0];
|
|
286
|
+
if (!(await awaitSeeded(node))) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
278
289
|
|
|
279
|
-
await node.openBasicCommissioningWindow(timeout);
|
|
290
|
+
await node.openBasicCommissioningWindow(Seconds(timeout));
|
|
280
291
|
|
|
281
292
|
console.log(`Basic Commissioning Window for node ${nodeId} opened`);
|
|
282
293
|
},
|
|
@@ -301,10 +312,14 @@ export default function commands(theNode: MatterNode) {
|
|
|
301
312
|
await theNode.start();
|
|
302
313
|
const { nodeId, timeout } = argv;
|
|
303
314
|
const node = (await theNode.connectAndGetNodes(nodeId, { autoSubscribe: false }))[0];
|
|
304
|
-
|
|
315
|
+
if (!(await awaitSeeded(node))) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const { qrPairingCode, manualPairingCode } = await node.openEnhancedCommissioningWindow(
|
|
319
|
+
Seconds(timeout),
|
|
320
|
+
);
|
|
305
321
|
|
|
306
322
|
console.log(`Enhanced Commissioning Window for node ${nodeId} opened`);
|
|
307
|
-
const { qrPairingCode, manualPairingCode } = data;
|
|
308
323
|
|
|
309
324
|
console.log(QrCode.get(qrPairingCode));
|
|
310
325
|
console.log(
|
|
@@ -333,11 +348,19 @@ export default function commands(theNode: MatterNode) {
|
|
|
333
348
|
},
|
|
334
349
|
async argv => {
|
|
335
350
|
await theNode.start();
|
|
336
|
-
const { nodeId, force } = argv;
|
|
351
|
+
const { nodeId: nodeIdStr, force } = argv;
|
|
337
352
|
if (force) {
|
|
338
|
-
|
|
353
|
+
// Force: skip the on-device decommission attempt and just drop the peer locally.
|
|
354
|
+
const node = findCommissionedNode(theNode, NodeId(BigInt(nodeIdStr)));
|
|
355
|
+
if (node === undefined) {
|
|
356
|
+
throw new ImplementationError(`Node ${nodeIdStr} not commissioned`);
|
|
357
|
+
}
|
|
358
|
+
await node.delete();
|
|
339
359
|
} else {
|
|
340
|
-
const node = (await theNode.connectAndGetNodes(
|
|
360
|
+
const node = (await theNode.connectAndGetNodes(nodeIdStr, { autoSubscribe: false }))[0];
|
|
361
|
+
if (!(await awaitSeeded(node))) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
341
364
|
await node.decommission();
|
|
342
365
|
}
|
|
343
366
|
},
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { Diagnostic, Seconds } from "@matter/general";
|
|
7
|
+
import { ChannelType, Diagnostic, Seconds } from "@matter/general";
|
|
8
8
|
import { CommissionableDeviceIdentifiers } from "@matter/protocol";
|
|
9
9
|
import { ManualPairingCodeCodec, VendorId } from "@matter/types";
|
|
10
10
|
import type { Argv } from "yargs";
|
|
@@ -88,9 +88,6 @@ export default function commands(theNode: MatterNode) {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
await theNode.start();
|
|
91
|
-
if (theNode.commissioningController === undefined) {
|
|
92
|
-
throw new Error("CommissioningController not initialized");
|
|
93
|
-
}
|
|
94
91
|
|
|
95
92
|
const identifierData: CommissionableDeviceIdentifiers =
|
|
96
93
|
discriminator !== undefined
|
|
@@ -111,25 +108,42 @@ export default function commands(theNode: MatterNode) {
|
|
|
111
108
|
)} for ${once ? "first match or " : ""}${timeoutSeconds} seconds.`,
|
|
112
109
|
);
|
|
113
110
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
111
|
+
// scannerFilter mirrors the discoveryCapabilities->filter mapping in CommissioningDiscovery.
|
|
112
|
+
const discovery = theNode.node.peers.discover({
|
|
113
|
+
...identifierData,
|
|
114
|
+
timeout: Seconds(timeoutSeconds),
|
|
115
|
+
scannerFilter: scanner =>
|
|
116
|
+
scanner.type === ChannelType.UDP || (ble && scanner.type === ChannelType.BLE),
|
|
117
|
+
});
|
|
118
|
+
// A re-advertising device fires `discovered` again for the same node; dedupe by its
|
|
119
|
+
// canonical identity so the log doesn't spam duplicate lines.
|
|
120
|
+
const seen = new Set<string>();
|
|
121
|
+
discovery.discovered.on(node => {
|
|
122
|
+
const { deviceIdentifier, addresses } = node.state.commissioning;
|
|
123
|
+
// Prefer the stable device identifier; the address fallback is sorted so re-advertisement
|
|
124
|
+
// in a different order does not read as a new device (ServerAddress carries no volatile fields).
|
|
125
|
+
const id =
|
|
126
|
+
deviceIdentifier ||
|
|
127
|
+
(addresses ?? [])
|
|
128
|
+
.map(a => JSON.stringify(a))
|
|
129
|
+
.sort()
|
|
130
|
+
.join("|");
|
|
131
|
+
if (seen.has(id)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
seen.add(id);
|
|
135
|
+
console.log(`Discovered device ${Diagnostic.json(node.state.commissioning)}`);
|
|
136
|
+
if (once) {
|
|
137
|
+
discovery.stop();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const results = await discovery;
|
|
131
142
|
|
|
132
|
-
console.log(
|
|
143
|
+
console.log(
|
|
144
|
+
`Discovered ${results.length} devices`,
|
|
145
|
+
results.map(node => node.state.commissioning),
|
|
146
|
+
);
|
|
133
147
|
},
|
|
134
148
|
),
|
|
135
149
|
handler: async (argv: any) => {
|
package/src/shell/cmd_icd.ts
CHANGED
|
@@ -4,22 +4,26 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { Duration, Millis, ObserverGroup } from "@matter/general";
|
|
8
|
-
import { IcdClient, IcdMultiAdminError } from "@matter/node";
|
|
7
|
+
import { Duration, ImplementationError, Millis, ObserverGroup } from "@matter/general";
|
|
8
|
+
import { ClientNode, IcdClient, IcdMultiAdminError, NodeConnectionState } from "@matter/node";
|
|
9
9
|
import { IcdManagementClient } from "@matter/node/behaviors/icd-management";
|
|
10
10
|
import { NodeId, SubjectId, VendorId } from "@matter/types";
|
|
11
11
|
import { IcdManagement } from "@matter/types/clusters/icd-management";
|
|
12
|
-
import { NodeStates, PairedNode } from "@project-chip/matter.js/device";
|
|
13
12
|
import type { Argv } from "yargs";
|
|
14
13
|
import { MatterNode } from "../MatterNode.js";
|
|
14
|
+
import { awaitSeeded } from "../util/awaitSeeded.js";
|
|
15
15
|
|
|
16
16
|
const watchers = new Map<string, ObserverGroup>();
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
/** Look up a commissioned peer by node id without connecting or awaiting remote initialization. */
|
|
19
|
+
function findCommissionedNode(theNode: MatterNode, nodeId: NodeId): ClientNode | undefined {
|
|
20
|
+
return theNode.node.peers.commissioned.find(peer => peer.peerAddress?.nodeId === nodeId);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function printStatus(nodeId: NodeId, clientNode: ClientNode) {
|
|
20
24
|
if (!clientNode.behaviors.has(IcdClient)) {
|
|
21
|
-
if (!
|
|
22
|
-
console.log(`node ${
|
|
25
|
+
if (!clientNode.lifecycle.isSeeded) {
|
|
26
|
+
console.log(`node ${nodeId}: uninitialized (no cached data yet)`);
|
|
23
27
|
}
|
|
24
28
|
return;
|
|
25
29
|
}
|
|
@@ -31,10 +35,10 @@ async function printStatus(paired: PairedNode) {
|
|
|
31
35
|
mgmt === undefined
|
|
32
36
|
? "n/a"
|
|
33
37
|
: (IcdManagement.OperatingMode[mgmt.operatingMode ?? -1] ?? mgmt.operatingMode ?? "unknown");
|
|
34
|
-
const marker =
|
|
38
|
+
const marker = clientNode.lifecycle.isSeeded ? "" : " [uninitialized]";
|
|
35
39
|
console.log(
|
|
36
40
|
[
|
|
37
|
-
`node ${
|
|
41
|
+
`node ${nodeId}:${marker}`,
|
|
38
42
|
` registered=${icd.registered}`,
|
|
39
43
|
` available=${icd.available}`,
|
|
40
44
|
` awake=${awake}`,
|
|
@@ -58,7 +62,12 @@ function selectsAll(nodeIdStr: string) {
|
|
|
58
62
|
/** Resolve a node-id argument to concrete ids: `all` expands to every commissioned node, otherwise a single id. */
|
|
59
63
|
async function targetNodeIds(theNode: MatterNode, nodeIdStr: string): Promise<NodeId[]> {
|
|
60
64
|
await theNode.start();
|
|
61
|
-
|
|
65
|
+
if (!selectsAll(nodeIdStr)) {
|
|
66
|
+
return [NodeId(BigInt(nodeIdStr))];
|
|
67
|
+
}
|
|
68
|
+
return theNode.node.peers.commissioned
|
|
69
|
+
.map(peer => peer.peerAddress?.nodeId)
|
|
70
|
+
.filter((nodeId): nodeId is NodeId => nodeId !== undefined);
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
/**
|
|
@@ -84,19 +93,20 @@ async function eachNode(theNode: MatterNode, nodeIdStr: string, action: (nodeId:
|
|
|
84
93
|
}
|
|
85
94
|
}
|
|
86
95
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
96
|
+
/** Connect to `nodeId` and verify it exposes IcdManagement, throwing an {@link ImplementationError} otherwise. */
|
|
97
|
+
async function connectedIcdNode(theNode: MatterNode, nodeId: NodeId): Promise<ClientNode> {
|
|
98
|
+
const clientNode = (await theNode.connectAndGetNodes(String(nodeId)))[0];
|
|
99
|
+
if (clientNode === undefined) {
|
|
100
|
+
throw new ImplementationError(`Node ${nodeId} not found / not connected`);
|
|
91
101
|
}
|
|
92
|
-
|
|
93
|
-
|
|
102
|
+
// Behaviors populate as part of seeding; await it so a not-yet-seeded peer isn't misread as non-ICD.
|
|
103
|
+
if (!(await awaitSeeded(clientNode, { quiet: true }))) {
|
|
104
|
+
throw new ImplementationError(`Node ${nodeId} did not become ready (offline?)`);
|
|
94
105
|
}
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return (await pairedNodeFor(theNode, nodeId)).node;
|
|
106
|
+
if (!clientNode.behaviors.has(IcdClient)) {
|
|
107
|
+
throw new ImplementationError(`Node ${nodeId} is not an ICD device (no IcdManagement cluster)`);
|
|
108
|
+
}
|
|
109
|
+
return clientNode;
|
|
100
110
|
}
|
|
101
111
|
|
|
102
112
|
function stopWatch(nodeIdStr: string) {
|
|
@@ -120,8 +130,7 @@ async function startWatch(theNode: MatterNode, nodeId: NodeId) {
|
|
|
120
130
|
watchers.get(key)?.close();
|
|
121
131
|
watchers.delete(key);
|
|
122
132
|
|
|
123
|
-
const
|
|
124
|
-
const clientNode = paired.node;
|
|
133
|
+
const clientNode = await connectedIcdNode(theNode, nodeId);
|
|
125
134
|
const observers = new ObserverGroup();
|
|
126
135
|
const stamp = (msg: string) => console.log(`[icd ${nodeId}] ${msg}`);
|
|
127
136
|
// Event emitters discard emit()'s promise, so a rejection from this async read must not escape.
|
|
@@ -145,10 +154,12 @@ async function startWatch(theNode: MatterNode, nodeId: NodeId) {
|
|
|
145
154
|
observers.on(events.checkInMissed, async () => stamp(`check-in MISSED ${await wakefulnessSuffix()}`));
|
|
146
155
|
observers.on(events.keyRefreshed, () => stamp("key refreshed"));
|
|
147
156
|
observers.on(events.available$Changed, (value: boolean) => stamp(`available=${value}`));
|
|
148
|
-
observers.on(
|
|
157
|
+
observers.on(clientNode.lifecycle.connectionStateChanged, state =>
|
|
158
|
+
stamp(`connection=${NodeConnectionState[state] ?? state}`),
|
|
159
|
+
);
|
|
149
160
|
watchers.set(key, observers);
|
|
150
161
|
// Drop the watcher when the node goes away so it doesn't leak in the map.
|
|
151
|
-
clientNode.lifecycle.destroyed
|
|
162
|
+
observers.on(clientNode.lifecycle.destroyed, () => {
|
|
152
163
|
watchers.get(key)?.close();
|
|
153
164
|
watchers.delete(key);
|
|
154
165
|
});
|
|
@@ -198,7 +209,7 @@ export default function commands(theNode: MatterNode) {
|
|
|
198
209
|
ignoredVendors: argv.ignoreVendor?.map((v: string | number) => VendorId(Number(v))),
|
|
199
210
|
};
|
|
200
211
|
await eachNode(theNode, argv.nodeId, async nodeId => {
|
|
201
|
-
const clientNode = await
|
|
212
|
+
const clientNode = await connectedIcdNode(theNode, nodeId);
|
|
202
213
|
try {
|
|
203
214
|
await clientNode.act(agent => agent.get(IcdClient).register(options));
|
|
204
215
|
console.log(`Registered as Check-In client on node ${nodeId}`);
|
|
@@ -235,9 +246,14 @@ export default function commands(theNode: MatterNode) {
|
|
|
235
246
|
if (argv.force) {
|
|
236
247
|
// A registered-but-unreachable LIT peer parks every connecting/peer-I/O path on its
|
|
237
248
|
// never-coming Check-In; reach it via the non-connecting accessor and forget() locally.
|
|
238
|
-
const clientNode = (
|
|
249
|
+
const clientNode = findCommissionedNode(theNode, nodeId);
|
|
250
|
+
if (clientNode === undefined) {
|
|
251
|
+
throw new ImplementationError(`Node ${nodeId} is not commissioned`);
|
|
252
|
+
}
|
|
239
253
|
if (!clientNode.behaviors.has(IcdClient)) {
|
|
240
|
-
throw new
|
|
254
|
+
throw new ImplementationError(
|
|
255
|
+
`Node ${nodeId} is not an ICD device (no IcdManagement cluster)`,
|
|
256
|
+
);
|
|
241
257
|
}
|
|
242
258
|
await clientNode.act(agent => agent.get(IcdClient).forget());
|
|
243
259
|
console.log(
|
|
@@ -245,7 +261,7 @@ export default function commands(theNode: MatterNode) {
|
|
|
245
261
|
);
|
|
246
262
|
return;
|
|
247
263
|
}
|
|
248
|
-
const clientNode = await
|
|
264
|
+
const clientNode = await connectedIcdNode(theNode, nodeId);
|
|
249
265
|
await clientNode.act(agent => agent.get(IcdClient).unregister());
|
|
250
266
|
console.log(`Unregistered Check-In client on node ${nodeId}`);
|
|
251
267
|
});
|
|
@@ -269,7 +285,7 @@ export default function commands(theNode: MatterNode) {
|
|
|
269
285
|
}),
|
|
270
286
|
handler: async (argv: any) => {
|
|
271
287
|
await eachNode(theNode, argv.nodeId, async nodeId => {
|
|
272
|
-
const clientNode = await
|
|
288
|
+
const clientNode = await connectedIcdNode(theNode, nodeId);
|
|
273
289
|
const promised = await clientNode.act(agent =>
|
|
274
290
|
agent.get(IcdClient).stayActive(Millis(argv.durationMs)),
|
|
275
291
|
);
|
|
@@ -283,7 +299,7 @@ export default function commands(theNode: MatterNode) {
|
|
|
283
299
|
builder: (y: Argv) =>
|
|
284
300
|
y.positional("node-id", { describe: "node id, or 'all'", type: "string", demandOption: true }),
|
|
285
301
|
handler: async (argv: any) => {
|
|
286
|
-
//
|
|
302
|
+
// findCommissionedNode never connects/awaits remote init, so a peer stuck mid-read can't hang this command.
|
|
287
303
|
const nodeIds = await targetNodeIds(theNode, argv.nodeId);
|
|
288
304
|
if (nodeIds.length === 0) {
|
|
289
305
|
console.log("No commissioned nodes.");
|
|
@@ -292,7 +308,11 @@ export default function commands(theNode: MatterNode) {
|
|
|
292
308
|
// Diagnostic command: report an unreachable node inline rather than aborting, even for a single id.
|
|
293
309
|
for (const nodeId of nodeIds) {
|
|
294
310
|
try {
|
|
295
|
-
|
|
311
|
+
const clientNode = findCommissionedNode(theNode, nodeId);
|
|
312
|
+
if (clientNode === undefined) {
|
|
313
|
+
throw new ImplementationError(`Node ${nodeId} is not commissioned`);
|
|
314
|
+
}
|
|
315
|
+
await printStatus(nodeId, clientNode);
|
|
296
316
|
} catch (e) {
|
|
297
317
|
console.log(
|
|
298
318
|
`node ${nodeId}: unavailable (${e instanceof Error ? e.message : String(e)})`,
|