From 87952207af4430724684fa6b3b242c4a84fe333b Mon Sep 17 00:00:00 2001 From: Song GUO Date: Mon, 1 Nov 2021 23:27:46 +0800 Subject: [PATCH] [Python] Add write and read API with cluster object support (#10949) --- src/app/WriteClient.h | 1 + src/controller/python/BUILD.gn | 2 + src/controller/python/chip/ChipDeviceCtrl.py | 51 +++++- .../python/chip/clusters/Attribute.py | 160 ++++++++++++++++++ .../python/chip/clusters/ClusterObjects.py | 5 + .../python/chip/clusters/__init__.py | 2 + src/controller/python/chip/clusters/write.cpp | 146 ++++++++++++++++ .../python/chip/interaction_model/__init__.py | 2 +- .../python/test/test_scripts/base.py | 2 +- .../test/test_scripts/cluster_objects.py | 71 ++++++++ 10 files changed, 439 insertions(+), 3 deletions(-) create mode 100644 src/controller/python/chip/clusters/Attribute.py create mode 100644 src/controller/python/chip/clusters/write.cpp diff --git a/src/app/WriteClient.h b/src/app/WriteClient.h index fa85904ac50606..13435aa538d0bc 100644 --- a/src/app/WriteClient.h +++ b/src/app/WriteClient.h @@ -35,6 +35,7 @@ #include #include #include +#include namespace chip { namespace app { diff --git a/src/controller/python/BUILD.gn b/src/controller/python/BUILD.gn index b11909cac2e87a..06bea6a49b5086 100644 --- a/src/controller/python/BUILD.gn +++ b/src/controller/python/BUILD.gn @@ -46,6 +46,7 @@ shared_library("ChipDeviceCtrl") { "ChipDeviceController-StorageDelegate.h", "chip/clusters/CHIPClusters.cpp", "chip/clusters/command.cpp", + "chip/clusters/write.cpp", "chip/discovery/NodeResolution.cpp", "chip/interaction_model/Delegate.cpp", "chip/interaction_model/Delegate.h", @@ -111,6 +112,7 @@ pw_python_action("python") { "chip/ble/library_handle.py", "chip/ble/scan_devices.py", "chip/ble/types.py", + "chip/clusters/Attribute.py", "chip/clusters/CHIPClusters.py", "chip/clusters/ClusterObjects.py", "chip/clusters/Command.py", diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py index 62717f1b71cd32..3eee5650981f72 100644 --- a/src/controller/python/chip/ChipDeviceCtrl.py +++ b/src/controller/python/chip/ChipDeviceCtrl.py @@ -30,13 +30,15 @@ import asyncio from ctypes import * from .ChipStack import * -from .clusters.CHIPClusters import * from .interaction_model import delegate as im from .exceptions import * from .clusters import Command as ClusterCommand +from .clusters import Attribute as ClusterAttribute from .clusters import ClusterObjects as ClusterObjects +from .clusters.CHIPClusters import * import enum import threading +import typing __all__ = ["ChipDeviceController"] @@ -134,6 +136,7 @@ def HandleCommissioningComplete(nodeid, err): im.InitIMDelegate() ClusterCommand.Init(self) + ClusterAttribute.Init() self.cbHandleKeyExchangeCompleteFunct = _DevicePairingDelegate_OnPairingCompleteFunct( HandleKeyExchangeComplete) @@ -362,6 +365,52 @@ async def SendCommand(self, nodeid: int, endpoint: int, payload: ClusterObjects. future.set_exception(self._ChipStack.ErrorToException(res)) return await future + def WriteAttribute(self, nodeid: int, attributes): + eventLoop = asyncio.get_running_loop() + future = eventLoop.create_future() + + device = self.GetConnectedDeviceSync(nodeid) + res = self._ChipStack.Call( + lambda: ClusterAttribute.WriteAttributes( + future, eventLoop, device, attributes) + ) + if res != 0: + raise self._ChipStack.ErrorToException(res) + return future + + def ReadAttribute(self, nodeid: int, attributes: typing.List[typing.Tuple[int, ClusterAttribute.AttributeReadRequest]]): + eventLoop = asyncio.get_running_loop() + future = eventLoop.create_future() + + device = self.GetConnectedDeviceSync(nodeid) + # TODO: Here, we translates multi attribute read into many individual attribute reads, this should be fixed by implementing Python's attribute read API. + res = [] + for attr in attributes: + endpointId = attr[0] + attribute = attr[1] + clusterInfo = self._Cluster.GetClusterInfoById( + attribute.cluster_id) + if not clusterInfo: + raise UnknownCluster(attribute.cluster_id) + attributeInfo = clusterInfo.get("attributes", {}).get( + attribute.attribute_id, None) + if not attributeInfo: + raise UnknownAttribute( + clusterInfo["clusterName"], attribute.attribute_id) + self._Cluster.ReadAttribute( + device, clusterInfo["clusterName"], attributeInfo["attributeName"], endpointId, 0, False) + readRes = im.GetAttributeReadResponse( + im.DEFAULT_ATTRIBUTEREAD_APPID) + res.append(ClusterAttribute.AttributeReadResult( + Path=ClusterAttribute.AttributePath( + EndpointId=endpointId, ClusterId=attribute.cluster_id, AttributeId=attribute.attribute_id), + Status=readRes.status, + Data=(attribute.FromTagDictOrRawValue( + readRes.value) if readRes.value is not None else None), + )) + future.set_result(res) + return future + def ZCLSend(self, cluster, command, nodeid, endpoint, groupid, args, blocking=False): device = self.GetConnectedDeviceSync(nodeid) diff --git a/src/controller/python/chip/clusters/Attribute.py b/src/controller/python/chip/clusters/Attribute.py new file mode 100644 index 00000000000000..c33d0836bbf5cf --- /dev/null +++ b/src/controller/python/chip/clusters/Attribute.py @@ -0,0 +1,160 @@ +# +# Copyright (c) 2021 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. +# + +from asyncio.futures import Future +import ctypes +from dataclasses import dataclass +from typing import Type, Union, List, Any +from ctypes import CFUNCTYPE, c_char_p, c_size_t, c_void_p, c_uint32, c_uint16, py_object + +from .ClusterObjects import ClusterAttributeDescriptor +import chip.exceptions +import chip.interaction_model + + +@dataclass +class AttributePath: + EndpointId: int + ClusterId: int + AttributeId: int + + +@dataclass +class AttributeStatus: + Path: AttributePath + Status: Union[chip.interaction_model.Status, int] + + +AttributeWriteResult = AttributeStatus + + +@dataclass +class AttributeDescriptorWithEndpoint: + EndpointId: int + Attribute: ClusterAttributeDescriptor + + +@dataclass +class AttributeWriteRequest(AttributeDescriptorWithEndpoint): + Data: Any + + +AttributeReadRequest = AttributeDescriptorWithEndpoint + + +@dataclass +class AttributeReadResult(AttributeStatus): + Data: Any = None + + +class AsyncWriteTransaction: + def __init__(self, future: Future, eventLoop): + self._event_loop = eventLoop + self._future = future + self._res = [] + + def _handleResponse(self, path: AttributePath, status: int): + try: + imStatus = chip.interaction_model.Status(status) + self._res.append(AttributeWriteResult(Path=path, Status=imStatus)) + except: + self._res.append(AttributeWriteResult(Path=path, Status=status)) + + def handleResponse(self, path: AttributePath, status: int): + self._event_loop.call_soon_threadsafe( + self._handleResponse, path, status) + + def _handleError(self, chipError: int): + self._future.set_exception( + chip.exceptions.ChipStackError(chipError)) + + def handleError(self, chipError: int): + self._event_loop.call_soon_threadsafe( + self._handleError, chipError + ) + + def _handleDone(self): + if not self._future.done(): + self._future.set_result(self._res) + + def handleDone(self): + self._event_loop.call_soon_threadsafe(self._handleDone) + + +_OnWriteResponseCallbackFunct = CFUNCTYPE( + None, py_object, c_uint16, c_uint32, c_uint32, c_uint16) +_OnWriteErrorCallbackFunct = CFUNCTYPE( + None, py_object, c_uint32) +_OnWriteDoneCallbackFunct = CFUNCTYPE( + None, py_object) + + +@_OnWriteResponseCallbackFunct +def _OnWriteResponseCallback(closure, endpoint: int, cluster: int, attribute: int, status): + closure.handleResponse(AttributePath(endpoint, cluster, attribute), status) + + +@_OnWriteErrorCallbackFunct +def _OnWriteErrorCallback(closure, chiperror: int): + closure.handleError(chiperror) + + +@_OnWriteDoneCallbackFunct +def _OnWriteDoneCallback(closure): + closure.handleDone() + ctypes.pythonapi.Py_DecRef(ctypes.py_object(closure)) + + +def WriteAttributes(future: Future, eventLoop, device, attributes: List[AttributeWriteRequest]) -> int: + handle = chip.native.GetLibraryHandle() + transaction = AsyncWriteTransaction(future, eventLoop) + + writeargs = [] + for attr in attributes: + path = chip.interaction_model.AttributePathStruct.parse( + b'\x00' * chip.interaction_model.AttributePathStruct.sizeof()) + path.EndpointId = attr.EndpointId + path.ClusterId = attr.Attribute.cluster_id + path.AttributeId = attr.Attribute.attribute_id + path = chip.interaction_model.AttributePathStruct.build(path) + tlv = attr.Attribute.ToTLV(None, attr.Data) + writeargs.append(ctypes.c_char_p(path)) + writeargs.append(ctypes.c_char_p(bytes(tlv))) + writeargs.append(ctypes.c_int(len(tlv))) + + ctypes.pythonapi.Py_IncRef(ctypes.py_object(transaction)) + res = handle.pychip_WriteClient_WriteAttributes( + ctypes.py_object(transaction), device, ctypes.c_size_t(len(attributes)), *writeargs) + if res != 0: + ctypes.pythonapi.Py_DecRef(ctypes.py_object(transaction)) + return res + + +def Init(): + handle = chip.native.GetLibraryHandle() + + # Uses one of the type decorators as an indicator for everything being + # initialized. + if not handle.pychip_WriteClient_InitCallbacks.argtypes: + setter = chip.native.NativeLibraryHandleMethodArguments(handle) + + handle.pychip_WriteClient_WriteAttributes.restype = c_uint32 + setter.Set('pychip_WriteClient_InitCallbacks', None, [ + _OnWriteResponseCallbackFunct, _OnWriteErrorCallbackFunct, _OnWriteDoneCallbackFunct]) + + handle.pychip_WriteClient_InitCallbacks( + _OnWriteResponseCallback, _OnWriteErrorCallback, _OnWriteDoneCallback) diff --git a/src/controller/python/chip/clusters/ClusterObjects.py b/src/controller/python/chip/clusters/ClusterObjects.py index e7b637ece32143..e441a9e1efdf38 100644 --- a/src/controller/python/chip/clusters/ClusterObjects.py +++ b/src/controller/python/chip/clusters/ClusterObjects.py @@ -170,6 +170,11 @@ def FromTLV(cls, tlvBuffer: bytes): obj_class = cls._cluster_object return obj_class.FromDict(obj_class.descriptor.TagDictToLabelDict('', {0: tlv.TLVReader(tlvBuffer).get().get('Any', {})})).Value + @classmethod + def FromTagDictOrRawValue(cls, val: Any): + obj_class = cls._cluster_object + return obj_class.FromDict(obj_class.descriptor.TagDictToLabelDict('', {0: val})).Value + @ChipUtility.classproperty def cluster_id(self) -> int: raise NotImplementedError() diff --git a/src/controller/python/chip/clusters/__init__.py b/src/controller/python/chip/clusters/__init__.py index 8d9ee4334e4129..dee5aa9d605a23 100644 --- a/src/controller/python/chip/clusters/__init__.py +++ b/src/controller/python/chip/clusters/__init__.py @@ -22,4 +22,6 @@ """Provides Python APIs for CHIP.""" from . import Command +from . import Attribute from .Objects import * +from . import CHIPClusters diff --git a/src/controller/python/chip/clusters/write.cpp b/src/controller/python/chip/clusters/write.cpp new file mode 100644 index 00000000000000..f5d6b52517dbdd --- /dev/null +++ b/src/controller/python/chip/clusters/write.cpp @@ -0,0 +1,146 @@ +/* + * + * Copyright (c) 2021 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. + */ + +#include +#include +#include + +#include +#include +#include + +#include +#include + +using namespace chip; +using namespace chip::app; + +using PyObject = void *; + +extern "C" { +// Encodes n attribute write requests, follows 3 * n arguments, in the (AttributeWritePath*=void *, uint8_t*, size_t) order. +chip::ChipError::StorageType pychip_WriteClient_WriteAttributes(void * appContext, Controller::Device * device, size_t n, ...); +} + +namespace chip { +namespace python { + +struct __attribute__((packed)) AttributeWritePath +{ + chip::EndpointId endpointId; + chip::ClusterId clusterId; + chip::AttributeId attributeId; +}; + +using OnWriteResponseCallback = void (*)(PyObject appContext, chip::EndpointId endpointId, chip::ClusterId clusterId, + chip::AttributeId attributeId, + std::underlying_type_t imstatus); +using OnWriteErrorCallback = void (*)(PyObject appContext, uint32_t chiperror); +using OnWriteDoneCallback = void (*)(PyObject appContext); + +OnWriteResponseCallback gOnWriteResponseCallback = nullptr; +OnWriteErrorCallback gOnWriteErrorCallback = nullptr; +OnWriteDoneCallback gOnWriteDoneCallback = nullptr; + +class WriteClientCallback : public WriteClient::Callback +{ +public: + WriteClientCallback(PyObject appContext) : mAppContext(appContext) {} + + void OnResponse(const WriteClient * apWriteClient, const ConcreteAttributePath & aPath, app::StatusIB aStatus) override + { + gOnWriteResponseCallback(mAppContext, aPath.mEndpointId, aPath.mClusterId, aPath.mAttributeId, + to_underlying(aStatus.mStatus)); + } + + void OnError(const WriteClient * apWriteClient, CHIP_ERROR aProtocolError) override + { + gOnWriteErrorCallback(mAppContext, aProtocolError.AsInteger()); + } + + void OnDone(WriteClient * apCommandSender) override + { + gOnWriteDoneCallback(mAppContext); + // delete apCommandSender; + delete this; + }; + +private: + PyObject mAppContext = nullptr; +}; + +} // namespace python +} // namespace chip + +using namespace chip::python; + +extern "C" { +void pychip_WriteClient_InitCallbacks(OnWriteResponseCallback onWriteResponseCallback, OnWriteErrorCallback onWriteErrorCallback, + OnWriteDoneCallback onWriteDoneCallback) +{ + gOnWriteResponseCallback = onWriteResponseCallback; + gOnWriteErrorCallback = onWriteErrorCallback; + gOnWriteDoneCallback = onWriteDoneCallback; +} + +chip::ChipError::StorageType pychip_WriteClient_WriteAttributes(void * appContext, Controller::Device * device, size_t n, ...) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + + std::unique_ptr callback = std::make_unique(appContext); + app::WriteClientHandle client; + + va_list args; + va_start(args, n); + + SuccessOrExit(err = app::InteractionModelEngine::GetInstance()->NewWriteClient(client, callback.get())); + + { + for (size_t i = 0; i < n; i++) + { + void * path = va_arg(args, void *); + void * tlv = va_arg(args, void *); + int length = va_arg(args, int); + + AttributeWritePath pathObj; + memcpy(&pathObj, path, sizeof(AttributeWritePath)); + uint8_t * tlvBuffer = reinterpret_cast(tlv); + + TLV::TLVWriter * writer; + TLV::TLVReader reader; + + SuccessOrExit(err = client->PrepareAttribute( + chip::app::AttributePathParams(pathObj.endpointId, pathObj.clusterId, pathObj.attributeId))); + VerifyOrExit((writer = client->GetAttributeDataElementTLVWriter()) != nullptr, err = CHIP_ERROR_INCORRECT_STATE); + + reader.Init(tlvBuffer, static_cast(length)); + reader.Next(); + SuccessOrExit(err = writer->CopyElement(chip::TLV::ContextTag(chip::app::AttributeDataElement::kCsTag_Data), reader)); + + SuccessOrExit(err = client->FinishAttribute()); + } + } + + SuccessOrExit(err = device->SendWriteAttributeRequest(std::move(client), nullptr, nullptr)); + + callback.release(); + +exit: + va_end(args); + return err.AsInteger(); +} +} diff --git a/src/controller/python/chip/interaction_model/__init__.py b/src/controller/python/chip/interaction_model/__init__.py index cd886a32038ef5..60d63c2c8654d2 100644 --- a/src/controller/python/chip/interaction_model/__init__.py +++ b/src/controller/python/chip/interaction_model/__init__.py @@ -22,7 +22,7 @@ """Provides Python APIs for CHIP.""" import enum -from .delegate import OnSubscriptionReport, SetAttributeReportCallback, AttributePath +from .delegate import OnSubscriptionReport, SetAttributeReportCallback, AttributePath, AttributePathStruct from chip.exceptions import ChipStackException diff --git a/src/controller/python/test/test_scripts/base.py b/src/controller/python/test/test_scripts/base.py index 820e13371ccb92..1c824d2457983a 100644 --- a/src/controller/python/test/test_scripts/base.py +++ b/src/controller/python/test/test_scripts/base.py @@ -257,7 +257,7 @@ def TestReadBasicAttributes(self, nodeid: int, endpoint: int, group: int): "VendorID": 9050, "ProductName": "TEST_PRODUCT", "ProductID": 65279, - "UserLabel": "", + "UserLabel": "Test", "Location": "", "HardwareVersion": 0, "HardwareVersionString": "TEST_VERSION", diff --git a/src/controller/python/test/test_scripts/cluster_objects.py b/src/controller/python/test/test_scripts/cluster_objects.py index 1f40dbe49c8660..c12fba16ffbce1 100644 --- a/src/controller/python/test/test_scripts/cluster_objects.py +++ b/src/controller/python/test/test_scripts/cluster_objects.py @@ -18,6 +18,7 @@ import chip.clusters as Clusters import logging +from chip.clusters.Attribute import AttributePath, AttributeReadResult, AttributeStatus import chip.interaction_model logger = logging.getLogger('PythonMatterControllerTEST') @@ -71,6 +72,74 @@ async def SendCommandWithResponse(cls, devCtrl): if res.returnValue != 5: raise ValueError() + @classmethod + async def SendWriteRequest(cls, devCtrl): + res = await devCtrl.WriteAttribute(nodeid=NODE_ID, + attributes=[ + Clusters.Attribute.AttributeWriteRequest( + EndpointId=0, Attribute=Clusters.Basic.Attributes.UserLabel, Data="Test"), + Clusters.Attribute.AttributeWriteRequest( + EndpointId=0, Attribute=Clusters.Basic.Attributes.Location, Data="A loooong string") + ]) + expectedRes = [ + AttributeStatus(Path=AttributePath(EndpointId=0, ClusterId=40, + AttributeId=5), Status=chip.interaction_model.Status.Success), + AttributeStatus(Path=AttributePath(EndpointId=0, ClusterId=40, + AttributeId=6), Status=chip.interaction_model.Status.InvalidValue) + ] + + if res != expectedRes: + for i in range(len(res)): + if res[i] != expectedRes[i]: + logger.error( + f"Item {i} is not expected, expect {expectedRes[i]} got {res[i]}") + raise AssertionError("Read returned unexpected result.") + + @classmethod + async def SendReadRequest(cls, devCtrl): + res = await devCtrl.ReadAttribute(nodeid=NODE_ID, + attributes=[ + (0, Clusters.Basic.Attributes.VendorName), + (0, Clusters.Basic.Attributes.VendorID), + (0, Clusters.Basic.Attributes.ProductName), + (0, Clusters.Basic.Attributes.ProductID), + (0, Clusters.Basic.Attributes.UserLabel), + (0, Clusters.Basic.Attributes.Location), + (0, Clusters.Basic.Attributes.HardwareVersion), + (0, Clusters.Basic.Attributes.HardwareVersionString), + (0, Clusters.Basic.Attributes.SoftwareVersion), + (0, Clusters.Basic.Attributes.SoftwareVersionString), + ]) + expectedRes = [ + AttributeReadResult(Path=AttributePath( + EndpointId=0, ClusterId=40, AttributeId=1), Status=0, Data='TEST_VENDOR'), + AttributeReadResult(Path=AttributePath( + EndpointId=0, ClusterId=40, AttributeId=2), Status=0, Data=9050), + AttributeReadResult(Path=AttributePath( + EndpointId=0, ClusterId=40, AttributeId=3), Status=0, Data='TEST_PRODUCT'), + AttributeReadResult(Path=AttributePath( + EndpointId=0, ClusterId=40, AttributeId=4), Status=0, Data=65279), + AttributeReadResult(Path=AttributePath( + EndpointId=0, ClusterId=40, AttributeId=5), Status=0, Data='Test'), + AttributeReadResult(Path=AttributePath( + EndpointId=0, ClusterId=40, AttributeId=6), Status=0, Data=''), + AttributeReadResult(Path=AttributePath( + EndpointId=0, ClusterId=40, AttributeId=7), Status=0, Data=0), + AttributeReadResult(Path=AttributePath( + EndpointId=0, ClusterId=40, AttributeId=8), Status=0, Data='TEST_VERSION'), + AttributeReadResult(Path=AttributePath( + EndpointId=0, ClusterId=40, AttributeId=9), Status=0, Data=0), + AttributeReadResult(Path=AttributePath( + EndpointId=0, ClusterId=40, AttributeId=10), Status=0, Data='prerelease') + ] + + if res != expectedRes: + for i in range(len(res)): + if res[i] != expectedRes[i]: + logger.error( + f"Item {i} is not expected, expect {expectedRes[i]} got {res[i]}") + raise AssertionError("Read returned unexpected result.") + @classmethod async def RunTest(cls, devCtrl): try: @@ -78,6 +147,8 @@ async def RunTest(cls, devCtrl): await cls.RoundTripTest(devCtrl) await cls.RoundTripTestWithBadEndpoint(devCtrl) await cls.SendCommandWithResponse(devCtrl) + await cls.SendWriteRequest(devCtrl) + await cls.SendReadRequest(devCtrl) except Exception as ex: logger.error( f"Unexpected error occurred when running tests: {ex}")