@matter/protocol 0.15.0-alpha.0-20250625-4a4b1be1b → 0.15.0-alpha.0-20250626-fc3a84ce9

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 (117) hide show
  1. package/dist/cjs/cluster/client/ClusterClient.d.ts.map +1 -1
  2. package/dist/cjs/cluster/client/ClusterClient.js +7 -1
  3. package/dist/cjs/cluster/client/ClusterClient.js.map +1 -1
  4. package/dist/cjs/cluster/client/ClusterClientTypes.d.ts +10 -0
  5. package/dist/cjs/cluster/client/ClusterClientTypes.d.ts.map +1 -1
  6. package/dist/cjs/interaction/InteractionClient.d.ts +8 -1
  7. package/dist/cjs/interaction/InteractionClient.d.ts.map +1 -1
  8. package/dist/cjs/interaction/InteractionClient.js +15 -10
  9. package/dist/cjs/interaction/InteractionClient.js.map +1 -1
  10. package/dist/cjs/interaction/InteractionMessenger.d.ts +0 -1
  11. package/dist/cjs/interaction/InteractionMessenger.d.ts.map +1 -1
  12. package/dist/cjs/interaction/InteractionMessenger.js +0 -3
  13. package/dist/cjs/interaction/InteractionMessenger.js.map +1 -1
  14. package/dist/cjs/interaction/SubscriptionClient.d.ts +1 -1
  15. package/dist/cjs/interaction/SubscriptionClient.d.ts.map +1 -1
  16. package/dist/cjs/interaction/SubscriptionClient.js +1 -1
  17. package/dist/cjs/peer/ControllerCommissioner.d.ts +3 -2
  18. package/dist/cjs/peer/ControllerCommissioner.d.ts.map +1 -1
  19. package/dist/cjs/peer/ControllerCommissioner.js +6 -5
  20. package/dist/cjs/peer/ControllerCommissioner.js.map +1 -1
  21. package/dist/cjs/peer/ControllerCommissioningFlow.d.ts.map +1 -1
  22. package/dist/cjs/peer/ControllerCommissioningFlow.js +81 -52
  23. package/dist/cjs/peer/ControllerCommissioningFlow.js.map +1 -1
  24. package/dist/cjs/peer/PeerSet.d.ts +4 -3
  25. package/dist/cjs/peer/PeerSet.d.ts.map +1 -1
  26. package/dist/cjs/peer/PeerSet.js +9 -8
  27. package/dist/cjs/peer/PeerSet.js.map +1 -1
  28. package/dist/cjs/protocol/ChannelManager.d.ts +2 -2
  29. package/dist/cjs/protocol/ChannelManager.d.ts.map +1 -1
  30. package/dist/cjs/protocol/ChannelManager.js +4 -4
  31. package/dist/cjs/protocol/ChannelManager.js.map +1 -1
  32. package/dist/cjs/protocol/ExchangeManager.d.ts +5 -24
  33. package/dist/cjs/protocol/ExchangeManager.d.ts.map +1 -1
  34. package/dist/cjs/protocol/ExchangeManager.js +12 -55
  35. package/dist/cjs/protocol/ExchangeManager.js.map +1 -1
  36. package/dist/cjs/protocol/ExchangeProvider.d.ts +10 -6
  37. package/dist/cjs/protocol/ExchangeProvider.d.ts.map +1 -1
  38. package/dist/cjs/protocol/ExchangeProvider.js +12 -1
  39. package/dist/cjs/protocol/ExchangeProvider.js.map +1 -1
  40. package/dist/cjs/protocol/MessageChannel.d.ts +52 -0
  41. package/dist/cjs/protocol/MessageChannel.d.ts.map +1 -0
  42. package/dist/cjs/protocol/MessageChannel.js +130 -0
  43. package/dist/cjs/protocol/MessageChannel.js.map +6 -0
  44. package/dist/cjs/protocol/MessageExchange.d.ts +5 -5
  45. package/dist/cjs/protocol/MessageExchange.d.ts.map +1 -1
  46. package/dist/cjs/protocol/MessageExchange.js +34 -78
  47. package/dist/cjs/protocol/MessageExchange.js.map +1 -1
  48. package/dist/cjs/protocol/index.d.ts +1 -0
  49. package/dist/cjs/protocol/index.d.ts.map +1 -1
  50. package/dist/cjs/protocol/index.js +1 -0
  51. package/dist/cjs/protocol/index.js.map +1 -1
  52. package/dist/esm/cluster/client/ClusterClient.d.ts.map +1 -1
  53. package/dist/esm/cluster/client/ClusterClient.js +7 -1
  54. package/dist/esm/cluster/client/ClusterClient.js.map +1 -1
  55. package/dist/esm/cluster/client/ClusterClientTypes.d.ts +10 -0
  56. package/dist/esm/cluster/client/ClusterClientTypes.d.ts.map +1 -1
  57. package/dist/esm/interaction/InteractionClient.d.ts +8 -1
  58. package/dist/esm/interaction/InteractionClient.d.ts.map +1 -1
  59. package/dist/esm/interaction/InteractionClient.js +15 -10
  60. package/dist/esm/interaction/InteractionClient.js.map +1 -1
  61. package/dist/esm/interaction/InteractionMessenger.d.ts +0 -1
  62. package/dist/esm/interaction/InteractionMessenger.d.ts.map +1 -1
  63. package/dist/esm/interaction/InteractionMessenger.js +0 -3
  64. package/dist/esm/interaction/InteractionMessenger.js.map +1 -1
  65. package/dist/esm/interaction/SubscriptionClient.d.ts +1 -1
  66. package/dist/esm/interaction/SubscriptionClient.d.ts.map +1 -1
  67. package/dist/esm/interaction/SubscriptionClient.js +1 -1
  68. package/dist/esm/peer/ControllerCommissioner.d.ts +3 -2
  69. package/dist/esm/peer/ControllerCommissioner.d.ts.map +1 -1
  70. package/dist/esm/peer/ControllerCommissioner.js +4 -3
  71. package/dist/esm/peer/ControllerCommissioner.js.map +1 -1
  72. package/dist/esm/peer/ControllerCommissioningFlow.d.ts.map +1 -1
  73. package/dist/esm/peer/ControllerCommissioningFlow.js +81 -52
  74. package/dist/esm/peer/ControllerCommissioningFlow.js.map +1 -1
  75. package/dist/esm/peer/PeerSet.d.ts +4 -3
  76. package/dist/esm/peer/PeerSet.d.ts.map +1 -1
  77. package/dist/esm/peer/PeerSet.js +5 -4
  78. package/dist/esm/peer/PeerSet.js.map +1 -1
  79. package/dist/esm/protocol/ChannelManager.d.ts +2 -2
  80. package/dist/esm/protocol/ChannelManager.d.ts.map +1 -1
  81. package/dist/esm/protocol/ChannelManager.js +2 -2
  82. package/dist/esm/protocol/ChannelManager.js.map +1 -1
  83. package/dist/esm/protocol/ExchangeManager.d.ts +5 -24
  84. package/dist/esm/protocol/ExchangeManager.d.ts.map +1 -1
  85. package/dist/esm/protocol/ExchangeManager.js +13 -56
  86. package/dist/esm/protocol/ExchangeManager.js.map +1 -1
  87. package/dist/esm/protocol/ExchangeProvider.d.ts +10 -6
  88. package/dist/esm/protocol/ExchangeProvider.d.ts.map +1 -1
  89. package/dist/esm/protocol/ExchangeProvider.js +12 -1
  90. package/dist/esm/protocol/ExchangeProvider.js.map +1 -1
  91. package/dist/esm/protocol/MessageChannel.d.ts +52 -0
  92. package/dist/esm/protocol/MessageChannel.d.ts.map +1 -0
  93. package/dist/esm/protocol/MessageChannel.js +110 -0
  94. package/dist/esm/protocol/MessageChannel.js.map +6 -0
  95. package/dist/esm/protocol/MessageExchange.d.ts +5 -5
  96. package/dist/esm/protocol/MessageExchange.d.ts.map +1 -1
  97. package/dist/esm/protocol/MessageExchange.js +39 -83
  98. package/dist/esm/protocol/MessageExchange.js.map +1 -1
  99. package/dist/esm/protocol/index.d.ts +1 -0
  100. package/dist/esm/protocol/index.d.ts.map +1 -1
  101. package/dist/esm/protocol/index.js +1 -0
  102. package/dist/esm/protocol/index.js.map +1 -1
  103. package/package.json +6 -6
  104. package/src/cluster/client/ClusterClient.ts +8 -1
  105. package/src/cluster/client/ClusterClientTypes.ts +12 -0
  106. package/src/interaction/InteractionClient.ts +29 -16
  107. package/src/interaction/InteractionMessenger.ts +0 -4
  108. package/src/interaction/SubscriptionClient.ts +2 -2
  109. package/src/peer/ControllerCommissioner.ts +4 -3
  110. package/src/peer/ControllerCommissioningFlow.ts +96 -57
  111. package/src/peer/PeerSet.ts +5 -4
  112. package/src/protocol/ChannelManager.ts +3 -3
  113. package/src/protocol/ExchangeManager.ts +18 -67
  114. package/src/protocol/ExchangeProvider.ts +20 -6
  115. package/src/protocol/MessageChannel.ts +163 -0
  116. package/src/protocol/MessageExchange.ts +40 -119
  117. package/src/protocol/index.ts +1 -0
@@ -178,7 +178,7 @@ export class OperativeConnectionFailedError extends CommissioningError {}
178
178
  /** Error that throws when Commissioning fails but a process can be continued. */
179
179
  class RecoverableCommissioningError extends CommissioningError {}
180
180
 
181
- const DEFAULT_FAILSAFE_TIME_MS = 60_000; // 60 seconds
181
+ const DEFAULT_FAILSAFE_TIME_S = 60;
182
182
 
183
183
  /**
184
184
  * Class to abstract the Device commission flow in a step wise way as defined in Specs. The specs are not 100%
@@ -197,10 +197,10 @@ export class ControllerCommissioningFlow {
197
197
  readonly #clusterClients = new Map<ClusterId, ClusterClientObj>();
198
198
  #commissioningStartedTime: number | undefined;
199
199
  #commissioningExpiryTime: number | undefined;
200
- #lastFailSafeTime: number | undefined;
200
+ #currentFailSafeEndTime: number | undefined;
201
201
  protected lastBreadcrumb = 1;
202
202
  protected collectedCommissioningData: CollectedCommissioningData = {};
203
- #failSafeTimeMs = DEFAULT_FAILSAFE_TIME_MS;
203
+ #defaultFailSafeTimeS = DEFAULT_FAILSAFE_TIME_S;
204
204
 
205
205
  constructor(
206
206
  /** InteractionClient for the initiated PASE session */
@@ -251,8 +251,7 @@ export class ControllerCommissioningFlow {
251
251
  const result = await step.stepLogic();
252
252
  this.#setCommissioningStepResult(step, result);
253
253
 
254
- if (this.#lastFailSafeTime !== undefined) {
255
- const timeSinceLastArmFailsafe = Time.nowMs() - this.#lastFailSafeTime;
254
+ if (this.#currentFailSafeEndTime !== undefined) {
256
255
  if (this.#commissioningExpiryTime !== undefined && Time.nowMs() > this.#commissioningExpiryTime) {
257
256
  logger.error(
258
257
  `Commissioning step ${step.stepNumber}.${step.subStepNumber}: ${step.name} succeeded, but commissioning took too long in general!`,
@@ -268,13 +267,12 @@ export class ControllerCommissioningFlow {
268
267
  * timeout within 60 seconds of the completion of PASE session establishment, using the ArmFailSafe
269
268
  * command (see Section 11.9.6.2, “ArmFailSafe Command”)
270
269
  */
271
- if (timeSinceLastArmFailsafe > this.#failSafeTimeMs / 2) {
270
+ const timeLeft = Math.floor((this.#currentFailSafeEndTime - Time.nowMs()) / 1000);
271
+ if (timeLeft < this.#defaultFailSafeTimeS / 2) {
272
272
  logger.info(
273
273
  `After Commissioning step ${step.stepNumber}.${step.subStepNumber}: ${
274
274
  step.name
275
- } succeeded, ${Math.floor(
276
- timeSinceLastArmFailsafe / 1000,
277
- )}s elapsed since last arm failsafe, re-arming failsafe`,
275
+ } succeeded, ${timeLeft}s left for failsafe timer, re-arming failsafe`,
278
276
  );
279
277
  await this.#armFailsafe();
280
278
  failSafeTimerReArmedAfterPreviousStep = true;
@@ -597,40 +595,59 @@ export class ControllerCommissioningFlow {
597
595
  * reading BasicCommissioningInfo attribute (see Section 11.10.5.2, “BasicCommissioningInfo
598
596
  * Attribute”) prior to invoking the ArmFailSafe command.
599
597
  */
600
- async #armFailsafe() {
598
+ async #armFailsafe(timeS?: number) {
601
599
  const client = this.#getClusterClient(GeneralCommissioning.Cluster);
602
600
  if (this.collectedCommissioningData.basicCommissioningInfo === undefined) {
603
601
  const basicCommissioningInfo = await client.getBasicCommissioningInfoAttribute();
604
602
  this.collectedCommissioningData.basicCommissioningInfo = basicCommissioningInfo;
605
- this.#failSafeTimeMs = basicCommissioningInfo.failSafeExpiryLengthSeconds * 1000;
603
+ this.#defaultFailSafeTimeS = basicCommissioningInfo.failSafeExpiryLengthSeconds;
606
604
  this.#commissioningStartedTime = Time.nowMs();
607
605
  this.#commissioningExpiryTime =
608
606
  this.#commissioningStartedTime + basicCommissioningInfo.maxCumulativeFailsafeSeconds * 1000;
609
607
  }
608
+ const expiryLengthSeconds = timeS ?? this.#defaultFailSafeTimeS;
610
609
  this.#ensureGeneralCommissioningSuccess(
611
610
  "armFailSafe",
612
611
  await client.armFailSafe({
613
612
  breadcrumb: this.lastBreadcrumb,
614
- expiryLengthSeconds:
615
- this.collectedCommissioningData.basicCommissioningInfo?.failSafeExpiryLengthSeconds,
613
+ expiryLengthSeconds,
616
614
  }),
617
615
  );
618
- this.#lastFailSafeTime = Time.nowMs();
616
+ this.#currentFailSafeEndTime = Time.nowMs() + expiryLengthSeconds * 1000;
619
617
  return {
620
618
  code: CommissioningStepResultCode.Success,
621
619
  breadcrumb: this.lastBreadcrumb,
622
620
  };
623
621
  }
624
622
 
623
+ get #failSafeTimeLeftS() {
624
+ if (this.#currentFailSafeEndTime === undefined) {
625
+ return 0;
626
+ }
627
+ return Math.max(0, Math.ceil((this.#currentFailSafeEndTime - Time.nowMs()) / 1000));
628
+ }
629
+
630
+ async #ensureFailsafeTimerForS(maxProcessingTime: number) {
631
+ const minFailsafeTimeS = this.interactionClient.maximumPeerResponseTimeMs(maxProcessingTime);
632
+
633
+ const timeLeft = this.#failSafeTimeLeftS;
634
+ if (timeLeft < minFailsafeTimeS) {
635
+ logger.debug(`Failsafe timer has only ${timeLeft}s left, re-arming for at least ${minFailsafeTimeS}s`);
636
+ await this.#armFailsafe(Math.max(minFailsafeTimeS, this.#defaultFailSafeTimeS));
637
+ } else {
638
+ logger.debug(`Failsafe timer is already set for at least ${timeLeft}s`);
639
+ }
640
+ }
641
+
625
642
  async #resetFailsafeTimer() {
626
- if (this.#lastFailSafeTime === undefined) return;
643
+ if (this.#currentFailSafeEndTime === undefined) return;
627
644
  try {
628
645
  const client = this.#getClusterClient(GeneralCommissioning.Cluster);
629
646
  await client.armFailSafe({
630
647
  breadcrumb: this.lastBreadcrumb,
631
648
  expiryLengthSeconds: 0,
632
649
  });
633
- this.#lastFailSafeTime = undefined; // No failsafe active anymore
650
+ this.#currentFailSafeEndTime = undefined; // No failsafe active anymore
634
651
  } catch (error) {
635
652
  logger.error(`Error while resetting failsafe timer`, error);
636
653
  }
@@ -1000,20 +1017,27 @@ export class ControllerCommissioningFlow {
1000
1017
  const ssid = Bytes.fromString(this.commissioningOptions.wifiNetwork.wifiSsid);
1001
1018
  const credentials = Bytes.fromString(this.commissioningOptions.wifiNetwork.wifiCredentials);
1002
1019
 
1003
- const { networkingStatus, wiFiScanResults, debugText } = await networkCommissioningClusterClient.scanNetworks(
1004
- {
1005
- ssid,
1006
- breadcrumb: this.lastBreadcrumb++,
1007
- },
1008
- { useExtendedFailSafeMessageResponseTimeout: true },
1009
- );
1010
- if (networkingStatus !== NetworkCommissioning.NetworkCommissioningStatus.Success) {
1011
- throw new WifiNetworkSetupFailedError(`Commissionee failed to scan for WiFi networks: ${debugText}`);
1012
- }
1013
- if (wiFiScanResults === undefined || wiFiScanResults.length === 0) {
1014
- throw new WifiNetworkSetupFailedError(
1015
- `Commissionee did not return any WiFi networks for the requested SSID ${this.commissioningOptions.wifiNetwork.wifiSsid}`,
1016
- );
1020
+ // Only Scan when the device supports concurrent connections
1021
+ if (this.collectedCommissioningData.supportsConcurrentConnection !== false) {
1022
+ const scanMaxTimeSeconds = await networkCommissioningClusterClient.getScanMaxTimeSecondsAttribute();
1023
+ await this.#ensureFailsafeTimerForS(scanMaxTimeSeconds);
1024
+
1025
+ const { networkingStatus, wiFiScanResults, debugText } =
1026
+ await networkCommissioningClusterClient.scanNetworks(
1027
+ {
1028
+ ssid,
1029
+ breadcrumb: this.lastBreadcrumb++,
1030
+ },
1031
+ { expectedProcessingTimeMs: scanMaxTimeSeconds * 1000 },
1032
+ );
1033
+ if (networkingStatus !== NetworkCommissioning.NetworkCommissioningStatus.Success) {
1034
+ throw new WifiNetworkSetupFailedError(`Commissionee failed to scan for WiFi networks: ${debugText}`);
1035
+ }
1036
+ if (wiFiScanResults === undefined || wiFiScanResults.length === 0) {
1037
+ throw new WifiNetworkSetupFailedError(
1038
+ `Commissionee did not return any WiFi networks for the requested SSID ${this.commissioningOptions.wifiNetwork.wifiSsid}`,
1039
+ );
1040
+ }
1017
1041
  }
1018
1042
 
1019
1043
  const {
@@ -1056,12 +1080,15 @@ export class ControllerCommissioningFlow {
1056
1080
  };
1057
1081
  }
1058
1082
 
1083
+ const connectMaxTimeSeconds = await networkCommissioningClusterClient.getConnectMaxTimeSecondsAttribute();
1084
+ await this.#ensureFailsafeTimerForS(connectMaxTimeSeconds);
1085
+
1059
1086
  const connectResult = await networkCommissioningClusterClient.connectNetwork(
1060
1087
  {
1061
1088
  networkId: networkId,
1062
1089
  breadcrumb: this.lastBreadcrumb++,
1063
1090
  },
1064
- { useExtendedFailSafeMessageResponseTimeout: true },
1091
+ { expectedProcessingTimeMs: connectMaxTimeSeconds * 1000 },
1065
1092
  );
1066
1093
 
1067
1094
  if (connectResult.networkingStatus !== NetworkCommissioning.NetworkCommissioningStatus.Success) {
@@ -1135,33 +1162,42 @@ export class ControllerCommissioningFlow {
1135
1162
  true,
1136
1163
  );
1137
1164
 
1138
- const { networkingStatus, threadScanResults, debugText } = await networkCommissioningClusterClient.scanNetworks(
1139
- { breadcrumb: this.lastBreadcrumb++ },
1140
- { useExtendedFailSafeMessageResponseTimeout: true },
1141
- );
1142
- if (networkingStatus !== NetworkCommissioning.NetworkCommissioningStatus.Success) {
1143
- throw new ThreadNetworkSetupFailedError(`Commissionee failed to scan for Thread networks: ${debugText}`);
1144
- }
1145
- if (threadScanResults === undefined || threadScanResults.length === 0) {
1146
- throw new ThreadNetworkSetupFailedError(
1147
- `Commissionee did not return any Thread networks for the requested Network ${this.commissioningOptions.threadNetwork.networkName}`,
1165
+ // Only Scan when the device supports concurrent connections
1166
+ if (this.collectedCommissioningData.supportsConcurrentConnection !== false) {
1167
+ const scanMaxTimeSeconds = await networkCommissioningClusterClient.getScanMaxTimeSecondsAttribute();
1168
+ await this.#ensureFailsafeTimerForS(scanMaxTimeSeconds);
1169
+
1170
+ const { networkingStatus, threadScanResults, debugText } =
1171
+ await networkCommissioningClusterClient.scanNetworks(
1172
+ { breadcrumb: this.lastBreadcrumb++ },
1173
+ { expectedProcessingTimeMs: scanMaxTimeSeconds * 1000 },
1174
+ );
1175
+ if (networkingStatus !== NetworkCommissioning.NetworkCommissioningStatus.Success) {
1176
+ throw new ThreadNetworkSetupFailedError(
1177
+ `Commissionee failed to scan for Thread networks: ${debugText}`,
1178
+ );
1179
+ }
1180
+ if (threadScanResults === undefined || threadScanResults.length === 0) {
1181
+ throw new ThreadNetworkSetupFailedError(
1182
+ `Commissionee did not return any Thread networks for the requested Network ${this.commissioningOptions.threadNetwork.networkName}`,
1183
+ );
1184
+ }
1185
+ const wantedNetworkFound = threadScanResults.find(
1186
+ ({ networkName }) => networkName === this.commissioningOptions.threadNetwork?.networkName,
1148
1187
  );
1149
- }
1150
- const wantedNetworkFound = threadScanResults.find(
1151
- ({ networkName }) => networkName === this.commissioningOptions.threadNetwork?.networkName,
1152
- );
1153
- if (wantedNetworkFound === undefined) {
1154
- throw new ThreadNetworkSetupFailedError(
1155
- `Commissionee did not return the requested Network ${
1188
+ if (wantedNetworkFound === undefined) {
1189
+ throw new ThreadNetworkSetupFailedError(
1190
+ `Commissionee did not return the requested Network ${
1191
+ this.commissioningOptions.threadNetwork.networkName
1192
+ }: ${Diagnostic.json(threadScanResults)}`,
1193
+ );
1194
+ }
1195
+ logger.debug(
1196
+ `Commissionee found wanted Thread network ${
1156
1197
  this.commissioningOptions.threadNetwork.networkName
1157
- }: ${Diagnostic.json(threadScanResults)}`,
1198
+ }: ${Diagnostic.json(wantedNetworkFound)}`,
1158
1199
  );
1159
1200
  }
1160
- logger.debug(
1161
- `Commissionee found wanted Thread network ${
1162
- this.commissioningOptions.threadNetwork.networkName
1163
- }: ${Diagnostic.json(wantedNetworkFound)}`,
1164
- );
1165
1201
 
1166
1202
  const {
1167
1203
  networkingStatus: addNetworkingStatus,
@@ -1201,12 +1237,15 @@ export class ControllerCommissioningFlow {
1201
1237
  };
1202
1238
  }
1203
1239
 
1240
+ const connectMaxTimeSeconds = await networkCommissioningClusterClient.getConnectMaxTimeSecondsAttribute();
1241
+ await this.#ensureFailsafeTimerForS(connectMaxTimeSeconds);
1242
+
1204
1243
  const connectResult = await networkCommissioningClusterClient.connectNetwork(
1205
1244
  {
1206
1245
  networkId: networkId,
1207
1246
  breadcrumb: this.lastBreadcrumb++,
1208
1247
  },
1209
- { useExtendedFailSafeMessageResponseTimeout: true },
1248
+ { expectedProcessingTimeMs: connectMaxTimeSeconds * 1000 },
1210
1249
  );
1211
1250
 
1212
1251
  if (connectResult.networkingStatus !== NetworkCommissioning.NetworkCommissioningStatus.Success) {
@@ -1245,7 +1284,7 @@ export class ControllerCommissioningFlow {
1245
1284
  // TODO: Check whats needed for non-concurrent commissioning flows (maybe arm initially longer?)
1246
1285
  const reArmFailsafeInterval = Time.getPeriodicTimer(
1247
1286
  "Re-Arm Failsafe during reconnect",
1248
- this.#failSafeTimeMs / 2,
1287
+ (this.#defaultFailSafeTimeS / 2) * 1000,
1249
1288
  () => {
1250
1289
  const now = Time.nowMs();
1251
1290
  if (this.#commissioningExpiryTime !== undefined && now < this.#commissioningExpiryTime) {
@@ -1313,7 +1352,7 @@ export class ControllerCommissioningFlow {
1313
1352
  useExtendedFailSafeMessageResponseTimeout: true,
1314
1353
  }),
1315
1354
  );
1316
- this.#lastFailSafeTime = undefined; // gets deactivated when successful
1355
+ this.#currentFailSafeEndTime = undefined; // gets deactivated when successful
1317
1356
 
1318
1357
  return {
1319
1358
  code: CommissioningStepResultCode.Success,
@@ -32,14 +32,15 @@ import {
32
32
  import { SubscriptionClient } from "#interaction/SubscriptionClient.js";
33
33
  import { MdnsScanner } from "#mdns/MdnsScanner.js";
34
34
  import { PeerAddress, PeerAddressMap } from "#peer/PeerAddress.js";
35
+ import { ChannelManager } from "#protocol/ChannelManager.js";
36
+ import { ChannelNotConnectedError, ExchangeManager } from "#protocol/ExchangeManager.js";
37
+ import { DedicatedChannelExchangeProvider, ReconnectableExchangeProvider } from "#protocol/ExchangeProvider.js";
38
+ import { MessageChannel } from "#protocol/MessageChannel.js";
39
+ import { MessageExchange, RetransmissionLimitReachedError } from "#protocol/MessageExchange.js";
35
40
  import { ChannelStatusResponseError } from "#securechannel/index.js";
36
41
  import { CaseClient, SecureSession, Session } from "#session/index.js";
37
42
  import { SessionManager } from "#session/SessionManager.js";
38
43
  import { GroupId, NodeId, SECURE_CHANNEL_PROTOCOL_ID, SecureChannelStatusCode } from "#types";
39
- import { ChannelManager } from "../protocol/ChannelManager.js";
40
- import { ChannelNotConnectedError, ExchangeManager, MessageChannel } from "../protocol/ExchangeManager.js";
41
- import { DedicatedChannelExchangeProvider, ReconnectableExchangeProvider } from "../protocol/ExchangeProvider.js";
42
- import { MessageExchange, RetransmissionLimitReachedError } from "../protocol/MessageExchange.js";
43
44
  import { ControllerDiscovery, DiscoveryError, PairRetransmissionLimitReachedError } from "./ControllerDiscovery.js";
44
45
  import { InteractionQueue } from "./InteractionQueue.js";
45
46
  import { OperationalPeer } from "./OperationalPeer.js";
@@ -6,9 +6,9 @@
6
6
 
7
7
  import { AsyncObservable, Channel, Environment, Environmental, Logger, MatterError } from "#general";
8
8
  import { PeerAddress, PeerAddressMap } from "#peer/PeerAddress.js";
9
- import { NodeSession } from "../session/NodeSession.js";
10
- import { Session } from "../session/Session.js";
11
- import { MessageChannel } from "./ExchangeManager.js";
9
+ import { MessageChannel } from "#protocol/MessageChannel.js";
10
+ import { NodeSession } from "#session/NodeSession.js";
11
+ import { Session } from "#session/Session.js";
12
12
 
13
13
  const logger = Logger.get("ChannelManager");
14
14
 
@@ -4,6 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
+ import { DecodedMessage, MessageCodec, SessionType } from "#codec/MessageCodec.js";
7
8
  import {
8
9
  Channel,
9
10
  Crypto,
@@ -22,15 +23,15 @@ import {
22
23
  UnexpectedDataError,
23
24
  } from "#general";
24
25
  import { PeerAddress } from "#peer/PeerAddress.js";
26
+ import { MessageChannel } from "#protocol/MessageChannel.js";
27
+ import { SecureChannelMessenger } from "#securechannel/SecureChannelMessenger.js";
28
+ import { SecureChannelProtocol } from "#securechannel/SecureChannelProtocol.js";
29
+ import { NodeSession } from "#session/NodeSession.js";
30
+ import { Session } from "#session/Session.js";
31
+ import { SessionManager, UNICAST_UNSECURE_SESSION_ID } from "#session/SessionManager.js";
25
32
  import { NodeId, SECURE_CHANNEL_PROTOCOL_ID, SecureMessageType } from "#types";
26
- import { DecodedMessage, Message, MessageCodec, SessionType } from "../codec/MessageCodec.js";
27
- import { SecureChannelMessenger } from "../securechannel/SecureChannelMessenger.js";
28
- import { SecureChannelProtocol } from "../securechannel/SecureChannelProtocol.js";
29
- import { NodeSession } from "../session/NodeSession.js";
30
- import { Session } from "../session/Session.js";
31
- import { SessionManager, UNICAST_UNSECURE_SESSION_ID } from "../session/SessionManager.js";
32
33
  import { ChannelManager } from "./ChannelManager.js";
33
- import { ExchangeLogContext, MessageExchange, MessageExchangeContext } from "./MessageExchange.js";
34
+ import { DEFAULT_EXPECTED_PROCESSING_TIME_MS, MessageExchange, MessageExchangeContext } from "./MessageExchange.js";
34
35
  import { DuplicateMessageError } from "./MessageReceptionState.js";
35
36
  import { ProtocolHandler } from "./ProtocolHandler.js";
36
37
 
@@ -40,66 +41,6 @@ const MAXIMUM_CONCURRENT_EXCHANGES_PER_SESSION = 5;
40
41
 
41
42
  export class ChannelNotConnectedError extends MatterError {}
42
43
 
43
- export class MessageChannel implements Channel<Message> {
44
- public closed = false;
45
- #closeCallback?: () => Promise<void>;
46
-
47
- constructor(
48
- readonly channel: Channel<Uint8Array>,
49
- readonly session: Session,
50
- closeCallback?: () => Promise<void>,
51
- ) {
52
- this.#closeCallback = closeCallback;
53
- }
54
-
55
- set closeCallback(callback: () => Promise<void>) {
56
- this.#closeCallback = callback;
57
- }
58
-
59
- /** Is the underlying transport reliable? */
60
- get isReliable() {
61
- return this.channel.isReliable;
62
- }
63
-
64
- get type() {
65
- return this.channel.type;
66
- }
67
-
68
- /**
69
- * Max Payload size of the exchange which bases on the maximum payload size of the channel. The full encoded matter
70
- * message payload sent here can be as huge as allowed by the channel.
71
- */
72
- get maxPayloadSize() {
73
- return this.channel.maxPayloadSize;
74
- }
75
-
76
- async send(message: Message, logContext?: ExchangeLogContext) {
77
- logger.debug("Message »", MessageCodec.messageDiagnostics(message, logContext));
78
- const packet = this.session.encode(message);
79
- const bytes = MessageCodec.encodePacket(packet);
80
- if (bytes.length > this.maxPayloadSize) {
81
- logger.warn(
82
- `Matter message to send to ${this.name} is ${bytes.length}bytes long, which is larger than the maximum allowed size of ${this.maxPayloadSize}. This only works if both nodes support it.`,
83
- );
84
- }
85
-
86
- return await this.channel.send(bytes);
87
- }
88
-
89
- get name() {
90
- return `${this.channel.name} on session ${this.session.name}`;
91
- }
92
-
93
- async close() {
94
- const wasAlreadyClosed = this.closed;
95
- this.closed = true;
96
- await this.channel.close();
97
- if (!wasAlreadyClosed) {
98
- await this.#closeCallback?.();
99
- }
100
- }
101
- }
102
-
103
44
  /**
104
45
  * Interfaces {@link ExchangeManager} with other components.
105
46
  */
@@ -465,6 +406,16 @@ export class ExchangeManager {
465
406
  exchangeToClose.close().catch(error => logger.error("Error closing exchange", error)); // TODO Promise??
466
407
  }
467
408
 
409
+ calculateMaximumPeerResponseTimeMsFor(
410
+ channel: MessageChannel,
411
+ expectedProcessingTimeMs = DEFAULT_EXPECTED_PROCESSING_TIME_MS,
412
+ ) {
413
+ return channel.calculateMaximumPeerResponseTimeMs(
414
+ this.#sessionManager.sessionParameters,
415
+ expectedProcessingTimeMs,
416
+ );
417
+ }
418
+
468
419
  #messageExchangeContextFor(channel: MessageChannel): MessageExchangeContext {
469
420
  return {
470
421
  channel,
@@ -4,13 +4,14 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import { ChannelType, Observable } from "#general";
7
+ import { PeerAddress } from "#peer/PeerAddress.js";
8
+ import { ChannelManager } from "#protocol/ChannelManager.js";
9
+ import { ChannelNotConnectedError, ExchangeManager } from "#protocol/ExchangeManager.js";
10
+ import { MessageChannel } from "#protocol/MessageChannel.js";
11
+ import { DEFAULT_EXPECTED_PROCESSING_TIME_MS, MessageExchange } from "#protocol/MessageExchange.js";
12
+ import { ProtocolHandler } from "#protocol/ProtocolHandler.js";
13
+ import { Session } from "#session/Session.js";
7
14
  import { INTERACTION_PROTOCOL_ID } from "#types";
8
- import { PeerAddress } from "../peer/PeerAddress.js";
9
- import { ChannelManager } from "../protocol/ChannelManager.js";
10
- import { ChannelNotConnectedError, ExchangeManager, MessageChannel } from "../protocol/ExchangeManager.js";
11
- import { MessageExchange } from "../protocol/MessageExchange.js";
12
- import { ProtocolHandler } from "../protocol/ProtocolHandler.js";
13
- import { Session } from "../session/Session.js";
14
15
 
15
16
  /**
16
17
  * Interface for obtaining an exchange with a specific peer.
@@ -32,6 +33,7 @@ export abstract class ExchangeProvider {
32
33
  this.exchangeManager.addProtocolHandler(handler);
33
34
  }
34
35
 
36
+ abstract maximumPeerResponseTimeMs(expectedProcessingTimeMs?: number): number;
35
37
  abstract initiateExchange(): Promise<MessageExchange>;
36
38
  abstract reconnectChannel(): Promise<boolean>;
37
39
  abstract session: Session;
@@ -65,6 +67,10 @@ export class DedicatedChannelExchangeProvider extends ExchangeProvider {
65
67
  get channelType() {
66
68
  return this.#channel.type;
67
69
  }
70
+
71
+ maximumPeerResponseTimeMs(expectedProcessingTimeMs = DEFAULT_EXPECTED_PROCESSING_TIME_MS) {
72
+ return this.exchangeManager.calculateMaximumPeerResponseTimeMsFor(this.#channel, expectedProcessingTimeMs);
73
+ }
68
74
  }
69
75
 
70
76
  /**
@@ -125,4 +131,12 @@ export class ReconnectableExchangeProvider extends ExchangeProvider {
125
131
  }
126
132
  return this.channelManager.getChannel(this.#address).type;
127
133
  }
134
+
135
+ maximumPeerResponseTimeMs(expectedProcessingTimeMs = DEFAULT_EXPECTED_PROCESSING_TIME_MS) {
136
+ const channel = this.channelManager.getChannel(this.#address);
137
+ if (!channel) {
138
+ throw new ChannelNotConnectedError("Channel not connected");
139
+ }
140
+ return this.exchangeManager.calculateMaximumPeerResponseTimeMsFor(channel, expectedProcessingTimeMs);
141
+ }
128
142
  }
@@ -0,0 +1,163 @@
1
+ import { Message, MessageCodec } from "#codec/index.js";
2
+ import { Channel, Logger, MatterFlowError } from "#general";
3
+ import { DEFAULT_EXPECTED_PROCESSING_TIME_MS, ExchangeLogContext } from "#protocol/MessageExchange.js";
4
+ import { Session, SessionParameters } from "#session/index.js";
5
+
6
+ const logger = new Logger("MessageChannel");
7
+
8
+ /**
9
+ * The buffer time in milliseconds to add to the peer response time to also consider network delays and other factors.
10
+ * TODO: This is a pure guess and should be adjusted in the future.
11
+ */
12
+ const PEER_RESPONSE_TIME_BUFFER_MS = 5_000;
13
+
14
+ export namespace MRP {
15
+ /**
16
+ * The maximum number of transmission attempts for a given reliable message. The sender MAY choose this value as it
17
+ * sees fit.
18
+ */
19
+ export const MAX_TRANSMISSIONS = 5;
20
+
21
+ /** The base number for the exponential backoff equation. */
22
+ export const BACKOFF_BASE = 1.6;
23
+
24
+ /** The scaler for random jitter in the backoff equation. */
25
+ export const BACKOFF_JITTER = 0.25;
26
+
27
+ /** The scaler margin increase to backoff over the peer sleepy interval. */
28
+ export const BACKOFF_MARGIN = 1.1;
29
+
30
+ /** The number of retransmissions before transitioning from linear to exponential backoff. */
31
+ export const BACKOFF_THRESHOLD = 1;
32
+
33
+ /** @see {@link MatterSpecification.v12.Core}, section 4.11.8 */
34
+ export const STANDALONE_ACK_TIMEOUT_MS = 200;
35
+ }
36
+
37
+ export class MessageChannel implements Channel<Message> {
38
+ public closed = false;
39
+ #closeCallback?: () => Promise<void>;
40
+ // When the session is supporting MRP and the channel is not reliable, use MRP handling
41
+
42
+ constructor(
43
+ readonly channel: Channel<Uint8Array>,
44
+ readonly session: Session,
45
+ closeCallback?: () => Promise<void>,
46
+ ) {
47
+ this.#closeCallback = closeCallback;
48
+ }
49
+
50
+ set closeCallback(callback: () => Promise<void>) {
51
+ this.#closeCallback = callback;
52
+ }
53
+
54
+ get usesMrp() {
55
+ return this.session.supportsMRP && !this.channel.isReliable;
56
+ }
57
+
58
+ /** Is the underlying transport reliable? */
59
+ get isReliable() {
60
+ return this.channel.isReliable;
61
+ }
62
+
63
+ get type() {
64
+ return this.channel.type;
65
+ }
66
+
67
+ /**
68
+ * Max Payload size of the exchange which bases on the maximum payload size of the channel. The full encoded matter
69
+ * message payload sent here can be as huge as allowed by the channel.
70
+ */
71
+ get maxPayloadSize() {
72
+ return this.channel.maxPayloadSize;
73
+ }
74
+
75
+ async send(message: Message, logContext?: ExchangeLogContext) {
76
+ logger.debug("Message »", MessageCodec.messageDiagnostics(message, logContext));
77
+ const packet = this.session.encode(message);
78
+ const bytes = MessageCodec.encodePacket(packet);
79
+ if (bytes.length > this.maxPayloadSize) {
80
+ logger.warn(
81
+ `Matter message to send to ${this.name} is ${bytes.length}bytes long, which is larger than the maximum allowed size of ${this.maxPayloadSize}. This only works if both nodes support it.`,
82
+ );
83
+ }
84
+
85
+ return await this.channel.send(bytes);
86
+ }
87
+
88
+ get name() {
89
+ return `${this.channel.name} on session ${this.session.name}`;
90
+ }
91
+
92
+ async close() {
93
+ const wasAlreadyClosed = this.closed;
94
+ this.closed = true;
95
+ await this.channel.close();
96
+ if (!wasAlreadyClosed) {
97
+ await this.#closeCallback?.();
98
+ }
99
+ }
100
+
101
+ calculateMaximumPeerResponseTimeMs(
102
+ sessionParameters: SessionParameters,
103
+ expectedProcessingTimeMs = DEFAULT_EXPECTED_PROCESSING_TIME_MS,
104
+ ) {
105
+ switch (this.channel.type) {
106
+ case "tcp":
107
+ // TCP uses 30s timeout according to chip sdk implementation, so do the same
108
+ return 30_000 + PEER_RESPONSE_TIME_BUFFER_MS;
109
+ case "udp":
110
+ // UDP normally uses MRP, if not we have Group communication, which normally have no responses
111
+ if (!this.usesMrp) {
112
+ throw new MatterFlowError("No response expected for this message exchange because UDP and no MRP.");
113
+ }
114
+ return (
115
+ this.#calculateMrpMaximumPeerResponseTime(sessionParameters, expectedProcessingTimeMs) +
116
+ PEER_RESPONSE_TIME_BUFFER_MS
117
+ );
118
+ case "ble":
119
+ // chip sdk uses BTP_ACK_TIMEOUT_MS which is wrong in my eyes, so we use static 30s as like TCP here
120
+ return 30_000 + PEER_RESPONSE_TIME_BUFFER_MS;
121
+ default:
122
+ throw new MatterFlowError(
123
+ `Can not calculate expected timeout for unknown channel type: ${this.channel.type}`,
124
+ );
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Calculates the backoff time for a resubmission based on the current retransmission count.
130
+ * If no session parameters are provided, the parameters of the current session are used.
131
+ * If session parameters are provided, the method can be used to calculate the maximum backoff time for the other
132
+ * side of the exchange.
133
+ *
134
+ * @see {@link MatterSpecification.v10.Core}, section 4.11.2.1
135
+ */
136
+ getMrpResubmissionBackOffTime(retransmissionCount: number, sessionParameters?: SessionParameters) {
137
+ const { activeIntervalMs, idleIntervalMs } = sessionParameters ?? this.session.parameters;
138
+ const baseInterval =
139
+ sessionParameters !== undefined || this.session.isPeerActive() ? activeIntervalMs : idleIntervalMs;
140
+ return Math.floor(
141
+ MRP.BACKOFF_MARGIN *
142
+ baseInterval *
143
+ Math.pow(MRP.BACKOFF_BASE, Math.max(0, retransmissionCount - MRP.BACKOFF_THRESHOLD)) *
144
+ (1 + (sessionParameters !== undefined ? 1 : Math.random()) * MRP.BACKOFF_JITTER),
145
+ );
146
+ }
147
+
148
+ #calculateMrpMaximumPeerResponseTime(
149
+ sessionParameters: SessionParameters,
150
+ expectedProcessingTimeMs = DEFAULT_EXPECTED_PROCESSING_TIME_MS,
151
+ ) {
152
+ // We use the expected processing time and deduct the time we already waited since last resubmission
153
+ let finalWaitTime = expectedProcessingTimeMs;
154
+
155
+ // and then add the time the other side needs for a full resubmission cycle under the assumption we are active
156
+ for (let i = 0; i < MRP.MAX_TRANSMISSIONS; i++) {
157
+ finalWaitTime += this.getMrpResubmissionBackOffTime(i, sessionParameters);
158
+ }
159
+
160
+ // TODO: Also add any network latency buffer, for now lets consider it's included in the processing time already
161
+ return finalWaitTime;
162
+ }
163
+ }