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/examples/java-matter-controller/BUILD.gn b/examples/java-matter-controller/BUILD.gn index 34de1ed5585c12..da8e283e1272b1 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/PairOnNetworkLongImBatchInvokeCommand.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..ac53193d485dfa 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), + PairOnNetworkLongImBatchInvokeCommand(controller, credentialsIssuer), ) } diff --git a/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImBatchInvokeCommand.kt b/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImBatchInvokeCommand.kt new file mode 100644 index 00000000000000..5407e6e5d09116 --- /dev/null +++ b/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImBatchInvokeCommand.kt @@ -0,0 +1,160 @@ +/* + * 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.BatchInvokeCallback +import chip.devicecontroller.ChipDeviceController +import chip.devicecontroller.GetConnectedDeviceCallbackJni.GetConnectedDeviceCallback +import chip.devicecontroller.model.InvokeElement +import chip.devicecontroller.model.InvokeResponseData +import chip.devicecontroller.model.NoInvokeResponseData +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 PairOnNetworkLongImBatchInvokeCommand( + 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 : BatchInvokeCallback { + private var responseCount = 0 + + override fun onError(e: Exception) { + logger.log(Level.INFO, "Invoke receive onError" + e.message) + setFailure("invoke failure") + } + + override fun onResponse(invokeResponseData: InvokeResponseData) { + logger.log(Level.INFO, "Invoke receive OnResponse on $invokeResponseData") + responseCount++ + setSuccess() + } + + override fun onNoResponse(noInvokeResponseData: NoInvokeResponseData) { + logger.log(Level.INFO, "Invoke receive OnResponse on $noInvokeResponseData") + responseCount-- + } + + 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 element3: InvokeElement = + InvokeElement.newInstance( + /* endpointId= */ 1, + CLUSTER_ID_IDENTIFY, + IDENTIFY_COMMAND, + null, + """{"0:UINT":1}""" + ) + + val invokeList = listOf(element1, element2, element3) + currentCommissioner() + .pairDeviceWithAddress( + getNodeId(), + getRemoteAddr().address.hostAddress, + MATTER_PORT, + getDiscriminator(), + getSetupPINCode(), + null + ) + currentCommissioner().setCompletionListener(this) + waitCompleteMs(getTimeoutMillis()) + currentCommissioner() + .getConnectedDevicePointer(getNodeId(), InternalGetConnectedDeviceCallback()) + clear() + currentCommissioner().batchInvoke(InternalInvokeCallback(), devicePointer, invokeList, 0, 0) + waitCompleteMs(getTimeoutMillis()) + } + + companion object { + private val logger = Logger.getLogger(PairOnNetworkLongImBatchInvokeCommand::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_COMMONDS_NUM = 3 + } +} diff --git a/kotlin-detect-config.yaml b/kotlin-detect-config.yaml index 2ad54a05d6c483..c201496388af96 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/PairOnNetworkLongImBatchInvokeCommand.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..af473a0f297109 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 TestCmdOnnetworkLongImBatchInvoke(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.TestCmdOnnetworkLongImBatchInvoke(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/controller/java/AndroidCallbacks-JNI.cpp b/src/controller/java/AndroidCallbacks-JNI.cpp index c87b669f9b521f..1adb9c828a1601 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, BatchInvokeCallbackJni, newCallback) +(JNIEnv * env, jobject self) +{ + return newBatchInvokeCallback(env, self); +} + +JNI_METHOD(void, BatchInvokeCallbackJni, deleteCallback)(JNIEnv * env, jobject self, jlong callbackHandle) +{ + deleteBatchInvokeCallback(env, self, callbackHandle); +} diff --git a/src/controller/java/AndroidCallbacks.cpp b/src/controller/java/AndroidCallbacks.cpp index fb7975262dff57..b2d275b373e6a7 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,191 @@ jobject DecodeGeneralTLVValue(JNIEnv * env, TLV::TLVReader & readerForGeneralVal } } +BatchInvokeCallback::BatchInvokeCallback(jobject wrapperCallback) +{ + VerifyOrReturn(mWrapperCallbackRef.Init(wrapperCallback) == CHIP_NO_ERROR, + ChipLogError(Controller, "Could not init mWrapperCallbackRef for BatchInvokeCallback")); +} + +BatchInvokeCallback::~BatchInvokeCallback() +{ + if (mCommandSender != nullptr) + { + Platform::Delete(mCommandSender); + mCommandSender = nullptr; + } +} + +CHIP_ERROR BatchInvokeCallback::GetIndexForCommandRef(uint16_t aCommandRef, size_t & aIndex) +{ + auto search = commandRefToIndex.find(aCommandRef); + if (search == commandRefToIndex.end()) + { + return CHIP_ERROR_KEY_NOT_FOUND; + } + aIndex = commandRefToIndex[aCommandRef]; + return CHIP_NO_ERROR; +} + +void BatchInvokeCallback::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", "(IJJ[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", "(IJJILjava/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 BatchInvokeCallback::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 BatchInvokeCallback::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, "BatchInvokeCallback::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 BatchInvokeCallback::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()); +} + +CHIP_ERROR BatchInvokeCallback::AddCommandRefToIndexLookup(uint16_t aCommandRef, size_t aIndex) +{ + auto search = commandRefToIndex.find(aCommandRef); + if (search != commandRefToIndex.end()) + { + return CHIP_ERROR_DUPLICATE_KEY_ID; + } + commandRefToIndex[aCommandRef] = aIndex; + return CHIP_NO_ERROR; +} + jlong newConnectedDeviceCallback(JNIEnv * env, jobject self, jobject callback) { chip::DeviceLayer::StackLock lock; @@ -1085,5 +1270,19 @@ void deleteInvokeCallback(JNIEnv * env, jobject self, jlong callbackHandle) chip::Platform::Delete(invokeCallback); } +jlong newBatchInvokeCallback(JNIEnv * env, jobject self) +{ + chip::DeviceLayer::StackLock lock; + BatchInvokeCallback * invokeCallback = chip::Platform::New(self); + return reinterpret_cast(invokeCallback); +} + +void deleteBatchInvokeCallback(JNIEnv * env, jobject self, jlong callbackHandle) +{ + chip::DeviceLayer::StackLock lock; + BatchInvokeCallback * invokeCallback = reinterpret_cast(callbackHandle); + VerifyOrReturn(invokeCallback != nullptr, ChipLogError(Controller, "BatchInvokeCallback 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..53b9668648d97d 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,24 @@ struct InvokeCallback : public app::CommandSender::Callback JniGlobalReference mWrapperCallbackRef; }; +struct BatchInvokeCallback : public app::CommandSender::ExtendableCallback +{ + BatchInvokeCallback(jobject wrapperCallback); + ~BatchInvokeCallback(); + + 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; + + CHIP_ERROR GetIndexForCommandRef(uint16_t aCommandRef, size_t & aIndex); + CHIP_ERROR AddCommandRefToIndexLookup(uint16_t aCommandRef, size_t aIndex); + + std::unordered_map commandRefToIndex; + 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 +166,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 newBatchInvokeCallback(JNIEnv * env, jobject self); +void deleteBatchInvokeCallback(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..d6abbd0f4146d3 100644 --- a/src/controller/java/AndroidInteractionClient.cpp +++ b/src/controller/java/AndroidInteractionClient.cpp @@ -442,6 +442,195 @@ 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 nott 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 batchInvoke(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 batchInvoke() 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)); + + // CommandSender provides us with the CommandReference for this associated command. In order to match responses + // we have to add CommandRef to index lookup. + SuccessOrExit(err = callback->AddCommandRefToIndexLookup(finishCommandParams.commandRef.Value(), i)); + } + 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..4d1b01e126d02a 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 batchInvoke(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..2fa3cc642a332d 100644 --- a/src/controller/java/BUILD.gn +++ b/src/controller/java/BUILD.gn @@ -450,6 +450,8 @@ android_library("java") { sources = [ "src/chip/devicecontroller/AttestationInfo.java", "src/chip/devicecontroller/AttestationTrustStoreDelegate.java", + "src/chip/devicecontroller/BatchInvokeCallback.java", + "src/chip/devicecontroller/BatchInvokeCallbackJni.java", "src/chip/devicecontroller/CSRInfo.java", "src/chip/devicecontroller/ChipClusterException.java", "src/chip/devicecontroller/ChipCommandType.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/MatterCallbacks-JNI.cpp b/src/controller/java/MatterCallbacks-JNI.cpp index f20d9e47f093ff..0dd0451356b415 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, BatchInvokeCallbackJni, newCallback) +(JNIEnv * env, jobject self) +{ + return newBatchInvokeCallback(env, self); +} + +JNI_METHOD(void, BatchInvokeCallbackJni, deleteCallback)(JNIEnv * env, jobject self, jlong callbackHandle) +{ + deleteBatchInvokeCallback(env, self, callbackHandle); +} diff --git a/src/controller/java/MatterInteractionClient-JNI.cpp b/src/controller/java/MatterInteractionClient-JNI.cpp index 6a541723d041ed..851f7c06cbfeb0 100644 --- a/src/controller/java/MatterInteractionClient-JNI.cpp +++ b/src/controller/java/MatterInteractionClient-JNI.cpp @@ -65,3 +65,14 @@ JNI_METHOD(void, invoke) ChipLogError(Controller, "JNI IM Invoke Error: %" CHIP_ERROR_FORMAT, err.Format()); } } + +JNI_METHOD(void, batchInvoke) +(JNIEnv * env, jobject self, jlong handle, jlong callbackHandle, jlong devicePtr, jobject invokeElement, jint timedRequestTimeoutMs, + jint imTimeoutMs) +{ + CHIP_ERROR err = invoke(env, handle, callbackHandle, devicePtr, invokeElement, timedRequestTimeoutMs, imTimeoutMs); + if (err != CHIP_NO_ERROR) + { + ChipLogError(Controller, "JNI IM 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..569218caf4e7f7 --- /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 BatchInvokeCallback { + + /** + * 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); + + default 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..658119e36690de --- /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 BatchInvokeCallbackJni { + private final BatchInvokeCallback wrappedBatchInvokeCallback; + private long callbackHandle; + + public BatchInvokeCallbackJni(BatchInvokeCallback wrappedBatchInvokeCallback) { + this.wrappedBatchInvokeCallback = wrappedBatchInvokeCallback; + this.callbackHandle = newCallback(); + } + + long getCallbackHandle() { + return callbackHandle; + } + + private native long newCallback(); + + private native void deleteCallback(long callbackHandle); + + private void onError(Exception e) { + wrappedBatchInvokeCallback.onError(e); + } + + private void onResponse( + int endpointId, + long clusterId, + long commandId, + @Nullable Integer commandRef, + byte[] tlv, + String jsonString) { + wrappedBatchInvokeCallback.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) { + wrappedBatchInvokeCallback.onResponse( + InvokeResponseData.newInstance( + endpointId, + clusterId, + commandId, + Optional.ofNullable(commandRef), + status, + Optional.ofNullable(clusterStatus))); + } + + private void onNoResponse(int commandRef) { + wrappedBatchInvokeCallback.onNoResponse(NoInvokeResponseData.newInstance(commandRef)); + } + + private void onDone() { + wrappedBatchInvokeCallback.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..ae4a6723d23b2f 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 BatchInvoke command to target device + * @param BatchInvokeCallback 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 batchInvoke( + BatchInvokeCallback callback, + long devicePtr, + List invokeElementList, + int timedRequestTimeoutMs, + int imTimeoutMs) { + BatchInvokeCallbackJni jniCallback = new BatchInvokeCallbackJni(callback); + batchInvoke( + 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 batchInvoke( + 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/model/InvokeResponseData.java b/src/controller/java/src/chip/devicecontroller/model/InvokeResponseData.java new file mode 100644 index 00000000000000..5cd336649674f8 --- /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, + getJsonString(), + 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/lib/core/core.gni b/src/lib/core/core.gni index 8c7032ebac675a..02f98e3557f948 100644 --- a/src/lib/core/core.gni +++ b/src/lib/core/core.gni @@ -100,7 +100,8 @@ declare_args() { chip_tlv_validate_char_string_on_read = false chip_enable_sending_batch_commands = - current_os == "linux" || current_os == "mac" || current_os == "ios" + current_os == "linux" || current_os == "mac" || current_os == "ios" || + current_os == "android" } if (chip_target_style == "") {