diff --git a/docs/examples/index.md b/docs/examples/index.md index 83a1a6f936c90c..e5144f81885039 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -87,6 +87,15 @@ darwin-framework-tool/README java-matter-controller/README ``` +## Kotlin matter controller example + +```{toctree} +:glob: +:maxdepth: 1 + +kotlin-matter-controller/README +``` + ## Virtual Device App example ```{toctree} diff --git a/examples/kotlin-matter-controller/.gitignore b/examples/kotlin-matter-controller/.gitignore new file mode 100644 index 00000000000000..d59e52c6fd3f22 --- /dev/null +++ b/examples/kotlin-matter-controller/.gitignore @@ -0,0 +1,20 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties + +# Shared libs & JAR libs (those libs are copied into source tree for easy Android build). +*.so +*.jar +*.map diff --git a/examples/kotlin-matter-controller/.gn b/examples/kotlin-matter-controller/.gn new file mode 100644 index 00000000000000..5cd171abcda9ff --- /dev/null +++ b/examples/kotlin-matter-controller/.gn @@ -0,0 +1,25 @@ +# Copyright (c) 2020-2022 Project CHIP Authors +# +# 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. + +import("//build_overrides/build.gni") + +# The location of the build configuration file. +buildconfig = "${build_root}/config/BUILDCONFIG.gn" + +# CHIP uses angle bracket includes. +check_system_includes = true + +default_args = { + import("//args.gni") +} diff --git a/examples/kotlin-matter-controller/BUILD.gn b/examples/kotlin-matter-controller/BUILD.gn new file mode 100644 index 00000000000000..b13208e65ef749 --- /dev/null +++ b/examples/kotlin-matter-controller/BUILD.gn @@ -0,0 +1,56 @@ +# Copyright (c) 2022-2023 Project CHIP Authors +# +# 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. + +import("//build_overrides/build.gni") +import("//build_overrides/chip.gni") + +import("${chip_root}/build/chip/java/rules.gni") +import("${chip_root}/build/chip/tools.gni") + +kotlin_binary("kotlin-matter-controller") { + output_name = "kotlin-matter-controller" + deps = [ + "${chip_root}/src/controller/java:kotlin_matter_controller", + "${chip_root}/src/controller/java:onboarding_payload", + "${chip_root}/third_party/java_deps:annotation", + ] + + sources = [ + "java/src/com/matter/controller/Main.kt", + "java/src/com/matter/controller/commands/common/Argument.kt", + "java/src/com/matter/controller/commands/common/ArgumentType.kt", + "java/src/com/matter/controller/commands/common/Command.kt", + "java/src/com/matter/controller/commands/common/CommandManager.kt", + "java/src/com/matter/controller/commands/common/CredentialsIssuer.kt", + "java/src/com/matter/controller/commands/common/FutureResult.kt", + "java/src/com/matter/controller/commands/common/IPAddress.kt", + "java/src/com/matter/controller/commands/common/MatterCommand.kt", + "java/src/com/matter/controller/commands/common/RealResult.kt", + "java/src/com/matter/controller/commands/pairing/DiscoveryFilterType.kt", + "java/src/com/matter/controller/commands/pairing/PairOnNetworkLongCommand.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", + "java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImWriteCommand.kt", + "java/src/com/matter/controller/commands/pairing/PairingCommand.kt", + "java/src/com/matter/controller/commands/pairing/PairingModeType.kt", + "java/src/com/matter/controller/commands/pairing/PairingNetworkType.kt", + ] + + kotlinc_flags = [ "-Xlint:deprecation" ] +} + +group("default") { + deps = [ ":kotlin-matter-controller" ] +} diff --git a/examples/kotlin-matter-controller/Manifest.txt b/examples/kotlin-matter-controller/Manifest.txt new file mode 100644 index 00000000000000..2ef5017b51334a --- /dev/null +++ b/examples/kotlin-matter-controller/Manifest.txt @@ -0,0 +1,2 @@ +Main-Class: com.matter.controller.MainKt +Class-Path: ../lib/third_party/connectedhomeip/src/controller/java/*.jar ../lib/third_party/connectedhomeip/third_party/java_deps/stub_src/Android.jar ../lib/third_party/connectedhomeip/third_party/java_deps/json-20220924.jar ../lib/third_party/connectedhomeip/third_party/java_deps/jsr305-3.0.2.jar ../lib/third_party/connectedhomeip/third_party/java_deps/kotlin-stdlib-1.8.20.jar ../lib/third_party/connectedhomeip/third_party/java_deps/kotlinx-coroutines-core-jvm-1.7.3.jar diff --git a/examples/kotlin-matter-controller/README.md b/examples/kotlin-matter-controller/README.md new file mode 100644 index 00000000000000..c4e84cbd5982a4 --- /dev/null +++ b/examples/kotlin-matter-controller/README.md @@ -0,0 +1,118 @@ +# Matter Controller Kotlin App Example + +This is a Matter Controller Kotlin app that can be used to commission and +control Matter accessory devices. + +
+ +- [Matter Controller Kotlin App Example](#matter-controller-kotlin-app-example) + - [Requirements for building](#requirements-for-building) + - [Preparing for build](#preparing-for-build) + - [Building & Running the app](#building--running-the-app) + +
+ +## Requirements for building + +You need to have the following two software installed on your Ubuntu system: + +1. Java Runtime Environment (JRE) +2. Java Development Kit (JDK) + +openjdk version "11.0.20.1" or above is needed + +First, check if we have the Java Runtime Environment and Java Development Kit +installed on our system or not. + +```shell +java -version +``` + +If not, you can install the Java Runtime Environment and Java Development Kit on +your system through the following command as root: + +``` +sudo apt install default-jre +sudo apt install default-jdk +``` + +You also need to install kotlin compiler on your Ubuntu system: + +kotlin compiler version 1.8.10 or above is needed to compile +kotlin-matter-controller, if you already have lower version kotlin compiler +installed on your Ubuntu from apt, +you need to remove the Kotlin compiler package, run the following command: + +```shell +sudo apt-get remove kotlin +``` + +Wait for the removal process to complete. Once it's done, the Kotlin compiler +will be removed from your system. + +(Optional) If you want to remove any configuration files associated with Kotlin, +run the following command: + +```shell +sudo apt-get purge kotlin +``` + +Install kotlin compiler 1.8.10 or above, such as +[kotlin-compiler-1.8.10-url](https://github.com/JetBrains/kotlin/releases/download/v1.8.10/kotlin-compiler-1.8.10.zip) + +```shell +cd /usr/lib \ +&& sudo wget -q [kotlin-compiler-1.8.10-url] \ +&& sudo unzip kotlin-compiler-*.zip \ +&& sudo rm kotlin-compiler-*.zip \ +&& sudo rm -f kotlinc/bin/*.bat +``` + +Add a directory to PATH permanently by editing the `.bashrc` file located in the +Home directory. Follow these steps: + +1. Open the `.bashrc` file using a text editor. +2. Go to the end of the file. +3. Paste the export syntax at the end of the file. + +```shell +export PATH="/usr/lib/kotlinc/bin:$PATH" +``` + +### Linux + +```shell +export JAVA_PATH=[JDK path] +``` + +
+ +## Preparing for build + +Complete the following steps to prepare the Matter build: + +1. Check out the Matter repository. + +2. Run bootstrap (**only required first time**) + + ```shell + source scripts/bootstrap.sh + ``` + +## Building & Running the app + +This is the simplest option. In the command line, run the following command from +the top Matter directory: + +```shell +./scripts/build/build_examples.py --target linux-x64-kotlin-matter-controller build +``` + +The Java executable file `kotlin-matter-controller` will be generated at +`out/android-x86-kotlin-matter-controller/bin/` + +Run the kotlin-matter-controller + +```shell +java -Djava.library.path=../lib/jni -jar kotlin-matter-controller +``` diff --git a/examples/kotlin-matter-controller/args.gni b/examples/kotlin-matter-controller/args.gni new file mode 100644 index 00000000000000..9b0c312fbdc837 --- /dev/null +++ b/examples/kotlin-matter-controller/args.gni @@ -0,0 +1,26 @@ +# Copyright (c) 2020-2022 Project CHIP Authors +# +# 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. + +import("//build_overrides/chip.gni") + +import("${chip_root}/config/standalone/args.gni") + +chip_device_project_config_include = "" +chip_project_config_include = "" +chip_system_project_config_include = "" + +chip_project_config_include_dirs = + [ "${chip_root}/examples/kotlin-matter-controller/include" ] +chip_project_config_include_dirs += [ "${chip_root}/config/standalone" ] +chip_stack_lock_tracking = "fatal" diff --git a/examples/kotlin-matter-controller/build_overrides b/examples/kotlin-matter-controller/build_overrides new file mode 120000 index 00000000000000..b430cf6a2e6391 --- /dev/null +++ b/examples/kotlin-matter-controller/build_overrides @@ -0,0 +1 @@ +../build_overrides \ No newline at end of file diff --git a/examples/kotlin-matter-controller/include/CHIPProjectAppConfig.h b/examples/kotlin-matter-controller/include/CHIPProjectAppConfig.h new file mode 100644 index 00000000000000..09183cc4485256 --- /dev/null +++ b/examples/kotlin-matter-controller/include/CHIPProjectAppConfig.h @@ -0,0 +1,56 @@ +/* + * + * Copyright (c) 2020-2022 Project CHIP Authors + * + * 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. + */ + +#ifndef CHIPPROJECTCONFIG_H +#define CHIPPROJECTCONFIG_H + +#define CHIP_CONFIG_MAX_FABRICS 17 + +#define CHIP_CONFIG_EVENT_LOGGING_NUM_EXTERNAL_CALLBACKS 2 + +// Enable support functions for parsing command-line arguments +#define CHIP_CONFIG_ENABLE_ARG_PARSER 1 + +// Use a default pairing code if one hasn't been provisioned in flash. +#define CHIP_DEVICE_CONFIG_USE_TEST_SETUP_PIN_CODE 20202021 +#define CHIP_DEVICE_CONFIG_USE_TEST_SETUP_DISCRIMINATOR 0xF00 + +// Enable reading DRBG seed data from /dev/(u)random. +// This is needed for test applications and the CHIP device manager to function +// properly when CHIP_CONFIG_RNG_IMPLEMENTATION_CHIPDRBG is enabled. +#define CHIP_CONFIG_DEV_RANDOM_DRBG_SEED 1 + +// For convenience, Chip Security Test Mode can be enabled and the +// requirement for authentication in various protocols can be disabled. +// +// WARNING: These options make it possible to circumvent basic Chip security functionality, +// including message encryption. Because of this they MUST NEVER BE ENABLED IN PRODUCTION BUILDS. +// +#define CHIP_CONFIG_SECURITY_TEST_MODE 0 + +#define CHIP_CONFIG_ENABLE_UPDATE 1 + +#define CHIP_SYSTEM_CONFIG_PACKETBUFFER_POOL_SIZE 0 + +#define CHIP_CONFIG_DATA_MANAGEMENT_CLIENT_EXPERIMENTAL 1 + +#define CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONER_DISCOVERY 1 + +// Enable some test-only interaction model APIs. +#define CONFIG_BUILD_FOR_HOST_UNIT_TEST 1 + +#endif /* CHIPPROJECTCONFIG_H */ diff --git a/examples/kotlin-matter-controller/java/src/com/matter/controller/Main.kt b/examples/kotlin-matter-controller/java/src/com/matter/controller/Main.kt new file mode 100644 index 00000000000000..997c1b92b0ebb4 --- /dev/null +++ b/examples/kotlin-matter-controller/java/src/com/matter/controller/Main.kt @@ -0,0 +1,70 @@ +/* + * 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 com.matter.controller + +import com.matter.controller.commands.common.Command +import com.matter.controller.commands.common.CommandManager +import com.matter.controller.commands.common.CredentialsIssuer +import com.matter.controller.commands.pairing.PairOnNetworkLongCommand +import com.matter.controller.commands.pairing.PairOnNetworkLongImInvokeCommand +import com.matter.controller.commands.pairing.PairOnNetworkLongImReadCommand +import com.matter.controller.commands.pairing.PairOnNetworkLongImSubscribeCommand +import com.matter.controller.commands.pairing.PairOnNetworkLongImWriteCommand +import matter.controller.ControllerParams +import matter.controller.MatterController +import matter.controller.MatterControllerImpl + +private fun getPairingCommands( + controller: MatterController, + credentialsIssuer: CredentialsIssuer +): List { + return listOf( + PairOnNetworkLongCommand(controller, credentialsIssuer), + ) +} + +private fun getImCommands( + controller: MatterController, + credentialsIssuer: CredentialsIssuer +): List { + return listOf( + PairOnNetworkLongImReadCommand(controller, credentialsIssuer), + PairOnNetworkLongImSubscribeCommand(controller, credentialsIssuer), + PairOnNetworkLongImWriteCommand(controller, credentialsIssuer), + PairOnNetworkLongImInvokeCommand(controller, credentialsIssuer), + ) +} + +fun main(args: Array) { + val controller: MatterController = MatterControllerImpl(ControllerParams(countryCode = "US")) + val credentialsIssuer = CredentialsIssuer() + val commandManager = CommandManager() + + commandManager.register("pairing", getPairingCommands(controller, credentialsIssuer)) + commandManager.register("im", getImCommands(controller, credentialsIssuer)) + + try { + commandManager.run(args) + } catch (e: Exception) { + println("Run command failed with exception: " + e.message) + controller.close() + System.exit(1) + } + + controller.close() +} diff --git a/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/Argument.kt b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/Argument.kt new file mode 100644 index 00000000000000..7f9f11d183ec80 --- /dev/null +++ b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/Argument.kt @@ -0,0 +1,184 @@ +/* + * 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 com.matter.controller.commands.common + +import java.net.InetAddress +import java.net.UnknownHostException +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicLong + +// TODO Issue #29632:Refactor class Argument to have specializations for various types. +class Argument { + val name: String + val type: ArgumentType + private val minValue: Long + private val maxValue: Long + val value: Any + val desc: String? + val isOptional: Boolean + + constructor(name: String, value: IPAddress, optional: Boolean) { + this.name = name + type = ArgumentType.ADDRESS + minValue = 0 + maxValue = 0 + this.value = value + desc = null + isOptional = optional + } + + constructor(name: String, value: StringBuffer, desc: String?, optional: Boolean) { + this.name = name + type = ArgumentType.STRING + minValue = 0 + maxValue = 0 + this.value = value + this.desc = desc + isOptional = optional + } + + constructor(name: String, value: AtomicBoolean, desc: String?, optional: Boolean) { + this.name = name + type = ArgumentType.BOOL + minValue = 0 + maxValue = 0 + this.value = value + this.desc = desc + isOptional = optional + } + + constructor( + name: String, + min: Short, + max: Short, + value: AtomicInteger, + desc: String?, + optional: Boolean + ) { + this.name = name + type = ArgumentType.NUMBER_INT16 + minValue = min.toLong() + maxValue = max.toLong() + this.value = value + this.desc = desc + isOptional = optional + } + + constructor( + name: String, + min: Int, + max: Int, + value: AtomicInteger, + desc: String?, + optional: Boolean + ) { + this.name = name + type = ArgumentType.NUMBER_INT32 + minValue = min.toLong() + maxValue = max.toLong() + this.value = value + this.desc = desc + isOptional = optional + } + + constructor( + name: String, + min: Short, + max: Short, + value: AtomicLong, + desc: String?, + optional: Boolean + ) { + this.name = name + type = ArgumentType.NUMBER_INT32 + minValue = min.toLong() + maxValue = max.toLong() + this.value = value + this.desc = desc + isOptional = optional + } + + constructor( + name: String, + min: Long, + max: Long, + value: AtomicLong, + desc: String?, + optional: Boolean + ) { + this.name = name + type = ArgumentType.NUMBER_INT64 + minValue = min + maxValue = max + this.value = value + this.desc = desc + isOptional = optional + } + + fun setValue(value: String) { + var isValidArgument = false + when (type) { + ArgumentType.ATTRIBUTE -> { + val str = this.value as String + isValidArgument = value == str + } + ArgumentType.NUMBER_INT16 -> { + val numShort = this.value as AtomicInteger + numShort.set(value.toInt()) + isValidArgument = numShort.toInt() >= minValue && numShort.toInt() <= maxValue + } + ArgumentType.NUMBER_INT32 -> { + val num = this.value as AtomicInteger + num.set(value.toInt()) + isValidArgument = num.toInt() >= minValue && num.toInt() <= maxValue + } + ArgumentType.NUMBER_INT64 -> { + val numLong = this.value as AtomicLong + numLong.set(value.toLong()) + isValidArgument = numLong.toInt() >= minValue && numLong.toInt() <= maxValue + } + ArgumentType.STRING -> { + val stringBuffer = this.value as StringBuffer + stringBuffer.append(value) + val str = stringBuffer.toString() + isValidArgument = value == str + } + ArgumentType.BOOL -> { + val atomicBoolean = this.value as AtomicBoolean + try { + atomicBoolean.set(value.toBoolean()) + isValidArgument = true + } catch (e: Exception) { + isValidArgument = false + } + } + ArgumentType.ADDRESS -> + isValidArgument = + try { + val ipAddress = this.value as IPAddress + ipAddress.address = InetAddress.getByName(value) + true + } catch (e: UnknownHostException) { + false + } + else -> {} + } + require(isValidArgument) { "Invalid argument " + name + ": " + value } + } +} diff --git a/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/ArgumentType.kt b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/ArgumentType.kt new file mode 100644 index 00000000000000..acd31fbb0ac25a --- /dev/null +++ b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/ArgumentType.kt @@ -0,0 +1,39 @@ +/* + * 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 com.matter.controller.commands.common + +enum class ArgumentType { + NUMBER_INT8, + NUMBER_INT16, + NUMBER_INT32, + NUMBER_INT64, + FLOAT, + DOUBLE, + BOOL, + STRING, + CHARSTRING, + OCTETSTRING, + ATTRIBUTE, + ADDRESS, + COMPLEX, + CUSTOM, + VECTOR_BOOL, + VECTOR16, + VECTOR32, + VECTOR_CUSTOM +} diff --git a/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/Command.kt b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/Command.kt new file mode 100644 index 00000000000000..1b05e0205f9a0b --- /dev/null +++ b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/Command.kt @@ -0,0 +1,259 @@ +/* + * 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 com.matter.controller.commands.common + +import java.util.ArrayList +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicLong + +/** + * @brief Matter Controller command + * @details The base class of all the commands the Matter Controller supports, which are actions + * that may be performed. Commands are verb-like, such as pair a Matter device or discover Matter + * devices from the environment. + */ +abstract class Command(val name: String, val helpText: String? = null) { + private val arguments = ArrayList() + + fun getArgumentName(index: Int): String { + return arguments[index].name + } + + fun getArgumentsCount(): Int { + return arguments.size + } + + fun getArgumentIsOptional(index: Int): Boolean { + return arguments[index].isOptional + } + + /** + * @return A pointer to an Optional where the Attribute argument will be stored + * @brief Get attribute argument if it exists, there is at most one Attribute argument per command + */ + fun getAttribute(): String? { + return arguments.find { arg -> arg.type === ArgumentType.ATTRIBUTE }?.value as? String + } + + /** + * @return A pointer to an Optional where the argument description will be stored + * @brief Get argument description if it exists + */ + fun getArgumentDescription(index: Int): String? { + return arguments[index].desc + } + + fun addArgumentToList(arg: Argument) { + if (arg.isOptional || arguments.isEmpty()) { + // Safe to just append to the end of a list. + arguments.add(arg) + return + } + + // mandatory arg needs to be inserted before the optional arguments. + var index = 0 + while (index < arguments.size && !arguments[index].isOptional) { + index++ + } + + // Insert before the first optional arg. + arguments.add(index, arg) + } + + /** + * @param name The name that will be displayed in the command help + * @param out A pointer to a AtomicBoolean where the argv value will be stored + * @param desc The description of the argument that will be displayed in the command help + * @param optional Indicate if an optional argument + * @return The number of arguments currently added to the command + * @brief Add a bool command argument + */ + fun addArgument(name: String, out: AtomicBoolean, desc: String?, optional: Boolean) { + val arg = Argument(name, out, desc, optional) + addArgumentToList(arg) + } + + /** + * @param name The name that will be displayed in the command help + * @param min The minimum value of the argv value + * @param max The minimum value of the argv value + * @param out A pointer to a AtomicInteger where the argv value will be stored + * @param desc The description of the argument that will be displayed in the command help + * @param optional Indicate if an optional argument + * @return The number of arguments currently added to the command + * @brief Add a short command argument + */ + fun addArgument( + name: String, + min: Short, + max: Short, + out: AtomicInteger, + desc: String?, + optional: Boolean + ) { + val arg = Argument(name, min, max, out, desc, optional) + addArgumentToList(arg) + } + + /** + * @param name The name that will be displayed in the command help + * @param min The minimum value of the argv value + * @param max The minimum value of the argv value + * @param out A pointer to a AtomicInteger where the argv value will be stored + * @param desc The description of the argument that will be displayed in the command help + * @param optional Indicate if an optional argument + * @return The number of arguments currently added to the command + * @brief Add an int command argument + */ + fun addArgument( + name: String, + min: Int, + max: Int, + out: AtomicInteger, + desc: String?, + optional: Boolean + ) { + val arg = Argument(name, min, max, out, desc, optional) + addArgumentToList(arg) + } + + /** + * @param name The name that will be displayed in the command help + * @param min The minimum value of the argv value + * @param max The minimum value of the argv value + * @param out A pointer to a MutableInteger where the argv value will be stored + * @param desc The description of the argument that will be displayed in the command help + * @param optional Indicate if an optional argument + * @return The number of arguments currently added to the command + * @brief Add a long Integer command argument + */ + fun addArgument( + name: String, + min: Short, + max: Short, + out: AtomicLong, + desc: String?, + optional: Boolean + ) { + val arg = Argument(name, min, max, out, desc, optional) + addArgumentToList(arg) + } + + /** + * @param name The name that will be displayed in the command help + * @param min The minimum value of the argv value + * @param max The minimum value of the argv value + * @param out A pointer to a AtomicLong where the argv value will be stored + * @param desc The description of the argument that will be displayed in the command help + * @param optional Indicate if an optional argument + * @return The number of arguments currently added to the command + * @brief Add a long Integer command argument + */ + fun addArgument( + name: String, + min: Long, + max: Long, + out: AtomicLong, + desc: String?, + optional: Boolean + ) { + val arg = Argument(name, min, max, out, desc, optional) + addArgumentToList(arg) + } + + /** + * @param name The name that will be displayed in the command help + * @param out A pointer to a IPAddress where the argv value will be stored + * @param optional Indicate if an optional argument + * @return The number of arguments currently added to the command + * @brief Add an IP address command argument + */ + fun addArgument(name: String, out: IPAddress, optional: Boolean) { + val arg = Argument(name, out, optional) + addArgumentToList(arg) + } + + /** + * @param name The name that will be displayed in the command help + * @param out A pointer to a StringBuffer where the argv value will be stored + * @param desc The description of the argument that will be displayed in the command help + * @param optional Indicate if an optional argument + * @return The number of arguments currently added to the command + * @brief Add a String command argument + */ + fun addArgument(name: String, out: StringBuffer, desc: String?, optional: Boolean) { + val arg = Argument(name, out, desc, optional) + addArgumentToList(arg) + } + + /** + * @param args Supplied command-line arguments as an array of String objects. + * @brief Initialize command arguments + */ + fun setArgumentValues(args: Array) { + val argc = args.size + var mandatoryArgsCount = 0 + var currentIndex = 0 + for (arg in arguments) { + if (!arg.isOptional) { + mandatoryArgsCount++ + } + } + require(argc >= mandatoryArgsCount) { + "setArgumentValues: Wrong arguments number: $argc instead of $mandatoryArgsCount" + } + + // Initialize mandatory arguments + for (i in 0 until mandatoryArgsCount) { + setArgumentValue(currentIndex++, args[i]) + } + + // Initialize optional arguments + // Optional arguments expect a name and a value, so it is increased by 2 on every step. + var i = mandatoryArgsCount + while (i < argc) { + + // optional arguments starts with OPTIONAL_ARGUMENT_PREFIX + require( + !(args[i].length <= OPTIONAL_ARGUMENT_PREFIX_LENGTH && + !args[i].startsWith(OPTIONAL_ARGUMENT_PREFIX)) + ) { + "setArgumentValues: Invalid optional argument: " + args[i] + } + if (args[i].substring(OPTIONAL_ARGUMENT_PREFIX_LENGTH) == arguments[currentIndex].name) { + require(i + 1 < argc) { + "setArgumentValues: Optional argument " + args[i] + " missing value" + } + setArgumentValue(currentIndex++, args[i + 1]) + } + i += 2 + } + } + + private fun setArgumentValue(argIndex: Int, argValue: String) { + arguments[argIndex].setValue(argValue) + } + + @Throws(Exception::class) abstract fun run() + + companion object { + private const val OPTIONAL_ARGUMENT_PREFIX = "--" + private const val OPTIONAL_ARGUMENT_PREFIX_LENGTH = 2 + } +} diff --git a/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/CommandManager.kt b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/CommandManager.kt new file mode 100644 index 00000000000000..36389a743cf8c5 --- /dev/null +++ b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/CommandManager.kt @@ -0,0 +1,313 @@ +/* + * 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 com.matter.controller.commands.common + +import java.util.Arrays +import java.util.HashMap +import java.util.logging.Level +import java.util.logging.Logger +import kotlin.collections.MutableMap + +class CommandManager { + private val clusters: MutableMap> = HashMap() + + fun register(clusterName: String, commandsList: List) { + clusters[clusterName] = commandsList + } + + @Throws(Exception::class) + fun run(args: Array) { + val command: Command? + if (args.size < 1) { + logger.log(Level.INFO, "Missing cluster name") + showHelpInfo() + return + } + val commands = clusters[args[0]] + if (commands == null) { + logger.log(Level.INFO, "Unknown cluster: " + args[0]) + showHelpInfo() + return + } + if (args.size < 2) { + logger.log(Level.INFO, "Missing command name") + showCluster(args[0], commands) + return + } + if (!isGlobalCommand(args[0])) { + command = getCommand(commands, args[1]) + if (command == null) { + System.out.printf("Unknown command: %s", args[1]) + showCluster(args[0], commands) + throw IllegalArgumentException() + } + } else if (isEventCommand(args[1])) { + if (args.size < 3) { + logger.log(Level.INFO, "Missing event name") + showClusterEvents(args[0], args[1], commands) + throw IllegalArgumentException() + } + command = getGlobalCommand(commands, args[1], args[2]) + if (command == null) { + logger.log(Level.INFO, "Unknown event: " + args[2]) + showClusterEvents(args[0], args[1], commands) + throw IllegalArgumentException() + } + } else { + if (args.size < 3) { + logger.log(Level.INFO, "Missing attribute name") + showClusterAttributes(args[0], args[1], commands) + throw IllegalArgumentException() + } + command = getGlobalCommand(commands, args[1], args[2]) + if (command == null) { + logger.log(Level.INFO, "Unknown attribute: " + args[2]) + showClusterAttributes(args[0], args[1], commands) + throw IllegalArgumentException() + } + } + + // need skip over binary and command name and only get arguments + val temp = Arrays.copyOfRange(args, 2, args.size) + try { + command.setArgumentValues(temp) + } catch (e: IllegalArgumentException) { + logger.log(Level.INFO, "Arguments init failed with exception: " + e.message) + showCommand(args[0], command) + System.exit(1) + } + command.run() + } + + private fun isAttributeCommand(commandName: String): Boolean { + return commandName == "read" || commandName == "write" || commandName == "subscribe" + } + + private fun isEventCommand(commandName: String): Boolean { + return commandName == "read-event" || commandName == "subscribe-event" + } + + private fun isGlobalCommand(commandName: String): Boolean { + return isAttributeCommand(commandName) || isEventCommand(commandName) + } + + private fun getCommand(commands: List, commandName: String): Command? { + for (command in commands) { + if (commandName == command.name) { + return command + } + } + return null + } + + private fun getGlobalCommand( + commands: List, + commandName: String, + attributeName: String + ): Command? { + for (command in commands) { + if (commandName == command.name && attributeName == command.getAttribute()) { + return command + } + } + return null + } + + private fun showHelpInfo() { + logger.log(Level.INFO, "Usage:") + logger.log(Level.INFO, " cluster_name command_name [param1 param2 ...]") + logger.log(Level.INFO, "\n") + logger.log( + Level.INFO, + " +-------------------------------------------------------------------------------------+" + ) + logger.log( + Level.INFO, + " | Clusters: |" + ) + logger.log( + Level.INFO, + " +-------------------------------------------------------------------------------------+" + ) + for (key in clusters.keys) { + System.out.printf(" | * %-82s|\n", key) + } + logger.log( + Level.INFO, + " +-------------------------------------------------------------------------------------+" + ) + } + + private fun showCluster(clusterName: String, commands: List) { + logger.log(Level.INFO, "Usage:") + logger.log( + Level.INFO, + " kotlin-matter-controller $clusterName command_name [param1 param2 ...]" + ) + logger.log(Level.INFO, "\n") + logger.log( + Level.INFO, + " +-------------------------------------------------------------------------------------+" + ) + logger.log( + Level.INFO, + " | Commands: |" + ) + logger.log( + Level.INFO, + " +-------------------------------------------------------------------------------------+" + ) + var readCommand = false + var writeCommand = false + var subscribeCommand = false + var readEventCommand = false + var subscribeEventCommand = false + for (command in commands) { + var shouldPrint = true + val cmdName = command.name + if (isGlobalCommand(cmdName)) { + if (cmdName == "read" && !readCommand) { + readCommand = true + } else if (cmdName == "write" && !writeCommand) { + writeCommand = true + } else if (cmdName == "subscribe" && !subscribeCommand) { + subscribeCommand = true + } else if (cmdName == "read-event" && !readEventCommand) { + readEventCommand = true + } else if (cmdName == "subscribe-event" && !subscribeEventCommand) { + subscribeEventCommand = true + } else { + shouldPrint = false + } + } + if (shouldPrint) { + System.out.printf(" | * %-82s|\n", cmdName) + } + } + logger.log( + Level.INFO, + " +-------------------------------------------------------------------------------------+\n" + ) + } + + private fun showClusterAttributes( + clusterName: String, + commandName: String, + commands: List + ) { + logger.log(Level.INFO, "Usage:") + System.out.printf( + " kotlin-matter-controller %s %s attribute-name [param1 param2 ...]\n", + clusterName, + commandName + ) + logger.log(Level.INFO, "\n") + logger.log( + Level.INFO, + " +-------------------------------------------------------------------------------------+" + ) + logger.log( + Level.INFO, + " | Attributes: |" + ) + logger.log( + Level.INFO, + " +-------------------------------------------------------------------------------------+" + ) + for (command in commands) { + if (commandName == command.name) { + System.out.printf(" | * %-82s|\n", command.getAttribute()) + } + } + logger.log( + Level.INFO, + " +-------------------------------------------------------------------------------------+" + ) + } + + private fun showClusterEvents(clusterName: String, commandName: String, commands: List) { + logger.log(Level.INFO, "Usage:") + System.out.printf( + " kotlin-matter-controller %s %s event-name [param1 param2 ...]\n", + clusterName, + commandName + ) + logger.log(Level.INFO, "\n") + logger.log( + Level.INFO, + " +-------------------------------------------------------------------------------------+" + ) + logger.log( + Level.INFO, + " | Events: |" + ) + logger.log( + Level.INFO, + " +-------------------------------------------------------------------------------------+" + ) + for (command in commands) { + if (commandName == command.name) { + System.out.printf(" | * %-82s|\n", command.getAttribute()) + } + } + logger.log( + Level.INFO, + " +-------------------------------------------------------------------------------------+" + ) + } + + private fun showCommand(clusterName: String, command: Command) { + logger.log(Level.INFO, "Usage:") + var arguments: String? = command.name + var description = "" + val argumentsCount = command.getArgumentsCount() + for (i in 0 until argumentsCount) { + var arg = "" + val isOptional = command.getArgumentIsOptional(i) + if (isOptional) { + arg += "[--" + } + arg += command.getArgumentName(i) + if (isOptional) { + arg += "]" + } + arguments += " " + arguments += arg + val argDescription = command.getArgumentDescription(i) + if (argDescription != null) { + description += "\n" + description += arg + description += ":\n " + description += argDescription + description += "\n" + } + } + System.out.format(" %s %s\n", clusterName, arguments) + val helpText = command.helpText + if (helpText != null) { + System.out.format("\n%s\n", helpText) + } + if (!description.isEmpty()) { + logger.log(Level.INFO, description) + } + } + + companion object { + private val logger = Logger.getLogger(CommandManager::class.java.name) + } +} diff --git a/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/CredentialsIssuer.kt b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/CredentialsIssuer.kt new file mode 100644 index 00000000000000..a0a384860af93c --- /dev/null +++ b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/CredentialsIssuer.kt @@ -0,0 +1,24 @@ +/* + * 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 com.matter.controller.commands.common + +/** + * Credentials Issuer which contains all credential information of the issuer of the command, such + * as operational credentials for a given fabric, the DAC verifier of the commissioner, etc .. + */ +class CredentialsIssuer diff --git a/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/FutureResult.kt b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/FutureResult.kt new file mode 100644 index 00000000000000..5fe4b5240dce3d --- /dev/null +++ b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/FutureResult.kt @@ -0,0 +1,77 @@ +/* + * 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 com.matter.controller.commands.common + +import java.util.concurrent.TimeoutException +import java.util.logging.Level +import java.util.logging.Logger + +/** + * Implements the future result that encapculates the optional realResult, application would wait + * for realResult set by other thread wben receiving data from the other end. If the expected + * duration elapsed without receiving the expected realResult, the runtime exception would be + * raised. + */ +class FutureResult { + private var realResult: RealResult? = null + private val lock = Object() + var timeoutMs: Long = 0 + + fun setRealResult(realResult: RealResult) { + synchronized(lock) { + if (this.realResult != null) { + throw TimeoutException("Error, real result has been set!") + } + this.realResult = realResult + lock.notifyAll() + } + } + + fun waitResult() { + val start = System.currentTimeMillis() + synchronized(lock) { + while (realResult == null) { + val remainingTime = timeoutMs - (System.currentTimeMillis() - start) + if (remainingTime <= 0) { + throw TimeoutException("Timeout!") + } + + try { + lock.wait(remainingTime) + } catch (e: InterruptedException) { + logger.log(Level.INFO, "Wait Result failed with exception: " + e.message) + } + } + + val errorResult = realResult as? RealResult.Error + if (errorResult != null) { + logger.log(Level.INFO, "Error: ${errorResult.error}") + throw TimeoutException("Received failure test result") + } + } + } + + fun clear() { + synchronized(lock) { realResult = null } + } + + companion object { + private val logger = Logger.getLogger(FutureResult::class.java.name) + } +} diff --git a/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/IPAddress.kt b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/IPAddress.kt new file mode 100644 index 00000000000000..c28ce9c5d5a055 --- /dev/null +++ b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/IPAddress.kt @@ -0,0 +1,26 @@ +/* + * 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 com.matter.controller.commands.common + +import java.net.InetAddress + +class IPAddress(var address: InetAddress) { + override fun toString(): String { + return address.toString() + } +} diff --git a/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/MatterCommand.kt b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/MatterCommand.kt new file mode 100644 index 00000000000000..6ca0e223ee921c --- /dev/null +++ b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/MatterCommand.kt @@ -0,0 +1,113 @@ +/* + * 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 com.matter.controller.commands.common + +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicLong +import matter.controller.MatterController + +abstract class MatterCommand( + private val matterController: MatterController, + private val credIssuerCmds: CredentialsIssuer?, + commandName: String, + helpText: String? = null +) : Command(commandName, helpText) { + private val commissionerName = StringBuffer() + private val paaTrustStorePath = StringBuffer() + private val cdTrustStorePath = StringBuffer() + private val commissionerNodeId: AtomicLong = AtomicLong() + private val useMaxSizedCerts: AtomicBoolean = AtomicBoolean() + private val onlyAllowTrustedCdKeys: AtomicBoolean = AtomicBoolean() + private val futureResult = FutureResult() + + init { + addArgument( + "paa-trust-store-path", + paaTrustStorePath, + "Path to directory holding PAA certificate information. Can be absolute or relative to the current working " + + "directory.", + true + ) + addArgument( + "cd-trust-store-path", + cdTrustStorePath, + "Path to directory holding CD certificate information. Can be absolute or relative to the current working " + + "directory.", + true + ) + addArgument( + "commissioner-name", + commissionerName, + "Name of fabric to use. Valid values are \"alpha\", \"beta\", \"gamma\", and integers greater than or equal to " + + "4. The default if not specified is \"alpha\".", + true + ) + addArgument( + "commissioner-nodeid", + 0, + Long.MAX_VALUE, + commissionerNodeId, + "The node id to use for kotlin-matter-controller. If not provided, kTestControllerNodeId (112233, 0x1B669) will be used.", + true + ) + addArgument( + "use-max-sized-certs", + useMaxSizedCerts, + "Maximize the size of operational certificates. If not provided or 0 (\"false\"), normally sized operational " + + "certificates are generated.", + true + ) + addArgument( + "only-allow-trusted-cd-keys", + onlyAllowTrustedCdKeys, + "Only allow trusted CD verifying keys (disallow test keys). If not provided or 0 (\"false\"), untrusted CD " + + "verifying keys are allowed. If 1 (\"true\"), test keys are disallowed.", + true + ) + } + + // This method returns the commissioner instance to be used for running the command. + fun currentCommissioner(): MatterController { + return matterController + } + + /////////// Command Interface ///////// + @Throws(Exception::class) + override fun run() { + runCommand() + } + + protected abstract fun runCommand() + + fun setSuccess() { + futureResult.setRealResult(RealResult.success()) + } + + fun setFailure(error: String?) { + futureResult.setRealResult(RealResult.error(error)) + } + + fun waitCompleteMs(timeoutMs: Long) { + futureResult.timeoutMs = timeoutMs + futureResult.waitResult() + } + + fun clear() { + futureResult.clear() + } +} diff --git a/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/RealResult.kt b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/RealResult.kt new file mode 100644 index 00000000000000..50debc33f0d014 --- /dev/null +++ b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/common/RealResult.kt @@ -0,0 +1,41 @@ +/* + * 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 com.matter.controller.commands.common + +/** + * Represents a result that can either indicate success or failure with an associated error message. + * + * In the context of RealResult, success is represented by [Success] and failure by [Error]. When + * there is an error, an error message explains the reason for the failure. + */ +sealed class RealResult { + data class Error(val error: String) : RealResult() + + object Success : RealResult() + + companion object { + fun success(): RealResult { + return Success + } + + fun error(error: String?): RealResult { + return error?.let { Error(it) } ?: Success + } + } +} diff --git a/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/DiscoveryFilterType.kt b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/DiscoveryFilterType.kt new file mode 100644 index 00000000000000..eb254ae5ec638d --- /dev/null +++ b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/DiscoveryFilterType.kt @@ -0,0 +1,30 @@ +/* + * 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 com.matter.controller.commands.pairing + +enum class DiscoveryFilterType { + NONE, + SHORT_DISCRIMINATOR, + LONG_DISCRIMINATOR, + VENDOR_ID, + DEVICE_TYPE, + COMMISSIONING_MODE, + INSTANCE_NAME, + COMMISSIONER, + COMPRESSED_FABRIC_ID +} diff --git a/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongCommand.kt b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongCommand.kt new file mode 100644 index 00000000000000..594d30d5fa3c8c --- /dev/null +++ b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongCommand.kt @@ -0,0 +1,46 @@ +/* + * 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 com.matter.controller.commands.pairing + +import com.matter.controller.commands.common.CredentialsIssuer +import matter.controller.MatterController + +private const val MATTER_PORT = 5540 + +class PairOnNetworkLongCommand(controller: MatterController, credsIssue: CredentialsIssuer?) : + PairingCommand( + controller, + "onnetwork-long", + credsIssue, + PairingModeType.ON_NETWORK, + PairingNetworkType.NONE, + DiscoveryFilterType.LONG_DISCRIMINATOR + ) { + override fun runCommand() { + currentCommissioner() + .pairDevice( + getNodeId(), + getRemoteAddr().address.hostAddress, + MATTER_PORT, + getDiscriminator(), + getSetupPINCode(), + ) + currentCommissioner().setCompletionListener(this) + waitCompleteMs(getTimeoutMillis()) + } +} diff --git a/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImInvokeCommand.kt b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImInvokeCommand.kt new file mode 100644 index 00000000000000..313e67419927a0 --- /dev/null +++ b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImInvokeCommand.kt @@ -0,0 +1,96 @@ +/* + * 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 com.matter.controller.commands.pairing + +import chip.tlv.AnonymousTag +import chip.tlv.ContextSpecificTag +import chip.tlv.TlvWriter +import com.matter.controller.commands.common.CredentialsIssuer +import java.time.Duration +import java.util.logging.Level +import java.util.logging.Logger +import kotlinx.coroutines.runBlocking +import matter.controller.InvokeRequest +import matter.controller.InvokeResponse +import matter.controller.MatterController +import matter.controller.model.CommandPath + +class PairOnNetworkLongImInvokeCommand( + controller: MatterController, + credsIssue: CredentialsIssuer? +) : + PairingCommand( + controller, + "onnetwork-long-im-invoke", + credsIssue, + PairingModeType.ON_NETWORK, + PairingNetworkType.NONE, + DiscoveryFilterType.LONG_DISCRIMINATOR + ) { + override fun runCommand() { + val IdentifyTime: UShort = 1u + val tlvWriter1 = TlvWriter() + tlvWriter1.startStructure(AnonymousTag) + tlvWriter1.put(ContextSpecificTag(0), IdentifyTime) + tlvWriter1.endStructure() + + val element1: InvokeRequest = + InvokeRequest( + CommandPath(endpointId = 0u, clusterId = CLUSTER_ID_IDENTIFY, commandId = IDENTIFY_COMMAND), + tlvPayload = tlvWriter1.getEncoded(), + timedRequest = Duration.ZERO + ) + + currentCommissioner() + .pairDevice( + getNodeId(), + getRemoteAddr().address.hostAddress, + MATTER_PORT, + getDiscriminator(), + getSetupPINCode(), + ) + currentCommissioner().setCompletionListener(this) + waitCompleteMs(getTimeoutMillis()) + + runBlocking { + try { + val response: InvokeResponse = currentCommissioner().invoke(element1) + logger.log(Level.INFO, "Invoke command succeeded") + if (response.payload.isNotEmpty()) { + // TODO:Handle TLV data response + } + } catch (ex: Exception) { + setFailure("invoke failure: ${ex.message}") + } finally { + clear() + } + } + + setSuccess() + } + + companion object { + private val logger = Logger.getLogger(PairOnNetworkLongImInvokeCommand::class.java.name) + + private const val MATTER_PORT = 5540 + private const val CLUSTER_ID_IDENTIFY = 0x0003u + private const val IDENTIFY_COMMAND = 0u + private const val CLUSTER_ID_TEST = 0xFFF1FC05u + private const val TEST_ADD_ARGUMENT_COMMAND = 0X04u + } +} diff --git a/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImReadCommand.kt b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImReadCommand.kt new file mode 100644 index 00000000000000..15cf62ed504067 --- /dev/null +++ b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImReadCommand.kt @@ -0,0 +1,109 @@ +package com.matter.controller.commands.pairing + +import com.matter.controller.commands.common.CredentialsIssuer +import java.util.logging.Level +import java.util.logging.Logger +import kotlinx.coroutines.runBlocking +import matter.controller.MatterController +import matter.controller.ReadData +import matter.controller.ReadRequest +import matter.controller.ReadResponse +import matter.controller.model.AttributePath +import matter.controller.model.EventPath + +class PairOnNetworkLongImReadCommand(controller: MatterController, credsIssue: CredentialsIssuer?) : + PairingCommand( + controller, + "onnetwork-long-im-read", + credsIssue, + PairingModeType.ON_NETWORK, + PairingNetworkType.NONE, + DiscoveryFilterType.LONG_DISCRIMINATOR + ) { + override fun runCommand() { + val attributePaths = + listOf( + AttributePath( + endpointId = DEFAULT_ENDPOINT, + clusterId = CLUSTER_ID_BASIC, + attributeId = ATTR_ID_LOCAL_CONFIG_DISABLED, + ), + AttributePath( + endpointId = DEFAULT_ENDPOINT, + clusterId = CLUSTER_ID_BASIC, + attributeId = GLOBAL_ATTRIBUTE_LIST, + ) + ) + + val eventPaths = + listOf( + EventPath( + endpointId = WILDCARD_ENDPOINT_ID, + clusterId = WILDCARD_CLUSTER_ID, + eventId = WILDCARD_EVENT_ID + ) + ) + + val readRequest: ReadRequest = ReadRequest(eventPaths, attributePaths) + + currentCommissioner() + .pairDevice( + getNodeId(), + getRemoteAddr().address.hostAddress, + MATTER_PORT, + getDiscriminator(), + getSetupPINCode(), + ) + currentCommissioner().setCompletionListener(this) + waitCompleteMs(getTimeoutMillis()) + + runBlocking { + try { + val response: ReadResponse = currentCommissioner().read(readRequest) + logger.log(Level.INFO, "Read command succeeded") + validateResponse(response) + } catch (ex: Exception) { + logger.log(Level.WARNING, "General read failure occurred with error ${ex.message}") + setFailure("read failure") + } finally { + clear() + } + } + + setSuccess() + } + + private fun findAttributeById(successes: List, id: UInt): ReadData.Attribute? { + return successes.filterIsInstance().find { it.path.attributeId == id } + } + + private fun findEventById(successes: List, id: UInt): ReadData.Event? { + return successes.filterIsInstance().find { it.path.eventId == id } + } + + private fun validateResponse(response: ReadResponse) { + require(response.successes.isNotEmpty()) { "Unexpected: response.successes is empty" } + require(response.failures.isEmpty()) { "Unexpected: response.failures is not empty" } + + val localConfigDisabledAttribute = + findAttributeById(response.successes, ATTR_ID_LOCAL_CONFIG_DISABLED) + requireNotNull(localConfigDisabledAttribute) { "No local config disabled attribute found." } + + // TODO: Add more validation rules as needed + } + + companion object { + private val logger = Logger.getLogger(PairOnNetworkLongImReadCommand::class.java.name) + + private const val MATTER_PORT = 5540 + private const val DEFAULT_ENDPOINT: UShort = 0u + private const val WILDCARD_ENDPOINT_ID: UShort = 0xffffu + private const val WILDCARD_CLUSTER_ID: UInt = 0xffffffffu + private const val WILDCARD_ATTRIBUTE_ID: UInt = 0xffffffffu + private const val WILDCARD_EVENT_ID: UInt = 0xffffffffu + private const val CLUSTER_ID_BASIC: UInt = 0x0028u + private const val ATTR_ID_LOCAL_CONFIG_DISABLED: UInt = 16u + private const val EVENT_ID_START_UP: UInt = 0u + private const val GLOBAL_ATTRIBUTE_LIST: UInt = 65531u + } +} diff --git a/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImSubscribeCommand.kt b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImSubscribeCommand.kt new file mode 100644 index 00000000000000..c2ea79683ec980 --- /dev/null +++ b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImSubscribeCommand.kt @@ -0,0 +1,113 @@ +package com.matter.controller.commands.pairing + +import com.matter.controller.commands.common.CredentialsIssuer +import java.util.logging.Level +import java.util.logging.Logger +import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.runBlocking +import matter.controller.MatterController +import matter.controller.SubscribeRequest +import matter.controller.SubscriptionState +import matter.controller.model.AttributePath +import matter.controller.model.EventPath + +class PairOnNetworkLongImSubscribeCommand( + controller: MatterController, + credsIssue: CredentialsIssuer? +) : + PairingCommand( + controller, + "onnetwork-long-im-subscribe", + credsIssue, + PairingModeType.ON_NETWORK, + PairingNetworkType.NONE, + DiscoveryFilterType.LONG_DISCRIMINATOR + ) { + override fun runCommand() { + val attributePaths = + listOf( + AttributePath( + endpointId = WILDCARD_ENDPOINT_ID, + clusterId = WILDCARD_CLUSTER_ID, + attributeId = WILDCARD_EVENT_ID, + ) + ) + + val eventPaths = + listOf( + EventPath( + endpointId = WILDCARD_ENDPOINT_ID, + clusterId = WILDCARD_CLUSTER_ID, + eventId = WILDCARD_EVENT_ID + ) + ) + + val subscribeRequest: SubscribeRequest = SubscribeRequest(eventPaths, attributePaths) + + currentCommissioner() + .pairDevice( + getNodeId(), + getRemoteAddr().address.hostAddress, + MATTER_PORT, + getDiscriminator(), + getSetupPINCode(), + ) + currentCommissioner().setCompletionListener(this) + waitCompleteMs(getTimeoutMillis()) + + runBlocking { + try { + startSubscription(subscribeRequest) + } catch (ex: Exception) { + logger.log(Level.WARNING, "General subscribe failure occurred with error ${ex.message}") + setFailure("subscribe failure") + } finally { + clear() + } + } + + setSuccess() + } + + private suspend fun startSubscription(request: SubscribeRequest) { + logger.log(Level.INFO, "Starting subscription") + + currentCommissioner() + .subscribe(request) + .takeWhile { subscriptionState -> + // Keep collecting as long as it's not SubscriptionEstablished + subscriptionState !is SubscriptionState.SubscriptionEstablished + } + .collect { subscriptionState -> + when (subscriptionState) { + is SubscriptionState.NodeStateUpdate -> { + logger.log(Level.INFO, "Received NodeStateUpdate: ${subscriptionState.updateState}") + + // TODO: Add more validation rules as needed + } + is SubscriptionState.SubscriptionErrorNotification -> { + logger.log( + Level.WARNING, + "Received SubscriptionErrorNotification with terminationCause: ${subscriptionState.terminationCause}" + ) + } + is SubscriptionState.SubscriptionEstablished -> { + logger.log(Level.INFO, "Subscription is established") + } + else -> { + logger.log(Level.SEVERE, "Unexpected subscription state: $subscriptionState") + } + } + } + } + + companion object { + private val logger = Logger.getLogger(PairOnNetworkLongImSubscribeCommand::class.java.name) + + private const val MATTER_PORT = 5540 + private const val WILDCARD_ENDPOINT_ID: UShort = 0xffffu + private const val WILDCARD_CLUSTER_ID: UInt = 0xffffffffu + private const val WILDCARD_ATTRIBUTE_ID: UInt = 0xffffffffu + private const val WILDCARD_EVENT_ID: UInt = 0xffffffffu + } +} diff --git a/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImWriteCommand.kt b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImWriteCommand.kt new file mode 100644 index 00000000000000..376e132194c889 --- /dev/null +++ b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImWriteCommand.kt @@ -0,0 +1,116 @@ +/* + * 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 com.matter.controller.commands.pairing + +import chip.tlv.AnonymousTag +import chip.tlv.TlvWriter +import com.matter.controller.commands.common.CredentialsIssuer +import java.time.Duration +import java.util.logging.Level +import java.util.logging.Logger +import kotlinx.coroutines.runBlocking +import matter.controller.MatterController +import matter.controller.WriteRequest +import matter.controller.WriteRequests +import matter.controller.WriteResponse +import matter.controller.model.AttributePath + +class PairOnNetworkLongImWriteCommand( + controller: MatterController, + credsIssue: CredentialsIssuer? +) : + PairingCommand( + controller, + "onnetwork-long-im-write", + credsIssue, + PairingModeType.ON_NETWORK, + PairingNetworkType.NONE, + DiscoveryFilterType.LONG_DISCRIMINATOR + ) { + override fun runCommand() { + val tlvWriter1 = TlvWriter() + tlvWriter1.put(AnonymousTag, true) + val writeRequests: WriteRequests = + WriteRequests( + requests = + listOf( + WriteRequest( + attributePath = + AttributePath( + endpointId = 0u, + clusterId = CLUSTER_ID_BASIC, + attributeId = ATTR_ID_LOCAL_CONFIG_DISABLED + ), + tlvPayload = tlvWriter1.getEncoded() + ) + ), + timedRequest = Duration.ZERO + ) + + currentCommissioner() + .pairDevice( + getNodeId(), + getRemoteAddr().address.hostAddress, + MATTER_PORT, + getDiscriminator(), + getSetupPINCode(), + ) + currentCommissioner().setCompletionListener(this) + waitCompleteMs(getTimeoutMillis()) + + runBlocking { + try { + val response: WriteResponse = currentCommissioner().write(writeRequests) + + if (response is WriteResponse.Success) { + logger.log(Level.INFO, "Write command succeeded") + } else if (response is WriteResponse.PartialWriteFailure) { + logger.log( + Level.WARNING, + "Partial write failure occurred with ${response.failures.size} errors" + ) + + for ((index, error) in response.failures.withIndex()) { + logger.log(Level.WARNING, "Error ${index + 1}:") + logger.log(Level.WARNING, "Attribute Path: ${error.attributePath}") + logger.log(Level.WARNING, "Exception Message: ${error.ex.message}") + } + + setFailure("invoke failure") + } + } catch (ex: Exception) { + setFailure("invoke failure: ${ex.message}") + } catch (ex: Exception) { + logger.log(Level.WARNING, "General write failure occurred with error ${ex.message}") + setFailure("invoke failure") + } finally { + clear() + } + } + + setSuccess() + } + + companion object { + private val logger = Logger.getLogger(PairOnNetworkLongImWriteCommand::class.java.name) + + private const val MATTER_PORT = 5540 + private const val CLUSTER_ID_BASIC = 0x0028u + private const val ATTR_ID_LOCAL_CONFIG_DISABLED = 16u + } +} diff --git a/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairingCommand.kt b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairingCommand.kt new file mode 100644 index 00000000000000..96b856568ccbc5 --- /dev/null +++ b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairingCommand.kt @@ -0,0 +1,219 @@ +/* + * 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 com.matter.controller.commands.pairing + +import com.matter.controller.commands.common.CredentialsIssuer +import com.matter.controller.commands.common.IPAddress +import com.matter.controller.commands.common.MatterCommand +import java.net.InetAddress +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicLong +import java.util.logging.Level +import java.util.logging.Logger +import matter.controller.MatterController + +abstract class PairingCommand( + controller: MatterController, + commandName: String, + credsIssuer: CredentialsIssuer?, + private val pairingMode: PairingModeType = PairingModeType.NONE, + private val networkType: PairingNetworkType = PairingNetworkType.NONE, + private val filterType: DiscoveryFilterType = DiscoveryFilterType.NONE +) : MatterCommand(controller, credsIssuer, commandName), MatterController.CompletionListener { + private val remoteAddr: IPAddress = IPAddress(InetAddress.getByName("::1")) + private val nodeId = AtomicLong() + private val discoveryFilterCode = AtomicLong() + private val timeoutMillis = AtomicLong() + private val discoverOnce = AtomicBoolean() + private val useOnlyOnNetworkDiscovery = AtomicBoolean() + private val remotePort = AtomicInteger() + private val discriminator = AtomicInteger() + private val setupPINCode = AtomicLong() + private val operationalDataset = StringBuffer() + private val ssid = StringBuffer() + private val password = StringBuffer() + private val onboardingPayload = StringBuffer() + private val discoveryFilterInstanceName = StringBuffer() + + init { + addArgument("node-id", 0, Long.MAX_VALUE, nodeId, null, false) + + when (networkType) { + PairingNetworkType.NONE -> {} + PairingNetworkType.WIFI -> { + addArgument("ssid", ssid, null, false) + addArgument("password", password, null, false) + } + PairingNetworkType.THREAD -> + addArgument("operationalDataset", operationalDataset, null, false) + } + + when (pairingMode) { + PairingModeType.NONE -> {} + PairingModeType.CODE, + PairingModeType.CODE_PASE_ONLY -> { + addArgument("payload", onboardingPayload, null, false) + addArgument("discover-once", discoverOnce, null, true) + addArgument("use-only-onnetwork-discovery", useOnlyOnNetworkDiscovery, null, true) + } + PairingModeType.ADDRESS_PASE_ONLY -> { + addArgument("setup-pin-code", 0, 134217727, setupPINCode, null, false) + addArgument("device-remote-ip", remoteAddr, false) + addArgument("device-remote-port", 0.toShort(), Short.MAX_VALUE, remotePort, null, false) + } + PairingModeType.BLE -> { + addArgument("setup-pin-code", 0, 134217727, setupPINCode, null, false) + addArgument("discriminator", 0.toShort(), 4096.toShort(), discriminator, null, false) + } + PairingModeType.ON_NETWORK -> + addArgument("setup-pin-code", 0, 134217727, setupPINCode, null, false) + PairingModeType.SOFT_AP -> { + addArgument("setup-pin-code", 0, 134217727, setupPINCode, null, false) + addArgument("discriminator", 0.toShort(), 4096.toShort(), discriminator, null, false) + addArgument("device-remote-ip", remoteAddr, false) + addArgument("device-remote-port", 0.toShort(), Short.MAX_VALUE, remotePort, null, false) + } + PairingModeType.ALREADY_DISCOVERED -> { + addArgument("setup-pin-code", 0, 134217727, setupPINCode, null, false) + addArgument("device-remote-ip", remoteAddr, false) + addArgument("device-remote-port", 0.toShort(), Short.MAX_VALUE, remotePort, null, false) + } + } + + when (filterType) { + DiscoveryFilterType.NONE -> {} + DiscoveryFilterType.SHORT_DISCRIMINATOR, + DiscoveryFilterType.LONG_DISCRIMINATOR -> + addArgument("discriminator", 0.toShort(), 4096.toShort(), discriminator, null, false) + DiscoveryFilterType.VENDOR_ID -> + addArgument("vendor-id", 1.toShort(), Short.MAX_VALUE, discoveryFilterCode, null, false) + DiscoveryFilterType.COMPRESSED_FABRIC_ID -> + addArgument("fabric-id", 0L, Long.MAX_VALUE, discoveryFilterCode, null, false) + DiscoveryFilterType.COMMISSIONING_MODE, + DiscoveryFilterType.COMMISSIONER -> {} + DiscoveryFilterType.DEVICE_TYPE -> + addArgument("device-type", 0.toShort(), Short.MAX_VALUE, discoveryFilterCode, null, false) + DiscoveryFilterType.INSTANCE_NAME -> + addArgument("name", discoveryFilterInstanceName, null, false) + } + + addArgument("timeout", 0L, Long.MAX_VALUE, timeoutMillis, null, false) + } + + override fun onConnectDeviceComplete() { + logger.log(Level.INFO, "onConnectDeviceComplete") + } + + override fun onStatusUpdate(status: Int) { + logger.log(Level.INFO, "onStatusUpdate with status: $status") + } + + override fun onPairingComplete(errorCode: Int) { + logger.log(Level.INFO, "onPairingComplete with error code: $errorCode") + if (errorCode != 0) { + setFailure("onPairingComplete failure") + } + } + + override fun onPairingDeleted(errorCode: Int) { + logger.log(Level.INFO, "onPairingDeleted with error code: $errorCode") + } + + override fun onCommissioningComplete(nodeId: Long, errorCode: Int) { + logger.log(Level.INFO, "onCommissioningComplete with error code: $errorCode") + if (errorCode == 0) { + setSuccess() + } else { + setFailure("onCommissioningComplete failure") + } + } + + override fun onReadCommissioningInfo( + vendorId: Int, + productId: Int, + wifiEndpointId: Int, + threadEndpointId: Int + ) { + logger.log(Level.INFO, "onReadCommissioningInfo") + } + + override fun onCommissioningStatusUpdate(nodeId: Long, stage: String?, errorCode: Int) { + logger.log(Level.INFO, "onCommissioningStatusUpdate") + } + + override fun onNotifyChipConnectionClosed() { + logger.log(Level.INFO, "onNotifyChipConnectionClosed") + } + + override fun onError(error: Throwable) { + setFailure(error.toString()) + logger.log(Level.INFO, "onError with error: $error") + } + + override fun onOpCSRGenerationComplete(csr: ByteArray) { + logger.log(Level.INFO, "onOpCSRGenerationComplete") + for (i in csr.indices) { + print(csr[i].toString() + " ") + } + } + + fun getNodeId(): Long { + return nodeId.get() + } + + fun getRemoteAddr(): IPAddress { + return remoteAddr + } + + fun getRemotePort(): Int { + return remotePort.get() + } + + fun getSetupPINCode(): Long { + return setupPINCode.get() + } + + fun getDiscriminator(): Int { + return discriminator.get() + } + + fun getTimeoutMillis(): Long { + return timeoutMillis.get() + } + + fun getOnboardingPayload(): String { + return onboardingPayload.toString() + } + + private fun String.hexToByteArray(): ByteArray { + return chunked(2).map { byteStr -> byteStr.toUByte(16).toByte() }.toByteArray() + } + + fun getDiscoverOnce(): Boolean { + return discoverOnce.get() + } + + fun getUseOnlyOnNetworkDiscovery(): Boolean { + return useOnlyOnNetworkDiscovery.get() + } + + companion object { + private val logger = Logger.getLogger(PairingCommand::class.java.name) + } +} diff --git a/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairingModeType.kt b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairingModeType.kt new file mode 100644 index 00000000000000..590688da9feda5 --- /dev/null +++ b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairingModeType.kt @@ -0,0 +1,29 @@ +/* + * 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 com.matter.controller.commands.pairing + +enum class PairingModeType { + NONE, + CODE, + CODE_PASE_ONLY, + ADDRESS_PASE_ONLY, + BLE, + SOFT_AP, + ALREADY_DISCOVERED, + ON_NETWORK +} diff --git a/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairingNetworkType.kt b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairingNetworkType.kt new file mode 100644 index 00000000000000..4cde1fc97642d6 --- /dev/null +++ b/examples/kotlin-matter-controller/java/src/com/matter/controller/commands/pairing/PairingNetworkType.kt @@ -0,0 +1,24 @@ +/* + * 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 com.matter.controller.commands.pairing + +enum class PairingNetworkType { + NONE, + WIFI, + THREAD +} diff --git a/examples/kotlin-matter-controller/third_party/connectedhomeip b/examples/kotlin-matter-controller/third_party/connectedhomeip new file mode 120000 index 00000000000000..1b20c9fb816b63 --- /dev/null +++ b/examples/kotlin-matter-controller/third_party/connectedhomeip @@ -0,0 +1 @@ +../../../ \ No newline at end of file diff --git a/scripts/build/build/targets.py b/scripts/build/build/targets.py index c1b0fad61be8ff..17807371745ea6 100755 --- a/scripts/build/build/targets.py +++ b/scripts/build/build/targets.py @@ -111,6 +111,8 @@ def BuildHostTarget(): TargetPart('thermostat', app=HostApp.THERMOSTAT), TargetPart('java-matter-controller', app=HostApp.JAVA_MATTER_CONTROLLER), + TargetPart('kotlin-matter-controller', + app=HostApp.KOTLIN_MATTER_CONTROLLER), TargetPart('minmdns', app=HostApp.MIN_MDNS), TargetPart('light', app=HostApp.LIGHT), TargetPart('lock', app=HostApp.LOCK), @@ -332,6 +334,8 @@ def BuildAndroidTarget(): TargetPart('tv-casting-app', app=AndroidApp.TV_CASTING_APP), TargetPart('java-matter-controller', app=AndroidApp.JAVA_MATTER_CONTROLLER), + TargetPart('kotlin-matter-controller', + app=AndroidApp.KOTLIN_MATTER_CONTROLLER), TargetPart('virtual-device-app', app=AndroidApp.VIRTUAL_DEVICE_APP), ]) diff --git a/scripts/build/builders/android.py b/scripts/build/builders/android.py index a5b86e7ccd1388..14802fe5e83e80 100644 --- a/scripts/build/builders/android.py +++ b/scripts/build/builders/android.py @@ -71,6 +71,7 @@ class AndroidApp(Enum): TV_SERVER = auto() TV_CASTING_APP = auto() JAVA_MATTER_CONTROLLER = auto() + KOTLIN_MATTER_CONTROLLER = auto() VIRTUAL_DEVICE_APP = auto() def AppName(self): diff --git a/scripts/build/builders/host.py b/scripts/build/builders/host.py index 01a9e771136032..fcc159894bda29 100644 --- a/scripts/build/builders/host.py +++ b/scripts/build/builders/host.py @@ -67,6 +67,7 @@ class HostApp(Enum): TV_CASTING = auto() BRIDGE = auto() JAVA_MATTER_CONTROLLER = auto() + KOTLIN_MATTER_CONTROLLER = auto() CONTACT_SENSOR = auto() DISHWASHER = auto() REFRIGERATOR = auto() @@ -113,6 +114,8 @@ def ExamplePath(self): return 'bridge-app/linux' elif self == HostApp.JAVA_MATTER_CONTROLLER: return 'java-matter-controller' + elif self == HostApp.KOTLIN_MATTER_CONTROLLER: + return 'kotlin-matter-controller' elif self == HostApp.CONTACT_SENSOR: return 'contact-sensor-app/linux' elif self == HostApp.DISHWASHER: @@ -197,6 +200,9 @@ def OutputNames(self): elif self == HostApp.JAVA_MATTER_CONTROLLER: yield 'java-matter-controller' yield 'java-matter-controller.map' + elif self == HostApp.KOTLIN_MATTER_CONTROLLER: + yield 'kotlin-matter-controller' + yield 'kotlin-matter-controller.map' elif self == HostApp.CONTACT_SENSOR: yield 'contact-sensor-app' yield 'contact-sensor-app.map' @@ -451,6 +457,15 @@ def generate(self): ], title="Copying Manifest.txt to " + self.output_dir, ) + if exampleName == "kotlin-matter-controller": + self._Execute( + [ + "cp", + os.path.join(self.root, "Manifest.txt"), + self.output_dir, + ], + title="Copying Manifest.txt to " + self.output_dir, + ) if self.app == HostApp.TESTS and self.use_coverage: self.coverage_dir = os.path.join(self.output_dir, 'coverage') @@ -484,6 +499,9 @@ def PostBuildCommand(self): if self.app == HostApp.JAVA_MATTER_CONTROLLER: self.createJavaExecutable("java-matter-controller") + if self.app == HostApp.KOTLIN_MATTER_CONTROLLER: + self.createJavaExecutable("kotlin-matter-controller") + def build_outputs(self): outputs = {} diff --git a/scripts/build/testdata/all_targets_linux_x64.txt b/scripts/build/testdata/all_targets_linux_x64.txt index dd29ccecea337c..4c65d6c04db464 100644 --- a/scripts/build/testdata/all_targets_linux_x64.txt +++ b/scripts/build/testdata/all_targets_linux_x64.txt @@ -1,6 +1,6 @@ ameba-amebad-{all-clusters,all-clusters-minimal,light,light-switch,pigweed} asr-{asr582x,asr595x,asr550x}-{all-clusters,all-clusters-minimal,lighting,light-switch,lock,bridge,temperature-measurement,thermostat,ota-requestor,dishwasher,refrigerator}[-ota][-shell][-no_logging][-factory][-rotating_id][-rio] -android-{arm,arm64,x86,x64,androidstudio-arm,androidstudio-arm64,androidstudio-x86,androidstudio-x64}-{chip-tool,chip-test,tv-server,tv-casting-app,java-matter-controller,virtual-device-app}[-no-debug] +android-{arm,arm64,x86,x64,androidstudio-arm,androidstudio-arm64,androidstudio-x86,androidstudio-x64}-{chip-tool,chip-test,tv-server,tv-casting-app,java-matter-controller,kotlin-matter-controller,virtual-device-app}[-no-debug] bouffalolab-{bl602-iot-matter-v1,bl602-night-light,xt-zb6-devkit,bl706-night-light,bl706dk,bl704ldk}-light[-shell][-115200][-rpc][-cdc][-resetcnt][-rotating_device_id][-mfd][-mfdtest][-ethernet][-wifi][-thread][-fp][-memmonitor] cc32xx-lock ti-cc13x2x7_26x2x7-{lighting,lock,pump,pump-controller}[-mtd] @@ -10,7 +10,7 @@ efr32-{brd4161a,brd4187c,brd4186c,brd4163a,brd4164a,brd4166a,brd4170a,brd4186a,b esp32-{m5stack,c3devkit,devkitc,qemu}-{all-clusters,all-clusters-minimal,ota-provider,ota-requestor,shell,light,lock,bridge,temperature-measurement,ota-requestor,tests}[-rpc][-ipv6only][-tracing] genio-lighting-app linux-fake-tests[-mbedtls][-boringssl][-asan][-tsan][-ubsan][-libfuzzer][-ossfuzz][-coverage][-dmalloc][-clang] -linux-{x64,arm64}-{rpc-console,all-clusters,all-clusters-minimal,chip-tool,thermostat,java-matter-controller,minmdns,light,lock,shell,ota-provider,ota-requestor,simulated-app1,simulated-app2,python-bindings,tv-app,tv-casting-app,bridge,tests,chip-cert,address-resolve-tool,contact-sensor,dishwasher,refrigerator,rvc}[-nodeps][-platform-mdns][-minmdns-verbose][-libnl][-same-event-loop][-no-interactive][-ipv6only][-no-ble][-no-wifi][-no-thread][-mbedtls][-boringssl][-asan][-tsan][-ubsan][-libfuzzer][-ossfuzz][-coverage][-dmalloc][-clang][-test][-rpc][-with-ui] +linux-{x64,arm64}-{rpc-console,all-clusters,all-clusters-minimal,chip-tool,thermostat,java-matter-controller,kotlin-matter-controller,minmdns,light,lock,shell,ota-provider,ota-requestor,simulated-app1,simulated-app2,python-bindings,tv-app,tv-casting-app,bridge,tests,chip-cert,address-resolve-tool,contact-sensor,dishwasher,refrigerator,rvc}[-nodeps][-platform-mdns][-minmdns-verbose][-libnl][-same-event-loop][-no-interactive][-ipv6only][-no-ble][-no-wifi][-no-thread][-mbedtls][-boringssl][-asan][-tsan][-ubsan][-libfuzzer][-ossfuzz][-coverage][-dmalloc][-clang][-test][-rpc][-with-ui] linux-x64-efr32-test-runner[-clang] imx-{chip-tool,lighting-app,thermostat,all-clusters-app,all-clusters-minimal-app,ota-provider-app}[-release] infineon-psoc6-{lock,light,all-clusters,all-clusters-minimal}[-ota][-updateimage]