@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.
- package/dist/cjs/cluster/client/ClusterClient.d.ts.map +1 -1
- package/dist/cjs/cluster/client/ClusterClient.js +7 -1
- package/dist/cjs/cluster/client/ClusterClient.js.map +1 -1
- package/dist/cjs/cluster/client/ClusterClientTypes.d.ts +10 -0
- package/dist/cjs/cluster/client/ClusterClientTypes.d.ts.map +1 -1
- package/dist/cjs/interaction/InteractionClient.d.ts +8 -1
- package/dist/cjs/interaction/InteractionClient.d.ts.map +1 -1
- package/dist/cjs/interaction/InteractionClient.js +15 -10
- package/dist/cjs/interaction/InteractionClient.js.map +1 -1
- package/dist/cjs/interaction/InteractionMessenger.d.ts +0 -1
- package/dist/cjs/interaction/InteractionMessenger.d.ts.map +1 -1
- package/dist/cjs/interaction/InteractionMessenger.js +0 -3
- package/dist/cjs/interaction/InteractionMessenger.js.map +1 -1
- package/dist/cjs/interaction/SubscriptionClient.d.ts +1 -1
- package/dist/cjs/interaction/SubscriptionClient.d.ts.map +1 -1
- package/dist/cjs/interaction/SubscriptionClient.js +1 -1
- package/dist/cjs/peer/ControllerCommissioner.d.ts +3 -2
- package/dist/cjs/peer/ControllerCommissioner.d.ts.map +1 -1
- package/dist/cjs/peer/ControllerCommissioner.js +6 -5
- package/dist/cjs/peer/ControllerCommissioner.js.map +1 -1
- package/dist/cjs/peer/ControllerCommissioningFlow.d.ts.map +1 -1
- package/dist/cjs/peer/ControllerCommissioningFlow.js +81 -52
- package/dist/cjs/peer/ControllerCommissioningFlow.js.map +1 -1
- package/dist/cjs/peer/PeerSet.d.ts +4 -3
- package/dist/cjs/peer/PeerSet.d.ts.map +1 -1
- package/dist/cjs/peer/PeerSet.js +9 -8
- package/dist/cjs/peer/PeerSet.js.map +1 -1
- package/dist/cjs/protocol/ChannelManager.d.ts +2 -2
- package/dist/cjs/protocol/ChannelManager.d.ts.map +1 -1
- package/dist/cjs/protocol/ChannelManager.js +4 -4
- package/dist/cjs/protocol/ChannelManager.js.map +1 -1
- package/dist/cjs/protocol/ExchangeManager.d.ts +5 -24
- package/dist/cjs/protocol/ExchangeManager.d.ts.map +1 -1
- package/dist/cjs/protocol/ExchangeManager.js +12 -55
- package/dist/cjs/protocol/ExchangeManager.js.map +1 -1
- package/dist/cjs/protocol/ExchangeProvider.d.ts +10 -6
- package/dist/cjs/protocol/ExchangeProvider.d.ts.map +1 -1
- package/dist/cjs/protocol/ExchangeProvider.js +12 -1
- package/dist/cjs/protocol/ExchangeProvider.js.map +1 -1
- package/dist/cjs/protocol/MessageChannel.d.ts +52 -0
- package/dist/cjs/protocol/MessageChannel.d.ts.map +1 -0
- package/dist/cjs/protocol/MessageChannel.js +130 -0
- package/dist/cjs/protocol/MessageChannel.js.map +6 -0
- package/dist/cjs/protocol/MessageExchange.d.ts +5 -5
- package/dist/cjs/protocol/MessageExchange.d.ts.map +1 -1
- package/dist/cjs/protocol/MessageExchange.js +34 -78
- package/dist/cjs/protocol/MessageExchange.js.map +1 -1
- package/dist/cjs/protocol/index.d.ts +1 -0
- package/dist/cjs/protocol/index.d.ts.map +1 -1
- package/dist/cjs/protocol/index.js +1 -0
- package/dist/cjs/protocol/index.js.map +1 -1
- package/dist/esm/cluster/client/ClusterClient.d.ts.map +1 -1
- package/dist/esm/cluster/client/ClusterClient.js +7 -1
- package/dist/esm/cluster/client/ClusterClient.js.map +1 -1
- package/dist/esm/cluster/client/ClusterClientTypes.d.ts +10 -0
- package/dist/esm/cluster/client/ClusterClientTypes.d.ts.map +1 -1
- package/dist/esm/interaction/InteractionClient.d.ts +8 -1
- package/dist/esm/interaction/InteractionClient.d.ts.map +1 -1
- package/dist/esm/interaction/InteractionClient.js +15 -10
- package/dist/esm/interaction/InteractionClient.js.map +1 -1
- package/dist/esm/interaction/InteractionMessenger.d.ts +0 -1
- package/dist/esm/interaction/InteractionMessenger.d.ts.map +1 -1
- package/dist/esm/interaction/InteractionMessenger.js +0 -3
- package/dist/esm/interaction/InteractionMessenger.js.map +1 -1
- package/dist/esm/interaction/SubscriptionClient.d.ts +1 -1
- package/dist/esm/interaction/SubscriptionClient.d.ts.map +1 -1
- package/dist/esm/interaction/SubscriptionClient.js +1 -1
- package/dist/esm/peer/ControllerCommissioner.d.ts +3 -2
- package/dist/esm/peer/ControllerCommissioner.d.ts.map +1 -1
- package/dist/esm/peer/ControllerCommissioner.js +4 -3
- package/dist/esm/peer/ControllerCommissioner.js.map +1 -1
- package/dist/esm/peer/ControllerCommissioningFlow.d.ts.map +1 -1
- package/dist/esm/peer/ControllerCommissioningFlow.js +81 -52
- package/dist/esm/peer/ControllerCommissioningFlow.js.map +1 -1
- package/dist/esm/peer/PeerSet.d.ts +4 -3
- package/dist/esm/peer/PeerSet.d.ts.map +1 -1
- package/dist/esm/peer/PeerSet.js +5 -4
- package/dist/esm/peer/PeerSet.js.map +1 -1
- package/dist/esm/protocol/ChannelManager.d.ts +2 -2
- package/dist/esm/protocol/ChannelManager.d.ts.map +1 -1
- package/dist/esm/protocol/ChannelManager.js +2 -2
- package/dist/esm/protocol/ChannelManager.js.map +1 -1
- package/dist/esm/protocol/ExchangeManager.d.ts +5 -24
- package/dist/esm/protocol/ExchangeManager.d.ts.map +1 -1
- package/dist/esm/protocol/ExchangeManager.js +13 -56
- package/dist/esm/protocol/ExchangeManager.js.map +1 -1
- package/dist/esm/protocol/ExchangeProvider.d.ts +10 -6
- package/dist/esm/protocol/ExchangeProvider.d.ts.map +1 -1
- package/dist/esm/protocol/ExchangeProvider.js +12 -1
- package/dist/esm/protocol/ExchangeProvider.js.map +1 -1
- package/dist/esm/protocol/MessageChannel.d.ts +52 -0
- package/dist/esm/protocol/MessageChannel.d.ts.map +1 -0
- package/dist/esm/protocol/MessageChannel.js +110 -0
- package/dist/esm/protocol/MessageChannel.js.map +6 -0
- package/dist/esm/protocol/MessageExchange.d.ts +5 -5
- package/dist/esm/protocol/MessageExchange.d.ts.map +1 -1
- package/dist/esm/protocol/MessageExchange.js +39 -83
- package/dist/esm/protocol/MessageExchange.js.map +1 -1
- package/dist/esm/protocol/index.d.ts +1 -0
- package/dist/esm/protocol/index.d.ts.map +1 -1
- package/dist/esm/protocol/index.js +1 -0
- package/dist/esm/protocol/index.js.map +1 -1
- package/package.json +6 -6
- package/src/cluster/client/ClusterClient.ts +8 -1
- package/src/cluster/client/ClusterClientTypes.ts +12 -0
- package/src/interaction/InteractionClient.ts +29 -16
- package/src/interaction/InteractionMessenger.ts +0 -4
- package/src/interaction/SubscriptionClient.ts +2 -2
- package/src/peer/ControllerCommissioner.ts +4 -3
- package/src/peer/ControllerCommissioningFlow.ts +96 -57
- package/src/peer/PeerSet.ts +5 -4
- package/src/protocol/ChannelManager.ts +3 -3
- package/src/protocol/ExchangeManager.ts +18 -67
- package/src/protocol/ExchangeProvider.ts +20 -6
- package/src/protocol/MessageChannel.ts +163 -0
- package/src/protocol/MessageExchange.ts +40 -119
- 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
|
|
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
|
-
#
|
|
200
|
+
#currentFailSafeEndTime: number | undefined;
|
|
201
201
|
protected lastBreadcrumb = 1;
|
|
202
202
|
protected collectedCommissioningData: CollectedCommissioningData = {};
|
|
203
|
-
#
|
|
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.#
|
|
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
|
-
|
|
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, ${
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
{
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
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
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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(
|
|
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
|
-
{
|
|
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.#
|
|
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.#
|
|
1355
|
+
this.#currentFailSafeEndTime = undefined; // gets deactivated when successful
|
|
1317
1356
|
|
|
1318
1357
|
return {
|
|
1319
1358
|
code: CommissioningStepResultCode.Success,
|
package/src/peer/PeerSet.ts
CHANGED
|
@@ -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 {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
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 {
|
|
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
|
+
}
|