diff --git a/.github/workflows/java-tests.yaml b/.github/workflows/java-tests.yaml index f19d451fbc2df6..3aa6935d1735c4 100644 --- a/.github/workflows/java-tests.yaml +++ b/.github/workflows/java-tests.yaml @@ -132,6 +132,17 @@ jobs: --tool-cluster "im" \ --tool-args "onnetwork-long-im-invoke --nodeid 1 --setup-pin-code 20202021 --discriminator 3840 -t 1000" \ --factoryreset \ + ' + - name: Run IM Batch Invoke Test + run: | + scripts/run_in_python_env.sh out/venv \ + './scripts/tests/run_java_test.py \ + --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app \ + --app-args "--discriminator 3840 --interface-id -1" \ + --tool-path out/linux-x64-java-matter-controller \ + --tool-cluster "im" \ + --tool-args "onnetwork-long-im-batch-invoke --nodeid 1 --setup-pin-code 20202021 --discriminator 3840 -t 1000" \ + --factoryreset \ ' - name: Run IM Read Test run: | diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e5afb69fabd2b1..a5398ccb7567a2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -203,7 +203,7 @@ jobs: # TODO: TLVDebug should ideally not be excluded here. # TODO: protocol_decoder.cpp should ideally not be excluded here. # TODO: PersistentStorageMacros.h should ideally not be excluded here. - git grep -I -n "PRI.64" -- './*' ':(exclude).github/workflows/lint.yml' ':(exclude)examples/chip-tool' ':(exclude)examples/tv-casting-app' ':(exclude)src/app/MessageDef/MessageDefHelper.cpp' ':(exclude)src/app/tests/integration/chip_im_initiator.cpp' ':(exclude)src/lib/core/TLVDebug.cpp' ':(exclude)src/lib/dnssd/tests/TestTxtFields.cpp' ':(exclude)src/lib/format/protocol_decoder.cpp' ':(exclude)src/lib/support/PersistentStorageMacros.h' ':(exclude)src/messaging/tests/echo/echo_requester.cpp' ':(exclude)src/platform/Linux' ':(exclude)src/platform/Ameba' ':(exclude)src/platform/ESP32' ':(exclude)src/platform/webos' ':(exclude)zzz_generated/chip-tool' ':(exclude)src/tools/chip-cert/Cmd_PrintCert.cpp' && exit 1 || exit 0 + git grep -I -n "PRI.64" -- './*' ':(exclude).github/workflows/lint.yml' ':(exclude)examples/chip-tool' ':(exclude)examples/tv-casting-app' ':(exclude)src/app/MessageDef/MessageDefHelper.cpp' ':(exclude)src/app/tests/integration/chip_im_initiator.cpp' ':(exclude)src/lib/core/TLVDebug.cpp' ':(exclude)src/lib/dnssd/tests/TestTxtFields.cpp' ':(exclude)src/lib/format/protocol_decoder.cpp' ':(exclude)src/lib/support/PersistentStorageMacros.h' ':(exclude)src/messaging/tests/echo/echo_requester.cpp' ':(exclude)src/platform/Linux' ':(exclude)src/platform/Ameba' ':(exclude)src/platform/ESP32' ':(exclude)src/platform/Darwin' ':(exclude)src/darwin' ':(exclude)src/platform/webos' ':(exclude)zzz_generated/chip-tool' ':(exclude)src/tools/chip-cert/Cmd_PrintCert.cpp' && exit 1 || exit 0 # git grep exits with 0 if it finds a match, but we want # to fail (exit nonzero) on match. And we want to exclude this file, diff --git a/examples/java-matter-controller/BUILD.gn b/examples/java-matter-controller/BUILD.gn index 34de1ed5585c12..76acd190eade16 100644 --- a/examples/java-matter-controller/BUILD.gn +++ b/examples/java-matter-controller/BUILD.gn @@ -57,6 +57,7 @@ kotlin_binary("java-matter-controller") { "java/src/com/matter/controller/commands/pairing/PairOnNetworkFabricCommand.kt", "java/src/com/matter/controller/commands/pairing/PairOnNetworkInstanceNameCommand.kt", "java/src/com/matter/controller/commands/pairing/PairOnNetworkLongCommand.kt", + "java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImExtendableInvokeCommand.kt", "java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImInvokeCommand.kt", "java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImReadCommand.kt", "java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImSubscribeCommand.kt", diff --git a/examples/java-matter-controller/java/src/com/matter/controller/Main.kt b/examples/java-matter-controller/java/src/com/matter/controller/Main.kt index d0e2ef892b0e95..a1a66a8420b196 100644 --- a/examples/java-matter-controller/java/src/com/matter/controller/Main.kt +++ b/examples/java-matter-controller/java/src/com/matter/controller/Main.kt @@ -67,6 +67,7 @@ private fun getImCommands( PairOnNetworkLongImSubscribeCommand(controller, credentialsIssuer), PairOnNetworkLongImWriteCommand(controller, credentialsIssuer), PairOnNetworkLongImInvokeCommand(controller, credentialsIssuer), + PairOnNetworkLongImExtendableInvokeCommand(controller, credentialsIssuer), ) } diff --git a/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImExtendableInvokeCommand.kt b/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImExtendableInvokeCommand.kt new file mode 100644 index 00000000000000..d80e71acb53d73 --- /dev/null +++ b/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImExtendableInvokeCommand.kt @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.matter.controller.commands.pairing + +import chip.devicecontroller.ChipDeviceController +import chip.devicecontroller.ExtendableInvokeCallback +import chip.devicecontroller.GetConnectedDeviceCallbackJni.GetConnectedDeviceCallback +import chip.devicecontroller.model.InvokeElement +import chip.devicecontroller.model.InvokeResponseData +import chip.devicecontroller.model.NoInvokeResponseData +import chip.devicecontroller.model.Status +import com.matter.controller.commands.common.CredentialsIssuer +import java.util.logging.Level +import java.util.logging.Logger +import kotlin.UShort +import matter.tlv.AnonymousTag +import matter.tlv.ContextSpecificTag +import matter.tlv.TlvWriter + +class PairOnNetworkLongImExtendableInvokeCommand( + controller: ChipDeviceController, + credsIssue: CredentialsIssuer? +) : + PairingCommand( + controller, + "onnetwork-long-im-batch-invoke", + credsIssue, + PairingModeType.ON_NETWORK, + PairingNetworkType.NONE, + DiscoveryFilterType.LONG_DISCRIMINATOR + ) { + private var devicePointer: Long = 0 + + private fun setDevicePointer(devicePointer: Long) { + this.devicePointer = devicePointer + } + + private inner class InternalInvokeCallback : ExtendableInvokeCallback { + private var responseCount = 0 + + override fun onError(e: Exception) { + logger.log(Level.INFO, "Batch Invoke receive onError" + e.message) + setFailure("invoke failure") + } + + override fun onResponse(invokeResponseData: InvokeResponseData) { + logger.log(Level.INFO, "Batch Invoke receive OnResponse on $invokeResponseData") + val clusterId = invokeResponseData.getClusterId().getId() + val commandId = invokeResponseData.getCommandId().getId() + val tlvData = invokeResponseData.getTlvByteArray() + val jsonData = invokeResponseData.getJsonString() + val status = invokeResponseData.getStatus() + + if (clusterId == CLUSTER_ID_IDENTIFY && commandId == IDENTIFY_COMMAND) { + if (tlvData != null || jsonData != null) { + setFailure("invoke failure with problematic payload") + } + if ( + status != null && status.status != Status.Code.Success && status.clusterStatus.isPresent() + ) { + setFailure("invoke failure with incorrect status") + } + } + + if (clusterId == CLUSTER_ID_TEST && commandId == TEST_ADD_ARGUMENT_RSP_COMMAND) { + if (tlvData == null || jsonData == null) { + setFailure("invoke failure with problematic payload") + } + + if (!jsonData.equals("""{"0:UINT":2}""")) { + setFailure("invoke failure with problematic json") + } + + if (status != null) { + setFailure("invoke failure with incorrect status") + } + } + responseCount++ + } + + override fun onNoResponse(noInvokeResponseData: NoInvokeResponseData) { + logger.log(Level.INFO, "Batch Invoke receive onNoResponse on $noInvokeResponseData") + } + + override fun onDone() { + if (responseCount == TEST_COMMONDS_NUM) { + setSuccess() + } else { + setFailure("invoke failure") + } + } + } + + private inner class InternalGetConnectedDeviceCallback : GetConnectedDeviceCallback { + override fun onDeviceConnected(devicePointer: Long) { + setDevicePointer(devicePointer) + logger.log(Level.INFO, "onDeviceConnected") + } + + override fun onConnectionFailure(nodeId: Long, error: Exception) { + logger.log(Level.INFO, "onConnectionFailure") + } + } + + override fun runCommand() { + val number: UShort = 1u + val tlvWriter1 = TlvWriter() + tlvWriter1.startStructure(AnonymousTag) + tlvWriter1.put(ContextSpecificTag(0), number) + tlvWriter1.endStructure() + + val element1: InvokeElement = + InvokeElement.newInstance( + /* endpointId= */ 0, + CLUSTER_ID_IDENTIFY, + IDENTIFY_COMMAND, + tlvWriter1.getEncoded(), + null + ) + + val tlvWriter2 = TlvWriter() + tlvWriter2.startStructure(AnonymousTag) + tlvWriter2.put(ContextSpecificTag(0), number) + tlvWriter2.put(ContextSpecificTag(1), number) + tlvWriter2.endStructure() + + val element2: InvokeElement = + InvokeElement.newInstance( + /* endpointId= */ 1, + CLUSTER_ID_TEST, + TEST_ADD_ARGUMENT_COMMAND, + tlvWriter2.getEncoded(), + null + ) + + val invokeList = listOf(element1, element2) + currentCommissioner() + .pairDeviceWithAddress( + getNodeId(), + getRemoteAddr().address.hostAddress, + MATTER_PORT, + getDiscriminator(), + getSetupPINCode(), + null + ) + currentCommissioner().setCompletionListener(this) + waitCompleteMs(getTimeoutMillis()) + currentCommissioner() + .getConnectedDevicePointer(getNodeId(), InternalGetConnectedDeviceCallback()) + clear() + currentCommissioner() + .extendableInvoke(InternalInvokeCallback(), devicePointer, invokeList, 0, 0) + waitCompleteMs(getTimeoutMillis()) + } + + companion object { + private val logger = + Logger.getLogger(PairOnNetworkLongImExtendableInvokeCommand::class.java.name) + + private const val MATTER_PORT = 5540 + private const val CLUSTER_ID_IDENTIFY = 0x0003L + private const val IDENTIFY_COMMAND = 0L + private const val CLUSTER_ID_TEST = 0xFFF1FC05L + private const val TEST_ADD_ARGUMENT_COMMAND = 0X04L + private const val TEST_ADD_ARGUMENT_RSP_COMMAND = 0X01L + private const val TEST_COMMONDS_NUM = 2 + } +} diff --git a/kotlin-detect-config.yaml b/kotlin-detect-config.yaml index 2ad54a05d6c483..c8398293d93e8b 100644 --- a/kotlin-detect-config.yaml +++ b/kotlin-detect-config.yaml @@ -104,6 +104,7 @@ style: - "**/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkInstanceNameCommand.kt" - "**/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongCommand.kt" - "**/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImInvokeCommand.kt" + - "**/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImExtendableInvokeCommand.kt" - "**/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImWriteCommand.kt" - "**/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkShortCommand.kt" - "**/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkVendorCommand.kt" diff --git a/scripts/tests/java/im_test.py b/scripts/tests/java/im_test.py index d8acc6362e000d..6ac393629fbee8 100755 --- a/scripts/tests/java/im_test.py +++ b/scripts/tests/java/im_test.py @@ -71,6 +71,14 @@ def TestCmdOnnetworkLongImInvoke(self, nodeid, setuppin, discriminator, timeout) DumpProgramOutputToQueue(self.thread_list, Fore.GREEN + "JAVA " + Style.RESET_ALL, java_process, self.queue) return java_process.wait() + def TestCmdOnnetworkLongImExtendableInvoke(self, nodeid, setuppin, discriminator, timeout): + java_command = self.command + ['im', 'onnetwork-long-im-batch-invoke', nodeid, setuppin, discriminator, timeout] + logging.info(f"Execute: {java_command}") + java_process = subprocess.Popen( + java_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + DumpProgramOutputToQueue(self.thread_list, Fore.GREEN + "JAVA " + Style.RESET_ALL, java_process, self.queue) + return java_process.wait() + def TestCmdOnnetworkLongImWrite(self, nodeid, setuppin, discriminator, timeout): java_command = self.command + ['im', 'onnetwork-long-im-write', nodeid, setuppin, discriminator, timeout] logging.info(f"Execute: {java_command}") @@ -101,6 +109,11 @@ def RunTest(self): code = self.TestCmdOnnetworkLongImInvoke(self.nodeid, self.setup_pin_code, self.discriminator, self.timeout) if code != 0: raise Exception(f"Testing pairing onnetwork-long-im-invoke failed with error {code}") + elif self.command_name == 'onnetwork-long-im-batch-invoke': + logging.info("Testing pairing onnetwork-long-im-batch-invoke") + code = self.TestCmdOnnetworkLongImExtendableInvoke(self.nodeid, self.setup_pin_code, self.discriminator, self.timeout) + if code != 0: + raise Exception(f"Testing pairing onnetwork-long-im-batch-invoke failed with error {code}") elif self.command_name == 'onnetwork-long-im-write': logging.info("Testing pairing onnetwork-long-im-write") code = self.TestCmdOnnetworkLongImWrite(self.nodeid, self.setup_pin_code, self.discriminator, self.timeout) diff --git a/src/app/clusters/on-off-server/on-off-server.cpp b/src/app/clusters/on-off-server/on-off-server.cpp index ac1c4a0c3bf3c6..e4ba82bb8bdc2f 100644 --- a/src/app/clusters/on-off-server/on-off-server.cpp +++ b/src/app/clusters/on-off-server/on-off-server.cpp @@ -214,6 +214,7 @@ class DefaultOnOffSceneHandler : public scenes::DefaultSceneHandlerImpl ScenesManagement::ScenesServer::Instance().IsHandlerRegistered(endpoint, LevelControlServer::GetSceneHandler()))) #endif { + VerifyOrReturnError(mTransitionTimeInterface.sceneEventControl(endpoint) != nullptr, CHIP_ERROR_INVALID_ARGUMENT); OnOffServer::Instance().scheduleTimerCallbackMs(mTransitionTimeInterface.sceneEventControl(endpoint), timeMs); } @@ -221,7 +222,7 @@ class DefaultOnOffSceneHandler : public scenes::DefaultSceneHandlerImpl } private: - OnOffTransitionTimeInterface mTransitionTimeInterface = OnOffTransitionTimeInterface(Attributes::OnOff::Id, sceneOnOffCallback); + OnOffTransitionTimeInterface mTransitionTimeInterface = OnOffTransitionTimeInterface(OnOff::Id, sceneOnOffCallback); }; static DefaultOnOffSceneHandler sOnOffSceneHandler; diff --git a/src/controller/AutoCommissioner.cpp b/src/controller/AutoCommissioner.cpp index 69a9c9b9c6bbf3..557cdf43a6ad9c 100644 --- a/src/controller/AutoCommissioner.cpp +++ b/src/controller/AutoCommissioner.cpp @@ -673,20 +673,10 @@ CHIP_ERROR AutoCommissioner::CommissioningStepFinished(CHIP_ERROR err, Commissio { CompletionStatus completionStatus; completionStatus.err = err; - - if (err == CHIP_NO_ERROR) - { - ChipLogProgress(Controller, "Successfully finished commissioning step '%s'", StageToString(report.stageCompleted)); - } - else - { - ChipLogProgress(Controller, "Error on commissioning step '%s': '%s'", StageToString(report.stageCompleted), err.AsString()); - } - if (err != CHIP_NO_ERROR) { + ChipLogError(Controller, "Error on commissioning step '%s': '%s'", StageToString(report.stageCompleted), err.AsString()); completionStatus.failedStage = MakeOptional(report.stageCompleted); - ChipLogError(Controller, "Failed to perform commissioning step %d", static_cast(report.stageCompleted)); if (report.Is()) { completionStatus.attestationResult = MakeOptional(report.Get().attestationResult); @@ -727,19 +717,14 @@ CHIP_ERROR AutoCommissioner::CommissioningStepFinished(CHIP_ERROR err, Commissio } else { + ChipLogProgress(Controller, "Successfully finished commissioning step '%s'", StageToString(report.stageCompleted)); switch (report.stageCompleted) { case CommissioningStage::kReadCommissioningInfo: break; case CommissioningStage::kReadCommissioningInfo2: { - if (!report.Is()) - { - ChipLogError(Controller, - "[BUG] Should read commissioning info, but report is not ReadCommissioningInfo. THIS IS A BUG."); - } - ReadCommissioningInfo commissioningInfo = report.Get(); - mDeviceCommissioningInfo = report.Get(); + if (!mParams.GetFailsafeTimerSeconds().HasValue() && mDeviceCommissioningInfo.general.recommendedFailsafe > 0) { mParams.SetFailsafeTimerSeconds(mDeviceCommissioningInfo.general.recommendedFailsafe); @@ -751,11 +736,11 @@ CHIP_ERROR AutoCommissioner::CommissioningStepFinished(CHIP_ERROR err, Commissio // Don't send DST unless the device says it needs it mNeedsDST = false; - mParams.SetSupportsConcurrentConnection(commissioningInfo.supportsConcurrentConnection); + mParams.SetSupportsConcurrentConnection(mDeviceCommissioningInfo.supportsConcurrentConnection); if (mParams.GetCheckForMatchingFabric()) { - chip::NodeId nodeId = commissioningInfo.remoteNodeId; + chip::NodeId nodeId = mDeviceCommissioningInfo.remoteNodeId; if (nodeId != kUndefinedNodeId) { mParams.SetRemoteNodeId(nodeId); @@ -764,7 +749,7 @@ CHIP_ERROR AutoCommissioner::CommissioningStepFinished(CHIP_ERROR err, Commissio if (mParams.GetICDRegistrationStrategy() != ICDRegistrationStrategy::kIgnore) { - if (commissioningInfo.icd.isLIT && commissioningInfo.icd.checkInProtocolSupport) + if (mDeviceCommissioningInfo.icd.isLIT && mDeviceCommissioningInfo.icd.checkInProtocolSupport) { mNeedIcdRegistration = true; ChipLogDetail(Controller, "AutoCommissioner: ICD supports the check-in protocol."); diff --git a/src/controller/CHIPDeviceController.cpp b/src/controller/CHIPDeviceController.cpp index 69e95244dcce01..35c616ac0426db 100644 --- a/src/controller/CHIPDeviceController.cpp +++ b/src/controller/CHIPDeviceController.cpp @@ -38,11 +38,11 @@ #include #include -#include - #include #include +#include #include +#include #include #include #include @@ -400,11 +400,7 @@ DeviceCommissioner::DeviceCommissioner() : #endif // CHIP_DEVICE_CONFIG_ENABLE_AUTOMATIC_CASE_RETRIES mDeviceAttestationInformationVerificationCallback(OnDeviceAttestationInformationVerification, this), mDeviceNOCChainCallback(OnDeviceNOCChainGeneration, this), mSetUpCodePairer(this) -{ - mPairingDelegate = nullptr; - mDeviceBeingCommissioned = nullptr; - mDeviceInPASEEstablishment = nullptr; -} +{} CHIP_ERROR DeviceCommissioner::Init(CommissionerInitParams params) { @@ -483,7 +479,8 @@ void DeviceCommissioner::Shutdown() ChipLogDetail(Controller, "Setup in progress, stopping setup before shutting down"); OnSessionEstablishmentError(CHIP_ERROR_CONNECTION_ABORTED); } - // TODO: If we have a commissioning step in progress, is there a way to cancel that callback? + + CancelCommissioningInteractions(); #if CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONER_DISCOVERY // make this commissioner discoverable if (mUdcTransportMgr != nullptr) @@ -816,6 +813,13 @@ CHIP_ERROR DeviceCommissioner::Commission(NodeId remoteDeviceId, CommissioningPa CHIP_ERROR DeviceCommissioner::Commission(NodeId remoteDeviceId) { MATTER_TRACE_SCOPE("Commission", "DeviceCommissioner"); + + if (mDefaultCommissioner == nullptr) + { + ChipLogError(Controller, "No default commissioner is specified"); + return CHIP_ERROR_INCORRECT_STATE; + } + CommissioneeDeviceProxy * device = FindCommissioneeDevice(remoteDeviceId); if (device == nullptr || (!device->IsSecureConnected() && !device->IsSessionSetupInProgress())) { @@ -831,13 +835,8 @@ CHIP_ERROR DeviceCommissioner::Commission(NodeId remoteDeviceId) if (mCommissioningStage != CommissioningStage::kSecurePairing) { - ChipLogError(Controller, "Commissioning already in progress - not restarting"); - return CHIP_ERROR_INCORRECT_STATE; - } - - if (mDefaultCommissioner == nullptr) - { - ChipLogError(Controller, "No default commissioner is specified"); + ChipLogError(Controller, "Commissioning already in progress (stage '%s') - not restarting", + StageToString(mCommissioningStage)); return CHIP_ERROR_INCORRECT_STATE; } @@ -860,6 +859,13 @@ DeviceCommissioner::ContinueCommissioningAfterDeviceAttestation(DeviceProxy * de Credentials::AttestationVerificationResult attestationResult) { MATTER_TRACE_SCOPE("continueCommissioningDevice", "DeviceCommissioner"); + + if (mDefaultCommissioner == nullptr) + { + ChipLogError(Controller, "No default commissioner is specified"); + return CHIP_ERROR_INCORRECT_STATE; + } + if (device == nullptr || device != mDeviceBeingCommissioned) { ChipLogError(Controller, "Invalid device for commissioning %p", device); @@ -884,12 +890,6 @@ DeviceCommissioner::ContinueCommissioningAfterDeviceAttestation(DeviceProxy * de return CHIP_ERROR_INCORRECT_STATE; } - if (mDefaultCommissioner == nullptr) - { - ChipLogError(Controller, "No default commissioner is specified"); - return CHIP_ERROR_INCORRECT_STATE; - } - ChipLogProgress(Controller, "Continuing commissioning after attestation failure for device ID 0x" ChipLogFormatX64, ChipLogValueX64(commissioneeDevice->GetDeviceId())); @@ -920,6 +920,7 @@ CHIP_ERROR DeviceCommissioner::StopPairing(NodeId remoteDeviceId) // If we're still in the process of discovering the device, just stop the SetUpCodePairer if (mSetUpCodePairer.StopPairing(remoteDeviceId)) { + mRunCommissioningAfterConnection = false; return CHIP_NO_ERROR; } @@ -929,6 +930,7 @@ CHIP_ERROR DeviceCommissioner::StopPairing(NodeId remoteDeviceId) if (mDeviceBeingCommissioned == device) { + CancelCommissioningInteractions(); CommissioningStageComplete(CHIP_ERROR_CANCELLED); } else @@ -938,6 +940,21 @@ CHIP_ERROR DeviceCommissioner::StopPairing(NodeId remoteDeviceId) return CHIP_NO_ERROR; } +void DeviceCommissioner::CancelCommissioningInteractions() +{ + if (mReadClient) + { + ChipLogDetail(Controller, "Cancelling read request for step '%s'", StageToString(mCommissioningStage)); + mReadClient.reset(); // destructor cancels + } + if (mInvokeCancelFn) + { + ChipLogDetail(Controller, "Cancelling command invocation for step '%s'", StageToString(mCommissioningStage)); + mInvokeCancelFn(); + mInvokeCancelFn = nullptr; + } +} + CHIP_ERROR DeviceCommissioner::UnpairDevice(NodeId remoteDeviceId) { MATTER_TRACE_SCOPE("UnpairDevice", "DeviceCommissioner"); @@ -1014,7 +1031,8 @@ CHIP_ERROR DeviceCommissioner::SendCertificateChainRequestCommand(DeviceProxy * OperationalCredentials::Commands::CertificateChainRequest::Type request; request.certificateType = static_cast(certificateType); - return SendCommand(device, request, OnCertificateChainResponse, OnCertificateChainFailureResponse, timeout); + return SendCommissioningCommand(device, request, OnCertificateChainResponse, OnCertificateChainFailureResponse, kRootEndpointId, + timeout); } void DeviceCommissioner::OnCertificateChainFailureResponse(void * context, CHIP_ERROR error) @@ -1048,7 +1066,8 @@ CHIP_ERROR DeviceCommissioner::SendAttestationRequestCommand(DeviceProxy * devic OperationalCredentials::Commands::AttestationRequest::Type request; request.attestationNonce = attestationNonce; - ReturnErrorOnFailure(SendCommand(device, request, OnAttestationResponse, OnAttestationFailureResponse, timeout)); + ReturnErrorOnFailure( + SendCommissioningCommand(device, request, OnAttestationResponse, OnAttestationFailureResponse, kRootEndpointId, timeout)); ChipLogDetail(Controller, "Sent Attestation request, waiting for the Attestation Information"); return CHIP_NO_ERROR; } @@ -1218,7 +1237,7 @@ bool DeviceCommissioner::ExtendArmFailSafe(DeviceProxy * proxy, CommissioningSta request.expiryLengthSeconds = armFailSafeTimeout; request.breadcrumb = breadcrumb; ChipLogProgress(Controller, "Arming failsafe (%u seconds)", request.expiryLengthSeconds); - CHIP_ERROR err = SendCommand(proxy, request, onSuccess, onFailure, kRootEndpointId, commandTimeout); + CHIP_ERROR err = SendCommissioningCommand(proxy, request, onSuccess, onFailure, kRootEndpointId, commandTimeout); if (err != CHIP_NO_ERROR) { onFailure(this, err); @@ -1309,7 +1328,8 @@ CHIP_ERROR DeviceCommissioner::SendOperationalCertificateSigningRequestCommand(D OperationalCredentials::Commands::CSRRequest::Type request; request.CSRNonce = csrNonce; - ReturnErrorOnFailure(SendCommand(device, request, OnOperationalCertificateSigningRequest, OnCSRFailureResponse, timeout)); + ReturnErrorOnFailure(SendCommissioningCommand(device, request, OnOperationalCertificateSigningRequest, OnCSRFailureResponse, + kRootEndpointId, timeout)); ChipLogDetail(Controller, "Sent CSR request, waiting for the CSR"); return CHIP_NO_ERROR; } @@ -1431,7 +1451,8 @@ CHIP_ERROR DeviceCommissioner::SendOperationalCertificate(DeviceProxy * device, request.caseAdminSubject = adminSubject; request.adminVendorId = mVendorId; - ReturnErrorOnFailure(SendCommand(device, request, OnOperationalCertificateAddResponse, OnAddNOCFailureResponse, timeout)); + ReturnErrorOnFailure(SendCommissioningCommand(device, request, OnOperationalCertificateAddResponse, OnAddNOCFailureResponse, + kRootEndpointId, timeout)); ChipLogProgress(Controller, "Sent operational certificate to the device"); @@ -1515,7 +1536,8 @@ CHIP_ERROR DeviceCommissioner::SendTrustedRootCertificate(DeviceProxy * device, OperationalCredentials::Commands::AddTrustedRootCertificate::Type request; request.rootCACertificate = rcac; - ReturnErrorOnFailure(SendCommand(device, request, OnRootCertSuccessResponse, OnRootCertFailureResponse, timeout)); + ReturnErrorOnFailure( + SendCommissioningCommand(device, request, OnRootCertSuccessResponse, OnRootCertFailureResponse, kRootEndpointId, timeout)); ChipLogProgress(Controller, "Sent root certificate to the device"); @@ -1623,13 +1645,13 @@ void DeviceCommissioner::OnNodeDiscovered(const chip::Dnssd::DiscoveredNodeData mSetUpCodePairer.NotifyCommissionableDeviceDiscovered(nodeData); } -void OnBasicSuccess(void * context, const chip::app::DataModel::NullObjectType &) +void DeviceCommissioner::OnBasicSuccess(void * context, const chip::app::DataModel::NullObjectType &) { DeviceCommissioner * commissioner = static_cast(context); commissioner->CommissioningStageComplete(CHIP_NO_ERROR); } -void OnBasicFailure(void * context, CHIP_ERROR error) +void DeviceCommissioner::OnBasicFailure(void * context, CHIP_ERROR error) { ChipLogProgress(Controller, "Received failure response %s\n", chip::ErrorStr(error)); DeviceCommissioner * commissioner = static_cast(context); @@ -1638,7 +1660,7 @@ void OnBasicFailure(void * context, CHIP_ERROR error) void DeviceCommissioner::CleanupCommissioning(DeviceProxy * proxy, NodeId nodeId, const CompletionStatus & completionStatus) { - commissioningCompletionStatus = completionStatus; + mCommissioningCompletionStatus = completionStatus; if (completionStatus.err == CHIP_NO_ERROR) { @@ -1650,15 +1672,17 @@ void DeviceCommissioner::CleanupCommissioning(DeviceProxy * proxy, NodeId nodeId } // Send the callbacks, we're done. CommissioningStageComplete(CHIP_NO_ERROR); - SendCommissioningCompleteCallbacks(nodeId, commissioningCompletionStatus); + SendCommissioningCompleteCallbacks(nodeId, mCommissioningCompletionStatus); } - else if (completionStatus.failedStage.HasValue() && completionStatus.failedStage.Value() >= kWiFiNetworkSetup) + else if (completionStatus.failedStage.HasValue() && completionStatus.failedStage.Value() >= kWiFiNetworkSetup && + completionStatus.err != CHIP_ERROR_CANCELLED) { // If we were already doing network setup, we need to retain the pase session and start again from network setup stage. // We do not need to reset the failsafe here because we want to keep everything on the device up to this point, so just - // send the completion callbacks. + // send the completion callbacks (see "Commissioning Flows Error Handling" in the spec). This does not apply if + // we're cleaning up because cancellation has been requested via StopPairing(). CommissioningStageComplete(CHIP_NO_ERROR); - SendCommissioningCompleteCallbacks(nodeId, commissioningCompletionStatus); + SendCommissioningCompleteCallbacks(nodeId, mCommissioningCompletionStatus); } else { @@ -1671,8 +1695,8 @@ void DeviceCommissioner::CleanupCommissioning(DeviceProxy * proxy, NodeId nodeId ChipLogProgress(Controller, "Expiring failsafe on proxy %p", proxy); mDeviceBeingCommissioned = proxy; // We actually want to do the same thing on success or failure because we're already in a failure state - CHIP_ERROR err = SendCommand(proxy, request, OnDisarmFailsafe, OnDisarmFailsafeFailure, - /* timeout = */ NullOptional); + CHIP_ERROR err = SendCommissioningCommand(proxy, request, OnDisarmFailsafe, OnDisarmFailsafeFailure, kRootEndpointId, + /* timeout = */ NullOptional); if (err != CHIP_NO_ERROR) { // We won't get any async callbacks here, so just pretend like the @@ -1694,7 +1718,7 @@ void DeviceCommissioner::OnDisarmFailsafe(void * context, void DeviceCommissioner::OnDisarmFailsafeFailure(void * context, CHIP_ERROR error) { - ChipLogProgress(Controller, "Received failure response when disarming failsafe%s\n", chip::ErrorStr(error)); + ChipLogProgress(Controller, "Ignoring failure to disarm failsafe: %" CHIP_ERROR_FORMAT, error.Format()); DeviceCommissioner * commissioner = static_cast(context); commissioner->DisarmDone(); } @@ -1711,7 +1735,7 @@ void DeviceCommissioner::DisarmDone() // Signal completion - this will reset mDeviceBeingCommissioned. CommissioningStageComplete(CHIP_NO_ERROR); - SendCommissioningCompleteCallbacks(nodeId, commissioningCompletionStatus); + SendCommissioningCompleteCallbacks(nodeId, mCommissioningCompletionStatus); // If we've disarmed the failsafe, it's because we're starting again, so kill the pase connection. if (commissionee != nullptr) @@ -1722,7 +1746,10 @@ void DeviceCommissioner::DisarmDone() void DeviceCommissioner::SendCommissioningCompleteCallbacks(NodeId nodeId, const CompletionStatus & completionStatus) { + ChipLogProgress(Controller, "Commissioning complete for node ID 0x" ChipLogFormatX64 ": %s", ChipLogValueX64(nodeId), + (completionStatus.err == CHIP_NO_ERROR ? "success" : completionStatus.err.AsString())); mCommissioningStage = CommissioningStage::kSecurePairing; + if (mPairingDelegate == nullptr) { return; @@ -1746,17 +1773,12 @@ void DeviceCommissioner::CommissioningStageComplete(CHIP_ERROR err, Commissionin { // Once this stage is complete, reset mDeviceBeingCommissioned - this will be reset when the delegate calls the next step. MATTER_TRACE_SCOPE("CommissioningStageComplete", "DeviceCommissioner"); - if (mDeviceBeingCommissioned == nullptr) - { - // We are getting a stray callback (e.g. due to un-cancellable - // operations) when we are not in fact commissioning anything. Just - // ignore it. - return; - } + VerifyOrDie(mDeviceBeingCommissioned); NodeId nodeId = mDeviceBeingCommissioned->GetDeviceId(); DeviceProxy * proxy = mDeviceBeingCommissioned; mDeviceBeingCommissioned = nullptr; + mInvokeCancelFn = nullptr; if (mPairingDelegate != nullptr) { @@ -1769,7 +1791,7 @@ void DeviceCommissioner::CommissioningStageComplete(CHIP_ERROR err, Commissionin } report.stageCompleted = mCommissioningStage; CHIP_ERROR status = mCommissioningDelegate->CommissioningStepFinished(err, report); - if (status != CHIP_NO_ERROR) + if (status != CHIP_NO_ERROR && mCommissioningStage != CommissioningStage::kCleanup) { // Commissioning delegate will only return error if it failed to perform the appropriate commissioning step. // In this case, we should complete the commissioning for it. @@ -1898,9 +1920,10 @@ void DeviceCommissioner::OnDeviceConnectionRetryFn(void * context, const ScopedN #endif // CHIP_DEVICE_CONFIG_ENABLE_AUTOMATIC_CASE_RETRIES // ClusterStateCache::Callback impl -void DeviceCommissioner::OnDone(app::ReadClient *) +void DeviceCommissioner::OnDone(app::ReadClient * readClient) { - mReadClient = nullptr; + VerifyOrDie(readClient != nullptr && readClient == mReadClient.get()); + mReadClient.reset(); switch (mCommissioningStage) { case CommissioningStage::kReadCommissioningInfo: @@ -1913,6 +1936,7 @@ void DeviceCommissioner::OnDone(app::ReadClient *) ParseCommissioningInfo(); break; default: + VerifyOrDie(false); break; } } @@ -2484,9 +2508,31 @@ void DeviceCommissioner::OnCommissioningCompleteResponse( commissioner->CommissioningStageComplete(err, report); } +template +CHIP_ERROR +DeviceCommissioner::SendCommissioningCommand(DeviceProxy * device, const RequestObjectT & request, + CommandResponseSuccessCallback successCb, + CommandResponseFailureCallback failureCb, EndpointId endpoint, + Optional timeout) + +{ + VerifyOrDie(!mInvokeCancelFn); // we don't make parallel calls + + auto onSuccessCb = [context = this, successCb](const app::ConcreteCommandPath & aPath, const app::StatusIB & aStatus, + const typename RequestObjectT::ResponseType & responseData) { + successCb(context, responseData); + }; + auto onFailureCb = [context = this, failureCb](CHIP_ERROR aError) { failureCb(context, aError); }; + + return InvokeCommandRequest(device->GetExchangeManager(), device->GetSecureSession().Value(), endpoint, request, onSuccessCb, + onFailureCb, NullOptional, timeout, &mInvokeCancelFn); +} + void DeviceCommissioner::SendCommissioningReadRequest(DeviceProxy * proxy, Optional timeout, app::AttributePathParams * readPaths, size_t readPathsSize) { + VerifyOrDie(!mReadClient); // we don't perform parallel reads + app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance(); app::ReadPrepareParams readParams(proxy->GetSecureSession().Value()); readParams.mIsFabricFiltered = false; @@ -2635,7 +2681,7 @@ void DeviceCommissioner::PerformCommissioningStep(DeviceProxy * proxy, Commissio request.UTCTime = utcTime.count() - kChipEpochUsSinceUnixEpoch; // For now, we assume a seconds granularity request.granularity = TimeSynchronization::GranularityEnum::kSecondsGranularity; - CHIP_ERROR err = SendCommand(proxy, request, OnBasicSuccess, OnSetUTCError, endpoint, timeout); + CHIP_ERROR err = SendCommissioningCommand(proxy, request, OnBasicSuccess, OnSetUTCError, endpoint, timeout); if (err != CHIP_NO_ERROR) { // We won't get any async callbacks here, so just complete our stage. @@ -2654,7 +2700,7 @@ void DeviceCommissioner::PerformCommissioningStep(DeviceProxy * proxy, Commissio } TimeSynchronization::Commands::SetTimeZone::Type request; request.timeZone = params.GetTimeZone().Value(); - CHIP_ERROR err = SendCommand(proxy, request, OnSetTimeZoneResponse, OnBasicFailure, endpoint, timeout); + CHIP_ERROR err = SendCommissioningCommand(proxy, request, OnSetTimeZoneResponse, OnBasicFailure, endpoint, timeout); if (err != CHIP_NO_ERROR) { // We won't get any async callbacks here, so just complete our stage. @@ -2673,7 +2719,7 @@ void DeviceCommissioner::PerformCommissioningStep(DeviceProxy * proxy, Commissio } TimeSynchronization::Commands::SetDSTOffset::Type request; request.DSTOffset = params.GetDSTOffsets().Value(); - CHIP_ERROR err = SendCommand(proxy, request, OnBasicSuccess, OnBasicFailure, endpoint, timeout); + CHIP_ERROR err = SendCommissioningCommand(proxy, request, OnBasicSuccess, OnBasicFailure, endpoint, timeout); if (err != CHIP_NO_ERROR) { // We won't get any async callbacks here, so just complete our stage. @@ -2692,7 +2738,7 @@ void DeviceCommissioner::PerformCommissioningStep(DeviceProxy * proxy, Commissio } TimeSynchronization::Commands::SetDefaultNTP::Type request; request.defaultNTP = params.GetDefaultNTP().Value(); - CHIP_ERROR err = SendCommand(proxy, request, OnBasicSuccess, OnBasicFailure, endpoint, timeout); + CHIP_ERROR err = SendCommissioningCommand(proxy, request, OnBasicSuccess, OnBasicFailure, endpoint, timeout); if (err != CHIP_NO_ERROR) { // We won't get any async callbacks here, so just complete our stage. @@ -2709,7 +2755,7 @@ void DeviceCommissioner::PerformCommissioningStep(DeviceProxy * proxy, Commissio request.ssid.Emplace(params.GetWiFiCredentials().Value().ssid); } request.breadcrumb.Emplace(breadcrumb); - CHIP_ERROR err = SendCommand(proxy, request, OnScanNetworksResponse, OnScanNetworksFailure, endpoint, timeout); + CHIP_ERROR err = SendCommissioningCommand(proxy, request, OnScanNetworksResponse, OnScanNetworksFailure, endpoint, timeout); if (err != CHIP_NO_ERROR) { // We won't get any async callbacks here, so just complete our stage. @@ -2776,7 +2822,7 @@ void DeviceCommissioner::PerformCommissioningStep(DeviceProxy * proxy, Commissio request.newRegulatoryConfig = regulatoryConfig; request.countryCode = countryCode; request.breadcrumb = breadcrumb; - CHIP_ERROR err = SendCommand(proxy, request, OnSetRegulatoryConfigResponse, OnBasicFailure, endpoint, timeout); + CHIP_ERROR err = SendCommissioningCommand(proxy, request, OnSetRegulatoryConfigResponse, OnBasicFailure, endpoint, timeout); if (err != CHIP_NO_ERROR) { // We won't get any async callbacks here, so just complete our stage. @@ -2968,7 +3014,7 @@ void DeviceCommissioner::PerformCommissioningStep(DeviceProxy * proxy, Commissio } TimeSynchronization::Commands::SetTrustedTimeSource::Type request; request.trustedTimeSource = params.GetTrustedTimeSource().Value(); - CHIP_ERROR err = SendCommand(proxy, request, OnBasicSuccess, OnBasicFailure, endpoint, timeout); + CHIP_ERROR err = SendCommissioningCommand(proxy, request, OnBasicSuccess, OnBasicFailure, endpoint, timeout); if (err != CHIP_NO_ERROR) { // We won't get any async callbacks here, so just complete our stage. @@ -2990,7 +3036,7 @@ void DeviceCommissioner::PerformCommissioningStep(DeviceProxy * proxy, Commissio request.ssid = params.GetWiFiCredentials().Value().ssid; request.credentials = params.GetWiFiCredentials().Value().credentials; request.breadcrumb.Emplace(breadcrumb); - CHIP_ERROR err = SendCommand(proxy, request, OnNetworkConfigResponse, OnBasicFailure, endpoint, timeout); + CHIP_ERROR err = SendCommissioningCommand(proxy, request, OnNetworkConfigResponse, OnBasicFailure, endpoint, timeout); if (err != CHIP_NO_ERROR) { // We won't get any async callbacks here, so just complete our stage. @@ -3010,7 +3056,7 @@ void DeviceCommissioner::PerformCommissioningStep(DeviceProxy * proxy, Commissio NetworkCommissioning::Commands::AddOrUpdateThreadNetwork::Type request; request.operationalDataset = params.GetThreadOperationalDataset().Value(); request.breadcrumb.Emplace(breadcrumb); - CHIP_ERROR err = SendCommand(proxy, request, OnNetworkConfigResponse, OnBasicFailure, endpoint, timeout); + CHIP_ERROR err = SendCommissioningCommand(proxy, request, OnNetworkConfigResponse, OnBasicFailure, endpoint, timeout); if (err != CHIP_NO_ERROR) { // We won't get any async callbacks here, so just complete our stage. @@ -3044,7 +3090,7 @@ void DeviceCommissioner::PerformCommissioningStep(DeviceProxy * proxy, Commissio params.GetSupportsConcurrentConnection().HasValue() ? (params.GetSupportsConcurrentConnection().Value() ? "true" : "false") : "missing"); - err = SendCommand(proxy, request, OnConnectNetworkResponse, OnBasicFailure, endpoint, timeout); + err = SendCommissioningCommand(proxy, request, OnConnectNetworkResponse, OnBasicFailure, endpoint, timeout); if (err != CHIP_NO_ERROR) { @@ -3069,7 +3115,7 @@ void DeviceCommissioner::PerformCommissioningStep(DeviceProxy * proxy, Commissio NetworkCommissioning::Commands::ConnectNetwork::Type request; request.networkID = extendedPanId; request.breadcrumb.Emplace(breadcrumb); - CHIP_ERROR err = SendCommand(proxy, request, OnConnectNetworkResponse, OnBasicFailure, endpoint, timeout); + CHIP_ERROR err = SendCommissioningCommand(proxy, request, OnConnectNetworkResponse, OnBasicFailure, endpoint, timeout); if (err != CHIP_NO_ERROR) { // We won't get any async callbacks here, so just complete our stage. @@ -3099,7 +3145,8 @@ void DeviceCommissioner::PerformCommissioningStep(DeviceProxy * proxy, Commissio request.monitoredSubject = params.GetICDMonitoredSubject().Value(); request.key = params.GetICDSymmetricKey().Value(); - CHIP_ERROR err = SendCommand(proxy, request, OnICDManagementRegisterClientResponse, OnBasicFailure, endpoint, timeout); + CHIP_ERROR err = + SendCommissioningCommand(proxy, request, OnICDManagementRegisterClientResponse, OnBasicFailure, endpoint, timeout); if (err != CHIP_NO_ERROR) { // We won't get any async callbacks here, so just complete our stage. @@ -3137,7 +3184,8 @@ void DeviceCommissioner::PerformCommissioningStep(DeviceProxy * proxy, Commissio break; case CommissioningStage::kSendComplete: { GeneralCommissioning::Commands::CommissioningComplete::Type request; - CHIP_ERROR err = SendCommand(proxy, request, OnCommissioningCompleteResponse, OnBasicFailure, endpoint, timeout); + CHIP_ERROR err = + SendCommissioningCommand(proxy, request, OnCommissioningCompleteResponse, OnBasicFailure, endpoint, timeout); if (err != CHIP_NO_ERROR) { // We won't get any async callbacks here, so just complete our stage. diff --git a/src/controller/CHIPDeviceController.h b/src/controller/CHIPDeviceController.h index a7e6f6dac4bcb9..024421dd8a1a6e 100644 --- a/src/controller/CHIPDeviceController.h +++ b/src/controller/CHIPDeviceController.h @@ -84,10 +84,6 @@ using namespace chip::Protocols::UserDirectedCommissioning; inline constexpr uint16_t kNumMaxActiveDevices = CHIP_CONFIG_CONTROLLER_MAX_ACTIVE_DEVICES; -// Raw functions for cluster callbacks -void OnBasicFailure(void * context, CHIP_ERROR err); -void OnBasicSuccess(void * context, const chip::app::DataModel::NullObjectType &); - struct ControllerInitParams { DeviceControllerSystemState * systemState = nullptr; @@ -569,8 +565,12 @@ class DLL_EXPORT DeviceCommissioner : public DeviceController, /** * @brief - * This function stops a pairing process that's in progress. It does not delete the pairing of a previously - * paired device. + * This function stops a pairing or commissioning process that is in progress. + * It does not delete the pairing of a previously paired device. + * + * Note that cancelling an ongoing commissioning process is an asynchronous operation. + * The pairing delegate (if any) will receive OnCommissioningComplete and OnCommissioningFailure + * failure callbacks with a status code of CHIP_ERROR_CANCELLED once cancellation is complete. * * @param[in] remoteDeviceId The remote device Id. * @@ -769,13 +769,14 @@ class DLL_EXPORT DeviceCommissioner : public DeviceController, OnExtendFailsafeFailure onFailure); private: - DevicePairingDelegate * mPairingDelegate; + DevicePairingDelegate * mPairingDelegate = nullptr; DeviceProxy * mDeviceBeingCommissioned = nullptr; CommissioneeDeviceProxy * mDeviceInPASEEstablishment = nullptr; CommissioningStage mCommissioningStage = CommissioningStage::kSecurePairing; bool mRunCommissioningAfterConnection = false; + Internal::InvokeCancelFn mInvokeCancelFn; ObjectPool mCommissioneeDevicePool; @@ -794,6 +795,9 @@ class DLL_EXPORT DeviceCommissioner : public DeviceController, CHIP_ERROR LoadKeyId(PersistentStorageDelegate * delegate, uint16_t & out); + static void OnBasicFailure(void * context, CHIP_ERROR err); + static void OnBasicSuccess(void * context, const chip::app::DataModel::NullObjectType &); + /* This function sends a Device Attestation Certificate chain request to the device. The function does not hold a reference to the device object. */ @@ -955,26 +959,14 @@ class DLL_EXPORT DeviceCommissioner : public DeviceController, void ReleaseCommissioneeDevice(CommissioneeDeviceProxy * device); template - CHIP_ERROR SendCommand(DeviceProxy * device, const RequestObjectT & request, - CommandResponseSuccessCallback successCb, - CommandResponseFailureCallback failureCb, Optional timeout) - { - return SendCommand(device, request, successCb, failureCb, 0, timeout); - } - - template - CHIP_ERROR SendCommand(DeviceProxy * device, const RequestObjectT & request, - CommandResponseSuccessCallback successCb, - CommandResponseFailureCallback failureCb, EndpointId endpoint, Optional timeout) - { - ClusterBase cluster(*device->GetExchangeManager(), device->GetSecureSession().Value(), endpoint); - cluster.SetCommandTimeout(timeout); - - return cluster.InvokeCommand(request, this, successCb, failureCb); - } - + CHIP_ERROR SendCommissioningCommand(DeviceProxy * device, const RequestObjectT & request, + CommandResponseSuccessCallback successCb, + CommandResponseFailureCallback failureCb, EndpointId endpoint, + Optional timeout); void SendCommissioningReadRequest(DeviceProxy * proxy, Optional timeout, app::AttributePathParams * readPaths, size_t readPathsSize); + void CancelCommissioningInteractions(); + #if CHIP_CONFIG_ENABLE_READ_CLIENT void ParseCommissioningInfo(); // Parsing attributes read in kReadCommissioningInfo stage. @@ -1024,7 +1016,7 @@ class DLL_EXPORT DeviceCommissioner : public DeviceController, nullptr; // Commissioning delegate to call when PairDevice / Commission functions are used CommissioningDelegate * mCommissioningDelegate = nullptr; // Commissioning delegate that issued the PerformCommissioningStep command - CompletionStatus commissioningCompletionStatus; + CompletionStatus mCommissioningCompletionStatus; #if CHIP_CONFIG_ENABLE_READ_CLIENT Platform::UniquePtr mAttributeCache; diff --git a/src/controller/InvokeInteraction.h b/src/controller/InvokeInteraction.h index d167cde140db23..7576f8aac2e78b 100644 --- a/src/controller/InvokeInteraction.h +++ b/src/controller/InvokeInteraction.h @@ -18,13 +18,19 @@ #pragma once -#include #include #include +#include + namespace chip { namespace Controller { +namespace Internal { +// Cancellation functions on InvokeCommandRequest() are for internal use only. +typedef std::function InvokeCancelFn; +} // namespace Internal + /* * A typed command invocation function that takes as input a cluster-object representation of a command request and * callbacks for success and failure and either returns a decoded cluster-object representation of the response through @@ -49,7 +55,8 @@ InvokeCommandRequest(Messaging::ExchangeManager * aExchangeMgr, const SessionHan typename TypedCommandCallback::OnSuccessCallbackType onSuccessCb, typename TypedCommandCallback::OnErrorCallbackType onErrorCb, const Optional & timedInvokeTimeoutMs, - const Optional & responseTimeout = NullOptional) + const Optional & responseTimeout = NullOptional, + Internal::InvokeCancelFn * outCancelFn = nullptr) { // InvokeCommandRequest expects responses, so cannot happen over a group session. VerifyOrReturnError(!sessionHandle->IsGroupSession(), CHIP_ERROR_INVALID_ARGUMENT); @@ -81,6 +88,15 @@ InvokeCommandRequest(Messaging::ExchangeManager * aExchangeMgr, const SessionHan ReturnErrorOnFailure(commandSender->AddRequestData(commandPath, requestCommandData, timedInvokeTimeoutMs)); ReturnErrorOnFailure(commandSender->SendCommandRequest(sessionHandle, responseTimeout)); + // If requested by the caller, provide a way to cancel the invoke interaction. + if (outCancelFn != nullptr) + { + *outCancelFn = [rawDecoderPtr = decoder.get(), rawCommandSender = commandSender.get()]() { + chip::Platform::Delete(rawCommandSender); + chip::Platform::Delete(rawDecoderPtr); + }; + } + // // We've effectively transferred ownership of the above allocated objects to CommandSender, and we need to wait for it to call // us back when processing is completed (through OnDone) to eventually free up resources. diff --git a/src/controller/java/AndroidCallbacks-JNI.cpp b/src/controller/java/AndroidCallbacks-JNI.cpp index c87b669f9b521f..fff1226bdb37d2 100644 --- a/src/controller/java/AndroidCallbacks-JNI.cpp +++ b/src/controller/java/AndroidCallbacks-JNI.cpp @@ -69,3 +69,14 @@ JNI_METHOD(void, InvokeCallbackJni, deleteCallback)(JNIEnv * env, jobject self, { deleteInvokeCallback(env, self, callbackHandle); } + +JNI_METHOD(jlong, ExtendableInvokeCallbackJni, newCallback) +(JNIEnv * env, jobject self) +{ + return newExtendableInvokeCallback(env, self); +} + +JNI_METHOD(void, ExtendableInvokeCallbackJni, deleteCallback)(JNIEnv * env, jobject self, jlong callbackHandle) +{ + deleteExtendableInvokeCallback(env, self, callbackHandle); +} diff --git a/src/controller/java/AndroidCallbacks.cpp b/src/controller/java/AndroidCallbacks.cpp index fb7975262dff57..fc9fbe02f12292 100644 --- a/src/controller/java/AndroidCallbacks.cpp +++ b/src/controller/java/AndroidCallbacks.cpp @@ -791,6 +791,7 @@ InvokeCallback::~InvokeCallback() if (mCommandSender != nullptr) { Platform::Delete(mCommandSender); + mCommandSender = nullptr; } } @@ -802,7 +803,6 @@ void InvokeCallback::OnResponse(app::CommandSender * apCommandSender, const app: VerifyOrReturn(env != nullptr, ChipLogError(Controller, "Could not get JNIEnv for current thread")); jmethodID onResponseMethod; JniLocalReferenceScope scope(env); - VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Unable to create Java InvokeElement: %s", ErrorStr(err))); VerifyOrReturn(mWrapperCallbackRef.HasValidObjectRef(), ChipLogError(Controller, "mWrapperCallbackRef is not valid in %s", __func__)); jobject wrapperCallbackRef = mWrapperCallbackRef.ObjectRef(); @@ -1023,6 +1023,170 @@ jobject DecodeGeneralTLVValue(JNIEnv * env, TLV::TLVReader & readerForGeneralVal } } +ExtendableInvokeCallback::ExtendableInvokeCallback(jobject wrapperCallback) +{ + VerifyOrReturn(mWrapperCallbackRef.Init(wrapperCallback) == CHIP_NO_ERROR, + ChipLogError(Controller, "Could not init mWrapperCallbackRef for ExtendableInvokeCallback")); +} + +ExtendableInvokeCallback::~ExtendableInvokeCallback() +{ + if (mCommandSender != nullptr) + { + Platform::Delete(mCommandSender); + mCommandSender = nullptr; + } +} + +void ExtendableInvokeCallback::OnResponse(app::CommandSender * apCommandSender, + const app::CommandSender::ResponseData & aResponseData) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturn(env != nullptr, ChipLogError(Controller, "Could not get JNIEnv for current thread")); + jmethodID onResponseMethod; + JniLocalReferenceScope scope(env); + VerifyOrReturn(mWrapperCallbackRef.HasValidObjectRef(), + ChipLogError(Controller, "mWrapperCallbackRef is not valid in %s", __func__)); + jobject wrapperCallbackRef = mWrapperCallbackRef.ObjectRef(); + DeviceLayer::StackUnlock unlock; + + jobject jCommandRef = nullptr; + if (aResponseData.commandRef.HasValue()) + { + err = JniReferences::GetInstance().CreateBoxedObject( + "java/lang/Integer", "(I)V", static_cast(aResponseData.commandRef.Value()), jCommandRef); + VerifyOrReturn(err == CHIP_NO_ERROR, + ChipLogError(Controller, "Could not CreateBoxedObject with error %" CHIP_ERROR_FORMAT, err.Format())); + } + + if (aResponseData.data != nullptr) + { + TLV::TLVReader readerForJavaTLV; + TLV::TLVReader readerForJson; + readerForJavaTLV.Init(*(aResponseData.data)); + + // Create TLV byte array to pass to Java layer + size_t bufferLen = readerForJavaTLV.GetRemainingLength() + readerForJavaTLV.GetLengthRead(); + std::unique_ptr buffer = std::unique_ptr(new uint8_t[bufferLen]); + uint32_t size = 0; + + TLV::TLVWriter writer; + writer.Init(buffer.get(), bufferLen); + err = writer.CopyElement(TLV::AnonymousTag(), readerForJavaTLV); + VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Failed CopyElement: %" CHIP_ERROR_FORMAT, err.Format())); + size = writer.GetLengthWritten(); + + chip::ByteArray jniByteArray(env, reinterpret_cast(buffer.get()), static_cast(size)); + + // Convert TLV to JSON + std::string json; + readerForJson.Init(buffer.get(), size); + err = readerForJson.Next(); + VerifyOrReturn(err == CHIP_NO_ERROR, + ChipLogError(Controller, "Failed readerForJson next: %" CHIP_ERROR_FORMAT, err.Format())); + err = TlvToJson(readerForJson, json); + VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Failed TlvToJson: %" CHIP_ERROR_FORMAT, err.Format())); + UtfString jsonString(env, json.c_str()); + + err = JniReferences::GetInstance().FindMethod(env, wrapperCallbackRef, "onResponse", + "(IJJLjava/lang/Integer;[BLjava/lang/String;)V", &onResponseMethod); + VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Unable to find onResponse method: %s", ErrorStr(err))); + + env->CallVoidMethod(wrapperCallbackRef, onResponseMethod, static_cast(aResponseData.path.mEndpointId), + static_cast(aResponseData.path.mClusterId), static_cast(aResponseData.path.mCommandId), + jCommandRef, jniByteArray.jniValue(), jsonString.jniValue()); + } + else + { + err = JniReferences::GetInstance().FindMethod(env, wrapperCallbackRef, "onResponse", + "(IJJLjava/lang/Integer;ILjava/lang/Integer;)V", &onResponseMethod); + VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Unable to find onResponse method: %s", ErrorStr(err))); + + jobject jClusterState = nullptr; + if (aResponseData.statusIB.mClusterStatus.HasValue()) + { + err = JniReferences::GetInstance().CreateBoxedObject( + "java/lang/Integer", "(I)V", static_cast(aResponseData.statusIB.mClusterStatus.Value()), jClusterState); + VerifyOrReturn(err == CHIP_NO_ERROR, + ChipLogError(Controller, "Could not CreateBoxedObject with error %" CHIP_ERROR_FORMAT, err.Format())); + } + + env->CallVoidMethod(wrapperCallbackRef, onResponseMethod, static_cast(aResponseData.path.mEndpointId), + static_cast(aResponseData.path.mClusterId), static_cast(aResponseData.path.mCommandId), + jCommandRef, aResponseData.statusIB.mStatus, jClusterState); + } + + VerifyOrReturn(!env->ExceptionCheck(), env->ExceptionDescribe()); +} + +void ExtendableInvokeCallback::OnNoResponse(app::CommandSender * commandSender, + const app::CommandSender::NoResponseData & aNoResponseData) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturn(env != nullptr, ChipLogError(Controller, "Could not get JNIEnv for current thread")); + jmethodID onNoResponseMethod; + JniLocalReferenceScope scope(env); + VerifyOrReturn(mWrapperCallbackRef.HasValidObjectRef(), + ChipLogError(Controller, "mWrapperCallbackRef is not valid in %s", __func__)); + jobject wrapperCallbackRef = mWrapperCallbackRef.ObjectRef(); + DeviceLayer::StackUnlock unlock; + + err = JniReferences::GetInstance().FindMethod(env, wrapperCallbackRef, "onNoResponse", "(I)V", &onNoResponseMethod); + VerifyOrReturn(err == CHIP_NO_ERROR, + ChipLogError(Controller, "Unable to find onNoResponse method: %" CHIP_ERROR_FORMAT, err.Format())); + env->CallVoidMethod(wrapperCallbackRef, onNoResponseMethod, static_cast(aNoResponseData.commandRef)); + VerifyOrReturn(!env->ExceptionCheck(), env->ExceptionDescribe()); +} + +void ExtendableInvokeCallback::OnError(const app::CommandSender * apCommandSender, const app::CommandSender::ErrorData & aErrorData) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturn(env != nullptr, ChipLogError(Controller, "Could not get JNIEnv for current thread")); + JniLocalReferenceScope scope(env); + ChipLogError(Controller, "ExtendableInvokeCallback::OnError is called with %u", aErrorData.error.AsInteger()); + jthrowable exception; + err = AndroidControllerExceptions::GetInstance().CreateAndroidControllerException(env, ErrorStr(aErrorData.error), + aErrorData.error.AsInteger(), exception); + VerifyOrReturn( + err == CHIP_NO_ERROR, + ChipLogError(Controller, "Unable to create AndroidControllerException with error: %" CHIP_ERROR_FORMAT, err.Format())); + + jmethodID onErrorMethod; + VerifyOrReturn(mWrapperCallbackRef.HasValidObjectRef(), + ChipLogError(Controller, "mWrapperCallbackRef is not valid in %s", __func__)); + jobject wrapperCallback = mWrapperCallbackRef.ObjectRef(); + err = JniReferences::GetInstance().FindMethod(env, wrapperCallback, "onError", "(Ljava/lang/Exception;)V", &onErrorMethod); + VerifyOrReturn(err == CHIP_NO_ERROR, + ChipLogError(Controller, "Unable to find onError method: %" CHIP_ERROR_FORMAT, err.Format())); + + DeviceLayer::StackUnlock unlock; + env->CallVoidMethod(wrapperCallback, onErrorMethod, exception); + VerifyOrReturn(!env->ExceptionCheck(), env->ExceptionDescribe()); +} + +void ExtendableInvokeCallback::OnDone(app::CommandSender * apCommandSender) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturn(env != nullptr, ChipLogError(Controller, "Could not get JNIEnv for current thread")); + JniLocalReferenceScope scope(env); + jmethodID onDoneMethod; + VerifyOrReturn(mWrapperCallbackRef.HasValidObjectRef(), + ChipLogError(Controller, "mWrapperCallbackRef is not valid in %s", __func__)); + jobject wrapperCallback = mWrapperCallbackRef.ObjectRef(); + JniGlobalReference globalRef(std::move(mWrapperCallbackRef)); + + err = JniReferences::GetInstance().FindMethod(env, wrapperCallback, "onDone", "()V", &onDoneMethod); + VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Could not find onDone method")); + + DeviceLayer::StackUnlock unlock; + env->CallVoidMethod(wrapperCallback, onDoneMethod); + VerifyOrReturn(!env->ExceptionCheck(), env->ExceptionDescribe()); +} + jlong newConnectedDeviceCallback(JNIEnv * env, jobject self, jobject callback) { chip::DeviceLayer::StackLock lock; @@ -1085,5 +1249,19 @@ void deleteInvokeCallback(JNIEnv * env, jobject self, jlong callbackHandle) chip::Platform::Delete(invokeCallback); } +jlong newExtendableInvokeCallback(JNIEnv * env, jobject self) +{ + chip::DeviceLayer::StackLock lock; + ExtendableInvokeCallback * invokeCallback = chip::Platform::New(self); + return reinterpret_cast(invokeCallback); +} + +void deleteExtendableInvokeCallback(JNIEnv * env, jobject self, jlong callbackHandle) +{ + chip::DeviceLayer::StackLock lock; + ExtendableInvokeCallback * invokeCallback = reinterpret_cast(callbackHandle); + VerifyOrReturn(invokeCallback != nullptr, ChipLogError(Controller, "ExtendableInvokeCallback handle is nullptr")); + chip::Platform::Delete(invokeCallback); +} } // namespace Controller } // namespace chip diff --git a/src/controller/java/AndroidCallbacks.h b/src/controller/java/AndroidCallbacks.h index 700a1fb467f13f..0c49daf1b21a84 100644 --- a/src/controller/java/AndroidCallbacks.h +++ b/src/controller/java/AndroidCallbacks.h @@ -25,6 +25,7 @@ #include #include #include +#include #include namespace chip { @@ -138,6 +139,20 @@ struct InvokeCallback : public app::CommandSender::Callback JniGlobalReference mWrapperCallbackRef; }; +struct ExtendableInvokeCallback : public app::CommandSender::ExtendableCallback +{ + ExtendableInvokeCallback(jobject wrapperCallback); + ~ExtendableInvokeCallback(); + + void OnResponse(app::CommandSender * commandSender, const app::CommandSender::ResponseData & aResponseData) override; + void OnNoResponse(app::CommandSender * commandSender, const app::CommandSender::NoResponseData & aNoResponseData) override; + void OnError(const app::CommandSender * apCommandSender, const app::CommandSender::ErrorData & aErrorData) override; + void OnDone(app::CommandSender * apCommandSender) override; + + app::CommandSender * mCommandSender = nullptr; + JniGlobalReference mWrapperCallbackRef; +}; + jlong newConnectedDeviceCallback(JNIEnv * env, jobject self, jobject callback); void deleteConnectedDeviceCallback(JNIEnv * env, jobject self, jlong callbackHandle); jlong newReportCallback(JNIEnv * env, jobject self, jobject subscriptionEstablishedCallbackJava, @@ -147,6 +162,8 @@ jlong newWriteAttributesCallback(JNIEnv * env, jobject self); void deleteWriteAttributesCallback(JNIEnv * env, jobject self, jlong callbackHandle); jlong newInvokeCallback(JNIEnv * env, jobject self); void deleteInvokeCallback(JNIEnv * env, jobject self, jlong callbackHandle); +jlong newExtendableInvokeCallback(JNIEnv * env, jobject self); +void deleteExtendableInvokeCallback(JNIEnv * env, jobject self, jlong callbackHandle); } // namespace Controller } // namespace chip diff --git a/src/controller/java/AndroidInteractionClient.cpp b/src/controller/java/AndroidInteractionClient.cpp index a0b22bfde530af..ec809570bcb1c7 100644 --- a/src/controller/java/AndroidInteractionClient.cpp +++ b/src/controller/java/AndroidInteractionClient.cpp @@ -431,7 +431,7 @@ CHIP_ERROR write(JNIEnv * env, jlong handle, jlong callbackHandle, jlong deviceP CHIP_ERROR PutPreencodedInvokeRequest(app::CommandSender & commandSender, app::CommandPathParams & path, const ByteSpan & data) { - // PrepareCommand does nott create the struct container with kFields and copycontainer below sets the + // PrepareCommand does not create the struct container with kFields and copycontainer below sets the // kFields container already ReturnErrorOnFailure(commandSender.PrepareCommand(path, false /* aStartDataStruct */)); TLV::TLVWriter * writer = commandSender.GetCommandDataIBTLVWriter(); @@ -442,6 +442,191 @@ CHIP_ERROR PutPreencodedInvokeRequest(app::CommandSender & commandSender, app::C return writer->CopyContainer(TLV::ContextTag(app::CommandDataIB::Tag::kFields), reader); } +CHIP_ERROR PutPreencodedInvokeRequest(app::CommandSender & commandSender, app::CommandPathParams & path, const ByteSpan & data, + app::CommandSender::PrepareCommandParameters & prepareCommandParams) +{ + // PrepareCommand does not create the struct container with kFields and copycontainer below sets the + // kFields container already + ReturnErrorOnFailure(commandSender.PrepareCommand(path, prepareCommandParams)); + TLV::TLVWriter * writer = commandSender.GetCommandDataIBTLVWriter(); + VerifyOrReturnError(writer != nullptr, CHIP_ERROR_INCORRECT_STATE); + TLV::TLVReader reader; + reader.Init(data); + ReturnErrorOnFailure(reader.Next()); + return writer->CopyContainer(TLV::ContextTag(app::CommandDataIB::Tag::kFields), reader); +} + +CHIP_ERROR extendableInvoke(JNIEnv * env, jlong handle, jlong callbackHandle, jlong devicePtr, jobject invokeElementList, + jint timedRequestTimeoutMs, jint imTimeoutMs) +{ + chip::DeviceLayer::StackLock lock; + CHIP_ERROR err = CHIP_NO_ERROR; + auto callback = reinterpret_cast(callbackHandle); + app::CommandSender * commandSender = nullptr; + uint16_t groupId = 0; + bool isEndpointIdValid = false; + bool isGroupIdValid = false; + jint listSize = 0; + uint16_t convertedTimedRequestTimeoutMs = static_cast(timedRequestTimeoutMs); + app::CommandSender::ConfigParameters config; + + ChipLogDetail(Controller, "IM extendableInvoke() called"); + + DeviceProxy * device = reinterpret_cast(devicePtr); + VerifyOrExit(device != nullptr, err = CHIP_ERROR_INCORRECT_STATE); + VerifyOrExit(device->GetSecureSession().HasValue(), err = CHIP_ERROR_MISSING_SECURE_SESSION); + + VerifyOrExit(invokeElementList != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT); + SuccessOrExit(err = JniReferences::GetInstance().GetListSize(invokeElementList, listSize)); + + if ((listSize > 1) && (device->GetSecureSession().Value()->IsGroupSession())) + { + ChipLogError(Controller, "Not allow group session for InvokeRequests that has more than 1 CommandDataIB)"); + err = CHIP_ERROR_INVALID_ARGUMENT; + goto exit; + } + + commandSender = Platform::New(callback, device->GetExchangeManager(), timedRequestTimeoutMs != 0); + config.SetRemoteMaxPathsPerInvoke(device->GetSecureSession().Value()->GetRemoteSessionParameters().GetMaxPathsPerInvoke()); + SuccessOrExit(err = commandSender->SetCommandSenderConfig(config)); + + for (uint8_t i = 0; i < listSize; i++) + { + jmethodID getEndpointIdMethod = nullptr; + jmethodID getClusterIdMethod = nullptr; + jmethodID getCommandIdMethod = nullptr; + jmethodID getGroupIdMethod = nullptr; + jmethodID getTlvByteArrayMethod = nullptr; + jmethodID getJsonStringMethod = nullptr; + jmethodID isEndpointIdValidMethod = nullptr; + jmethodID isGroupIdValidMethod = nullptr; + jlong endpointIdObj = 0; + jlong clusterIdObj = 0; + jlong commandIdObj = 0; + jobject groupIdObj = nullptr; + jbyteArray tlvBytesObj = nullptr; + jobject invokeElement = nullptr; + SuccessOrExit(err = JniReferences::GetInstance().GetListItem(invokeElementList, i, invokeElement)); + SuccessOrExit( + err = JniReferences::GetInstance().FindMethod(env, invokeElement, "getEndpointId", "(J)J", &getEndpointIdMethod)); + SuccessOrExit(err = + JniReferences::GetInstance().FindMethod(env, invokeElement, "getClusterId", "(J)J", &getClusterIdMethod)); + SuccessOrExit(err = + JniReferences::GetInstance().FindMethod(env, invokeElement, "getCommandId", "(J)J", &getCommandIdMethod)); + SuccessOrExit(err = JniReferences::GetInstance().FindMethod(env, invokeElement, "getGroupId", "()Ljava/util/Optional;", + &getGroupIdMethod)); + SuccessOrExit(err = JniReferences::GetInstance().FindMethod(env, invokeElement, "isEndpointIdValid", "()Z", + &isEndpointIdValidMethod)); + SuccessOrExit( + err = JniReferences::GetInstance().FindMethod(env, invokeElement, "isGroupIdValid", "()Z", &isGroupIdValidMethod)); + SuccessOrExit( + err = JniReferences::GetInstance().FindMethod(env, invokeElement, "getTlvByteArray", "()[B", &getTlvByteArrayMethod)); + + isEndpointIdValid = (env->CallBooleanMethod(invokeElement, isEndpointIdValidMethod) == JNI_TRUE); + isGroupIdValid = (env->CallBooleanMethod(invokeElement, isGroupIdValidMethod) == JNI_TRUE); + + if (isEndpointIdValid) + { + endpointIdObj = env->CallLongMethod(invokeElement, getEndpointIdMethod, static_cast(kInvalidEndpointId)); + VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN); + } + + if (isGroupIdValid) + { + VerifyOrExit(device->GetSecureSession().Value()->IsGroupSession(), err = CHIP_ERROR_INVALID_ARGUMENT); + groupIdObj = env->CallObjectMethod(invokeElement, getGroupIdMethod); + VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN); + VerifyOrExit(groupIdObj != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT); + + jobject boxedGroupId = nullptr; + + SuccessOrExit(err = JniReferences::GetInstance().GetOptionalValue(groupIdObj, boxedGroupId)); + VerifyOrExit(boxedGroupId != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT); + groupId = static_cast(JniReferences::GetInstance().IntegerToPrimitive(boxedGroupId)); + } + + clusterIdObj = env->CallLongMethod(invokeElement, getClusterIdMethod, static_cast(kInvalidClusterId)); + VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN); + + commandIdObj = env->CallLongMethod(invokeElement, getCommandIdMethod, static_cast(kInvalidCommandId)); + VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN); + + tlvBytesObj = static_cast(env->CallObjectMethod(invokeElement, getTlvByteArrayMethod)); + VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN); + + app::CommandSender::PrepareCommandParameters prepareCommandParams; + prepareCommandParams.commandRef.SetValue(static_cast(i)); + + { + uint16_t id = isEndpointIdValid ? static_cast(endpointIdObj) : groupId; + app::CommandPathFlags flag = + isEndpointIdValid ? app::CommandPathFlags::kEndpointIdValid : app::CommandPathFlags::kGroupIdValid; + app::CommandPathParams path(id, static_cast(clusterIdObj), static_cast(commandIdObj), flag); + + if (tlvBytesObj != nullptr) + { + JniByteArray tlvBytesObjBytes(env, tlvBytesObj); + SuccessOrExit( + err = PutPreencodedInvokeRequest(*commandSender, path, tlvBytesObjBytes.byteSpan(), prepareCommandParams)); + } + else + { + SuccessOrExit(err = JniReferences::GetInstance().FindMethod(env, invokeElement, "getJsonString", + "()Ljava/lang/String;", &getJsonStringMethod)); + jstring jsonJniString = static_cast(env->CallObjectMethod(invokeElement, getJsonStringMethod)); + VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN); + VerifyOrExit(jsonJniString != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT); + JniUtfString jsonUtfJniString(env, jsonJniString); + // The invoke does not support chunk, kMaxSecureSduLengthBytes should be enough for command json blob + uint8_t tlvBytes[chip::app::kMaxSecureSduLengthBytes] = { 0 }; + MutableByteSpan tlvEncodingLocal{ tlvBytes }; + SuccessOrExit(err = JsonToTlv(std::string(jsonUtfJniString.c_str(), static_cast(jsonUtfJniString.size())), + tlvEncodingLocal)); + SuccessOrExit(err = PutPreencodedInvokeRequest(*commandSender, path, tlvEncodingLocal, prepareCommandParams)); + } + } + + app::CommandSender::FinishCommandParameters finishCommandParams(convertedTimedRequestTimeoutMs != 0 + ? Optional(convertedTimedRequestTimeoutMs) + : Optional::Missing()); + + finishCommandParams.commandRef = prepareCommandParams.commandRef; + SuccessOrExit(err = commandSender->FinishCommand(finishCommandParams)); + } + SuccessOrExit(err = device->GetSecureSession().Value()->IsGroupSession() + ? commandSender->SendGroupCommandRequest(device->GetSecureSession().Value()) + : commandSender->SendCommandRequest(device->GetSecureSession().Value(), + imTimeoutMs != 0 + ? MakeOptional(System::Clock::Milliseconds32(imTimeoutMs)) + : Optional::Missing())); + + callback->mCommandSender = commandSender; +exit: + if (err != CHIP_NO_ERROR) + { + ChipLogError(Controller, "JNI IM Invoke Error: %s", err.AsString()); + if (err == CHIP_JNI_ERROR_EXCEPTION_THROWN) + { + env->ExceptionDescribe(); + env->ExceptionClear(); + } + app::CommandSender::ErrorData errorData; + errorData.error = err; + callback->OnError(nullptr, errorData); + if (commandSender != nullptr) + { + Platform::Delete(commandSender); + commandSender = nullptr; + } + if (callback != nullptr) + { + Platform::Delete(callback); + callback = nullptr; + } + } + return err; +} + CHIP_ERROR invoke(JNIEnv * env, jlong handle, jlong callbackHandle, jlong devicePtr, jobject invokeElement, jint timedRequestTimeoutMs, jint imTimeoutMs) { diff --git a/src/controller/java/AndroidInteractionClient.h b/src/controller/java/AndroidInteractionClient.h index 095061f0c98172..f38af6f4eb2a96 100644 --- a/src/controller/java/AndroidInteractionClient.h +++ b/src/controller/java/AndroidInteractionClient.h @@ -29,3 +29,5 @@ CHIP_ERROR write(JNIEnv * env, jlong handle, jlong callbackHandle, jlong deviceP jint timedRequestTimeoutMs, jint imTimeoutMs); CHIP_ERROR invoke(JNIEnv * env, jlong handle, jlong callbackHandle, jlong devicePtr, jobject invokeElement, jint timedRequestTimeoutMs, jint imTimeoutMs); +CHIP_ERROR extendableInvoke(JNIEnv * env, jlong handle, jlong callbackHandle, jlong devicePtr, jobject invokeElementList, + jint timedRequestTimeoutMs, jint imTimeoutMs); diff --git a/src/controller/java/BUILD.gn b/src/controller/java/BUILD.gn index 31dbaafc9fb21f..a2f3c866bdaa76 100644 --- a/src/controller/java/BUILD.gn +++ b/src/controller/java/BUILD.gn @@ -459,6 +459,8 @@ android_library("java") { "src/chip/devicecontroller/ControllerParams.java", "src/chip/devicecontroller/DeviceAttestationDelegate.java", "src/chip/devicecontroller/DiscoveredDevice.java", + "src/chip/devicecontroller/ExtendableInvokeCallback.java", + "src/chip/devicecontroller/ExtendableInvokeCallbackJni.java", "src/chip/devicecontroller/GetConnectedDeviceCallbackJni.java", "src/chip/devicecontroller/GroupKeySecurityPolicy.java", "src/chip/devicecontroller/ICDClientInfo.java", @@ -493,6 +495,8 @@ android_library("java") { "src/chip/devicecontroller/model/EndpointState.java", "src/chip/devicecontroller/model/EventState.java", "src/chip/devicecontroller/model/InvokeElement.java", + "src/chip/devicecontroller/model/InvokeResponseData.java", + "src/chip/devicecontroller/model/NoInvokeResponseData.java", "src/chip/devicecontroller/model/NodeState.java", "src/chip/devicecontroller/model/Status.java", ] diff --git a/src/controller/java/CHIPDeviceController-JNI.cpp b/src/controller/java/CHIPDeviceController-JNI.cpp index 206103ca82a311..fc2c0694b29849 100644 --- a/src/controller/java/CHIPDeviceController-JNI.cpp +++ b/src/controller/java/CHIPDeviceController-JNI.cpp @@ -2285,6 +2285,18 @@ JNI_METHOD(void, invoke) } } +JNI_METHOD(void, extendableInvoke) +(JNIEnv * env, jclass clz, jlong handle, jlong callbackHandle, jlong devicePtr, jobject invokeElementList, + jint timedRequestTimeoutMs, jint imTimeoutMs) +{ + CHIP_ERROR err = + extendableInvoke(env, handle, callbackHandle, devicePtr, invokeElementList, timedRequestTimeoutMs, imTimeoutMs); + if (err != CHIP_NO_ERROR) + { + ChipLogError(Controller, "JNI IM Batch Invoke Error: %" CHIP_ERROR_FORMAT, err.Format()); + } +} + void * IOThreadMain(void * arg) { JNIEnv * env; diff --git a/src/controller/java/MatterCallbacks-JNI.cpp b/src/controller/java/MatterCallbacks-JNI.cpp index f20d9e47f093ff..93101df18066c4 100644 --- a/src/controller/java/MatterCallbacks-JNI.cpp +++ b/src/controller/java/MatterCallbacks-JNI.cpp @@ -69,3 +69,14 @@ JNI_METHOD(void, InvokeCallbackJni, deleteCallback)(JNIEnv * env, jobject self, { deleteInvokeCallback(env, self, callbackHandle); } + +JNI_METHOD(jlong, ExtendableInvokeCallbackJni, newCallback) +(JNIEnv * env, jobject self) +{ + return newExtendableInvokeCallback(env, self); +} + +JNI_METHOD(void, ExtendableInvokeCallbackJni, deleteCallback)(JNIEnv * env, jobject self, jlong callbackHandle) +{ + deleteExtendableInvokeCallback(env, self, callbackHandle); +} diff --git a/src/controller/java/MatterInteractionClient-JNI.cpp b/src/controller/java/MatterInteractionClient-JNI.cpp index 6a541723d041ed..152e52aedd65d7 100644 --- a/src/controller/java/MatterInteractionClient-JNI.cpp +++ b/src/controller/java/MatterInteractionClient-JNI.cpp @@ -65,3 +65,15 @@ JNI_METHOD(void, invoke) ChipLogError(Controller, "JNI IM Invoke Error: %" CHIP_ERROR_FORMAT, err.Format()); } } + +JNI_METHOD(void, extendableInvoke) +(JNIEnv * env, jobject self, jlong handle, jlong callbackHandle, jlong devicePtr, jobject invokeElementList, + jint timedRequestTimeoutMs, jint imTimeoutMs) +{ + CHIP_ERROR err = + extendableInvoke(env, handle, callbackHandle, devicePtr, invokeElementList, timedRequestTimeoutMs, imTimeoutMs); + if (err != CHIP_NO_ERROR) + { + ChipLogError(Controller, "JNI IM Batch Invoke Error: %" CHIP_ERROR_FORMAT, err.Format()); + } +} diff --git a/src/controller/java/src/chip/devicecontroller/BatchInvokeCallback.java b/src/controller/java/src/chip/devicecontroller/BatchInvokeCallback.java new file mode 100644 index 00000000000000..1b07e38de0615e --- /dev/null +++ b/src/controller/java/src/chip/devicecontroller/BatchInvokeCallback.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package chip.devicecontroller; + +import chip.devicecontroller.model.InvokeResponseData; +import chip.devicecontroller.model.NoInvokeResponseData; + +/** An interface for receiving invoke response. */ +public interface ExtendableInvokeCallback { + + /** + * OnError will be called when an error occurs after failing to call + * + * @param Exception The IllegalStateException which encapsulated the error message, the possible + * chip error could be - CHIP_ERROR_TIMEOUT: A response was not received within the expected + * response timeout. - CHIP_ERROR_*TLV*: A malformed, non-compliant response was received from + * the server. - CHIP_ERROR encapsulating the converted error from the StatusIB: If we got a + * non-path-specific status response from the server. - CHIP_ERROR*: All other cases. + */ + void onError(Exception e); + + /** + * OnResponse will be called when a write response has been received and processed for the given + * path. + * + * @param invokeResponseData invoke response that has either payload or status + */ + void onResponse(InvokeResponseData invokeResponseData); + + /** + * onNoResponse will be called for each request that failed to receive a response after the server + * indicates completion of all requests. + * + * @param noInvokeResponseData failed response data + */ + void onNoResponse(NoInvokeResponseData noInvokeResponseData); + + void onDone(); +} diff --git a/src/controller/java/src/chip/devicecontroller/BatchInvokeCallbackJni.java b/src/controller/java/src/chip/devicecontroller/BatchInvokeCallbackJni.java new file mode 100644 index 00000000000000..6982f58429a353 --- /dev/null +++ b/src/controller/java/src/chip/devicecontroller/BatchInvokeCallbackJni.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package chip.devicecontroller; + +import chip.devicecontroller.model.InvokeResponseData; +import chip.devicecontroller.model.NoInvokeResponseData; +import java.util.Optional; +import javax.annotation.Nullable; + +/** JNI wrapper callback class for {@link InvokeCallback}. */ +public final class ExtendableInvokeCallbackJni { + private final ExtendableInvokeCallback wrappedExtendableInvokeCallback; + private long callbackHandle; + + public ExtendableInvokeCallbackJni(ExtendableInvokeCallback wrappedExtendableInvokeCallback) { + this.wrappedExtendableInvokeCallback = wrappedExtendableInvokeCallback; + this.callbackHandle = newCallback(); + } + + long getCallbackHandle() { + return callbackHandle; + } + + private native long newCallback(); + + private native void deleteCallback(long callbackHandle); + + private void onError(Exception e) { + wrappedExtendableInvokeCallback.onError(e); + } + + private void onResponse( + int endpointId, + long clusterId, + long commandId, + @Nullable Integer commandRef, + byte[] tlv, + String jsonString) { + wrappedExtendableInvokeCallback.onResponse( + InvokeResponseData.newInstance( + endpointId, clusterId, commandId, Optional.ofNullable(commandRef), tlv, jsonString)); + } + + private void onResponse( + int endpointId, + long clusterId, + long commandId, + @Nullable Integer commandRef, + int status, + @Nullable Integer clusterStatus) { + wrappedExtendableInvokeCallback.onResponse( + InvokeResponseData.newInstance( + endpointId, + clusterId, + commandId, + Optional.ofNullable(commandRef), + status, + Optional.ofNullable(clusterStatus))); + } + + private void onNoResponse(int commandRef) { + wrappedExtendableInvokeCallback.onNoResponse(NoInvokeResponseData.newInstance(commandRef)); + } + + private void onDone() { + wrappedExtendableInvokeCallback.onDone(); + } + + // TODO(#8578): Replace finalizer with PhantomReference. + @SuppressWarnings("deprecation") + protected void finalize() throws Throwable { + super.finalize(); + + if (callbackHandle != 0) { + deleteCallback(callbackHandle); + callbackHandle = 0; + } + } +} diff --git a/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java b/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java index 9e5c6e12f42520..25d300ec1cadc1 100644 --- a/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java +++ b/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java @@ -1205,6 +1205,32 @@ public void invoke( imTimeoutMs); } + /** + * @brief ExtendableInvoke command to target device + * @param ExtendableInvokeCallback Callback when invoke responses have been received and processed + * for the given batched invoke commands. + * @param devicePtr connected device pointer + * @param invokeElementList invoke element list + * @param timedRequestTimeoutMs this is timed request if this value is larger than 0 + * @param imTimeoutMs im interaction time out value, it would override the default value in c++ im + * layer if this value is non-zero. + */ + public void extendableInvoke( + ExtendableInvokeCallback callback, + long devicePtr, + List invokeElementList, + int timedRequestTimeoutMs, + int imTimeoutMs) { + ExtendableInvokeCallbackJni jniCallback = new ExtendableInvokeCallbackJni(callback); + extendableInvoke( + deviceControllerPtr, + jniCallback.getCallbackHandle(), + devicePtr, + invokeElementList, + timedRequestTimeoutMs, + imTimeoutMs); + } + /** Create a root (self-signed) X.509 DER encoded certificate */ public static byte[] createRootCertificate( KeypairDelegate keypair, long issuerId, @Nullable Long fabricId) { @@ -1377,6 +1403,14 @@ static native void invoke( int timedRequestTimeoutMs, int imTimeoutMs); + static native void extendableInvoke( + long deviceControllerPtr, + long callbackHandle, + long devicePtr, + List invokeElementList, + int timedRequestTimeoutMs, + int imTimeoutMs); + private native long newDeviceController(ControllerParams params); private native void setDeviceAttestationDelegate( diff --git a/src/controller/java/src/chip/devicecontroller/ExtendableInvokeCallback.java b/src/controller/java/src/chip/devicecontroller/ExtendableInvokeCallback.java new file mode 100644 index 00000000000000..1b07e38de0615e --- /dev/null +++ b/src/controller/java/src/chip/devicecontroller/ExtendableInvokeCallback.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package chip.devicecontroller; + +import chip.devicecontroller.model.InvokeResponseData; +import chip.devicecontroller.model.NoInvokeResponseData; + +/** An interface for receiving invoke response. */ +public interface ExtendableInvokeCallback { + + /** + * OnError will be called when an error occurs after failing to call + * + * @param Exception The IllegalStateException which encapsulated the error message, the possible + * chip error could be - CHIP_ERROR_TIMEOUT: A response was not received within the expected + * response timeout. - CHIP_ERROR_*TLV*: A malformed, non-compliant response was received from + * the server. - CHIP_ERROR encapsulating the converted error from the StatusIB: If we got a + * non-path-specific status response from the server. - CHIP_ERROR*: All other cases. + */ + void onError(Exception e); + + /** + * OnResponse will be called when a write response has been received and processed for the given + * path. + * + * @param invokeResponseData invoke response that has either payload or status + */ + void onResponse(InvokeResponseData invokeResponseData); + + /** + * onNoResponse will be called for each request that failed to receive a response after the server + * indicates completion of all requests. + * + * @param noInvokeResponseData failed response data + */ + void onNoResponse(NoInvokeResponseData noInvokeResponseData); + + void onDone(); +} diff --git a/src/controller/java/src/chip/devicecontroller/ExtendableInvokeCallbackJni.java b/src/controller/java/src/chip/devicecontroller/ExtendableInvokeCallbackJni.java new file mode 100644 index 00000000000000..6982f58429a353 --- /dev/null +++ b/src/controller/java/src/chip/devicecontroller/ExtendableInvokeCallbackJni.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package chip.devicecontroller; + +import chip.devicecontroller.model.InvokeResponseData; +import chip.devicecontroller.model.NoInvokeResponseData; +import java.util.Optional; +import javax.annotation.Nullable; + +/** JNI wrapper callback class for {@link InvokeCallback}. */ +public final class ExtendableInvokeCallbackJni { + private final ExtendableInvokeCallback wrappedExtendableInvokeCallback; + private long callbackHandle; + + public ExtendableInvokeCallbackJni(ExtendableInvokeCallback wrappedExtendableInvokeCallback) { + this.wrappedExtendableInvokeCallback = wrappedExtendableInvokeCallback; + this.callbackHandle = newCallback(); + } + + long getCallbackHandle() { + return callbackHandle; + } + + private native long newCallback(); + + private native void deleteCallback(long callbackHandle); + + private void onError(Exception e) { + wrappedExtendableInvokeCallback.onError(e); + } + + private void onResponse( + int endpointId, + long clusterId, + long commandId, + @Nullable Integer commandRef, + byte[] tlv, + String jsonString) { + wrappedExtendableInvokeCallback.onResponse( + InvokeResponseData.newInstance( + endpointId, clusterId, commandId, Optional.ofNullable(commandRef), tlv, jsonString)); + } + + private void onResponse( + int endpointId, + long clusterId, + long commandId, + @Nullable Integer commandRef, + int status, + @Nullable Integer clusterStatus) { + wrappedExtendableInvokeCallback.onResponse( + InvokeResponseData.newInstance( + endpointId, + clusterId, + commandId, + Optional.ofNullable(commandRef), + status, + Optional.ofNullable(clusterStatus))); + } + + private void onNoResponse(int commandRef) { + wrappedExtendableInvokeCallback.onNoResponse(NoInvokeResponseData.newInstance(commandRef)); + } + + private void onDone() { + wrappedExtendableInvokeCallback.onDone(); + } + + // TODO(#8578): Replace finalizer with PhantomReference. + @SuppressWarnings("deprecation") + protected void finalize() throws Throwable { + super.finalize(); + + if (callbackHandle != 0) { + deleteCallback(callbackHandle); + callbackHandle = 0; + } + } +} diff --git a/src/controller/java/src/chip/devicecontroller/model/InvokeResponseData.java b/src/controller/java/src/chip/devicecontroller/model/InvokeResponseData.java new file mode 100644 index 00000000000000..263051580f09e5 --- /dev/null +++ b/src/controller/java/src/chip/devicecontroller/model/InvokeResponseData.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2023 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package chip.devicecontroller.model; + +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import org.json.JSONException; +import org.json.JSONObject; + +/** Class for tracking invoke response data with either data or status */ +public final class InvokeResponseData { + private static final Logger logger = Logger.getLogger(InvokeResponseData.class.getName()); + @Nullable private final ChipPathId endpointId; + private final ChipPathId clusterId, commandId; + private final Optional commandRef; + @Nullable private final byte[] tlv; + @Nullable private final JSONObject json; + @Nullable private final Status status; + + private InvokeResponseData( + ChipPathId endpointId, + ChipPathId clusterId, + ChipPathId commandId, + Optional commandRef, + @Nullable byte[] tlv, + @Nullable String jsonString) { + this.endpointId = endpointId; + this.clusterId = clusterId; + this.commandId = commandId; + this.commandRef = commandRef; + + if (tlv != null) { + this.tlv = tlv.clone(); + } else { + this.tlv = null; + } + + JSONObject jsonObject = null; + if (jsonString != null) { + try { + jsonObject = new JSONObject(jsonString); + } catch (JSONException ex) { + logger.log(Level.SEVERE, "Error parsing JSON string", ex); + } + } + + this.json = jsonObject; + this.status = null; + } + + private InvokeResponseData( + ChipPathId endpointId, + ChipPathId clusterId, + ChipPathId commandId, + Optional commandRef, + int status, + Optional clusterStatus) { + this.endpointId = endpointId; + this.clusterId = clusterId; + this.commandId = commandId; + this.commandRef = commandRef; + this.status = Status.newInstance(status, clusterStatus); + this.tlv = null; + this.json = null; + } + + public ChipPathId getEndpointId() { + return endpointId; + } + + public ChipPathId getClusterId() { + return clusterId; + } + + public ChipPathId getCommandId() { + return commandId; + } + + public Optional getCommandRef() { + return commandRef; + } + + @Nullable + public Status getStatus() { + return status; + } + + // For use in JNI. + private long getEndpointId(long wildcardValue) { + return endpointId.getId(wildcardValue); + } + + private long getClusterId(long wildcardValue) { + return clusterId.getId(wildcardValue); + } + + private long getCommandId(long wildcardValue) { + return commandId.getId(wildcardValue); + } + + public boolean isEndpointIdValid() { + return endpointId != null; + } + + @Nullable + public byte[] getTlvByteArray() { + if (tlv != null) { + return tlv.clone(); + } + return null; + } + + @Nullable + public JSONObject getJsonObject() { + return json; + } + + @Nullable + public String getJsonString() { + if (json == null) return null; + return json.toString(); + } + + // check whether the current InvokeResponseData has same path as others. + @Override + public boolean equals(Object object) { + if (object instanceof InvokeResponseData) { + InvokeResponseData that = (InvokeResponseData) object; + return Objects.equals(this.endpointId, that.endpointId) + && Objects.equals(this.clusterId, that.clusterId) + && Objects.equals(this.commandId, that.commandId); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(endpointId, clusterId, commandId); + } + + @Override + public String toString() { + return String.format( + Locale.ENGLISH, + "Endpoint %s, cluster %s, command %s, payload: %s, status: %s", + endpointId, + clusterId, + commandId, + json == null ? "null" : getJsonString(), + status == null ? "null" : status.toString()); + } + + public static InvokeResponseData newInstance( + ChipPathId endpointId, + ChipPathId clusterId, + ChipPathId commandId, + Optional commandRef, + @Nullable byte[] tlv, + @Nullable String jsonString) { + return new InvokeResponseData(endpointId, clusterId, commandId, commandRef, tlv, jsonString); + } + + public static InvokeResponseData newInstance( + int endpointId, + long clusterId, + long commandId, + Optional commandRef, + @Nullable byte[] tlv, + @Nullable String jsonString) { + return new InvokeResponseData( + ChipPathId.forId(endpointId), + ChipPathId.forId(clusterId), + ChipPathId.forId(commandId), + commandRef, + tlv, + jsonString); + } + + public static InvokeResponseData newInstance( + ChipPathId endpointId, + ChipPathId clusterId, + ChipPathId commandId, + Optional commandRef, + int status, + Optional clusterStatus) { + return new InvokeResponseData( + endpointId, clusterId, commandId, commandRef, status, clusterStatus); + } + + public static InvokeResponseData newInstance( + int endpointId, + long clusterId, + long commandId, + Optional commandRef, + int status, + Optional clusterStatus) { + return new InvokeResponseData( + ChipPathId.forId(endpointId), + ChipPathId.forId(clusterId), + ChipPathId.forId(commandId), + commandRef, + status, + clusterStatus); + } +} diff --git a/src/controller/java/src/chip/devicecontroller/model/NoInvokeResponseData.java b/src/controller/java/src/chip/devicecontroller/model/NoInvokeResponseData.java new file mode 100644 index 00000000000000..03e930c62a978f --- /dev/null +++ b/src/controller/java/src/chip/devicecontroller/model/NoInvokeResponseData.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package chip.devicecontroller.model; + +import java.util.logging.Logger; + +/** Class for tracking failed invoke response data. */ +public final class NoInvokeResponseData { + private static final Logger logger = Logger.getLogger(NoInvokeResponseData.class.getName()); + private final Integer commandRef; + + private NoInvokeResponseData(int commandRef) { + this.commandRef = commandRef; + } + + public Integer getCommandRef() { + return commandRef; + } + + public static NoInvokeResponseData newInstance(int commandRef) { + return new NoInvokeResponseData(commandRef); + } +} diff --git a/src/controller/java/src/chip/devicecontroller/model/Status.java b/src/controller/java/src/chip/devicecontroller/model/Status.java index d859fae9f189e0..28abd36465689b 100644 --- a/src/controller/java/src/chip/devicecontroller/model/Status.java +++ b/src/controller/java/src/chip/devicecontroller/model/Status.java @@ -124,4 +124,8 @@ public static Status newInstance(int status) { public static Status newInstance(int status, Integer clusterStatus) { return new Status(status, Optional.ofNullable(clusterStatus)); } + + public static Status newInstance(int status, Optional clusterStatus) { + return new Status(status, clusterStatus); + } } diff --git a/src/darwin/Framework/CHIP/MTRDevice.h b/src/darwin/Framework/CHIP/MTRDevice.h index 23a1ecf51ad56c..ee7b1608d8246c 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.h +++ b/src/darwin/Framework/CHIP/MTRDevice.h @@ -381,6 +381,15 @@ MTR_EXTERN NSString * const MTRDataVersionKey MTR_NEWLY_AVAILABLE; */ - (void)deviceBecameActive:(MTRDevice *)device MTR_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)); +/** + * Notifies delegate when the device attribute cache has been primed with initial configuration data of the device + * + * This is called when the MTRDevice object goes from not knowing the device to having cached the first attribute reports that include basic mandatory information, e.g. Descriptor clusters. + * + * The intention is that after this is called, the client should be able to call read for mandatory attributes and likely expect non-nil values. + */ +- (void)deviceCachePrimed:(MTRDevice *)device MTR_NEWLY_AVAILABLE; + @end @interface MTRDevice (Deprecated) diff --git a/src/darwin/Framework/CHIP/MTRDevice.mm b/src/darwin/Framework/CHIP/MTRDevice.mm index dbf1785c9bb6ba..0f304f42b7a741 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.mm +++ b/src/darwin/Framework/CHIP/MTRDevice.mm @@ -225,6 +225,7 @@ @implementation MTRDevice { #ifdef DEBUG NSUInteger _unitTestAttributesReportedSinceLastCheck; #endif + BOOL _delegateDeviceCachePrimedCalled; } - (instancetype)initWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceController *)controller @@ -502,6 +503,11 @@ - (void)setDelegate:(id)delegate queue:(dispatch_queue_t)queu _weakDelegate = [MTRWeakReference weakReferenceWithObject:delegate]; _delegateQueue = queue; + // If Check if cache is already primed and client hasn't been informed yet, call the -deviceCachePrimed: callback + if (!_delegateDeviceCachePrimedCalled && [self _isCachePrimedWithInitialConfigurationData]) { + [self _callDelegateDeviceCachePrimed]; + } + if (setUpSubscription) { [self _setupSubscription]; } @@ -574,6 +580,29 @@ - (BOOL)_subscriptionAbleToReport return (delegate != nil) && (state == MTRDeviceStateReachable); } +- (BOOL)_callDelegateWithBlock:(void (^)(id))block +{ + os_unfair_lock_assert_owner(&self->_lock); + id delegate = _weakDelegate.strongObject; + if (delegate) { + dispatch_async(_delegateQueue, ^{ + block(delegate); + }); + return YES; + } + return NO; +} + +- (void)_callDelegateDeviceCachePrimed +{ + os_unfair_lock_assert_owner(&self->_lock); + _delegateDeviceCachePrimedCalled = [self _callDelegateWithBlock:^(id delegate) { + if ([delegate respondsToSelector:@selector(deviceCachePrimed:)]) { + [delegate deviceCachePrimed:self]; + } + }]; +} + // assume lock is held - (void)_changeState:(MTRDeviceState)state { @@ -611,6 +640,11 @@ - (void)_handleSubscriptionEstablished // reset subscription attempt wait time when subscription succeeds _lastSubscriptionAttemptWait = 0; + // As subscription is established, check if the delegate needs to be informed + if (!_delegateDeviceCachePrimedCalled) { + [self _callDelegateDeviceCachePrimed]; + } + [self _changeState:MTRDeviceStateReachable]; os_unfair_lock_unlock(&self->_lock); @@ -741,6 +775,7 @@ - (void)_handleReportEnd _receivingReport = NO; _receivingPrimingReport = NO; _estimatedStartTimeFromGeneralDiagnosticsUpTime = nil; + // For unit testing only #ifdef DEBUG id delegate = _weakDelegate.strongObject; @@ -1948,17 +1983,24 @@ - (NSArray *)_getAttributesToReportWithReportedValues:(NSArray *)attributeValues reportChanges:(BOOL)reportChanges { + os_unfair_lock_lock(&self->_lock); + if (reportChanges) { - [self _handleAttributeReport:attributeValues]; + [self _reportAttributes:[self _getAttributesToReportWithReportedValues:attributeValues]]; } else { - os_unfair_lock_lock(&self->_lock); for (NSDictionary * responseValue in attributeValues) { MTRAttributePath * path = responseValue[MTRAttributePathKey]; NSDictionary * dataValue = responseValue[MTRDataKey]; _readCache[path] = dataValue; } - os_unfair_lock_unlock(&self->_lock); } + + // If cache is set from storage and is primed with initial configuration data, then assume the client had beeen informed in the past, and mark that the callback has been called + if ([self _isCachePrimedWithInitialConfigurationData]) { + _delegateDeviceCachePrimedCalled = YES; + } + + os_unfair_lock_unlock(&self->_lock); } // If value is non-nil, associate with expectedValueID @@ -2135,6 +2177,42 @@ - (void)_removeExpectedValueForAttributePath:(MTRAttributePath *)attributePath e } } +// This method checks if there is a need to inform delegate that the attribute cache has been "primed" +- (BOOL)_isCachePrimedWithInitialConfigurationData +{ + os_unfair_lock_assert_owner(&self->_lock); + + // Check if root node descriptor exists + NSDictionary * rootDescriptorPartsListDataValue = _readCache[[MTRAttributePath attributePathWithEndpointID:@(kRootEndpointId) clusterID:@(MTRClusterIDTypeDescriptorID) attributeID:@(MTRAttributeIDTypeClusterDescriptorAttributePartsListID)]]; + if (!rootDescriptorPartsListDataValue || ![MTRArrayValueType isEqualToString:rootDescriptorPartsListDataValue[MTRTypeKey]]) { + return NO; + } + NSArray * partsList = rootDescriptorPartsListDataValue[MTRValueKey]; + if (![partsList isKindOfClass:[NSArray class]] || !partsList.count) { + MTR_LOG_ERROR("%@ unexpected type %@ for parts list %@", self, [partsList class], partsList); + return NO; + } + + // Check if we have cached descriptor clusters for each listed endpoint + for (NSDictionary * endpointDataValue in partsList) { + if (![MTRUnsignedIntegerValueType isEqual:endpointDataValue[MTRTypeKey]]) { + MTR_LOG_ERROR("%@ unexpected type for parts list item %@", self, endpointDataValue); + continue; + } + NSNumber * endpoint = endpointDataValue[MTRValueKey]; + if (![endpoint isKindOfClass:[NSNumber class]]) { + MTR_LOG_ERROR("%@ unexpected type for parts list item %@", self, endpointDataValue); + continue; + } + NSDictionary * descriptorDeviceTypeListDataValue = _readCache[[MTRAttributePath attributePathWithEndpointID:endpoint clusterID:@(MTRClusterIDTypeDescriptorID) attributeID:@(MTRAttributeIDTypeClusterDescriptorAttributeDeviceTypeListID)]]; + if (![MTRArrayValueType isEqualToString:descriptorDeviceTypeListDataValue[MTRTypeKey]] || !descriptorDeviceTypeListDataValue[MTRValueKey]) { + return NO; + } + } + + return YES; +} + - (MTRBaseDevice *)newBaseDevice { return [MTRBaseDevice deviceWithNodeID:self.nodeID controller:self.deviceController]; diff --git a/src/darwin/Framework/CHIP/MTRError.h b/src/darwin/Framework/CHIP/MTRError.h index 8eaefbd50df643..3068a1b2405779 100644 --- a/src/darwin/Framework/CHIP/MTRError.h +++ b/src/darwin/Framework/CHIP/MTRError.h @@ -56,21 +56,25 @@ typedef NS_ERROR_ENUM(MTRErrorDomain, MTRErrorCode){ MTRErrorCodeIntegrityCheckFailed = 8, MTRErrorCodeTimeout = 9, MTRErrorCodeBufferTooSmall = 10, + /** * MTRErrorCodeFabricExists is returned when trying to commission a device * into a fabric when it's already part of that fabric. */ MTRErrorCodeFabricExists = 11, + /** * MTRErrorCodeUnknownSchema means the schema for the given cluster/attribute, * cluster/event, or cluster/command combination is not known. */ MTRErrorCodeUnknownSchema MTR_AVAILABLE(ios(17.0), macos(14.0), watchos(10.0), tvos(17.0)) = 12, + /** * MTRErrorCodeSchemaMismatch means that provided data did not match the * expected schema. */ MTRErrorCodeSchemaMismatch MTR_AVAILABLE(ios(17.0), macos(14.0), watchos(10.0), tvos(17.0)) = 13, + /** * MTRErrorCodeTLVDecodeFailed means that the TLV being decoded was malformed in * some way. This can include things like lengths running past the end of @@ -87,6 +91,11 @@ typedef NS_ERROR_ENUM(MTRErrorDomain, MTRErrorCode){ * application's Info.plist. */ MTRErrorCodeDNSSDUnauthorized MTR_AVAILABLE(ios(17.2), macos(14.2), watchos(10.2), tvos(17.2)) = 15, + + /** + * The operation was cancelled. + */ + MTRErrorCodeCancelled MTR_NEWLY_AVAILABLE = 16, }; // clang-format on diff --git a/src/darwin/Framework/CHIP/MTRError.mm b/src/darwin/Framework/CHIP/MTRError.mm index 0cb40006ac3293..d607f00400d158 100644 --- a/src/darwin/Framework/CHIP/MTRError.mm +++ b/src/darwin/Framework/CHIP/MTRError.mm @@ -61,52 +61,72 @@ + (NSError *)errorForCHIPErrorCode:(CHIP_ERROR)errorCode logContext:(id)contextT return [MTRError errorForIMStatus:status]; } - NSMutableDictionary * userInfo = [[NSMutableDictionary alloc] init]; - MTRErrorCode code = MTRErrorCodeGeneralError; - - if (errorCode == CHIP_ERROR_INVALID_STRING_LENGTH) { + MTRErrorCode code; + NSString * description; + NSDictionary * additionalUserInfo; + switch (errorCode.AsInteger()) { + case CHIP_ERROR_INVALID_STRING_LENGTH.AsInteger(): code = MTRErrorCodeInvalidStringLength; - [userInfo addEntriesFromDictionary:@{ NSLocalizedDescriptionKey : NSLocalizedString(@"A list length is invalid.", nil) }]; - } else if (errorCode == CHIP_ERROR_INVALID_INTEGER_VALUE) { + description = NSLocalizedString(@"A list length is invalid.", nil); + break; + case CHIP_ERROR_INVALID_INTEGER_VALUE.AsInteger(): code = MTRErrorCodeInvalidIntegerValue; - [userInfo addEntriesFromDictionary:@{ NSLocalizedDescriptionKey : NSLocalizedString(@"Unexpected integer value.", nil) }]; - } else if (errorCode == CHIP_ERROR_INVALID_ARGUMENT) { + description = NSLocalizedString(@"Unexpected integer value.", nil); + break; + case CHIP_ERROR_INVALID_ARGUMENT.AsInteger(): code = MTRErrorCodeInvalidArgument; - [userInfo addEntriesFromDictionary:@{ NSLocalizedDescriptionKey : NSLocalizedString(@"An argument is invalid.", nil) }]; - } else if (errorCode == CHIP_ERROR_INVALID_MESSAGE_LENGTH) { + description = NSLocalizedString(@"An argument is invalid.", nil); + break; + case CHIP_ERROR_INVALID_MESSAGE_LENGTH.AsInteger(): code = MTRErrorCodeInvalidMessageLength; - [userInfo - addEntriesFromDictionary:@{ NSLocalizedDescriptionKey : NSLocalizedString(@"A message length is invalid.", nil) }]; - } else if (errorCode == CHIP_ERROR_INCORRECT_STATE) { + description = NSLocalizedString(@"A message length is invalid.", nil); + break; + case CHIP_ERROR_INCORRECT_STATE.AsInteger(): code = MTRErrorCodeInvalidState; - [userInfo addEntriesFromDictionary:@{ NSLocalizedDescriptionKey : NSLocalizedString(@"Invalid object state.", nil) }]; - } else if (errorCode == CHIP_ERROR_INTEGRITY_CHECK_FAILED) { + description = NSLocalizedString(@"Invalid object state.", nil); + break; + case CHIP_ERROR_INTEGRITY_CHECK_FAILED.AsInteger(): code = MTRErrorCodeIntegrityCheckFailed; - [userInfo addEntriesFromDictionary:@{ NSLocalizedDescriptionKey : NSLocalizedString(@"Integrity check failed.", nil) }]; - } else if (errorCode == CHIP_ERROR_TIMEOUT) { + description = NSLocalizedString(@"Integrity check failed.", nil); + break; + case CHIP_ERROR_TIMEOUT.AsInteger(): code = MTRErrorCodeTimeout; - [userInfo addEntriesFromDictionary:@{ NSLocalizedDescriptionKey : NSLocalizedString(@"Transaction timed out.", nil) }]; - } else if (errorCode == CHIP_ERROR_BUFFER_TOO_SMALL) { + description = NSLocalizedString(@"Transaction timed out.", nil); + break; + case CHIP_ERROR_BUFFER_TOO_SMALL.AsInteger(): code = MTRErrorCodeBufferTooSmall; - [userInfo addEntriesFromDictionary:@{ NSLocalizedDescriptionKey : NSLocalizedString(@"A buffer is too small.", nil) }]; - } else if (errorCode == CHIP_ERROR_FABRIC_EXISTS) { + description = NSLocalizedString(@"A buffer is too small.", nil); + break; + case CHIP_ERROR_FABRIC_EXISTS.AsInteger(): code = MTRErrorCodeFabricExists; - [userInfo addEntriesFromDictionary:@{ - NSLocalizedDescriptionKey : NSLocalizedString(@"The device is already a member of this fabric.", nil) - }]; - } else if (errorCode == CHIP_ERROR_DECODE_FAILED) { + description = NSLocalizedString(@"The device is already a member of this fabric.", nil); + break; + case CHIP_ERROR_DECODE_FAILED.AsInteger(): code = MTRErrorCodeTLVDecodeFailed; - [userInfo addEntriesFromDictionary:@{ NSLocalizedDescriptionKey : NSLocalizedString(@"TLV decoding failed.", nil) }]; - } else if (errorCode == CHIP_ERROR_DNS_SD_UNAUTHORIZED) { + description = NSLocalizedString(@"TLV decoding failed.", nil); + break; + case CHIP_ERROR_DNS_SD_UNAUTHORIZED.AsInteger(): code = MTRErrorCodeDNSSDUnauthorized; - [userInfo addEntriesFromDictionary:@{ NSLocalizedDescriptionKey : NSLocalizedString(@"Access denied to perform DNS-SD lookups. Check that \"_matter._tcp\" and/or \"_matterc._udp\" are listed under the NSBonjourServices key in Info.plist", nil) }]; - } else { + description = NSLocalizedString(@"Access denied to perform DNS-SD lookups. " + "Check that \"_matter._tcp\" and/or \"_matterc._udp\" " + "are listed under the NSBonjourServices key in Info.plist", + nil); + break; + case CHIP_ERROR_CANCELLED.AsInteger(): + code = MTRErrorCodeCancelled; + description = NSLocalizedString(@"The operation was cancelled.", nil); + break; + default: code = MTRErrorCodeGeneralError; - [userInfo addEntriesFromDictionary:@{ - NSLocalizedDescriptionKey : - [NSString stringWithFormat:NSLocalizedString(@"Undefined error:%u.", nil), errorCode.AsInteger()], - @"errorCode" : @(errorCode.AsInteger()), - }]; + description = [NSString stringWithFormat:NSLocalizedString(@"General error: %u", nil), errorCode.AsInteger()]; + additionalUserInfo = @{ @"errorCode" : @(errorCode.AsInteger()) }; + } + + NSDictionary * userInfo = @{ NSLocalizedDescriptionKey : description }; + if (additionalUserInfo) { + NSMutableDictionary * combined = [userInfo mutableCopy]; + [combined addEntriesFromDictionary:additionalUserInfo]; + userInfo = combined; } auto * error = [NSError errorWithDomain:MTRErrorDomain code:code userInfo:userInfo]; @@ -125,105 +145,87 @@ + (NSError *)errorForIMStatus:(const chip::app::StatusIB &)status using chip::Protocols::InteractionModel::Status; switch (status.mStatus) { case Status::Failure: - default: { + default: description = NSLocalizedString(@"Operation was not successful.", nil); break; - } - case Status::InvalidSubscription: { + case Status::InvalidSubscription: description = NSLocalizedString(@"Subscription ID is not active.", nil); break; - } - case Status::UnsupportedAccess: { + case Status::UnsupportedAccess: description = NSLocalizedString(@"The sender of the action or command does not have authorization or access.", nil); break; - } - case Status::UnsupportedEndpoint: { + case Status::UnsupportedEndpoint: description = NSLocalizedString(@"The endpoint indicated is unsupported on the node.", nil); break; - } - case Status::InvalidAction: { - description = NSLocalizedString( - @"The action is malformed, has missing fields, or fields with invalid values. Action not carried out.", nil); + case Status::InvalidAction: + description = NSLocalizedString(@"The action is malformed, has missing fields, or fields with invalid values. " + "Action not carried out.", + nil); break; - } - case Status::UnsupportedCommand: { - description = NSLocalizedString( - @"The specified action or command indicated is not supported on the device. Command or action not carried out.", nil); + case Status::UnsupportedCommand: + description = NSLocalizedString(@"The specified action or command indicated is not supported on the device." + "Command or action not carried out.", + nil); break; - } - case Status::InvalidCommand: { - description = NSLocalizedString( - @"The cluster command is malformed, has missing fields, or fields with invalid values. Command not carried out.", nil); + case Status::InvalidCommand: + description = NSLocalizedString(@"The cluster command is malformed, has missing fields, or fields with invalid values." + "Command not carried out.", + nil); break; - } - case Status::UnsupportedAttribute: { - description - = NSLocalizedString(@"The specified attribute or attribute data field or entry does not exist on the device.", nil); + case Status::UnsupportedAttribute: + description = NSLocalizedString(@"The specified attribute or attribute data field or entry does not exist on the device.", nil); break; - } - case Status::ConstraintError: { + case Status::ConstraintError: description = NSLocalizedString(@"Out of range error or set to a reserved value.", nil); break; - } - case Status::UnsupportedWrite: { + case Status::UnsupportedWrite: description = NSLocalizedString(@"Attempt to write a read-only attribute.", nil); break; - } - case Status::ResourceExhausted: { + case Status::ResourceExhausted: description = NSLocalizedString(@"An action or operation failed due to insufficient available resources. ", nil); break; - } - case Status::NotFound: { + case Status::NotFound: description = NSLocalizedString(@"The indicated data field or entry could not be found.", nil); break; - } - case Status::UnreportableAttribute: { + case Status::UnreportableAttribute: description = NSLocalizedString(@"Reports cannot be issued for this attribute.", nil); break; - } - case Status::InvalidDataType: { - description = NSLocalizedString( - @"The data type indicated is undefined or invalid for the indicated data field. Command or action not carried out.", + case Status::InvalidDataType: + description = NSLocalizedString(@"The data type indicated is undefined or invalid for the indicated data field. " + "Command or action not carried out.", nil); break; - } - case Status::UnsupportedRead: { + case Status::UnsupportedRead: description = NSLocalizedString(@"Attempt to read a write-only attribute.", nil); break; - } - case Status::DataVersionMismatch: { + case Status::DataVersionMismatch: description = NSLocalizedString(@"Cluster instance data version did not match request path.", nil); break; - } - case Status::Timeout: { + case Status::Timeout: description = NSLocalizedString(@"The transaction was aborted due to time being exceeded.", nil); break; - } case Status::Busy: { - description = NSLocalizedString( - @"The receiver is busy processing another action that prevents the execution of the incoming action.", nil); + description = NSLocalizedString(@"The receiver is busy processing another action " + "that prevents the execution of the incoming action.", + nil); break; - } - case Status::UnsupportedCluster: { + case Status::UnsupportedCluster: description = NSLocalizedString(@"The cluster indicated is not supported", nil); break; - } // Gap in values is intentional. - case Status::NoUpstreamSubscription: { + case Status::NoUpstreamSubscription: description = NSLocalizedString(@"Proxy does not have a subscription to the source.", nil); break; } - case Status::NeedsTimedInteraction: { - description = NSLocalizedString(@"An Untimed Write or Untimed Invoke interaction was used for an attribute or command that " - @"requires a Timed Write or Timed Invoke.", + case Status::NeedsTimedInteraction: + description = NSLocalizedString(@"An Untimed Write or Untimed Invoke interaction was used " + "for an attribute or command that requires a Timed Write or Timed Invoke.", nil); break; - } - case Status::UnsupportedEvent: { + case Status::UnsupportedEvent: description = NSLocalizedString(@"The event indicated is unsupported on the cluster.", nil); break; } - } NSMutableDictionary * userInfo = [[NSMutableDictionary alloc] init]; userInfo[NSLocalizedDescriptionKey] = description; @@ -301,17 +303,21 @@ + (CHIP_ERROR)errorToCHIPErrorCode:(NSError * _Nullable)error case MTRErrorCodeDNSSDUnauthorized: code = CHIP_ERROR_DNS_SD_UNAUTHORIZED.AsInteger(); break; + case MTRErrorCodeCancelled: + code = CHIP_ERROR_CANCELLED.AsInteger(); + break; case MTRErrorCodeGeneralError: { - if (error.userInfo != nil && error.userInfo[@"errorCode"] != nil) { - code = static_cast([error.userInfo[@"errorCode"] unsignedLongValue]); + id userInfoErrorCode = error.userInfo[@"errorCode"]; + if ([userInfoErrorCode isKindOfClass:NSNumber.class]) { + code = static_cast([userInfoErrorCode unsignedLongValue]); break; } // Weird error we did not create. Fall through. + } default: code = CHIP_ERROR_INTERNAL.AsInteger(); break; } - } return chip::ChipError(code); } diff --git a/src/darwin/Framework/CHIP/MTRMetricsCollector.mm b/src/darwin/Framework/CHIP/MTRMetricsCollector.mm index 6387738a7c13c8..e829c57f486729 100644 --- a/src/darwin/Framework/CHIP/MTRMetricsCollector.mm +++ b/src/darwin/Framework/CHIP/MTRMetricsCollector.mm @@ -193,19 +193,19 @@ - (void)handleMetricEvent:(MetricEvent)event using ValueType = MetricEvent::Value::Type; switch (event.ValueType()) { case ValueType::kInt32: - MTR_LOG_INFO("Received metric event, key: %s, type: %d, value: %d", event.key(), event.type(), event.ValueInt32()); + MTR_LOG_INFO("Received metric event, key: %s, type: %d, value: %d", event.key(), static_cast(event.type()), event.ValueInt32()); break; case ValueType::kUInt32: - MTR_LOG_INFO("Received metric event, key: %s, type: %d, value: %u", event.key(), event.type(), event.ValueUInt32()); + MTR_LOG_INFO("Received metric event, key: %s, type: %d, value: %u", event.key(), static_cast(event.type()), event.ValueUInt32()); break; case ValueType::kChipErrorCode: - MTR_LOG_INFO("Received metric event, key: %s, type: %d, error value: %u", event.key(), event.type(), event.ValueErrorCode()); + MTR_LOG_INFO("Received metric event, key: %s, type: %d, error value: %u", event.key(), static_cast(event.type()), event.ValueErrorCode()); break; case ValueType::kUndefined: - MTR_LOG_INFO("Received metric event, key: %s, type: %d, value: nil", event.key(), event.type()); + MTR_LOG_INFO("Received metric event, key: %s, type: %d, value: nil", event.key(), static_cast(event.type())); break; default: - MTR_LOG_INFO("Received metric event, key: %s, type: %d, unknown value", event.key(), event.type()); + MTR_LOG_INFO("Received metric event, key: %s, type: %d, unknown value", event.key(), static_cast(event.type())); return; } diff --git a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m index e4628bd475b5ae..e5539f4f75d993 100644 --- a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m +++ b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m @@ -2843,7 +2843,7 @@ - (void)test031_MTRDeviceAttributeCacheLocalTestStorage { dispatch_queue_t queue = dispatch_get_main_queue(); - // First start with clean slate and + // First start with clean slate by removing the MTRDevice and clearing the persisted cache __auto_type * device = [MTRDevice deviceWithNodeID:@(kDeviceId) controller:sController]; [sController removeDevice:device]; [sController.controllerDataStore clearAllStoredAttributes]; @@ -2853,6 +2853,7 @@ - (void)test031_MTRDeviceAttributeCacheLocalTestStorage // Now recreate device and get subscription primed device = [MTRDevice deviceWithNodeID:@(kDeviceId) controller:sController]; XCTestExpectation * gotReportsExpectation = [self expectationWithDescription:@"Attribute and Event reports have been received"]; + XCTestExpectation * gotDeviceCachePrimed = [self expectationWithDescription:@"Device cache primed for the first time"]; __auto_type * delegate = [[MTRDeviceTestDelegate alloc] init]; __weak __auto_type weakDelegate = delegate; delegate.onReportEnd = ^{ @@ -2860,9 +2861,12 @@ - (void)test031_MTRDeviceAttributeCacheLocalTestStorage __strong __auto_type strongDelegate = weakDelegate; strongDelegate.onReportEnd = nil; }; + delegate.onDeviceCachePrimed = ^{ + [gotDeviceCachePrimed fulfill]; + }; [device setDelegate:delegate queue:queue]; - [self waitForExpectations:@[ gotReportsExpectation ] timeout:60]; + [self waitForExpectations:@[ gotReportsExpectation, gotDeviceCachePrimed ] timeout:60]; NSUInteger attributesReportedWithFirstSubscription = [device unitTestAttributesReportedSinceLastCheck]; @@ -2879,10 +2883,17 @@ - (void)test031_MTRDeviceAttributeCacheLocalTestStorage __strong __auto_type strongDelegate = weakDelegate; strongDelegate.onReportEnd = nil; }; + __block BOOL onDeviceCachePrimedCalled = NO; + delegate.onDeviceCachePrimed = ^{ + onDeviceCachePrimedCalled = YES; + }; [device setDelegate:delegate queue:queue]; [self waitForExpectations:@[ resubGotReportsExpectation ] timeout:60]; + // Make sure that the new callback is only ever called once, the first time subscription was primed + XCTAssertFalse(onDeviceCachePrimedCalled); + NSUInteger attributesReportedWithSecondSubscription = [device unitTestAttributesReportedSinceLastCheck]; XCTAssertTrue(attributesReportedWithSecondSubscription < attributesReportedWithFirstSubscription); diff --git a/src/darwin/Framework/CHIPTests/MTRPairingTests.m b/src/darwin/Framework/CHIPTests/MTRPairingTests.m index 6757dea7ceb541..54b8a4991325b7 100644 --- a/src/darwin/Framework/CHIPTests/MTRPairingTests.m +++ b/src/darwin/Framework/CHIPTests/MTRPairingTests.m @@ -32,7 +32,7 @@ static const uint16_t kPairingTimeoutInSeconds = 10; static const uint16_t kTimeoutInSeconds = 3; -static uint64_t sDeviceId = 0x12344321; +static uint64_t sDeviceId = 100000000; static NSString * kOnboardingPayload = @"MT:Y.K90SO527JA0648G00"; static const uint16_t kLocalPort = 5541; static const uint16_t kTestVendorId = 0xFFF1u; @@ -82,6 +82,7 @@ @interface MTRPairingTestControllerDelegate : NSObject attestationDelegate; @property (nonatomic, nullable) NSNumber * failSafeExtension; +@property (nullable) NSError * commissioningCompleteError; @end @implementation MTRPairingTestControllerDelegate @@ -100,22 +101,22 @@ - (id)initWithExpectation:(XCTestExpectation *)expectation - (void)controller:(MTRDeviceController *)controller commissioningSessionEstablishmentDone:(NSError * _Nullable)error { - XCTAssertEqual(error.code, 0); + XCTAssertNil(error); __auto_type * params = [[MTRCommissioningParameters alloc] init]; params.deviceAttestationDelegate = self.attestationDelegate; params.failSafeTimeout = self.failSafeExtension; NSError * commissionError = nil; - [controller commissionNodeWithID:@(sDeviceId) commissioningParams:params error:&commissionError]; - XCTAssertNil(commissionError); + XCTAssertTrue([controller commissionNodeWithID:@(sDeviceId) commissioningParams:params error:&commissionError], + @"Failed to start commissioning for node ID %" PRIu64 ": %@", sDeviceId, commissionError); // Keep waiting for onCommissioningComplete } - (void)controller:(MTRDeviceController *)controller commissioningComplete:(NSError * _Nullable)error { - XCTAssertEqual(error.code, 0); + self.commissioningCompleteError = error; [_expectation fulfill]; _expectation = nil; } @@ -123,6 +124,7 @@ - (void)controller:(MTRDeviceController *)controller commissioningComplete:(NSEr @end @interface MTRPairingTests : XCTestCase +@property (nullable) MTRPairingTestControllerDelegate * controllerDelegate; @end @implementation MTRPairingTests @@ -148,11 +150,9 @@ + (void)setUp + (void)tearDown { - MTRDeviceController * controller = sController; - XCTAssertNotNil(controller); - - [controller shutdown]; - XCTAssertFalse([controller isRunning]); + [sController shutdown]; + XCTAssertFalse([sController isRunning]); + sController = nil; [[MTRDeviceControllerFactory sharedInstance] stopControllerFactory]; } @@ -163,6 +163,12 @@ - (void)setUp [self setContinueAfterFailure:NO]; } +- (void)tearDown +{ + [sController setDeviceControllerDelegate:(id _Nonnull) nil queue:dispatch_get_main_queue()]; // TODO: do we need a clearDeviceControllerDelegate API? + self.controllerDelegate = nil; +} + // attestationDelegate and failSafeExtension can both be nil - (void)doPairingTestWithAttestationDelegate:(id)attestationDelegate failSafeExtension:(NSNumber *)failSafeExtension { @@ -176,6 +182,7 @@ - (void)doPairingTestWithAttestationDelegate:(id)a dispatch_queue_t callbackQueue = dispatch_queue_create("com.chip.pairing", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL); [sController setDeviceControllerDelegate:controllerDelegate queue:callbackQueue]; + self.controllerDelegate = controllerDelegate; NSError * error; __auto_type * payload = [MTRSetupPayload setupPayloadWithOnboardingPayload:kOnboardingPayload error:&error]; @@ -186,6 +193,7 @@ - (void)doPairingTestWithAttestationDelegate:(id)a XCTAssertNil(error); [self waitForExpectations:@[ expectation ] timeout:kPairingTimeoutInSeconds]; + XCTAssertNil(controllerDelegate.commissioningCompleteError); ResetCommissionee([MTRBaseDevice deviceWithNodeID:@(sDeviceId) controller:sController], dispatch_get_main_queue(), self, kTimeoutInSeconds); @@ -232,4 +240,62 @@ - (void)test004_PairWithAttestationDelegateFailsafeExtensionLong [self waitForExpectations:@[ expectation ] timeout:kTimeoutInSeconds]; } +- (void)doPairingAndWaitForProgress:(NSString *)trigger +{ + XCTestExpectation * expectation = [self expectationWithDescription:@"Trigger message seen"]; + MTRSetLogCallback(MTRLogTypeDetail, ^(MTRLogType type, NSString * moduleName, NSString * message) { + if ([message containsString:trigger]) { + [expectation fulfill]; + } + }); + + __auto_type * controllerDelegate = [[MTRPairingTestControllerDelegate alloc] initWithExpectation:nil + attestationDelegate:nil + failSafeExtension:nil]; + [sController setDeviceControllerDelegate:controllerDelegate queue:dispatch_get_main_queue()]; + self.controllerDelegate = controllerDelegate; + + __auto_type * payload = [MTRSetupPayload setupPayloadWithOnboardingPayload:kOnboardingPayload error:NULL]; + XCTAssertNotNil(payload); + NSError * error; + XCTAssertTrue([sController setupCommissioningSessionWithPayload:payload newNodeID:@(++sDeviceId) error:&error]); + XCTAssertNil(error); + + [self waitForExpectations:@[ expectation ] timeout:kPairingTimeoutInSeconds]; + MTRSetLogCallback(0, nil); +} + +- (void)doPairingTestAfterCancellationAtProgress:(NSString *)trigger +{ + // Run pairing up and wait for the trigger + [self doPairingAndWaitForProgress:trigger]; + + // Call StopPairing and wait for the commissioningComplete callback + XCTestExpectation * expectation = [self expectationWithDescription:@"commissioningComplete delegate method called"]; + self.controllerDelegate.expectation = expectation; + + NSError * error; + XCTAssertTrue([sController stopDevicePairing:sDeviceId error:&error], @"stopDevicePairing failed: %@", error); + [self waitForExpectations:@[ expectation ] timeout:kTimeoutInSeconds]; + + // Validate that the completion correctly indicated cancellation + error = self.controllerDelegate.commissioningCompleteError; + XCTAssertEqualObjects(error.domain, MTRErrorDomain); + XCTAssertEqual(error.code, MTRErrorCodeCancelled); + + // Now pair again. If the previous attempt was cancelled correctly this should work fine. + [self doPairingTestWithAttestationDelegate:nil failSafeExtension:nil]; +} + +- (void)test005_pairingAfterCancellation_ReadCommissioningInfo +{ + // @"Sending read request for commissioning information" + [self doPairingTestAfterCancellationAtProgress:@"Performing next commissioning step 'ReadCommissioningInfo'"]; +} + +- (void)test006_pairingAfterCancellation_ConfigRegulatoryCommand +{ + [self doPairingTestAfterCancellationAtProgress:@"Performing next commissioning step 'ConfigRegulatory'"]; +} + @end diff --git a/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.h b/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.h index 0f7fce14226525..e8fd8f969b2aeb 100644 --- a/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.h +++ b/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.h @@ -27,6 +27,7 @@ typedef void (^MTRDeviceTestDelegateDataHandler)(NSArray gLogFilter(kLogCategory_Max); uint8_t GetLogFilter() { - return gLogFilter; + return gLogFilter.load(); } void SetLogFilter(uint8_t category) { - gLogFilter = category; + gLogFilter.store(category); } bool IsCategoryEnabled(uint8_t category) { - return (category <= gLogFilter); + return (category <= GetLogFilter()); } #endif // CHIP_LOG_FILTERING diff --git a/src/platform/ASR/ASROTAImageProcessor.cpp b/src/platform/ASR/ASROTAImageProcessor.cpp index 92a3ab08a4da7b..d83ba75362cb69 100644 --- a/src/platform/ASR/ASROTAImageProcessor.cpp +++ b/src/platform/ASR/ASROTAImageProcessor.cpp @@ -20,9 +20,6 @@ #include #include -/// No error, operation OK -#define LEGA_OTA_OK 0L - namespace chip { CHIP_ERROR ASROTAImageProcessor::PrepareDownload() @@ -121,7 +118,8 @@ void ASROTAImageProcessor::HandlePrepareDownload(intptr_t context) imageProcessor->mHeaderParser.Init(); - imageProcessor->mDownloader->OnPreparedForDownload(err == LEGA_OTA_OK ? CHIP_NO_ERROR : CHIP_ERROR_INTERNAL); + imageProcessor->mDownloader->OnPreparedForDownload( + ((err == LEGA_OTA_OK) || (err == LEGA_OTA_INIT_ALREADY)) ? CHIP_NO_ERROR : CHIP_ERROR_INTERNAL); } void ASROTAImageProcessor::HandleFinalize(intptr_t context) diff --git a/src/platform/ASR/BLEManagerImpl.cpp b/src/platform/ASR/BLEManagerImpl.cpp index 51bd920b832dc7..e960a617b4e33b 100644 --- a/src/platform/ASR/BLEManagerImpl.cpp +++ b/src/platform/ASR/BLEManagerImpl.cpp @@ -110,6 +110,9 @@ CHIP_ERROR BLEManagerImpl::_Init() } else { + matter_ble_stack_open(); + matter_ble_add_service(); + BLEMgrImpl().SetStackInit(); mFlags.Set(Flags::kFlag_StackInitialized, true); log_i("ble is alread open!\n"); } diff --git a/third_party/asr/asr582x/asr_sdk b/third_party/asr/asr582x/asr_sdk index 54644715210812..8d6fd5e9f7f04e 160000 --- a/third_party/asr/asr582x/asr_sdk +++ b/third_party/asr/asr582x/asr_sdk @@ -1 +1 @@ -Subproject commit 54644715210812fa6c1e6e9433b5b824ba735c67 +Subproject commit 8d6fd5e9f7f04efd06d12a923a94f288e9d8c24b