diff --git a/0001-Support-custom-platform-tag.patch b/0001-Support-custom-platform-tag.patch index 1f3d3e7..c54addb 100644 --- a/0001-Support-custom-platform-tag.patch +++ b/0001-Support-custom-platform-tag.patch @@ -1,5 +1,4 @@ -From d06671d9031156ce3b29e06d7edb344d8f6ee0d6 Mon Sep 17 00:00:00 2001 -Message-ID: +From bcd84a21c67a681fcaba43118d9d9bb7ed618b55 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 22 Nov 2022 10:51:17 +0100 Subject: [PATCH] Support custom platform tag @@ -9,7 +8,7 @@ Subject: [PATCH] Support custom platform tag 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/controller/python/BUILD.gn b/src/controller/python/BUILD.gn -index 4b84c63d5f..b01dafbc30 100644 +index 5fc2212098..caa33c3a40 100644 --- a/src/controller/python/BUILD.gn +++ b/src/controller/python/BUILD.gn @@ -35,6 +35,15 @@ declare_args() { @@ -28,7 +27,7 @@ index 4b84c63d5f..b01dafbc30 100644 } shared_library("ChipDeviceCtrl") { -@@ -340,16 +349,7 @@ chip_python_wheel_action("chip-core") { +@@ -344,16 +353,7 @@ chip_python_wheel_action("chip-core") { cpu_tag = current_cpu } @@ -47,5 +46,5 @@ index 4b84c63d5f..b01dafbc30 100644 tags = "cp37-abi3-" + py_platform_tag -- -2.42.0 +2.45.2 diff --git a/0002-Use-data-as-platform-storage-location.patch b/0002-Use-data-as-platform-storage-location.patch index 24dfcb7..82f975b 100644 --- a/0002-Use-data-as-platform-storage-location.patch +++ b/0002-Use-data-as-platform-storage-location.patch @@ -1,7 +1,4 @@ -From 88ba0f8233f97a9bb773341da7c996deee9675fe Mon Sep 17 00:00:00 2001 -Message-ID: <88ba0f8233f97a9bb773341da7c996deee9675fe.1698087175.git.stefan@agner.ch> -In-Reply-To: -References: +From 5c1316070604e8e7ade9b518ad717c63276dd06a Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Fri, 27 May 2022 16:38:14 +0200 Subject: [PATCH] Use /data as platform storage location @@ -11,7 +8,7 @@ Subject: [PATCH] Use /data as platform storage location 1 file changed, 6 insertions(+) diff --git a/src/platform/Linux/BUILD.gn b/src/platform/Linux/BUILD.gn -index a2cfa6b39c..f6fd74ab0c 100644 +index d73a2dcb0f..97c397994e 100644 --- a/src/platform/Linux/BUILD.gn +++ b/src/platform/Linux/BUILD.gn @@ -38,6 +38,12 @@ if (chip_mdns == "platform") { @@ -28,5 +25,5 @@ index a2cfa6b39c..f6fd74ab0c 100644 "../DeviceSafeQueue.cpp", "../DeviceSafeQueue.h", -- -2.42.0 +2.45.2 diff --git a/0003-Linux-Increase-number-of-endpoints.patch b/0003-Linux-Increase-number-of-endpoints.patch index 292e041..deffa66 100644 --- a/0003-Linux-Increase-number-of-endpoints.patch +++ b/0003-Linux-Increase-number-of-endpoints.patch @@ -1,4 +1,4 @@ -From 47ca82473af8d77eb89732884542c719ccdd9312 Mon Sep 17 00:00:00 2001 +From 3bcfcd1d91050868d0153bf7692db3ac844ccc94 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 29 Feb 2024 19:07:15 +0100 Subject: [PATCH] Linux: Increase number of endpoints @@ -36,5 +36,5 @@ index 3aab9a7b9b..02e664eddc 100644 // On linux platform, we have sys/socket.h, so HAVE_SO_BINDTODEVICE should be set to 1 -- -2.44.0 +2.45.2 diff --git a/0004-Python-Implement-async-friendly-GetConnectedDevice.patch b/0004-Implement-async-friendly-GetConnectedDevice.patch similarity index 97% rename from 0004-Python-Implement-async-friendly-GetConnectedDevice.patch rename to 0004-Implement-async-friendly-GetConnectedDevice.patch index 3759f7a..770a6fd 100644 --- a/0004-Python-Implement-async-friendly-GetConnectedDevice.patch +++ b/0004-Implement-async-friendly-GetConnectedDevice.patch @@ -1,7 +1,7 @@ -From f9fc067ad51d3989a2045f19fc5641971ce1ee20 Mon Sep 17 00:00:00 2001 +From 7ce75765081aeebef5f3adc397dff4db256348eb Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 27 Mar 2024 22:13:19 +0100 -Subject: [PATCH] [Python] Implement async friendly GetConnectedDevice +Subject: [PATCH] Implement async friendly GetConnectedDevice Currently GetConnectedDeviceSync() is blocking e.g. when a new session needs to be created. This is not asyncio friendly as it blocks the @@ -129,5 +129,5 @@ index 369260787d..b3d0aa2d7f 100644 v) for v in attributes] if attributes else None clusterDataVersionFilters = [self._parseDataVersionFilterTuple( -- -2.44.0 +2.45.2 diff --git a/0005-Enable-node-ID-logging-in-exchanges.patch b/0005-Enable-node-ID-logging-in-exchanges.patch index 2301737..06669c0 100644 --- a/0005-Enable-node-ID-logging-in-exchanges.patch +++ b/0005-Enable-node-ID-logging-in-exchanges.patch @@ -1,4 +1,4 @@ -From 3c551b7a706428d2fde77df0bc93d81bb37a2022 Mon Sep 17 00:00:00 2001 +From c1536db9448763f2e45b3aeef34f30cbfb41889e Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 18 Apr 2024 21:46:59 +0200 Subject: [PATCH] Enable node ID logging in exchanges @@ -22,5 +22,5 @@ index 558e0ee08e..5a3f5facb7 100644 /** -- -2.44.0 +2.45.2 diff --git a/0001-Python-Fix-OnRead-Event-Attribute-DataCallback-for-A.patch b/0006-Fix-OnRead-Event-Attribute-DataCallback-for-Arm64-Ap.patch similarity index 92% rename from 0001-Python-Fix-OnRead-Event-Attribute-DataCallback-for-A.patch rename to 0006-Fix-OnRead-Event-Attribute-DataCallback-for-Arm64-Ap.patch index 619a8ae..0db0b18 100644 --- a/0001-Python-Fix-OnRead-Event-Attribute-DataCallback-for-A.patch +++ b/0006-Fix-OnRead-Event-Attribute-DataCallback-for-Arm64-Ap.patch @@ -1,9 +1,8 @@ -From dce7020f3a9e542e6afbc59f131c77210df5f7db Mon Sep 17 00:00:00 2001 -Message-ID: +From 113143a945bbe5663b78b59b05d6b00670d79292 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 25 Apr 2024 15:19:17 +0200 -Subject: [PATCH] [Python] Fix OnRead[Event|Attribute]DataCallback for Arm64 - Apple Patform devices +Subject: [PATCH] Fix OnRead[Event|Attribute]DataCallback for Arm64 Apple + Patform devices On M1/Arm64 macOS systems, the OnReadEventDataCallback often returned an invalid status, e.g.: @@ -50,5 +49,5 @@ index e31f3431b8..b73b4a49b4 100644 // When the apData is nullptr, means we did not receive a valid event data from server, status will be some error // status. -- -2.44.0 +2.45.2 diff --git a/0006-Python-Add-raw-attribute-callback.patch b/0007-Add-raw-attribute-callback.patch similarity index 95% rename from 0006-Python-Add-raw-attribute-callback.patch rename to 0007-Add-raw-attribute-callback.patch index 44c8b68..1931922 100644 --- a/0006-Python-Add-raw-attribute-callback.patch +++ b/0007-Add-raw-attribute-callback.patch @@ -1,8 +1,7 @@ -From 9bc05af1e1ef2ec93336dc0eecba16b6802b6fb1 Mon Sep 17 00:00:00 2001 -Message-ID: <9bc05af1e1ef2ec93336dc0eecba16b6802b6fb1.1716466458.git.stefan@agner.ch> +From 4fe4daf7f165dc007ab6092bee2adef67650d129 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 23 May 2024 12:48:54 +0200 -Subject: [PATCH] [Python] Add raw attribute callback +Subject: [PATCH] Add raw attribute callback Add new subscription callback which uses raw AttributePath as paths of changed attributes. This allows to subscribe to custom clusters, @@ -102,5 +101,5 @@ index 9e46eed469..ce522bf452 100644 # Clear it out once we've notified of all changes in this transaction. self._changedPathSet = set() -- -2.45.1 +2.45.2 diff --git a/0008-Python-Eliminate-ZCLSubscribeAttribute-33337.patch b/0008-Python-Eliminate-ZCLSubscribeAttribute-33337.patch new file mode 100644 index 0000000..81e86fe --- /dev/null +++ b/0008-Python-Eliminate-ZCLSubscribeAttribute-33337.patch @@ -0,0 +1,594 @@ +From ba5d91afca90d186fb78de54f5d71705747b097a Mon Sep 17 00:00:00 2001 +From: Stefan Agner +Date: Mon, 13 May 2024 15:16:56 +0200 +Subject: [PATCH] [Python] Eliminate ZCLSubscribeAttribute (#33337) + +* Use asyncio sleep to unblock asyncio event loop + +* Avoid fixed sleep in TestCaseEviction + +Use asyncio Event and wait_for to wait for the change and continue +immediately when received. + +* Make TestSubscription an async test + +The current test implementation starves the asyncio event loop by +synchronously waiting for the threading.Condition. This prevents +making the SubscriptionTransaction fully leveraging the async +paradigm. + +It probably would be possible to mix asyncio.sleep() and threading, +but instead embrace the async pradigm for this test. + +* Make TestSubscriptionResumption an async test + +The current test implementation starves the asyncio event loop by +synchronously waiting for the threading.Condition. This prevents +making the SubscriptionTransaction fully leveraging the async +paradigm. + +It probably would be possible to mix asyncio.sleep() and threading, +but instead embrace the async pradigm for this test. + +* Make TestSubscriptionResumptionCapacityStep1 an async test + +Eliminate use of ZCLSubscribeAttribute and embrace asyncio. + +* Make TestSubscriptionResumptionCapacityStep2 an async test + +Eliminate use of ZCLSubscribeAttribute and embrace asyncio. + +* Remove ZCLSubscribeAttribute from subscription_resumption_timeout_test + +Use ReadAttribute with asyncio in subscription_resumption_timeout_test +as well. + +* Rewrite TestWriteBasicAttributes to drop ZCLRead/WriteAttribute + +* Improve wait for end of update task in TestSubscription +--- + .../python/test/test_scripts/base.py | 219 +++++++++--------- + .../test/test_scripts/mobile-device-test.py | 9 +- + ...cription_resumption_capacity_test_ctrl1.py | 5 +- + ...cription_resumption_capacity_test_ctrl2.py | 6 +- + .../subscription_resumption_test.py | 5 +- + .../subscription_resumption_timeout_test.py | 10 +- + 6 files changed, 128 insertions(+), 126 deletions(-) + +diff --git a/src/controller/python/test/test_scripts/base.py b/src/controller/python/test/test_scripts/base.py +index ed091858d2..5edb78f8e1 100644 +--- a/src/controller/python/test/test_scripts/base.py ++++ b/src/controller/python/test/test_scripts/base.py +@@ -697,13 +697,13 @@ class BaseTestHelper: + # on the sub we established previously. Since it was just marked defunct, it should return back to being + # active and a report should get delivered. + # +- sawValueChange = False ++ sawValueChangeEvent = asyncio.Event() ++ loop = asyncio.get_running_loop() + + def OnValueChange(path: Attribute.TypedAttributePath, transaction: Attribute.SubscriptionTransaction) -> None: +- nonlocal sawValueChange + self.logger.info("Saw value change!") + if (path.AttributeType == Clusters.UnitTesting.Attributes.Int8u and path.Path.EndpointId == 1): +- sawValueChange = True ++ loop.call_soon_threadsafe(sawValueChangeEvent.set) + + self.logger.info("Testing CASE defunct logic") + +@@ -720,14 +720,15 @@ class BaseTestHelper: + # was received. + # + await self.devCtrl2.WriteAttribute(nodeid, [(1, Clusters.UnitTesting.Attributes.Int8u(4))]) +- time.sleep(2) + +- sub.Shutdown() +- +- if sawValueChange is False: ++ try: ++ await asyncio.wait_for(sawValueChangeEvent.wait(), 2) ++ except TimeoutError: + self.logger.error( + "Didn't see value change in time, likely because sub got terminated due to unexpected session eviction!") + return False ++ finally: ++ sub.Shutdown() + + # + # In this test, we're going to setup a subscription on fabric1 through devCtl, then, constantly keep +@@ -739,7 +740,7 @@ class BaseTestHelper: + # + self.logger.info("Testing fabric-isolated CASE eviction") + +- sawValueChange = False ++ sawValueChangeEvent.clear() + sub = await self.devCtrl.ReadAttribute(nodeid, [(Clusters.UnitTesting.Attributes.Int8u)], reportInterval=(0, 1)) + sub.SetAttributeUpdateCallback(OnValueChange) + +@@ -752,20 +753,21 @@ class BaseTestHelper: + # was received. + # + await self.devCtrl2.WriteAttribute(nodeid, [(1, Clusters.UnitTesting.Attributes.Int8u(4))]) +- time.sleep(2) +- +- sub.Shutdown() + +- if sawValueChange is False: ++ try: ++ await asyncio.wait_for(sawValueChangeEvent.wait(), 2) ++ except TimeoutError: + self.logger.error("Didn't see value change in time, likely because sub got terminated due to other fabric (fabric1)") + return False ++ finally: ++ sub.Shutdown() + + # + # Do the same test again, but reversing the roles of fabric1 and fabric2. + # + self.logger.info("Testing fabric-isolated CASE eviction (reverse)") + +- sawValueChange = False ++ sawValueChangeEvent.clear() + sub = await self.devCtrl2.ReadAttribute(nodeid, [(Clusters.UnitTesting.Attributes.Int8u)], reportInterval=(0, 1)) + sub.SetAttributeUpdateCallback(OnValueChange) + +@@ -774,13 +776,13 @@ class BaseTestHelper: + await self.devCtrl.ReadAttribute(nodeid, [(Clusters.BasicInformation.Attributes.ClusterRevision)]) + + await self.devCtrl.WriteAttribute(nodeid, [(1, Clusters.UnitTesting.Attributes.Int8u(4))]) +- time.sleep(2) +- +- sub.Shutdown() +- +- if sawValueChange is False: ++ try: ++ await asyncio.wait_for(sawValueChangeEvent.wait(), 2) ++ except TimeoutError: + self.logger.error("Didn't see value change in time, likely because sub got terminated due to other fabric (fabric2)") + return False ++ finally: ++ sub.Shutdown() + + return True + +@@ -1198,121 +1200,114 @@ class BaseTestHelper: + return False + return True + +- def TestWriteBasicAttributes(self, nodeid: int, endpoint: int, group: int): ++ async def TestWriteBasicAttributes(self, nodeid: int, endpoint: int): + @ dataclass + class AttributeWriteRequest: +- cluster: str +- attribute: str ++ cluster: Clusters.ClusterObjects.Cluster ++ attribute: Clusters.ClusterObjects.ClusterAttributeDescriptor + value: Any + expected_status: IM.Status = IM.Status.Success + + requests = [ +- AttributeWriteRequest("BasicInformation", "NodeLabel", "Test"), +- AttributeWriteRequest("BasicInformation", "Location", ++ AttributeWriteRequest(Clusters.BasicInformation, Clusters.BasicInformation.Attributes.NodeLabel, "Test"), ++ AttributeWriteRequest(Clusters.BasicInformation, Clusters.BasicInformation.Attributes.Location, + "a pretty loooooooooooooog string", IM.Status.ConstraintError), + ] +- failed_zcl = [] ++ failed_attribute_write = [] + for req in requests: + try: + try: +- self.devCtrl.ZCLWriteAttribute(cluster=req.cluster, +- attribute=req.attribute, +- nodeid=nodeid, +- endpoint=endpoint, +- groupid=group, +- value=req.value) ++ await self.devCtrl.WriteAttribute(nodeid, [(endpoint, req.attribute, 0)]) + if req.expected_status != IM.Status.Success: + raise AssertionError( +- f"Write attribute {req.cluster}.{req.attribute} expects failure but got success response") ++ f"Write attribute {req.attribute.__qualname__} expects failure but got success response") + except Exception as ex: + if req.expected_status != IM.Status.Success: + continue + else: + raise ex +- res = self.devCtrl.ZCLReadAttribute( +- cluster=req.cluster, attribute=req.attribute, nodeid=nodeid, endpoint=endpoint, groupid=group) +- TestResult(f"Read attribute {req.cluster}.{req.attribute}", res).assertValueEqual( +- req.value) ++ ++ res = await self.devCtrl.ReadAttribute(nodeid, [(endpoint, req.attribute)]) ++ val = res[endpoint][req.cluster][req.attribute] ++ if val != req.value: ++ raise Exception( ++ f"Read attribute {req.attribute.__qualname__}: expected value {req.value}, got {val}") + except Exception as ex: +- failed_zcl.append(str(ex)) +- if failed_zcl: +- self.logger.exception(f"Following attributes failed: {failed_zcl}") ++ failed_attribute_write.append(str(ex)) ++ if failed_attribute_write: ++ self.logger.exception(f"Following attributes failed: {failed_attribute_write}") + return False + return True + +- def TestSubscription(self, nodeid: int, endpoint: int): ++ async def TestSubscription(self, nodeid: int, endpoint: int): + desiredPath = None + receivedUpdate = 0 +- updateLock = threading.Lock() +- updateCv = threading.Condition(updateLock) ++ updateEvent = asyncio.Event() ++ loop = asyncio.get_running_loop() + + def OnValueChange(path: Attribute.TypedAttributePath, transaction: Attribute.SubscriptionTransaction) -> None: +- nonlocal desiredPath, updateCv, updateLock, receivedUpdate ++ nonlocal desiredPath, updateEvent, receivedUpdate + if path.Path != desiredPath: + return + + data = transaction.GetAttribute(path) + logger.info( + f"Received report from server: path: {path.Path}, value: {data}") +- with updateLock: +- receivedUpdate += 1 +- updateCv.notify_all() +- +- class _conductAttributeChange(threading.Thread): +- def __init__(self, devCtrl: ChipDeviceCtrl.ChipDeviceController, nodeid: int, endpoint: int): +- super(_conductAttributeChange, self).__init__() +- self.nodeid = nodeid +- self.endpoint = endpoint +- self.devCtrl = devCtrl +- +- def run(self): +- for i in range(5): +- time.sleep(3) +- self.devCtrl.ZCLSend( +- "OnOff", "Toggle", self.nodeid, self.endpoint, 0, {}) ++ receivedUpdate += 1 ++ loop.call_soon_threadsafe(updateEvent.set) ++ ++ async def _conductAttributeChange(devCtrl: ChipDeviceCtrl.ChipDeviceController, nodeid: int, endpoint: int): ++ for i in range(5): ++ await asyncio.sleep(3) ++ await self.devCtrl.SendCommand(nodeid, endpoint, Clusters.OnOff.Commands.Toggle()) + + try: + desiredPath = Clusters.Attribute.AttributePath( + EndpointId=1, ClusterId=6, AttributeId=0) + # OnOff Cluster, OnOff Attribute +- subscription = self.devCtrl.ZCLSubscribeAttribute( +- "OnOff", "OnOff", nodeid, endpoint, 1, 10) ++ subscription = await self.devCtrl.ReadAttribute(nodeid, [(endpoint, Clusters.OnOff.Attributes.OnOff)], None, False, reportInterval=(1, 10), ++ keepSubscriptions=False, autoResubscribe=True) + subscription.SetAttributeUpdateCallback(OnValueChange) +- changeThread = _conductAttributeChange( +- self.devCtrl, nodeid, endpoint) + # Reset the number of subscriptions received as subscribing causes a callback. +- changeThread.start() +- with updateCv: +- while receivedUpdate < 5: +- # We should observe 5 attribute changes +- # The changing thread will change the value after 3 seconds. If we're waiting more than 10, assume something +- # is really wrong and bail out here with some information. +- if not updateCv.wait(10.0): +- self.logger.error( +- "Failed to receive subscription update") +- break +- +- # thread changes 5 times, and sleeps for 3 seconds in between. +- # Add an additional 3 seconds of slack. Timeout is in seconds. +- changeThread.join(18.0) ++ taskAttributeChange = loop.create_task(_conductAttributeChange(self.devCtrl, nodeid, endpoint)) + +- # +- # Clean-up by shutting down the sub. Otherwise, we're going to get callbacks through +- # OnValueChange on what will soon become an invalid +- # execution context above. +- # +- subscription.Shutdown() ++ while receivedUpdate < 5: ++ # We should observe 5 attribute changes ++ # The changing thread will change the value after 3 seconds. If we're waiting more than 10, assume something ++ # is really wrong and bail out here with some information. ++ try: ++ await asyncio.wait_for(updateEvent.wait(), 10) ++ updateEvent.clear() ++ except TimeoutError: ++ self.logger.error( ++ "Failed to receive subscription update") ++ break + +- if changeThread.is_alive(): +- # Thread join timed out +- self.logger.error("Failed to join change thread") +- return False ++ # At this point the task should really have done the three attribute, ++ # otherwise something is wrong. Wait for just 1s in case of a race ++ # condition between the last attribute update and the callback. ++ try: ++ await asyncio.wait_for(taskAttributeChange, 1) ++ except asyncio.TimeoutError: ++ # If attribute change task did not finish something is wrong. Cancel ++ # the task. ++ taskAttributeChange.cancel() ++ # This will throw a asyncio.CancelledError and makes sure the test ++ # is declared failed. ++ await taskAttributeChange + + return True if receivedUpdate == 5 else False + + except Exception as ex: + self.logger.exception(f"Failed to finish API test: {ex}") + return False ++ finally: ++ # ++ # Clean-up by shutting down the sub. Otherwise, we're going to get callbacks through ++ # OnValueChange on what will soon become an invalid ++ # execution context above. ++ # ++ subscription.Shutdown() + + return True + +@@ -1346,7 +1341,7 @@ class BaseTestHelper: + + return status == IM.Status.UnsupportedAccess + +- def TestSubscriptionResumption(self, nodeid: int, endpoint: int, remote_ip: str, ssh_port: int, remote_server_app: str): ++ async def TestSubscriptionResumption(self, nodeid: int, endpoint: int, remote_ip: str, ssh_port: int, remote_server_app: str): + ''' + This test validates that the device can resume the subscriptions after restarting. + It is executed in Linux Cirque tests and the steps of this test are: +@@ -1355,42 +1350,40 @@ class BaseTestHelper: + 3. Validate that the controller can receive a report from the remote server app + ''' + desiredPath = None +- receivedUpdate = False +- updateLock = threading.Lock() +- updateCv = threading.Condition(updateLock) ++ updateEvent = asyncio.Event() ++ loop = asyncio.get_running_loop() + + def OnValueReport(path: Attribute.TypedAttributePath, transaction: Attribute.SubscriptionTransaction) -> None: +- nonlocal desiredPath, updateCv, updateLock, receivedUpdate ++ nonlocal desiredPath, updateEvent, receivedUpdate + if path.Path != desiredPath: + return + + data = transaction.GetAttribute(path) + logger.info( + f"Received report from server: path: {path.Path}, value: {data}") +- with updateLock: +- receivedUpdate = True +- updateCv.notify_all() ++ loop.call_soon_threadsafe(updateEvent.set) + + try: + desiredPath = Clusters.Attribute.AttributePath( + EndpointId=0, ClusterId=0x28, AttributeId=5) + # BasicInformation Cluster, NodeLabel Attribute +- subscription = self.devCtrl.ZCLSubscribeAttribute( +- "BasicInformation", "NodeLabel", nodeid, endpoint, 1, 50, keepSubscriptions=True, autoResubscribe=False) ++ subscription = await self.devCtrl.ReadAttribute(nodeid, [(endpoint, Clusters.BasicInformation.Attributes.NodeLabel)], None, False, reportInterval=(1, 50), ++ keepSubscriptions=True, autoResubscribe=False) + subscription.SetAttributeUpdateCallback(OnValueReport) + +- self.logger.info("Restart remote deivce") ++ self.logger.info("Restart remote device") + restartRemoteThread = restartRemoteDevice( + remote_ip, ssh_port, "root", "admin", remote_server_app, "--thread --discriminator 3840") + restartRemoteThread.start() + # After device restarts, the attribute will be set dirty so the subscription can receive + # the update +- with updateCv: +- while receivedUpdate is False: +- if not updateCv.wait(10.0): +- self.logger.error( +- "Failed to receive subscription resumption report") +- break ++ receivedUpdate = False ++ try: ++ await asyncio.wait_for(updateEvent.wait(), 10) ++ receivedUpdate = True ++ except TimeoutError: ++ self.logger.error( ++ "Failed to receive subscription resumption report") + + restartRemoteThread.join(10.0) + +@@ -1437,25 +1430,26 @@ class BaseTestHelper: + controller 1 in container 1 while the Step2 is executed in controller 2 in container 2 + ''' + +- def TestSubscriptionResumptionCapacityStep1(self, nodeid: int, endpoint: int, passcode: int, subscription_capacity: int): ++ async def TestSubscriptionResumptionCapacityStep1(self, nodeid: int, endpoint: int, passcode: int, subscription_capacity: int): + try: + # BasicInformation Cluster, NodeLabel Attribute + for i in range(subscription_capacity): +- self.devCtrl.ZCLSubscribeAttribute( +- "BasicInformation", "NodeLabel", nodeid, endpoint, 1, 50, keepSubscriptions=True, autoResubscribe=False) ++ await self.devCtrl.ReadAttribute(nodeid, [(endpoint, Clusters.BasicInformation.Attributes.NodeLabel)], None, ++ False, reportInterval=(1, 50), ++ keepSubscriptions=True, autoResubscribe=False) + + logger.info("Send OpenCommissioningWindow command on fist controller") + discriminator = 3840 + salt = secrets.token_bytes(16) + iterations = 2000 + verifier = GenerateVerifier(passcode, salt, iterations) +- asyncio.run(self.devCtrl.SendCommand( ++ await self.devCtrl.SendCommand( + nodeid, 0, Clusters.AdministratorCommissioning.Commands.OpenCommissioningWindow( + commissioningTimeout=180, + PAKEPasscodeVerifier=verifier, + discriminator=discriminator, + iterations=iterations, +- salt=salt), timedRequestTimeoutMs=10000)) ++ salt=salt), timedRequestTimeoutMs=10000) + return True + + except Exception as ex: +@@ -1464,8 +1458,8 @@ class BaseTestHelper: + + return True + +- def TestSubscriptionResumptionCapacityStep2(self, nodeid: int, endpoint: int, remote_ip: str, ssh_port: int, +- remote_server_app: str, subscription_capacity: int): ++ async def TestSubscriptionResumptionCapacityStep2(self, nodeid: int, endpoint: int, remote_ip: str, ssh_port: int, ++ remote_server_app: str, subscription_capacity: int): + try: + self.logger.info("Restart remote deivce") + extra_agrs = f"--thread --discriminator 3840 --subscription-capacity {subscription_capacity}" +@@ -1479,8 +1473,9 @@ class BaseTestHelper: + self.logger.info("Send a new subscription request from the second controller") + # Close previous session so that the second controller will res-establish the session with the remote device + self.devCtrl.CloseSession(nodeid) +- self.devCtrl.ZCLSubscribeAttribute( +- "BasicInformation", "NodeLabel", nodeid, endpoint, 1, 50, keepSubscriptions=True, autoResubscribe=False) ++ await self.devCtrl.ReadAttribute(nodeid, [(endpoint, Clusters.BasicInformation.Attributes.NodeLabel)], None, ++ False, reportInterval=(1, 50), ++ keepSubscriptions=True, autoResubscribe=False) + + if restartRemoteThread.is_alive(): + # Thread join timed out +diff --git a/src/controller/python/test/test_scripts/mobile-device-test.py b/src/controller/python/test/test_scripts/mobile-device-test.py +index 9ceaa35d24..33ae713fe0 100755 +--- a/src/controller/python/test/test_scripts/mobile-device-test.py ++++ b/src/controller/python/test/test_scripts/mobile-device-test.py +@@ -129,9 +129,8 @@ def TestDatamodel(test: BaseTestHelper, device_nodeid: int): + "Failed to test Read Basic Attributes") + + logger.info("Testing attribute writing") +- FailIfNot(test.TestWriteBasicAttributes(nodeid=device_nodeid, +- endpoint=ENDPOINT_ID, +- group=GROUP_ID), ++ FailIfNot(asyncio.run(test.TestWriteBasicAttributes(nodeid=device_nodeid, ++ endpoint=ENDPOINT_ID)), + "Failed to test Write Basic Attributes") + + logger.info("Testing attribute reading basic again") +@@ -141,11 +140,11 @@ def TestDatamodel(test: BaseTestHelper, device_nodeid: int): + "Failed to test Read Basic Attributes") + + logger.info("Testing subscription") +- FailIfNot(test.TestSubscription(nodeid=device_nodeid, endpoint=LIGHTING_ENDPOINT_ID), ++ FailIfNot(asyncio.run(test.TestSubscription(nodeid=device_nodeid, endpoint=LIGHTING_ENDPOINT_ID)), + "Failed to subscribe attributes.") + + logger.info("Testing another subscription that kills previous subscriptions") +- FailIfNot(test.TestSubscription(nodeid=device_nodeid, endpoint=LIGHTING_ENDPOINT_ID), ++ FailIfNot(asyncio.run(test.TestSubscription(nodeid=device_nodeid, endpoint=LIGHTING_ENDPOINT_ID)), + "Failed to subscribe attributes.") + + logger.info("Testing re-subscription") +diff --git a/src/controller/python/test/test_scripts/subscription_resumption_capacity_test_ctrl1.py b/src/controller/python/test/test_scripts/subscription_resumption_capacity_test_ctrl1.py +index 19065b8a35..e02564e293 100755 +--- a/src/controller/python/test/test_scripts/subscription_resumption_capacity_test_ctrl1.py ++++ b/src/controller/python/test/test_scripts/subscription_resumption_capacity_test_ctrl1.py +@@ -19,6 +19,7 @@ + + # Commissioning test. + ++import asyncio + import os + import sys + from optparse import OptionParser +@@ -113,8 +114,8 @@ def main(): + "Failed on on-network commissioing") + + FailIfNot( +- test.TestSubscriptionResumptionCapacityStep1( +- options.nodeid, TEST_ENDPOINT_ID, options.setuppin, options.subscriptionCapacity), ++ asyncio.run(test.TestSubscriptionResumptionCapacityStep1( ++ options.nodeid, TEST_ENDPOINT_ID, options.setuppin, options.subscriptionCapacity)), + "Failed on step 1 of testing subscription resumption capacity") + + timeoutTicker.stop() +diff --git a/src/controller/python/test/test_scripts/subscription_resumption_capacity_test_ctrl2.py b/src/controller/python/test/test_scripts/subscription_resumption_capacity_test_ctrl2.py +index 2f3058afcd..ac449a9f54 100755 +--- a/src/controller/python/test/test_scripts/subscription_resumption_capacity_test_ctrl2.py ++++ b/src/controller/python/test/test_scripts/subscription_resumption_capacity_test_ctrl2.py +@@ -19,6 +19,7 @@ + + # Commissioning test. + ++import asyncio + import os + import sys + from optparse import OptionParser +@@ -125,8 +126,9 @@ def main(): + "Failed on on-network commissioing") + + FailIfNot( +- test.TestSubscriptionResumptionCapacityStep2(options.nodeid, TEST_ENDPOINT_ID, options.deviceAddress, +- TEST_SSH_PORT, options.remoteServerApp, options.subscriptionCapacity), ++ asyncio.run( ++ test.TestSubscriptionResumptionCapacityStep2(options.nodeid, TEST_ENDPOINT_ID, options.deviceAddress, ++ TEST_SSH_PORT, options.remoteServerApp, options.subscriptionCapacity)), + "Failed on testing subscription resumption capacity") + + timeoutTicker.stop() +diff --git a/src/controller/python/test/test_scripts/subscription_resumption_test.py b/src/controller/python/test/test_scripts/subscription_resumption_test.py +index 8b2000fb07..79edf6a289 100755 +--- a/src/controller/python/test/test_scripts/subscription_resumption_test.py ++++ b/src/controller/python/test/test_scripts/subscription_resumption_test.py +@@ -19,6 +19,7 @@ + + # Commissioning test. + ++import asyncio + import os + import sys + from optparse import OptionParser +@@ -115,8 +116,8 @@ def main(): + "Failed on on-network commissioing") + + FailIfNot( +- test.TestSubscriptionResumption(options.nodeid, TEST_ENDPOINT_ID, options.deviceAddress, +- TEST_SSH_PORT, options.remoteServerApp), "Failed to resume subscription") ++ asyncio.run(test.TestSubscriptionResumption(options.nodeid, TEST_ENDPOINT_ID, options.deviceAddress, ++ TEST_SSH_PORT, options.remoteServerApp)), "Failed to resume subscription") + + timeoutTicker.stop() + +diff --git a/src/controller/python/test/test_scripts/subscription_resumption_timeout_test.py b/src/controller/python/test/test_scripts/subscription_resumption_timeout_test.py +index 1f6411f636..4932e5b4cc 100755 +--- a/src/controller/python/test/test_scripts/subscription_resumption_timeout_test.py ++++ b/src/controller/python/test/test_scripts/subscription_resumption_timeout_test.py +@@ -19,11 +19,13 @@ + + # Commissioning test. + ++import asyncio + import os + import sys + from optparse import OptionParser + + from base import BaseTestHelper, FailIfNot, TestFail, TestTimeout, logger ++from chip import clusters as Clusters + + TEST_DISCRIMINATOR = 3840 + TEST_SETUPPIN = 20202021 +@@ -101,10 +103,12 @@ def main(): + + FailIfNot( + test.TestOnNetworkCommissioning(options.discriminator, options.setuppin, options.nodeid, options.deviceAddress), +- "Failed on on-network commissioing") ++ "Failed on on-network commissioning") ++ + try: +- test.devCtrl.ZCLSubscribeAttribute("BasicInformation", "NodeLabel", options.nodeid, TEST_ENDPOINT_ID, 1, 2, +- keepSubscriptions=True, autoResubscribe=False) ++ asyncio.run(test.devCtrl.ReadAttribute(options.nodeid, ++ [(TEST_ENDPOINT_ID, Clusters.BasicInformation.Attributes.NodeLabel)], ++ None, False, reportInterval=(1, 2), keepSubscriptions=True, autoResubscribe=False)) + except Exception as ex: + TestFail(f"Failed to subscribe attribute: {ex}") + +-- +2.45.2 + diff --git a/0009-Python-Eliminate-ZCLReadAttribute-ZCLSend-33428.patch b/0009-Python-Eliminate-ZCLReadAttribute-ZCLSend-33428.patch new file mode 100644 index 0000000..dbbe138 --- /dev/null +++ b/0009-Python-Eliminate-ZCLReadAttribute-ZCLSend-33428.patch @@ -0,0 +1,589 @@ +From 393dbaae77c7bf26a869b8498ce5e731ff0b9651 Mon Sep 17 00:00:00 2001 +From: Stefan Agner +Date: Thu, 16 May 2024 09:13:00 +0200 +Subject: [PATCH] [Python] Eliminate ZCLReadAttribute/ZCLSend (#33428) + +* Convert TestLevelControlCluster to asyncio + +Remove ZCLReadAttribute and ZCLSend API use from the level control +test TestLevelControlCluster and convert to asyncio. + +* Convert TestReadBasicAttributes to asyncio + +Remove ZCLReadAttribute API use from basic information cluster test +and convert to use asyncio. + +* Use SendCommand directly in send_zcl_command + +Avoid using ZCLSend API instead use SendCommand directly in the +send_zcl_command helper function. + +* Convert TestFailsafe to use asyncio/SendCommand + +Remove ZCLSend API usage and call SendCommand directly. Also convert +the test to a test using asyncio. + +* Convert TestOnOffCluster to use asyncio/SendCommand + +Remove ZCLSend API usage and call SendCommand directly. Also convert +the test to a test using asyncio. + +* Drop TestResult helper class + +The class is no longer required. Test results are tested directly. + +* Fix send_zcl_command argument formatting + +* Catch exception more specifically + +* Fix TestWriteBasicAttributes for all cases + +It seems TestWriteBasicAttributes did not correctly write +the attributes. The broad exception handling seems to have hidden +this issue even. Make sure the attributes with the correct value +get written, and check for unexpected and expected IM errors +in the per-attribute results specifically. + +* Fix TestFailsafe by catching correct exception + +* Drop unused import +--- + .../python/test/test_scripts/base.py | 189 ++++++++---------- + .../commissioning_failure_test.py | 6 +- + .../test/test_scripts/commissioning_test.py | 6 +- + .../test/test_scripts/failsafe_tests.py | 3 +- + .../test/test_scripts/mobile-device-test.py | 30 ++- + .../test_scripts/split_commissioning_test.py | 11 +- + .../mbed/integration_tests/common/utils.py | 41 ++-- + 7 files changed, 134 insertions(+), 152 deletions(-) + +diff --git a/src/controller/python/test/test_scripts/base.py b/src/controller/python/test/test_scripts/base.py +index 5edb78f8e1..c9b1881dd7 100644 +--- a/src/controller/python/test/test_scripts/base.py ++++ b/src/controller/python/test/test_scripts/base.py +@@ -178,29 +178,6 @@ class TestTimeout(threading.Thread): + TestFail("Timeout", doCrash=True) + + +-class TestResult: +- def __init__(self, operationName, result): +- self.operationName = operationName +- self.result = result +- +- def assertStatusEqual(self, expected): +- if self.result is None: +- raise Exception(f"{self.operationName}: no result got") +- if self.result.status != expected: +- raise Exception( +- f"{self.operationName}: expected status {expected}, got {self.result.status}") +- return self +- +- def assertValueEqual(self, expected): +- self.assertStatusEqual(0) +- if self.result is None: +- raise Exception(f"{self.operationName}: no result got") +- if self.result.value != expected: +- raise Exception( +- f"{self.operationName}: expected value {expected}, got {self.result.value}") +- return self +- +- + class BaseTestHelper: + def __init__(self, nodeid: int, paaTrustStorePath: str, testCommissioner: bool = False, + keypair: p256keypair.P256Keypair = None): +@@ -368,15 +345,16 @@ class BaseTestHelper: + def TestUsedTestCommissioner(self): + return self.devCtrl.GetTestCommissionerUsed() + +- def TestFailsafe(self, nodeid: int): ++ async def TestFailsafe(self, nodeid: int): + self.logger.info("Testing arm failsafe") + + self.logger.info("Setting failsafe on CASE connection") +- err, resp = self.devCtrl.ZCLSend("GeneralCommissioning", "ArmFailSafe", nodeid, +- 0, 0, dict(expiryLengthSeconds=60, breadcrumb=1), blocking=True) +- if err != 0: ++ try: ++ resp = await self.devCtrl.SendCommand(nodeid, 0, ++ Clusters.GeneralCommissioning.Commands.ArmFailSafe(expiryLengthSeconds=60, breadcrumb=1)) ++ except IM.InteractionModelError as ex: + self.logger.error( +- "Failed to send arm failsafe command error is {} with im response{}".format(err, resp)) ++ "Failed to send arm failsafe command error is {}".format(ex.status)) + return False + + if resp.errorCode is not Clusters.GeneralCommissioning.Enums.CommissioningErrorEnum.kOk: +@@ -387,17 +365,17 @@ class BaseTestHelper: + self.logger.info( + "Attempting to open basic commissioning window - this should fail since the failsafe is armed") + try: +- asyncio.run(self.devCtrl.SendCommand( ++ await self.devCtrl.SendCommand( + nodeid, + 0, + Clusters.AdministratorCommissioning.Commands.OpenBasicCommissioningWindow(180), + timedRequestTimeoutMs=10000 +- )) ++ ) + # we actually want the exception here because we want to see a failure, so return False here + self.logger.error( + 'Incorrectly succeeded in opening basic commissioning window') + return False +- except Exception: ++ except IM.InteractionModelError: + pass + + # TODO: +@@ -413,39 +391,39 @@ class BaseTestHelper: + self.logger.info( + "Attempting to open enhanced commissioning window - this should fail since the failsafe is armed") + try: +- asyncio.run(self.devCtrl.SendCommand( ++ await self.devCtrl.SendCommand( + nodeid, 0, Clusters.AdministratorCommissioning.Commands.OpenCommissioningWindow( + commissioningTimeout=180, + PAKEPasscodeVerifier=verifier, + discriminator=discriminator, + iterations=iterations, +- salt=salt), timedRequestTimeoutMs=10000)) ++ salt=salt), timedRequestTimeoutMs=10000) + + # we actually want the exception here because we want to see a failure, so return False here + self.logger.error( + 'Incorrectly succeeded in opening enhanced commissioning window') + return False +- except Exception: ++ except IM.InteractionModelError: + pass + + self.logger.info("Disarming failsafe on CASE connection") +- err, resp = self.devCtrl.ZCLSend("GeneralCommissioning", "ArmFailSafe", nodeid, +- 0, 0, dict(expiryLengthSeconds=0, breadcrumb=1), blocking=True) +- if err != 0: ++ try: ++ resp = await self.devCtrl.SendCommand(nodeid, 0, ++ Clusters.GeneralCommissioning.Commands.ArmFailSafe(expiryLengthSeconds=0, breadcrumb=1)) ++ except IM.InteractionModelError as ex: + self.logger.error( +- "Failed to send arm failsafe command error is {} with im response{}".format(err, resp)) ++ "Failed to send arm failsafe command error is {}".format(ex.status)) + return False + + self.logger.info( + "Opening Commissioning Window - this should succeed since the failsafe was just disarmed") + try: +- asyncio.run( +- self.devCtrl.SendCommand( +- nodeid, +- 0, +- Clusters.AdministratorCommissioning.Commands.OpenBasicCommissioningWindow(180), +- timedRequestTimeoutMs=10000 +- )) ++ await self.devCtrl.SendCommand( ++ nodeid, ++ 0, ++ Clusters.AdministratorCommissioning.Commands.OpenBasicCommissioningWindow(180), ++ timedRequestTimeoutMs=10000 ++ ) + except Exception: + self.logger.error( + 'Failed to open commissioning window after disarming failsafe') +@@ -453,11 +431,12 @@ class BaseTestHelper: + + self.logger.info( + "Attempting to arm failsafe over CASE - this should fail since the commissioning window is open") +- err, resp = self.devCtrl.ZCLSend("GeneralCommissioning", "ArmFailSafe", nodeid, +- 0, 0, dict(expiryLengthSeconds=60, breadcrumb=1), blocking=True) +- if err != 0: ++ try: ++ resp = await self.devCtrl.SendCommand(nodeid, 0, ++ Clusters.GeneralCommissioning.Commands.ArmFailSafe(expiryLengthSeconds=60, breadcrumb=1)) ++ except IM.InteractionModelError as ex: + self.logger.error( +- "Failed to send arm failsafe command error is {} with im response{}".format(err, resp)) ++ "Failed to send arm failsafe command error is {}".format(ex.status)) + return False + if resp.errorCode is Clusters.GeneralCommissioning.Enums.CommissioningErrorEnum.kBusyWithOtherAdmin: + return True +@@ -1094,50 +1073,48 @@ class BaseTestHelper: + self.devCtrl.SetThreadOperationalDataset(bytes.fromhex(dataset)) + return True + +- def TestOnOffCluster(self, nodeid: int, endpoint: int, group: int): ++ async def TestOnOffCluster(self, nodeid: int, endpoint: int): + self.logger.info( + "Sending On/Off commands to device {} endpoint {}".format(nodeid, endpoint)) +- err, resp = self.devCtrl.ZCLSend("OnOff", "On", nodeid, +- endpoint, group, {}, blocking=True) +- if err != 0: ++ ++ try: ++ await self.devCtrl.SendCommand(nodeid, endpoint, ++ Clusters.OnOff.Commands.On()) ++ except IM.InteractionModelError as ex: + self.logger.error( +- "failed to send OnOff.On: error is {} with im response{}".format(err, resp)) ++ "failed to send OnOff.On: error is {}".format(ex.status)) + return False +- err, resp = self.devCtrl.ZCLSend("OnOff", "Off", nodeid, +- endpoint, group, {}, blocking=True) +- if err != 0: ++ ++ try: ++ await self.devCtrl.SendCommand(nodeid, endpoint, ++ Clusters.OnOff.Commands.Off()) ++ except IM.InteractionModelError as ex: + self.logger.error( +- "failed to send OnOff.Off: error is {} with im response {}".format(err, resp)) ++ "failed to send OnOff.Off: error is {}".format(ex.status)) + return False + return True + +- def TestLevelControlCluster(self, nodeid: int, endpoint: int, group: int): ++ async def TestLevelControlCluster(self, nodeid: int, endpoint: int): + self.logger.info( + f"Sending MoveToLevel command to device {nodeid} endpoint {endpoint}") +- try: +- commonArgs = dict(transitionTime=0, optionsMask=1, optionsOverride=1) + ++ commonArgs = dict(transitionTime=0, optionsMask=1, optionsOverride=1) ++ ++ async def _moveClusterLevel(setLevel): ++ await self.devCtrl.SendCommand(nodeid, ++ endpoint, ++ Clusters.LevelControl.Commands.MoveToLevel(**commonArgs, level=setLevel)) ++ res = await self.devCtrl.ReadAttribute(nodeid, [(endpoint, Clusters.LevelControl.Attributes.CurrentLevel)]) ++ readVal = res[endpoint][Clusters.LevelControl][Clusters.LevelControl.Attributes.CurrentLevel] ++ if readVal != setLevel: ++ raise Exception(f"Read attribute LevelControl.CurrentLevel: expected value {setLevel}, got {readVal}") ++ ++ try: + # Move to 1 +- self.devCtrl.ZCLSend("LevelControl", "MoveToLevel", nodeid, +- endpoint, group, dict(**commonArgs, level=1), blocking=True) +- res = self.devCtrl.ZCLReadAttribute(cluster="LevelControl", +- attribute="CurrentLevel", +- nodeid=nodeid, +- endpoint=endpoint, +- groupid=group) +- TestResult("Read attribute LevelControl.CurrentLevel", +- res).assertValueEqual(1) ++ await _moveClusterLevel(1) + + # Move to 254 +- self.devCtrl.ZCLSend("LevelControl", "MoveToLevel", nodeid, +- endpoint, group, dict(**commonArgs, level=254), blocking=True) +- res = self.devCtrl.ZCLReadAttribute(cluster="LevelControl", +- attribute="CurrentLevel", +- nodeid=nodeid, +- endpoint=endpoint, +- groupid=group) +- TestResult("Read attribute LevelControl.CurrentLevel", +- res).assertValueEqual(254) ++ await _moveClusterLevel(254) + + return True + except Exception as ex: +@@ -1170,29 +1147,27 @@ class BaseTestHelper: + self.logger.exception("Failed to resolve. {}".format(ex)) + return False + +- def TestReadBasicAttributes(self, nodeid: int, endpoint: int, group: int): ++ async def TestReadBasicAttributes(self, nodeid: int, endpoint: int): ++ attrs = Clusters.BasicInformation.Attributes + basic_cluster_attrs = { +- "VendorName": "TEST_VENDOR", +- "VendorID": 0xFFF1, +- "ProductName": "TEST_PRODUCT", +- "ProductID": 0x8001, +- "NodeLabel": "Test", +- "Location": "XX", +- "HardwareVersion": 0, +- "HardwareVersionString": "TEST_VERSION", +- "SoftwareVersion": 1, +- "SoftwareVersionString": "1.0", ++ attrs.VendorName: "TEST_VENDOR", ++ attrs.VendorID: 0xFFF1, ++ attrs.ProductName: "TEST_PRODUCT", ++ attrs.ProductID: 0x8001, ++ attrs.NodeLabel: "Test", ++ attrs.Location: "XX", ++ attrs.HardwareVersion: 0, ++ attrs.HardwareVersionString: "TEST_VERSION", ++ attrs.SoftwareVersion: 1, ++ attrs.SoftwareVersionString: "1.0", + } + failed_zcl = {} + for basic_attr, expected_value in basic_cluster_attrs.items(): + try: +- res = self.devCtrl.ZCLReadAttribute(cluster="BasicInformation", +- attribute=basic_attr, +- nodeid=nodeid, +- endpoint=endpoint, +- groupid=group) +- TestResult(f"Read attribute {basic_attr}", res).assertValueEqual( +- expected_value) ++ res = await self.devCtrl.ReadAttribute(nodeid, [(endpoint, basic_attr)]) ++ readVal = res[endpoint][Clusters.BasicInformation][basic_attr] ++ if readVal != expected_value: ++ raise Exception(f"Read attribute: expected value {expected_value}, got {readVal}") + except Exception as ex: + failed_zcl[basic_attr] = str(ex) + if failed_zcl: +@@ -1216,16 +1191,16 @@ class BaseTestHelper: + failed_attribute_write = [] + for req in requests: + try: +- try: +- await self.devCtrl.WriteAttribute(nodeid, [(endpoint, req.attribute, 0)]) +- if req.expected_status != IM.Status.Success: +- raise AssertionError( +- f"Write attribute {req.attribute.__qualname__} expects failure but got success response") +- except Exception as ex: +- if req.expected_status != IM.Status.Success: +- continue +- else: +- raise ex ++ # Errors tested here is in the per-attribute result list (type AttributeStatus) ++ write_res = await self.devCtrl.WriteAttribute(nodeid, [(endpoint, req.attribute(req.value))]) ++ status = write_res[0].Status ++ if req.expected_status != status: ++ raise AssertionError( ++ f"Write attribute {req.attribute.__qualname__} expects {req.expected_status} but got {status}") ++ ++ # Only execute read tests where write is successful. ++ if req.expected_status != IM.Status.Success: ++ continue + + res = await self.devCtrl.ReadAttribute(nodeid, [(endpoint, req.attribute)]) + val = res[endpoint][req.cluster][req.attribute] +diff --git a/src/controller/python/test/test_scripts/commissioning_failure_test.py b/src/controller/python/test/test_scripts/commissioning_failure_test.py +index 4681dd108d..a535c8b184 100755 +--- a/src/controller/python/test/test_scripts/commissioning_failure_test.py ++++ b/src/controller/python/test/test_scripts/commissioning_failure_test.py +@@ -19,6 +19,7 @@ + + # Commissioning test. + ++import asyncio + import os + import sys + from optparse import OptionParser +@@ -121,9 +122,8 @@ def main(): + FailIfNot(test.TestCommissionFailure(1, 0), "Failed to commission device") + + logger.info("Testing on off cluster") +- FailIfNot(test.TestOnOffCluster(nodeid=1, +- endpoint=LIGHTING_ENDPOINT_ID, +- group=GROUP_ID), "Failed to test on off cluster") ++ FailIfNot(asyncio.run(test.TestOnOffCluster(nodeid=1, ++ endpoint=LIGHTING_ENDPOINT_ID)), "Failed to test on off cluster") + + timeoutTicker.stop() + +diff --git a/src/controller/python/test/test_scripts/commissioning_test.py b/src/controller/python/test/test_scripts/commissioning_test.py +index b6adc0f477..4a7f15d6c3 100755 +--- a/src/controller/python/test/test_scripts/commissioning_test.py ++++ b/src/controller/python/test/test_scripts/commissioning_test.py +@@ -19,6 +19,7 @@ + + # Commissioning test. + ++import asyncio + import os + import sys + from optparse import OptionParser +@@ -146,9 +147,8 @@ def main(): + TestFail("Must provide device address or setup payload to commissioning the device") + + logger.info("Testing on off cluster") +- FailIfNot(test.TestOnOffCluster(nodeid=options.nodeid, +- endpoint=LIGHTING_ENDPOINT_ID, +- group=GROUP_ID), "Failed to test on off cluster") ++ FailIfNot(asyncio.run(test.TestOnOffCluster(nodeid=options.nodeid, ++ endpoint=LIGHTING_ENDPOINT_ID)), "Failed to test on off cluster") + + FailIfNot(test.TestUsedTestCommissioner(), + "Test commissioner check failed") +diff --git a/src/controller/python/test/test_scripts/failsafe_tests.py b/src/controller/python/test/test_scripts/failsafe_tests.py +index 4b3838430c..d1a2034e73 100755 +--- a/src/controller/python/test/test_scripts/failsafe_tests.py ++++ b/src/controller/python/test/test_scripts/failsafe_tests.py +@@ -19,6 +19,7 @@ + + # Commissioning test. + ++import asyncio + import os + import sys + from optparse import OptionParser +@@ -99,7 +100,7 @@ def main(): + nodeid=1), + "Failed to finish key exchange") + +- FailIfNot(test.TestFailsafe(nodeid=1), "Failed failsafe test") ++ FailIfNot(asyncio.run(test.TestFailsafe(nodeid=1)), "Failed failsafe test") + + timeoutTicker.stop() + +diff --git a/src/controller/python/test/test_scripts/mobile-device-test.py b/src/controller/python/test/test_scripts/mobile-device-test.py +index 33ae713fe0..8f6f534dce 100755 +--- a/src/controller/python/test/test_scripts/mobile-device-test.py ++++ b/src/controller/python/test/test_scripts/mobile-device-test.py +@@ -102,20 +102,17 @@ def TestDatamodel(test: BaseTestHelper, device_nodeid: int): + logger.info("Testing datamodel functions") + + logger.info("Testing on off cluster") +- FailIfNot(test.TestOnOffCluster(nodeid=device_nodeid, +- endpoint=LIGHTING_ENDPOINT_ID, +- group=GROUP_ID), "Failed to test on off cluster") ++ FailIfNot(asyncio.run(test.TestOnOffCluster(nodeid=device_nodeid, ++ endpoint=LIGHTING_ENDPOINT_ID)), "Failed to test on off cluster") + + logger.info("Testing level control cluster") +- FailIfNot(test.TestLevelControlCluster(nodeid=device_nodeid, +- endpoint=LIGHTING_ENDPOINT_ID, +- group=GROUP_ID), ++ FailIfNot(asyncio.run(test.TestLevelControlCluster(nodeid=device_nodeid, ++ endpoint=LIGHTING_ENDPOINT_ID)), + "Failed to test level control cluster") + + logger.info("Testing sending commands to non exist endpoint") +- FailIfNot(not test.TestOnOffCluster(nodeid=device_nodeid, +- endpoint=233, +- group=GROUP_ID), "Failed to test on off cluster on non-exist endpoint") ++ FailIfNot(not asyncio.run(test.TestOnOffCluster(nodeid=device_nodeid, ++ endpoint=233)), "Failed to test on off cluster on non-exist endpoint") + + # Test experimental Python cluster objects API + logger.info("Testing cluster objects API") +@@ -123,9 +120,8 @@ def TestDatamodel(test: BaseTestHelper, device_nodeid: int): + "Failed when testing Python Cluster Object APIs") + + logger.info("Testing attribute reading") +- FailIfNot(test.TestReadBasicAttributes(nodeid=device_nodeid, +- endpoint=ENDPOINT_ID, +- group=GROUP_ID), ++ FailIfNot(asyncio.run(test.TestReadBasicAttributes(nodeid=device_nodeid, ++ endpoint=ENDPOINT_ID)), + "Failed to test Read Basic Attributes") + + logger.info("Testing attribute writing") +@@ -134,9 +130,8 @@ def TestDatamodel(test: BaseTestHelper, device_nodeid: int): + "Failed to test Write Basic Attributes") + + logger.info("Testing attribute reading basic again") +- FailIfNot(test.TestReadBasicAttributes(nodeid=1, +- endpoint=ENDPOINT_ID, +- group=GROUP_ID), ++ FailIfNot(asyncio.run(test.TestReadBasicAttributes(nodeid=1, ++ endpoint=ENDPOINT_ID)), + "Failed to test Read Basic Attributes") + + logger.info("Testing subscription") +@@ -152,9 +147,8 @@ def TestDatamodel(test: BaseTestHelper, device_nodeid: int): + "Failed to validated re-subscription") + + logger.info("Testing on off cluster over resolved connection") +- FailIfNot(test.TestOnOffCluster(nodeid=device_nodeid, +- endpoint=LIGHTING_ENDPOINT_ID, +- group=GROUP_ID), "Failed to test on off cluster") ++ FailIfNot(asyncio.run(test.TestOnOffCluster(nodeid=device_nodeid, ++ endpoint=LIGHTING_ENDPOINT_ID)), "Failed to test on off cluster") + + logger.info("Testing writing/reading fabric sensitive data") + asyncio.run(test.TestFabricSensitive(nodeid=device_nodeid)) +diff --git a/src/controller/python/test/test_scripts/split_commissioning_test.py b/src/controller/python/test/test_scripts/split_commissioning_test.py +index 47fedb3aad..9233d58b90 100755 +--- a/src/controller/python/test/test_scripts/split_commissioning_test.py ++++ b/src/controller/python/test/test_scripts/split_commissioning_test.py +@@ -19,6 +19,7 @@ + + # Commissioning test. + ++import asyncio + import os + import sys + from optparse import OptionParser +@@ -118,14 +119,12 @@ def main(): + "Failed to commission device 2") + + logger.info("Testing on off cluster on device 1") +- FailIfNot(test.TestOnOffCluster(nodeid=1, +- endpoint=LIGHTING_ENDPOINT_ID, +- group=GROUP_ID), "Failed to test on off cluster on device 1") ++ FailIfNot(asyncio.run(test.TestOnOffCluster(nodeid=1, ++ endpoint=LIGHTING_ENDPOINT_ID)), "Failed to test on off cluster on device 1") + + logger.info("Testing on off cluster on device 2") +- FailIfNot(test.TestOnOffCluster(nodeid=2, +- endpoint=LIGHTING_ENDPOINT_ID, +- group=GROUP_ID), "Failed to test on off cluster on device 2") ++ FailIfNot(asyncio.run(test.TestOnOffCluster(nodeid=2, ++ endpoint=LIGHTING_ENDPOINT_ID)), "Failed to test on off cluster on device 2") + + timeoutTicker.stop() + +diff --git a/src/test_driver/mbed/integration_tests/common/utils.py b/src/test_driver/mbed/integration_tests/common/utils.py +index 036b612d7b..2b1db4da30 100644 +--- a/src/test_driver/mbed/integration_tests/common/utils.py ++++ b/src/test_driver/mbed/integration_tests/common/utils.py +@@ -14,6 +14,7 @@ + # limitations under the License. + + ++import asyncio + import logging + import platform + import random +@@ -114,21 +115,33 @@ def send_zcl_command(devCtrl, line): + if len(args) < 5: + raise exceptions.InvalidArgumentCount(5, len(args)) + +- if args[0] not in all_commands: +- raise exceptions.UnknownCluster(args[0]) +- command = all_commands.get(args[0]).get(args[1], None) ++ cluster = args[0] ++ command = args[1] ++ if cluster not in all_commands: ++ raise exceptions.UnknownCluster(cluster) ++ commandObj = all_commands.get(cluster).get(command, None) + # When command takes no arguments, (not command) is True +- if command is None: +- raise exceptions.UnknownCommand(args[0], args[1]) +- err, res = devCtrl.ZCLSend(args[0], args[1], int( +- args[2]), int(args[3]), int(args[4]), FormatZCLArguments(args[5:], command), blocking=True) +- if err != 0: +- log.error("Failed to send ZCL command [{}] {}.".format(err, res)) +- elif res is not None: +- log.info("Success, received command response:") +- log.info(res) +- else: +- log.info("Success, no command response.") ++ if commandObj is None: ++ raise exceptions.UnknownCommand(cluster, command) ++ ++ try: ++ req = commandObj(**FormatZCLArguments(args[5:], commandObj)) ++ except BaseException: ++ raise exceptions.UnknownCommand(cluster, command) ++ ++ nodeid = int(args[2]) ++ endpoint = int(args[3]) ++ try: ++ res = asyncio.run(devCtrl.SendCommand(nodeid, endpoint, req)) ++ logging.debug(f"CommandResponse {res}") ++ if res is not None: ++ log.info("Success, received command response:") ++ log.info(res) ++ else: ++ log.info("Success, no command response.") ++ except exceptions.InteractionModelError as ex: ++ return (int(ex.status), None) ++ log.error("Failed to send ZCL command [{}] {}.".format(int(ex.status), None)) + except exceptions.ChipStackException as ex: + log.error("An exception occurred during processing ZCL command:") + log.error(str(ex)) +-- +2.45.2 + diff --git a/0010-Python-Create-pairingDelegate-for-each-DeviceControl.patch b/0010-Python-Create-pairingDelegate-for-each-DeviceControl.patch new file mode 100644 index 0000000..973f96b --- /dev/null +++ b/0010-Python-Create-pairingDelegate-for-each-DeviceControl.patch @@ -0,0 +1,671 @@ +From 25180ab3236c2af49f6003109e1a2e8769c3ba4b Mon Sep 17 00:00:00 2001 +From: "tianfeng.yang" <130436698+tianfeng-yang@users.noreply.github.com> +Date: Wed, 17 Apr 2024 21:53:13 +0800 +Subject: [PATCH] [Python] Create pairingDelegate for each DeviceController + (#32369) + +* Create pairingDelegate for each DeviceController + +* restore the original pairingcomplete callback logic +--- + .../ChipDeviceController-ScriptBinding.cpp | 97 +++++++++++-------- + src/controller/python/OpCredsBinding.cpp | 40 +++++--- + src/controller/python/chip/ChipDeviceCtrl.py | 69 ++++++++----- + 3 files changed, 128 insertions(+), 78 deletions(-) + +diff --git a/src/controller/python/ChipDeviceController-ScriptBinding.cpp b/src/controller/python/ChipDeviceController-ScriptBinding.cpp +index d78c9da8ed..a55d3865bd 100644 +--- a/src/controller/python/ChipDeviceController-ScriptBinding.cpp ++++ b/src/controller/python/ChipDeviceController-ScriptBinding.cpp +@@ -105,7 +105,6 @@ chip::Controller::CommissioningParameters sCommissioningParameters; + + } // namespace + +-chip::Controller::ScriptDevicePairingDelegate sPairingDelegate; + chip::Controller::ScriptPairingDeviceDiscoveryDelegate sPairingDeviceDiscoveryDelegate; + chip::Credentials::GroupDataProviderImpl sGroupDataProvider; + chip::Credentials::PersistentStorageOpCertStore sPersistentStorageOpCertStore; +@@ -121,9 +120,8 @@ extern "C" { + PyChipError pychip_DeviceController_StackInit(Controller::Python::StorageAdapter * storageAdapter, bool enableServerInteractions); + PyChipError pychip_DeviceController_StackShutdown(); + +-PyChipError pychip_DeviceController_NewDeviceController(chip::Controller::DeviceCommissioner ** outDevCtrl, +- chip::NodeId localDeviceId, bool useTestCommissioner); +-PyChipError pychip_DeviceController_DeleteDeviceController(chip::Controller::DeviceCommissioner * devCtrl); ++PyChipError pychip_DeviceController_DeleteDeviceController(chip::Controller::DeviceCommissioner * devCtrl, ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate); + PyChipError pychip_DeviceController_GetAddressAndPort(chip::Controller::DeviceCommissioner * devCtrl, chip::NodeId nodeId, + char * outAddress, uint64_t maxAddressLen, uint16_t * outPort); + PyChipError pychip_DeviceController_GetCompressedFabricId(chip::Controller::DeviceCommissioner * devCtrl, uint64_t * outFabricId); +@@ -168,15 +166,17 @@ PyChipError pychip_DeviceController_DiscoverCommissionableNodesDeviceType(chip:: + uint16_t device_type); + PyChipError pychip_DeviceController_DiscoverCommissionableNodesCommissioningEnabled(chip::Controller::DeviceCommissioner * devCtrl); + +-PyChipError pychip_DeviceController_OnNetworkCommission(chip::Controller::DeviceCommissioner * devCtrl, uint64_t nodeId, +- uint32_t setupPasscode, const uint8_t filterType, const char * filterParam, +- uint32_t discoveryTimeoutMsec); ++PyChipError pychip_DeviceController_OnNetworkCommission(chip::Controller::DeviceCommissioner * devCtrl, ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ uint64_t nodeId, uint32_t setupPasscode, const uint8_t filterType, ++ const char * filterParam, uint32_t discoveryTimeoutMsec); + + PyChipError pychip_DeviceController_PostTaskOnChipThread(ChipThreadTaskRunnerFunct callback, void * pythonContext); + +-PyChipError pychip_DeviceController_OpenCommissioningWindow(chip::Controller::DeviceCommissioner * devCtrl, chip::NodeId nodeid, +- uint16_t timeout, uint32_t iteration, uint16_t discriminator, +- uint8_t optionInt); ++PyChipError pychip_DeviceController_OpenCommissioningWindow(chip::Controller::DeviceCommissioner * devCtrl, ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ chip::NodeId nodeid, uint16_t timeout, uint32_t iteration, ++ uint16_t discriminator, uint8_t optionInt); + + void pychip_DeviceController_PrintDiscoveredDevices(chip::Controller::DeviceCommissioner * devCtrl); + bool pychip_DeviceController_GetIPForDiscoveredDevice(chip::Controller::DeviceCommissioner * devCtrl, int idx, char * addrStr, +@@ -184,19 +184,28 @@ bool pychip_DeviceController_GetIPForDiscoveredDevice(chip::Controller::DeviceCo + + // Pairing Delegate + PyChipError +-pychip_ScriptDevicePairingDelegate_SetKeyExchangeCallback(chip::Controller::DeviceCommissioner * devCtrl, ++pychip_ScriptDevicePairingDelegate_SetKeyExchangeCallback(chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, + chip::Controller::DevicePairingDelegate_OnPairingCompleteFunct callback); + + PyChipError pychip_ScriptDevicePairingDelegate_SetCommissioningCompleteCallback( +- chip::Controller::DeviceCommissioner * devCtrl, chip::Controller::DevicePairingDelegate_OnCommissioningCompleteFunct callback); ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ chip::Controller::DevicePairingDelegate_OnCommissioningCompleteFunct callback); + + PyChipError pychip_ScriptDevicePairingDelegate_SetCommissioningStatusUpdateCallback( +- chip::Controller::DeviceCommissioner * devCtrl, ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, + chip::Controller::DevicePairingDelegate_OnCommissioningStatusUpdateFunct callback); ++ + PyChipError +-pychip_ScriptDevicePairingDelegate_SetFabricCheckCallback(chip::Controller::DevicePairingDelegate_OnFabricCheckFunct callback); ++pychip_ScriptDevicePairingDelegate_SetFabricCheckCallback(chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ chip::Controller::DevicePairingDelegate_OnFabricCheckFunct callback); ++ + PyChipError pychip_ScriptDevicePairingDelegate_SetOpenWindowCompleteCallback( +- chip::Controller::DeviceCommissioner * devCtrl, chip::Controller::DevicePairingDelegate_OnWindowOpenCompleteFunct callback); ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ chip::Controller::DevicePairingDelegate_OnWindowOpenCompleteFunct callback); ++ ++PyChipError ++pychip_ScriptDevicePairingDelegate_SetExpectingPairingComplete(chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ bool value); + + // BLE + PyChipError pychip_DeviceCommissioner_CloseBleConnection(chip::Controller::DeviceCommissioner * devCtrl); +@@ -354,7 +363,6 @@ const char * pychip_DeviceController_StatusReportToString(uint32_t profileId, ui + PyChipError pychip_DeviceController_ConnectBLE(chip::Controller::DeviceCommissioner * devCtrl, uint16_t discriminator, + uint32_t setupPINCode, chip::NodeId nodeid) + { +- sPairingDelegate.SetExpectingPairingComplete(true); + return ToPyChipError(devCtrl->PairDevice(nodeid, + chip::RendezvousParameters() + .SetPeerAddress(Transport::PeerAddress(Transport::Type::kBle)) +@@ -378,14 +386,12 @@ PyChipError pychip_DeviceController_ConnectIP(chip::Controller::DeviceCommission + addr.SetTransportType(chip::Transport::Type::kUdp).SetIPAddress(peerAddr).SetInterface(ifaceOutput); + params.SetPeerAddress(addr).SetDiscriminator(0); + +- sPairingDelegate.SetExpectingPairingComplete(true); + return ToPyChipError(devCtrl->PairDevice(nodeid, params, sCommissioningParameters)); + } + + PyChipError pychip_DeviceController_ConnectWithCode(chip::Controller::DeviceCommissioner * devCtrl, const char * onboardingPayload, + chip::NodeId nodeid, uint8_t discoveryType) + { +- sPairingDelegate.SetExpectingPairingComplete(true); + return ToPyChipError(devCtrl->PairDevice(nodeid, onboardingPayload, sCommissioningParameters, + static_cast(discoveryType))); + } +@@ -430,9 +436,10 @@ PyChipError pychip_DeviceController_UnpairDevice(chip::Controller::DeviceCommiss + return ToPyChipError(err); + } + +-PyChipError pychip_DeviceController_OnNetworkCommission(chip::Controller::DeviceCommissioner * devCtrl, uint64_t nodeId, +- uint32_t setupPasscode, const uint8_t filterType, const char * filterParam, +- uint32_t discoveryTimeoutMsec) ++PyChipError pychip_DeviceController_OnNetworkCommission(chip::Controller::DeviceCommissioner * devCtrl, ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ uint64_t nodeId, uint32_t setupPasscode, const uint8_t filterType, ++ const char * filterParam, uint32_t discoveryTimeoutMsec) + { + Dnssd::DiscoveryFilter filter(static_cast(filterType)); + switch (static_cast(filterType)) +@@ -467,9 +474,8 @@ PyChipError pychip_DeviceController_OnNetworkCommission(chip::Controller::Device + return ToPyChipError(CHIP_ERROR_INVALID_ARGUMENT); + } + +- sPairingDelegate.SetExpectingPairingComplete(true); +- CHIP_ERROR err = sPairingDeviceDiscoveryDelegate.Init(nodeId, setupPasscode, sCommissioningParameters, &sPairingDelegate, +- devCtrl, discoveryTimeoutMsec); ++ CHIP_ERROR err = sPairingDeviceDiscoveryDelegate.Init(nodeId, setupPasscode, sCommissioningParameters, pairingDelegate, devCtrl, ++ discoveryTimeoutMsec); + VerifyOrReturnError(err == CHIP_NO_ERROR, ToPyChipError(err)); + return ToPyChipError(devCtrl->DiscoverCommissionableNodes(filter)); + } +@@ -587,7 +593,6 @@ PyChipError pychip_DeviceController_EstablishPASESessionIP(chip::Controller::Dev + addr.SetPort(port); + } + params.SetPeerAddress(addr).SetDiscriminator(0); +- sPairingDelegate.SetExpectingPairingComplete(true); + return ToPyChipError(devCtrl->EstablishPASEConnection(nodeid, params)); + } + +@@ -598,14 +603,12 @@ PyChipError pychip_DeviceController_EstablishPASESessionBLE(chip::Controller::De + RendezvousParameters params = chip::RendezvousParameters().SetSetupPINCode(setupPINCode); + addr.SetTransportType(chip::Transport::Type::kBle); + params.SetPeerAddress(addr).SetDiscriminator(discriminator); +- sPairingDelegate.SetExpectingPairingComplete(true); + return ToPyChipError(devCtrl->EstablishPASEConnection(nodeid, params)); + } + + PyChipError pychip_DeviceController_EstablishPASESession(chip::Controller::DeviceCommissioner * devCtrl, const char * setUpCode, + chip::NodeId nodeid) + { +- sPairingDelegate.SetExpectingPairingComplete(true); + return ToPyChipError(devCtrl->EstablishPASEConnection(nodeid, setUpCode)); + } + +@@ -656,15 +659,17 @@ PyChipError pychip_DeviceController_DiscoverCommissionableNodesCommissioningEnab + } + + PyChipError pychip_ScriptDevicePairingDelegate_SetOpenWindowCompleteCallback( +- chip::Controller::DeviceCommissioner * devCtrl, chip::Controller::DevicePairingDelegate_OnWindowOpenCompleteFunct callback) ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ chip::Controller::DevicePairingDelegate_OnWindowOpenCompleteFunct callback) + { +- sPairingDelegate.SetCommissioningWindowOpenCallback(callback); ++ pairingDelegate->SetCommissioningWindowOpenCallback(callback); + return ToPyChipError(CHIP_NO_ERROR); + } + +-PyChipError pychip_DeviceController_OpenCommissioningWindow(chip::Controller::DeviceCommissioner * devCtrl, chip::NodeId nodeid, +- uint16_t timeout, uint32_t iteration, uint16_t discriminator, +- uint8_t optionInt) ++PyChipError pychip_DeviceController_OpenCommissioningWindow(chip::Controller::DeviceCommissioner * devCtrl, ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ chip::NodeId nodeid, uint16_t timeout, uint32_t iteration, ++ uint16_t discriminator, uint8_t optionInt) + { + const auto option = static_cast(optionInt); + if (option == Controller::CommissioningWindowOpener::CommissioningWindowOption::kOriginalSetupCode) +@@ -680,7 +685,7 @@ PyChipError pychip_DeviceController_OpenCommissioningWindow(chip::Controller::De + Platform::New(static_cast(devCtrl)); + PyChipError err = ToPyChipError(opener->OpenCommissioningWindow(nodeid, System::Clock::Seconds16(timeout), iteration, + discriminator, NullOptional, NullOptional, +- sPairingDelegate.GetOpenWindowCallback(opener), payload)); ++ pairingDelegate->GetOpenWindowCallback(opener), payload)); + return err; + } + +@@ -688,32 +693,42 @@ PyChipError pychip_DeviceController_OpenCommissioningWindow(chip::Controller::De + } + + PyChipError +-pychip_ScriptDevicePairingDelegate_SetKeyExchangeCallback(chip::Controller::DeviceCommissioner * devCtrl, ++pychip_ScriptDevicePairingDelegate_SetKeyExchangeCallback(chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, + chip::Controller::DevicePairingDelegate_OnPairingCompleteFunct callback) + { +- sPairingDelegate.SetKeyExchangeCallback(callback); ++ pairingDelegate->SetKeyExchangeCallback(callback); + return ToPyChipError(CHIP_NO_ERROR); + } + + PyChipError pychip_ScriptDevicePairingDelegate_SetCommissioningCompleteCallback( +- chip::Controller::DeviceCommissioner * devCtrl, chip::Controller::DevicePairingDelegate_OnCommissioningCompleteFunct callback) ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ chip::Controller::DevicePairingDelegate_OnCommissioningCompleteFunct callback) + { +- sPairingDelegate.SetCommissioningCompleteCallback(callback); ++ pairingDelegate->SetCommissioningCompleteCallback(callback); + return ToPyChipError(CHIP_NO_ERROR); + } + + PyChipError pychip_ScriptDevicePairingDelegate_SetCommissioningStatusUpdateCallback( +- chip::Controller::DeviceCommissioner * devCtrl, ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, + chip::Controller::DevicePairingDelegate_OnCommissioningStatusUpdateFunct callback) + { +- sPairingDelegate.SetCommissioningStatusUpdateCallback(callback); ++ pairingDelegate->SetCommissioningStatusUpdateCallback(callback); ++ return ToPyChipError(CHIP_NO_ERROR); ++} ++ ++PyChipError ++pychip_ScriptDevicePairingDelegate_SetFabricCheckCallback(chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ chip::Controller::DevicePairingDelegate_OnFabricCheckFunct callback) ++{ ++ pairingDelegate->SetFabricCheckCallback(callback); + return ToPyChipError(CHIP_NO_ERROR); + } + + PyChipError +-pychip_ScriptDevicePairingDelegate_SetFabricCheckCallback(chip::Controller::DevicePairingDelegate_OnFabricCheckFunct callback) ++pychip_ScriptDevicePairingDelegate_SetExpectingPairingComplete(chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ bool value) + { +- sPairingDelegate.SetFabricCheckCallback(callback); ++ pairingDelegate->SetExpectingPairingComplete(value); + return ToPyChipError(CHIP_NO_ERROR); + } + +diff --git a/src/controller/python/OpCredsBinding.cpp b/src/controller/python/OpCredsBinding.cpp +index 79995d133b..66a6c84184 100644 +--- a/src/controller/python/OpCredsBinding.cpp ++++ b/src/controller/python/OpCredsBinding.cpp +@@ -402,12 +402,11 @@ void pychip_OnCommissioningStatusUpdate(chip::PeerId peerId, chip::Controller::C + * TODO(#25214): Need clean up API + * + */ +-PyChipError pychip_OpCreds_AllocateControllerForPythonCommissioningFLow(chip::Controller::DeviceCommissioner ** outDevCtrl, +- chip::python::pychip_P256Keypair * operationalKey, +- uint8_t * noc, uint32_t nocLen, uint8_t * icac, +- uint32_t icacLen, uint8_t * rcac, uint32_t rcacLen, +- const uint8_t * ipk, uint32_t ipkLen, +- chip::VendorId adminVendorId, bool enableServerInteractions) ++PyChipError pychip_OpCreds_AllocateControllerForPythonCommissioningFLow( ++ chip::Controller::DeviceCommissioner ** outDevCtrl, chip::Controller::ScriptDevicePairingDelegate ** outPairingDelegate, ++ chip::python::pychip_P256Keypair * operationalKey, uint8_t * noc, uint32_t nocLen, uint8_t * icac, uint32_t icacLen, ++ uint8_t * rcac, uint32_t rcacLen, const uint8_t * ipk, uint32_t ipkLen, chip::VendorId adminVendorId, ++ bool enableServerInteractions) + { + ReturnErrorCodeIf(nocLen > Controller::kMaxCHIPDERCertLength, ToPyChipError(CHIP_ERROR_NO_MEMORY)); + ReturnErrorCodeIf(icacLen > Controller::kMaxCHIPDERCertLength, ToPyChipError(CHIP_ERROR_NO_MEMORY)); +@@ -415,11 +414,13 @@ PyChipError pychip_OpCreds_AllocateControllerForPythonCommissioningFLow(chip::Co + + ChipLogDetail(Controller, "Creating New Device Controller"); + ++ auto pairingDelegate = std::make_unique(); ++ VerifyOrReturnError(pairingDelegate != nullptr, ToPyChipError(CHIP_ERROR_NO_MEMORY)); + auto devCtrl = std::make_unique(); + VerifyOrReturnError(devCtrl != nullptr, ToPyChipError(CHIP_ERROR_NO_MEMORY)); + + Controller::SetupParams initParams; +- initParams.pairingDelegate = &sPairingDelegate; ++ initParams.pairingDelegate = pairingDelegate.get(); + initParams.operationalCredentialsDelegate = &sPlaceholderOperationalCredentialsIssuer; + initParams.operationalKeypair = operationalKey; + initParams.controllerRCAC = ByteSpan(rcac, rcacLen); +@@ -450,13 +451,15 @@ PyChipError pychip_OpCreds_AllocateControllerForPythonCommissioningFLow(chip::Co + chip::Credentials::SetSingleIpkEpochKey(&sGroupDataProvider, devCtrl->GetFabricIndex(), fabricIpk, compressedFabricIdSpan); + VerifyOrReturnError(err == CHIP_NO_ERROR, ToPyChipError(err)); + +- *outDevCtrl = devCtrl.release(); ++ *outDevCtrl = devCtrl.release(); ++ *outPairingDelegate = pairingDelegate.release(); + + return ToPyChipError(CHIP_NO_ERROR); + } + + // TODO(#25214): Need clean up API + PyChipError pychip_OpCreds_AllocateController(OpCredsContext * context, chip::Controller::DeviceCommissioner ** outDevCtrl, ++ chip::Controller::ScriptDevicePairingDelegate ** outPairingDelegate, + FabricId fabricId, chip::NodeId nodeId, chip::VendorId adminVendorId, + const char * paaTrustStorePath, bool useTestCommissioner, + bool enableServerInteractions, CASEAuthTag * caseAuthTags, uint32_t caseAuthTagLen, +@@ -468,6 +471,8 @@ PyChipError pychip_OpCreds_AllocateController(OpCredsContext * context, chip::Co + + VerifyOrReturnError(context != nullptr, ToPyChipError(CHIP_ERROR_INVALID_ARGUMENT)); + ++ auto pairingDelegate = std::make_unique(); ++ VerifyOrReturnError(pairingDelegate != nullptr, ToPyChipError(CHIP_ERROR_NO_MEMORY)); + auto devCtrl = std::make_unique(); + VerifyOrReturnError(devCtrl != nullptr, ToPyChipError(CHIP_ERROR_NO_MEMORY)); + +@@ -524,7 +529,7 @@ PyChipError pychip_OpCreds_AllocateController(OpCredsContext * context, chip::Co + VerifyOrReturnError(err == CHIP_NO_ERROR, ToPyChipError(err)); + + Controller::SetupParams initParams; +- initParams.pairingDelegate = &sPairingDelegate; ++ initParams.pairingDelegate = pairingDelegate.get(); + initParams.operationalCredentialsDelegate = context->mAdapter.get(); + initParams.operationalKeypair = controllerKeyPair; + initParams.controllerRCAC = rcacSpan; +@@ -538,9 +543,9 @@ PyChipError pychip_OpCreds_AllocateController(OpCredsContext * context, chip::Co + if (useTestCommissioner) + { + initParams.defaultCommissioner = &sTestCommissioner; +- sPairingDelegate.SetCommissioningSuccessCallback(pychip_OnCommissioningSuccess); +- sPairingDelegate.SetCommissioningFailureCallback(pychip_OnCommissioningFailure); +- sPairingDelegate.SetCommissioningStatusUpdateCallback(pychip_OnCommissioningStatusUpdate); ++ pairingDelegate->SetCommissioningSuccessCallback(pychip_OnCommissioningSuccess); ++ pairingDelegate->SetCommissioningFailureCallback(pychip_OnCommissioningFailure); ++ pairingDelegate->SetCommissioningStatusUpdateCallback(pychip_OnCommissioningStatusUpdate); + } + + err = Controller::DeviceControllerFactory::GetInstance().SetupCommissioner(initParams, *devCtrl); +@@ -562,7 +567,8 @@ PyChipError pychip_OpCreds_AllocateController(OpCredsContext * context, chip::Co + chip::Credentials::SetSingleIpkEpochKey(&sGroupDataProvider, devCtrl->GetFabricIndex(), defaultIpk, compressedFabricIdSpan); + VerifyOrReturnError(err == CHIP_NO_ERROR, ToPyChipError(err)); + +- *outDevCtrl = devCtrl.release(); ++ *outDevCtrl = devCtrl.release(); ++ *outPairingDelegate = pairingDelegate.release(); + + return ToPyChipError(CHIP_NO_ERROR); + } +@@ -596,7 +602,8 @@ void pychip_OpCreds_FreeDelegate(OpCredsContext * context) + Platform::Delete(context); + } + +-PyChipError pychip_DeviceController_DeleteDeviceController(chip::Controller::DeviceCommissioner * devCtrl) ++PyChipError pychip_DeviceController_DeleteDeviceController(chip::Controller::DeviceCommissioner * devCtrl, ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate) + { + if (devCtrl != nullptr) + { +@@ -604,6 +611,11 @@ PyChipError pychip_DeviceController_DeleteDeviceController(chip::Controller::Dev + delete devCtrl; + } + ++ if (pairingDelegate != nullptr) ++ { ++ delete pairingDelegate; ++ } ++ + return ToPyChipError(CHIP_NO_ERROR); + } + +diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py +index b3d0aa2d7f..9cbd7a32d2 100644 +--- a/src/controller/python/chip/ChipDeviceCtrl.py ++++ b/src/controller/python/chip/ChipDeviceCtrl.py +@@ -253,8 +253,10 @@ class ChipDeviceControllerBase(): + + self._InitLib() + ++ pairingDelegate = c_void_p(None) + devCtrl = c_void_p(None) + ++ self.pairingDelegate = pairingDelegate + self.devCtrl = devCtrl + self.name = name + self.fabricCheckNodeId = -1 +@@ -263,7 +265,7 @@ class ChipDeviceControllerBase(): + self._Cluster = ChipClusters(builtins.chipStack) + self._Cluster.InitLib(self._dmLib) + +- def _set_dev_ctrl(self, devCtrl): ++ def _set_dev_ctrl(self, devCtrl, pairingDelegate): + def HandleCommissioningComplete(nodeid, err): + if err.is_success: + logging.info("Commissioning complete") +@@ -321,25 +323,26 @@ class ChipDeviceControllerBase(): + if not err.is_success: + HandleCommissioningComplete(0, err) + ++ self.pairingDelegate = pairingDelegate + self.devCtrl = devCtrl + + self.cbHandlePASEEstablishmentCompleteFunct = _DevicePairingDelegate_OnPairingCompleteFunct( + HandlePASEEstablishmentComplete) + self._dmLib.pychip_ScriptDevicePairingDelegate_SetKeyExchangeCallback( +- self.devCtrl, self.cbHandlePASEEstablishmentCompleteFunct) ++ self.pairingDelegate, self.cbHandlePASEEstablishmentCompleteFunct) + + self.cbHandleCommissioningCompleteFunct = _DevicePairingDelegate_OnCommissioningCompleteFunct( + HandleCommissioningComplete) + self._dmLib.pychip_ScriptDevicePairingDelegate_SetCommissioningCompleteCallback( +- self.devCtrl, self.cbHandleCommissioningCompleteFunct) ++ self.pairingDelegate, self.cbHandleCommissioningCompleteFunct) + + self.cbHandleFabricCheckFunct = _DevicePairingDelegate_OnFabricCheckFunct(HandleFabricCheck) +- self._dmLib.pychip_ScriptDevicePairingDelegate_SetFabricCheckCallback(self.cbHandleFabricCheckFunct) ++ self._dmLib.pychip_ScriptDevicePairingDelegate_SetFabricCheckCallback(self.pairingDelegate, self.cbHandleFabricCheckFunct) + + self.cbHandleOpenWindowCompleteFunct = _DevicePairingDelegate_OnOpenWindowCompleteFunct( + HandleOpenWindowComplete) + self._dmLib.pychip_ScriptDevicePairingDelegate_SetOpenWindowCompleteCallback( +- self.devCtrl, self.cbHandleOpenWindowCompleteFunct) ++ self.pairingDelegate, self.cbHandleOpenWindowCompleteFunct) + + self.cbHandleDeviceUnpairCompleteFunct = _DeviceUnpairingCompleteFunct(HandleUnpairDeviceComplete) + +@@ -355,6 +358,11 @@ class ChipDeviceControllerBase(): + + ChipDeviceController.activeList.add(self) + ++ def _enablePairingCompeleteCallback(self, value: bool): ++ self._ChipStack.Call( ++ lambda: self._dmLib.pychip_ScriptDevicePairingDelegate_SetExpectingPairingComplete(self.pairingDelegate, value) ++ ).raise_on_error() ++ + @property + def fabricAdmin(self) -> FabricAdmin.FabricAdmin: + return self._fabricAdmin +@@ -389,8 +397,9 @@ class ChipDeviceControllerBase(): + if self.devCtrl is not None: + self._ChipStack.Call( + lambda: self._dmLib.pychip_DeviceController_DeleteDeviceController( +- self.devCtrl) ++ self.devCtrl, self.pairingDelegate) + ).raise_on_error() ++ self.pairingDelegate = None + self.devCtrl = None + + ChipDeviceController.activeList.remove(self) +@@ -437,6 +446,7 @@ class ChipDeviceControllerBase(): + self._ChipStack.commissioningCompleteEvent.clear() + + self.state = DCState.COMMISSIONING ++ self._enablePairingCompeleteCallback(True) + self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_ConnectBLE( + self.devCtrl, discriminator, setupPinCode, nodeid) +@@ -487,6 +497,7 @@ class ChipDeviceControllerBase(): + self.CheckIsActive() + + self.state = DCState.RENDEZVOUS_ONGOING ++ self._enablePairingCompeleteCallback(True) + return self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_EstablishPASESessionBLE( + self.devCtrl, setupPinCode, discriminator, nodeid) +@@ -496,6 +507,7 @@ class ChipDeviceControllerBase(): + self.CheckIsActive() + + self.state = DCState.RENDEZVOUS_ONGOING ++ self._enablePairingCompeleteCallback(True) + return self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_EstablishPASESessionIP( + self.devCtrl, ipaddr.encode("utf-8"), setupPinCode, nodeid, port) +@@ -505,6 +517,7 @@ class ChipDeviceControllerBase(): + self.CheckIsActive() + + self.state = DCState.RENDEZVOUS_ONGOING ++ self._enablePairingCompeleteCallback(True) + return self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_EstablishPASESession( + self.devCtrl, setUpCode.encode("utf-8"), nodeid) +@@ -726,7 +739,7 @@ class ChipDeviceControllerBase(): + self.CheckIsActive() + self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_OpenCommissioningWindow( +- self.devCtrl, nodeid, timeout, iteration, discriminator, option) ++ self.devCtrl, self.pairingDelegate, nodeid, timeout, iteration, discriminator, option) + ).raise_on_error() + self._ChipStack.callbackRes.raise_on_error() + return self._ChipStack.openCommissioningWindowPincode[nodeid] +@@ -1565,16 +1578,13 @@ class ChipDeviceControllerBase(): + self._dmLib = CDLL(self._ChipStack.LocateChipDLL()) + + self._dmLib.pychip_DeviceController_DeleteDeviceController.argtypes = [ +- c_void_p] ++ c_void_p, c_void_p] + self._dmLib.pychip_DeviceController_DeleteDeviceController.restype = PyChipError + + self._dmLib.pychip_DeviceController_ConnectBLE.argtypes = [ + c_void_p, c_uint16, c_uint32, c_uint64] + self._dmLib.pychip_DeviceController_ConnectBLE.restype = PyChipError + +- self._dmLib.pychip_DeviceController_ConnectIP.argtypes = [ +- c_void_p, c_char_p, c_uint32, c_uint64] +- + self._dmLib.pychip_DeviceController_SetThreadOperationalDataset.argtypes = [ + c_char_p, c_uint32] + self._dmLib.pychip_DeviceController_SetThreadOperationalDataset.restype = PyChipError +@@ -1610,7 +1620,7 @@ class ChipDeviceControllerBase(): + self._dmLib.pychip_DeviceController_Commission.restype = PyChipError + + self._dmLib.pychip_DeviceController_OnNetworkCommission.argtypes = [ +- c_void_p, c_uint64, c_uint32, c_uint8, c_char_p, c_uint32] ++ c_void_p, c_void_p, c_uint64, c_uint32, c_uint8, c_char_p, c_uint32] + self._dmLib.pychip_DeviceController_OnNetworkCommission.restype = PyChipError + + self._dmLib.pychip_DeviceController_DiscoverCommissionableNodes.argtypes = [ +@@ -1648,6 +1658,7 @@ class ChipDeviceControllerBase(): + self._dmLib.pychip_DeviceController_EstablishPASESessionBLE.argtypes = [ + c_void_p, c_uint32, c_uint16, c_uint64] + self._dmLib.pychip_DeviceController_EstablishPASESessionBLE.restype = PyChipError ++ + self._dmLib.pychip_DeviceController_EstablishPASESession.argtypes = [ + c_void_p, c_char_p, c_uint64] + self._dmLib.pychip_DeviceController_EstablishPASESession.restype = PyChipError +@@ -1655,10 +1666,12 @@ class ChipDeviceControllerBase(): + self._dmLib.pychip_DeviceController_DiscoverAllCommissionableNodes.argtypes = [ + c_void_p] + self._dmLib.pychip_DeviceController_DiscoverAllCommissionableNodes.restype = PyChipError ++ + self._dmLib.pychip_DeviceController_PrintDiscoveredDevices.argtypes = [ + c_void_p] + self._dmLib.pychip_DeviceController_PrintDiscoveredDevices.argtypes = [ + c_void_p, _ChipDeviceController_IterateDiscoveredCommissionableNodesFunct] ++ + self._dmLib.pychip_DeviceController_HasDiscoveredCommissionableNode.argtypes = [c_void_p] + self._dmLib.pychip_DeviceController_HasDiscoveredCommissionableNode.restype = c_bool + +@@ -1703,9 +1716,13 @@ class ChipDeviceControllerBase(): + self._dmLib.pychip_ScriptDevicePairingDelegate_SetCommissioningStatusUpdateCallback.restype = PyChipError + + self._dmLib.pychip_ScriptDevicePairingDelegate_SetFabricCheckCallback.argtypes = [ +- _DevicePairingDelegate_OnFabricCheckFunct] ++ c_void_p, _DevicePairingDelegate_OnFabricCheckFunct] + self._dmLib.pychip_ScriptDevicePairingDelegate_SetFabricCheckCallback.restype = PyChipError + ++ self._dmLib.pychip_ScriptDevicePairingDelegate_SetExpectingPairingComplete.argtypes = [ ++ c_void_p, c_bool] ++ self._dmLib.pychip_ScriptDevicePairingDelegate_SetExpectingPairingComplete.restype = PyChipError ++ + self._dmLib.pychip_GetConnectedDeviceByNodeId.argtypes = [ + c_void_p, c_uint64, py_object, _DeviceAvailableCallbackFunct] + self._dmLib.pychip_GetConnectedDeviceByNodeId.restype = PyChipError +@@ -1733,8 +1750,9 @@ class ChipDeviceControllerBase(): + self._dmLib.pychip_DeviceController_GetCompressedFabricId.restype = PyChipError + + self._dmLib.pychip_DeviceController_OpenCommissioningWindow.argtypes = [ +- c_void_p, c_uint64, c_uint16, c_uint32, c_uint16, c_uint8] ++ c_void_p, c_void_p, c_uint64, c_uint16, c_uint32, c_uint16, c_uint8] + self._dmLib.pychip_DeviceController_OpenCommissioningWindow.restype = PyChipError ++ + self._dmLib.pychip_TestCommissionerUsed.argtypes = [] + self._dmLib.pychip_TestCommissionerUsed.restype = c_bool + +@@ -1750,6 +1768,7 @@ class ChipDeviceControllerBase(): + self._dmLib.pychip_SetTestCommissionerSimulateFailureOnStage.argtypes = [ + c_uint8] + self._dmLib.pychip_SetTestCommissionerSimulateFailureOnStage.restype = c_bool ++ + self._dmLib.pychip_SetTestCommissionerSimulateFailureOnReport.argtypes = [ + c_uint8] + self._dmLib.pychip_SetTestCommissionerSimulateFailureOnReport.restype = c_bool +@@ -1762,8 +1781,7 @@ class ChipDeviceControllerBase(): + self._dmLib.pychip_GetCompletionError.restype = PyChipError + + self._dmLib.pychip_DeviceController_IssueNOCChain.argtypes = [ +- c_void_p, py_object, c_char_p, c_size_t, c_uint64 +- ] ++ c_void_p, py_object, c_char_p, c_size_t, c_uint64] + self._dmLib.pychip_DeviceController_IssueNOCChain.restype = PyChipError + + self._dmLib.pychip_OpCreds_InitGroupTestingData.argtypes = [ +@@ -1784,11 +1802,11 @@ class ChipDeviceControllerBase(): + self._dmLib.pychip_DeviceController_GetLogFilter = c_uint8 + + self._dmLib.pychip_OpCreds_AllocateController.argtypes = [c_void_p, POINTER( +- c_void_p), c_uint64, c_uint64, c_uint16, c_char_p, c_bool, c_bool, POINTER(c_uint32), c_uint32, c_void_p] ++ c_void_p), POINTER(c_void_p), c_uint64, c_uint64, c_uint16, c_char_p, c_bool, c_bool, POINTER(c_uint32), c_uint32, c_void_p] + self._dmLib.pychip_OpCreds_AllocateController.restype = PyChipError + + self._dmLib.pychip_OpCreds_AllocateControllerForPythonCommissioningFLow.argtypes = [ +- POINTER(c_void_p), c_void_p, POINTER(c_char), c_uint32, POINTER(c_char), c_uint32, POINTER(c_char), c_uint32, POINTER(c_char), c_uint32, c_uint16, c_bool] ++ POINTER(c_void_p), POINTER(c_void_p), c_void_p, POINTER(c_char), c_uint32, POINTER(c_char), c_uint32, POINTER(c_char), c_uint32, POINTER(c_char), c_uint32, c_uint16, c_bool] + self._dmLib.pychip_OpCreds_AllocateControllerForPythonCommissioningFLow.restype = PyChipError + + self._dmLib.pychip_DeviceController_SetIpk.argtypes = [c_void_p, POINTER(c_char), c_size_t] +@@ -1810,6 +1828,7 @@ class ChipDeviceController(ChipDeviceControllerBase): + + self._dmLib.pychip_DeviceController_SetIssueNOCChainCallbackPythonCallback(_IssueNOCChainCallbackPythonCallback) + ++ pairingDelegate = c_void_p(None) + devCtrl = c_void_p(None) + + c_catTags = (c_uint32 * len(catTags))() +@@ -1821,7 +1840,7 @@ class ChipDeviceController(ChipDeviceControllerBase): + self._externalKeyPair = keypair + self._ChipStack.Call( + lambda: self._dmLib.pychip_OpCreds_AllocateController(c_void_p( +- opCredsContext), pointer(devCtrl), fabricId, nodeId, adminVendorId, c_char_p(None if len(paaTrustStorePath) == 0 else str.encode(paaTrustStorePath)), useTestCommissioner, self._ChipStack.enableServerInteractions, c_catTags, len(catTags), None if keypair is None else keypair.native_object) ++ opCredsContext), pointer(devCtrl), pointer(pairingDelegate), fabricId, nodeId, adminVendorId, c_char_p(None if len(paaTrustStorePath) == 0 else str.encode(paaTrustStorePath)), useTestCommissioner, self._ChipStack.enableServerInteractions, c_catTags, len(catTags), None if keypair is None else keypair.native_object) + ).raise_on_error() + + self._fabricAdmin = fabricAdmin +@@ -1829,7 +1848,7 @@ class ChipDeviceController(ChipDeviceControllerBase): + self._nodeId = nodeId + self._caIndex = fabricAdmin.caIndex + +- self._set_dev_ctrl(devCtrl=devCtrl) ++ self._set_dev_ctrl(devCtrl=devCtrl, pairingDelegate=pairingDelegate) + + self._finish_init() + +@@ -1975,9 +1994,10 @@ class ChipDeviceController(ChipDeviceControllerBase): + + self._ChipStack.commissioningCompleteEvent.clear() + ++ self._enablePairingCompeleteCallback(True) + self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_OnNetworkCommission( +- self.devCtrl, nodeId, setupPinCode, int(filterType), str(filter).encode("utf-8") + b"\x00" if filter is not None else None, discoveryTimeoutMsec) ++ self.devCtrl, self.pairingDelegate, nodeId, setupPinCode, int(filterType), str(filter).encode("utf-8") + b"\x00" if filter is not None else None, discoveryTimeoutMsec) + ) + if not self._ChipStack.commissioningCompleteEvent.isSet(): + # Error 50 is a timeout +@@ -1998,6 +2018,7 @@ class ChipDeviceController(ChipDeviceControllerBase): + + self._ChipStack.commissioningCompleteEvent.clear() + ++ self._enablePairingCompeleteCallback(True) + self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_ConnectWithCode( + self.devCtrl, setupPayload, nodeid, discoveryType.value) +@@ -2017,6 +2038,7 @@ class ChipDeviceController(ChipDeviceControllerBase): + + self._ChipStack.commissioningCompleteEvent.clear() + ++ self._enablePairingCompeleteCallback(True) + self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_ConnectIP( + self.devCtrl, ipaddr.encode("utf-8"), setupPinCode, nodeid) +@@ -2061,6 +2083,7 @@ class BareChipDeviceController(ChipDeviceControllerBase): + ''' + super().__init__(name or f"ctrl(v/{adminVendorId})") + ++ pairingDelegate = c_void_p(None) + devCtrl = c_void_p(None) + + # Device should hold a reference to the key to avoid it being GC-ed. +@@ -2069,9 +2092,9 @@ class BareChipDeviceController(ChipDeviceControllerBase): + + self._ChipStack.Call( + lambda: self._dmLib.pychip_OpCreds_AllocateControllerForPythonCommissioningFLow( +- c_void_p(devCtrl), nativeKey, noc, len(noc), icac, len(icac) if icac else 0, rcac, len(rcac), ipk, len(ipk) if ipk else 0, adminVendorId, self._ChipStack.enableServerInteractions) ++ c_void_p(devCtrl), c_void_p(pairingDelegate), nativeKey, noc, len(noc), icac, len(icac) if icac else 0, rcac, len(rcac), ipk, len(ipk) if ipk else 0, adminVendorId, self._ChipStack.enableServerInteractions) + ).raise_on_error() + +- self._set_dev_ctrl(devCtrl) ++ self._set_dev_ctrl(devCtrl, pairingDelegate) + + self._finish_init() +-- +2.45.2 + diff --git a/0011-Python-Call-SDK-asyncio-friendly-32764.patch b/0011-Python-Call-SDK-asyncio-friendly-32764.patch new file mode 100644 index 0000000..d70ef52 --- /dev/null +++ b/0011-Python-Call-SDK-asyncio-friendly-32764.patch @@ -0,0 +1,359 @@ +From 56b56ee08006d935cb86dd3fb38e96d79c78bb45 Mon Sep 17 00:00:00 2001 +From: Stefan Agner +Date: Thu, 16 May 2024 09:35:25 +0200 +Subject: [PATCH] [Python] Call SDK asyncio friendly (#32764) + +* [Python] Rename CallAsync to CallAsyncWithCallback + +CallAsync continuously calls a callback function during the wait +for the call. Rename the function to reflect that fact. + +This frees up CallAsync for an asyncio friendly implementation. + +* [Python] Implement asyncio variant of CallAsync + +Call Matter SDK in a asyncio friendly way. During posting of the task +onto the CHIP mainloop, it makes sure that the asyncio loop is not +blocked. + +* [Python] Use CallAsync where appropriate + +* Rename AsyncSimpleCallableHandle to AsyncioCallableHandle + +* Rename CallAsyncWithCallback to CallAsyncWithCompleteCallback + +Also add a comment that the function needs to be released by registering +a callback and setting the complete event. + +* Add comments about lock +--- + src/controller/python/chip/ChipDeviceCtrl.py | 43 ++++++++------- + src/controller/python/chip/ChipStack.py | 53 ++++++++++++++++++- + .../python/chip/clusters/Command.py | 16 +++--- + 3 files changed, 83 insertions(+), 29 deletions(-) + +diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py +index 9cbd7a32d2..3282ffd191 100644 +--- a/src/controller/python/chip/ChipDeviceCtrl.py ++++ b/src/controller/python/chip/ChipDeviceCtrl.py +@@ -186,7 +186,7 @@ class DeviceProxyWrapper(): + def __del__(self): + if (self._dmLib is not None and hasattr(builtins, 'chipStack') and builtins.chipStack is not None): + # This destructor is called from any threading context, including on the Matter threading context. +- # So, we cannot call chipStack.Call or chipStack.CallAsync which waits for the posted work to ++ # So, we cannot call chipStack.Call or chipStack.CallAsyncWithCompleteCallback which waits for the posted work to + # actually be executed. Instead, we just post/schedule the work and move on. + builtins.chipStack.PostTaskOnChipThread(lambda: self._dmLib.pychip_FreeOperationalDeviceProxy(self._deviceProxy)) + +@@ -447,7 +447,7 @@ class ChipDeviceControllerBase(): + + self.state = DCState.COMMISSIONING + self._enablePairingCompeleteCallback(True) +- self._ChipStack.CallAsync( ++ self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_ConnectBLE( + self.devCtrl, discriminator, setupPinCode, nodeid) + ).raise_on_error() +@@ -459,7 +459,7 @@ class ChipDeviceControllerBase(): + def UnpairDevice(self, nodeid: int): + self.CheckIsActive() + +- return self._ChipStack.CallAsync( ++ return self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_UnpairDevice( + self.devCtrl, nodeid, self.cbHandleDeviceUnpairCompleteFunct) + ).raise_on_error() +@@ -498,7 +498,7 @@ class ChipDeviceControllerBase(): + + self.state = DCState.RENDEZVOUS_ONGOING + self._enablePairingCompeleteCallback(True) +- return self._ChipStack.CallAsync( ++ return self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_EstablishPASESessionBLE( + self.devCtrl, setupPinCode, discriminator, nodeid) + ) +@@ -508,7 +508,7 @@ class ChipDeviceControllerBase(): + + self.state = DCState.RENDEZVOUS_ONGOING + self._enablePairingCompeleteCallback(True) +- return self._ChipStack.CallAsync( ++ return self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_EstablishPASESessionIP( + self.devCtrl, ipaddr.encode("utf-8"), setupPinCode, nodeid, port) + ) +@@ -518,7 +518,7 @@ class ChipDeviceControllerBase(): + + self.state = DCState.RENDEZVOUS_ONGOING + self._enablePairingCompeleteCallback(True) +- return self._ChipStack.CallAsync( ++ return self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_EstablishPASESession( + self.devCtrl, setUpCode.encode("utf-8"), nodeid) + ) +@@ -737,7 +737,7 @@ class ChipDeviceControllerBase(): + Returns CommissioningParameters + ''' + self.CheckIsActive() +- self._ChipStack.CallAsync( ++ self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_OpenCommissioningWindow( + self.devCtrl, self.pairingDelegate, nodeid, timeout, iteration, discriminator, option) + ).raise_on_error() +@@ -842,7 +842,7 @@ class ChipDeviceControllerBase(): + + if allowPASE: + returnDevice = c_void_p(None) +- res = self._ChipStack.Call(lambda: self._dmLib.pychip_GetDeviceBeingCommissioned( ++ res = await self._ChipStack.CallAsync(lambda: self._dmLib.pychip_GetDeviceBeingCommissioned( + self.devCtrl, nodeid, byref(returnDevice)), timeoutMs) + if res.is_success: + logging.info('Using PASE connection') +@@ -872,11 +872,12 @@ class ChipDeviceControllerBase(): + + closure = DeviceAvailableClosure(eventLoop, future) + ctypes.pythonapi.Py_IncRef(ctypes.py_object(closure)) +- self._ChipStack.Call(lambda: self._dmLib.pychip_GetConnectedDeviceByNodeId( ++ res = await self._ChipStack.CallAsync(lambda: self._dmLib.pychip_GetConnectedDeviceByNodeId( + self.devCtrl, nodeid, ctypes.py_object(closure), _DeviceAvailableCallback), +- timeoutMs).raise_on_error() ++ timeoutMs) ++ res.raise_on_error() + +- # The callback might have been received synchronously (during self._ChipStack.Call()). ++ # The callback might have been received synchronously (during self._ChipStack.CallAsync()). + # In that case the Future has already been set it will return immediately + if (timeoutMs): + timeout = float(timeoutMs) / 1000 +@@ -1004,13 +1005,14 @@ class ChipDeviceControllerBase(): + future = eventLoop.create_future() + + device = await self.GetConnectedDevice(nodeid, timeoutMs=interactionTimeoutMs) +- ClusterCommand.SendCommand( ++ res = await ClusterCommand.SendCommand( + future, eventLoop, responseType, device.deviceProxy, ClusterCommand.CommandPath( + EndpointId=endpoint, + ClusterId=payload.cluster_id, + CommandId=payload.command_id, + ), payload, timedRequestTimeoutMs=timedRequestTimeoutMs, +- interactionTimeoutMs=interactionTimeoutMs, busyWaitMs=busyWaitMs, suppressResponse=suppressResponse).raise_on_error() ++ interactionTimeoutMs=interactionTimeoutMs, busyWaitMs=busyWaitMs, suppressResponse=suppressResponse) ++ res.raise_on_error() + return await future + + async def SendBatchCommands(self, nodeid: int, commands: typing.List[ClusterCommand.InvokeRequestInfo], +@@ -1046,10 +1048,11 @@ class ChipDeviceControllerBase(): + + device = await self.GetConnectedDevice(nodeid, timeoutMs=interactionTimeoutMs) + +- ClusterCommand.SendBatchCommands( ++ res = await ClusterCommand.SendBatchCommands( + future, eventLoop, device.deviceProxy, commands, + timedRequestTimeoutMs=timedRequestTimeoutMs, +- interactionTimeoutMs=interactionTimeoutMs, busyWaitMs=busyWaitMs, suppressResponse=suppressResponse).raise_on_error() ++ interactionTimeoutMs=interactionTimeoutMs, busyWaitMs=busyWaitMs, suppressResponse=suppressResponse) ++ res.raise_on_error() + return await future + + def SendGroupCommand(self, groupid: int, payload: ClusterObjects.ClusterCommand, busyWaitMs: typing.Union[None, int] = None): +@@ -1879,7 +1882,7 @@ class ChipDeviceController(ChipDeviceControllerBase): + self._ChipStack.commissioningCompleteEvent.clear() + self.state = DCState.COMMISSIONING + +- self._ChipStack.CallAsync( ++ self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_Commission( + self.devCtrl, nodeid) + ) +@@ -1995,7 +1998,7 @@ class ChipDeviceController(ChipDeviceControllerBase): + self._ChipStack.commissioningCompleteEvent.clear() + + self._enablePairingCompeleteCallback(True) +- self._ChipStack.CallAsync( ++ self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_OnNetworkCommission( + self.devCtrl, self.pairingDelegate, nodeId, setupPinCode, int(filterType), str(filter).encode("utf-8") + b"\x00" if filter is not None else None, discoveryTimeoutMsec) + ) +@@ -2019,7 +2022,7 @@ class ChipDeviceController(ChipDeviceControllerBase): + self._ChipStack.commissioningCompleteEvent.clear() + + self._enablePairingCompeleteCallback(True) +- self._ChipStack.CallAsync( ++ self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_ConnectWithCode( + self.devCtrl, setupPayload, nodeid, discoveryType.value) + ) +@@ -2039,7 +2042,7 @@ class ChipDeviceController(ChipDeviceControllerBase): + self._ChipStack.commissioningCompleteEvent.clear() + + self._enablePairingCompeleteCallback(True) +- self._ChipStack.CallAsync( ++ self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_ConnectIP( + self.devCtrl, ipaddr.encode("utf-8"), setupPinCode, nodeid) + ) +@@ -2053,7 +2056,7 @@ class ChipDeviceController(ChipDeviceControllerBase): + The NOC chain will be provided in TLV cert format.""" + self.CheckIsActive() + +- return self._ChipStack.CallAsync( ++ return self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_IssueNOCChain( + self.devCtrl, py_object(self), csr.NOCSRElements, len(csr.NOCSRElements), nodeId) + ) +diff --git a/src/controller/python/chip/ChipStack.py b/src/controller/python/chip/ChipStack.py +index 6df7e41de4..35f9e24ef4 100644 +--- a/src/controller/python/chip/ChipStack.py ++++ b/src/controller/python/chip/ChipStack.py +@@ -26,6 +26,7 @@ + + from __future__ import absolute_import, print_function + ++import asyncio + import builtins + import logging + import os +@@ -164,6 +165,35 @@ class AsyncCallableHandle: + return self._res + + ++class AsyncioCallableHandle: ++ """Class which handles Matter SDK Calls asyncio friendly""" ++ ++ def __init__(self, callback): ++ self._callback = callback ++ self._loop = asyncio.get_event_loop() ++ self._future = self._loop.create_future() ++ self._result = None ++ self._exception = None ++ ++ @property ++ def future(self): ++ return self._future ++ ++ def _done(self): ++ if self._exception: ++ self._future.set_exception(self._exception) ++ else: ++ self._future.set_result(self._result) ++ ++ def __call__(self): ++ try: ++ self._result = self._callback() ++ except Exception as ex: ++ self._exception = ex ++ self._loop.call_soon_threadsafe(self._done) ++ pythonapi.Py_DecRef(py_object(self)) ++ ++ + _CompleteFunct = CFUNCTYPE(None, c_void_p, c_void_p) + _ErrorFunct = CFUNCTYPE(None, c_void_p, c_void_p, + c_ulong, POINTER(DeviceStatusStruct)) +@@ -178,6 +208,7 @@ class ChipStack(object): + bluetoothAdapter=None, enableServerInteractions=True): + builtins.enableDebugMode = False + ++ # TODO: Probably no longer necessary, see https://github.com/project-chip/connectedhomeip/issues/33321. + self.networkLock = Lock() + self.completeEvent = Event() + self.commissioningCompleteEvent = Event() +@@ -318,6 +349,7 @@ class ChipStack(object): + logFunct = 0 + if not isinstance(logFunct, _LogMessageFunct): + logFunct = _LogMessageFunct(logFunct) ++ # TODO: Lock probably no longer necessary, see https://github.com/project-chip/connectedhomeip/issues/33321. + with self.networkLock: + # NOTE: ChipStack must hold a reference to the CFUNCTYPE object while it is + # set. Otherwise it may get garbage collected, and logging calls from the +@@ -360,6 +392,7 @@ class ChipStack(object): + # throw error if op in progress + self.callbackRes = None + self.completeEvent.clear() ++ # TODO: Lock probably no longer necessary, see https://github.com/project-chip/connectedhomeip/issues/33321. + with self.networkLock: + res = self.PostTaskOnChipThread(callFunct).Wait(timeoutMs) + self.completeEvent.set() +@@ -367,14 +400,32 @@ class ChipStack(object): + return self.callbackRes + return res + +- def CallAsync(self, callFunct): ++ async def CallAsync(self, callFunct, timeoutMs: int = None): ++ '''Run a Python function on CHIP stack, and wait for the response. ++ This function will post a task on CHIP mainloop and waits for the call response in a asyncio friendly manner. ++ ''' ++ callObj = AsyncioCallableHandle(callFunct) ++ pythonapi.Py_IncRef(py_object(callObj)) ++ ++ res = self._ChipStackLib.pychip_DeviceController_PostTaskOnChipThread( ++ self.cbHandleChipThreadRun, py_object(callObj)) ++ ++ if not res.is_success: ++ pythonapi.Py_DecRef(py_object(callObj)) ++ raise res.to_exception() ++ ++ return await asyncio.wait_for(callObj.future, timeoutMs / 1000 if timeoutMs else None) ++ ++ def CallAsyncWithCompleteCallback(self, callFunct): + '''Run a Python function on CHIP stack, and wait for the application specific response. + This function is a wrapper of PostTaskOnChipThread, which includes some handling of application specific logics. + Calling this function on CHIP on CHIP mainloop thread will cause deadlock. ++ Make sure to register the necessary callbacks which release the function by setting the completeEvent. + ''' + # throw error if op in progress + self.callbackRes = None + self.completeEvent.clear() ++ # TODO: Lock probably no longer necessary, see https://github.com/project-chip/connectedhomeip/issues/33321. + with self.networkLock: + res = self.PostTaskOnChipThread(callFunct).Wait() + +diff --git a/src/controller/python/chip/clusters/Command.py b/src/controller/python/chip/clusters/Command.py +index 89aae537c9..6ef25cb211 100644 +--- a/src/controller/python/chip/clusters/Command.py ++++ b/src/controller/python/chip/clusters/Command.py +@@ -291,9 +291,9 @@ def TestOnlySendCommandTimedRequestFlagWithNoTimedInvoke(future: Future, eventLo + )) + + +-def SendCommand(future: Future, eventLoop, responseType: Type, device, commandPath: CommandPath, payload: ClusterCommand, +- timedRequestTimeoutMs: Union[None, int] = None, interactionTimeoutMs: Union[None, int] = None, busyWaitMs: Union[None, int] = None, +- suppressResponse: Union[None, bool] = None) -> PyChipError: ++async def SendCommand(future: Future, eventLoop, responseType: Type, device, commandPath: CommandPath, payload: ClusterCommand, ++ timedRequestTimeoutMs: Union[None, int] = None, interactionTimeoutMs: Union[None, int] = None, ++ busyWaitMs: Union[None, int] = None, suppressResponse: Union[None, bool] = None) -> PyChipError: + ''' Send a cluster-object encapsulated command to a device and does the following: + - On receipt of a successful data response, returns the cluster-object equivalent through the provided future. + - None (on a successful response containing no data) +@@ -316,7 +316,7 @@ def SendCommand(future: Future, eventLoop, responseType: Type, device, commandPa + + payloadTLV = payload.ToTLV() + ctypes.pythonapi.Py_IncRef(ctypes.py_object(transaction)) +- return builtins.chipStack.Call( ++ return await builtins.chipStack.CallAsync( + lambda: handle.pychip_CommandSender_SendCommand( + ctypes.py_object(transaction), device, + c_uint16(0 if timedRequestTimeoutMs is None else timedRequestTimeoutMs), commandPath.EndpointId, +@@ -353,9 +353,9 @@ def _BuildPyInvokeRequestData(commands: List[InvokeRequestInfo], timedRequestTim + return pyBatchCommandsData + + +-def SendBatchCommands(future: Future, eventLoop, device, commands: List[InvokeRequestInfo], +- timedRequestTimeoutMs: Optional[int] = None, interactionTimeoutMs: Optional[int] = None, busyWaitMs: Optional[int] = None, +- suppressResponse: Optional[bool] = None) -> PyChipError: ++async def SendBatchCommands(future: Future, eventLoop, device, commands: List[InvokeRequestInfo], ++ timedRequestTimeoutMs: Optional[int] = None, interactionTimeoutMs: Optional[int] = None, ++ busyWaitMs: Optional[int] = None, suppressResponse: Optional[bool] = None) -> PyChipError: + ''' Initiates an InvokeInteraction with the batch commands provided. + + Arguments: +@@ -388,7 +388,7 @@ def SendBatchCommands(future: Future, eventLoop, device, commands: List[InvokeRe + transaction = AsyncBatchCommandsTransaction(future, eventLoop, responseTypes) + ctypes.pythonapi.Py_IncRef(ctypes.py_object(transaction)) + +- return builtins.chipStack.Call( ++ return await builtins.chipStack.CallAsync( + lambda: handle.pychip_CommandSender_SendBatchCommands( + py_object(transaction), device, + c_uint16(0 if timedRequestTimeoutMs is None else timedRequestTimeoutMs), +-- +2.45.2 + diff --git a/0012-Python-Make-AttributePath-more-pythonic-33571.patch b/0012-Python-Make-AttributePath-more-pythonic-33571.patch new file mode 100644 index 0000000..151e6ea --- /dev/null +++ b/0012-Python-Make-AttributePath-more-pythonic-33571.patch @@ -0,0 +1,308 @@ +From 518fdbac13ace67cfbd4482286320f4c45ab1b05 Mon Sep 17 00:00:00 2001 +From: Stefan Agner +Date: Fri, 24 May 2024 10:39:41 +0200 +Subject: [PATCH] [Python] Make AttributePath more pythonic (#33571) + +* [Python] Make AttributePath more pythonic + +Use dataclass default initializer to initialize AttributePath. Use +static method to initialize from Cluster or Attribute. + +Also hash the integer fields directly, this is more efficient than +formatting a string first. + +* Drop AttributePathWithListIndex + +Drop AttributePathWithListIndex as it is unused. + +* Make DataVersionFilter/EventPath pythonic as well + +Use frozen data classes and static initializers similar to +AttributePath. + +* Fix _parseEventPathTuple + +* Fix _parseDataVersionFilterTuple +--- + src/controller/python/chip/ChipDeviceCtrl.py | 47 +++------ + .../python/chip/clusters/Attribute.py | 97 ++++++------------- + .../test/test_scripts/cluster_objects.py | 2 +- + 3 files changed, 48 insertions(+), 98 deletions(-) + +diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py +index 3282ffd191..4d14a42f18 100644 +--- a/src/controller/python/chip/ChipDeviceCtrl.py ++++ b/src/controller/python/chip/ChipDeviceCtrl.py +@@ -1154,33 +1154,26 @@ class ChipDeviceControllerBase(): + # Concrete path + typing.Tuple[int, typing.Type[ClusterObjects.ClusterAttributeDescriptor]] + ]): +- endpoint = None +- cluster = None +- attribute = None +- + if pathTuple == ('*') or pathTuple == (): + # Wildcard +- pass ++ return ClusterAttribute.AttributePath() + elif not isinstance(pathTuple, tuple): + if isinstance(pathTuple, int): +- endpoint = pathTuple ++ return ClusterAttribute.AttributePath(EndpointId=pathTuple) + elif issubclass(pathTuple, ClusterObjects.Cluster): +- cluster = pathTuple ++ return ClusterAttribute.AttributePath.from_cluster(EndpointId=None, Cluster=pathTuple) + elif issubclass(pathTuple, ClusterObjects.ClusterAttributeDescriptor): +- attribute = pathTuple ++ return ClusterAttribute.AttributePath.from_attribute(EndpointId=None, Attribute=pathTuple) + else: + raise ValueError("Unsupported Attribute Path") + else: + # endpoint + (cluster) attribute / endpoint + cluster +- endpoint = pathTuple[0] + if issubclass(pathTuple[1], ClusterObjects.Cluster): +- cluster = pathTuple[1] ++ return ClusterAttribute.AttributePath.from_cluster(EndpointId=pathTuple[0], Cluster=pathTuple[1]) + elif issubclass(pathTuple[1], ClusterAttribute.ClusterAttributeDescriptor): +- attribute = pathTuple[1] ++ return ClusterAttribute.AttributePath.from_attribute(EndpointId=pathTuple[0], Attribute=pathTuple[1]) + else: + raise ValueError("Unsupported Attribute Path") +- return ClusterAttribute.AttributePath( +- EndpointId=endpoint, Cluster=cluster, Attribute=attribute) + + def _parseDataVersionFilterTuple(self, pathTuple: typing.List[typing.Tuple[int, typing.Type[ClusterObjects.Cluster], int]]): + endpoint = None +@@ -1193,7 +1186,7 @@ class ChipDeviceControllerBase(): + else: + raise ValueError("Unsupported Cluster Path") + dataVersion = pathTuple[2] +- return ClusterAttribute.DataVersionFilter( ++ return ClusterAttribute.DataVersionFilter.from_cluster( + EndpointId=endpoint, Cluster=cluster, DataVersion=dataVersion) + + def _parseEventPathTuple(self, pathTuple: typing.Union[ +@@ -1210,39 +1203,31 @@ class ChipDeviceControllerBase(): + typing.Tuple[int, + typing.Type[ClusterObjects.ClusterEvent], int] + ]): +- endpoint = None +- cluster = None +- event = None +- urgent = False + if pathTuple in [('*'), ()]: + # Wildcard +- pass ++ return ClusterAttribute.EventPath() + elif not isinstance(pathTuple, tuple): + logging.debug(type(pathTuple)) + if isinstance(pathTuple, int): +- endpoint = pathTuple ++ return ClusterAttribute.EventPath(EndpointId=pathTuple) + elif issubclass(pathTuple, ClusterObjects.Cluster): +- cluster = pathTuple ++ return ClusterAttribute.EventPath.from_cluster(EndpointId=None, Cluster=pathTuple) + elif issubclass(pathTuple, ClusterObjects.ClusterEvent): +- event = pathTuple ++ return ClusterAttribute.EventPath.from_event(EndpointId=None, Event=pathTuple) + else: + raise ValueError("Unsupported Event Path") + else: + if pathTuple[0] == '*': +- urgent = pathTuple[-1] +- pass ++ return ClusterAttribute.EventPath(Urgent=pathTuple[-1]) + else: ++ urgent = bool(pathTuple[-1]) if len(pathTuple) > 2 else False + # endpoint + (cluster) event / endpoint + cluster +- endpoint = pathTuple[0] + if issubclass(pathTuple[1], ClusterObjects.Cluster): +- cluster = pathTuple[1] ++ return ClusterAttribute.EventPath.from_cluster(EndpointId=pathTuple[0], Cluster=pathTuple[1], Urgent=urgent) + elif issubclass(pathTuple[1], ClusterAttribute.ClusterEvent): +- event = pathTuple[1] ++ return ClusterAttribute.EventPath.from_event(EndpointId=pathTuple[0], Event=pathTuple[1], Urgent=urgent) + else: + raise ValueError("Unsupported Attribute Path") +- urgent = bool(pathTuple[-1]) if len(pathTuple) > 2 else False +- return ClusterAttribute.EventPath( +- EndpointId=endpoint, Cluster=cluster, Event=event, Urgent=urgent) + + async def Read(self, nodeid: int, attributes: typing.List[typing.Union[ + None, # Empty tuple, all wildcard +@@ -1514,7 +1499,7 @@ class ChipDeviceControllerBase(): + + result = asyncio.run(self.ReadAttribute( + nodeid, [(endpoint, attributeType)])) +- path = ClusterAttribute.AttributePath( ++ path = ClusterAttribute.AttributePath.from_attribute( + EndpointId=endpoint, Attribute=attributeType) + return im.AttributeReadResult(path=im.AttributePath(nodeId=nodeid, endpointId=path.EndpointId, clusterId=path.ClusterId, attributeId=path.AttributeId), + status=0, value=result[endpoint][clusterType][attributeType], dataVersion=result[endpoint][clusterType][ClusterAttribute.DataVersion]) +diff --git a/src/controller/python/chip/clusters/Attribute.py b/src/controller/python/chip/clusters/Attribute.py +index ce522bf452..51389e19a1 100644 +--- a/src/controller/python/chip/clusters/Attribute.py ++++ b/src/controller/python/chip/clusters/Attribute.py +@@ -54,62 +54,43 @@ class EventPriority(Enum): + CRITICAL = 2 + + +-@dataclass ++@dataclass(frozen=True) + class AttributePath: + EndpointId: int = None + ClusterId: int = None + AttributeId: int = None + +- def __init__(self, EndpointId: int = None, Cluster=None, Attribute=None, ClusterId=None, AttributeId=None): +- self.EndpointId = EndpointId +- if Cluster is not None: +- # Wildcard read for a specific cluster +- if (Attribute is not None) or (ClusterId is not None) or (AttributeId is not None): +- raise Warning( +- "Attribute, ClusterId and AttributeId is ignored when Cluster is specified") +- self.ClusterId = Cluster.id +- return +- if Attribute is not None: +- if (ClusterId is not None) or (AttributeId is not None): +- raise Warning( +- "ClusterId and AttributeId is ignored when Attribute is specified") +- self.ClusterId = Attribute.cluster_id +- self.AttributeId = Attribute.attribute_id +- return +- self.ClusterId = ClusterId +- self.AttributeId = AttributeId ++ @staticmethod ++ def from_cluster(EndpointId: int, Cluster: Cluster) -> AttributePath: ++ if Cluster is None: ++ raise ValueError("Cluster cannot be None") ++ return AttributePath(EndpointId=EndpointId, ClusterId=Cluster.id) ++ ++ @staticmethod ++ def from_attribute(EndpointId: int, Attribute: ClusterAttributeDescriptor) -> AttributePath: ++ if Attribute is None: ++ raise ValueError("Attribute cannot be None") ++ return AttributePath(EndpointId=EndpointId, ClusterId=Attribute.cluster_id, AttributeId=Attribute.attribute_id) + + def __str__(self) -> str: + return f"{self.EndpointId}/{self.ClusterId}/{self.AttributeId}" + +- def __hash__(self): +- return str(self).__hash__() + +- +-@dataclass ++@dataclass(frozen=True) + class DataVersionFilter: + EndpointId: int = None + ClusterId: int = None + DataVersion: int = None + +- def __init__(self, EndpointId: int = None, Cluster=None, ClusterId=None, DataVersion=None): +- self.EndpointId = EndpointId +- if Cluster is not None: +- # Wildcard read for a specific cluster +- if (ClusterId is not None): +- raise Warning( +- "Attribute, ClusterId and AttributeId is ignored when Cluster is specified") +- self.ClusterId = Cluster.id +- else: +- self.ClusterId = ClusterId +- self.DataVersion = DataVersion ++ @staticmethod ++ def from_cluster(EndpointId: int, Cluster: Cluster, DataVersion: int = None) -> AttributePath: ++ if Cluster is None: ++ raise ValueError("Cluster cannot be None") ++ return DataVersionFilter(EndpointId=EndpointId, ClusterId=Cluster.id, DataVersion=DataVersion) + + def __str__(self) -> str: + return f"{self.EndpointId}/{self.ClusterId}/{self.DataVersion}" + +- def __hash__(self): +- return str(self).__hash__() +- + + @dataclass + class TypedAttributePath: +@@ -165,44 +146,28 @@ class TypedAttributePath: + self.AttributeId = self.AttributeType.attribute_id + + +-@dataclass ++@dataclass(frozen=True) + class EventPath: + EndpointId: int = None + ClusterId: int = None + EventId: int = None + Urgent: int = None + +- def __init__(self, EndpointId: int = None, Cluster=None, Event=None, ClusterId=None, EventId=None, Urgent=None): +- self.EndpointId = EndpointId +- self.Urgent = Urgent +- if Cluster is not None: +- # Wildcard read for a specific cluster +- if (Event is not None) or (ClusterId is not None) or (EventId is not None): +- raise Warning( +- "Event, ClusterId and AttributeId is ignored when Cluster is specified") +- self.ClusterId = Cluster.id +- return +- if Event is not None: +- if (ClusterId is not None) or (EventId is not None): +- raise Warning( +- "ClusterId and EventId is ignored when Event is specified") +- self.ClusterId = Event.cluster_id +- self.EventId = Event.event_id +- return +- self.ClusterId = ClusterId +- self.EventId = EventId ++ @staticmethod ++ def from_cluster(EndpointId: int, Cluster: Cluster, EventId: int = None, Urgent: int = None) -> "EventPath": ++ if Cluster is None: ++ raise ValueError("Cluster cannot be None") ++ return EventPath(EndpointId=EndpointId, ClusterId=Cluster.id, EventId=EventId, Urgent=Urgent) ++ ++ @staticmethod ++ def from_event(EndpointId: int, Event: ClusterEvent, Urgent: int = None) -> "EventPath": ++ if Event is None: ++ raise ValueError("Event cannot be None") ++ return EventPath(EndpointId=EndpointId, ClusterId=Event.cluster_id, EventId=Event.event_id, Urgent=Urgent) + + def __str__(self) -> str: + return f"{self.EndpointId}/{self.ClusterId}/{self.EventId}/{self.Urgent}" + +- def __hash__(self): +- return str(self).__hash__() +- +- +-@dataclass +-class AttributePathWithListIndex(AttributePath): +- ListIndex: int = None +- + + @dataclass + class EventHeader: +@@ -711,7 +676,7 @@ class AsyncReadTransaction: + def GetAllEventValues(self): + return self._events + +- def handleAttributeData(self, path: AttributePathWithListIndex, dataVersion: int, status: int, data: bytes): ++ def handleAttributeData(self, path: AttributePath, dataVersion: int, status: int, data: bytes): + try: + imStatus = chip.interaction_model.Status(status) + +diff --git a/src/controller/python/test/test_scripts/cluster_objects.py b/src/controller/python/test/test_scripts/cluster_objects.py +index 53516c1dc7..37f6819cbe 100644 +--- a/src/controller/python/test/test_scripts/cluster_objects.py ++++ b/src/controller/python/test/test_scripts/cluster_objects.py +@@ -164,7 +164,7 @@ class ClusterObjectTests: + ] + ) + expectedRes = [ +- AttributeStatus(Path=AttributePath( ++ AttributeStatus(Path=AttributePath.from_attribute( + EndpointId=1, + Attribute=Clusters.UnitTesting.Attributes.ListLongOctetString), Status=chip.interaction_model.Status.Success), + ] +-- +2.45.2 + diff --git a/0013-Python-Drop-chip-device-ctrl-33488.patch b/0013-Python-Drop-chip-device-ctrl-33488.patch new file mode 100644 index 0000000..f42cda9 --- /dev/null +++ b/0013-Python-Drop-chip-device-ctrl-33488.patch @@ -0,0 +1,2532 @@ +From 9be9b5e5f8b7988c71b178ffc24c76590c8f0f88 Mon Sep 17 00:00:00 2001 +From: Stefan Agner +Date: Fri, 24 May 2024 16:19:35 +0200 +Subject: [PATCH] [Python] Drop chip-device-ctrl (#33488) + +* Drop chip-device-ctrl and ZCL* APIs + +Drop the deprecated chip-device-ctrl and remove the ZCL* API from the +Python CHIP controller. + +* Update docs to reflect chip-repl commands + +Update the Python CHIP controller docs to reflect the CHIP REPL +instead of the now removed chip-device-ctrl tool. + +* Remove unused imports + +* Replace chip-device-ctrl with chip-repl + +* Update/reword main QUICK_START and READMEs + +* Fix wrong/buggy cross-reference + +* Remove common chip-device-ctrl example + +Remove the outdated chip-device-ctrl example and refer to the Python +controller REPL documentation. + +* Add --ble-adapter support to CHIP REPL and update docs + +Add support to select the Bluetooth adapter using the common +--ble-adapter command line argument. + +Update the advanced docs for the Python Controller. + +* Address review feedback + +* Trim list of commands/add link to official API docs +--- + docs/QUICK_START.md | 2 +- + .../python_chip_controller_advanced_usage.md | 226 ++-- + .../guides/python_chip_controller_building.md | 499 ++----- + .../lighting-app/infineon/cyw30739/README.md | 16 +- + examples/lighting-app/python/README.md | 15 +- + examples/lock-app/infineon/cyw30739/README.md | 16 +- + scripts/tools/linux_ip_namespace_setup.sh | 2 +- + src/controller/README.md | 2 +- + src/controller/python/BUILD.gn | 5 +- + src/controller/python/README.md | 14 +- + src/controller/python/chip-device-ctrl.py | 1202 ----------------- + src/controller/python/chip/ChipDeviceCtrl.py | 82 +- + src/controller/python/chip/ChipReplStartup.py | 4 +- + .../pycontroller/build-chip-wheel.py | 1 - + 14 files changed, 260 insertions(+), 1826 deletions(-) + delete mode 100755 src/controller/python/chip-device-ctrl.py + +diff --git a/docs/QUICK_START.md b/docs/QUICK_START.md +index 97f0f76b3b..10833e0c0c 100644 +--- a/docs/QUICK_START.md ++++ b/docs/QUICK_START.md +@@ -10,7 +10,7 @@ and platforms. + |
Controller / Admin
|
Node
| Description | + | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | [**chip-tool**](https://github.com/project-chip/connectedhomeip/blob/master/examples/chip-tool/README.md) (Linux / Mac)
Includes docs for all the cluster commands supported
| **all-clusters-app**
  • [M5Stack](https://github.com/project-chip/connectedhomeip/blob/master/examples/all-clusters-app/esp32/README.md) (ESP)
  • [Linux](https://github.com/project-chip/connectedhomeip/tree/master/examples/all-clusters-app/linux) simulation | Use the command line tool on a laptop to pair with and control an embedded Wi-Fi platform. This demo supports the “all-clusters-app”, so it provides the basic onoff light test and more. | +-| [**chip-device-ctrl.py**](https://github.com/project-chip/connectedhomeip/blob/master/src/controller/python/README.md) | **all-clusters-app**
  • [M5Stack](https://github.com/project-chip/connectedhomeip/blob/master/examples/all-clusters-app/esp32/README.md) (ESP)
  • [Linux](https://github.com/project-chip/connectedhomeip/tree/master/examples/all-clusters-app/linux) simulation | Same as above, but uses the pychip tool as Controller Node. | ++| [**chip-repl**](https://github.com/project-chip/connectedhomeip/blob/master/src/controller/python/README.md) | **all-clusters-app**
  • [M5Stack](https://github.com/project-chip/connectedhomeip/blob/master/examples/all-clusters-app/esp32/README.md) (ESP)
  • [Linux](https://github.com/project-chip/connectedhomeip/tree/master/examples/all-clusters-app/linux) simulation | Same as above, but uses the Python CHIP REPL as Controller Node. | + + ## Thread Nodes + +diff --git a/docs/guides/python_chip_controller_advanced_usage.md b/docs/guides/python_chip_controller_advanced_usage.md +index c3d3f55ddc..2eee5472fd 100644 +--- a/docs/guides/python_chip_controller_advanced_usage.md ++++ b/docs/guides/python_chip_controller_advanced_usage.md +@@ -7,8 +7,9 @@ tool or Matter accessories on Linux. + +
    + +-- [Bluetooth LE virtualization on Linux](#bluetooth-le-virtualization-on-linux) +-- [Debugging with gdb](#debugging-with-gdb) ++- [Using Python CHIP Controller advanced features](#using-python-chip-controller-advanced-features) ++ - [Bluetooth LE virtualization on Linux](#bluetooth-le-virtualization-on-linux) ++ - [Debugging with gdb](#debugging-with-gdb) + +
    + +@@ -62,38 +63,38 @@ interfaces working as Bluetooth LE central and peripheral, respectively. + TX bytes:3488 acl:95 sco:0 commands:110 errors:0 + ``` + +-4. Run the Python CHIP Controller with Bluetooth LE adapter defined from a ++4. Run the Python CHIP Controller REPL with Bluetooth LE adapter defined from a + command line: + +- For example, add `--bluetooth-adapter=hci2` to use the virtual interface +- `hci2` listed above. ++ For example, add `--ble-adapter=2` to use the virtual interface `hci2` ++ listed above. + + ``` +- chip-device-ctrl --bluetooth-adapter=hci2 ++ chip-repl --ble-adapter=2 + ``` + +
    + + ## Debugging with gdb + +-You can run the chip-device-ctrl under GDB for debugging, however, since the +-Matter core support library is a dynamic library, you cannot read the symbols +-unless it is fully loaded. ++You can run the chip-repl under GDB for debugging, however, since the Matter SDK ++library is a dynamic library, you cannot read the symbols unless it is fully ++loaded. + + The following block is a example debug session using GDB: + + ``` + # GDB cannot run scripts directly +-# so you need to run Python3 with the path of device controller +-# Here, we use the feature from bash to get the path of chip-device-ctrl without typing it. +-$ gdb --args python3 `which chip-device-ctrl` +-GNU gdb (Ubuntu 10.1-2ubuntu2) 10.1.90.20210411-git +-Copyright (C) 2021 Free Software Foundation, Inc. ++# so you need to run Python3 with the path of device controller REPL ++# Here, we use the feature from bash to get the path of chip-repl without typing it. ++$ gdb --args python3 `which chip-repl` ++GNU gdb (GDB) 14.2 ++Copyright (C) 2023 Free Software Foundation, Inc. + License GPLv3+: GNU GPL version 3 or later + This is free software: you are free to change and redistribute it. + There is NO WARRANTY, to the extent permitted by law. + Type "show copying" and "show warranty" for details. +-This GDB was configured as "aarch64-linux-gnu". ++This GDB was configured as "x86_64-pc-linux-gnu". + Type "show configuration" for configuration details. + For bug reporting instructions, please see: + . +@@ -103,6 +104,12 @@ Find the GDB manual and other documentation resources online at: + For help, type "help". + Type "apropos word" to search for commands related to "word"... + Reading symbols from python3... ++ ++This GDB supports auto-downloading debuginfo from the following URLs: ++ ++Enable debuginfod for this session? (y or [n]) n ++Debuginfod has been disabled. ++To make this setting permanent, add 'set debuginfod enabled off' to .gdbinit. + (No debugging symbols found in python3) + (gdb) + ``` +@@ -119,38 +126,68 @@ library, let run the Matter device controller first. + + ``` + (gdb) run +-Starting program: /usr/bin/python3 /home/ubuntu/.local/bin/chip-device-ctrl ++Starting program: /home/sag/projects/project-chip/connectedhomeip/out/venv/bin/python3 /home/sag/projects/project-chip/connectedhomeip/out/venv/bin/chip-repl + [Thread debugging using libthread_db enabled] +-Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1". +-CHIP:DIS: Init admin pairing table with server storage. +-CHIP:IN: local node id is 0x000000000001b669 +-CHIP:DL: MDNS failed to join multicast group on wpan0 for address type IPv4: Inet Error 1016 (0x000003F8): Address not found +-CHIP:ZCL: Using ZAP configuration... +-CHIP:ZCL: deactivate report event +-CHIP:CTL: Getting operational keys +-CHIP:CTL: Generating operational certificate for the controller +-CHIP:CTL: Getting root certificate for the controller from the issuer +-CHIP:CTL: Generating credentials +-CHIP:CTL: Loaded credentials successfully +-CHIP:DL: Platform main loop started. +-Chip Device Controller Shell ++Using host libthread_db library "/usr/lib/libthread_db.so.1". ++Python 3.11.9 (main, Apr 29 2024, 11:59:58) [GCC 13.2.1 20240417] ++Type 'copyright', 'credits' or 'license' for more information ++IPython 8.24.0 -- An enhanced Interactive Python. Type '?' for help. ++[1716395111.775747][364405:364405] CHIP:CTL: Setting attestation nonce to random value ++[1716395111.776196][364405:364405] CHIP:CTL: Setting CSR nonce to random value ++InitBLE 0[1716395111.776809][364405:364405] CHIP:DL: writing settings to file (/tmp/chip_counters.ini-T7hX27) ++[1716395111.776854][364405:364405] CHIP:DL: renamed tmp file to file (/tmp/chip_counters.ini) ++[1716395111.776860][364405:364405] CHIP:DL: NVS set: chip-counters/reboot-count = 9 (0x9) ++[1716395111.777261][364405:364405] CHIP:DL: Got Ethernet interface: eno2 ++[1716395111.777555][364405:364405] CHIP:DL: Found the primary Ethernet interface:eno2 ++[1716395111.777868][364405:364405] CHIP:DL: Got WiFi interface: wlp7s0 ++[1716395111.777877][364405:364405] CHIP:DL: Failed to reset WiFi statistic counts ++────────────────────────────────────────────────────────────────────────────────────────────────────────── Matter REPL ────────────────────────────────────────────────────────────────────────────────────────────────────────── ++ ++ ++ ++ Welcome to the Matter Python REPL! ++ ++ For help, please type matterhelp() ++ ++ To get more information on a particular object/class, you can pass ++ that into matterhelp() as well. ++ ++ ++───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ++2024-05-22 18:25:11 allenwind PersistentStorage[364405] WARNING Initializing persistent storage from file: /tmp/repl-storage.json ++2024-05-22 18:25:11 allenwind PersistentStorage[364405] WARNING Loading configuration from /tmp/repl-storage.json... ++2024-05-22 18:25:11 allenwind CertificateAuthorityManager[364405] WARNING Loading certificate authorities from storage... ++2024-05-22 18:25:11 allenwind CertificateAuthority[364405] WARNING New CertificateAuthority at index 1 ++2024-05-22 18:25:11 allenwind CertificateAuthority[364405] WARNING Loading fabric admins from storage... ++2024-05-22 18:25:11 allenwind FabricAdmin[364405] WARNING New FabricAdmin: FabricId: 0x0000000000000001, VendorId = 0xFFF1 ++2024-05-22 18:25:11 allenwind FabricAdmin[364405] WARNING Allocating new controller with CaIndex: 1, FabricId: 0x0000000000000001, NodeId: 0x000000000001B669, CatTags: [] ++ ++ ++The following objects have been created: ++ certificateAuthorityManager: Manages a list of CertificateAuthority instances. ++ caList: The list of CertificateAuthority instances. ++ caList: A specific FabricAdmin object at index m for the nth CertificateAuthority instance. + +-chip-device-ctrl > ++ ++Default CHIP Device Controller (NodeId: 112233): has been initialized to manage caList[0].adminList[0] (FabricId = 1), and is available as devCtrl ++ ++In [1]: + ``` + +-The prompt `chip-device-ctrl >` indicates that the Matter core library is loaded +-by Python, you can browse the symbols in the Matter core library, setting +-breakpoints on functions and many other functions provided by GDB. ++The prompt `In [1]:` indicates that the Matter SDK library has been loaded and ++initialized by the Python Controller REPL, you can browse the symbols in the ++Matter core library, setting breakpoints on functions and many other functions ++provided by GDB. + +-You can use `Ctrl-C` to send SIGINT to the controller anytime you want so you +-can set breakpoints. ++You can use `Ctrl-Z` to send `SIGTSTP` to the Python 3 REPL process anytime you ++want so you can set breakpoints (unfortunately Ctrl+C seems to be captured by ++the REPL). + +-> (`Ctrl-C` pressed here.) ++In [1]: (`Ctrl-Z` pressed here.) + + ``` +-Thread 1 "python3" received signal SIGINT, Interrupt. +-0x0000fffff7db79ec in __GI___select (nfds=, readfds=0xffffffffe760, writefds=0x0, exceptfds=0x0, timeout=) at ../sysdeps/unix/sysv/linux/select.c:49 +-49 ../sysdeps/unix/sysv/linux/select.c: No such file or directory. ++Thread 1 "python3" received signal SIGTSTP, Stopped (user). ++0x00007ffff7650ceb in kill () from /usr/lib/libc.so.6 + (gdb) + ``` + +@@ -159,40 +196,27 @@ command in GDB (`b` for short) + + ``` + (gdb) b DeviceCommissioner::PairDevice +-Breakpoint 1 at 0xfffff5b0f6b4 (2 locations) ++Breakpoint 1 at 0x7fffed453943: DeviceCommissioner::PairDevice. (4 locations) + (gdb) + ``` + +-Type `continue` (`c` for short) to continue the device controller, you may need +-another hit of `Enter` to see the prompt. ++Type `signal SIGCONT` to continue the device controller after stopping it with ++signal stop, you may need another hit of `Enter` to see the prompt. + + ``` +-(gdb) c +-Continuing. +- +-chip-device-ctrl > ++(gdb) signal SIGCONT ++Continuing with signal SIGCONT. ++In [1]: + ``` + + Let do pairing over IP to see the effect of the breakpoint we just set. + + ``` +-chip-device-ctrl > connect -ip 192.168.50.5 20202021 1 +-Device is assigned with nodeid = 1 +- +-Thread 1 "python3" hit Breakpoint 1, 0x0000fffff5b0f6b4 in chip::Controller::DeviceCommissioner::PairDevice(unsigned long, chip::RendezvousParameters&)@plt () +- from /home/ubuntu/.local/lib/python3.9/site-packages/chip/_ChipDeviceCtrl.so +-(gdb) +-``` +- +-The `@plt` symbol means it is a symbol used by dynamic library loader, type `c` +-(for `continue`) and it will break on the real function. ++In [1]: devCtrl.CommissionWithCode("MT:-24J0AFN00KA0648G00", 1234) + +-``` +-(gdb) c +-Continuing. +- +-Thread 1 "python3" hit Breakpoint 1, chip::Controller::DeviceCommissioner::PairDevice (this=0xd28540, remoteDeviceId=1, params=...) at ../../src/controller/CHIPDeviceController.cpp:827 +-827 { ++Thread 5 "python3" hit Breakpoint 1.1, chip::Controller::DeviceCommissioner::PairDevice (this=0x7fffd8003a90, remoteDeviceId=1234, setUpCode=0x7ffff453d490 "MT:-24J0AFN00KA0648G00", params=..., ++ discoveryType=chip::Controller::DiscoveryType::kAll, resolutionData=...) at ../../src/controller/CHIPDeviceController.cpp:646 ++646 { + (gdb) + ``` + +@@ -201,46 +225,44 @@ then you can use `bt` (for `backtrace`) to see the backtrace of the call stack. + + ``` + (gdb) bt +-#0 chip::Controller::DeviceCommissioner::PairDevice(unsigned long, chip::RendezvousParameters&) (this=0xd28540, remoteDeviceId=1, params=...) +- at ../../src/controller/CHIPDeviceController.cpp:827 +-#1 0x0000fffff5b3095c in pychip_DeviceController_ConnectIP(chip::Controller::DeviceCommissioner*, char const*, uint32_t, chip::NodeId) +- (devCtrl=0xd28540, peerAddrStr=0xfffff467ace0 "192.168.50.5", setupPINCode=20202021, nodeid=1) at ../../src/controller/python/ChipDeviceController-ScriptBinding.cpp:234 +-#2 0x0000fffff7639148 in () at /lib/aarch64-linux-gnu/libffi.so.8 +-#3 0x0000fffff7638750 in () at /lib/aarch64-linux-gnu/libffi.so.8 +-#4 0x0000fffff7665a44 in () at /usr/lib/python3.9/lib-dynload/_ctypes.cpython-39-aarch64-linux-gnu.so +-#5 0x0000fffff7664c7c in () at /usr/lib/python3.9/lib-dynload/_ctypes.cpython-39-aarch64-linux-gnu.so +-#6 0x00000000004a54f0 in _PyObject_MakeTpCall () +-#7 0x000000000049cb10 in _PyEval_EvalFrameDefault () +-#8 0x0000000000496d1c in () +-#9 0x00000000004b1eb0 in _PyFunction_Vectorcall () +-#10 0x0000000000498264 in _PyEval_EvalFrameDefault () +-#11 0x00000000004b1cb8 in _PyFunction_Vectorcall () +-#12 0x0000000000498418 in _PyEval_EvalFrameDefault () +-#13 0x0000000000496d1c in () +-#14 0x00000000004b1eb0 in _PyFunction_Vectorcall () +-#15 0x0000000000498418 in _PyEval_EvalFrameDefault () +-#16 0x00000000004b1cb8 in _PyFunction_Vectorcall () +-#17 0x00000000004c6bc8 in () +-#18 0x0000000000498264 in _PyEval_EvalFrameDefault () +-#19 0x00000000004b1cb8 in _PyFunction_Vectorcall () +-#20 0x0000000000498418 in _PyEval_EvalFrameDefault () +-#21 0x00000000004966f8 in () +-#22 0x00000000004b1f18 in _PyFunction_Vectorcall () +-#23 0x0000000000498418 in _PyEval_EvalFrameDefault () +-#24 0x00000000004b1cb8 in _PyFunction_Vectorcall () +-#25 0x0000000000498264 in _PyEval_EvalFrameDefault () +-#26 0x00000000004966f8 in () +-#27 0x0000000000496490 in _PyEval_EvalCodeWithName () +-#28 0x0000000000595b7c in PyEval_EvalCode () +-#29 0x00000000005c6a5c in () +-#30 0x00000000005c0a70 in () +-#31 0x00000000005c69a8 in () +-#32 0x00000000005c6148 in PyRun_SimpleFileExFlags () +-#33 0x00000000005b60bc in Py_RunMain () +-#34 0x0000000000585a08 in Py_BytesMain () +-#35 0x0000fffff7d0c9d4 in __libc_start_main (main= +- 0x5858fc <_start+60>, argc=2, argv=0xfffffffff498, init=, fini=, rtld_fini=, stack_end=) at ../csu/libc-start.c:332 +-#36 0x00000000005858f8 in _start () ++(gdb) bt ++#0 chip::Controller::DeviceCommissioner::PairDevice ++ (this=0x7fffd8003a90, remoteDeviceId=1234, setUpCode=0x7fffef2555d0 "MT:-24J0AFN00KA0648G00", params=..., discoveryType=chip::Controller::DiscoveryType::kAll, resolutionData=...) ++ at ../../src/controller/CHIPDeviceController.cpp:646 ++#1 0x00007fffed040825 in pychip_DeviceController_ConnectWithCode (devCtrl=0x7fffd8003a90, onboardingPayload=0x7fffef2555d0 "MT:-24J0AFN00KA0648G00", nodeid=1234, discoveryType=2 '\002') ++ at ../../src/controller/python/ChipDeviceController-ScriptBinding.cpp:395 ++#2 0x00007ffff6ad5596 in ??? () at /usr/lib/libffi.so.8 ++#3 0x00007ffff6ad200e in ??? () at /usr/lib/libffi.so.8 ++#4 0x00007ffff6ad4bd3 in ffi_call () at /usr/lib/libffi.so.8 ++#5 0x00007ffff6aeaffc in ??? () at /usr/lib/python3.11/lib-dynload/_ctypes.cpython-311-x86_64-linux-gnu.so ++#6 0x00007ffff6aeb4b4 in ??? () at /usr/lib/python3.11/lib-dynload/_ctypes.cpython-311-x86_64-linux-gnu.so ++#7 0x00007ffff794a618 in _PyObject_MakeTpCall () at /usr/lib/libpython3.11.so.1.0 ++#8 0x00007ffff78f3d03 in _PyEval_EvalFrameDefault () at /usr/lib/libpython3.11.so.1.0 ++#9 0x00007ffff7adef90 in ??? () at /usr/lib/libpython3.11.so.1.0 ++#10 0x00007ffff79ebc0b in _PyObject_FastCallDictTstate () at /usr/lib/libpython3.11.so.1.0 ++#11 0x00007ffff79ebe02 in _PyObject_Call_Prepend () at /usr/lib/libpython3.11.so.1.0 ++#12 0x00007ffff79ec114 in ??? () at /usr/lib/libpython3.11.so.1.0 ++#13 0x00007ffff794a618 in _PyObject_MakeTpCall () at /usr/lib/libpython3.11.so.1.0 ++#14 0x00007ffff78f3d03 in _PyEval_EvalFrameDefault () at /usr/lib/libpython3.11.so.1.0 ++#15 0x00007ffff7adef90 in ??? () at /usr/lib/libpython3.11.so.1.0 ++#16 0x00007ffff7955b97 in PyObject_Vectorcall () at /usr/lib/libpython3.11.so.1.0 ++#17 0x00007ffff6aea174 in ??? () at /usr/lib/python3.11/lib-dynload/_ctypes.cpython-311-x86_64-linux-gnu.so ++#18 0x00007ffff6aea28c in ??? () at /usr/lib/python3.11/lib-dynload/_ctypes.cpython-311-x86_64-linux-gnu.so ++#19 0x00007ffff6ad5152 in ??? () at /usr/lib/libffi.so.8 ++#20 0x00007ffff6ad57b8 in ??? () at /usr/lib/libffi.so.8 ++#21 0x00007fffed5de848 in chip::DeviceLayer::Internal::GenericPlatformManagerImpl::_DispatchEvent ++ (this=0x7fffed88dc90 , event=0x7fffe6fffe30) at ../../src/include/platform/internal/GenericPlatformManagerImpl.ipp:304 ++#22 0x00007fffed5dd90d in chip::DeviceLayer::PlatformManager::DispatchEvent (this=0x7fffed88dc80 , event=0x7fffe6fffe30) at ../../src/include/platform/PlatformManager.h:503 ++#23 0x00007fffed5df45b in chip::DeviceLayer::Internal::GenericPlatformManagerImpl_POSIX::ProcessDeviceEvents ++ (this=0x7fffed88dc90 ) at ../../src/include/platform/internal/GenericPlatformManagerImpl_POSIX.ipp:185 ++#24 0x00007fffed5dee64 in chip::DeviceLayer::Internal::GenericPlatformManagerImpl_POSIX::_RunEventLoop (this=0x7fffed88dc90 ) ++--Type for more, q to quit, c to continue without paging-- ++ at ../../src/include/platform/internal/GenericPlatformManagerImpl_POSIX.ipp:227 ++#25 0x00007fffed5dd888 in chip::DeviceLayer::PlatformManager::RunEventLoop (this=0x7fffed88dc80 ) at ../../src/include/platform/PlatformManager.h:403 ++#26 0x00007fffed5df3fe in chip::DeviceLayer::Internal::GenericPlatformManagerImpl_POSIX::EventLoopTaskMain (arg=0x7fffed88dc90 ) ++ at ../../src/include/platform/internal/GenericPlatformManagerImpl_POSIX.ipp:256 ++#27 0x00007ffff76a6ded in ??? () at /usr/lib/libc.so.6 ++#28 0x00007ffff772a0dc in ??? () at /usr/lib/libc.so.6 + (gdb) + ``` + +diff --git a/docs/guides/python_chip_controller_building.md b/docs/guides/python_chip_controller_building.md +index c940f2c925..8a7acc884a 100644 +--- a/docs/guides/python_chip_controller_building.md ++++ b/docs/guides/python_chip_controller_building.md +@@ -1,25 +1,20 @@ +-# Deprecation notice +- +-chip-device-ctrl is no longer maintained and should not be used. +- +-Matter-repl is the current python controller implementation. +- + # Working with Python CHIP Controller + +-The Python CHIP Controller is a tool that allows to commission a Matter device +-into the network and to communicate with it using the Zigbee Cluster Library +-(ZCL) messages. ++The Python CHIP controller is a library that allows to create a Matter fabric ++and commission Matter devices with it. + +-> The chip-device-ctrl tool will be deprecated, and will be replaced by +-> chip-repl. Continue reading to see how to do the same thing with chip-repl. ++The `chip-repl` is a REPl which sets up a Python CHIP Controller and allows to ++explore the Python CHIP Controller API and communicate with devices from the ++command line. + +
    + + - [Source files](#source-files) +-- [Building Android CHIPTool](#building-and-installing) +-- [Running the tool](#running-the-tool) +-- [Using Python CHIP Controller for Matter accessory testing](#using-python-chip-controller-for-matter-accessory-testing) +-- [List of commands](#list-of-commands) ++- [Building Python CHIP Controller](#building-and-installing) ++- [Running the CHIP REPL](#running-the-chip-repl) ++- [Using Python CHIP Controller REPL for Matter accessory testing](#using-python-chip-controller-repl-for-matter-accessory-testing) ++- [Example usage of the Python CHIP Controller REPL](#example-usage-of-the-python-chip-controller-repl) ++- [Explore Clusters, Attributes and Commands](#explore-clusters-attributes-and-commands) + +
    + +@@ -85,35 +80,31 @@ To build and run the Python CHIP controller: + scripts/build_python.sh -m platform -i separate + ``` + +- > Note: To get more details about available build configurations, run the ++ > Note: This builds the Python CHIP Controller along with the CHIP REPL as ++ > Python wheels and installs it into a separate Python virtual environment. ++ > To get more details about available build configurations, run the + > following command: `scripts/build_python.sh --help` + +
    + +-## Running the tool ++## Running the CHIP REPL + +-1. Activate the Python virtual environment: ++1. Activate the Python virtual environment with the Python CHIP Controller ++ installed: + + ``` + source out/python_env/bin/activate + ``` + +-2. Run the Python CHIP controller with root privileges, which is required to +- obtain access to the Bluetooth interface: +- +- ``` +- sudo out/python_env/bin/chip-device-ctrl +- ``` +- +- You can also select the Bluetooth LE interface using command line argument: ++2. Run the CHIP REPL to explore the API of the Python CHIP controller: + + ``` +- sudo out/python_env/bin/chip-device-ctrl --bluetooth-adapter=hci2 ++ chip-repl + ``` + +
    + +-## Using Python CHIP Controller for Matter accessory testing ++## Using Python CHIP Controller REPL for Matter accessory testing + + This section describes how to use Python CHIP controller to test the Matter + accessory. Below steps depend on the application clusters that you implemented +@@ -135,13 +126,14 @@ require physical trigger, for example pushing a button. Follow the documentation + of the Matter accessory example to learn how Bluetooth LE advertising is enabled + for the given example. + +-### Step 3: Discover Matter accessory device over Bluetooth LE ++### Step 3: Discover commissionable Matter accessory device + +-An uncommissioned accessory device advertises over Bluetooth LE. Run the +-following command to scan all advertised Matter devices: ++An uncommissioned accessory device advertises over Bluetooth LE or via mDNS if ++already on the network. Run the following command to scan all advertised Matter ++devices: + + ``` +-chip-device-ctrl > ble-scan ++devCtrl.DiscoverCommissionableNodes() + ``` + + ### Step 4: Set network pairing credentials +@@ -177,11 +169,12 @@ network interface, such as Thread or Wi-Fi. + datasets directly from the Thread Border Router, you might also use a + different out-of-band method. + +-2. Set the previously obtained Active Operational Dataset as a hex-encoded value +- using the following command: ++2. Set the previously obtained Active Operational Dataset as a byte array using ++ the following command: + + ``` +- chip-device-ctrl > set-pairing-thread-credential 0e080000000000010000000300001335060004001fffe002084fe76e9a8b5edaf50708fde46f999f0698e20510d47f5027a414ffeebaefa92285cc84fa030f4f70656e5468726561642d653439630102e49c0410b92f8c7fbb4f9f3e08492ee3915fbd2f0c0402a0fff8 ++ thread_dataset = bytes.fromhex("0e080000000000010000000300001335060004001fffe002084fe76e9a8b5edaf50708fde46f999f0698e20510d47f5027a414ffeebaefa92285cc84fa030f4f70656e5468726561642d653439630102e49c0410b92f8c7fbb4f9f3e08492ee3915fbd2f0c0402a0fff8") ++ devCtrl.SetThreadOperationalDataset(thread_dataset) + ``` + + #### Setting Wi-Fi network credentials +@@ -190,11 +183,9 @@ Assuming your Wi-Fi SSID is _TESTSSID_, and your Wi-Fi password is _P455W4RD_, + set the credentials to the controller by executing the following command: + + ``` +-chip-device-ctrl > set-pairing-wifi-credential TESTSSID P455W4RD ++devCtrl.SetWiFiCredentials(, ) + ``` + +-**REPL Command**: `devCtrl.SetWiFiCredentials(, )` +- + ### Step 5: Commission the Matter accessory device over Bluetooth LE + + The controller uses a 12-bit value called **discriminator** to discern between +@@ -222,16 +213,26 @@ with the following assumptions for the Matter accessory device: + - The temporary Node ID is _1234_ + + ``` +-chip-device-ctrl > connect -ble 3840 20202021 1234 ++devCtrl.ConnectBLE(3840, 20202021, 1234) + ``` + +-**REPL Command:** +-`devCtrl.ConnectBLE(, , )` +- + You can skip the last parameter, the Node ID, in the command. If you skip it, + the controller will assign it randomly. In that case, note down the Node ID, + because it is required later in the configuration process. + ++It is also possible to use the QR setup code instead. It typically is shown on ++the terminal of the device as well. For example: ++ ++``` ++CHIP:SVR: SetupQRCode: [MT:-24J0AFN00KA0648G00] ++``` ++ ++Use the following command to commission the device with the QR code: ++ ++``` ++devCtrl.CommissionWithCode("MT:-24J0AFN00KA0648G00", 1234) ++``` ++ + After connecting the device over Bluetooth LE, the controller will go through + the following stages: + +@@ -255,429 +256,155 @@ the following stages: + finished and the Python CHIP controller is now using only the IPv6 traffic + to reach the device. + +-### Step 6: Control application ZCL clusters. ++### Step 6: Control application clusters. + + For the light bulb example, execute the following command to toggle the LED + state: + + ``` +-chip-device-ctrl > zcl OnOff Toggle 1234 1 0 ++await devCtrl.SendCommand(1234, 1, Clusters.OnOff.Commands.Toggle()) + ``` + +-**REPL Command:** +-`await devCtrl.SendCommand(1234, 1, Clusters.OnOff.Commands.Toggle())` +- + To change the brightness of the LED, use the following command, with the level + value somewhere between 0 and 255. + + ``` +-chip-device-ctrl > zcl LevelControl MoveToLevel 1234 1 0 level=50 ++commandToSend = LevelControl.Commands.MoveToLevel(level=50, transitionTime=Null, optionsMask=0, optionsOverride=0) ++await devCtrl.SendCommand(1234, 1, commandToSend) + ``` + +-**REPL Command:** +-`await devCtrl.SendCommand(1234, 1, LevelControl.Commands.MoveToLevel(level=50, transitionTime=Null, optionsMask=0, optionsOverride=0))` +- + ### Step 7: Read basic information out of the accessory. + + Every Matter accessory device supports a Basic Information Cluster, which + maintains collection of attributes that a controller can obtain from a device, +-such as the vendor name, the product name, or software version. Use `zclread` +-command to read those values from the device: ++such as the vendor name, the product name, or software version. Use ++`ReadAttribute()` command to read those values from the device: + + ``` +-chip-device-ctrl > zclread BasicInformation VendorName 1234 1 0 +-chip-device-ctrl > zclread BasicInformation ProductName 1234 1 0 +-chip-device-ctrl > zclread BasicInformation SoftwareVersion 1234 1 0 ++attributes = [ ++ (0, Clusters.BasicInformation.Attributes.VendorName), ++ (0, Clusters.BasicInformation.Attributes.ProductName), ++ (0, Clusters.BasicInformation.Attributes.SoftwareVersion), ++] ++await devCtrl.ReadAttribute(1234, attributes) + ``` + +-**REPL Command:** +-`await devCtrl.ReadAttribute(1234, [(1, Clusters.BasicInformation.Attributes.VendorName)])` +- +-> Use the `zcl ? BasicInformation` command to list all available commands for +-> Basic Information Cluster. +-> + > In REPL, you can type `Clusters.BasicInformation.Attributes.` and then use the + > TAB key. + +
    + +-## List of commands +- +-### `ble-adapter-print` ++## Example usage of the Python CHIP Controller REPL + +-> BLE adapter operations is not yet supported in REPL ++These section covers a few useful commands of the Python CHIP Controller along ++with examples demonstrating how they can be called from the REPL. + +-Print the available Bluetooth adapters on device. Takes no arguments: +- +-``` +-chip-device-ctrl > ble-adapter-print +-2021-03-04 16:09:40,930 ChipBLEMgr INFO AdapterName: hci0 AdapterAddress: 00:AA:01:00:00:23 +-``` ++The ++[CHIP Device Controller API documentation offer](https://project-chip.github.io/connectedhomeip-doc/testing/ChipDeviceCtrlAPI.html#chip-chipdevicectrl) ++the full list of available commands. + +-### `ble-debug-log` +- +-> BLE adapter operations is not yet supported in REPL +- +-Enable the Bluetooth LE debug logs. +- +-``` +-chip-device-ctrl > ble-debug-log 1 +-``` +- +-### `ble-scan [-t ] [identifier]` +- +-> BLE adapter operations is not yet supported in REPL +- +-Start a scan action to search for valid CHIP devices over Bluetooth LE (for at +-most _timeout_ seconds). Stop when the device is matching the identifier or the +-counter times out. +- +-``` +-chip-device-ctrl > ble-scan +-2021-05-29 22:28:05,461 ChipBLEMgr INFO scanning started +-2021-05-29 22:28:07,206 ChipBLEMgr INFO Name = ChipLight +-2021-05-29 22:28:07,206 ChipBLEMgr INFO ID = f016e23d-0d00-35d5-93e7-588acdbc7e54 +-2021-05-29 22:28:07,207 ChipBLEMgr INFO RSSI = -79 +-2021-05-29 22:28:07,207 ChipBLEMgr INFO Address = E0:4D:84:3C:BB:C3 +-2021-05-29 22:28:07,209 ChipBLEMgr INFO Pairing State = 0 +-2021-05-29 22:28:07,209 ChipBLEMgr INFO Discriminator = 3840 +-2021-05-29 22:28:07,209 ChipBLEMgr INFO Vendor Id = 9050 +-2021-05-29 22:28:07,209 ChipBLEMgr INFO Product Id = 20044 +-2021-05-29 22:28:07,210 ChipBLEMgr INFO Adv UUID = 0000fff6-0000-1000-8000-00805f9b34fb +-2021-05-29 22:28:07,210 ChipBLEMgr INFO Adv Data = 00000f5a234c4e +-2021-05-29 22:28:07,210 ChipBLEMgr INFO +-2021-05-29 22:28:16,246 ChipBLEMgr INFO scanning stopped +-``` +- +-### `set-pairing-thread-credential ` ++### `SetThreadOperationalDataset()` + + Provides the controller with Thread network credentials that will be used in the + device commissioning procedure to configure the device with a Thread interface. + + ``` +-chip-device-ctrl > set-pairing-thread-credential 0e080000000000010000000300001335060004001fffe002084fe76e9a8b5edaf50708fde46f999f0698e20510d47f5027a414ffeebaefa92285cc84fa030f4f70656e5468726561642d653439630102e49c0410b92f8c7fbb4f9f3e08492ee3915fbd2f0c0402a0fff8 ++thread_dataset = bytes.fromhex("0e080000000000010000000300001335060004001fffe002084fe76e9a8b5edaf50708fde46f999f0698e20510d47f5027a414ffeebaefa92285cc84fa030f4f70656e5468726561642d653439630102e49c0410b92f8c7fbb4f9f3e08492ee3915fbd2f0c0402a0fff8") ++devCtrl.SetThreadOperationalDataset(thread_dataset) + ``` + +-**REPL Commands:** +-`devCtrl.SetThreadOperationalDataset(bytes.FromHex("0e080000000000010000000300001335060004001fffe002084fe76e9a8b5edaf50708fde46f999f0698e20510d47f5027a414ffeebaefa92285cc84fa030f4f70656e5468726561642d653439630102e49c0410b92f8c7fbb4f9f3e08492ee3915fbd2f0c0402a0fff8"))` +- +-### `set-pairing-wifi-credential ` ++### `SetWiFiCredentials(: str, : str)` + + Provides the controller with Wi-Fi network credentials that will be used in the + device commissioning procedure to configure the device with a Wi-Fi interface. + + ``` +-chip-device-ctrl > set-pairing-wifi-credential TESTSSID P455W4RD ++devCtrl.SetWiFiCredentials('TESTSSID', 'P455W4RD') + ``` + +-**REPL Commands:** `devCtrl.SetWiFiCredentials('TESTSSID', 'P455W4RD')` +- +-### `connect -ip
    []` +- +-Do key exchange and establish a secure session between controller and device +-using IP transport. +- +-The Node ID will be used by controller to distinguish multiple devices. This +-does not match the spec and will be removed later. The nodeid will not be +-persisted by controller / device. +- +-If no nodeid given, a random Node ID will be used. +- +-**REPL Commands:** +-`devCtrl.CommissionIP(b'', , )` +- +-### `connect -ble []` +- +-Do key exchange and establish a secure session between controller and device +-using Bluetooth LE transport. +- +-The Node ID will be used by controller to distinguish multiple devices. This +-does not match the spec and will be removed later. The nodeid will not be +-persisted by controller / device. +- +-If no nodeid given, a random Node ID will be used. +- +-**REPL Commands:** +-`devCtrl.ConnectBLE(, , )` ++### `CommissionWithCode(: str, : int, : DiscoveryType)` + +-### `close-session ` ++Commission with the given nodeid from the setupPayload. setupPayload may be a QR ++or the manual setup code. + +-If case there exists an open session (PASE or CASE) to the device with a given +-Node ID, mark it as expired. +- +-**REPL Commands:** `devCtrl.CloseSession()` +- +-### `discover` +- +-> To be implemented in REPL +- +-Discover available Matter accessory devices: +- +-``` +-chip-device-ctrl > discover -all + ``` +- +-### `resolve ` +- +-> To be implemented in REPL +- +-Resolve DNS-SD name corresponding with the given Node ID and update address of +-the node in the device controller: +- +-``` +-chip-device-ctrl > resolve 1234 ++devCtrl.CommissionWithCode("MT:-24J0AFN00KA0648G00", 1234) + ``` + +-### `setup-payload generate [-v ] [-p ] [-cf ] [-dc ] [-dv ] [-ps ]` +- +-> To be implemented in REPL ++### `SendCommand(: int, : int, Clusters..Commands.())` + +-Print the generated Onboarding Payload Contents in human-readable (Manual +-Pairing Code) and machine-readable (QR Code) format: ++Send a Matter command to the device. For example: + ++```python ++commandToSend = Clusters.LevelControl.Commands.MoveWithOnOff(moveMode=1, rate=2, optionsMask=0, optionsOverride=0) ++await devCtrl.SendCommand(1234, 1, commandToSend) + ``` +-chip-device-ctrl > setup-payload generate -v 9050 -p 65279 -cf 0 -dc 2 -dv 2976 -ps 34567890 +-Manual pairing code: [26318621095] +-SetupQRCode: [MT:YNJV7VSC00CMVH7SR00] +-``` +- +-### `setup-payload parse-manual ` +- +-> To be implemented in REPL +- +-Print the commissioning information encoded in the Manual Pairing Code: +- +-``` +-chip-device-ctrl > setup-payload parse-manual 34970112332 +-Version: 0 +-VendorID: 0 +-ProductID: 0 +-CommissioningFlow: 0 +-RendezvousInformation: 0 +-Discriminator: 3840 +-SetUpPINCode: 20202021 +-``` +- +-### `setup-payload parse-qr ` + +-> To be implemented in REPL +- +-Print the commissioning information encoded in the QR Code payload: ++To see available arguments just create a command object without argument: + + ``` +-chip-device-ctrl > setup-payload parse-qr "VP:vendorpayload%MT:W0GU2OTB00KA0648G00" +-Version: 0 +-VendorID: 9050 +-ProductID: 20043 +-CommissioningFlow: 0 +-RendezvousInformation: 2 [BLE] +-Discriminator: 3840 +-SetUpPINCode: 20202021 ++Clusters.LevelControl.Commands.MoveWithOnOff() + ``` + +-### `zcl [arguments]` +- +-Send a ZCL command to the device. For example: ++Shows which arguments are available: + + ``` +-chip-device-ctrl > zcl LevelControl MoveWithOnOff 12344321 1 0 moveMode=1 rate=2 ++MoveWithOnOff( ++│ moveMode=0, ++│ rate=Null, ++│ optionsMask=0, ++│ optionsOverride=0 ++) + ``` + +-**Format of arguments** ++### `ReadAttribute(: int, [(: int, Clusters..Attributes.)])` + +-For any integer and char string (null terminated) types, just use `key=value`, +-for example: `rate=2`, `string=123`, `string_2="123 456"` +- +-For byte string type, use `key=encoding:value`, currently, we support `str` and +-`hex` encoding, the `str` encoding will encode a NULL terminated string. For +-example, `networkId=hex:0123456789abcdef` (for +-`[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]`), `ssid=str:Test` (for +-`['T', 'e', 's', 't', 0x00]`). +- +-For boolean type, use `key=True` or `key=False`. +- +-**REPL Commands:** ++Read the value of an attribute. For example: + + ```python +-# await devCtrl.SendCommand(, , Clusters..Commands.()) +-# e.g. +-await devCtrl.SendCommand(12344321, 1, Clusters.LevelControl.Commands.MoveWithOnOff(moveMode=1, rate=2, optionsMask=0, optionsOverride=0)) +-``` +- +-### `zcl ?` +- +-List available clusters: +- +-``` +-chip-device-ctrl > zcl ? +-AccountLogin +-ApplicationBasic +-ApplicationLauncher +-AudioOutput +-BarrierControl +-BasicInformation +-Binding +-BridgedDeviceBasicInformation +-ColorControl +-ContentLaunch +-Descriptor +-DoorLock +-EthernetNetworkDiagnostics +-FixedLabel +-GeneralCommissioning +-GeneralDiagnostics +-GroupKeyManagement +-Groups +-Identify +-KeypadInput +-LevelControl +-LowPower +-MediaInput +-MediaPlayback +-NetworkCommissioning +-OnOff +-OperationalCredentials +-PumpConfigurationAndControl +-RelativeHumidityMeasurement +-ScenesManagement +-SoftwareDiagnostics +-Switch +-Channel +-TargetNavigator +-TemperatureMeasurement +-TestCluster +-Thermostat +-TrustedRootCertificates +-WakeOnLan +-WindowCovering +-``` +- +-**REPL Commands** +- +-Type `Clusters.` and hit TAB +- +-### `zcl ? ` +- +-List available commands in cluster. For example, for _Basic Information_ +-cluster: +- +-``` +-chip-device-ctrl > zcl ? BasicInformation +-DataModelRevision +-VendorName +-VendorID +-ProductName +-ProductID +-UserLabel +-Location +-HardwareVersion +-HardwareVersionString +-SoftwareVersion +-SoftwareVersionString +-ManufacturingDate +-PartNumber +-ProductURL +-ProductLabel +-SerialNumber +-LocalConfigDisabled +-ClusterRevision +-``` +- +-**REPL Commands** +- +-Type `Clusters.(cluster name).Commands.` and hit TAB +- +-### `zclread [arguments]` +- +-Read the value of ZCL attribute. For example: +- +-``` +-chip-device-ctrl > zclread BasicInformation VendorName 1234 1 0 +-``` +- +-**REPL Commands** +- +-```python +-# devCtrl.ReadAttribute(, [(, Clusters..Attributes.)]) +-# e.g. +-await devCtrl.ReadAttribute(1234, [(1, Clusters.BasicInformation.Attributes.VendorName)]) +-``` +- +-### `zclwrite ` +- +-Write the value to a ZCL attribute. For example: +- +-``` +-chip-device-ctrl > zclwrite TestCluster Int8u 1 1 0 1 +-chip-device-ctrl > zclwrite TestCluster Boolean 1 1 0 True +-chip-device-ctrl > zclwrite TestCluster OctetString 1 1 0 str:123123 +-chip-device-ctrl > zclwrite TestCluster CharString 1 1 0 233233 ++await devCtrl.ReadAttribute(1234, [(0, Clusters.BasicInformation.Attributes.VendorName)]) + ``` + +-Note: The format of the value is the same as the format of argument values for +-ZCL cluster commands. ++### `WriteAttribute(: int, [(: int, Clusters..Attributes.(value=))])` + +-**REPL Commands** ++Write a value to an attribute. For example: + + ```python +-# devCtrl.WriteAttribute(, [(, Clusters..Attributes.(value=))]) +-# e.g. +-await devCtrl.WriteAttribute(1, [(1, Clusters.UnitTesting.Attributes.Int8u(value=1))]) +-await devCtrl.WriteAttribute(1, [(1, Clusters.UnitTesting.Attributes.Boolean(value=True))]) +-await devCtrl.WriteAttribute(1, [(1, Clusters.UnitTesting.Attributes.OctetString(value=b'123123\x00'))]) +-await devCtrl.WriteAttribute(1, [(1, Clusters.UnitTesting.Attributes.CharString(value='233233'))]) ++await devCtrl.WriteAttribute(1234, [(1, Clusters.UnitTesting.Attributes.Int8u(value=1))]) ++await devCtrl.WriteAttribute(1234, [(1, Clusters.UnitTesting.Attributes.Boolean(value=True))]) ++await devCtrl.WriteAttribute(1234, [(1, Clusters.UnitTesting.Attributes.OctetString(value=b'123123\x00'))]) ++await devCtrl.WriteAttribute(1234, [(1, Clusters.UnitTesting.Attributes.CharString(value='233233'))]) + ``` + +-### `zclsubscribe ` ++### `ReadAttribute(: int, [(: int, Clusters..Attributes.)], reportInterval=(: int, : int))` + +-Configure ZCL attribute reporting settings. For example: +- +-``` +-chip-device-ctrl > zclsubscribe OccupancySensing Occupancy 1234 1 10 20 +-``` +- +-**REPL Commands** ++Configure Matter attribute reporting settings. For example: + + ```python +-# devCtrl.ReadAttribute(, [(, Clusters..Attributes.)], reportInterval=(, )) +-# e.g. +-await devCtrl.ReadAttribute(1, [(1, Clusters.OccupancySensing.Attributes.Occupancy)], reportInterval=(10, 20)) ++await devCtrl.ReadAttribute(1234, [(1, Clusters.OccupancySensing.Attributes.Occupancy)], reportInterval=(10, 20)) + ``` + +-### `zclsubscribe -shutdown ` +- +-Shutdown an existing attribute subscription. ++To shutdown an existing attribute subscription use the `Shutdown()` function on ++the returned subscription object: + +-``` +-chip-device-ctrl > zclsubscribe -shutdown 0xdeadbeefcafe ++```python ++sub = await devCtrl.ReadAttribute(1234, [(1, Clusters.OccupancySensing.Attributes.Occupancy)], reportInterval=(10, 20)) ++sub.Shutdown() + ``` + +-The subscription id can be obtained from previous subscription messages: ++## Explore Clusters, Attributes and Commands + +-``` +-chip-device-ctrl > zclsubscribe OnOff OnOff 1 1 10 20 +-(omitted messages) +-[1633922898.965587][1117858:1117866] CHIP:DMG: SubscribeResponse = +-[1633922898.965599][1117858:1117866] CHIP:DMG: { +-[1633922898.965610][1117858:1117866] CHIP:DMG: SubscriptionId = 0xdeadbeefcafe, +-[1633922898.965622][1117858:1117866] CHIP:DMG: MinIntervalFloorSeconds = 0xa, +-[1633922898.965633][1117858:1117866] CHIP:DMG: MaxIntervalCeilingSeconds = 0x14, +-[1633922898.965644][1117858:1117866] CHIP:DMG: } +-[1633922898.965662][1117858:1117866] CHIP:ZCL: SubscribeResponse: +-[1633922898.965673][1117858:1117866] CHIP:ZCL: SubscriptionId: 0xdeadbeefcafe +-[1633922898.965683][1117858:1117866] CHIP:ZCL: ApplicationIdentifier: 0 +-[1633922898.965694][1117858:1117866] CHIP:ZCL: status: EMBER_ZCL_STATUS_SUCCESS (0x00) +-[1633922898.965709][1117858:1117866] CHIP:ZCL: attributeValue: false +-(omitted messages) +-``` ++In the Python REPL the Clusters and Attributes are classes. The `Clusters` ++module contains all clusters. Tab completion can be used to explore available ++clusters, attributes and commands. + +-The subscription id is `0xdeadbeefcafe` in this case ++For example, to get a list of Clusters, type `Clusters.` and hit tab. Continue ++to hit tab to cycle through the available Clusters. Pressing return will select ++the Cluster. + +-**REPL Commands** ++To explore Attributes, use the same technique but with the Attributes sub-class ++of the Clusters class, for example, type `Clusters.(cluster name).Attributes.` ++and hit tab. + +-```python +-# SubscriptionTransaction.Shutdown() +-# e.g. +-sub = await devCtrl.ReadAttribute(1, [(1, Clusters.OccupancySensing.Attributes.Occupancy)], reportInterval=(10, 20)) +-sub.Shutdown() +-``` ++The same is true for Commands, use the Commands sub-class. type ++`Clusters.(cluster name).Commands.` and hit tab. +diff --git a/examples/lighting-app/infineon/cyw30739/README.md b/examples/lighting-app/infineon/cyw30739/README.md +index f085272b1c..ac11bb3033 100644 +--- a/examples/lighting-app/infineon/cyw30739/README.md ++++ b/examples/lighting-app/infineon/cyw30739/README.md +@@ -104,19 +104,7 @@ Put the CYW30739 in to the recovery mode before running the flash script. + [Openthread_border_router](https://github.com/project-chip/connectedhomeip/blob/master/docs/guides/openthread_border_router_pi.md) + for more information on how to setup a border router on a raspberryPi. + +-- You can provision and control the Chip device using the python controller, +- Chip tool standalone, Android or iOS app ++- You can provision and control the device using the Python controller REPL, ++ chip-tool standalone, Android or iOS app + + [Python Controller](https://github.com/project-chip/connectedhomeip/blob/master/src/controller/python/README.md) +- +- Here is an example with the Python controller: +- +- ```bash +- $ chip-device-ctrl +- chip-device-ctrl > connect -ble 3840 20202021 1234 +- chip-device-ctrl > zcl NetworkCommissioning AddThreadNetwork 1234 0 0 operationalDataset=hex:0e080000000000000000000300000b35060004001fffe00208dead00beef00cafe0708fddead00beef000005108e11d8ea8ffaa875713699f59e8807e0030a4f70656e5468726561640102c2980410edc641eb63b100b87e90a9980959befc0c0402a0fff8 breadcrumb=0 timeoutMs=1000 +- chip-device-ctrl > zcl NetworkCommissioning EnableNetwork 1234 0 0 networkID=hex:dead00beef00cafe breadcrumb=0 timeoutMs=1000 +- chip-device-ctrl > close-ble +- chip-device-ctrl > resolve 1234 +- chip-device-ctrl > zcl OnOff Toggle 1234 1 0 +- ``` +diff --git a/examples/lighting-app/python/README.md b/examples/lighting-app/python/README.md +index 8e34e59dea..a935a165c9 100644 +--- a/examples/lighting-app/python/README.md ++++ b/examples/lighting-app/python/README.md +@@ -32,17 +32,6 @@ cd examples/lighting-app/python + python lighting.py + ``` + +-Control the Python lighting matter device: ++Control the Python lighting matter device using the Python controller REPL: + +-```shell +-source ./out/python_env/bin/activate +- +-chip-device-ctrl +- +-chip-device-ctrl > connect -ble 3840 20202021 12344321 +-chip-device-ctrl > zcl NetworkCommissioning AddOrUpdateWiFiNetwork 12344321 0 0 ssid=str:YOUR_SSID credentials=str:YOUR_PASSWORD breadcrumb=0 +-chip-device-ctrl > zcl NetworkCommissioning ConnectNetwork 12344321 0 0 networkID=str:YOUR_SSID breadcrumb=0 +-chip-device-ctrl > close-ble +-chip-device-ctrl > resolve 5544332211 1 (pass appropriate fabric ID and node ID, you can get this from get-fabricid) +-chip-device-ctrl > zcl OnOff Toggle 12344321 1 0 +-``` ++[Python Controller](https://github.com/project-chip/connectedhomeip/blob/master/src/controller/python/README.md) +diff --git a/examples/lock-app/infineon/cyw30739/README.md b/examples/lock-app/infineon/cyw30739/README.md +index b91f4b9157..86c9cf0490 100644 +--- a/examples/lock-app/infineon/cyw30739/README.md ++++ b/examples/lock-app/infineon/cyw30739/README.md +@@ -104,19 +104,7 @@ Put the CYW30739 in to the recovery mode before running the flash script. + [Openthread_border_router](https://github.com/project-chip/connectedhomeip/blob/master/docs/guides/openthread_border_router_pi.md) + for more information on how to setup a border router on a raspberryPi. + +-- You can provision and control the Chip device using the python controller, +- Chip tool standalone, Android or iOS app ++- You can provision and control the device using the Python controller REPL, ++ chip-tool standalone, Android or iOS app + + [Python Controller](https://github.com/project-chip/connectedhomeip/blob/master/src/controller/python/README.md) +- +- Here is an example with the Python controller: +- +- ```bash +- $ chip-device-ctrl +- chip-device-ctrl > connect -ble 3840 20202021 1234 +- chip-device-ctrl > zcl NetworkCommissioning AddThreadNetwork 1234 0 0 operationalDataset=hex:0e080000000000000000000300000b35060004001fffe00208dead00beef00cafe0708fddead00beef000005108e11d8ea8ffaa875713699f59e8807e0030a4f70656e5468726561640102c2980410edc641eb63b100b87e90a9980959befc0c0402a0fff8 breadcrumb=0 timeoutMs=1000 +- chip-device-ctrl > zcl NetworkCommissioning EnableNetwork 1234 0 0 networkID=hex:dead00beef00cafe breadcrumb=0 timeoutMs=1000 +- chip-device-ctrl > close-ble +- chip-device-ctrl > resolve 1234 +- chip-device-ctrl > zcl OnOff Toggle 1234 1 0 +- ``` +diff --git a/scripts/tools/linux_ip_namespace_setup.sh b/scripts/tools/linux_ip_namespace_setup.sh +index 5d467b4b48..f761eea384 100755 +--- a/scripts/tools/linux_ip_namespace_setup.sh ++++ b/scripts/tools/linux_ip_namespace_setup.sh +@@ -124,7 +124,7 @@ function help() { + echo "sudo /$file_name -r /" + echo "" + echo "Terminal 2:" +- echo "/chip-device-ctrl" ++ echo "/chip-repl" + echo "" + echo "This script requires sudo for setup and requires access to ebtables-legacy" + echo "to set up dual ipv4/ipv6 namespaces. Defaults to ipv6 only." +diff --git a/src/controller/README.md b/src/controller/README.md +index e46c191f55..70a3fa5e8f 100644 +--- a/src/controller/README.md ++++ b/src/controller/README.md +@@ -26,7 +26,7 @@ The POSIX CLI chip-tool is located in + + ### Python + +-The Python chip-device-ctrl is located in ++The Python CHIP Controller library is located in + [../controller/python/](../controller/python). + + ## Feature Overview +diff --git a/src/controller/python/BUILD.gn b/src/controller/python/BUILD.gn +index caa33c3a40..e7b06d808e 100644 +--- a/src/controller/python/BUILD.gn ++++ b/src/controller/python/BUILD.gn +@@ -414,10 +414,7 @@ chip_python_wheel_action("chip-clusters") { + } + + chip_python_wheel_action("chip-repl") { +- py_scripts = [ +- "chip-device-ctrl.py", +- "chip-repl.py", +- ] ++ py_scripts = [ "chip-repl.py" ] + + py_manifest_files = [ + { +diff --git a/src/controller/python/README.md b/src/controller/python/README.md +index 34edea4446..e94a95e158 100644 +--- a/src/controller/python/README.md ++++ b/src/controller/python/README.md +@@ -1,10 +1,14 @@ +-# Python CHIP Device Controller ++# Python CHIP Controller + +-The Python CHIP controller is a tool that allows to commission a Matter device +-into the network and to communicate with it using the Zigbee Cluster Library +-(ZCL) messages. The tool uses the generic [Chip Device Controller](../) library. ++The Python CHIP controller is a library that allows to create a Matter fabric ++and commission Matter devices with it, as well as communicate with commissioned ++devices by reading/subscribing and writing Attributes and sending Commands. The ++Python CHIP controller is based on the native [Chip Device Controller](../) ++library. + +-To learn more about the tool, how to build it and use its commands and advanced ++The Python CHIP Controller comes with a REPL which allows to explore and use the ++Python CHIP controller library from a shell. To learn more about the Python CHIP ++Controller and the REPL, how to build it and use its commands and advanced + features, read the following guides: + + - [Working with Python CHIP Controller](../../../docs/guides/python_chip_controller_building.md) +diff --git a/src/controller/python/chip-device-ctrl.py b/src/controller/python/chip-device-ctrl.py +deleted file mode 100755 +index 469fb2c49f..0000000000 +--- a/src/controller/python/chip-device-ctrl.py ++++ /dev/null +@@ -1,1202 +0,0 @@ +-#!/usr/bin/env python +- +-# +-# Copyright (c) 2020-2021 Project CHIP Authors +-# Copyright (c) 2013-2018 Nest Labs, Inc. +-# 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. +-# +- +-# +-# @file +-# This file implements the Python-based Chip Device Controller Shell. +-# +- +-from __future__ import absolute_import, print_function +- +-import argparse +-import base64 +-import ctypes +-import logging +-import os +-import platform +-import random +-import shlex +-import string +-import sys +-import textwrap +-import time +-import traceback +-import warnings +-from cmd import Cmd +-from optparse import OptionParser, OptionValueError +- +-import chip.logging +-import coloredlogs +-from chip import ChipCommissionableNodeCtrl, ChipStack, exceptions, native +-from chip.setup_payload import SetupPayload +-from rich import pretty, print +- +-# Extend sys.path with one or more directories, relative to the location of the +-# running script, in which the chip package might be found . This makes it +-# possible to run the device manager shell from a non-standard install location, +-# as well as directly from its location the CHIP source tree. +-# +-# Note that relative package locations are prepended to sys.path so as to give +-# the local version of the package higher priority over any version installed in +-# a standard location. +-# +-scriptDir = os.path.dirname(os.path.abspath(__file__)) +-relChipPackageInstallDirs = [ +- ".", +- "../lib/python", +- "../lib/python%s.%s" % (sys.version_info.major, sys.version_info.minor), +- "../lib/Python%s%s" % (sys.version_info.major, sys.version_info.minor), +-] +-for relInstallDir in relChipPackageInstallDirs: +- absInstallDir = os.path.realpath(os.path.join(scriptDir, relInstallDir)) +- if os.path.isdir(os.path.join(absInstallDir, "chip")): +- sys.path.insert(0, absInstallDir) +- +- +-if platform.system() == 'Darwin': +- from chip.ChipCoreBluetoothMgr import CoreBluetoothManager as BleManager +-elif sys.platform.startswith('linux'): +- from chip.ChipBluezMgr import BluezManager as BleManager +- +-# The exceptions for CHIP Device Controller CLI +- +- +-class ChipDevCtrlException(exceptions.ChipStackException): +- pass +- +- +-class ParsingError(ChipDevCtrlException): +- def __init__(self, msg=None): +- self.msg = "Parsing Error: " + msg +- +- def __str__(self): +- return self.msg +- +- +-def DecodeBase64Option(option, opt, value): +- try: +- return base64.standard_b64decode(value) +- except TypeError: +- raise OptionValueError( +- "option %s: invalid base64 value: %r" % (opt, value)) +- +- +-def DecodeHexIntOption(option, opt, value): +- try: +- return int(value, 16) +- except ValueError: +- raise OptionValueError("option %s: invalid value: %r" % (opt, value)) +- +- +-def ParseEncodedString(value): +- if value.find(":") < 0: +- raise ParsingError( +- "value should be encoded in encoding:encodedvalue format") +- enc, encValue = value.split(":", 1) +- if enc == "str": +- return encValue.encode("utf-8") + b'\x00' +- elif enc == "hex": +- return bytes.fromhex(encValue) +- raise ParsingError("only str and hex encoding is supported") +- +- +-def ParseValueWithType(value, type): +- if type == 'int': +- return int(value) +- elif type == 'str': +- return value +- elif type == 'bytes': +- return ParseEncodedString(value) +- elif type == 'bool': +- return (value.upper() not in ['F', 'FALSE', '0']) +- else: +- raise ParsingError('cannot recognize type: {}'.format(type)) +- +- +-def FormatZCLArguments(args, command): +- commandArgs = {} +- for kvPair in args: +- if kvPair.find("=") < 0: +- raise ParsingError("Argument should in key=value format") +- key, value = kvPair.split("=", 1) +- valueType = command.get(key, None) +- commandArgs[key] = ParseValueWithType(value, valueType) +- return commandArgs +- +- +-def ShowColoredWarnings(message, category, filename, lineno, file=None, line=None): +- logging.warning(' %s:%s: %s:%s' % +- (filename, lineno, category.__name__, message)) +- return +- +- +-class DeviceMgrCmd(Cmd): +- def __init__(self, rendezvousAddr=None, controllerNodeId=1, bluetoothAdapter=None): +- self.lastNetworkId = None +- self.replHint = None +- +- pretty.install(indent_guides=True, expand_all=True) +- +- coloredlogs.install(level='DEBUG') +- chip.logging.RedirectToPythonLogging() +- +- logging.getLogger().setLevel(logging.DEBUG) +- warnings.showwarning = ShowColoredWarnings +- +- Cmd.__init__(self) +- +- Cmd.identchars = string.ascii_letters + string.digits + "-" +- +- if sys.stdin.isatty(): +- self.prompt = "chip-device-ctrl > " +- else: +- self.use_rawinput = 0 +- self.prompt = "" +- +- DeviceMgrCmd.command_names.sort() +- +- self.bleMgr = None +- +- self.chipStack = ChipStack.ChipStack( +- bluetoothAdapter=bluetoothAdapter, persistentStoragePath='/tmp/chip-device-ctrl-storage.json') +- self.certificateAuthorityManager = chip.CertificateAuthority.CertificateAuthorityManager(chipStack=self.chipStack) +- self.certificateAuthority = self.certificateAuthorityManager.NewCertificateAuthority() +- self.fabricAdmin = self.certificateAuthority.NewFabricAdmin(vendorId=0xFFF1, fabricId=1) +- self.devCtrl = self.fabricAdmin.NewController( +- nodeId=controllerNodeId, useTestCommissioner=True) +- +- self.commissionableNodeCtrl = ChipCommissionableNodeCtrl.ChipCommissionableNodeController( +- self.chipStack) +- +- # If we are on Linux and user selects non-default bluetooth adapter. +- if sys.platform.startswith("linux") and (bluetoothAdapter is not None): +- try: +- self.bleMgr = BleManager(self.devCtrl) +- self.bleMgr.ble_adapter_select( +- "hci{}".format(bluetoothAdapter)) +- except Exception as ex: +- traceback.print_exc() +- print( +- "Failed to initialize BLE, if you don't have BLE, run chip-device-ctrl with --no-ble") +- raise ex +- +- self.historyFileName = os.path.expanduser( +- "~/.chip-device-ctrl-history") +- +- try: +- import readline +- +- if "libedit" in readline.__doc__: +- readline.parse_and_bind("bind ^I rl_complete") +- readline.set_completer_delims(" ") +- try: +- readline.read_history_file(self.historyFileName) +- except IOError: +- pass +- except ImportError: +- pass +- +- command_names = [ +- "setup-payload", +- +- "ble-scan", +- "ble-adapter-select", +- "ble-adapter-print", +- "ble-debug-log", +- +- "connect", +- "close-ble", +- "close-session", +- "resolve", +- "paseonly", +- "commission", +- "zcl", +- "zclread", +- "zclsubscribe", +- +- "discover", +- +- "set-pairing-wifi-credential", +- "set-pairing-thread-credential", +- +- "open-commissioning-window", +- +- "get-fabricid", +- ] +- +- def parseline(self, line): +- cmd, arg, line = Cmd.parseline(self, line) +- if cmd: +- cmd = self.shortCommandName(cmd) +- line = cmd + " " + arg +- return cmd, arg, line +- +- def completenames(self, text, *ignored): +- return [ +- name + " " +- for name in DeviceMgrCmd.command_names +- if name.startswith(text) or self.shortCommandName(name).startswith(text) +- ] +- +- def shortCommandName(self, cmd): +- return cmd.replace("-", "") +- +- def precmd(self, line): +- if not self.use_rawinput and line != "EOF" and line != "": +- print(">>> " + line) +- return line +- +- def postcmd(self, stop, line): +- if self.replHint is not None: +- print("Try the following command in repl: ") +- print(self.replHint) +- print("") +- self.replHint = None +- if not stop and self.use_rawinput: +- self.prompt = "chip-device-ctrl > " +- return stop +- +- def postloop(self): +- try: +- import readline +- +- try: +- readline.write_history_file(self.historyFileName) +- except IOError: +- pass +- except ImportError: +- pass +- +- def do_help(self, line): +- if line: +- cmd, arg, unused = self.parseline(line) +- try: +- doc = getattr(self, "do_" + cmd).__doc__ +- except AttributeError: +- doc = None +- if doc: +- self.stdout.write("%s\n" % textwrap.dedent(doc)) +- else: +- self.stdout.write("No help on %s\n" % (line)) +- else: +- self.print_topics( +- "\nAvailable commands (type help for more information):", +- DeviceMgrCmd.command_names, +- 15, +- 80, +- ) +- +- def do_closeble(self, line): +- """ +- close-ble +- +- Close the ble connection to the device. +- """ +- +- warnings.warn( +- "This method is being deprecated. " +- "Please use the DeviceController.CloseBLEConnection method directly in the REPL", DeprecationWarning) +- +- args = shlex.split(line) +- +- if len(args) != 0: +- print("Usage:") +- self.do_help("close") +- return +- +- try: +- self.devCtrl.CloseBLEConnection() +- except exceptions.ChipStackException as ex: +- print(str(ex)) +- +- def do_setlogoutput(self, line): +- """ +- set-log-output [ none | error | progress | detail ] +- +- Set the level of Chip logging output. +- """ +- +- warnings.warn( +- "This method is being deprecated. " +- "Please use the DeviceController.SetLogFilter method directly in the REPL", DeprecationWarning) +- +- args = shlex.split(line) +- +- if len(args) == 0: +- print("Usage:") +- self.do_help("set-log-output") +- return +- if len(args) > 1: +- print("Unexpected argument: " + args[1]) +- return +- +- category = args[0].lower() +- if category == "none": +- category = 0 +- elif category == "error": +- category = 1 +- elif category == "progress": +- category = 2 +- elif category == "detail": +- category = 3 +- else: +- print("Invalid argument: " + args[0]) +- return +- +- try: +- self.devCtrl.SetLogFilter(category) +- except exceptions.ChipStackException as ex: +- print(str(ex)) +- return +- +- def do_setuppayload(self, line): +- """ +- setup-payload generate [options] +- +- Options: +- -vr Version +- -vi Vendor ID +- -pi Product ID +- -cf Custom Flow [Standard = 0, UserActionRequired = 1, Custom = 2] +- -dc Discovery Capabilities [SoftAP = 1 | BLE = 2 | OnNetwork = 4] +- -dv Discriminator Value +- -ps Passcode +- +- setup-payload parse-manual +- setup-payload parse-qr +- """ +- +- warnings.warn( +- "This method is being deprecated. " +- "Please use the SetupPayload function in the chip.setup_payload package directly", DeprecationWarning) +- +- try: +- arglist = shlex.split(line) +- if arglist[0] not in ("generate", "parse-manual", "parse-qr"): +- self.do_help("setup-payload") +- return +- +- if arglist[0] == "generate": +- parser = argparse.ArgumentParser() +- parser.add_argument("-vr", type=int, default=0, dest='version') +- parser.add_argument( +- "-pi", type=int, default=0, dest='productId') +- parser.add_argument( +- "-vi", type=int, default=0, dest='vendorId') +- parser.add_argument( +- '-cf', type=int, default=0, dest='customFlow') +- parser.add_argument( +- "-dc", type=int, default=0, dest='capabilities') +- parser.add_argument( +- "-dv", type=int, default=0, dest='discriminator') +- parser.add_argument("-ps", type=int, dest='passcode') +- args = parser.parse_args(arglist[1:]) +- +- SetupPayload().PrintOnboardingCodes(args.passcode, args.vendorId, args.productId, +- args.discriminator, args.customFlow, args.capabilities, args.version) +- +- if arglist[0] == "parse-manual": +- SetupPayload().ParseManualPairingCode(arglist[1]).Print() +- +- if arglist[0] == "parse-qr": +- SetupPayload().ParseQrCode(arglist[1]).Print() +- +- except exceptions.ChipStackException as ex: +- print(str(ex)) +- return +- +- def do_bleadapterselect(self, line): +- """ +- ble-adapter-select +- +- Start BLE adapter select, deprecated, you can select adapter by command line arguments. +- """ +- if sys.platform.startswith("linux"): +- if not self.bleMgr: +- self.bleMgr = BleManager(self.devCtrl) +- +- self.bleMgr.ble_adapter_select(line) +- print( +- "This change only applies to ble-scan\n" +- "Please run device controller with --bluetooth-adapter= to select adapter\n" + +- "e.g. chip-device-ctrl --bluetooth-adapter hci0" +- ) +- else: +- print( +- "ble-adapter-select only works in Linux, ble-adapter-select mac_address" +- ) +- +- return +- +- def do_bleadapterprint(self, line): +- """ +- ble-adapter-print +- +- Print attached BLE adapter. +- """ +- if sys.platform.startswith("linux"): +- if not self.bleMgr: +- self.bleMgr = BleManager(self.devCtrl) +- +- self.bleMgr.ble_adapter_print() +- else: +- print("ble-adapter-print only works in Linux") +- +- return +- +- def do_bledebuglog(self, line): +- """ +- ble-debug-log 0:1 +- 0: disable BLE debug log +- 1: enable BLE debug log +- """ +- if not self.bleMgr: +- self.bleMgr = BleManager(self.devCtrl) +- +- self.bleMgr.ble_debug_log(line) +- +- return +- +- def do_blescan(self, line): +- """ +- ble-scan +- +- Start BLE scanning operations. +- """ +- +- if not self.bleMgr: +- self.bleMgr = BleManager(self.devCtrl) +- +- self.bleMgr.scan(line) +- +- return +- +- def ConnectFromSetupPayload(self, setupPayload, nodeid): +- # TODO(cecille): Get this from the C++ code? +- ble = 1 << 1 +- # Devices may be uncommissioned, or may already be on the network. Need to check both ways. +- # TODO(cecille): implement soft-ap connection. +- +- # Any device that is already commissioned into a fabric needs to use on-network +- # pairing, so look first on the network regardless of the QR code contents. +- print("Attempting to find device on Network") +- longDiscriminator = ctypes.c_uint16( +- int(setupPayload.attributes['Discriminator'])) +- self.devCtrl.DiscoverCommissionableNodesLongDiscriminator( +- longDiscriminator) +- print("Waiting for device responses...") +- strlen = 100 +- addrStrStorage = ctypes.create_string_buffer(strlen) +- # If this device is on the network and we're looking specifically for 1 device, +- # expect a quick response. +- if self.wait_for_one_discovered_device(): +- self.devCtrl.GetIPForDiscoveredDevice( +- 0, addrStrStorage, strlen) +- addrStr = addrStrStorage.value.decode('utf-8') +- print("Connecting to device at " + addrStr) +- pincode = ctypes.c_uint32( +- int(setupPayload.attributes['SetUpPINCode'])) +- try: +- self.devCtrl.CommissionIP(addrStrStorage, pincode, nodeid) +- print("Connected") +- return 0 +- except Exception as ex: +- print(f"Unable to connect on network: {ex}") +- else: +- print("Unable to locate device on network") +- +- if int(setupPayload.attributes["RendezvousInformation"]) & ble: +- print("Attempting to connect via BLE") +- longDiscriminator = ctypes.c_uint16( +- int(setupPayload.attributes['Discriminator'])) +- pincode = ctypes.c_uint32( +- int(setupPayload.attributes['SetUpPINCode'])) +- try: +- self.devCtrl.ConnectBLE(longDiscriminator, pincode, nodeid) +- print("Connected") +- return 0 +- except Exception as ex: +- print(f"Unable to connect: {ex}") +- return -1 +- +- def do_paseonly(self, line): +- """ +- paseonly -ip [] +- +- TODO: Add more methods to connect to device (like cert for auth, and IP +- for connection) +- """ +- +- try: +- args = shlex.split(line) +- if len(args) <= 1: +- print("Usage:") +- self.do_help("paseonly") +- return +- nodeid = random.randint(1, 1000000) # Just a random number +- if len(args) == 4: +- nodeid = int(args[3]) +- print("Device is assigned with nodeid = {}".format(nodeid)) +- self.replHint = f"devCtrl.EstablishPASESessionIP({repr(args[1])}, {int(args[2])}, {nodeid})" +- if args[0] == "-ip" and len(args) >= 3: +- self.devCtrl.EstablishPASESessionIP(args[1], int(args[2]), nodeid) +- else: +- print("Usage:") +- self.do_help("paseonly") +- return +- print( +- "Device temporary node id (**this does not match spec**): {}".format(nodeid)) +- except Exception as ex: +- print(str(ex)) +- return +- +- def do_commission(self, line): +- """ +- commission nodeid +- +- Runs commissioning on a device that has been connected with paseonly +- """ +- try: +- args = shlex.split(line) +- if len(args) != 1: +- print("Usage:") +- self.do_help("commission") +- return +- nodeid = int(args[0]) +- self.replHint = f"devCtrl.Commission({nodeid})" +- self.devCtrl.Commission(nodeid) +- except Exception as ex: +- print(str(ex)) +- return +- +- def do_connect(self, line): +- """ +- connect -ip [] +- connect -ble [] +- connect -qr [] +- connect -code [] +- +- connect command is used for establishing a rendezvous session to the device. +- currently, only connect using setupPinCode is supported. +- -qr option will connect to the first device with a matching long discriminator. +- +- TODO: Add more methods to connect to device (like cert for auth, and IP +- for connection) +- """ +- +- warnings.warn( +- "This method is being deprecated. " +- "Please use the DeviceController.[ConnectBLE|CommissionIP] methods directly in the REPL", DeprecationWarning) +- +- try: +- args = shlex.split(line) +- if len(args) <= 1: +- print("Usage:") +- self.do_help("connect SetupPinCode") +- return +- +- nodeid = random.randint(1, 1000000) # Just a random number +- if len(args) == 4: +- nodeid = int(args[3]) +- print("Device is assigned with nodeid = {}".format(nodeid)) +- +- if args[0] == "-ip" and len(args) >= 3: +- self.replHint = f"devCtrl.CommissionIP({repr(args[1])}, {int(args[2])}, {nodeid})" +- self.devCtrl.CommissionIP(args[1], int(args[2]), nodeid) +- elif args[0] == "-ble" and len(args) >= 3: +- self.replHint = f"devCtrl.ConnectBLE({int(args[1])}, {int(args[2])}, {nodeid})" +- self.devCtrl.ConnectBLE(int(args[1]), int(args[2]), nodeid) +- elif args[0] in ['-qr', '-code'] and len(args) >= 2: +- if len(args) == 3: +- nodeid = int(args[2]) +- print("Parsing QR code {}".format(args[1])) +- +- setupPayload = None +- if args[0] == '-qr': +- setupPayload = SetupPayload().ParseQrCode(args[1]) +- elif args[0] == '-code': +- setupPayload = SetupPayload( +- ).ParseManualPairingCode(args[1]) +- +- if not int(setupPayload.attributes.get("RendezvousInformation", 0)): +- print("No rendezvous information provided, default to all.") +- setupPayload.attributes["RendezvousInformation"] = 0b111 +- setupPayload.Print() +- self.replHint = f"devCtrl.CommissionWithCode(setupPayload={repr(setupPayload)}, nodeid={nodeid})" +- self.ConnectFromSetupPayload(setupPayload, nodeid) +- else: +- print("Usage:") +- self.do_help("connect SetupPinCode") +- return +- print( +- "Device temporary node id (**this does not match spec**): {}".format(nodeid)) +- except exceptions.ChipStackException as ex: +- print(str(ex)) +- return +- +- def do_closesession(self, line): +- """ +- close-session +- +- Close any session associated with a given node ID. +- """ +- try: +- parser = argparse.ArgumentParser() +- parser.add_argument('nodeid', type=int, help='Peer node ID') +- args = parser.parse_args(shlex.split(line)) +- self.replHint = f"devCtrl.CloseSession({args.nodeid})" +- self.devCtrl.CloseSession(args.nodeid) +- except exceptions.ChipStackException as ex: +- print(str(ex)) +- except Exception: +- self.do_help("close-session") +- +- def do_resolve(self, line): +- """ +- resolve +- +- Resolve DNS-SD name corresponding with the given node ID and +- update address of the node in the device controller. +- """ +- try: +- args = shlex.split(line) +- if len(args) == 1: +- try: +- self.replHint = f"devCtrl.ResolveNode({int(args[0])});devCtrl.GetAddressAndPort({int(args[0])})" +- self.devCtrl.ResolveNode(int(args[0])) +- address = self.devCtrl.GetAddressAndPort(int(args[0])) +- address = "{}:{}".format( +- *address) if address else "unknown" +- print("Current address: " + address) +- except exceptions.ChipStackException as ex: +- print(str(ex)) +- else: +- self.do_help("resolve") +- except exceptions.ChipStackException as ex: +- print(str(ex)) +- return +- +- def wait_for_one_discovered_device(self): +- print("Waiting for device responses...") +- strlen = 100 +- addrStrStorage = ctypes.create_string_buffer(strlen) +- count = 0 +- maxWaitTime = 2 +- while (not self.devCtrl.GetIPForDiscoveredDevice(0, addrStrStorage, strlen) and count < maxWaitTime): +- time.sleep(0.2) +- count = count + 0.2 +- return count < maxWaitTime +- +- def wait_for_many_discovered_devices(self): +- # Discovery happens through mdns, which means we need to wait for responses to come back. +- # TODO(cecille): I suppose we could make this a command line arg. Or Add a callback when +- # x number of responses are received. For now, just 2 seconds. We can all wait that long. +- print("Waiting for device responses...") +- time.sleep(2) +- +- def do_discover(self, line): +- """ +- discover -qr qrcode +- discover -all +- discover -l long_discriminator +- discover -s short_discriminator +- discover -v vendor_id +- discover -t device_type +- discover -c +- +- discover command is used to discover available devices. +- """ +- try: +- arglist = shlex.split(line) +- if len(arglist) < 1: +- print("Usage:") +- self.do_help("discover") +- return +- parser = argparse.ArgumentParser() +- group = parser.add_mutually_exclusive_group() +- group.add_argument( +- '-all', help='discover all commissionable nodes and commissioners', action='store_true') +- group.add_argument( +- '-qr', help='discover commissionable nodes matching provided QR code', type=str) +- group.add_argument( +- '-l', help='discover commissionable nodes with given long discriminator', type=int) +- group.add_argument( +- '-s', help='discover commissionable nodes with given short discriminator', type=int) +- group.add_argument( +- '-v', help='discover commissionable nodes with given vendor ID', type=int) +- group.add_argument( +- '-t', help='discover commissionable nodes with given device type', type=int) +- group.add_argument( +- '-c', help='discover commissionable nodes in commissioning mode', action='store_true') +- args = parser.parse_args(arglist) +- if args.all: +- self.commissionableNodeCtrl.DiscoverCommissioners() +- self.wait_for_many_discovered_devices() +- self.commissionableNodeCtrl.PrintDiscoveredCommissioners() +- self.devCtrl.DiscoverAllCommissioning() +- self.wait_for_many_discovered_devices() +- elif args.qr is not None: +- setupPayload = SetupPayload().ParseQrCode(args.qr) +- longDiscriminator = ctypes.c_uint16( +- int(setupPayload.attributes['Discriminator'])) +- self.devCtrl.DiscoverCommissionableNodesLongDiscriminator( +- longDiscriminator) +- self.wait_for_one_discovered_device() +- elif args.l is not None: +- self.devCtrl.DiscoverCommissionableNodesLongDiscriminator( +- ctypes.c_uint16(args.l)) +- self.wait_for_one_discovered_device() +- elif args.s is not None: +- self.devCtrl.DiscoverCommissionableNodesShortDiscriminator( +- ctypes.c_uint16(args.s)) +- self.wait_for_one_discovered_device() +- elif args.v is not None: +- self.devCtrl.DiscoverCommissionableNodesVendor( +- ctypes.c_uint16(args.v)) +- self.wait_for_many_discovered_devices() +- elif args.t is not None: +- self.devCtrl.DiscoverCommissionableNodesDeviceType( +- ctypes.c_uint16(args.t)) +- self.wait_for_many_discovered_devices() +- elif args.c is not None: +- self.devCtrl.DiscoverCommissionableNodesCommissioningEnabled() +- self.wait_for_many_discovered_devices() +- else: +- self.do_help("discover") +- return +- self.devCtrl.PrintDiscoveredDevices() +- except exceptions.ChipStackException as ex: +- print('exception') +- print(str(ex)) +- return +- except Exception: +- self.do_help("discover") +- return +- +- def do_zcl(self, line): +- """ +- To send ZCL message to device: +- zcl [key=value]... +- To get a list of clusters: +- zcl ? +- To get a list of commands in cluster: +- zcl ? +- +- Send ZCL command to device nodeid +- """ +- try: +- args = shlex.split(line) +- all_commands = self.devCtrl.ZCLCommandList() +- if len(args) == 1 and args[0] == '?': +- print('\n'.join(all_commands.keys())) +- elif len(args) == 2 and args[0] == '?': +- if args[1] not in all_commands: +- raise exceptions.UnknownCluster(args[1]) +- for commands in all_commands.get(args[1]).items(): +- args = ", ".join(["{}: {}".format(argName, argType) +- for argName, argType in commands[1].items()]) +- print(commands[0]) +- if commands[1]: +- print(" ", args) +- else: +- print(" ") +- elif len(args) > 4: +- if args[0] not in all_commands: +- raise exceptions.UnknownCluster(args[0]) +- command = all_commands.get(args[0]).get(args[1], None) +- # When command takes no arguments, (not command) is True +- if command is None: +- raise exceptions.UnknownCommand(args[0], args[1]) +- req = eval(f"Clusters.{args[0]}.Commands.{args[1]}")(**FormatZCLArguments(args[5:], command)) +- self.replHint = f"await devCtrl.SendCommand({int(args[2])}, {int(args[3])}, Clusters.{repr(req)})" +- err, res = self.devCtrl.ZCLSend(args[0], args[1], int( +- args[2]), int(args[3]), int(args[4]), FormatZCLArguments(args[5:], command), blocking=True) +- if err != 0: +- print("Failed to receive command response: {}".format(res)) +- elif res is not None: +- print("Received command status response:") +- print(res) +- else: +- print("Success, no status code is attached with response.") +- else: +- self.do_help("zcl") +- except exceptions.ChipStackException as ex: +- print("An exception occurred during process ZCL command:") +- print(str(ex)) +- except Exception as ex: +- print("An exception occurred during processing input:") +- traceback.print_exc() +- print(str(ex)) +- +- def do_zclread(self, line): +- """ +- To read ZCL attribute: +- zclread +- """ +- try: +- args = shlex.split(line) +- all_attrs = self.devCtrl.ZCLAttributeList() +- if len(args) == 1 and args[0] == '?': +- print('\n'.join(all_attrs.keys())) +- elif len(args) == 2 and args[0] == '?': +- if args[1] not in all_attrs: +- raise exceptions.UnknownCluster(args[1]) +- print('\n'.join(all_attrs.get(args[1]).keys())) +- elif len(args) == 5: +- if args[0] not in all_attrs: +- raise exceptions.UnknownCluster(args[0]) +- self.replHint = (f"await devCtrl.ReadAttribute({int(args[2])}, [({int(args[3])}, " +- f"Clusters.{args[0]}.Attributes.{args[1]})])") +- res = self.devCtrl.ZCLReadAttribute(args[0], args[1], int( +- args[2]), int(args[3]), int(args[4])) +- if res is not None: +- print(repr(res)) +- else: +- self.do_help("zclread") +- except exceptions.ChipStackException as ex: +- print("An exception occurred during reading ZCL attribute:") +- print(str(ex)) +- except Exception as ex: +- print("An exception occurred during processing input:") +- print(str(ex)) +- +- def do_zclwrite(self, line): +- """ +- To write ZCL attribute: +- zclwrite +- """ +- try: +- args = shlex.split(line) +- all_attrs = self.devCtrl.ZCLAttributeList() +- if len(args) == 1 and args[0] == '?': +- print('\n'.join(all_attrs.keys())) +- elif len(args) == 2 and args[0] == '?': +- if args[1] not in all_attrs: +- raise exceptions.UnknownCluster(args[1]) +- cluster_attrs = all_attrs.get(args[1], {}) +- print('\n'.join(["{}: {}".format(key, cluster_attrs[key]["type"]) +- for key in cluster_attrs.keys() if cluster_attrs[key].get("writable", False)])) +- elif len(args) == 6: +- if args[0] not in all_attrs: +- raise exceptions.UnknownCluster(args[0]) +- attribute_type = all_attrs.get(args[0], {}).get( +- args[1], {}).get("type", None) +- self.replHint = ( +- f"await devCtrl.WriteAttribute({int(args[2])}, [({int(args[3])}, " +- f"Clusters.{args[0]}.Attributes.{args[1]}(value={repr(ParseValueWithType(args[5], attribute_type))}))])") +- res = self.devCtrl.ZCLWriteAttribute(args[0], args[1], int( +- args[2]), int(args[3]), int(args[4]), ParseValueWithType(args[5], attribute_type)) +- print(repr(res)) +- else: +- self.do_help("zclwrite") +- except exceptions.ChipStackException as ex: +- print("An exception occurred during writing ZCL attribute:") +- print(str(ex)) +- except Exception as ex: +- print("An exception occurred during processing input:") +- print(str(ex)) +- +- def do_zclsubscribe(self, line): +- """ +- To subscribe ZCL attribute reporting: +- zclsubscribe +- +- To shut down a subscription: +- zclsubscribe -shutdown +- """ +- try: +- args = shlex.split(line) +- all_attrs = self.devCtrl.ZCLAttributeList() +- if len(args) == 1 and args[0] == '?': +- print('\n'.join(all_attrs.keys())) +- elif len(args) == 2 and args[0] == '?': +- if args[1] not in all_attrs: +- raise exceptions.UnknownCluster(args[1]) +- cluster_attrs = all_attrs.get(args[1], {}) +- print('\n'.join([key for key in cluster_attrs.keys( +- ) if cluster_attrs[key].get("reportable", False)])) +- elif len(args) == 6: +- if args[0] not in all_attrs: +- raise exceptions.UnknownCluster(args[0]) +- res = self.devCtrl.ZCLSubscribeAttribute(args[0], args[1], int( +- args[2]), int(args[3]), int(args[4]), int(args[5])) +- self.replHint = (f"sub = await devCtrl.ReadAttribute({int(args[2])}, [({int(args[3])}, " +- f"Clusters.{args[0]}.Attributes.{args[1]})], reportInterval=({int(args[4])}, {int(args[5])}))") +- print(res.GetAllValues()) +- print(f"Subscription Established: {res}") +- elif len(args) == 2 and args[0] == '-shutdown': +- subscriptionId = int(args[1], base=0) +- self.replHint = "You can call sub.Shutdown() (sub is the return value of ReadAttribute() called before)" +- self.devCtrl.ZCLShutdownSubscription(subscriptionId) +- else: +- self.do_help("zclsubscribe") +- except exceptions.ChipStackException as ex: +- print("An exception occurred during configuring reporting of ZCL attribute:") +- print(str(ex)) +- except Exception as ex: +- print("An exception occurred during processing input:") +- print(str(ex)) +- +- def do_setpairingwificredential(self, line): +- """ +- set-pairing-wifi-credential ssid credentials +- """ +- try: +- args = shlex.split(line) +- if len(args) < 2: +- print("Usage:") +- self.do_help("set-pairing-wifi-credential") +- return +- self.devCtrl.SetWiFiCredentials( +- args[0], args[1]) +- self.replHint = f"devCtrl.SetWiFiCredentials({repr(args[0])}, {repr(args[1])})" +- except Exception as ex: +- print(str(ex)) +- return +- +- def do_setpairingthreadcredential(self, line): +- """ +- set-pairing-thread-credential threadOperationalDataset +- """ +- try: +- args = shlex.split(line) +- if len(args) < 1: +- print("Usage:") +- self.do_help("set-pairing-thread-credential") +- return +- self.replHint = f"devCtrl.SetThreadOperationalDataset(bytes.fromhex({repr(args[0])}))" +- self.devCtrl.SetThreadOperationalDataset(bytes.fromhex(args[0])) +- except Exception as ex: +- print(str(ex)) +- return +- +- def do_opencommissioningwindow(self, line): +- """ +- open-commissioning-window [options] +- +- Options: +- -t Timeout (in seconds) +- -o Option [TokenWithRandomPIN = 1, TokenWithProvidedPIN = 2] +- -d Discriminator Value +- -i Iteration +- +- This command is used by a current Administrator to instruct a Node to go into commissioning mode +- """ +- try: +- arglist = shlex.split(line) +- +- if len(arglist) <= 1: +- print("Usage:") +- self.do_help("open-commissioning-window") +- return +- parser = argparse.ArgumentParser() +- parser.add_argument( +- "-t", type=int, default=0, dest='timeout') +- parser.add_argument( +- "-o", type=int, default=1, dest='option') +- parser.add_argument( +- "-i", type=int, default=0, dest='iteration') +- parser.add_argument( +- "-d", type=int, default=0, dest='discriminator') +- args = parser.parse_args(arglist[1:]) +- +- if args.option < 1 or args.option > 2: +- print("Invalid option specified!") +- raise ValueError("Invalid option specified") +- +- self.replHint = (f"devCtrl.OpenCommissioningWindow(nodeid={int(arglist[0])}, timeout={args.timeout}, " +- f"iteration={args.iteration}, discriminator={args.discriminator}, option={args.option})") +- +- self.devCtrl.OpenCommissioningWindow( +- int(arglist[0]), args.timeout, args.iteration, args.discriminator, args.option) +- +- except exceptions.ChipStackException as ex: +- print(str(ex)) +- return +- except Exception: +- self.do_help("open-commissioning-window") +- return +- +- def do_getfabricid(self, line): +- """ +- get-fabricid +- +- Read the current Compressed Fabric Id of the controller device, return 0 if not available. +- """ +- try: +- args = shlex.split(line) +- +- if (len(args) > 0): +- print("Unexpected argument: " + args[1]) +- return +- +- compressed_fabricid = self.devCtrl.GetCompressedFabricId() +- raw_fabricid = self.devCtrl.fabricId +- +- self.replHint = "devCtrl.GetCompressedFabricId(), devCtrl.fabricId" +- except exceptions.ChipStackException as ex: +- print("An exception occurred during reading FabricID:") +- print(str(ex)) +- return +- +- print("Get fabric ID complete") +- +- print("Raw Fabric ID: 0x{:016x}".format(raw_fabricid) +- + " (" + str(raw_fabricid) + ")") +- +- print("Compressed Fabric ID: 0x{:016x}".format(compressed_fabricid) +- + " (" + str(compressed_fabricid) + ")") +- +- def do_history(self, line): +- """ +- history +- +- Show previously executed commands. +- """ +- +- try: +- import readline +- +- h = readline.get_current_history_length() +- for n in range(1, h + 1): +- print(readline.get_history_item(n)) +- except ImportError: +- pass +- +- def do_h(self, line): +- self.do_history(line) +- +- def do_exit(self, line): +- return True +- +- def do_quit(self, line): +- return True +- +- def do_q(self, line): +- return True +- +- def do_EOF(self, line): +- print() +- return True +- +- def emptyline(self): +- pass +- +- +-def main(): +- optParser = OptionParser() +- optParser.add_option( +- "-r", +- "--rendezvous-addr", +- action="store", +- dest="rendezvousAddr", +- help="Device rendezvous address", +- metavar="", +- ) +- optParser.add_option( +- "-n", +- "--controller-nodeid", +- action="store", +- dest="controllerNodeId", +- default=1, +- type='int', +- help="Controller node ID", +- metavar="", +- ) +- +- if sys.platform.startswith("linux"): +- optParser.add_option( +- "-b", +- "--bluetooth-adapter", +- action="store", +- dest="bluetoothAdapter", +- default="hci0", +- type="str", +- help="Controller bluetooth adapter ID, use --no-ble to disable bluetooth functions.", +- metavar="", +- ) +- optParser.add_option( +- "--no-ble", +- action="store_true", +- dest="disableBluetooth", +- help="Disable bluetooth, calling BLE related feature with this flag results in undefined behavior.", +- ) +- (options, remainingArgs) = optParser.parse_args(sys.argv[1:]) +- +- if len(remainingArgs) != 0: +- print("Unexpected argument: %s" % remainingArgs[0]) +- sys.exit(-1) +- +- adapterId = None +- if sys.platform.startswith("linux"): +- if options.disableBluetooth: +- adapterId = None +- elif not options.bluetoothAdapter.startswith("hci"): +- print( +- "Invalid bluetooth adapter: {}, adapter name looks like hci0, hci1 etc.") +- sys.exit(-1) +- else: +- try: +- adapterId = int(options.bluetoothAdapter[3:]) +- except ValueError: +- print( +- "Invalid bluetooth adapter: {}, adapter name looks like hci0, hci1 etc.") +- sys.exit(-1) +- native.Init(bluetoothAdapter=adapterId) +- try: +- devMgrCmd = DeviceMgrCmd(rendezvousAddr=options.rendezvousAddr, +- controllerNodeId=options.controllerNodeId, bluetoothAdapter=adapterId) +- except Exception as ex: +- print(ex) +- print("Failed to bringup CHIPDeviceController CLI") +- sys.exit(1) +- +- print("Chip Device Controller Shell") +- if options.rendezvousAddr: +- print("Rendezvous address set to %s" % options.rendezvousAddr) +- +- # Adapter ID will always be 0 +- if adapterId != 0: +- print("Bluetooth adapter set to hci{}".format(adapterId)) +- print() +- +- try: +- devMgrCmd.cmdloop() +- except KeyboardInterrupt: +- print("\nQuitting") +- +- sys.exit(0) +- +- +-if __name__ == "__main__": +- print(""" +- chip-device-ctrl will be deprecated and will be removed in the future. Please try chip-repl, which provides a lot of features. +- +- - Multi-fabric support, +- - Better complex type support for sending commands, +- - Native command highlight, +- - Parallel commands with asyncio, +- - Writing complex logic inline. +- +- You can still use chip-device-ctrl as usual for now, and you will learn how to do the same thing in chip-repl. +- +- Feel free to file an issue if some features are not supported by chip-repl yet. +- """) +- main() +diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py +index 4d14a42f18..1d1627c46d 100644 +--- a/src/controller/python/chip/ChipDeviceCtrl.py ++++ b/src/controller/python/chip/ChipDeviceCtrl.py +@@ -50,12 +50,9 @@ from . import discovery + from .clusters import Attribute as ClusterAttribute + from .clusters import ClusterObjects as ClusterObjects + from .clusters import Command as ClusterCommand +-from .clusters import Objects as GeneratedObjects + from .clusters.CHIPClusters import ChipClusters + from .crypto import p256keypair +-from .exceptions import UnknownAttribute, UnknownCommand +-from .interaction_model import InteractionModelError, SessionParameters, SessionParametersStruct +-from .interaction_model import delegate as im ++from .interaction_model import SessionParameters, SessionParametersStruct + from .native import PyChipError + + __all__ = ["ChipDeviceController", "CommissioningParameters"] +@@ -1464,83 +1461,6 @@ class ChipDeviceControllerBase(): + else: + return res.events + +- def ZCLSend(self, cluster, command, nodeid, endpoint, groupid, args, blocking=False): +- ''' Wrapper over SendCommand that catches the exceptions +- Returns a tuple of (errorCode, CommandResponse) +- ''' +- self.CheckIsActive() +- +- req = None +- try: +- req = eval( +- f"GeneratedObjects.{cluster}.Commands.{command}")(**args) +- except BaseException: +- raise UnknownCommand(cluster, command) +- try: +- res = asyncio.run(self.SendCommand(nodeid, endpoint, req)) +- logging.debug(f"CommandResponse {res}") +- return (0, res) +- except InteractionModelError as ex: +- return (int(ex.status), None) +- +- def ZCLReadAttribute(self, cluster, attribute, nodeid, endpoint, groupid, blocking=True): +- ''' Wrapper over ReadAttribute for a single attribute +- Returns an AttributeReadResult +- ''' +- self.CheckIsActive() +- +- clusterType = getattr(GeneratedObjects, cluster) +- +- try: +- attributeType = eval( +- f"GeneratedObjects.{cluster}.Attributes.{attribute}") +- except BaseException: +- raise UnknownAttribute(cluster, attribute) +- +- result = asyncio.run(self.ReadAttribute( +- nodeid, [(endpoint, attributeType)])) +- path = ClusterAttribute.AttributePath.from_attribute( +- EndpointId=endpoint, Attribute=attributeType) +- return im.AttributeReadResult(path=im.AttributePath(nodeId=nodeid, endpointId=path.EndpointId, clusterId=path.ClusterId, attributeId=path.AttributeId), +- status=0, value=result[endpoint][clusterType][attributeType], dataVersion=result[endpoint][clusterType][ClusterAttribute.DataVersion]) +- +- def ZCLWriteAttribute(self, cluster: str, attribute: str, nodeid, endpoint, groupid, value, dataVersion=0, blocking=True): +- ''' Wrapper over WriteAttribute for a single attribute +- return PyChipError +- ''' +- req = None +- try: +- req = eval( +- f"GeneratedObjects.{cluster}.Attributes.{attribute}")(value) +- except BaseException: +- raise UnknownAttribute(cluster, attribute) +- +- return asyncio.run(self.WriteAttribute(nodeid, [(endpoint, req, dataVersion)])) +- +- def ZCLSubscribeAttribute(self, cluster, attribute, nodeid, endpoint, minInterval, maxInterval, blocking=True, +- keepSubscriptions=False, autoResubscribe=True): +- ''' Wrapper over ReadAttribute for a single attribute +- Returns a SubscriptionTransaction. See ReadAttribute for more information. +- ''' +- self.CheckIsActive() +- +- req = None +- try: +- req = eval(f"GeneratedObjects.{cluster}.Attributes.{attribute}") +- except BaseException: +- raise UnknownAttribute(cluster, attribute) +- return asyncio.run(self.ReadAttribute(nodeid, [(endpoint, req)], None, False, reportInterval=(minInterval, maxInterval), +- keepSubscriptions=keepSubscriptions, autoResubscribe=autoResubscribe)) +- +- def ZCLCommandList(self): +- self.CheckIsActive() +- return self._Cluster.ListClusterCommands() +- +- def ZCLAttributeList(self): +- self.CheckIsActive() +- +- return self._Cluster.ListClusterAttributes() +- + def SetBlockingCB(self, blockingCB): + self.CheckIsActive() + +diff --git a/src/controller/python/chip/ChipReplStartup.py b/src/controller/python/chip/ChipReplStartup.py +index a49638cb99..b75c77efcd 100644 +--- a/src/controller/python/chip/ChipReplStartup.py ++++ b/src/controller/python/chip/ChipReplStartup.py +@@ -94,6 +94,8 @@ def main(): + "-d", "--debug", help="Set default logging level to debug.", action="store_true") + parser.add_argument( + "-t", "--trust-store", help="Path to the PAA trust store.", action="store", default="./credentials/development/paa-root-certs") ++ parser.add_argument( ++ "-b", "--ble-adapter", help="Set the Bluetooth adapter index.", type=int, default=None) + args = parser.parse_args() + + if not os.path.exists(args.trust_store): +@@ -128,7 +130,7 @@ or run `os.chdir` to the root of your CHIP repository checkout. + # nothing we can do ... things will NOT work + return + +- chip.native.Init() ++ chip.native.Init(bluetoothAdapter=args.ble_adapter) + + global certificateAuthorityManager + global chipStack +diff --git a/src/pybindings/pycontroller/build-chip-wheel.py b/src/pybindings/pycontroller/build-chip-wheel.py +index a61b591d5c..61bdf373e8 100644 +--- a/src/pybindings/pycontroller/build-chip-wheel.py ++++ b/src/pybindings/pycontroller/build-chip-wheel.py +@@ -60,7 +60,6 @@ packageName = args.package_name + chipPackageVer = args.build_number + + installScripts = [ +- # InstalledScriptInfo('chip-device-ctrl.py'), + # InstalledScriptInfo('chip-repl.py'), + ] + +-- +2.45.2 + diff --git a/0007-Python-Remove-obsolete-callback-handling.patch b/0014-Python-Remove-obsolete-callback-handling-33665.patch similarity index 68% rename from 0007-Python-Remove-obsolete-callback-handling.patch rename to 0014-Python-Remove-obsolete-callback-handling-33665.patch index b8cb9c4..cbbe00a 100644 --- a/0007-Python-Remove-obsolete-callback-handling.patch +++ b/0014-Python-Remove-obsolete-callback-handling-33665.patch @@ -1,8 +1,7 @@ -From 20f1c72293991ad01043660b777e53be0992bae5 Mon Sep 17 00:00:00 2001 -Message-ID: <20f1c72293991ad01043660b777e53be0992bae5.1717003814.git.stefan@agner.ch> +From 3997a66e9e436646a4652448bc72309d8afee2bf Mon Sep 17 00:00:00 2001 From: Stefan Agner -Date: Wed, 29 May 2024 19:05:43 +0200 -Subject: [PATCH] [Python] Remove obsolete callback handling +Date: Thu, 30 May 2024 22:41:35 +0200 +Subject: [PATCH] [Python] Remove obsolete callback handling (#33665) The Call() function currently still has some callback handling code the completeEvent and callbackRes variables. These are only used when @@ -16,15 +15,25 @@ However, when calling the SDK from multiple threads, then another Call() Might accidentally release a call to CallAsyncWithCompleteCallback() early. --- - src/controller/python/chip/ChipStack.py | 19 ------------------- - 1 file changed, 19 deletions(-) + src/controller/python/chip/ChipStack.py | 22 +--------------------- + 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/controller/python/chip/ChipStack.py b/src/controller/python/chip/ChipStack.py -index 6df7e41de4..a8f07941a2 100644 +index 35f9e24ef4..3a167bb6bc 100644 --- a/src/controller/python/chip/ChipStack.py +++ b/src/controller/python/chip/ChipStack.py -@@ -164,9 +164,6 @@ class AsyncCallableHandle: - return self._res +@@ -32,8 +32,7 @@ import logging + import os + import sys + import time +-from ctypes import (CFUNCTYPE, POINTER, Structure, c_bool, c_char_p, c_int64, c_uint8, c_uint16, c_uint32, c_ulong, c_void_p, +- py_object, pythonapi) ++from ctypes import CFUNCTYPE, Structure, c_bool, c_char_p, c_int64, c_uint8, c_uint16, c_uint32, c_void_p, py_object, pythonapi + from threading import Condition, Event, Lock + + import chip.native +@@ -194,9 +193,6 @@ class AsyncioCallableHandle: + pythonapi.Py_DecRef(py_object(self)) -_CompleteFunct = CFUNCTYPE(None, c_void_p, c_void_p) @@ -33,7 +42,7 @@ index 6df7e41de4..a8f07941a2 100644 _LogMessageFunct = CFUNCTYPE( None, c_int64, c_int64, c_char_p, c_uint8, c_char_p) _ChipThreadTaskRunnerFunct = CFUNCTYPE(None, py_object) -@@ -241,21 +238,11 @@ class ChipStack(object): +@@ -272,21 +268,11 @@ class ChipStack(object): self.logger.addHandler(logHandler) self.logger.setLevel(logging.DEBUG) @@ -55,13 +64,14 @@ index 6df7e41de4..a8f07941a2 100644 # set by other modules(BLE) that require service by thread while thread blocks. self.blockingCB = None -@@ -357,14 +344,8 @@ class ChipStack(object): +@@ -389,15 +375,9 @@ class ChipStack(object): This function is a wrapper of PostTaskOnChipThread, which includes some handling of application specific logics. Calling this function on CHIP on CHIP mainloop thread will cause deadlock. ''' - # throw error if op in progress - self.callbackRes = None - self.completeEvent.clear() + # TODO: Lock probably no longer necessary, see https://github.com/project-chip/connectedhomeip/issues/33321. with self.networkLock: res = self.PostTaskOnChipThread(callFunct).Wait(timeoutMs) - self.completeEvent.set() @@ -69,7 +79,7 @@ index 6df7e41de4..a8f07941a2 100644 - return self.callbackRes return res - def CallAsync(self, callFunct): + async def CallAsync(self, callFunct, timeoutMs: int = None): -- -2.45.1 +2.45.2 diff --git a/0015-Python-Add-automation-level-to-log-defines-33670.patch b/0015-Python-Add-automation-level-to-log-defines-33670.patch new file mode 100644 index 0000000..9b1cb60 --- /dev/null +++ b/0015-Python-Add-automation-level-to-log-defines-33670.patch @@ -0,0 +1,94 @@ +From 0fe7d8b3d9c920ce7dc970097962dca14f9d8f03 Mon Sep 17 00:00:00 2001 +From: Stefan Agner +Date: Fri, 31 May 2024 15:24:42 +0200 +Subject: [PATCH] [Python] Add "automation" level to log defines (#33670) + +So far the automation log level was missing. Add it to the log level +defines in the logging module. + +While at it, also rename to LOG_CATEGORY (instead of ERROR_CATEGORY) +and remove duplicated log level definitions in ChipStack. +--- + src/controller/python/chip/ChipStack.py | 16 ++++------------ + src/controller/python/chip/logging/__init__.py | 17 +++++++++-------- + 2 files changed, 13 insertions(+), 20 deletions(-) + +diff --git a/src/controller/python/chip/ChipStack.py b/src/controller/python/chip/ChipStack.py +index 3a167bb6bc..beeaedd6ae 100644 +--- a/src/controller/python/chip/ChipStack.py ++++ b/src/controller/python/chip/ChipStack.py +@@ -36,6 +36,7 @@ from ctypes import CFUNCTYPE, Structure, c_bool, c_char_p, c_int64, c_uint8, c_u + from threading import Condition, Event, Lock + + import chip.native ++from chip.logging import LOG_CATEGORY_AUTOMATION, LOG_CATEGORY_DETAIL, LOG_CATEGORY_ERROR, LOG_CATEGORY_PROGRESS + from chip.native import PyChipError + + from .ChipUtility import ChipUtility +@@ -78,23 +79,14 @@ class DeviceStatusStruct(Structure): + class LogCategory(object): + """Debug logging categories used by chip.""" + +- # NOTE: These values must correspond to those used in the chip C++ code. +- Disabled = 0 +- Error = 1 +- Progress = 2 +- Detail = 3 +- Retain = 4 +- + @staticmethod + def categoryToLogLevel(cat): +- if cat == LogCategory.Error: ++ if cat == LOG_CATEGORY_ERROR: + return logging.ERROR +- elif cat == LogCategory.Progress: ++ elif cat == LOG_CATEGORY_PROGRESS: + return logging.INFO +- elif cat == LogCategory.Detail: ++ elif cat in (LOG_CATEGORY_DETAIL, LOG_CATEGORY_AUTOMATION): + return logging.DEBUG +- elif cat == LogCategory.Retain: +- return logging.CRITICAL + else: + return logging.NOTSET + +diff --git a/src/controller/python/chip/logging/__init__.py b/src/controller/python/chip/logging/__init__.py +index 047d3f4f8e..aca671997d 100644 +--- a/src/controller/python/chip/logging/__init__.py ++++ b/src/controller/python/chip/logging/__init__.py +@@ -19,11 +19,12 @@ import logging + from chip.logging.library_handle import _GetLoggingLibraryHandle + from chip.logging.types import LogRedirectCallback_t + +-# Defines match support/logging/Constants.h (LogCategory enum) +-ERROR_CATEGORY_NONE = 0 +-ERROR_CATEGORY_ERROR = 1 +-ERROR_CATEGORY_PROGRESS = 2 +-ERROR_CATEGORY_DETAIL = 3 ++# Defines match src/lib/support/logging/Constants.h (LogCategory enum) ++LOG_CATEGORY_NONE = 0 ++LOG_CATEGORY_ERROR = 1 ++LOG_CATEGORY_PROGRESS = 2 ++LOG_CATEGORY_DETAIL = 3 ++LOG_CATEGORY_AUTOMATION = 4 + + + @LogRedirectCallback_t +@@ -34,11 +35,11 @@ def _RedirectToPythonLogging(category, module, message): + + logger = logging.getLogger('chip.native.%s' % module) + +- if category == ERROR_CATEGORY_ERROR: ++ if category == LOG_CATEGORY_ERROR: + logger.error("%s", message) +- elif category == ERROR_CATEGORY_PROGRESS: ++ elif category == LOG_CATEGORY_PROGRESS: + logger.info("%s", message) +- elif category == ERROR_CATEGORY_DETAIL: ++ elif category in (LOG_CATEGORY_DETAIL, LOG_CATEGORY_AUTOMATION): + logger.debug("%s", message) + else: + # All logs are expected to have some reasonable category. This treats +-- +2.45.2 + diff --git a/0016-Python-Remove-obsolete-logging-callbacks-33718.patch b/0016-Python-Remove-obsolete-logging-callbacks-33718.patch new file mode 100644 index 0000000..4d75379 --- /dev/null +++ b/0016-Python-Remove-obsolete-logging-callbacks-33718.patch @@ -0,0 +1,256 @@ +From 92dc4417ed88d016d0b21e377d1c04a97e55f34e Mon Sep 17 00:00:00 2001 +From: Stefan Agner +Date: Tue, 4 Jun 2024 07:14:58 +0200 +Subject: [PATCH] [Python] Remove obsolete logging callbacks (#33718) + +Since #5024 there is a new logging callback working. The old code has +partially been removed in #4690, but never completely. Drop the old +logging code for good. +--- + .../ChipDeviceController-ScriptBinding.cpp | 12 -- + src/controller/python/chip/ChipStack.py | 148 +----------------- + 2 files changed, 2 insertions(+), 158 deletions(-) + +diff --git a/src/controller/python/ChipDeviceController-ScriptBinding.cpp b/src/controller/python/ChipDeviceController-ScriptBinding.cpp +index a55d3865bd..728fd5801f 100644 +--- a/src/controller/python/ChipDeviceController-ScriptBinding.cpp ++++ b/src/controller/python/ChipDeviceController-ScriptBinding.cpp +@@ -212,7 +212,6 @@ PyChipError pychip_DeviceCommissioner_CloseBleConnection(chip::Controller::Devic + + const char * pychip_Stack_ErrorToString(ChipError::StorageType err); + const char * pychip_Stack_StatusReportToString(uint32_t profileId, uint16_t statusCode); +-void pychip_Stack_SetLogFunct(LogMessageFunct logFunct); + + PyChipError pychip_GetConnectedDeviceByNodeId(chip::Controller::DeviceCommissioner * devCtrl, chip::NodeId nodeId, + chip::Controller::Python::PyObject * context, DeviceAvailableFunc callback); +@@ -863,17 +862,6 @@ uint64_t pychip_GetCommandSenderHandle(chip::DeviceProxy * device) + return 0; + } + +-void pychip_Stack_SetLogFunct(LogMessageFunct logFunct) +-{ +- // TODO: determine if log redirection is supposed to be functioning in CHIP +- // +- // Background: original log baseline supported 'redirect logs to this +- // function' however CHIP does not currently provide this. +- // +- // Ideally log redirection should work so that python code can do things +- // like using the log module. +-} +- + PyChipError pychip_DeviceController_PostTaskOnChipThread(ChipThreadTaskRunnerFunct callback, void * pythonContext) + { + if (callback == nullptr || pythonContext == nullptr) +diff --git a/src/controller/python/chip/ChipStack.py b/src/controller/python/chip/ChipStack.py +index beeaedd6ae..06afff3ef3 100644 +--- a/src/controller/python/chip/ChipStack.py ++++ b/src/controller/python/chip/ChipStack.py +@@ -28,15 +28,11 @@ from __future__ import absolute_import, print_function + + import asyncio + import builtins +-import logging + import os +-import sys +-import time +-from ctypes import CFUNCTYPE, Structure, c_bool, c_char_p, c_int64, c_uint8, c_uint16, c_uint32, c_void_p, py_object, pythonapi ++from ctypes import CFUNCTYPE, Structure, c_bool, c_char_p, c_uint16, c_uint32, c_void_p, py_object, pythonapi + from threading import Condition, Event, Lock + + import chip.native +-from chip.logging import LOG_CATEGORY_AUTOMATION, LOG_CATEGORY_DETAIL, LOG_CATEGORY_ERROR, LOG_CATEGORY_PROGRESS + from chip.native import PyChipError + + from .ChipUtility import ChipUtility +@@ -76,51 +72,6 @@ class DeviceStatusStruct(Structure): + ] + + +-class LogCategory(object): +- """Debug logging categories used by chip.""" +- +- @staticmethod +- def categoryToLogLevel(cat): +- if cat == LOG_CATEGORY_ERROR: +- return logging.ERROR +- elif cat == LOG_CATEGORY_PROGRESS: +- return logging.INFO +- elif cat in (LOG_CATEGORY_DETAIL, LOG_CATEGORY_AUTOMATION): +- return logging.DEBUG +- else: +- return logging.NOTSET +- +- +-class ChipLogFormatter(logging.Formatter): +- """A custom logging.Formatter for logging chip library messages.""" +- +- def __init__( +- self, +- datefmt=None, +- logModulePrefix=False, +- logLevel=False, +- logTimestamp=False, +- logMSecs=True, +- ): +- fmt = "%(message)s" +- if logModulePrefix: +- fmt = "CHIP:%(chip-module)s: " + fmt +- if logLevel: +- fmt = "%(levelname)s:" + fmt +- if datefmt is not None or logTimestamp: +- fmt = "%(asctime)s " + fmt +- super(ChipLogFormatter, self).__init__(fmt=fmt, datefmt=datefmt) +- self.logMSecs = logMSecs +- +- def formatTime(self, record, datefmt=None): +- if datefmt is None: +- timestampStr = time.strftime("%Y-%m-%d %H:%M:%S%z") +- if self.logMSecs: +- timestampUS = record.__dict__.get("timestamp-usec", 0) +- timestampStr = "%s.%03ld" % (timestampStr, timestampUS / 1000) +- return timestampStr +- +- + class AsyncCallableHandle: + def __init__(self, callback): + self._callback = callback +@@ -185,15 +136,12 @@ class AsyncioCallableHandle: + pythonapi.Py_DecRef(py_object(self)) + + +-_LogMessageFunct = CFUNCTYPE( +- None, c_int64, c_int64, c_char_p, c_uint8, c_char_p) + _ChipThreadTaskRunnerFunct = CFUNCTYPE(None, py_object) + + + @_singleton + class ChipStack(object): +- def __init__(self, persistentStoragePath: str, installDefaultLogHandler=True, +- bluetoothAdapter=None, enableServerInteractions=True): ++ def __init__(self, persistentStoragePath: str, enableServerInteractions=True): + builtins.enableDebugMode = False + + # TODO: Probably no longer necessary, see https://github.com/project-chip/connectedhomeip/issues/33321. +@@ -206,8 +154,6 @@ class ChipStack(object): + self.callbackRes = None + self.commissioningEventRes = None + self.openCommissioningWindowPincode = {} +- self._activeLogFunct = None +- self.addModulePrefixToLogMessage = True + self._enableServerInteractions = enableServerInteractions + + # +@@ -216,50 +162,6 @@ class ChipStack(object): + # + self._loadLib() + +- # Arrange to log output from the chip library to a python logger object with the +- # name 'chip.ChipStack'. If desired, applications can override this behavior by +- # setting self.logger to a different python logger object, or by calling setLogFunct() +- # with their own logging function. +- self.logger = logging.getLogger(__name__) +- self.setLogFunct(self.defaultLogFunct) +- +- # Determine if there are already handlers installed for the logger. Python 3.5+ +- # has a method for this; on older versions the check has to be done manually. +- if hasattr(self.logger, "hasHandlers"): +- hasHandlers = self.logger.hasHandlers() +- else: +- hasHandlers = False +- logger = self.logger +- while logger is not None: +- if len(logger.handlers) > 0: +- hasHandlers = True +- break +- if not logger.propagate: +- break +- logger = logger.parent +- +- # If a logging handler has not already been initialized for 'chip.ChipStack', +- # or any one of its parent loggers, automatically configure a handler to log to +- # stdout. This maintains compatibility with a number of applications which expect +- # chip log output to go to stdout by default. +- # +- # This behavior can be overridden in a variety of ways: +- # - Initialize a different log handler before ChipStack is initialized. +- # - Pass installDefaultLogHandler=False when initializing ChipStack. +- # - Replace the StreamHandler on self.logger with a different handler object. +- # - Set a different Formatter object on the existing StreamHandler object. +- # - Reconfigure the existing ChipLogFormatter object. +- # - Configure chip to call an application-specific logging function by +- # calling self.setLogFunct(). +- # - Call self.setLogFunct(None), which will configure the chip library +- # to log directly to stdout, bypassing python altogether. +- # +- if installDefaultLogHandler and not hasHandlers: +- logHandler = logging.StreamHandler(stream=sys.stdout) +- logHandler.setFormatter(ChipLogFormatter()) +- self.logger.addHandler(logHandler) +- self.logger.setLevel(logging.DEBUG) +- + @_ChipThreadTaskRunnerFunct + def HandleChipThreadRun(callback): + callback() +@@ -292,49 +194,6 @@ class ChipStack(object): + def enableServerInteractions(self): + return self._enableServerInteractions + +- @property +- def defaultLogFunct(self): +- """Returns a python callable which, when called, logs a message to the python logger object +- currently associated with the ChipStack object. +- The returned function is suitable for passing to the setLogFunct() method.""" +- +- def logFunct(timestamp, timestampUSec, moduleName, logCat, message): +- moduleName = ChipUtility.CStringToString(moduleName) +- message = ChipUtility.CStringToString(message) +- if self.addModulePrefixToLogMessage: +- message = "CHIP:%s: %s" % (moduleName, message) +- logLevel = LogCategory.categoryToLogLevel(logCat) +- msgAttrs = { +- "chip-module": moduleName, +- "timestamp": timestamp, +- "timestamp-usec": timestampUSec, +- } +- self.logger.log(logLevel, message, extra=msgAttrs) +- +- return logFunct +- +- def setLogFunct(self, logFunct): +- """Set the function used by the chip library to log messages. +- The supplied object must be a python callable that accepts the following +- arguments: +- timestamp (integer) +- timestampUS (integer) +- module name (encoded UTF-8 string) +- log category (integer) +- message (encoded UTF-8 string) +- Specifying None configures the chip library to log directly to stdout.""" +- if logFunct is None: +- logFunct = 0 +- if not isinstance(logFunct, _LogMessageFunct): +- logFunct = _LogMessageFunct(logFunct) +- # TODO: Lock probably no longer necessary, see https://github.com/project-chip/connectedhomeip/issues/33321. +- with self.networkLock: +- # NOTE: ChipStack must hold a reference to the CFUNCTYPE object while it is +- # set. Otherwise it may get garbage collected, and logging calls from the +- # chip library will fail. +- self._activeLogFunct = logFunct +- self._ChipStackLib.pychip_Stack_SetLogFunct(logFunct) +- + def Shutdown(self): + # + # Terminate Matter thread and shutdown the stack. +@@ -484,9 +343,6 @@ class ChipStack(object): + self._ChipStackLib.pychip_Stack_StatusReportToString.restype = c_char_p + self._ChipStackLib.pychip_Stack_ErrorToString.argtypes = [c_uint32] + self._ChipStackLib.pychip_Stack_ErrorToString.restype = c_char_p +- self._ChipStackLib.pychip_Stack_SetLogFunct.argtypes = [ +- _LogMessageFunct] +- self._ChipStackLib.pychip_Stack_SetLogFunct.restype = PyChipError + + self._ChipStackLib.pychip_DeviceController_PostTaskOnChipThread.argtypes = [ + _ChipThreadTaskRunnerFunct, py_object] +-- +2.45.2 + diff --git a/0017-Python-Drop-network-lock-33720.patch b/0017-Python-Drop-network-lock-33720.patch new file mode 100644 index 0000000..c718aae --- /dev/null +++ b/0017-Python-Drop-network-lock-33720.patch @@ -0,0 +1,63 @@ +From 025bdb7a1e9b9669ab091b96f9ac0adefe3a53dd Mon Sep 17 00:00:00 2001 +From: Stefan Agner +Date: Wed, 5 Jun 2024 16:06:15 +0200 +Subject: [PATCH] [Python] Drop network lock (#33720) + +The network lock is not needed in the Python controller, as all calls +to the SDK are made by posting to the Matter SDK event loop through +ScheduleWork(), hence are guaranteed to be serialized. + +From how I understand ScheduleWork() works, it pushes the work to the +event loop through PostEvent() which at least on POSIX is using the +thread safe device queue (see GenericPlatformManagerImpl_POSIX.cpp). +--- + src/controller/python/chip/ChipStack.py | 12 ++---------- + 1 file changed, 2 insertions(+), 10 deletions(-) + +diff --git a/src/controller/python/chip/ChipStack.py b/src/controller/python/chip/ChipStack.py +index 06afff3ef3..b47c463982 100644 +--- a/src/controller/python/chip/ChipStack.py ++++ b/src/controller/python/chip/ChipStack.py +@@ -144,8 +144,6 @@ class ChipStack(object): + def __init__(self, persistentStoragePath: str, enableServerInteractions=True): + builtins.enableDebugMode = False + +- # TODO: Probably no longer necessary, see https://github.com/project-chip/connectedhomeip/issues/33321. +- self.networkLock = Lock() + self.completeEvent = Event() + self.commissioningCompleteEvent = Event() + self._ChipStackLib = None +@@ -212,7 +210,6 @@ class ChipStack(object): + # #20437 tracks consolidating these. + # + self._ChipStackLib.pychip_CommonStackShutdown() +- self.networkLock = None + self.completeEvent = None + self._ChipStackLib = None + self._chipDLLPath = None +@@ -226,10 +223,7 @@ class ChipStack(object): + This function is a wrapper of PostTaskOnChipThread, which includes some handling of application specific logics. + Calling this function on CHIP on CHIP mainloop thread will cause deadlock. + ''' +- # TODO: Lock probably no longer necessary, see https://github.com/project-chip/connectedhomeip/issues/33321. +- with self.networkLock: +- res = self.PostTaskOnChipThread(callFunct).Wait(timeoutMs) +- return res ++ return self.PostTaskOnChipThread(callFunct).Wait(timeoutMs) + + async def CallAsync(self, callFunct, timeoutMs: int = None): + '''Run a Python function on CHIP stack, and wait for the response. +@@ -256,9 +250,7 @@ class ChipStack(object): + # throw error if op in progress + self.callbackRes = None + self.completeEvent.clear() +- # TODO: Lock probably no longer necessary, see https://github.com/project-chip/connectedhomeip/issues/33321. +- with self.networkLock: +- res = self.PostTaskOnChipThread(callFunct).Wait() ++ res = self.PostTaskOnChipThread(callFunct).Wait() + + if not res.is_success: + self.completeEvent.set() +-- +2.45.2 + diff --git a/0018-Python-Remove-Python-Bluetooth-and-ChipStack-event-l.patch b/0018-Python-Remove-Python-Bluetooth-and-ChipStack-event-l.patch new file mode 100644 index 0000000..649bb66 --- /dev/null +++ b/0018-Python-Remove-Python-Bluetooth-and-ChipStack-event-l.patch @@ -0,0 +1,95 @@ +From a4fb58131d9023ad3a3ec01d44c5ae87946d6b59 Mon Sep 17 00:00:00 2001 +From: Stefan Agner +Date: Thu, 6 Jun 2024 17:11:34 +0200 +Subject: [PATCH] [Python] Remove Python Bluetooth and ChipStack event loop + integration (#33775) + +The Python Bluetooth implementation for Linux (`BluezManager` in +ChipBluezMgr.py) and macOS (`CoreBluetoothManager` in +ChipCoreBluetoothMgr.py) integrate with ChipStack to pump their event +loops on long running operations such as commissioning (through +`CallAsyncWithCompleteCallback()`). From what I can tell, the Python +Bluetooth stack is only used for some mbed integration tests. +Specifically through `scan_chip_ble_devices()` +in src/test_driver/mbed/integration_tests/common/utils.py. This +operation doesn't need the event loop integration. + +So as a first step, this PR simply breaks this tie and removes the +event loop integration with the Device ChipStack/ChipDeviceController. +--- + src/controller/python/chip/ChipBluezMgr.py | 1 - + src/controller/python/chip/ChipCoreBluetoothMgr.py | 2 -- + src/controller/python/chip/ChipDeviceCtrl.py | 5 ----- + src/controller/python/chip/ChipStack.py | 8 +------- + 4 files changed, 1 insertion(+), 15 deletions(-) + +diff --git a/src/controller/python/chip/ChipBluezMgr.py b/src/controller/python/chip/ChipBluezMgr.py +index e480750b60..bacf383710 100644 +--- a/src/controller/python/chip/ChipBluezMgr.py ++++ b/src/controller/python/chip/ChipBluezMgr.py +@@ -807,7 +807,6 @@ class BluezManager(ChipBleBase): + self.rx = None + self.setInputHook(self.readlineCB) + self.devMgr = devMgr +- self.devMgr.SetBlockingCB(self.devMgrCB) + + def __del__(self): + self.disconnect() +diff --git a/src/controller/python/chip/ChipCoreBluetoothMgr.py b/src/controller/python/chip/ChipCoreBluetoothMgr.py +index 3f792a5a4d..4a65f1e237 100644 +--- a/src/controller/python/chip/ChipCoreBluetoothMgr.py ++++ b/src/controller/python/chip/ChipCoreBluetoothMgr.py +@@ -184,8 +184,6 @@ class CoreBluetoothManager(ChipBleBase): + def __del__(self): + self.disconnect() + self.setInputHook(self.orig_input_hook) +- self.devCtrl.SetBlockingCB(None) +- self.devCtrl.SetBleEventCB(None) + + def devMgrCB(self): + """A callback used by ChipDeviceCtrl.py to drive the OSX runloop while the +diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py +index 1d1627c46d..acbbc88b3e 100644 +--- a/src/controller/python/chip/ChipDeviceCtrl.py ++++ b/src/controller/python/chip/ChipDeviceCtrl.py +@@ -1461,11 +1461,6 @@ class ChipDeviceControllerBase(): + else: + return res.events + +- def SetBlockingCB(self, blockingCB): +- self.CheckIsActive() +- +- self._ChipStack.blockingCB = blockingCB +- + def SetIpk(self, ipk: bytes): + self._ChipStack.Call( + lambda: self._dmLib.pychip_DeviceController_SetIpk(self.devCtrl, ipk, len(ipk)) +diff --git a/src/controller/python/chip/ChipStack.py b/src/controller/python/chip/ChipStack.py +index b47c463982..5fd0601ba2 100644 +--- a/src/controller/python/chip/ChipStack.py ++++ b/src/controller/python/chip/ChipStack.py +@@ -165,8 +165,6 @@ class ChipStack(object): + callback() + + self.cbHandleChipThreadRun = HandleChipThreadRun +- # set by other modules(BLE) that require service by thread while thread blocks. +- self.blockingCB = None + + # + # Storage has to be initialized BEFORE initializing the stack, since the latter +@@ -255,11 +253,7 @@ class ChipStack(object): + if not res.is_success: + self.completeEvent.set() + raise res.to_exception() +- while not self.completeEvent.isSet(): +- if self.blockingCB: +- self.blockingCB() +- +- self.completeEvent.wait(0.05) ++ self.completeEvent.wait() + if isinstance(self.callbackRes, ChipStackException): + raise self.callbackRes + return self.callbackRes +-- +2.45.2 + diff --git a/0019-Python-Add-TriggerResubscribeIfScheduled-to-Subscrip.patch b/0019-Python-Add-TriggerResubscribeIfScheduled-to-Subscrip.patch new file mode 100644 index 0000000..a32b77e --- /dev/null +++ b/0019-Python-Add-TriggerResubscribeIfScheduled-to-Subscrip.patch @@ -0,0 +1,54 @@ +From 9e3eeeaf21a3a258ea6d068f8ade8c321b7b5563 Mon Sep 17 00:00:00 2001 +From: Stefan Agner +Date: Fri, 7 Jun 2024 15:50:34 +0200 +Subject: [PATCH] [Python] Add TriggerResubscribeIfScheduled to + SubscriptionTransaction (#33774) + +Add TriggerResubscribeIfScheduled to SubscriptionTransaction. If the +ReadClient currently has a resubscription attempt scheduled, This +function allows to trigger that attempt immediately. This is useful +when the server side is up and communicating, and it's a good time to +try to resubscribe. +--- + src/controller/python/chip/clusters/Attribute.py | 7 +++++++ + src/controller/python/chip/clusters/attribute.cpp | 6 ++++++ + 2 files changed, 13 insertions(+) + +diff --git a/src/controller/python/chip/clusters/Attribute.py b/src/controller/python/chip/clusters/Attribute.py +index 51389e19a1..838936e83b 100644 +--- a/src/controller/python/chip/clusters/Attribute.py ++++ b/src/controller/python/chip/clusters/Attribute.py +@@ -478,6 +478,13 @@ class SubscriptionTransaction: + lambda: handle.pychip_ReadClient_OverrideLivenessTimeout(self._readTransaction._pReadClient, timeoutMs) + ) + ++ async def TriggerResubscribeIfScheduled(self, reason: str): ++ handle = chip.native.GetLibraryHandle() ++ await builtins.chipStack.CallAsync( ++ lambda: handle.pychip_ReadClient_TriggerResubscribeIfScheduled( ++ self._readTransaction._pReadClient, reason.encode("utf-8")) ++ ) ++ + def GetReportingIntervalsSeconds(self) -> Tuple[int, int]: + ''' + Retrieve the reporting intervals associated with an active subscription. +diff --git a/src/controller/python/chip/clusters/attribute.cpp b/src/controller/python/chip/clusters/attribute.cpp +index b73b4a49b4..7c5b2c906a 100644 +--- a/src/controller/python/chip/clusters/attribute.cpp ++++ b/src/controller/python/chip/clusters/attribute.cpp +@@ -464,6 +464,12 @@ void pychip_ReadClient_OverrideLivenessTimeout(ReadClient * pReadClient, uint32_ + pReadClient->OverrideLivenessTimeout(System::Clock::Milliseconds32(livenessTimeoutMs)); + } + ++void pychip_ReadClient_TriggerResubscribeIfScheduled(ReadClient * pReadClient, const char * reason) ++{ ++ VerifyOrDie(pReadClient != nullptr); ++ pReadClient->TriggerResubscribeIfScheduled(reason); ++} ++ + PyChipError pychip_ReadClient_GetReportingIntervals(ReadClient * pReadClient, uint16_t * minIntervalSec, uint16_t * maxIntervalSec) + { + VerifyOrDie(pReadClient != nullptr); +-- +2.45.2 +