@project-chip/matter.js 0.11.0-alpha.0-20240928-08865c2ce → 0.11.0-alpha.0-20241002-e7b377c34
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/cjs/CommissioningController.d.ts +7 -35
- package/dist/cjs/CommissioningController.d.ts.map +1 -1
- package/dist/cjs/CommissioningController.js +2 -1
- package/dist/cjs/CommissioningController.js.map +1 -1
- package/dist/cjs/CommissioningServer.d.ts.map +1 -1
- package/dist/cjs/CommissioningServer.js +2 -2
- package/dist/cjs/CommissioningServer.js.map +1 -1
- package/dist/cjs/MatterController.d.ts +21 -49
- package/dist/cjs/MatterController.d.ts.map +1 -1
- package/dist/cjs/MatterController.js +102 -502
- package/dist/cjs/MatterController.js.map +2 -2
- package/dist/cjs/compat/protocol.d.ts +1 -1
- package/dist/cjs/compat/protocol.d.ts.map +1 -1
- package/dist/cjs/compat/protocol.js +1 -2
- package/dist/cjs/compat/protocol.js.map +1 -1
- package/dist/cjs/device/LegacyInteractionServer.d.ts.map +1 -1
- package/dist/cjs/device/PairedNode.d.ts +1 -2
- package/dist/cjs/device/PairedNode.d.ts.map +1 -1
- package/dist/cjs/device/PairedNode.js +2 -3
- package/dist/cjs/device/PairedNode.js.map +1 -1
- package/dist/esm/CommissioningController.d.ts +7 -35
- package/dist/esm/CommissioningController.d.ts.map +1 -1
- package/dist/esm/CommissioningController.js +2 -1
- package/dist/esm/CommissioningController.js.map +1 -1
- package/dist/esm/CommissioningServer.d.ts.map +1 -1
- package/dist/esm/CommissioningServer.js +2 -2
- package/dist/esm/CommissioningServer.js.map +1 -1
- package/dist/esm/MatterController.d.ts +21 -49
- package/dist/esm/MatterController.d.ts.map +1 -1
- package/dist/esm/MatterController.js +105 -519
- package/dist/esm/MatterController.js.map +2 -2
- package/dist/esm/compat/protocol.d.ts +1 -1
- package/dist/esm/compat/protocol.d.ts.map +1 -1
- package/dist/esm/compat/protocol.js +2 -4
- package/dist/esm/compat/protocol.js.map +1 -1
- package/dist/esm/device/LegacyInteractionServer.d.ts.map +1 -1
- package/dist/esm/device/PairedNode.d.ts +1 -2
- package/dist/esm/device/PairedNode.d.ts.map +1 -1
- package/dist/esm/device/PairedNode.js +1 -1
- package/dist/esm/device/PairedNode.js.map +1 -1
- package/package.json +8 -8
- package/src/CommissioningController.ts +7 -42
- package/src/CommissioningServer.ts +2 -2
- package/src/MatterController.ts +126 -713
- package/src/compat/protocol.ts +2 -3
- package/src/device/PairedNode.ts +1 -1
package/src/MatterController.ts
CHANGED
|
@@ -11,53 +11,39 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { GeneralCommissioning } from "#clusters";
|
|
14
|
+
import { NodeCommissioningOptions } from "#CommissioningController.js";
|
|
14
15
|
import {
|
|
15
16
|
CRYPTO_SYMMETRIC_KEY_LENGTH,
|
|
16
|
-
Channel,
|
|
17
17
|
ChannelType,
|
|
18
18
|
Construction,
|
|
19
19
|
Crypto,
|
|
20
20
|
ImplementationError,
|
|
21
21
|
Logger,
|
|
22
22
|
NetInterfaceSet,
|
|
23
|
-
NoResponseTimeoutError,
|
|
24
|
-
ServerAddress,
|
|
25
23
|
ServerAddressIp,
|
|
26
24
|
StorageBackendMemory,
|
|
27
25
|
StorageContext,
|
|
28
26
|
StorageManager,
|
|
29
27
|
SupportedStorageTypes,
|
|
30
|
-
Time,
|
|
31
|
-
Timer,
|
|
32
|
-
anyPromise,
|
|
33
|
-
createPromise,
|
|
34
|
-
isIPv6,
|
|
35
28
|
serverAddressToString,
|
|
36
29
|
} from "#general";
|
|
37
30
|
import {
|
|
38
|
-
CaseClient,
|
|
39
31
|
ChannelManager,
|
|
40
32
|
ClusterClient,
|
|
41
|
-
CommissionableDevice,
|
|
42
33
|
CommissioningError,
|
|
43
|
-
CommissioningSuccessfullyFinished,
|
|
44
34
|
ControllerCommissioner,
|
|
45
|
-
ControllerCommissioningOptions,
|
|
46
|
-
ControllerDiscovery,
|
|
47
35
|
DiscoveryData,
|
|
48
|
-
|
|
36
|
+
DiscoveryOptions,
|
|
49
37
|
ExchangeManager,
|
|
50
|
-
ExchangeProvider,
|
|
51
38
|
Fabric,
|
|
52
39
|
FabricBuilder,
|
|
53
40
|
FabricJsonObject,
|
|
54
41
|
FabricManager,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
PaseClient,
|
|
42
|
+
NodeDiscoveryType,
|
|
43
|
+
OperationalPeer,
|
|
44
|
+
PeerCommissioningOptions,
|
|
45
|
+
PeerSet,
|
|
46
|
+
PeerStore,
|
|
61
47
|
ResumptionRecord,
|
|
62
48
|
RetransmissionLimitReachedError,
|
|
63
49
|
RootCertificateManager,
|
|
@@ -72,7 +58,6 @@ import {
|
|
|
72
58
|
FabricId,
|
|
73
59
|
FabricIndex,
|
|
74
60
|
NodeId,
|
|
75
|
-
SECURE_CHANNEL_PROTOCOL_ID,
|
|
76
61
|
TlvEnum,
|
|
77
62
|
TlvField,
|
|
78
63
|
TlvObject,
|
|
@@ -81,7 +66,6 @@ import {
|
|
|
81
66
|
TypeFromSchema,
|
|
82
67
|
VendorId,
|
|
83
68
|
} from "#types";
|
|
84
|
-
import { NodeCommissioningOptions } from "./CommissioningController.js";
|
|
85
69
|
|
|
86
70
|
const TlvCommissioningSuccessFailureResponse = TlvObject({
|
|
87
71
|
/** Contain the result of the operation. */
|
|
@@ -98,37 +82,20 @@ export type CommissionedNodeDetails = {
|
|
|
98
82
|
basicInformationData?: Record<string, SupportedStorageTypes>;
|
|
99
83
|
};
|
|
100
84
|
|
|
101
|
-
|
|
102
|
-
discoveryType?: NodeDiscoveryType;
|
|
103
|
-
timeoutSeconds?: number;
|
|
104
|
-
discoveryData?: DiscoveryData;
|
|
105
|
-
};
|
|
106
|
-
|
|
85
|
+
const DEFAULT_ADMIN_VENDOR_ID = VendorId(0xfff1);
|
|
107
86
|
const DEFAULT_FABRIC_INDEX = FabricIndex(1);
|
|
108
87
|
const DEFAULT_FABRIC_ID = FabricId(1);
|
|
109
|
-
const DEFAULT_ADMIN_VENDOR_ID = VendorId(0xfff1);
|
|
110
|
-
|
|
111
|
-
const RECONNECTION_POLLING_INTERVAL_MS = 600_000; // 10 minutes
|
|
112
|
-
const RETRANSMISSION_DISCOVERY_TIMEOUT_MS = 5_000;
|
|
113
88
|
|
|
114
89
|
const CONTROLLER_CONNECTIONS_PER_FABRIC_AND_NODE = 3;
|
|
115
90
|
const CONTROLLER_MAX_PATHS_PER_INVOKE = 10;
|
|
116
91
|
|
|
117
92
|
const logger = Logger.get("MatterController");
|
|
118
93
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
None = 0,
|
|
122
|
-
|
|
123
|
-
/** Retransmission discovery means that we ignore known addresses and start a query for 5s. */
|
|
124
|
-
RetransmissionDiscovery = 1,
|
|
94
|
+
// Operational peer extended with basic information as required for conversion to CommissionedNodeDetails
|
|
95
|
+
type CommissionedPeer = OperationalPeer & { basicInformationData?: Record<string, SupportedStorageTypes> };
|
|
125
96
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
/** Full discovery means that the device is discovered until it is found, excluding known addresses. */
|
|
130
|
-
FullDiscovery = 3,
|
|
131
|
-
}
|
|
97
|
+
// Backward-compatible persistence record for nodes
|
|
98
|
+
type StoredOperationalPeer = [NodeId, CommissionedNodeDetails];
|
|
132
99
|
|
|
133
100
|
export class MatterController {
|
|
134
101
|
public static async create(options: {
|
|
@@ -152,7 +119,7 @@ export class MatterController {
|
|
|
152
119
|
scanners,
|
|
153
120
|
netInterfaces,
|
|
154
121
|
sessionClosedCallback,
|
|
155
|
-
adminVendorId
|
|
122
|
+
adminVendorId,
|
|
156
123
|
adminFabricId = FabricId(DEFAULT_FABRIC_ID),
|
|
157
124
|
adminFabricIndex = FabricIndex(DEFAULT_FABRIC_INDEX),
|
|
158
125
|
caseAuthenticatedTags,
|
|
@@ -172,7 +139,6 @@ export class MatterController {
|
|
|
172
139
|
netInterfaces,
|
|
173
140
|
certificateManager,
|
|
174
141
|
fabric,
|
|
175
|
-
adminVendorId: fabric.rootVendorId,
|
|
176
142
|
sessionClosedCallback,
|
|
177
143
|
});
|
|
178
144
|
} else {
|
|
@@ -182,7 +148,7 @@ export class MatterController {
|
|
|
182
148
|
.setRootCert(certificateManager.rootCert)
|
|
183
149
|
.setRootNodeId(rootNodeId)
|
|
184
150
|
.setIdentityProtectionKey(ipkValue)
|
|
185
|
-
.setRootVendorId(adminVendorId);
|
|
151
|
+
.setRootVendorId(adminVendorId ?? DEFAULT_ADMIN_VENDOR_ID);
|
|
186
152
|
fabricBuilder.setOperationalCert(
|
|
187
153
|
certificateManager.generateNoc(
|
|
188
154
|
fabricBuilder.publicKey,
|
|
@@ -201,7 +167,6 @@ export class MatterController {
|
|
|
201
167
|
netInterfaces,
|
|
202
168
|
certificateManager,
|
|
203
169
|
fabric,
|
|
204
|
-
adminVendorId,
|
|
205
170
|
sessionClosedCallback,
|
|
206
171
|
});
|
|
207
172
|
}
|
|
@@ -245,7 +210,6 @@ export class MatterController {
|
|
|
245
210
|
netInterfaces,
|
|
246
211
|
certificateManager,
|
|
247
212
|
fabric,
|
|
248
|
-
adminVendorId: fabric.rootVendorId,
|
|
249
213
|
sessionClosedCallback,
|
|
250
214
|
});
|
|
251
215
|
await controller.construction;
|
|
@@ -256,27 +220,17 @@ export class MatterController {
|
|
|
256
220
|
private readonly netInterfaces = new NetInterfaceSet();
|
|
257
221
|
private readonly channelManager = new ChannelManager(CONTROLLER_CONNECTIONS_PER_FABRIC_AND_NODE);
|
|
258
222
|
private readonly exchangeManager: ExchangeManager;
|
|
259
|
-
private readonly
|
|
260
|
-
private readonly
|
|
261
|
-
private readonly commissionedNodes = new Map<NodeId, CommissionedNodeDetails>();
|
|
223
|
+
private readonly peers: PeerSet;
|
|
224
|
+
private readonly commissioner: ControllerCommissioner;
|
|
262
225
|
#construction: Construction<MatterController>;
|
|
263
226
|
|
|
264
227
|
readonly sessionStorage: StorageContext;
|
|
265
228
|
readonly fabricStorage?: StorageContext;
|
|
266
|
-
readonly
|
|
229
|
+
readonly nodesStore: CommissionedNodeStore;
|
|
267
230
|
private readonly scanners: ScannerSet;
|
|
268
231
|
private readonly certificateManager: RootCertificateManager;
|
|
269
232
|
private readonly fabric: Fabric;
|
|
270
|
-
private readonly adminVendorId: VendorId;
|
|
271
233
|
private readonly sessionClosedCallback?: (peerNodeId: NodeId) => void;
|
|
272
|
-
readonly #runningNodeDiscoveries = new Map<
|
|
273
|
-
NodeId,
|
|
274
|
-
{
|
|
275
|
-
type: NodeDiscoveryType;
|
|
276
|
-
promises?: (() => Promise<MessageChannel>)[];
|
|
277
|
-
timer?: Timer;
|
|
278
|
-
}
|
|
279
|
-
>();
|
|
280
234
|
|
|
281
235
|
get construction() {
|
|
282
236
|
return this.#construction;
|
|
@@ -290,7 +244,6 @@ export class MatterController {
|
|
|
290
244
|
netInterfaces: NetInterfaceSet;
|
|
291
245
|
certificateManager: RootCertificateManager;
|
|
292
246
|
fabric: Fabric;
|
|
293
|
-
adminVendorId: VendorId;
|
|
294
247
|
sessionClosedCallback?: (peerNodeId: NodeId) => void;
|
|
295
248
|
}) {
|
|
296
249
|
const {
|
|
@@ -302,17 +255,14 @@ export class MatterController {
|
|
|
302
255
|
certificateManager,
|
|
303
256
|
fabric,
|
|
304
257
|
sessionClosedCallback,
|
|
305
|
-
adminVendorId,
|
|
306
258
|
} = options;
|
|
307
259
|
this.sessionStorage = sessionStorage;
|
|
308
260
|
this.fabricStorage = fabricStorage;
|
|
309
|
-
this.nodesStorage = nodesStorage;
|
|
310
261
|
this.scanners = scanners;
|
|
311
262
|
this.netInterfaces = netInterfaces;
|
|
312
263
|
this.certificateManager = certificateManager;
|
|
313
264
|
this.fabric = fabric;
|
|
314
265
|
this.sessionClosedCallback = sessionClosedCallback;
|
|
315
|
-
this.adminVendorId = adminVendorId;
|
|
316
266
|
|
|
317
267
|
const fabricManager = new FabricManager();
|
|
318
268
|
fabricManager.addFabric(fabric);
|
|
@@ -324,12 +274,9 @@ export class MatterController {
|
|
|
324
274
|
maxPathsPerInvoke: CONTROLLER_MAX_PATHS_PER_INVOKE,
|
|
325
275
|
},
|
|
326
276
|
});
|
|
327
|
-
this.paseClient = new PaseClient(this.sessionManager);
|
|
328
|
-
this.caseClient = new CaseClient(this.sessionManager);
|
|
329
277
|
this.sessionManager.sessions.deleted.on(async session => {
|
|
330
278
|
this.sessionClosedCallback?.(session.peerNodeId);
|
|
331
279
|
});
|
|
332
|
-
this.sessionManager.resubmissionStarted.on(this.#handleResubmissionStarted.bind(this));
|
|
333
280
|
|
|
334
281
|
this.exchangeManager = new ExchangeManager({
|
|
335
282
|
sessionManager: this.sessionManager,
|
|
@@ -338,17 +285,29 @@ export class MatterController {
|
|
|
338
285
|
});
|
|
339
286
|
this.exchangeManager.addProtocolHandler(new StatusReportOnlySecureChannelProtocol());
|
|
340
287
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
288
|
+
// Adapts the historical storage format for MatterController to OperationalPeer objects
|
|
289
|
+
this.nodesStore = new CommissionedNodeStore(nodesStorage, fabric);
|
|
290
|
+
|
|
291
|
+
this.nodesStore.peers = this.peers = new PeerSet({
|
|
292
|
+
sessions: this.sessionManager,
|
|
293
|
+
channels: this.channelManager,
|
|
294
|
+
exchanges: this.exchangeManager,
|
|
295
|
+
scanners: this.scanners,
|
|
296
|
+
netInterfaces: this.netInterfaces,
|
|
297
|
+
store: this.nodesStore,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
this.commissioner = new ControllerCommissioner({
|
|
301
|
+
peers: this.peers,
|
|
302
|
+
scanners: this.scanners,
|
|
303
|
+
netInterfaces: this.netInterfaces,
|
|
304
|
+
exchanges: this.exchangeManager,
|
|
305
|
+
sessions: this.sessionManager,
|
|
306
|
+
certificates: this.certificateManager,
|
|
307
|
+
});
|
|
351
308
|
|
|
309
|
+
this.#construction = Construction(this, async () => {
|
|
310
|
+
await this.peers.construction.ready;
|
|
352
311
|
await this.sessionManager.construction.ready;
|
|
353
312
|
});
|
|
354
313
|
}
|
|
@@ -394,267 +353,35 @@ export class MatterController {
|
|
|
394
353
|
options: NodeCommissioningOptions,
|
|
395
354
|
completeCommissioningCallback?: (peerNodeId: NodeId, discoveryData?: DiscoveryData) => Promise<boolean>,
|
|
396
355
|
): Promise<NodeId> {
|
|
397
|
-
const {
|
|
398
|
-
commissioning
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
passcode,
|
|
404
|
-
} = options;
|
|
405
|
-
const commissionableDevice =
|
|
406
|
-
"commissionableDevice" in options.discovery ? options.discovery.commissionableDevice : undefined;
|
|
407
|
-
let {
|
|
408
|
-
discovery: { discoveryCapabilities = {}, knownAddress },
|
|
409
|
-
} = options;
|
|
410
|
-
let identifierData = "identifierData" in options.discovery ? options.discovery.identifierData : {};
|
|
411
|
-
|
|
412
|
-
if (
|
|
413
|
-
this.scanners.hasScannerFor(ChannelType.UDP) &&
|
|
414
|
-
this.netInterfaces.hasInterfaceFor(ChannelType.UDP, "::") !== undefined
|
|
415
|
-
) {
|
|
416
|
-
discoveryCapabilities.onIpNetwork = true; // We always discover on network as defined by specs
|
|
417
|
-
}
|
|
418
|
-
if (commissionableDevice !== undefined) {
|
|
419
|
-
let { addresses } = commissionableDevice;
|
|
420
|
-
if (discoveryCapabilities.ble === true) {
|
|
421
|
-
discoveryCapabilities = { onIpNetwork: true, ble: addresses.some(address => address.type === "ble") };
|
|
422
|
-
} else if (discoveryCapabilities.onIpNetwork === true) {
|
|
423
|
-
// do not use BLE if not specified, even if existing
|
|
424
|
-
addresses = addresses.filter(address => address.type !== "ble");
|
|
425
|
-
}
|
|
426
|
-
addresses.sort(a => (a.type === "udp" ? -1 : 1)); // Sort addresses to use UDP first
|
|
427
|
-
knownAddress = addresses[0];
|
|
428
|
-
if ("instanceId" in commissionableDevice && commissionableDevice.instanceId !== undefined) {
|
|
429
|
-
// it is an UDP discovery
|
|
430
|
-
identifierData = { instanceId: commissionableDevice.instanceId as string };
|
|
431
|
-
} else {
|
|
432
|
-
identifierData = { longDiscriminator: commissionableDevice.D };
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const scannersToUse = this.collectScanners(discoveryCapabilities);
|
|
437
|
-
|
|
438
|
-
logger.info(
|
|
439
|
-
`Commissioning device with identifier ${Logger.toJSON(identifierData)} and ${
|
|
440
|
-
scannersToUse.length
|
|
441
|
-
} scanners and knownAddress ${Logger.toJSON(knownAddress)}`,
|
|
442
|
-
);
|
|
443
|
-
|
|
444
|
-
// If we have a known address we try this first before we discover the device
|
|
445
|
-
let paseSecureChannel: MessageChannel | undefined;
|
|
446
|
-
let discoveryData: DiscoveryData | undefined;
|
|
356
|
+
const commissioningOptions: PeerCommissioningOptions = {
|
|
357
|
+
...options.commissioning,
|
|
358
|
+
fabric: this.fabric,
|
|
359
|
+
discovery: options.discovery,
|
|
360
|
+
passcode: options.passcode,
|
|
361
|
+
};
|
|
447
362
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
363
|
+
if (completeCommissioningCallback) {
|
|
364
|
+
commissioningOptions.performCaseCommissioning = async (peerAddress, discoveryData) => {
|
|
365
|
+
const result = await completeCommissioningCallback(peerAddress.nodeId, discoveryData);
|
|
366
|
+
if (!result) {
|
|
367
|
+
throw new RetransmissionLimitReachedError("Device could not be discovered");
|
|
368
|
+
}
|
|
369
|
+
};
|
|
455
370
|
}
|
|
456
|
-
if (paseSecureChannel === undefined) {
|
|
457
|
-
const discoveredDevices = await ControllerDiscovery.discoverDeviceAddressesByIdentifier(
|
|
458
|
-
scannersToUse,
|
|
459
|
-
identifierData,
|
|
460
|
-
timeoutSeconds,
|
|
461
|
-
);
|
|
462
371
|
|
|
463
|
-
|
|
464
|
-
discoveredDevices,
|
|
465
|
-
NoResponseTimeoutError,
|
|
466
|
-
async () =>
|
|
467
|
-
scannersToUse.flatMap(scanner => scanner.getDiscoveredCommissionableDevices(identifierData)),
|
|
468
|
-
async (address, device) => {
|
|
469
|
-
const channel = await this.initializePaseSecureChannel(address, passcode, device);
|
|
470
|
-
discoveryData = device;
|
|
471
|
-
return channel;
|
|
472
|
-
},
|
|
473
|
-
);
|
|
372
|
+
const address = await this.commissioner.commission(commissioningOptions);
|
|
474
373
|
|
|
475
|
-
|
|
476
|
-
paseSecureChannel = result;
|
|
477
|
-
}
|
|
374
|
+
await this.fabricStorage?.set("fabric", this.fabric.toStorageObject());
|
|
478
375
|
|
|
479
|
-
return
|
|
480
|
-
paseSecureChannel,
|
|
481
|
-
commissioningOptions,
|
|
482
|
-
discoveryData,
|
|
483
|
-
completeCommissioningCallback,
|
|
484
|
-
);
|
|
376
|
+
return address.nodeId;
|
|
485
377
|
}
|
|
486
378
|
|
|
487
379
|
async disconnect(nodeId: NodeId) {
|
|
488
|
-
|
|
489
|
-
await this.channelManager.removeAllNodeChannels(this.fabric, nodeId);
|
|
380
|
+
return this.peers.disconnect(this.fabric.addressOf(nodeId));
|
|
490
381
|
}
|
|
491
382
|
|
|
492
383
|
async removeNode(nodeId: NodeId) {
|
|
493
|
-
|
|
494
|
-
await this.sessionManager.removeAllSessionsForNode(nodeId);
|
|
495
|
-
await this.sessionManager.removeResumptionRecord(nodeId);
|
|
496
|
-
await this.channelManager.removeAllNodeChannels(this.fabric, nodeId);
|
|
497
|
-
this.commissionedNodes.delete(nodeId);
|
|
498
|
-
await this.storeCommissionedNodes();
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* Method to start commission process with a PASE pairing.
|
|
503
|
-
* If this not successful and throws an RetransmissionLimitReachedError the address is invalid or the passcode
|
|
504
|
-
* is wrong.
|
|
505
|
-
*/
|
|
506
|
-
private async initializePaseSecureChannel(
|
|
507
|
-
address: ServerAddress,
|
|
508
|
-
passcode: number,
|
|
509
|
-
device?: CommissionableDevice,
|
|
510
|
-
): Promise<MessageChannel> {
|
|
511
|
-
let paseChannel: Channel<Uint8Array>;
|
|
512
|
-
if (device !== undefined) {
|
|
513
|
-
logger.info(`Commissioning device`, MdnsScanner.discoveryDataDiagnostics(device));
|
|
514
|
-
}
|
|
515
|
-
if (address.type === "udp") {
|
|
516
|
-
const { ip } = address;
|
|
517
|
-
|
|
518
|
-
const isIpv6Address = isIPv6(ip);
|
|
519
|
-
const paseInterface = this.netInterfaces.interfaceFor(ChannelType.UDP, isIpv6Address ? "::" : "0.0.0.0");
|
|
520
|
-
if (paseInterface === undefined) {
|
|
521
|
-
// mainly IPv6 address when IPv4 is disabled
|
|
522
|
-
throw new PairRetransmissionLimitReachedError(
|
|
523
|
-
`IPv${isIpv6Address ? "6" : "4"} interface not initialized. Cannot use ${ip} for commissioning.`,
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
|
-
paseChannel = await paseInterface.openChannel(address);
|
|
527
|
-
} else {
|
|
528
|
-
const ble = this.netInterfaces.interfaceFor(ChannelType.BLE);
|
|
529
|
-
if (!ble) {
|
|
530
|
-
throw new PairRetransmissionLimitReachedError(
|
|
531
|
-
`BLE interface not initialized. Cannot use ${address.peripheralAddress} for commissioning.`,
|
|
532
|
-
);
|
|
533
|
-
}
|
|
534
|
-
// TODO Have a Timeout mechanism here for connections
|
|
535
|
-
paseChannel = await ble.openChannel(address);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Do PASE paring
|
|
539
|
-
const unsecureSession = this.sessionManager.createInsecureSession({
|
|
540
|
-
// Use the session parameters from MDNS announcements when available and rest is assumed to be fallbacks
|
|
541
|
-
sessionParameters: {
|
|
542
|
-
idleIntervalMs: device?.SII,
|
|
543
|
-
activeIntervalMs: device?.SAI,
|
|
544
|
-
activeThresholdMs: device?.SAT,
|
|
545
|
-
},
|
|
546
|
-
isInitiator: true,
|
|
547
|
-
});
|
|
548
|
-
const paseUnsecureMessageChannel = new MessageChannel(paseChannel, unsecureSession);
|
|
549
|
-
const paseExchange = this.exchangeManager.initiateExchangeWithChannel(
|
|
550
|
-
paseUnsecureMessageChannel,
|
|
551
|
-
SECURE_CHANNEL_PROTOCOL_ID,
|
|
552
|
-
);
|
|
553
|
-
|
|
554
|
-
let paseSecureSession;
|
|
555
|
-
try {
|
|
556
|
-
paseSecureSession = await this.paseClient.pair(
|
|
557
|
-
this.sessionManager.sessionParameters,
|
|
558
|
-
paseExchange,
|
|
559
|
-
passcode,
|
|
560
|
-
);
|
|
561
|
-
} catch (e) {
|
|
562
|
-
// Close the exchange and rethrow
|
|
563
|
-
await paseExchange.close();
|
|
564
|
-
throw e;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
await unsecureSession.destroy();
|
|
568
|
-
return new MessageChannel(paseChannel, paseSecureSession);
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/**
|
|
572
|
-
* Method to commission a device with a PASE secure channel. It returns the NodeId of the commissioned device on
|
|
573
|
-
* success.
|
|
574
|
-
*/
|
|
575
|
-
private async commissionDevice(
|
|
576
|
-
paseSecureMessageChannel: MessageChannel,
|
|
577
|
-
commissioningOptions: ControllerCommissioningOptions,
|
|
578
|
-
discoveryData?: DiscoveryData,
|
|
579
|
-
completeCommissioningCallback?: (peerNodeId: NodeId, discoveryData?: DiscoveryData) => Promise<boolean>,
|
|
580
|
-
): Promise<NodeId> {
|
|
581
|
-
// TODO: Create the fabric only when needed before commissioning (to do when refactoring MatterController away)
|
|
582
|
-
// TODO also move certificateManager and other parts into that class to get rid of them here
|
|
583
|
-
// TODO Depending on the Error type during commissioning we can do a retry ...
|
|
584
|
-
/*
|
|
585
|
-
Whenever the Fail-Safe timer is armed, Commissioners and Administrators SHALL NOT consider any cluster
|
|
586
|
-
operation to have timed-out before waiting at least 30 seconds for a valid response from the cluster server.
|
|
587
|
-
Some commands and attributes with complex side-effects MAY require longer and have specific timing requirements
|
|
588
|
-
stated in their respective cluster specification.
|
|
589
|
-
|
|
590
|
-
In concurrent connection commissioning flow, the failure of any of the steps 2 through 10 SHALL result in the
|
|
591
|
-
Commissioner and Commissionee returning to step 2 (device discovery and commissioning channel establishment) and
|
|
592
|
-
repeating each step. The failure of any of the steps 11 through 15 in concurrent connection commissioning flow
|
|
593
|
-
SHALL result in the Commissioner and Commissionee returning to step 11 (configuration of operational network
|
|
594
|
-
information). In the case of failure of any of the steps 11 through 15 in concurrent connection commissioning
|
|
595
|
-
flow, the Commissioner and Commissionee SHALL reuse the existing PASE-derived encryption keys over the
|
|
596
|
-
commissioning channel and all steps up to and including step 10 are considered to have been successfully
|
|
597
|
-
completed.
|
|
598
|
-
In non-concurrent connection commissioning flow, the failure of any of the steps 2 through 15 SHALL result in
|
|
599
|
-
the Commissioner and Commissionee returning to step 2 (device discovery and commissioning channel establishment)
|
|
600
|
-
and repeating each step.
|
|
601
|
-
|
|
602
|
-
Commissioners that need to restart from step 2 MAY immediately expire the fail-safe by invoking the ArmFailSafe
|
|
603
|
-
command with an ExpiryLengthSeconds field set to 0. Otherwise, Commissioners will need to wait until the current
|
|
604
|
-
fail-safe timer has expired for the Commissionee to begin accepting PASE again.
|
|
605
|
-
In both concurrent connection commissioning flow and non-concurrent connection commissioning flow, the
|
|
606
|
-
Commissionee SHALL exit Commissioning Mode after 20 failed attempts.
|
|
607
|
-
*/
|
|
608
|
-
|
|
609
|
-
const peerNodeId = commissioningOptions.nodeId ?? NodeId.randomOperationalNodeId();
|
|
610
|
-
const commissioningManager = new ControllerCommissioner(
|
|
611
|
-
// Use the created secure session to do the commissioning
|
|
612
|
-
new InteractionClient(new ExchangeProvider(this.exchangeManager, paseSecureMessageChannel), peerNodeId),
|
|
613
|
-
this.certificateManager,
|
|
614
|
-
this.fabric,
|
|
615
|
-
commissioningOptions,
|
|
616
|
-
peerNodeId,
|
|
617
|
-
this.adminVendorId,
|
|
618
|
-
async () => {
|
|
619
|
-
// TODO Right now we always close after step 12 because we do not check for commissioning flow requirements
|
|
620
|
-
/*
|
|
621
|
-
In concurrent connection commissioning flow the commissioning channel SHALL terminate after
|
|
622
|
-
successful step 15 (CommissioningComplete command invocation). In non-concurrent connection
|
|
623
|
-
commissioning flow the commissioning channel SHALL terminate after successful step 12 (trigger
|
|
624
|
-
joining of operational network at Commissionee). The PASE-derived encryption keys SHALL be deleted
|
|
625
|
-
when commissioning channel terminates. The PASE session SHALL be terminated by both Commissioner and
|
|
626
|
-
Commissionee once the CommissioningComplete command is received by the Commissionee.
|
|
627
|
-
*/
|
|
628
|
-
await paseSecureMessageChannel.close(); // We reconnect using Case, so close PASE connection
|
|
629
|
-
|
|
630
|
-
if (completeCommissioningCallback !== undefined) {
|
|
631
|
-
if (!(await completeCommissioningCallback(peerNodeId, discoveryData))) {
|
|
632
|
-
throw new RetransmissionLimitReachedError("Device could not be discovered");
|
|
633
|
-
}
|
|
634
|
-
throw new CommissioningSuccessfullyFinished();
|
|
635
|
-
}
|
|
636
|
-
// Look for the device broadcast over MDNS and do CASE pairing
|
|
637
|
-
return await this.connect(peerNodeId, {
|
|
638
|
-
discoveryType: NodeDiscoveryType.TimedDiscovery,
|
|
639
|
-
timeoutSeconds: 120,
|
|
640
|
-
discoveryData,
|
|
641
|
-
}); // Wait maximum 120s to find the operational device for commissioning process
|
|
642
|
-
},
|
|
643
|
-
);
|
|
644
|
-
|
|
645
|
-
try {
|
|
646
|
-
await commissioningManager.executeCommissioning();
|
|
647
|
-
} catch (error) {
|
|
648
|
-
if (this.commissionedNodes.has(peerNodeId)) {
|
|
649
|
-
// We might have added data for an operational address that we need to cleanup
|
|
650
|
-
this.commissionedNodes.delete(peerNodeId);
|
|
651
|
-
}
|
|
652
|
-
throw error;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
await this.fabricStorage?.set("fabric", this.fabric.toStorageObject());
|
|
656
|
-
|
|
657
|
-
return peerNodeId;
|
|
384
|
+
return this.peers.delete(this.fabric.addressOf(nodeId));
|
|
658
385
|
}
|
|
659
386
|
|
|
660
387
|
/**
|
|
@@ -676,357 +403,45 @@ export class MatterController {
|
|
|
676
403
|
useExtendedFailSafeMessageResponseTimeout: true,
|
|
677
404
|
});
|
|
678
405
|
if (errorCode !== GeneralCommissioning.CommissioningError.Ok) {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
this.commissionedNodes.delete(peerNodeId);
|
|
682
|
-
}
|
|
406
|
+
// We might have added data for an operational address that we need to cleanup
|
|
407
|
+
await this.peers.delete(this.fabric.addressOf(peerNodeId));
|
|
683
408
|
throw new CommissioningError(`Commission error on commissioningComplete: ${errorCode}, ${debugText}`);
|
|
684
409
|
}
|
|
685
410
|
await this.fabricStorage?.set("fabric", this.fabric.toStorageObject());
|
|
686
411
|
}
|
|
687
412
|
|
|
688
|
-
#handleResubmissionStarted(peerNodeId?: NodeId) {
|
|
689
|
-
if (peerNodeId === undefined) {
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
if (this.#runningNodeDiscoveries.has(peerNodeId)) {
|
|
693
|
-
// We already discover for this node, so we do not need to start a new discovery
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
this.#runningNodeDiscoveries.set(peerNodeId, { type: NodeDiscoveryType.RetransmissionDiscovery });
|
|
697
|
-
this.scanners
|
|
698
|
-
.scannerFor(ChannelType.UDP)
|
|
699
|
-
?.findOperationalDevice(this.fabric, peerNodeId, RETRANSMISSION_DISCOVERY_TIMEOUT_MS, true)
|
|
700
|
-
.catch(error => {
|
|
701
|
-
logger.error(`Failed to discover device ${peerNodeId} after resubmission started.`, error);
|
|
702
|
-
})
|
|
703
|
-
.finally(() => {
|
|
704
|
-
if (this.#runningNodeDiscoveries.get(peerNodeId)?.type === NodeDiscoveryType.RetransmissionDiscovery) {
|
|
705
|
-
this.#runningNodeDiscoveries.delete(peerNodeId);
|
|
706
|
-
}
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
private async reconnectKnownAddress(
|
|
711
|
-
peerNodeId: NodeId,
|
|
712
|
-
operationalAddress: ServerAddressIp,
|
|
713
|
-
discoveryData?: DiscoveryData,
|
|
714
|
-
expectedProcessingTimeMs?: number,
|
|
715
|
-
): Promise<MessageChannel | undefined> {
|
|
716
|
-
const { ip, port } = operationalAddress;
|
|
717
|
-
try {
|
|
718
|
-
logger.debug(
|
|
719
|
-
`Resume device connection to configured server at ${ip}:${port}${expectedProcessingTimeMs !== undefined ? ` with expected processing time of ${expectedProcessingTimeMs}ms` : ""} ...`,
|
|
720
|
-
);
|
|
721
|
-
const channel = await this.pair(peerNodeId, operationalAddress, discoveryData, expectedProcessingTimeMs);
|
|
722
|
-
await this.setOperationalDeviceData(peerNodeId, operationalAddress);
|
|
723
|
-
return channel;
|
|
724
|
-
} catch (error) {
|
|
725
|
-
if (error instanceof NoResponseTimeoutError) {
|
|
726
|
-
logger.debug(
|
|
727
|
-
`Failed to resume connection to node ${peerNodeId} connection with ${ip}:${port}, discover the device ...`,
|
|
728
|
-
error,
|
|
729
|
-
);
|
|
730
|
-
// We remove all sessions, this also informs the PairedNode class
|
|
731
|
-
await this.sessionManager.removeAllSessionsForNode(peerNodeId);
|
|
732
|
-
return undefined;
|
|
733
|
-
} else {
|
|
734
|
-
throw error;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
private async connectOrDiscoverNode(
|
|
740
|
-
peerNodeId: NodeId,
|
|
741
|
-
operationalAddress?: ServerAddressIp,
|
|
742
|
-
discoveryOptions: DiscoveryOptions = {},
|
|
743
|
-
) {
|
|
744
|
-
const {
|
|
745
|
-
discoveryType: requestedDiscoveryType = NodeDiscoveryType.FullDiscovery,
|
|
746
|
-
timeoutSeconds,
|
|
747
|
-
discoveryData = this.commissionedNodes.get(peerNodeId)?.discoveryData,
|
|
748
|
-
} = discoveryOptions;
|
|
749
|
-
if (timeoutSeconds !== undefined && requestedDiscoveryType !== NodeDiscoveryType.TimedDiscovery) {
|
|
750
|
-
throw new ImplementationError("Cannot set timeout without timed discovery.");
|
|
751
|
-
}
|
|
752
|
-
if (requestedDiscoveryType === NodeDiscoveryType.RetransmissionDiscovery) {
|
|
753
|
-
throw new ImplementationError("Cannot set retransmission discovery type.");
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
const mdnsScanner = this.scanners.scannerFor(ChannelType.UDP) as MdnsScanner | undefined;
|
|
757
|
-
if (!mdnsScanner) {
|
|
758
|
-
throw new ImplementationError("Cannot discover device without mDNS scanner.");
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
const existingDiscoveryDetails = this.#runningNodeDiscoveries.get(peerNodeId) ?? {
|
|
762
|
-
type: NodeDiscoveryType.None,
|
|
763
|
-
};
|
|
764
|
-
|
|
765
|
-
// If we currently run another "lower" retransmission type we cancel it
|
|
766
|
-
if (
|
|
767
|
-
existingDiscoveryDetails.type !== NodeDiscoveryType.None &&
|
|
768
|
-
existingDiscoveryDetails.type < requestedDiscoveryType
|
|
769
|
-
) {
|
|
770
|
-
mdnsScanner.cancelOperationalDeviceDiscovery(this.fabric, peerNodeId);
|
|
771
|
-
this.#runningNodeDiscoveries.delete(peerNodeId);
|
|
772
|
-
existingDiscoveryDetails.type = NodeDiscoveryType.None;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
const { type: runningDiscoveryType, promises } = existingDiscoveryDetails;
|
|
776
|
-
|
|
777
|
-
// If we have a last known address try to reach the device directly when we are not already discovering
|
|
778
|
-
// In worst case parallel cases we do this step twice, but that's ok
|
|
779
|
-
if (
|
|
780
|
-
operationalAddress !== undefined &&
|
|
781
|
-
(runningDiscoveryType === NodeDiscoveryType.None || requestedDiscoveryType === NodeDiscoveryType.None)
|
|
782
|
-
) {
|
|
783
|
-
const directReconnection = await this.reconnectKnownAddress(peerNodeId, operationalAddress, discoveryData);
|
|
784
|
-
if (directReconnection !== undefined) {
|
|
785
|
-
return directReconnection;
|
|
786
|
-
}
|
|
787
|
-
if (requestedDiscoveryType === NodeDiscoveryType.None) {
|
|
788
|
-
throw new DiscoveryError(`Node ${peerNodeId} is not reachable right now.`);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
if (promises !== undefined) {
|
|
793
|
-
if (runningDiscoveryType > requestedDiscoveryType) {
|
|
794
|
-
// We already run a "longer" discovery, so we know it is unreachable for now
|
|
795
|
-
throw new DiscoveryError(
|
|
796
|
-
`Node ${peerNodeId} is not reachable right now and discovery already running.`,
|
|
797
|
-
);
|
|
798
|
-
} else {
|
|
799
|
-
// If we are already discovering this node, so we reuse promises
|
|
800
|
-
return await anyPromise(promises);
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
const discoveryPromises = new Array<() => Promise<MessageChannel>>();
|
|
805
|
-
let reconnectionPollingTimer: Timer | undefined;
|
|
806
|
-
|
|
807
|
-
if (operationalAddress !== undefined) {
|
|
808
|
-
// Additionally to general discovery we also try to poll the formerly known operational address
|
|
809
|
-
if (requestedDiscoveryType === NodeDiscoveryType.FullDiscovery) {
|
|
810
|
-
const { promise, resolver, rejecter } = createPromise<MessageChannel>();
|
|
811
|
-
|
|
812
|
-
reconnectionPollingTimer = Time.getPeriodicTimer(
|
|
813
|
-
"Controller reconnect",
|
|
814
|
-
RECONNECTION_POLLING_INTERVAL_MS,
|
|
815
|
-
async () => {
|
|
816
|
-
try {
|
|
817
|
-
logger.debug(`Polling for device at ${serverAddressToString(operationalAddress)} ...`);
|
|
818
|
-
const result = await this.reconnectKnownAddress(
|
|
819
|
-
peerNodeId,
|
|
820
|
-
operationalAddress,
|
|
821
|
-
discoveryData,
|
|
822
|
-
);
|
|
823
|
-
if (result !== undefined && reconnectionPollingTimer?.isRunning) {
|
|
824
|
-
reconnectionPollingTimer?.stop();
|
|
825
|
-
mdnsScanner.cancelOperationalDeviceDiscovery(this.fabric, peerNodeId);
|
|
826
|
-
this.#runningNodeDiscoveries.delete(peerNodeId);
|
|
827
|
-
resolver(result);
|
|
828
|
-
}
|
|
829
|
-
} catch (error) {
|
|
830
|
-
if (reconnectionPollingTimer?.isRunning) {
|
|
831
|
-
reconnectionPollingTimer?.stop();
|
|
832
|
-
mdnsScanner.cancelOperationalDeviceDiscovery(this.fabric, peerNodeId);
|
|
833
|
-
this.#runningNodeDiscoveries.delete(peerNodeId);
|
|
834
|
-
rejecter(error);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
},
|
|
838
|
-
).start();
|
|
839
|
-
|
|
840
|
-
discoveryPromises.push(() => promise);
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
discoveryPromises.push(async () => {
|
|
845
|
-
const scanResult = await ControllerDiscovery.discoverOperationalDevice(
|
|
846
|
-
this.fabric,
|
|
847
|
-
peerNodeId,
|
|
848
|
-
mdnsScanner,
|
|
849
|
-
timeoutSeconds,
|
|
850
|
-
timeoutSeconds === undefined,
|
|
851
|
-
);
|
|
852
|
-
const { timer } = this.#runningNodeDiscoveries.get(peerNodeId) ?? {};
|
|
853
|
-
timer?.stop();
|
|
854
|
-
this.#runningNodeDiscoveries.delete(peerNodeId);
|
|
855
|
-
|
|
856
|
-
const { result } = await ControllerDiscovery.iterateServerAddresses(
|
|
857
|
-
[scanResult],
|
|
858
|
-
NoResponseTimeoutError,
|
|
859
|
-
async () => {
|
|
860
|
-
const device = mdnsScanner.getDiscoveredOperationalDevice(this.fabric, peerNodeId);
|
|
861
|
-
return device !== undefined ? [device] : [];
|
|
862
|
-
},
|
|
863
|
-
async (address, device) => {
|
|
864
|
-
const result = await this.pair(peerNodeId, address, device);
|
|
865
|
-
await this.setOperationalDeviceData(peerNodeId, address, {
|
|
866
|
-
...discoveryData,
|
|
867
|
-
...device,
|
|
868
|
-
});
|
|
869
|
-
return result;
|
|
870
|
-
},
|
|
871
|
-
);
|
|
872
|
-
|
|
873
|
-
return result;
|
|
874
|
-
});
|
|
875
|
-
|
|
876
|
-
this.#runningNodeDiscoveries.set(peerNodeId, {
|
|
877
|
-
type: requestedDiscoveryType,
|
|
878
|
-
promises: discoveryPromises,
|
|
879
|
-
timer: reconnectionPollingTimer,
|
|
880
|
-
});
|
|
881
|
-
|
|
882
|
-
return await anyPromise(discoveryPromises).finally(() => this.#runningNodeDiscoveries.delete(peerNodeId));
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
/**
|
|
886
|
-
* Resume a device connection and establish a CASE session that was previously paired with the controller. This
|
|
887
|
-
* method will try to connect to the device using the previously used server address (if set). If that fails, the
|
|
888
|
-
* device is discovered again using its operational instance details.
|
|
889
|
-
* It returns the operational MessageChannel on success.
|
|
890
|
-
*/
|
|
891
|
-
private async resume(peerNodeId: NodeId, discoveryOptions?: DiscoveryOptions) {
|
|
892
|
-
const operationalAddress = this.getLastOperationalAddress(peerNodeId);
|
|
893
|
-
|
|
894
|
-
try {
|
|
895
|
-
return await this.connectOrDiscoverNode(peerNodeId, operationalAddress, discoveryOptions);
|
|
896
|
-
} catch (error) {
|
|
897
|
-
if (
|
|
898
|
-
(error instanceof DiscoveryError || error instanceof NoResponseTimeoutError) &&
|
|
899
|
-
this.commissionedNodes.has(peerNodeId)
|
|
900
|
-
) {
|
|
901
|
-
logger.info(`Resume failed, remove all sessions for node ${peerNodeId}`);
|
|
902
|
-
// We remove all sessions, this also informs the PairedNode class
|
|
903
|
-
await this.sessionManager.removeAllSessionsForNode(peerNodeId);
|
|
904
|
-
}
|
|
905
|
-
throw error;
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
/** Pair with an operational device (already commissioned) and establish a CASE session. */
|
|
910
|
-
private async pair(
|
|
911
|
-
peerNodeId: NodeId,
|
|
912
|
-
operationalServerAddress: ServerAddressIp,
|
|
913
|
-
discoveryData?: DiscoveryData,
|
|
914
|
-
expectedProcessingTimeMs?: number,
|
|
915
|
-
) {
|
|
916
|
-
const { ip, port } = operationalServerAddress;
|
|
917
|
-
// Do CASE pairing
|
|
918
|
-
const isIpv6Address = isIPv6(ip);
|
|
919
|
-
const operationalInterface = this.netInterfaces.interfaceFor(ChannelType.UDP, isIpv6Address ? "::" : "0.0.0.0");
|
|
920
|
-
|
|
921
|
-
if (operationalInterface === undefined) {
|
|
922
|
-
throw new PairRetransmissionLimitReachedError(
|
|
923
|
-
`IPv${
|
|
924
|
-
isIpv6Address ? "6" : "4"
|
|
925
|
-
} interface not initialized for port ${port}. Cannot use ${ip} for pairing.`,
|
|
926
|
-
);
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
const operationalChannel = await operationalInterface.openChannel(operationalServerAddress);
|
|
930
|
-
const { sessionParameters } = this.findResumptionRecordByNodeId(peerNodeId) ?? {};
|
|
931
|
-
const unsecureSession = this.sessionManager.createInsecureSession({
|
|
932
|
-
// Use the session parameters from MDNS announcements when available and rest is assumed to be fallbacks
|
|
933
|
-
sessionParameters: {
|
|
934
|
-
idleIntervalMs: discoveryData?.SII ?? sessionParameters?.idleIntervalMs,
|
|
935
|
-
activeIntervalMs: discoveryData?.SAI ?? sessionParameters?.activeIntervalMs,
|
|
936
|
-
activeThresholdMs: discoveryData?.SAT ?? sessionParameters?.activeThresholdMs,
|
|
937
|
-
},
|
|
938
|
-
isInitiator: true,
|
|
939
|
-
});
|
|
940
|
-
const operationalUnsecureMessageExchange = new MessageChannel(operationalChannel, unsecureSession);
|
|
941
|
-
let operationalSecureSession;
|
|
942
|
-
try {
|
|
943
|
-
const exchange = this.exchangeManager.initiateExchangeWithChannel(
|
|
944
|
-
operationalUnsecureMessageExchange,
|
|
945
|
-
SECURE_CHANNEL_PROTOCOL_ID,
|
|
946
|
-
);
|
|
947
|
-
|
|
948
|
-
try {
|
|
949
|
-
operationalSecureSession = await this.caseClient.pair(
|
|
950
|
-
exchange,
|
|
951
|
-
this.fabric,
|
|
952
|
-
peerNodeId,
|
|
953
|
-
expectedProcessingTimeMs,
|
|
954
|
-
);
|
|
955
|
-
} catch (e) {
|
|
956
|
-
await exchange.close();
|
|
957
|
-
throw e;
|
|
958
|
-
}
|
|
959
|
-
} catch (e) {
|
|
960
|
-
NoResponseTimeoutError.accept(e);
|
|
961
|
-
|
|
962
|
-
// Convert error
|
|
963
|
-
throw new PairRetransmissionLimitReachedError(e.message);
|
|
964
|
-
}
|
|
965
|
-
await unsecureSession.destroy();
|
|
966
|
-
const channel = new MessageChannel(operationalChannel, operationalSecureSession);
|
|
967
|
-
await this.channelManager.setChannel(this.fabric, peerNodeId, channel);
|
|
968
|
-
return channel;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
413
|
isCommissioned() {
|
|
972
|
-
return this.
|
|
414
|
+
return this.peers.size > 0;
|
|
973
415
|
}
|
|
974
416
|
|
|
975
417
|
getCommissionedNodes() {
|
|
976
|
-
return
|
|
418
|
+
return this.peers.map(peer => peer.address.nodeId);
|
|
977
419
|
}
|
|
978
420
|
|
|
979
421
|
getCommissionedNodesDetails() {
|
|
980
|
-
return
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
: undefined,
|
|
422
|
+
return this.peers.map(peer => {
|
|
423
|
+
const { address, operationalAddress, discoveryData, basicInformationData } = peer as CommissionedPeer;
|
|
424
|
+
return {
|
|
425
|
+
nodeId: address.nodeId,
|
|
426
|
+
operationalAddress: operationalAddress ? serverAddressToString(operationalAddress) : undefined,
|
|
986
427
|
advertisedName: discoveryData?.DN,
|
|
987
428
|
discoveryData,
|
|
988
429
|
basicInformationData,
|
|
989
|
-
}),
|
|
990
|
-
);
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
private async setOperationalDeviceData(
|
|
994
|
-
nodeId: NodeId,
|
|
995
|
-
operationalServerAddress: ServerAddressIp,
|
|
996
|
-
discoveryData?: DiscoveryData,
|
|
997
|
-
) {
|
|
998
|
-
const nodeDetails = this.commissionedNodes.get(nodeId) ?? {};
|
|
999
|
-
nodeDetails.operationalServerAddress = operationalServerAddress;
|
|
1000
|
-
if (discoveryData !== undefined) {
|
|
1001
|
-
nodeDetails.discoveryData = {
|
|
1002
|
-
...nodeDetails.discoveryData,
|
|
1003
|
-
...discoveryData,
|
|
1004
430
|
};
|
|
1005
|
-
}
|
|
1006
|
-
this.commissionedNodes.set(nodeId, nodeDetails);
|
|
1007
|
-
await this.storeCommissionedNodes();
|
|
431
|
+
});
|
|
1008
432
|
}
|
|
1009
433
|
|
|
1010
434
|
async enhanceCommissionedNodeDetails(
|
|
1011
435
|
nodeId: NodeId,
|
|
1012
436
|
data: { basicInformationData: Record<string, SupportedStorageTypes> },
|
|
1013
437
|
) {
|
|
1014
|
-
const nodeDetails = this.
|
|
438
|
+
const nodeDetails = this.peers.get(this.fabric.addressOf(nodeId)) as CommissionedPeer;
|
|
1015
439
|
if (nodeDetails === undefined) {
|
|
1016
440
|
throw new Error(`Node ${nodeId} is not commissioned.`);
|
|
1017
441
|
}
|
|
1018
442
|
const { basicInformationData } = data;
|
|
1019
443
|
nodeDetails.basicInformationData = basicInformationData;
|
|
1020
|
-
this.
|
|
1021
|
-
await this.storeCommissionedNodes();
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
private getLastOperationalAddress(nodeId: NodeId) {
|
|
1025
|
-
return this.commissionedNodes.get(nodeId)?.operationalServerAddress;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
private async storeCommissionedNodes() {
|
|
1029
|
-
await this.nodesStorage.set("commissionedNodes", Array.from(this.commissionedNodes.entries()));
|
|
444
|
+
await this.nodesStore.save();
|
|
1030
445
|
}
|
|
1031
446
|
|
|
1032
447
|
/**
|
|
@@ -1034,59 +449,7 @@ export class MatterController {
|
|
|
1034
449
|
* Returns a InteractionClient on success.
|
|
1035
450
|
*/
|
|
1036
451
|
async connect(peerNodeId: NodeId, discoveryOptions: DiscoveryOptions) {
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
let channel: MessageChannel;
|
|
1040
|
-
try {
|
|
1041
|
-
channel = this.channelManager.getChannel(this.fabric, peerNodeId);
|
|
1042
|
-
} catch (error) {
|
|
1043
|
-
NoChannelError.accept(error);
|
|
1044
|
-
|
|
1045
|
-
channel = await this.resume(peerNodeId, discoveryOptions);
|
|
1046
|
-
}
|
|
1047
|
-
return new InteractionClient(
|
|
1048
|
-
new ExchangeProvider(this.exchangeManager, channel, async () => {
|
|
1049
|
-
if (!this.channelManager.hasChannel(this.fabric, peerNodeId)) {
|
|
1050
|
-
throw new RetransmissionLimitReachedError(`Device ${peerNodeId} is currently not reachable.`);
|
|
1051
|
-
}
|
|
1052
|
-
await this.channelManager.removeAllNodeChannels(this.fabric, peerNodeId);
|
|
1053
|
-
|
|
1054
|
-
const mdnsScanner = this.scanners.scannerFor(ChannelType.UDP) as MdnsScanner | undefined;
|
|
1055
|
-
const discoveredAddresses = mdnsScanner?.getDiscoveredOperationalDevice(this.fabric, peerNodeId);
|
|
1056
|
-
const lastKnownAddress = this.getLastOperationalAddress(peerNodeId);
|
|
1057
|
-
|
|
1058
|
-
if (
|
|
1059
|
-
lastKnownAddress !== undefined &&
|
|
1060
|
-
discoveredAddresses !== undefined &&
|
|
1061
|
-
discoveredAddresses.addresses.some(
|
|
1062
|
-
({ ip, port }) => ip === lastKnownAddress.ip && port === lastKnownAddress.port,
|
|
1063
|
-
)
|
|
1064
|
-
) {
|
|
1065
|
-
// We found the same address, so assume somehow cached response because we just tried to connect,
|
|
1066
|
-
// and it failed, so clear list
|
|
1067
|
-
discoveredAddresses.addresses.length = 0;
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
// Try to use first result for one last try before we need to reconnect
|
|
1071
|
-
const operationalAddress = discoveredAddresses?.addresses[0];
|
|
1072
|
-
if (operationalAddress === undefined) {
|
|
1073
|
-
logger.info(
|
|
1074
|
-
`Re-Discovering device failed (no address found), remove all sessions for node ${peerNodeId}`,
|
|
1075
|
-
);
|
|
1076
|
-
// We remove all sessions, this also informs the PairedNode class
|
|
1077
|
-
await this.sessionManager.removeAllSessionsForNode(peerNodeId);
|
|
1078
|
-
throw new RetransmissionLimitReachedError(`No operational address found for node ${peerNodeId}`);
|
|
1079
|
-
}
|
|
1080
|
-
if (
|
|
1081
|
-
(await this.reconnectKnownAddress(peerNodeId, operationalAddress, discoveryData, 2_000)) ===
|
|
1082
|
-
undefined
|
|
1083
|
-
) {
|
|
1084
|
-
throw new RetransmissionLimitReachedError(`Device ${peerNodeId} is not reachable.`);
|
|
1085
|
-
}
|
|
1086
|
-
return this.channelManager.getChannel(this.fabric, peerNodeId);
|
|
1087
|
-
}),
|
|
1088
|
-
peerNodeId,
|
|
1089
|
-
);
|
|
452
|
+
return this.peers.connect(this.fabric.addressOf(peerNodeId), discoveryOptions);
|
|
1090
453
|
}
|
|
1091
454
|
|
|
1092
455
|
async getNextAvailableSessionId() {
|
|
@@ -1098,7 +461,7 @@ export class MatterController {
|
|
|
1098
461
|
}
|
|
1099
462
|
|
|
1100
463
|
findResumptionRecordByNodeId(nodeId: NodeId) {
|
|
1101
|
-
return this.sessionManager.
|
|
464
|
+
return this.sessionManager.findResumptionRecordByAddress(this.fabric.addressOf(nodeId));
|
|
1102
465
|
}
|
|
1103
466
|
|
|
1104
467
|
async saveResumptionRecord(resumptionRecord: ResumptionRecord) {
|
|
@@ -1110,11 +473,7 @@ export class MatterController {
|
|
|
1110
473
|
}
|
|
1111
474
|
|
|
1112
475
|
async close() {
|
|
1113
|
-
|
|
1114
|
-
for (const [nodeId, { timer }] of this.#runningNodeDiscoveries.entries()) {
|
|
1115
|
-
timer?.stop();
|
|
1116
|
-
mdnsScanner?.cancelOperationalDeviceDiscovery(this.fabric, nodeId, false); // This ends discovery without triggering promises
|
|
1117
|
-
}
|
|
476
|
+
await this.peers.close();
|
|
1118
477
|
await this.exchangeManager.close();
|
|
1119
478
|
await this.sessionManager.close();
|
|
1120
479
|
await this.channelManager.close();
|
|
@@ -1125,3 +484,57 @@ export class MatterController {
|
|
|
1125
484
|
return this.sessionManager.getActiveSessionInformation();
|
|
1126
485
|
}
|
|
1127
486
|
}
|
|
487
|
+
|
|
488
|
+
class CommissionedNodeStore extends PeerStore {
|
|
489
|
+
declare peers: PeerSet;
|
|
490
|
+
|
|
491
|
+
constructor(
|
|
492
|
+
private nodesStorage: StorageContext,
|
|
493
|
+
private fabric: Fabric,
|
|
494
|
+
) {
|
|
495
|
+
super();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async loadPeers() {
|
|
499
|
+
if (!(await this.nodesStorage.has("commissionedNodes"))) {
|
|
500
|
+
return [];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const commissionedNodes = await this.nodesStorage.get<StoredOperationalPeer[]>("commissionedNodes");
|
|
504
|
+
return commissionedNodes.map(
|
|
505
|
+
([nodeId, { operationalServerAddress, discoveryData, basicInformationData }]) =>
|
|
506
|
+
({
|
|
507
|
+
address: this.fabric.addressOf(nodeId),
|
|
508
|
+
operationalAddress: operationalServerAddress,
|
|
509
|
+
discoveryData,
|
|
510
|
+
basicInformationData,
|
|
511
|
+
}) satisfies CommissionedPeer,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async updatePeer() {
|
|
516
|
+
return this.save();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async deletePeer() {
|
|
520
|
+
return this.save();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async save() {
|
|
524
|
+
await this.nodesStorage.set(
|
|
525
|
+
"commissionedNodes",
|
|
526
|
+
this.peers.map(peer => {
|
|
527
|
+
const {
|
|
528
|
+
address,
|
|
529
|
+
operationalAddress: operationalServerAddress,
|
|
530
|
+
basicInformationData,
|
|
531
|
+
discoveryData,
|
|
532
|
+
} = peer as CommissionedPeer;
|
|
533
|
+
return [
|
|
534
|
+
address.nodeId,
|
|
535
|
+
{ operationalServerAddress, basicInformationData, discoveryData },
|
|
536
|
+
] satisfies StoredOperationalPeer;
|
|
537
|
+
}),
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
}
|