From 9158833de1ecfe4672b3593624778ee65c367cd8 Mon Sep 17 00:00:00 2001 From: Yufeng Wang Date: Thu, 5 Oct 2023 20:31:10 -0700 Subject: [PATCH] [Android]Initial implementation of Kotlin Matter Controller (#29574) * Initial implemenation of Kotlin Matter Controller * Rename to KotlinMatterController * Rename Interactions to InteractionClient * Address review comments * Restyled by gn --------- Co-authored-by: Restyled.io --- kotlin-detect-config.yaml | 2 + src/controller/java/BUILD.gn | 33 + .../controller/CompletionListenerAdapter.kt | 70 ++ .../src/matter/controller/ControllerParams.kt | 42 ++ .../matter/controller/InteractionClient.kt | 59 ++ .../src/matter/controller/MatterController.kt | 107 +++ .../controller/MatterControllerException.kt | 24 + .../matter/controller/MatterControllerImpl.kt | 612 ++++++++++++++++++ .../java/src/matter/controller/Messages.kt | 201 ++++++ .../matter/controller/OperationalKeyConfig.kt | 52 ++ .../java/src/matter/controller/model/Paths.kt | 59 ++ .../src/matter/controller/model/States.kt | 75 +++ 12 files changed, 1336 insertions(+) create mode 100644 src/controller/java/src/matter/controller/CompletionListenerAdapter.kt create mode 100644 src/controller/java/src/matter/controller/ControllerParams.kt create mode 100644 src/controller/java/src/matter/controller/InteractionClient.kt create mode 100644 src/controller/java/src/matter/controller/MatterController.kt create mode 100644 src/controller/java/src/matter/controller/MatterControllerException.kt create mode 100644 src/controller/java/src/matter/controller/MatterControllerImpl.kt create mode 100644 src/controller/java/src/matter/controller/Messages.kt create mode 100644 src/controller/java/src/matter/controller/OperationalKeyConfig.kt create mode 100644 src/controller/java/src/matter/controller/model/Paths.kt create mode 100644 src/controller/java/src/matter/controller/model/States.kt diff --git a/kotlin-detect-config.yaml b/kotlin-detect-config.yaml index 165bd85bec2917..d1e3b160e15974 100644 --- a/kotlin-detect-config.yaml +++ b/kotlin-detect-config.yaml @@ -289,6 +289,8 @@ complexity: - "**/src/controller/java/src/chip/onboardingpayload/OnboardingPayload.kt" - "**/src/controller/java/src/chip/tlv/TlvReader.kt" - "**/src/controller/java/src/chip/tlv/TlvWriter.kt" + - "**/src/controller/java/src/matter/controller/MatterControllerImpl.kt" + - "**/src/controller/java/src/matter/controller/CompletionListenerAdapter.kt" - "**/src/controller/java/tests/chip/jsontlv/JsonToTlvToJsonTest.kt" - "**/src/controller/java/tests/chip/onboardingpayload/ManualCodeTest.kt" - "**/src/controller/java/tests/chip/onboardingpayload/QRCodeTest.kt" diff --git a/src/controller/java/BUILD.gn b/src/controller/java/BUILD.gn index 0eb1bd9141a8ac..e7e4a949309a18 100644 --- a/src/controller/java/BUILD.gn +++ b/src/controller/java/BUILD.gn @@ -351,6 +351,39 @@ kotlin_library("chipcluster_test") { kotlinc_flags = [ "-Xlint:deprecation" ] } +kotlin_library("kotlin_matter_controller") { + output_name = "KotlinMatterController.jar" + + deps = [ + ":java", + "${chip_root}/third_party/java_deps:annotation", + ] + + sources = [ + "src/matter/controller/CompletionListenerAdapter.kt", + "src/matter/controller/ControllerParams.kt", + "src/matter/controller/InteractionClient.kt", + "src/matter/controller/MatterController.kt", + "src/matter/controller/MatterControllerException.kt", + "src/matter/controller/MatterControllerImpl.kt", + "src/matter/controller/Messages.kt", + "src/matter/controller/OperationalKeyConfig.kt", + "src/matter/controller/model/Paths.kt", + "src/matter/controller/model/States.kt", + ] + + if (matter_enable_java_compilation) { + deps += [ + "${chip_root}/third_party/java_deps:json", + "${chip_root}/third_party/java_deps:kotlin-stdlib", + "${chip_root}/third_party/java_deps:kotlinx-coroutines-core-jvm", + "${chip_root}/third_party/java_deps/stub_src", + ] + } else { + deps += [ ":android" ] + } +} + group("unit_tests") { deps = [ ":chipcluster_test", diff --git a/src/controller/java/src/matter/controller/CompletionListenerAdapter.kt b/src/controller/java/src/matter/controller/CompletionListenerAdapter.kt new file mode 100644 index 00000000000000..c1956154115d89 --- /dev/null +++ b/src/controller/java/src/matter/controller/CompletionListenerAdapter.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 matter.controller + +import chip.devicecontroller.ChipDeviceController +import java.util.logging.Level +import java.util.logging.Logger + +class CompletionListenerAdapter(val listener: MatterController.CompletionListener) : + chip.devicecontroller.ChipDeviceController.CompletionListener { + + override fun onConnectDeviceComplete() = listener.onConnectDeviceComplete() + + override fun onStatusUpdate(status: Int) = listener.onStatusUpdate(status) + + override fun onPairingComplete(errorCode: Int) = listener.onPairingComplete(errorCode) + + override fun onPairingDeleted(errorCode: Int) = listener.onPairingDeleted(errorCode) + + override fun onNotifyChipConnectionClosed() = listener.onNotifyChipConnectionClosed() + + override fun onCommissioningComplete(nodeId: Long, errorCode: Int) = + listener.onCommissioningComplete(nodeId, errorCode) + + override fun onCommissioningStatusUpdate(nodeId: Long, stage: String?, errorCode: Int) = + listener.onCommissioningStatusUpdate(nodeId, stage, errorCode) + + override fun onReadCommissioningInfo( + vendorId: Int, + productId: Int, + wifiEndpointId: Int, + threadEndpointId: Int + ) = listener.onReadCommissioningInfo(vendorId, productId, wifiEndpointId, threadEndpointId) + + override fun onOpCSRGenerationComplete(csr: ByteArray) = listener.onOpCSRGenerationComplete(csr) + + override fun onError(error: Throwable) = listener.onError(error) + + override fun onCloseBleComplete() { + logger.log(Level.INFO, "Not implemented, override the abstract function.") + } + + companion object { + private val logger = Logger.getLogger(MatterController::class.java.simpleName) + + fun from( + listener: MatterController.CompletionListener? + ): chip.devicecontroller.ChipDeviceController.CompletionListener? { + if (listener == null) { + return null + } + return CompletionListenerAdapter(listener) + } + } +} diff --git a/src/controller/java/src/matter/controller/ControllerParams.kt b/src/controller/java/src/matter/controller/ControllerParams.kt new file mode 100644 index 00000000000000..d0fb23222ea68e --- /dev/null +++ b/src/controller/java/src/matter/controller/ControllerParams.kt @@ -0,0 +1,42 @@ +/* + * 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 matter.controller + +/** + * Parameters representing initialization arguments for [MatterController]. + * + * @param operationalKeyConfig An optional signing configuration for operational control of devices. + * @param udpListenPort A port for UDP communications, or [UDP_PORT_AUTO] to select automatically. + * @param vendorId The identifier for the vendor using this controller. + * @param countryCode The Regulatory Location country code. + */ +class ControllerParams +@JvmOverloads +constructor( + val operationalKeyConfig: OperationalKeyConfig? = null, + val udpListenPort: Int = UDP_PORT_AUTO, + val vendorId: Int = VENDOR_ID_TEST, + val countryCode: String? = null, +) { + companion object { + /** Matter assigned vendor ID for Google. */ + const val VENDOR_ID_TEST = 0xFFF1 + /** Indicates that the UDP listen port should be chosen automatically. */ + const val UDP_PORT_AUTO = 0 + } +} diff --git a/src/controller/java/src/matter/controller/InteractionClient.kt b/src/controller/java/src/matter/controller/InteractionClient.kt new file mode 100644 index 00000000000000..71f86ff056228d --- /dev/null +++ b/src/controller/java/src/matter/controller/InteractionClient.kt @@ -0,0 +1,59 @@ +/* + * 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 matter.controller + +import kotlinx.coroutines.flow.Flow + +interface InteractionClient { + /** + * Subscribes to periodic updates of all attributes and events from a device. + * + * @param request The Subscribe command's path and arguments. + * @return A Flow of SubscriptionState representing the subscription's state updates. + */ + fun subscribe(request: SubscribeRequest): Flow + + /** + * Issues a read request to a target device for specified attributes and events. + * + * @param request Read command's path and arguments. + * @return A response to the read request. + * @throws Generic Exception if an error occurs during the read operation. + */ + suspend fun read(request: ReadRequest): ReadResponse + + /** + * Issues attribute write requests to a target device. + * + * @param writeRequests A list of attribute WriteRequest. + * @return A response to the write request. + * @throws Generic Exception or MatterControllerException if an error occurs during the write + * operation. + */ + suspend fun write(writeRequests: WriteRequests): WriteResponse + + /** + * Invokes a command on a target device. + * + * @param request Invoke command's path and arguments. + * @return A response to the invoke request. + * @throws Generic Exception or MatterControllerException if an error occurs during the invoke + * operation. + */ + suspend fun invoke(request: InvokeRequest): InvokeResponse +} diff --git a/src/controller/java/src/matter/controller/MatterController.kt b/src/controller/java/src/matter/controller/MatterController.kt new file mode 100644 index 00000000000000..5ac5a85020db98 --- /dev/null +++ b/src/controller/java/src/matter/controller/MatterController.kt @@ -0,0 +1,107 @@ +/* + * 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 matter.controller + +import java.io.Closeable + +/** Controller interface for interacting with a CHIP device. */ +interface MatterController : Closeable, InteractionClient { + /** Interface for listening to callbacks from the MatterController. */ + interface CompletionListener { + /** Notifies the completion of the "ConnectDevice" command. */ + fun onConnectDeviceComplete() + + /** Notifies the pairing status. */ + fun onStatusUpdate(status: Int) + + /** Notifies the completion of pairing. */ + fun onPairingComplete(errorCode: Int) + + /** Notifies the deletion of a pairing session. */ + fun onPairingDeleted(errorCode: Int) + + /** Notifies that the CHIP connection has been closed. */ + fun onNotifyChipConnectionClosed() + + /** Notifies the completion of commissioning. */ + fun onCommissioningComplete(nodeId: Long, errorCode: Int) + + /** Notifies the completion of reading commissioning information. */ + fun onReadCommissioningInfo( + vendorId: Int, + productId: Int, + wifiEndpointId: Int, + threadEndpointId: Int + ) + + /** Notifies the completion of each stage of commissioning. */ + fun onCommissioningStatusUpdate(nodeId: Long, stage: String?, errorCode: Int) + + /** Notifies the listener of an error. */ + fun onError(error: Throwable) + + /** Notifies the Commissioner when the OpCSR for the Comissionee is generated. */ + fun onOpCSRGenerationComplete(csr: ByteArray) + } + + /** + * Sets a completion listener for receiving controller events. + * + * @param listener The listener to set. + */ + fun setCompletionListener(listener: CompletionListener?) + + /** + * Commissions a device into a Matter fabric. + * + * @param nodeId The ID of the node to connect to. + * @param address The IP address at which the node is located. + * @param port The port at which the node is located. + * @param discriminator A 12-bit value used to discern between multiple commissionable Matter + * device advertisements. + * @param pinCode The pincode for this node. + */ + fun pairDevice( + nodeId: Long, + address: String, + port: Int, + discriminator: Int, + pinCode: Long, + ) + + /** + * Removes pairing for a paired device. If the device is currently being paired, it will stop the + * pairing process. + * + * @param nodeId The remote device ID. + */ + fun unpairDevice(nodeId: Long) + + /** + * Establishes a secure PASE connection to the given device via IP address. + * + * @param nodeId The ID of the node to connect to. + * @param address The IP address at which the node is located. + * @param port The port at which the node is located. + * @param setupPincode The pincode for this node. + */ + fun establishPaseConnection(nodeId: Long, address: String, port: Int, setupPincode: Long) + + /** Closes any active connections through this device controller. */ + override fun close() +} diff --git a/src/controller/java/src/matter/controller/MatterControllerException.kt b/src/controller/java/src/matter/controller/MatterControllerException.kt new file mode 100644 index 00000000000000..dfe97947b19dba --- /dev/null +++ b/src/controller/java/src/matter/controller/MatterControllerException.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 matter.controller + +class MatterControllerException(errorCode: Long, message: String? = null) : + RuntimeException(message ?: "Error Code $errorCode") { + + val errorCode: Long = errorCode +} diff --git a/src/controller/java/src/matter/controller/MatterControllerImpl.kt b/src/controller/java/src/matter/controller/MatterControllerImpl.kt new file mode 100644 index 00000000000000..5583b69f330636 --- /dev/null +++ b/src/controller/java/src/matter/controller/MatterControllerImpl.kt @@ -0,0 +1,612 @@ +/* + * 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 matter.controller + +import chip.devicecontroller.ChipDeviceController +import chip.devicecontroller.ChipDeviceControllerException +import chip.devicecontroller.GetConnectedDeviceCallbackJni.GetConnectedDeviceCallback +import chip.devicecontroller.InvokeCallback +import chip.devicecontroller.ReportCallback +import chip.devicecontroller.ResubscriptionAttemptCallback +import chip.devicecontroller.SubscriptionEstablishedCallback +import chip.devicecontroller.WriteAttributesCallback +import chip.devicecontroller.model.AttributeWriteRequest +import chip.devicecontroller.model.ChipAttributePath +import chip.devicecontroller.model.ChipEventPath +import chip.devicecontroller.model.ChipPathId +import chip.devicecontroller.model.InvokeElement +import java.util.logging.Level +import java.util.logging.Logger +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.onFailure +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.suspendCancellableCoroutine +import matter.controller.model.AttributePath +import matter.controller.model.AttributeState +import matter.controller.model.ClusterState +import matter.controller.model.EndpointState +import matter.controller.model.EventPath +import matter.controller.model.EventState +import matter.controller.model.NodeState + +/** Controller to interact with the CHIP device. */ +class MatterControllerImpl(params: ControllerParams) : MatterController { + private val deviceController: ChipDeviceController + private var nodeId: Long? = null + + override fun setCompletionListener(listener: MatterController.CompletionListener?) = + deviceController.setCompletionListener(CompletionListenerAdapter.from(listener)) + + override fun pairDevice( + nodeId: Long, + address: String, + port: Int, + discriminator: Int, + pinCode: Long, + ) { + this.nodeId = nodeId + deviceController.pairDeviceWithAddress(nodeId, address, port, discriminator, pinCode, null) + } + + override fun unpairDevice(nodeId: Long) { + deviceController.unpairDevice(nodeId) + this.nodeId = null + } + + override fun establishPaseConnection( + nodeId: Long, + address: String, + port: Int, + setupPincode: Long + ) { + deviceController.establishPaseConnection(nodeId, address, port, setupPincode) + } + + override fun subscribe(request: SubscribeRequest): Flow { + // To prevent potential issues related to concurrent modification, assign + // the value of the mutable property 'nodeId' to a temporary variable. + val nodeId = this.nodeId + check(nodeId != null) { "nodeId has not been initialized yet" } + + val attributePaths = generateAttributePaths(request) + val eventPaths = generateEventPaths(request) + val successes = mutableListOf() + val failures = mutableListOf() + + return callbackFlow { + val devicePtr: Long = getConnectedDevicePointer(nodeId) + val subscriptionEstablishedHandler = SubscriptionEstablishedCallback { + logger.log(Level.INFO, "Subscription to device established") + + trySendBlocking(SubscriptionState.SubscriptionEstablished).onFailure { ex -> + logger.log( + Level.SEVERE, + "Error sending SubscriptionCompletedNotification to subscriber: %s", + ex + ) + } + } + + val resubscriptionAttemptHandler = + ResubscriptionAttemptCallback { terminationCause, nextResubscribeIntervalMsec -> + logger.log( + Level.WARNING, + "ResubscriptionAttempt terminationCause:${terminationCause}, " + + "nextResubscribeIntervalMsec:${nextResubscribeIntervalMsec}" + ) + + trySendBlocking( + SubscriptionState.SubscriptionErrorNotification(terminationCause.toUInt()) + ) + .onFailure { ex -> + logger.log( + Level.SEVERE, + "Error sending ResubscriptionNotification to subscriber: %s", + ex + ) + } + } + + val reportHandler = + object : ReportCallback { + override fun onReport(nodeState: chip.devicecontroller.model.NodeState) { + logger.log(Level.INFO, "Received subscribe update report") + + val tmpNodeState: NodeState = nodeState.wrap() + + for (endpoint in tmpNodeState.endpoints) { + for (cluster in endpoint.value.clusters) { + for (attribute in cluster.value.attributes) { + val attributePath = + AttributePath( + endpointId = endpoint.key.toUShort(), + clusterId = cluster.key.toUInt(), + attributeId = attribute.key.toUInt() + ) + val readData = ReadData.Attribute(attributePath, attribute.value.tlvValue) + successes.add(readData) + } + + for (eventList in cluster.value.events) { + for (event in eventList.value) { + val timestamp: Timestamp = + when (event.timestampType) { + chip.devicecontroller.model.EventState.MILLIS_SINCE_BOOT -> + Timestamp.MillisSinceBoot(event.timestampValue) + chip.devicecontroller.model.EventState.MILLIS_SINCE_EPOCH -> + Timestamp.MillisSinceEpoch(event.timestampValue) + else -> { + logger.log(Level.SEVERE, "Unsupported event timestamp type - ignoring") + break + } + } + + val eventPath = + EventPath( + endpointId = endpoint.key.toUShort(), + clusterId = cluster.key.toUInt(), + eventId = eventList.key.toUInt() + ) + + val readData = + ReadData.Event( + path = eventPath, + eventNumber = event.eventNumber.toULong(), + priorityLevel = event.priorityLevel.toUByte(), + timeStamp = timestamp, + data = event.tlvValue + ) + successes.add(readData) + } + } + } + } + + trySendBlocking(SubscriptionState.NodeStateUpdate(ReadResponse(successes, failures))) + .onFailure { ex -> + logger.log(Level.SEVERE, "Error sending NodeStateUpdate to subscriber: %s", ex) + } + } + + override fun onError( + attributePath: ChipAttributePath?, + eventPath: ChipEventPath?, + ex: Exception + ) { + attributePath?.let { + logger.log(Level.INFO, "Report error for attributePath:%s", it.toString()) + val tmpAttributePath: AttributePath = attributePath.wrap() + val attributeFailure = ReadFailure.Attribute(path = tmpAttributePath, error = ex) + failures.add(attributeFailure) + } + eventPath?.let { + logger.log(Level.INFO, "Report error for eventPath:%s", it.toString()) + val tmpEventPath: EventPath = eventPath.wrap() + val eventFailure = ReadFailure.Event(path = tmpEventPath, error = ex) + failures.add(eventFailure) + } + + // The underlying subscription is terminated if both attributePath & eventPath are + // null + if (attributePath == null && eventPath == null) { + logger.log(Level.SEVERE, "The underlying subscription is terminated") + + trySendBlocking( + SubscriptionState.SubscriptionErrorNotification(CHIP_ERROR_UNEXPECTED_EVENT) + ) + .onFailure { exception -> + logger.log( + Level.SEVERE, + "Error sending SubscriptionErrorNotification to subscriber: %s", + exception + ) + } + } + } + + override fun onDone() { + logger.log(Level.INFO, "Subscription update completed") + } + } + + deviceController.subscribeToPath( + subscriptionEstablishedHandler, + resubscriptionAttemptHandler, + reportHandler, + devicePtr, + attributePaths, + eventPaths, + request.minInterval.seconds.toInt(), + request.maxInterval.seconds.toInt(), + request.keepSubscriptions, + request.fabricFiltered, + CHIP_IM_TIMEOUT_MS + ) + + awaitClose { logger.log(Level.FINE, "Closing flow") } + } + .buffer(capacity = UNLIMITED) + } + + override suspend fun read(request: ReadRequest): ReadResponse { + // To prevent potential issues related to concurrent modification, assign + // the value of the mutable property 'nodeId' to a temporary variable. + val nodeId = this.nodeId + check(nodeId != null) { "nodeId has not been initialized yet" } + + val devicePtr: Long = getConnectedDevicePointer(nodeId) + + val chipAttributePaths = + request.attributePaths.map { attributePath -> + val endpointId = attributePath.endpointId.toInt() + val clusterId = attributePath.clusterId.toLong() + val attributeId = attributePath.attributeId.toLong() + ChipAttributePath.newInstance(endpointId, clusterId, attributeId) + } + + val chipEventPaths = + request.eventPaths.map { eventPath -> + val endpointId = eventPath.endpointId.toInt() + val clusterId = eventPath.clusterId.toLong() + val eventId = eventPath.eventId.toLong() + ChipEventPath.newInstance(endpointId, clusterId, eventId) + } + + val successes = mutableListOf() + val failures = mutableListOf() + + return suspendCancellableCoroutine { continuation -> + val reportCallback = + object : ReportCallback { + override fun onReport(nodeState: chip.devicecontroller.model.NodeState) { + logger.log(Level.FINE, "Received read report") + + val tmpNodeState: NodeState = nodeState.wrap() + + for (endpoint in tmpNodeState.endpoints) { + for (cluster in endpoint.value.clusters) { + for (attribute in cluster.value.attributes) { + val attributePath = + AttributePath( + endpointId = endpoint.key.toUShort(), + clusterId = cluster.key.toUInt(), + attributeId = attribute.key.toUInt() + ) + val readData = ReadData.Attribute(attributePath, attribute.value.tlvValue) + successes.add(readData) + } + + for (eventList in cluster.value.events) { + for (event in eventList.value) { + val timestamp: Timestamp = + when (event.timestampType) { + chip.devicecontroller.model.EventState.MILLIS_SINCE_BOOT -> + Timestamp.MillisSinceBoot(event.timestampValue) + chip.devicecontroller.model.EventState.MILLIS_SINCE_EPOCH -> + Timestamp.MillisSinceEpoch(event.timestampValue) + else -> { + logger.log(Level.SEVERE, "Unsupported event timestamp type - ignoring") + break + } + } + val eventPath = + EventPath( + endpointId = endpoint.key.toUShort(), + clusterId = cluster.key.toUInt(), + eventId = eventList.key.toUInt() + ) + + val readData = + ReadData.Event( + path = eventPath, + eventNumber = event.eventNumber.toULong(), + priorityLevel = event.priorityLevel.toUByte(), + timeStamp = timestamp, + data = event.tlvValue + ) + successes.add(readData) + } + } + } + } + } + + override fun onError( + attributePath: ChipAttributePath?, + eventPath: ChipEventPath?, + ex: Exception + ) { + attributePath?.let { + logger.log(Level.INFO, "Report error for attributePath:%s", it.toString()) + val tmpAttributePath: AttributePath = attributePath.wrap() + val attributeFailure = ReadFailure.Attribute(path = tmpAttributePath, error = ex) + failures.add(attributeFailure) + } + eventPath?.let { + logger.log(Level.INFO, "Report error for eventPath:%s", it.toString()) + val tmpEventPath: EventPath = eventPath.wrap() + val eventFailure = ReadFailure.Event(path = tmpEventPath, error = ex) + failures.add(eventFailure) + } + + // The underlying subscription is terminated if both attributePath & eventPath are null + if (attributePath == null && eventPath == null) { + continuation.resumeWithException( + Exception("Read command failed with error ${ex.message}") + ) + } + } + + override fun onDone() { + logger.log(Level.FINE, "read command completed") + continuation.resume(ReadResponse(successes, failures)) + } + } + + deviceController.readPath( + reportCallback, + devicePtr, + chipAttributePaths, + chipEventPaths, + false, + CHIP_IM_TIMEOUT_MS, + ) + } + } + + override suspend fun write(writeRequests: WriteRequests): WriteResponse { + // To prevent potential issues related to concurrent modification, assign + // the value of the mutable property 'nodeId' to a temporary variable. + val nodeId = this.nodeId + check(nodeId != null) { "nodeId has not been initialized yet" } + + val devicePtr: Long = getConnectedDevicePointer(nodeId) + + val attributeWriteRequests = + writeRequests.requests.map { request -> + AttributeWriteRequest.newInstance( + ChipPathId.forId(request.attributePath.endpointId.toLong()), + ChipPathId.forId(request.attributePath.clusterId.toLong()), + ChipPathId.forId(request.attributePath.attributeId.toLong()), + request.tlvPayload + ) + } + + val failures = mutableListOf() + + return suspendCancellableCoroutine { continuation -> + val writeCallback = + object : WriteAttributesCallback { + override fun onResponse(attributePath: ChipAttributePath) { + logger.log(Level.INFO, "write success for attributePath:%s", attributePath.toString()) + } + + override fun onError(attributePath: ChipAttributePath?, ex: Exception) { + logger.log( + Level.SEVERE, + "Failed to write attribute at path: %s", + attributePath.toString() + ) + + if (attributePath == null) { + if (ex is ChipDeviceControllerException) { + continuation.resumeWithException( + MatterControllerException(ex.errorCode, ex.message) + ) + } else { + continuation.resumeWithException(ex) + } + } else { + failures.add(AttributeWriteError(attributePath.wrap(), ex)) + } + } + + override fun onDone() { + logger.log(Level.INFO, "writeAttributes onDone is received") + + if (failures.isNotEmpty()) { + continuation.resume(WriteResponse.PartialWriteFailure(failures)) + } else { + continuation.resume(WriteResponse.Success) + } + } + } + + deviceController.write( + writeCallback, + devicePtr, + attributeWriteRequests.toList(), + writeRequests.timedRequest?.toMillis()?.toInt() ?: 0, + CHIP_IM_TIMEOUT_MS, + ) + } + } + + override suspend fun invoke(request: InvokeRequest): InvokeResponse { + // To prevent potential issues related to concurrent modification, assign + // the value of the mutable property 'nodeId' to a temporary variable. + val nodeId = this.nodeId + check(nodeId != null) { "nodeId has not been initialized yet" } + + val devicePtr: Long = getConnectedDevicePointer(nodeId) + + val invokeRequest = + InvokeElement.newInstance( + ChipPathId.forId(request.commandPath.endpointId.toLong()), + ChipPathId.forId(request.commandPath.clusterId.toLong()), + ChipPathId.forId(request.commandPath.commandId.toLong()), + request.tlvPayload, + /* jsonString= */ null + ) + + return suspendCancellableCoroutine { continuation -> + var invokeCallback = + object : InvokeCallback { + override fun onResponse(invokeElement: InvokeElement?, successCode: Long) { + logger.log(Level.FINE, "Invoke onResponse is received") + val tlvByteArray = invokeElement?.getTlvByteArray() ?: byteArrayOf() + continuation.resume(InvokeResponse(tlvByteArray)) + } + + override fun onError(ex: Exception) { + if (ex is ChipDeviceControllerException) { + continuation.resumeWithException(MatterControllerException(ex.errorCode, ex.message)) + } else { + continuation.resumeWithException(ex) + } + } + } + + deviceController.invoke( + invokeCallback, + devicePtr, + invokeRequest, + request.timedRequest?.toMillis()?.toInt() ?: 0, + CHIP_IM_TIMEOUT_MS + ) + } + } + + override fun close() { + logger.log(Level.INFO, "MatterController is closed") + deviceController.shutdownCommissioning() + } + + private suspend fun getConnectedDevicePointer(nodeId: Long): Long { + return suspendCancellableCoroutine { cont -> + logger.log(Level.INFO, "Looking up pointer for %016X", nodeId) + deviceController.getConnectedDevicePointer( + nodeId, + object : GetConnectedDeviceCallback { + override fun onDeviceConnected(devicePointer: Long) { + logger.log(Level.INFO, "Resolved pointer ${devicePointer} for device ${nodeId}") + cont.resume(devicePointer) + } + + override fun onConnectionFailure(nodeId: Long, error: Exception) { + logger.log(Level.SEVERE, "Failed to establish CASE session for device ${nodeId}") + cont.resumeWithException( + Exception("Failed to establish CASE session for device %016X".format(nodeId)) + ) + } + } + ) + } + } + + private fun generateAttributePaths(request: SubscribeRequest): List { + return request.attributePaths.map { attributePath -> + ChipAttributePath.newInstance( + attributePath.endpointId.toInt(), + attributePath.clusterId.toLong(), + attributePath.attributeId.toLong() + ) + } + } + + private fun generateEventPaths(request: SubscribeRequest): List { + return request.eventPaths.map { eventPath -> + ChipEventPath.newInstance( + eventPath.endpointId.toInt(), + eventPath.clusterId.toLong(), + eventPath.eventId.toLong(), + false + ) + } + } + + private fun ChipAttributePath.wrap(): AttributePath { + return AttributePath( + endpointId.getId().toUShort(), + clusterId.getId().toUInt(), + attributeId.getId().toUInt() + ) + } + + private fun ChipEventPath.wrap(): EventPath { + return EventPath( + endpointId.getId().toUShort(), + clusterId.getId().toUInt(), + eventId.getId().toUInt() + ) + } + + private fun chip.devicecontroller.model.NodeState.wrap(): NodeState { + return NodeState( + endpoints = endpointStates.mapValues { (id, value) -> value.wrap(id) }, + ) + } + + private fun chip.devicecontroller.model.EndpointState.wrap(id: Int): EndpointState { + return EndpointState(id, clusterStates.mapValues { (id, value) -> value.wrap(id) }) + } + + private fun chip.devicecontroller.model.ClusterState.wrap(id: Long): ClusterState { + return ClusterState( + id, + attributeStates.mapValues { (id, value) -> value.wrap(id) }, + eventStates.mapValues { (id, value) -> value.map { eventState -> eventState.wrap(id) } } + ) + } + + private fun chip.devicecontroller.model.AttributeState.wrap(id: Long): AttributeState { + return AttributeState(id, tlv, json.toString()) + } + + private fun chip.devicecontroller.model.EventState.wrap(id: Long): EventState { + return EventState(id, eventNumber, priorityLevel, timestampType, timestampValue, tlv) + } + + init { + val config: OperationalKeyConfig? = params.operationalKeyConfig + val paramsBuilder = + chip.devicecontroller.ControllerParams.newBuilder() + .setUdpListenPort(params.udpListenPort) + .setControllerVendorId(params.vendorId) + .setCountryCode(params.countryCode) + + if (config != null) { + val intermediateCertificate = config.certificateData.intermediateCertificate + paramsBuilder + .setRootCertificate(config.certificateData.trustedRootCertificate) + .setIntermediateCertificate(intermediateCertificate ?: byteArrayOf()) + .setOperationalCertificate(config.certificateData.operationalCertificate) + .setKeypairDelegate(config.keypairDelegate) + .setIpk(config.ipk) + } + + deviceController = ChipDeviceController(paramsBuilder.build()) + } + + companion object { + private val logger = Logger.getLogger(MatterController::class.java.simpleName) + + // im interaction time out value, it would override the default value in c++ im + // layer if this value is non-zero. + private const val CHIP_IM_TIMEOUT_MS = 0 + + // CHIP error values, lift from ChipError.h in the Matter SDK. + private const val CHIP_ERROR_UNEXPECTED_EVENT: UInt = 0xc0u + } +} diff --git a/src/controller/java/src/matter/controller/Messages.kt b/src/controller/java/src/matter/controller/Messages.kt new file mode 100644 index 00000000000000..36f3fd6fc3ad2c --- /dev/null +++ b/src/controller/java/src/matter/controller/Messages.kt @@ -0,0 +1,201 @@ +/* + * 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 matter.controller + +import java.time.Duration +import matter.controller.model.AttributePath +import matter.controller.model.CommandPath +import matter.controller.model.EventPath + +private const val DEFAULT_SUBSCRIPTION_MIN_INTERVAL_S: Long = 0L +private const val DEFAULT_SUBSCRIPTION_MAX_INTERVAL_S: Long = 30L + +/** + * Representation of Timestamp type. + * + * This sealed class represents timestamps in two different formats: + * - [MillisSinceBoot]: System Time in milliseconds since boot. + * - [MillisSinceEpoch]: POSIX Time in milliseconds from the UNIX epoch (1970-01-01 00:00:00 UTC). + * + * @param value The time offset in milliseconds. + */ +sealed class Timestamp { + data class MillisSinceBoot(val value: Long) : Timestamp() + + data class MillisSinceEpoch(val value: Long) : Timestamp() +} + +/** + * Information about a read request element. + * + * @param eventPaths A list of event path information in the read request. + * @param attributePaths A list of attribute path information in the read request. + */ +class ReadRequest( + val eventPaths: List, + val attributePaths: List, +) + +/** Represents data received from a read operation. */ +sealed class ReadData { + /** + * Represents data related to an event. + * + * @param path The event path associated with the data. + * @param eventNumber the event number value that is scoped to the node + * @param priorityLevel the priority describes the usage semantics of the event + * @param timeStamp the timestamp at the time the event was created + * @param data The ByteArray containing the data in TLV format. + */ + class Event( + val path: EventPath, + val eventNumber: ULong, + val priorityLevel: UByte, + val timeStamp: Timestamp, + val data: ByteArray + ) : ReadData() + + /** + * Represents data related to an attribute. + * + * @param path The attribute path associated with the data. + * @param data The ByteArray containing the data in TLV format. + */ + class Attribute(val path: AttributePath, val data: ByteArray) : ReadData() +} + +/** Represents a failure that can occur during a read operation. */ +sealed class ReadFailure { + /** + * Represents a failure related to an event path. + * + * @param path The event path information associated with the failure. + * @param error The exception that describes the failure. + */ + class Event(val path: EventPath, val error: Exception) : ReadFailure() + + /** + * Represents a failure related to an attribute path. + * + * @param path The attribute path information associated with the failure. + * @param error The exception that describes the failure. + */ + class Attribute(val path: AttributePath, val error: Exception) : ReadFailure() +} + +/** + * Represents the response from a read operation, containing both successes and failures. + * + * @param successes A list of successfully read data elements. + * @param failures A list of failures that occurred during the read operation. + */ +class ReadResponse(val successes: List, val failures: List) + +/** + * Information about a subscribe request element. + * + * @param eventPaths A list of event path information in the read request. + * @param attributePaths A list of attribute path information in the read request. + * @param minInterval The minimum interval boundary floor in seconds. + * @param maxInterval The maximum interval boundary ceiling in seconds. + * @param keepSubscriptions Indicates whether to keep existing subscriptions. + * @param fabricFiltered Limits the data read within fabric-scoped lists to the accessing fabric. + */ +class SubscribeRequest( + val eventPaths: List, + val attributePaths: List, + val minInterval: Duration = Duration.ofSeconds(DEFAULT_SUBSCRIPTION_MIN_INTERVAL_S), + val maxInterval: Duration = Duration.ofSeconds(DEFAULT_SUBSCRIPTION_MAX_INTERVAL_S), + val keepSubscriptions: Boolean = true, + val fabricFiltered: Boolean = true +) + +/** An interface representing the possible states of a subscription. */ +sealed class SubscriptionState { + /** + * Represents an error notification in the subscription. + * + * @param terminationCause The cause of the subscription termination. + */ + class SubscriptionErrorNotification(val terminationCause: UInt) : SubscriptionState() + + /** + * Represents an update in the state of a subscribed node. + * + * @param updateState The state update received from the subscribed node. + */ + class NodeStateUpdate(val updateState: ReadResponse) : SubscriptionState() + + /** Represents the state where the subscription has been successfully established. */ + object SubscriptionEstablished : SubscriptionState() +} + +/** + * A write request representation. + * + * @param attributePath The attribute path information in the write request. + * @param tlvPayload The ByteArray representation of the TLV payload. + */ +class WriteRequest( + val attributePath: AttributePath, + val tlvPayload: ByteArray, +) + +/** + * Information about a collection of write request elements. + * + * @param requests A list of write request elements. + * @param timedRequest If set, indicates that this is a timed request with the specified duration. + */ +class WriteRequests(val requests: List, val timedRequest: Duration?) + +/** + * Information about a write attribute error. + * + * @param attributePath The attribute path field in write response. + * @param ex The IllegalStateException which encapsulated the error message. + */ +class AttributeWriteError(val attributePath: AttributePath, val ex: Exception) + +/** An interface representing the possible write responses. */ +sealed interface WriteResponse { + object Success : WriteResponse + + class PartialWriteFailure(val failures: List) : WriteResponse +} + +/** + * Information about a invoke request element. + * + * @param commandPath Invoked command's path information. + * @param tlvPayload The ByteArray representation of the TLV payload. + * @param timedRequest If set, indicates that this is a timed request with the specified duration. + */ +class InvokeRequest( + val commandPath: CommandPath, + val tlvPayload: ByteArray, + val timedRequest: Duration? +) + +/** + * InvokeResponse will be received when a invoke response has been successful received and + * processed. + * + * @param payload An invoke response that could contain tlv data or empty. + */ +class InvokeResponse(val payload: ByteArray) diff --git a/src/controller/java/src/matter/controller/OperationalKeyConfig.kt b/src/controller/java/src/matter/controller/OperationalKeyConfig.kt new file mode 100644 index 00000000000000..c8e4404906e527 --- /dev/null +++ b/src/controller/java/src/matter/controller/OperationalKeyConfig.kt @@ -0,0 +1,52 @@ +/* + * 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 matter.controller + +import chip.devicecontroller.KeypairDelegate + +/** + * CertificateData Configuration data for X.509 certificates. + * + * @property trustedRootCertificate The trusted root X.509 certificate in DER-encoded form. + * @property intermediateCertificate The optional intermediate X.509 certificate in DER-encoded + * form. + * @property operationalCertificate The node operational X.509 certificate in DER-encoded form. + */ +class CertificateData( + val trustedRootCertificate: ByteArray, + val intermediateCertificate: ByteArray?, + val operationalCertificate: ByteArray +) + +/** + * Configuration for use with CASE (Chip Authentication Session Establishment) session + * establishment. + * + * @property keypairDelegate A delegate for signing operations. + * @property certificateData Configuration data for X.509 certificates. + * @property ipk The Identity Protection Key. + * @property fabricId The fabric ID to which these operational credentials are associated. + * @property nodeId The Admin Node ID to which these operational credentials are associated. + */ +class OperationalKeyConfig( + val keypairDelegate: KeypairDelegate, + val certificateData: CertificateData, + val ipk: ByteArray, + val fabricId: Long, + val nodeId: Long +) diff --git a/src/controller/java/src/matter/controller/model/Paths.kt b/src/controller/java/src/matter/controller/model/Paths.kt new file mode 100644 index 00000000000000..76e26db62f0e1e --- /dev/null +++ b/src/controller/java/src/matter/controller/model/Paths.kt @@ -0,0 +1,59 @@ +/* + * 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 matter.controller.model + +/** + * Represents a full path for reading an attribute from a node. + * + * @param endpointId The UShort representing the endpoint to read from. + * @param clusterId The UInt representing the cluster on the endpoint to read from. + * @param attributeId The UInt representing the attribute(s) on the cluster to read. + */ +data class AttributePath(val endpointId: UShort, val clusterId: UInt, val attributeId: UInt) { + override fun toString(): String = "$endpointId/$clusterId/$attributeId" +} + +/** + * Represents a full path to an event emitted from a node. + * + * @param endpointId The UShort representing the endpoint to read from. + * @param clusterId The UInt representing the cluster on the endpoint to read from. + * @param eventId The UInt representing the event(s) from the cluster. + */ +data class EventPath( + val endpointId: UShort, + val clusterId: UInt, + val eventId: UInt, +) { + override fun toString(): String = "$endpointId/$clusterId/$eventId" +} + +/** + * Represents a full path to a command sent to a node. + * + * @param endpointId The UShort representing the endpoint to read from. + * @param clusterId The UInt representing the cluster on the endpoint to read from. + * @param commandId The UInt representing the command(s) from the cluster. + */ +data class CommandPath( + val endpointId: UShort, + val clusterId: UInt, + val commandId: UInt, +) { + override fun toString(): String = "$endpointId/$clusterId/$commandId" +} diff --git a/src/controller/java/src/matter/controller/model/States.kt b/src/controller/java/src/matter/controller/model/States.kt new file mode 100644 index 00000000000000..39899e7ce735f8 --- /dev/null +++ b/src/controller/java/src/matter/controller/model/States.kt @@ -0,0 +1,75 @@ +/* + * 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 matter.controller.model + +/** + * Represents information about a node, including data on all available endpoints. + * + * @param endpoints A mapping of endpoint IDs with the associated cluster data. + */ +data class NodeState(val endpoints: Map) + +/** + * Represents information about an endpoint and its cluster data. + * + * @param id The endpoint ID. + * @param clusters A mapping of cluster IDs to the cluster data. + */ +data class EndpointState(val id: Int, val clusters: Map) + +/** + * Represents information about a cluster. + * + * @param id The cluster ID. + * @param attributes A mapping of attribute IDs in this cluster with their respective values. + * @param events A mapping of event IDs to lists of events that occurred on the node under this + * cluster. + */ +data class ClusterState( + val id: Long, + val attributes: Map, + val events: Map> +) + +/** + * Represents information about an attribute. + * + * @param id The attribute ID. + * @param tlvValue The raw TLV-encoded attribute value. + * @param jsonValue A JSON string representing the raw attribute value. + */ +data class AttributeState(val id: Long, val tlvValue: ByteArray, val jsonValue: String) + +/** + * Represents information about an event. + * + * @param eventId The event ID. + * @param eventNumber The event number value that is scoped to the node. + * @param priorityLevel The priority level describing the usage semantics of the event. + * @param timestampType Indicates POSIX Time or SYSTEM Time, in milliseconds. + * @param timestampValue Represents an offset, in milliseconds since the UNIX epoch or boot. + * @param tlvValue The raw TLV-encoded event value. + */ +data class EventState( + val eventId: Long, + val eventNumber: Long, + val priorityLevel: Int, + val timestampType: Int, + val timestampValue: Long, + val tlvValue: ByteArray +)