diff --git a/scripts/tests/cirque_tests.sh b/scripts/tests/cirque_tests.sh index caa7014f5fdd94..b018117df6216a 100755 --- a/scripts/tests/cirque_tests.sh +++ b/scripts/tests/cirque_tests.sh @@ -44,6 +44,7 @@ CIRQUE_TESTS=( "SplitCommissioningTest" "CommissioningFailureTest" "CommissioningFailureOnReportTest" + "CommissioningWindowTest" ) BOLD_GREEN_TEXT="\033[1;32m" diff --git a/src/app/clusters/administrator-commissioning-server/administrator-commissioning-server.cpp b/src/app/clusters/administrator-commissioning-server/administrator-commissioning-server.cpp index a78ccaf4c6ab9f..b38cc27407965a 100644 --- a/src/app/clusters/administrator-commissioning-server/administrator-commissioning-server.cpp +++ b/src/app/clusters/administrator-commissioning-server/administrator-commissioning-server.cpp @@ -193,6 +193,8 @@ bool emberAfAdministratorCommissioningClusterRevokeCommissioningCallback( { ChipLogProgress(Zcl, "Received command to close commissioning window"); + Server::GetInstance().GetFailSafeContext().ForceFailSafeTimerExpiry(); + if (!Server::GetInstance().GetCommissioningWindowManager().IsCommissioningWindowOpen()) { ChipLogError(Zcl, "Commissioning window is currently not open"); diff --git a/src/controller/AutoCommissioner.h b/src/controller/AutoCommissioner.h index f2807a4a26542d..297946f3fe86d0 100644 --- a/src/controller/AutoCommissioner.h +++ b/src/controller/AutoCommissioner.h @@ -48,6 +48,12 @@ class AutoCommissioner : public CommissioningDelegate CommissioningStage GetNextCommissioningStage(CommissioningStage currentStage, CHIP_ERROR & lastErr); DeviceCommissioner * GetCommissioner() { return mCommissioner; } CHIP_ERROR PerformStep(CommissioningStage nextStage); + CommissioneeDeviceProxy * GetCommissioneeDeviceProxy() { return mCommissioneeDeviceProxy; } + /** + * The device argument to GetCommandTimeout is the device whose session will + * be used for sending the relevant command. + */ + Optional GetCommandTimeout(DeviceProxy * device, CommissioningStage stage) const; private: DeviceProxy * GetDeviceProxyForStep(CommissioningStage nextStage); @@ -64,11 +70,6 @@ class AutoCommissioner : public CommissioningDelegate ByteSpan GetPAI() const { return ByteSpan(mPAI, mPAILen); } CHIP_ERROR NOCChainGenerated(ByteSpan noc, ByteSpan icac, ByteSpan rcac, IdentityProtectionKeySpan ipk, NodeId adminSubject); - /** - * The device argument to GetCommandTimeout is the device whose session will - * be used for sending the relevant command. - */ - Optional GetCommandTimeout(DeviceProxy * device, CommissioningStage stage) const; EndpointId GetEndpoint(const CommissioningStage & stage) const; CommissioningStage GetNextCommissioningStageInternal(CommissioningStage currentStage, CHIP_ERROR & lastErr); diff --git a/src/controller/python/ChipDeviceController-ScriptBinding.cpp b/src/controller/python/ChipDeviceController-ScriptBinding.cpp index ea376baa96391e..62fed10f223b71 100644 --- a/src/controller/python/ChipDeviceController-ScriptBinding.cpp +++ b/src/controller/python/ChipDeviceController-ScriptBinding.cpp @@ -174,6 +174,8 @@ PyChipError pychip_ScriptDevicePairingDelegate_SetCommissioningCompleteCallback( PyChipError pychip_ScriptDevicePairingDelegate_SetCommissioningStatusUpdateCallback( chip::Controller::DeviceCommissioner * devCtrl, chip::Controller::DevicePairingDelegate_OnCommissioningStatusUpdateFunct callback); +PyChipError pychip_ScriptDevicePairingDelegate_SetOpenWindowCompleteCallback( + chip::Controller::DeviceCommissioner * devCtrl, chip::Controller::DevicePairingDelegate_OnWindowOpenCompleteFunct callback); // BLE PyChipError pychip_DeviceCommissioner_CloseBleConnection(chip::Controller::DeviceCommissioner * devCtrl); @@ -524,6 +526,13 @@ PyChipError pychip_DeviceController_DiscoverCommissionableNodesCommissioningEnab return ToPyChipError(devCtrl->DiscoverCommissionableNodes(filter)); } +PyChipError pychip_ScriptDevicePairingDelegate_SetOpenWindowCompleteCallback( + chip::Controller::DeviceCommissioner * devCtrl, chip::Controller::DevicePairingDelegate_OnWindowOpenCompleteFunct callback) +{ + sPairingDelegate.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) @@ -538,8 +547,12 @@ PyChipError pychip_DeviceController_OpenCommissioningWindow(chip::Controller::De if (option == Controller::CommissioningWindowOpener::CommissioningWindowOption::kTokenWithRandomPIN) { SetupPayload payload; - return ToPyChipError(Controller::AutoCommissioningWindowOpener::OpenCommissioningWindow( - devCtrl, nodeid, System::Clock::Seconds16(timeout), iteration, discriminator, NullOptional, NullOptional, payload)); + auto opener = + Platform::New(static_cast(devCtrl)); + PyChipError err = ToPyChipError(opener->OpenCommissioningWindow(nodeid, System::Clock::Seconds16(timeout), iteration, + discriminator, NullOptional, NullOptional, + sPairingDelegate.GetOpenWindowCallback(opener), payload)); + return err; } return ToPyChipError(CHIP_ERROR_INVALID_ARGUMENT); diff --git a/src/controller/python/ChipDeviceController-ScriptDevicePairingDelegate.cpp b/src/controller/python/ChipDeviceController-ScriptDevicePairingDelegate.cpp index 67cc6abdb01702..eff99f442694b6 100644 --- a/src/controller/python/ChipDeviceController-ScriptDevicePairingDelegate.cpp +++ b/src/controller/python/ChipDeviceController-ScriptDevicePairingDelegate.cpp @@ -20,10 +20,21 @@ #include "ChipDeviceController-ScriptDevicePairingDelegate.h" #include "lib/support/TypeTraits.h" #include +#include namespace chip { namespace Controller { +namespace { +void OnWindowCompleteStatic(void * context, NodeId deviceId, CHIP_ERROR status, SetupPayload payload) +{ + auto self = reinterpret_cast(context); + self->OnOpenCommissioningWindow(deviceId, status, payload); +} +} // namespace + +ScriptDevicePairingDelegate::ScriptDevicePairingDelegate() : mOpenWindowCallback(OnWindowCompleteStatic, this) {} + void ScriptDevicePairingDelegate::SetKeyExchangeCallback(DevicePairingDelegate_OnPairingCompleteFunct callback) { mOnPairingCompleteCallback = callback; @@ -34,6 +45,11 @@ void ScriptDevicePairingDelegate::SetCommissioningCompleteCallback(DevicePairing mOnCommissioningCompleteCallback = callback; } +void ScriptDevicePairingDelegate::SetCommissioningWindowOpenCallback(DevicePairingDelegate_OnWindowOpenCompleteFunct callback) +{ + mOnWindowOpenCompleteCallback = callback; +} + void ScriptDevicePairingDelegate::SetCommissioningSuccessCallback(DevicePairingDelegate_OnCommissioningSuccessFunct callback) { mOnCommissioningSuccessCallback = callback; @@ -91,5 +107,28 @@ void ScriptDevicePairingDelegate::OnCommissioningStatusUpdate(PeerId peerId, Com } } +void ScriptDevicePairingDelegate::OnOpenCommissioningWindow(NodeId deviceId, CHIP_ERROR status, SetupPayload payload) +{ + if (mOnWindowOpenCompleteCallback != nullptr) + { + QRCodeSetupPayloadGenerator generator(payload); + std::string code; + generator.payloadBase38Representation(code); + ChipLogProgress(Zcl, "code = %s", code.c_str()); + mOnWindowOpenCompleteCallback(deviceId, payload.setUpPINCode, code.c_str(), ToPyChipError(status)); + } + if (mWindowOpener != nullptr) + { + Platform::Delete(mWindowOpener); + mWindowOpener = nullptr; + } +} +Callback::Callback * +ScriptDevicePairingDelegate::GetOpenWindowCallback(Controller::CommissioningWindowOpener * context) +{ + mWindowOpener = context; + return &mOpenWindowCallback; +} + } // namespace Controller } // namespace chip diff --git a/src/controller/python/ChipDeviceController-ScriptDevicePairingDelegate.h b/src/controller/python/ChipDeviceController-ScriptDevicePairingDelegate.h index cc41b30d3eb4b1..a66ae6b891bc49 100644 --- a/src/controller/python/ChipDeviceController-ScriptDevicePairingDelegate.h +++ b/src/controller/python/ChipDeviceController-ScriptDevicePairingDelegate.h @@ -26,6 +26,7 @@ #pragma once #include +#include #include namespace chip { @@ -34,6 +35,8 @@ namespace Controller { extern "C" { typedef void (*DevicePairingDelegate_OnPairingCompleteFunct)(PyChipError err); typedef void (*DevicePairingDelegate_OnCommissioningCompleteFunct)(NodeId nodeId, PyChipError err); +typedef void (*DevicePairingDelegate_OnWindowOpenCompleteFunct)(NodeId nodeId, uint32_t setupPinCode, const char * setupCode, + PyChipError err); // Used for testing by OpCredsBinding typedef void (*DevicePairingDelegate_OnCommissioningSuccessFunct)(PeerId peerId); @@ -48,25 +51,33 @@ typedef void (*DevicePairingDelegate_OnCommissioningStatusUpdateFunct)(PeerId pe class ScriptDevicePairingDelegate final : public Controller::DevicePairingDelegate { public: + ScriptDevicePairingDelegate(); ~ScriptDevicePairingDelegate() = default; void SetKeyExchangeCallback(DevicePairingDelegate_OnPairingCompleteFunct callback); void SetCommissioningCompleteCallback(DevicePairingDelegate_OnCommissioningCompleteFunct callback); void SetCommissioningStatusUpdateCallback(DevicePairingDelegate_OnCommissioningStatusUpdateFunct callback); void SetCommissioningSuccessCallback(DevicePairingDelegate_OnCommissioningSuccessFunct callback); void SetCommissioningFailureCallback(DevicePairingDelegate_OnCommissioningFailureFunct callback); + void SetCommissioningWindowOpenCallback(DevicePairingDelegate_OnWindowOpenCompleteFunct callback); void OnPairingComplete(CHIP_ERROR error) override; void OnCommissioningComplete(NodeId nodeId, CHIP_ERROR err) override; void OnCommissioningSuccess(PeerId peerId) override; void OnCommissioningFailure(PeerId peerId, CHIP_ERROR error, CommissioningStage stageFailed, Optional additionalErrorInfo) override; void OnCommissioningStatusUpdate(PeerId peerId, CommissioningStage stageCompleted, CHIP_ERROR error) override; + Callback::Callback * + GetOpenWindowCallback(Controller::CommissioningWindowOpener * context); + void OnOpenCommissioningWindow(NodeId deviceId, CHIP_ERROR status, SetupPayload payload); private: DevicePairingDelegate_OnPairingCompleteFunct mOnPairingCompleteCallback = nullptr; DevicePairingDelegate_OnCommissioningCompleteFunct mOnCommissioningCompleteCallback = nullptr; + DevicePairingDelegate_OnWindowOpenCompleteFunct mOnWindowOpenCompleteCallback = nullptr; DevicePairingDelegate_OnCommissioningSuccessFunct mOnCommissioningSuccessCallback = nullptr; DevicePairingDelegate_OnCommissioningFailureFunct mOnCommissioningFailureCallback = nullptr; DevicePairingDelegate_OnCommissioningStatusUpdateFunct mOnCommissioningStatusUpdateCallback = nullptr; + Callback::Callback mOpenWindowCallback; + Controller::CommissioningWindowOpener * mWindowOpener = nullptr; }; } // namespace Controller diff --git a/src/controller/python/OpCredsBinding.cpp b/src/controller/python/OpCredsBinding.cpp index f0d9b1fa21dfdd..3b12f8febd859f 100644 --- a/src/controller/python/OpCredsBinding.cpp +++ b/src/controller/python/OpCredsBinding.cpp @@ -133,6 +133,30 @@ class TestCommissioner : public chip::Controller::AutoCommissioner // Pretend we received an error from the device during this stage err = CHIP_ERROR_INTERNAL; } + if (mPrematureCompleteAfter == report.stageCompleted) + { + auto commissioner = chip::Controller::AutoCommissioner::GetCommissioner(); + auto proxy = chip::Controller::AutoCommissioner::GetCommissioneeDeviceProxy(); + auto stage = chip::Controller::CommissioningStage::kSendComplete; + auto params = chip::Controller::CommissioningParameters(); + commissioner->PerformCommissioningStep(proxy, stage, params, this, 0, GetCommandTimeout(proxy, stage)); + return CHIP_NO_ERROR; + } + + if (mPrematureCompleteAfter != chip::Controller::CommissioningStage::kError && + report.stageCompleted == chip::Controller::CommissioningStage::kSendComplete) + { + if (report.Is()) + { + uint8_t code = chip::to_underlying(report.Get().commissioningError); + mCompletionError = chip::ChipError(chip::ChipError::SdkPart::kIMClusterStatus, code); + } + else + { + mCompletionError = err; + } + } + return chip::Controller::AutoCommissioner::CommissioningStepFinished(err, report); } // This will cause the COMMISSIONER to fail after the given stage. Setting this to kSecurePairing will cause the @@ -156,6 +180,15 @@ class TestCommissioner : public chip::Controller::AutoCommissioner mFailOnReportAfterStage = stage; return true; } + bool PrematureCompleteAfter(chip::Controller::CommissioningStage stage) + { + if (!ValidStage(stage) && stage != chip::Controller::CommissioningStage::kError) + { + return false; + } + mPrematureCompleteAfter = stage; + return true; + } bool CheckCallbacks() { bool successFailureOk; @@ -208,6 +241,7 @@ class TestCommissioner : public chip::Controller::AutoCommissioner } mSimulateFailureOnStage = chip::Controller::CommissioningStage::kError; mFailOnReportAfterStage = chip::Controller::CommissioningStage::kError; + mPrematureCompleteAfter = chip::Controller::CommissioningStage::kError; } bool GetTestCommissionerUsed() { return mTestCommissionerUsed; } void OnCommissioningSuccess(chip::PeerId peerId) { mReceivedCommissioningSuccess = true; } @@ -226,19 +260,35 @@ class TestCommissioner : public chip::Controller::AutoCommissioner { mReceivedStageFailure[chip::to_underlying(stageCompleted)] = true; } + if (stageCompleted == chip::Controller::CommissioningStage::kCleanup && + mPrematureCompleteAfter != chip::Controller::CommissioningStage::kError) + { + // We need to manually clean up the proxy here because we're doing bad things in the name of testing + ChipLogProgress(Controller, "Cleaning up dangling proxies"); + auto commissioner = chip::Controller::AutoCommissioner::GetCommissioner(); + auto proxy = chip::Controller::AutoCommissioner::GetCommissioneeDeviceProxy(); + if (proxy != nullptr) + { + commissioner->StopPairing(proxy->GetDeviceId()); + } + } } + CHIP_ERROR GetCompletionError() { return mCompletionError; } + private: static constexpr uint8_t kNumCommissioningStages = chip::to_underlying(chip::Controller::CommissioningStage::kCleanup) + 1; chip::Controller::CommissioningStage mSimulateFailureOnStage = chip::Controller::CommissioningStage::kError; chip::Controller::CommissioningStage mFailOnReportAfterStage = chip::Controller::CommissioningStage::kError; + chip::Controller::CommissioningStage mPrematureCompleteAfter = chip::Controller::CommissioningStage::kError; bool mTestCommissionerUsed = false; bool mReceivedCommissioningSuccess = false; chip::Controller::CommissioningStage mReceivedCommissioningFailureStage = chip::Controller::CommissioningStage::kError; bool mReceivedStageSuccess[kNumCommissioningStages]; bool mReceivedStageFailure[kNumCommissioningStages]; - bool mIsWifi = false; - bool mIsThread = false; + bool mIsWifi = false; + bool mIsThread = false; + CHIP_ERROR mCompletionError = CHIP_NO_ERROR; bool ValidStage(chip::Controller::CommissioningStage stage) { if (!mIsWifi && @@ -469,5 +519,14 @@ bool pychip_SetTestCommissionerSimulateFailureOnReport(uint8_t failStage) { return sTestCommissioner.SimulateFailOnReport(static_cast(failStage)); } +bool pychip_SetTestCommissionerPrematureCompleteAfter(uint8_t stage) +{ + return sTestCommissioner.PrematureCompleteAfter(static_cast(stage)); +} + +PyChipError pychip_GetCompletionError() +{ + return ToPyChipError(sTestCommissioner.GetCompletionError()); +} } // extern "C" diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py index 8668a09a3d7091..c608d598c5fcab 100644 --- a/src/controller/python/chip/ChipDeviceCtrl.py +++ b/src/controller/python/chip/ChipDeviceCtrl.py @@ -61,6 +61,8 @@ _DevicePairingDelegate_OnPairingCompleteFunct = CFUNCTYPE(None, PyChipError) _DevicePairingDelegate_OnCommissioningCompleteFunct = CFUNCTYPE( None, c_uint64, PyChipError) +_DevicePairingDelegate_OnOpenWindowCompleteFunct = CFUNCTYPE( + None, c_uint64, c_uint32, c_char_p, PyChipError) _DevicePairingDelegate_OnCommissioningStatusUpdateFunct = CFUNCTYPE( None, c_uint64, c_uint8, PyChipError) # void (*)(Device *, CHIP_ERROR). @@ -256,11 +258,23 @@ def HandleCommissioningComplete(nodeid, err): self.state = DCState.IDLE self._ChipStack.callbackRes = err self._ChipStack.commissioningEventRes = err + if self._dmLib.pychip_TestCommissionerUsed(): + self._ChipStack.commissioningEventRes = self._dmLib.pychip_GetCompletionError() self._ChipStack.commissioningCompleteEvent.set() self._ChipStack.completeEvent.set() - def HandlePASEEstablishmentComplete(err: PyChipError): + def HandleOpenWindowComplete(nodeid: int, setupPinCode: int, setupCode: str, err: PyChipError) -> None: if err.is_success: + print("Open Commissioning Window complete setting nodeid {} pincode to {}".format(nodeid, setupPinCode)) + self._ChipStack.openCommissioningWindowPincode[nodeid] = (setupPinCode, setupCode) + else: + print("Failed to open commissioning window: {}".format(err)) + + self._ChipStack.callbackRes = err + self._ChipStack.completeEvent.set() + + def HandlePASEEstablishmentComplete(err: PyChipError): + if not err.is_success: print("Failed to establish secure session to device: {}".format(err)) self._ChipStack.callbackRes = err.to_exception() else: @@ -288,6 +302,11 @@ def HandlePASEEstablishmentComplete(err: PyChipError): self._dmLib.pychip_ScriptDevicePairingDelegate_SetCommissioningCompleteCallback( self.devCtrl, self.cbHandleCommissioningCompleteFunct) + self.cbHandleOpenWindowCompleteFunct = _DevicePairingDelegate_OnOpenWindowCompleteFunct( + HandleOpenWindowComplete) + self._dmLib.pychip_ScriptDevicePairingDelegate_SetOpenWindowCompleteCallback( + self.devCtrl, self.cbHandleOpenWindowCompleteFunct) + self.state = DCState.IDLE self._isActive = True @@ -469,6 +488,10 @@ def SetTestCommissionerSimulateFailureOnReport(self, stage: int): return self._dmLib.pychip_SetTestCommissionerSimulateFailureOnReport( stage) + def SetTestCommissionerPrematureCompleteAfter(self, stage: int): + return self._dmLib.pychip_SetTestCommissionerPrematureCompleteAfter( + stage) + def CheckTestCommissionerCallbacks(self): return self._ChipStack.Call( lambda: self._dmLib.pychip_TestCommissioningCallbacks() @@ -512,8 +535,8 @@ def CommissionOnNetwork(self, nodeId: int, setupPinCode: int, filterType: Discov ) if not self._ChipStack.commissioningCompleteEvent.isSet(): # Error 50 is a timeout - return False - return self._ChipStack.commissioningEventRes == 0 + return False, -1 + return self._ChipStack.commissioningEventRes == 0, self._ChipStack.commissioningEventRes def CommissionWithCode(self, setupPayload: str, nodeid: int): self.CheckIsActive() @@ -737,13 +760,14 @@ def DiscoverAllCommissioning(self): self.devCtrl) ).raise_on_error() - def OpenCommissioningWindow(self, nodeid, timeout, iteration, discriminator, option): + def OpenCommissioningWindow(self, nodeid: int, timeout: int, iteration: int, discriminator: int, option: int) -> (int, str): self.CheckIsActive() - - self._ChipStack.Call( + self._ChipStack.CallAsync( lambda: self._dmLib.pychip_DeviceController_OpenCommissioningWindow( self.devCtrl, nodeid, timeout, iteration, discriminator, option) ).raise_on_error() + self._ChipStack.callbackRes.raise_on_error() + return self._ChipStack.openCommissioningWindowPincode[nodeid] def GetCompressedFabricId(self): self.CheckIsActive() @@ -1366,9 +1390,13 @@ def _InitLib(self): c_void_p, _DevicePairingDelegate_OnCommissioningCompleteFunct] self._dmLib.pychip_ScriptDevicePairingDelegate_SetCommissioningCompleteCallback.restype = PyChipError + self._dmLib.pychip_ScriptDevicePairingDelegate_SetOpenWindowCompleteCallback.argtypes = [ + c_void_p, _DevicePairingDelegate_OnOpenWindowCompleteFunct] + self._dmLib.pychip_ScriptDevicePairingDelegate_SetOpenWindowCompleteCallback.restype = PyChipError + self._dmLib.pychip_ScriptDevicePairingDelegate_SetCommissioningStatusUpdateCallback.argtypes = [ c_void_p, _DevicePairingDelegate_OnCommissioningStatusUpdateFunct] - self._dmLib.pychip_ScriptDevicePairingDelegate_SetCommissioningCompleteCallback.restype = PyChipError + self._dmLib.pychip_ScriptDevicePairingDelegate_SetCommissioningStatusUpdateCallback.restype = PyChipError self._dmLib.pychip_GetConnectedDeviceByNodeId.argtypes = [ c_void_p, c_uint64, _DeviceAvailableFunct] @@ -1415,6 +1443,13 @@ def _InitLib(self): c_uint8] self._dmLib.pychip_SetTestCommissionerSimulateFailureOnReport.restype = c_bool + self._dmLib.pychip_SetTestCommissionerPrematureCompleteAfter.argtypes = [ + c_uint8] + self._dmLib.pychip_SetTestCommissionerPrematureCompleteAfter.restype = c_bool + + self._dmLib.pychip_GetCompletionError.argtypes = [] + 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 ] diff --git a/src/controller/python/chip/ChipStack.py b/src/controller/python/chip/ChipStack.py index 4154a92ec1460a..ad217249937fc6 100644 --- a/src/controller/python/chip/ChipStack.py +++ b/src/controller/python/chip/ChipStack.py @@ -185,6 +185,7 @@ def __init__(self, persistentStoragePath: str, installDefaultLogHandler=True, bl self.devMgr = None self.callbackRes = None self.commissioningEventRes = None + self.openCommissioningWindowPincode = {} self._activeLogFunct = None self.addModulePrefixToLogMessage = True self._enableServerInteractions = enableServerInteractions diff --git a/src/controller/python/test/test_scripts/base.py b/src/controller/python/test/test_scripts/base.py index cca12fdc992a85..3188e310848ae7 100644 --- a/src/controller/python/test/test_scripts/base.py +++ b/src/controller/python/test/test_scripts/base.py @@ -231,10 +231,37 @@ def TestDiscovery(self, discriminator: int): self.logger.info(f"Found device {res[0]}") return res[0] - def TestPaseOnly(self, ip: str, setuppin: int, nodeid: int): + def CreateNewFabricController(self): + self.logger.info("Creating 2nd Fabric Admin") + self.fabricAdmin2 = self.certificateAuthority.NewFabricAdmin(vendorId=0xFFF1, fabricId=2) + + self.logger.info("Creating Device Controller on 2nd Fabric") + self.devCtrl2 = self.fabricAdmin2.NewController( + self.controllerNodeId, self.paaTrustStorePath) + return True + + async def TestRevokeCommissioningWindow(self, ip: str, setuppin: int, nodeid: int): + await self.devCtrl.SendCommand(nodeid, 0, Clusters.AdministratorCommissioning.Commands.OpenBasicCommissioningWindow(180), timedRequestTimeoutMs=10000) + if not self.TestPaseOnly(ip=ip, setuppin=setuppin, nodeid=nodeid, devCtrl=self.devCtrl2): + return False + + await self.devCtrl2.SendCommand(nodeid, 0, Clusters.GeneralCommissioning.Commands.ArmFailSafe(expiryLengthSeconds=180, breadcrumb=0)) + + await self.devCtrl.SendCommand(nodeid, 0, Clusters.AdministratorCommissioning.Commands.RevokeCommissioning(), timedRequestTimeoutMs=10000) + await self.devCtrl.SendCommand(nodeid, 0, Clusters.AdministratorCommissioning.Commands.OpenBasicCommissioningWindow(180), timedRequestTimeoutMs=10000) + await self.devCtrl.SendCommand(nodeid, 0, Clusters.AdministratorCommissioning.Commands.RevokeCommissioning(), timedRequestTimeoutMs=10000) + return True + + def TestEnhancedCommissioningWindow(self, ip: str, nodeid: int): + pin, code = self.devCtrl.OpenCommissioningWindow(nodeid=nodeid, timeout=600, iteration=10000, discriminator=3840, option=1) + return self.TestPaseOnly(ip=ip, nodeid=nodeid, setuppin=pin, devCtrl=self.devCtrl2) + + def TestPaseOnly(self, ip: str, setuppin: int, nodeid: int, devCtrl=None): + if devCtrl is None: + devCtrl = self.devCtrl self.logger.info( "Attempting to establish PASE session with device id: {} addr: {}".format(str(nodeid), ip)) - if self.devCtrl.EstablishPASESessionIP( + if devCtrl.EstablishPASESessionIP( ip, setuppin, nodeid) is not None: self.logger.info( "Failed to establish PASE session with device id: {} addr: {}".format(str(nodeid), ip)) diff --git a/src/controller/python/test/test_scripts/commissioning_window_test.py b/src/controller/python/test/test_scripts/commissioning_window_test.py new file mode 100755 index 00000000000000..6a113aede20baf --- /dev/null +++ b/src/controller/python/test/test_scripts/commissioning_window_test.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 + +# +# Copyright (c) 2022 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Commissioning test. + +import asyncio +import os +import sys +from optparse import OptionParser + +from base import BaseTestHelper, FailIfNot, TestFail, TestTimeout, logger + +# The thread network dataset tlv for testing, splitted into T-L-V. + +TEST_THREAD_NETWORK_DATASET_TLV = "0e080000000000010000" + \ + "000300000c" + \ + "35060004001fffe0" + \ + "0208fedcba9876543210" + \ + "0708fd00000000001234" + \ + "0510ffeeddccbbaa99887766554433221100" + \ + "030e54657374696e674e6574776f726b" + \ + "0102d252" + \ + "041081cb3b2efa781cc778397497ff520fa50c0302a0ff" +# Network id, for the thread network, current a const value, will be changed to XPANID of the thread network. +TEST_THREAD_NETWORK_ID = "fedcba9876543210" +TEST_DISCRIMINATOR = 3840 + +ENDPOINT_ID = 0 +LIGHTING_ENDPOINT_ID = 1 +GROUP_ID = 0 + + +async def main(): + optParser = OptionParser() + optParser.add_option( + "-t", + "--timeout", + action="store", + dest="testTimeout", + default=75, + type='int', + help="The program will return with timeout after specified seconds.", + metavar="", + ) + optParser.add_option( + "--address", + action="store", + dest="deviceAddress", + default='', + type='str', + help="Address of the first device", + ) + optParser.add_option( + "-p", + "--paa-trust-store-path", + action="store", + dest="paaTrustStorePath", + default='', + type='str', + help="Path that contains valid and trusted PAA Root Certificates.", + metavar="" + ) + + (options, remainingArgs) = optParser.parse_args(sys.argv[1:]) + + timeoutTicker = TestTimeout(options.testTimeout) + timeoutTicker.start() + + test = BaseTestHelper( + nodeid=112233, paaTrustStorePath=options.paaTrustStorePath, testCommissioner=False) + + FailIfNot(test.SetNetworkCommissioningParameters(dataset=TEST_THREAD_NETWORK_DATASET_TLV), + "Failed to finish network commissioning") + + logger.info("Commissioning DUT from first commissioner") + FailIfNot(test.TestPaseOnly(ip=options.deviceAddress, setuppin=20202021, nodeid=1), + "Unable to establish PASE connection to device") + FailIfNot(test.TestCommissionOnly(nodeid=1), "Unable to commission device") + + logger.info("Creating controller on a new fabric") + FailIfNot(test.CreateNewFabricController(), "Unable to create new controller") + + logger.info("Testing RevokeCommissioning") + FailIfNot(await test.TestRevokeCommissioningWindow(ip=options.deviceAddress, + setuppin=20202021, + nodeid=1), + "RevokeCommissioning test failed") + + logger.info("Test Enhanced Commissioning Window") + FailIfNot(test.TestEnhancedCommissioningWindow(ip=options.deviceAddress, nodeid=1), "EnhancedCommissioningWindow open failed") + + timeoutTicker.stop() + + logger.info("Test finished") + + # TODO: Python device controller cannot be shutdown clean sometimes and will block on AsyncDNSResolverSockets shutdown. + # Call os._exit(0) to force close it. + os._exit(0) + + +if __name__ == "__main__": + try: + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) + loop.close() + except Exception as ex: + logger.exception(ex) + TestFail("Exception occurred when running tests.") diff --git a/src/python_testing/TC_CGEN_2_4.py b/src/python_testing/TC_CGEN_2_4.py new file mode 100644 index 00000000000000..c089615a8d4489 --- /dev/null +++ b/src/python_testing/TC_CGEN_2_4.py @@ -0,0 +1,153 @@ +# +# Copyright (c) 2022 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import asyncio +import logging +import queue +import time +from threading import Event + +import chip.CertificateAuthority +import chip.clusters as Clusters +import chip.FabricAdmin +from chip import ChipDeviceCtrl +from chip.clusters.Attribute import SubscriptionTransaction, TypedAttributePath +from chip.interaction_model import InteractionModelError +from chip.utils import CommissioningBuildingBlocks +from matter_testing_support import MatterBaseTest, async_test_body, default_matter_test_main +from mobly import asserts + + +class TC_CGEN_2_4(MatterBaseTest): + + def OpenCommissioningWindow(self) -> int: + try: + pin, code = self.th1.OpenCommissioningWindow( + nodeid=self.dut_node_id, timeout=600, iteration=10000, discriminator=self.matter_test_config.discriminator, option=1) + return pin, code + + except Exception as e: + logging.exception('Error running OpenCommissioningWindow %s', e) + asserts.assert_true(False, 'Failed to open commissioning window') + + async def CommissionToStageSendCompleteAndCleanup(self, stage: int, expectedErrorPart: chip.native.ErrorSDKPart, expectedErrCode: int): + + logging.info("-----------------Fail on step {}-------------------------".format(stage)) + pin, code = self.OpenCommissioningWindow() + self.th2.ResetTestCommissioner() + # This will run the commissioning up to the point where stage x is run and the response is sent before the test commissioner simulates a failure + self.th2.SetTestCommissionerPrematureCompleteAfter(stage) + success, errcode = self.th2.CommissionOnNetwork( + nodeId=self.dut_node_id, setupPinCode=pin, filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, filter=self.matter_test_config.discriminator) + logging.info('Commissioning complete done. Successful? {}, errorcode = {}'.format(success, errcode)) + asserts.assert_false(success, 'Commissioning complete did not error as expected') + asserts.assert_true(errcode.sdk_part == expectedErrorPart, 'Unexpected error type returned from CommissioningComplete') + asserts.assert_true(errcode.sdk_code == expectedErrCode, 'Unexpected error code returned from CommissioningComplete') + revokeCmd = Clusters.AdministratorCommissioning.Commands.RevokeCommissioning() + await self.th1.SendCommand(nodeid=self.dut_node_id, endpoint=0, payload=revokeCmd, timedRequestTimeoutMs=6000) + # The failsafe cleanup is scheduled after the command completes, so give it a bit of time to do that + time.sleep(1) + + @async_test_body + async def test_TC_CGEN_2_4(self): + self.th1 = self.default_controller + th2_certificate_authority = self.certificate_authority_manager.NewCertificateAuthority() + th2_fabric_admin = th2_certificate_authority.NewFabricAdmin(vendorId=0xFFF1, fabricId=self.th1.fabricId + 1) + self.th2 = th2_fabric_admin.NewController(nodeId=2, useTestCommissioner=True) + # Stage 3 = kArmFailsafe, expect General error 0x7e (UNSUPPORTED_ACCESS) + await self.CommissionToStageSendCompleteAndCleanup(3, chip.native.ErrorSDKPart.IM_GLOBAL_STATUS, 0x7e) + # Stage 4 = kConfigRegulatory, expect General error 0x7e (UNSUPPORTED_ACCESS) + await self.CommissionToStageSendCompleteAndCleanup(4, chip.native.ErrorSDKPart.IM_GLOBAL_STATUS, 0x7e) + # Stage 5 = kSendPAICertificateRequest, expect General error 0x7e (UNSUPPORTED_ACCESS) + await self.CommissionToStageSendCompleteAndCleanup(5, chip.native.ErrorSDKPart.IM_GLOBAL_STATUS, 0x7e) + # Stage 6 = kSendDACCertificateRequest, expect General error 0x7e (UNSUPPORTED_ACCESS) + await self.CommissionToStageSendCompleteAndCleanup(6, chip.native.ErrorSDKPart.IM_GLOBAL_STATUS, 0x7e) + # Stage 7 = kSendAttestationRequest, expect General error 0x7e (UNSUPPORTED_ACCESS) + await self.CommissionToStageSendCompleteAndCleanup(7, chip.native.ErrorSDKPart.IM_GLOBAL_STATUS, 0x7e) + # Stage 9 = kSendOpCertSigningRequest, expect General error 0x7e (UNSUPPORTED_ACCESS) + await self.CommissionToStageSendCompleteAndCleanup(9, chip.native.ErrorSDKPart.IM_GLOBAL_STATUS, 0x7e) + # Stage 12 = kSendTrustedRootCert, expect General error 0x7e (UNSUPPORTED_ACCESS) + await self.CommissionToStageSendCompleteAndCleanup(12, chip.native.ErrorSDKPart.IM_GLOBAL_STATUS, 0x7e) + # Stage 13 = kSendNOC, expect cluster error InvalidAuthentication + await self.CommissionToStageSendCompleteAndCleanup(13, chip.native.ErrorSDKPart.IM_CLUSTER_STATUS, 0x02) + + logging.info('Step 15 - TH1 opens a commissioning window') + pin, code = self.OpenCommissioningWindow() + + logging.info('Step 16 - TH2 fully commissions the DUT') + self.th2.ResetTestCommissioner() + success, errcode = self.th2.CommissionOnNetwork( + nodeId=self.dut_node_id, setupPinCode=pin, filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, filter=self.matter_test_config.discriminator) + logging.info('Commissioning complete done. Successful? {}, errorcode = {}'.format(success, errcode)) + + logging.info('Step 17 - TH1 sends an arm failsafe') + cmd = Clusters.GeneralCommissioning.Commands.ArmFailSafe(expiryLengthSeconds=900, breadcrumb=0) + # This will throw if not successful + await self.th1.SendCommand(nodeid=self.dut_node_id, endpoint=0, payload=cmd) + + logging.info('Step 18 - TH1 reads the location capability') + attr = Clusters.GeneralCommissioning.Attributes.LocationCapability + cap = await self.read_single_attribute(dev_ctrl=self.th1, node_id=self.dut_node_id, endpoint=0, attribute=attr) + if cap == Clusters.GeneralCommissioning.Enums.RegulatoryLocationType.kIndoor: + newloc = Clusters.GeneralCommissioning.Enums.RegulatoryLocationType.kOutdoor + elif cap == Clusters.GeneralCommissioning.Enums.RegulatoryLocationType.kOutdoor: + newloc = Clusters.GeneralCommissioning.Enums.RegulatoryLocationType.kIndoor + else: + # TODO: figure out how to use the extender + #newloc = MatterIntEnum.extend_enum_if_value_doesnt_exist(Clusters.GeneralCommissioning.Enums.RegulatoryLocationType, 3) + newlog = cap + + logging.info('Step 19 Send SetRgulatoryConfig with incorrect location') + #cmd = Clusters.GeneralCommissioning.Commands.SetRegulatoryConfig(newRegulatoryConfig=newloc, countryCode="XX", breadcrumb=0) + # try: + # await self.th1.SendCommand(nodeid=self.dut_node_id, endpoint=0, payload=cmd) + # except InteractionModelError as ex: + # print("got the real error") + # pass + + logging.info('Step 20 - TH2 sends CommissioningComplete') + cmd = Clusters.GeneralCommissioning.Commands.CommissioningComplete() + resp = await self.th2.SendCommand(nodeid=self.dut_node_id, endpoint=0, payload=cmd) + asserts.assert_true(isinstance(resp, Clusters.GeneralCommissioning.Commands.CommissioningCompleteResponse), + 'Incorrect response type from command') + asserts.assert_true( + resp.errorCode == Clusters.GeneralCommissioning.Enums.CommissioningError.kInvalidAuthentication, 'Incorrect error code') + + logging.info('Step 21 - TH1 sends an arm failsafe with timeout==0') + cmd = Clusters.GeneralCommissioning.Commands.ArmFailSafe(expiryLengthSeconds=0, breadcrumb=0) + # This will throw if not successful + await self.th1.SendCommand(nodeid=self.dut_node_id, endpoint=0, payload=cmd) + + logging.info('Step 22 - TH1 sends CommissioningComplete') + cmd = Clusters.GeneralCommissioning.Commands.CommissioningComplete() + resp = await self.th2.SendCommand(nodeid=self.dut_node_id, endpoint=0, payload=cmd) + asserts.assert_true(isinstance(resp, Clusters.GeneralCommissioning.Commands.CommissioningCompleteResponse), + 'Incorrect response type from command') + asserts.assert_true(resp.errorCode == Clusters.GeneralCommissioning.Enums.CommissioningError.kNoFailSafe, + 'Incorrect error code') + + logging.info('Step 23 - TH2 reads fabric index') + attr = Clusters.OperationalCredentials.Attributes.CurrentFabricIndex + th2FabricIndex = await self.read_single_attribute(dev_ctrl=self.th2, node_id=self.dut_node_id, endpoint=0, attribute=attr) + + logging.info('Step 24 - TH1 removes TH2') + cmd = Clusters.OperationalCredentials.Commands.RemoveFabric(fabricIndex=th2FabricIndex) + await self.th1.SendCommand(nodeid=self.dut_node_id, endpoint=0, payload=cmd) + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/test_driver/linux-cirque/CommissioningWindowTest.py b/src/test_driver/linux-cirque/CommissioningWindowTest.py new file mode 100755 index 00000000000000..16fad7ebdff8bd --- /dev/null +++ b/src/test_driver/linux-cirque/CommissioningWindowTest.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +""" +Copyright (c) 2021 Project CHIP Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import logging +import os +import pprint +import sys +import time + +from helper.CHIPTestBase import CHIPVirtualHome + +logger = logging.getLogger('MobileDeviceTest') +logger.setLevel(logging.INFO) + +sh = logging.StreamHandler() +sh.setFormatter( + logging.Formatter( + '%(asctime)s [%(name)s] %(levelname)s %(message)s')) +logger.addHandler(sh) + +CHIP_PORT = 5540 + +CIRQUE_URL = "http://localhost:5000" +CHIP_REPO = os.path.join(os.path.abspath( + os.path.dirname(__file__)), "..", "..", "..") +TEST_EXTPANID = "fedcba9876543210" +TEST_DISCRIMINATOR = 3840 +TEST_DISCRIMINATOR2 = 3584 +MATTER_DEVELOPMENT_PAA_ROOT_CERTS = "credentials/development/paa-root-certs" + +DEVICE_CONFIG = { + 'device0': { + 'type': 'MobileDevice', + 'base_image': 'connectedhomeip/chip-cirque-device-base', + 'capability': ['TrafficControl', 'Mount'], + 'rcp_mode': True, + 'docker_network': 'Ipv6', + 'traffic_control': {'latencyMs': 100}, + "mount_pairs": [[CHIP_REPO, CHIP_REPO]], + }, + 'device1': { + 'type': 'CHIPEndDevice', + 'base_image': 'connectedhomeip/chip-cirque-device-base', + 'capability': ['Thread', 'TrafficControl', 'Mount'], + 'rcp_mode': True, + 'docker_network': 'Ipv6', + 'traffic_control': {'latencyMs': 100}, + "mount_pairs": [[CHIP_REPO, CHIP_REPO]], + }, +} + + +class TestCommissioningWindow(CHIPVirtualHome): + def __init__(self, device_config): + super().__init__(CIRQUE_URL, device_config) + self.logger = logger + + def setup(self): + self.initialize_home() + + def test_routine(self): + self.run_controller_test() + + def run_controller_test(self): + servers = [{ + "ip": device['description']['ipv6_addr'], + "id": device['id'] + } for device in self.non_ap_devices + if device['type'] == 'CHIPEndDevice'] + req_ids = [device['id'] for device in self.non_ap_devices + if device['type'] == 'MobileDevice'] + + servers[0]['discriminator'] = TEST_DISCRIMINATOR + servers[0]['nodeid'] = 1 + + for server in servers: + self.execute_device_cmd(server['id'], "CHIPCirqueDaemon.py -- run gdb -return-child-result -q -ex \"set pagination off\" -ex run -ex \"bt 25\" --args {} --thread --discriminator {}".format( + os.path.join(CHIP_REPO, "out/debug/standalone/chip-all-clusters-app"), server['discriminator'])) + + self.reset_thread_devices([server['id'] for server in servers]) + + req_device_id = req_ids[0] + + self.execute_device_cmd(req_device_id, "pip3 install {}".format(os.path.join( + CHIP_REPO, "out/debug/linux_x64_gcc/controller/python/chip_clusters-0.0-py3-none-any.whl"))) + self.execute_device_cmd(req_device_id, "pip3 install {}".format(os.path.join( + CHIP_REPO, "out/debug/linux_x64_gcc/controller/python/chip_core-0.0-cp37-abi3-linux_x86_64.whl"))) + self.execute_device_cmd(req_device_id, "pip3 install {}".format(os.path.join( + CHIP_REPO, "out/debug/linux_x64_gcc/controller/python/chip_repl-0.0-py3-none-any.whl"))) + + command = "gdb -return-child-result -q -ex run -ex bt --args python3 {} -t 150 --address {} --paa-trust-store-path {}".format( + os.path.join( + CHIP_REPO, "src/controller/python/test/test_scripts/commissioning_window_test.py"), + servers[0]['ip'], + os.path.join(CHIP_REPO, MATTER_DEVELOPMENT_PAA_ROOT_CERTS)) + ret = self.execute_device_cmd(req_device_id, command) + + self.assertEqual(ret['return_code'], '0', + "Test failed: non-zero return code") + + +if __name__ == "__main__": + sys.exit(TestCommissioningWindow(DEVICE_CONFIG).run_test())