diff --git a/examples/ota-provider-app/linux/main.cpp b/examples/ota-provider-app/linux/main.cpp index 9d9aedd0edda31..754169a7b971c6 100644 --- a/examples/ota-provider-app/linux/main.cpp +++ b/examples/ota-provider-app/linux/main.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include "AppMain.h" @@ -59,6 +60,8 @@ constexpr uint16_t kOptionIgnoreApplyUpdate = 'y'; constexpr uint16_t kOptionPollInterval = 'P'; OTAProviderExample gOtaProvider; +NamedPipeCommands sChipNamedPipeCommands; +OtaProviderAppCommandDelegate sOtaProviderAppCommandDelegate; chip::ota::DefaultOTAProviderUserConsent gUserConsentProvider; // Global variables used for passing the CLI arguments to the OTAProviderExample object @@ -403,9 +406,28 @@ void ApplicationInit() } chip::app::Clusters::OTAProvider::SetDelegate(kOtaProviderEndpoint, &gOtaProvider); + + std::string path = std::string(LinuxDeviceOptions::GetInstance().app_pipe); + std::string path_out = std::string(LinuxDeviceOptions::GetInstance().app_pipe_out); + + if ((!path.empty()) and (sChipNamedPipeCommands.Start(path, path_out, &sOtaProviderAppCommandDelegate) != CHIP_NO_ERROR)) + { + ChipLogError(NotSpecified, "Failed to start CHIP NamedPipeCommand"); + sChipNamedPipeCommands.Stop(); + } + else + { + sOtaProviderAppCommandDelegate.SetPipes(&sChipNamedPipeCommands); + } } -void ApplicationShutdown() {} +void ApplicationShutdown() +{ + if (sChipNamedPipeCommands.Stop() != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to stop CHIP NamedPipeCommands"); + } +} namespace { class OtaProviderAppMainLoopImplementation : public AppMainLoopImplementation diff --git a/examples/ota-provider-app/ota-provider-common/BUILD.gn b/examples/ota-provider-app/ota-provider-common/BUILD.gn index cf920862ca072d..40fd1fd8cd3ac4 100644 --- a/examples/ota-provider-app/ota-provider-common/BUILD.gn +++ b/examples/ota-provider-app/ota-provider-common/BUILD.gn @@ -17,7 +17,10 @@ import("//build_overrides/chip.gni") import("${chip_root}/src/app/chip_data_model.gni") config("config") { - include_dirs = [ ".." ] + include_dirs = [ + "..", + "${chip_root}/examples/platform/linux", + ] } chip_data_model("ota-provider-common") { @@ -28,9 +31,12 @@ chip_data_model("ota-provider-common") { "BdxOtaSender.h", "OTAProviderExample.cpp", "OTAProviderExample.h", + "OtaProviderAppCommandDelegate.cpp", + "OtaProviderAppCommandDelegate.h", ] deps = [ + "${chip_root}/examples/platform/linux:app-main", "${chip_root}/src/app/clusters/ota-provider:user-consent", "${chip_root}/src/protocols/bdx", ] diff --git a/examples/ota-provider-app/ota-provider-common/OTAProviderExample.cpp b/examples/ota-provider-app/ota-provider-common/OTAProviderExample.cpp index 556c64162664d7..e3c627d31f889d 100644 --- a/examples/ota-provider-app/ota-provider-common/OTAProviderExample.cpp +++ b/examples/ota-provider-app/ota-provider-common/OTAProviderExample.cpp @@ -322,11 +322,35 @@ void OTAProviderExample::SendQueryImageResponse(app::CommandHandler * commandObj commandObj->AddResponse(commandPath, response); } +void OTAProviderExample::SaveCommandSnapshot(const QueryImage::DecodableType & commandData) +{ + mVendorId = commandData.vendorID; + mProductId = commandData.productID; + mHardwareVersion = commandData.hardwareVersion.ValueOr(0); + mSoftwareVersion = commandData.softwareVersion; + mRequestorCanConsent = commandData.requestorCanConsent.ValueOr(false); + + mLocation.clear(); + if (commandData.location.HasValue()) + { + mLocation = NullTerminated(commandData.location.Value()).c_str(); + } + + mProtocolsSupported.clear(); + auto iter = commandData.protocolsSupported.begin(); + while (iter.Next()) + { + mProtocolsSupported.push_back(iter.GetValue()); + } +} + void OTAProviderExample::HandleQueryImage(app::CommandHandler * commandObj, const app::ConcreteCommandPath & commandPath, const QueryImage::DecodableType & commandData) { bool requestorCanConsent = commandData.requestorCanConsent.ValueOr(false); + SaveCommandSnapshot(commandData); + if (mIgnoreQueryImageCount > 0) { ChipLogDetail(SoftwareUpdate, "Skip sending QueryImageResponse, ignore count: %" PRIu32, mIgnoreQueryImageCount); diff --git a/examples/ota-provider-app/ota-provider-common/OTAProviderExample.h b/examples/ota-provider-app/ota-provider-common/OTAProviderExample.h index 55880151270eb7..feabb695a85b5b 100644 --- a/examples/ota-provider-app/ota-provider-common/OTAProviderExample.h +++ b/examples/ota-provider-app/ota-provider-common/OTAProviderExample.h @@ -29,21 +29,19 @@ /** * A reference implementation for an OTA Provider. Includes a method for providing a path to a local OTA file to serve. */ -class OTAProviderExample : public chip::app::Clusters::OTAProviderDelegate -{ +class OTAProviderExample : public chip::app::Clusters::OTAProviderDelegate { public: OTAProviderExample(); - using OTAQueryStatus = chip::app::Clusters::OtaSoftwareUpdateProvider::OTAQueryStatus; + using OTAQueryStatus = chip::app::Clusters::OtaSoftwareUpdateProvider::OTAQueryStatus; using OTAApplyUpdateAction = chip::app::Clusters::OtaSoftwareUpdateProvider::OTAApplyUpdateAction; static constexpr uint16_t SW_VER_STR_MAX_LEN = 64; - static constexpr uint16_t OTA_URL_MAX_LEN = 512; - static constexpr size_t kFilepathBufLen = 256; - static constexpr size_t kUriMaxLen = 256; + static constexpr uint16_t OTA_URL_MAX_LEN = 512; + static constexpr size_t kFilepathBufLen = 256; + static constexpr size_t kUriMaxLen = 256; - typedef struct DeviceSoftwareVersionModel - { + typedef struct DeviceSoftwareVersionModel { chip::VendorId vendorId; uint16_t productId; uint32_t softwareVersion; @@ -91,15 +89,23 @@ class OTAProviderExample : public chip::app::Clusters::OTAProviderDelegate void SetMaxBDXBlockSize(uint16_t blockSize) { mMaxBDXBlockSize = blockSize; } + uint32_t GetVendorId() { return mVendorId; } + uint32_t GetProductId() { return mProductId; } + short unsigned int GetHardwareVersion() { return mHardwareVersion; } + uint32_t GetSoftwareVersion() { return mSoftwareVersion; } + std::vector GetProtocolsSupported() { return mProtocolsSupported; } + bool GetRequestorCanConsent() { return mRequestorCanConsent; } + std::string GetLocation() { return mLocation; } + private: bool SelectOTACandidate(const uint16_t requestorVendorID, const uint16_t requestorProductID, - const uint32_t requestorSoftwareVersion, - OTAProviderExample::DeviceSoftwareVersionModel & finalCandidate); + const uint32_t requestorSoftwareVersion, + OTAProviderExample::DeviceSoftwareVersionModel & finalCandidate); chip::ota::UserConsentSubject GetUserConsentSubject(const chip::app::CommandHandler * commandObj, const chip::app::ConcreteCommandPath & commandPath, - const chip::app::Clusters::OtaSoftwareUpdateProvider::Commands::QueryImage::DecodableType & commandData, - uint32_t targetVersion); + const chip::app::Clusters::OtaSoftwareUpdateProvider::Commands::QueryImage::DecodableType & commandData, + uint32_t targetVersion); bool ParseOTAHeader(chip::OTAImageHeaderParser & parser, const char * otaFilePath, chip::OTAImageHeader & header); @@ -108,7 +114,8 @@ class OTAProviderExample : public chip::app::Clusters::OTAProviderDelegate */ void SendQueryImageResponse(chip::app::CommandHandler * commandObj, const chip::app::ConcreteCommandPath & commandPath, - const chip::app::Clusters::OtaSoftwareUpdateProvider::Commands::QueryImage::DecodableType & commandData); + const chip::app::Clusters::OtaSoftwareUpdateProvider::Commands::QueryImage::DecodableType & commandData); + void SaveCommandSnapshot(const chip::app::Clusters::OtaSoftwareUpdateProvider::Commands::QueryImage::DecodableType & commandData); BdxOtaSender mBdxOtaSender; std::vector mCandidates; @@ -122,8 +129,14 @@ class OTAProviderExample : public chip::app::Clusters::OTAProviderDelegate uint32_t mDelayedApplyActionTimeSec; chip::ota::OTAProviderUserConsentDelegate * mUserConsentDelegate; bool mUserConsentNeeded; - uint32_t mSoftwareVersion; char mSoftwareVersionString[SW_VER_STR_MAX_LEN]; uint32_t mPollInterval; uint16_t mMaxBDXBlockSize; + uint32_t mVendorId; + uint32_t mProductId; + short unsigned int mHardwareVersion; + uint32_t mSoftwareVersion; + std::vector mProtocolsSupported; + bool mRequestorCanConsent; + std::string mLocation; }; diff --git a/examples/ota-provider-app/ota-provider-common/OtaProviderAppCommandDelegate.cpp b/examples/ota-provider-app/ota-provider-common/OtaProviderAppCommandDelegate.cpp new file mode 100644 index 00000000000000..55b903733bd579 --- /dev/null +++ b/examples/ota-provider-app/ota-provider-common/OtaProviderAppCommandDelegate.cpp @@ -0,0 +1,139 @@ +/* + *json + * Copyright (c) 2025 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. + */ + +#include "OtaProviderAppCommandDelegate.h" +#include + +using namespace chip; +using namespace chip::app; +using namespace chip::app::Clusters; +using namespace chip::DeviceLayer; + +OtaProviderAppCommandHandler * OtaProviderAppCommandHandler::FromJSON(const char * json, OtaProviderAppCommandDelegate * delegate) +{ + Json::Reader reader; + Json::Value value; + + if (!reader.parse(json, value)) + { + ChipLogError(NotSpecified, + "OTA Provider Example: Error parsing JSON with error %s:", reader.getFormattedErrorMessages().c_str()); + return nullptr; + } + + if (value.empty() || !value.isObject()) + { + ChipLogError(NotSpecified, "OTA Provider Example: Invalid JSON command received"); + return nullptr; + } + + if (!value.isMember("Name") || !value["Name"].isString()) + { + ChipLogError(NotSpecified, "OTA Provider Example: Invalid JSON command received: command name is missing"); + return nullptr; + } + + return chip::Platform::New(std::move(value), delegate); +} + +static std::string ToString(const Json::Value & v) +{ + Json::StreamWriterBuilder w; + w["identation"] = ""; + return Json::writeString(w, v); +} + +Json::Value OtaProviderAppCommandHandler::BuildOtaProviderSnapshot(uint16_t endpoint) +{ + Json::Value payload(Json::objectValue); + + payload["VendorID"] = gOtaProvider.GetVendorId(); + payload["ProductID"] = gOtaProvider.GetProductId(); + payload["SoftwareVersion"] = gOtaProvider.GetSoftwareVersion(); + payload["HardwareVersion"] = gOtaProvider.GetHardwareVersion(); + payload["Location"] = gOtaProvider.GetLocation(); + payload["RequestorCanConsent"] = gOtaProvider.GetRequestorCanConsent(); + + const auto & protos = gOtaProvider.GetProtocolsSupported(); + + Json::Value arr(Json::arrayValue); + for (chip::app::Clusters::OtaSoftwareUpdateProvider::DownloadProtocolEnum p : protos) + { + arr.append(Json::UInt(p)); + } + payload["ProtocolsSupported"] = arr; + + return payload; +} + +void OtaProviderAppCommandHandler::HandleCommand(intptr_t context) +{ + auto * self = reinterpret_cast(context); + std::string name; + std::string cluster; + uint16_t endpoint = 0; + OtaProviderAppCommandDelegate * delegate = nullptr; + + VerifyOrExit(!self->mJsonValue.empty(), ChipLogError(NotSpecified, "Invalid JSON event command received")); + + name = self->mJsonValue["Name"].asString(); + cluster = self->mJsonValue.get("Cluster", "").asString(); + endpoint = static_cast(self->mJsonValue.get("Endpoint", 0).asUInt()); + delegate = self->mDelegate; + + if (name == "QueryImageSnapshot") + { + Json::Value out(Json::objectValue); + out["Name"] = "SnapshotResponse"; + out["Cluster"] = cluster; + out["Endpoint"] = endpoint; + + if (cluster == "OtaSoftwareUpdateProvider") + { + out["Payload"] = self->BuildOtaProviderSnapshot(endpoint); + } + else + { + out["Error"] = "Unsupported cluster for snapshot"; + } + + if (delegate && delegate->GetPipes()) + { + delegate->GetPipes()->WriteToOutPipe(ToString(out)); + } + } + else + { + ChipLogError(NotSpecified, "Unhandled command: Should never happen"); + } + +exit: + chip::Platform::Delete(self); +} + +void OtaProviderAppCommandDelegate::OnEventCommandReceived(const char * json) +{ + auto handler = OtaProviderAppCommandHandler::FromJSON(json, this); + if (nullptr == handler) + { + ChipLogError(NotSpecified, "OTA Provider App: Unable to instantiate a command handler"); + return; + } + + chip::DeviceLayer::PlatformMgr().ScheduleWork(OtaProviderAppCommandHandler::HandleCommand, reinterpret_cast(handler)); +} diff --git a/examples/ota-provider-app/ota-provider-common/OtaProviderAppCommandDelegate.h b/examples/ota-provider-app/ota-provider-common/OtaProviderAppCommandDelegate.h new file mode 100644 index 00000000000000..1d477d8870f2f5 --- /dev/null +++ b/examples/ota-provider-app/ota-provider-common/OtaProviderAppCommandDelegate.h @@ -0,0 +1,58 @@ +/* + * + * Copyright (c) 2025 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. + */ + +#pragma once + +#include "NamedPipeCommands.h" + +#include +#include +#include + +#include + +class OtaProviderAppCommandDelegate; +extern OTAProviderExample gOtaProvider; + +class OtaProviderAppCommandHandler { +public: + static OtaProviderAppCommandHandler * FromJSON(const char * json, OtaProviderAppCommandDelegate * delegate); + + static void HandleCommand(intptr_t context); + Json::Value BuildOtaProviderSnapshot(uint16_t endpoint); + + OtaProviderAppCommandHandler(Json::Value && v, OtaProviderAppCommandDelegate * d) + : mJsonValue(std::move(v)) + , mDelegate(d) + { + } + +private: + Json::Value mJsonValue; + OtaProviderAppCommandDelegate * mDelegate = nullptr; +}; + +class OtaProviderAppCommandDelegate : public NamedPipeCommandDelegate { +public: + void OnEventCommandReceived(const char * json) override; + void SetPipes(NamedPipeCommands * pipes) { mPipes = pipes; } + NamedPipeCommands * GetPipes() const { return mPipes; } + +private: + NamedPipeCommands * mPipes = nullptr; +}; diff --git a/examples/platform/linux/NamedPipeCommands.cpp b/examples/platform/linux/NamedPipeCommands.cpp index e07ea242f0faae..4e527217692797 100644 --- a/examples/platform/linux/NamedPipeCommands.cpp +++ b/examples/platform/linux/NamedPipeCommands.cpp @@ -35,9 +35,10 @@ CHIP_ERROR NamedPipeCommands::Start(std::string & path, NamedPipeCommandDelegate VerifyOrReturnError(!mStarted, CHIP_NO_ERROR); VerifyOrReturnError(delegate != nullptr, CHIP_ERROR_INVALID_ARGUMENT); - mStarted = true; - mDelegate = delegate; - mChipEventFifoPath = path; + mStarted = true; + mDelegate = delegate; + mChipEventFifoPath = path; + mChipEventFifoPathOut = ""; // Creating the named file(FIFO) VerifyOrReturnError((mkfifo(path.c_str(), 0666) == 0) || (errno == EEXIST), CHIP_ERROR_OPEN_FAILED); @@ -48,6 +49,27 @@ CHIP_ERROR NamedPipeCommands::Start(std::string & path, NamedPipeCommandDelegate return CHIP_NO_ERROR; } +CHIP_ERROR NamedPipeCommands::Start(std::string & path, std::string & path_out, NamedPipeCommandDelegate * delegate) +{ + VerifyOrReturnError(!mStarted, CHIP_NO_ERROR); + VerifyOrReturnError(delegate != nullptr, CHIP_ERROR_INVALID_ARGUMENT); + + mStarted = true; + mDelegate = delegate; + mChipEventFifoPath = path; + mChipEventFifoPathOut = path_out; + + // Creating the named file(FIFO) + VerifyOrReturnError((mkfifo(path.c_str(), 0666) == 0) || (errno == EEXIST), CHIP_ERROR_OPEN_FAILED); + VerifyOrReturnError( + pthread_create(&mChipEventCommandListener, nullptr, EventCommandListenerTask, reinterpret_cast(this)) == 0, + CHIP_ERROR_UNEXPECTED_EVENT); + + VerifyOrReturnError((mkfifo(path_out.c_str(), 0666) == 0) || (errno == EEXIST), CHIP_ERROR_OPEN_FAILED); + + return CHIP_NO_ERROR; +} + CHIP_ERROR NamedPipeCommands::Stop() { VerifyOrReturnError(mStarted, CHIP_NO_ERROR); @@ -60,12 +82,36 @@ CHIP_ERROR NamedPipeCommands::Stop() // Wait further for the thread to terminate if we had previously created it. VerifyOrReturnError(pthread_join(mChipEventCommandListener, nullptr) == 0, CHIP_ERROR_SHUT_DOWN); + if (mOutFd != -1) + { + close(mOutFd); + mOutFd = -1; + } + VerifyOrReturnError(unlink(mChipEventFifoPath.c_str()) == 0, CHIP_ERROR_WRITE_FAILED); mChipEventFifoPath.clear(); + if (!mChipEventFifoPath.empty()) + { + VerifyOrReturnError(unlink(mChipEventFifoPathOut.c_str()) == 0, CHIP_ERROR_WRITE_FAILED); + mChipEventFifoPathOut.clear(); + } + return CHIP_NO_ERROR; } +void NamedPipeCommands::WriteToOutPipe(const std::string & json) +{ + mOutFd = open(mChipEventFifoPathOut.c_str(), O_WRONLY); + + if (mOutFd == -1 || json.empty()) + return; + + (void) write(mOutFd, json.c_str(), json.size()); + (void) write(mOutFd, "\n", 1); + close(mOutFd); +} + void * NamedPipeCommands::EventCommandListenerTask(void * arg) { char readbuf[kChipEventCmdBufSize]; diff --git a/examples/platform/linux/NamedPipeCommands.h b/examples/platform/linux/NamedPipeCommands.h index ceacec36ca5593..785402072f5982 100644 --- a/examples/platform/linux/NamedPipeCommands.h +++ b/examples/platform/linux/NamedPipeCommands.h @@ -22,24 +22,27 @@ #include #include -class NamedPipeCommandDelegate -{ +class NamedPipeCommandDelegate { public: - virtual ~NamedPipeCommandDelegate() = default; + virtual ~NamedPipeCommandDelegate() = default; virtual void OnEventCommandReceived(const char * json) = 0; }; -class NamedPipeCommands -{ +class NamedPipeCommands { public: CHIP_ERROR Start(std::string & path, NamedPipeCommandDelegate * delegate); + CHIP_ERROR Start(std::string & path, std::string & path_out, NamedPipeCommandDelegate * delegate); CHIP_ERROR Stop(); + void WriteToOutPipe(const std::string & json); + const std::string & OutPath() const { return mChipEventFifoPathOut; } private: bool mStarted = false; pthread_t mChipEventCommandListener; std::string mChipEventFifoPath; + std::string mChipEventFifoPathOut; NamedPipeCommandDelegate * mDelegate = nullptr; + int mOutFd = -1; static void * EventCommandListenerTask(void * arg); }; diff --git a/examples/platform/linux/Options.cpp b/examples/platform/linux/Options.cpp index d9987460651b25..324824b94fef70 100644 --- a/examples/platform/linux/Options.cpp +++ b/examples/platform/linux/Options.cpp @@ -93,6 +93,7 @@ enum kDeviceOption_KVS, kDeviceOption_InterfaceId, kDeviceOption_AppPipe, + kDeviceOption_AppPipeOut, kDeviceOption_Spake2pVerifierBase64, kDeviceOption_Spake2pSaltBase64, kDeviceOption_Spake2pIterations, @@ -201,6 +202,7 @@ OptionDef sDeviceOptionDefs[] = { { "KVS", kArgumentRequired, kDeviceOption_KVS }, { "interface-id", kArgumentRequired, kDeviceOption_InterfaceId }, { "app-pipe", kArgumentRequired, kDeviceOption_AppPipe }, + { "app-pipe-out", kArgumentRequired, kDeviceOption_AppPipeOut }, #if CHIP_CONFIG_TRANSPORT_TRACE_ENABLED { "trace_file", kArgumentRequired, kDeviceOption_TraceFile }, { "trace_log", kArgumentRequired, kDeviceOption_TraceLog }, @@ -364,6 +366,9 @@ const char * sDeviceOptionHelp = "\n" " --app-pipe \n" " Custom path for the current application to send out of band commands.\n" + "\n" + " --app-pipe-out \n" + " Custom path for the current application to send out of band commands to the test.\n" #if CHIP_CONFIG_TRANSPORT_TRACE_ENABLED "\n" " --trace_file \n" @@ -715,6 +720,10 @@ bool HandleOption(const char * aProgram, OptionSet * aOptions, int aIdentifier, LinuxDeviceOptions::GetInstance().app_pipe = aValue; break; + case kDeviceOption_AppPipeOut: + LinuxDeviceOptions::GetInstance().app_pipe_out = aValue; + break; + case kDeviceOption_InterfaceId: LinuxDeviceOptions::GetInstance().interfaceId = Inet::InterfaceId(static_cast(atoi(aValue))); diff --git a/examples/platform/linux/Options.h b/examples/platform/linux/Options.h index ae6c1b7afa4e9f..b40b305131c177 100644 --- a/examples/platform/linux/Options.h +++ b/examples/platform/linux/Options.h @@ -45,44 +45,44 @@ #include #endif -struct LinuxDeviceOptions -{ +struct LinuxDeviceOptions { chip::PayloadContents payload; chip::Optional discriminator; chip::Optional> spake2pVerifier; chip::Optional> spake2pSalt; chip::Optional dacProviderFile; uint32_t spake2pIterations = 0; // When not provided (0), will default elsewhere - uint32_t mBleDevice = 0; - bool wifiSupports5g = false; - bool mWiFi = false; - bool mThread = false; - bool cameraDeferredOffer = false; - bool cameraTestVideosrc = false; - bool cameraTestAudiosrc = false; - bool cameraAudioPlayback = false; + uint32_t mBleDevice = 0; + bool wifiSupports5g = false; + bool mWiFi = false; + bool mThread = false; + bool cameraDeferredOffer = false; + bool cameraTestVideosrc = false; + bool cameraTestAudiosrc = false; + bool cameraAudioPlayback = false; chip::Optional cameraVideoDevice; #if CHIP_DEVICE_CONFIG_ENABLE_WIFIPAF - bool mWiFiPAF = false; + bool mWiFiPAF = false; const char * mWiFiPAFExtCmds = nullptr; - uint32_t mPublishId = 0; + uint32_t mPublishId = 0; #endif #if CHIP_DEVICE_CONFIG_ENABLE_BOTH_COMMISSIONER_AND_COMMISSIONEE || CHIP_DEVICE_ENABLE_PORT_PARAMS - uint16_t securedDevicePort = CHIP_PORT; + uint16_t securedDevicePort = CHIP_PORT; uint16_t unsecuredCommissionerPort = CHIP_UDC_PORT; #endif // CHIP_DEVICE_CONFIG_ENABLE_BOTH_COMMISSIONER_AND_COMMISSIONEE || CHIP_DEVICE_ENABLE_PORT_PARAMS #if CHIP_DEVICE_CONFIG_ENABLE_BOTH_COMMISSIONER_AND_COMMISSIONEE - uint16_t securedCommissionerPort = CHIP_PORT + 12; // TODO: why + 12? + uint16_t securedCommissionerPort = CHIP_PORT + 12; // TODO: why + 12? chip::FabricId commissionerFabricId = chip::kUndefinedFabricId; #endif // CHIP_DEVICE_CONFIG_ENABLE_BOTH_COMMISSIONER_AND_COMMISSIONEE - const char * command = nullptr; - const char * PICS = nullptr; - const char * KVS = nullptr; - const char * app_pipe = ""; + const char * command = nullptr; + const char * PICS = nullptr; + const char * KVS = nullptr; + const char * app_pipe = ""; + const char * app_pipe_out = ""; chip::Inet::InterfaceId interfaceId = chip::Inet::InterfaceId::Null(); #if CHIP_CONFIG_TRANSPORT_TRACE_ENABLED bool traceStreamDecodeEnabled = false; - bool traceStreamToLogEnabled = false; + bool traceStreamToLogEnabled = false; chip::Optional traceStreamFilename; #endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED chip::Credentials::DeviceAttestationCredentialsProvider * dacProvider = nullptr; @@ -94,7 +94,7 @@ struct LinuxDeviceOptions uint16_t rpcServerPort = 33000; #endif #if CONFIG_BUILD_FOR_HOST_UNIT_TEST - int32_t subscriptionCapacity = CHIP_IM_MAX_NUM_SUBSCRIPTIONS; + int32_t subscriptionCapacity = CHIP_IM_MAX_NUM_SUBSCRIPTIONS; int32_t subscriptionResumptionRetryIntervalSec = -1; #endif #if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS diff --git a/src/app/tests/suites/certification/Test_TC_SU_2_8.yaml b/src/app/tests/suites/certification/Test_TC_SU_2_8.yaml deleted file mode 100644 index a7a4ed9582f5fd..00000000000000 --- a/src/app/tests/suites/certification/Test_TC_SU_2_8.yaml +++ /dev/null @@ -1,91 +0,0 @@ -# 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. -# Auto-generated scripts for harness use only, please review before automation. The endpoints and cluster names are currently set to default - -name: 3.6.1. [TC-SU-2.8] OTA functionality in Multi Fabric scenario - -PICS: - - MCORE.OTA.Requestor - -config: - nodeId: 0x12344321 - cluster: "Basic Information" - endpoint: 0 - -tests: - - label: "Step 1: DUT sends a QueryImage command to TH1/OTA-P." - verification: | - ./chip-tool otasoftwareupdaterequestor write default-otaproviders '[{"fabricIndex": 1, "providerNodeID": 123, "endpoint": 0}]' 321 0 - - where 321 is OTA Requestor node ID and 123 is OTA Provider node ID - - Verify on the OTA Provider logs receives QueryImage with following fields: - - VendorId - Should match the value reported by the Basic Information Cluster VendorID attribute of the DUT. - - ProductId - Should match the value reported by the Basic Information Cluster ProductID attribute of the DUT. - - HardwareVersion - If present, verify that it matches the value reported by the Basic Information Cluster HardwareVersion attribute of the DUT. - - SoftwareVersion - Should match the value reported by the Basic Information Cluster SoftwareVersion attribute of the DUT. - - Verify the field ProtocolsSupported lists the BDX Synchronous protocol. - - Verify the default value of RequestorCanConsent is set to False unless DUT sets it to True. - - If the Location field is present, verify that the value is same as Basic Information Cluster Location Attribute of the DUT. - - [1645743053317] [97806:20280749] CHIP: [ZCL] OTA Provider received QueryImage - [1645743053317] [97806:20280749] CHIP: [ZCL] VendorID: 0xfff1 - [1645743053317] [97806:20280749] CHIP: [ZCL] ProductID: 32769 - [1645743053317] [97806:20280749] CHIP: [ZCL] SoftwareVersion: 0 - [1645743053317] [97806:20280749] CHIP: [ZCL] ProtocolsSupported: [ - [1645743053317] [97806:20280749] CHIP: [ZCL] 0 - [1645743053317] [97806:20280749] CHIP: [ZCL] ] - [1645743053317] [97806:20280749] CHIP: [ZCL] HardwareVersion: 0 - [1645743053317] [97806:20280749] CHIP: [ZCL] Location: XX - [1645743053317] [97806:20280749] CHIP: [ZCL] RequestorCanConsent: 0 - disabled: true - - - label: - "Step 2: DUT sends a QueryImage command to TH1/OTA-P. TH1/OTA-P does - not respond with QueryImageResponse." - PICS: MCORE.OTA.Retry - verification: | - ./chip-tool otasoftwareupdaterequestor write default-otaproviders '[{"fabricIndex": 1, "providerNodeID": 123, "endpoint": 0}]' 321 0 - - Verify SUCCESS status response On TH(chip-tool) log: - - [1686302244.664128][30278:30280] CHIP:DMG: StatusIB = - [1686302244.664157][30278:30280] CHIP:DMG: { - [1686302244.664198][30278:30280] CHIP:DMG: status = 0x00 (SUCCESS), - [1686302244.664228][30278:30280] CHIP:DMG: }, - - - ./chip-tool otasoftwareupdaterequestor write default-otaproviders '[{"fabricIndex": 2, "providerNodeID": 222, "endpoint": 0}]' 321 0 --commissioner-name beta - - Verify SUCCESS status response On TH(chip-tool) log: - - [1686302244.664128][30278:30280] CHIP:DMG: StatusIB = - [1686302244.664157][30278:30280] CHIP:DMG: { - [1686302244.664198][30278:30280] CHIP:DMG: status = 0x00 (SUCCESS), - [1686302244.664228][30278:30280] CHIP:DMG: }, - - Kill Default OTA Provider for fabric index 1 before DUT sends a query - - Verify that the DUT sends a QueryImage command to TH2/OTA-P when failing to reach out to TH1/OTA-P after a finite number of unsuccessful attempts. - disabled: true - - - label: - "Step 3: DUT sends a QueryImage command to TH2/OTA-P. TH2/OTA-P sends - a QueryImageResponse back to the DUT. Status is set to - UpdateAvailable." - verification: | - ./chip-tool otasoftwareupdaterequestor write default-otaproviders '[{"fabricIndex": 2, "providerNodeID": 1, "endpoint": 0}]' 0x858 0 --commissioner-name beta - - Verify there is a transfer of the software image from TH2/OTA-P to the DUT - disabled: true diff --git a/src/python_testing/TC_SU_2_8.py b/src/python_testing/TC_SU_2_8.py new file mode 100644 index 00000000000000..d7abcc8c1e3df0 --- /dev/null +++ b/src/python_testing/TC_SU_2_8.py @@ -0,0 +1,409 @@ +# +# Copyright (c) 2025 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. +# + +# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments +# for details about the block below. +# +# === BEGIN CI TEST ARGUMENTS === +# test-runner-runs: +# run1: +# app: ${OTA_REQUESTOR_APP} +# app-args: > +# --discriminator 1234 +# --passcode 20202021 +# --secured-device-port 5541 +# --KVS /tmp/chip_kvs_requestor +# --autoApplyImage +# --trace-to json:${TRACE_APP}.json +# script-args: > +# --storage-path admin_storage.json +# --commissioning-method on-network +# --discriminator 1234 +# --passcode 20202021 +# --endpoint 0 +# --trace-to json:${TRACE_TEST_JSON}.json +# --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto +# factory-reset: true +# quiet: true +# === END CI TEST ARGUMENTS === + +import asyncio +import logging +import re + +from mobly import asserts +from TC_SUTestBase import SoftwareUpdateBaseTest + +import matter.clusters as Clusters +from matter import ChipDeviceCtrl +from matter.interaction_model import Status +from matter.testing.apps import OtaImagePath, OTAProviderSubprocess +from matter.testing.event_attribute_reporting import EventSubscriptionHandler +from matter.testing.matter_testing import MatterBaseTest, TestStep, async_test_body, default_matter_test_main + + +class TC_SU_2_8(SoftwareUpdateBaseTest, MatterBaseTest): + """ + This test case verifies that the DUT is able to successfully send a QueryImage command to the OTA-P in multi fabric scenario. + """ + + def desc_TC_SU_2_8(self) -> str: + return "[TC-SU-2.8] OTA functionality in Multi Fabric scenario" + + def pics_TC_SU_2_8(self): + """Return the PICS definitions associated with this test.""" + pics = [ + "MCORE.OTA.Requestor", + ] + return pics + + def steps_TC_SU_2_8(self) -> list[TestStep]: + steps = [ + TestStep(0, "Commissioning, already done.", is_commissioning=True), + TestStep(1, "DUT sends a QueryImage commands to TH1/OTA-P.", + "Verify the QueryImage command received on the server has the following mandatory fields." + "VendorId - Should match the value reported by the Basic Information Cluster VendorID attribute of the DUT." + "ProductId - Should match the value reported by the Basic Information Cluster ProductID attribute of the DUT." + "HardwareVersion - If present, verify that it matches the value reported by the Basic Information Cluster HardwareVersion attribute of the DUT." + "SoftwareVersion - Should match the value reported by the Basic Information Cluster SoftwareVersion attribute of the DUT." + "Verify the field ProtocolsSupported lists the BDX Synchronous protocol." + "If (MCORE.OTA.HTTPS_Supported) HTTPS protocol should be listed." + "Verify the default value of RequestorCanConsent is set to False unless DUT sets it to True." + "If the Location field is present, verify that the value is same as Basic Information Cluster Location Attribute of the DUT."), + TestStep(2, "Configure DefaultOTAProviders with invalid node ID. DUT tries to send a QueryImage command to TH1/OTA-P. DUT sends QueryImage command to TH2/OTA-P.", + "TH1/OTA-P does not respond with QueryImage response command. StateTransition goes from idle to querying, then a download error happens and finally it goes back to idle." + "Subscribe to events for OtaSoftwareUpdateRequestor cluster and verify StateTransition reaches downloading state. Also check if the targetSoftwareVersion is 2."), + ] + return steps + + @async_test_body + async def test_TC_SU_2_8(self): + + # Variables for TH1-OTA Provider + p1_node = 10 + p1_node_invalid = 13 + p1_disc = 1112 + + # Variables for TH2-OTA Provider + p2_node = 11 + p2_disc = 1111 + + p_pass = 20202021 + + provider_port = 5540 + + app_path = "./out/debug/chip-ota-provider-app" + provider_ota_file = OtaImagePath(path="firmware_v1.ota") + + # States + idle = Clusters.Objects.OtaSoftwareUpdateRequestor.Enums.UpdateStateEnum.kIdle + querying = Clusters.Objects.OtaSoftwareUpdateRequestor.Enums.UpdateStateEnum.kQuerying + downloading = Clusters.Objects.OtaSoftwareUpdateRequestor.Enums.UpdateStateEnum.kDownloading + + target_version = 2 + + endpoint = self.get_endpoint(default=0) + dut_node_id = self.dut_node_id + requestor_node_id = self.dut_node_id + th1 = self.default_controller + fabric_id_th2 = th1.fabricId + 1 + vendor_id = self.matter_test_config.admin_vendor_id + + logging.info(f"Endpoint: {endpoint}.") + logging.info(f"DUT Node ID: {dut_node_id}.") + logging.info(f"Requestor Node ID: {requestor_node_id}.") + logging.info(f"Vendor ID: {vendor_id}.") + + logging.info(f"TH1 fabric id: {th1.fabricId}.") + logging.info(f"TH2 fabric id: {fabric_id_th2}.") + + # Commissioning step + self.step(0) + + # Start OTA Provider + logging.info("Starting OTA Provider 1") + + provider_1 = OTAProviderSubprocess( + app=app_path, + storage_dir='/tmp', + port=provider_port, + discriminator=p1_disc, + passcode=p_pass, + ota_source=provider_ota_file, + kvs_path='/tmp/chip_kvs_provider', + log_file='/tmp/provider_1.log', + err_log_file='/tmp/provider_1.log', + app_pipe='/tmp/su_2_8_fifo', + app_pipe_out='/tmp/su_2_8_fifo_out' + ) + + provider_1.start() + + # Commissioning Provider-TH1 + logging.info("Commissioning OTA Provider to TH1") + + resp = await th1.CommissionOnNetwork( + nodeId=p1_node, + setupPinCode=p_pass, + filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, + filter=p1_disc + ) + + logging.info(f"Commissioning response: {resp}.") + + # DUT sends a QueryImage commands to TH1/OTA-P + self.step(1) + + # Define ACL entry + await self.create_acl_entry(dev_ctrl=th1, provider_node_id=p1_node, requestor_node_id=requestor_node_id) + + # Write default OTA providers + await self.write_ota_providers(th1, p1_node, endpoint) + + # Announce after subscription + await self.announce_ota_provider(controller=th1, provider_node_id=p1_node, requestor_node_id=requestor_node_id, vendor_id=vendor_id, endpoint=endpoint) + + await asyncio.sleep(2) + + command = {"Name": "QueryImageSnapshot", "Cluster": "OtaSoftwareUpdateProvider", "Endpoint": 0} + fifo_in = "/tmp/su_2_8_fifo" + fifo_out = "/tmp/su_2_8_fifo_out" + self.write_to_app_pipe(command, fifo_in) + response_data = self.read_from_app_pipe(fifo_out) + + # Check VendorID + vid = response_data["Payload"]["VendorID"] + vendor_id_basic_information = await self.read_single_attribute_check_success( + dev_ctrl=th1, + cluster=Clusters.BasicInformation, + attribute=Clusters.BasicInformation.Attributes.VendorID + ) + + asserts.assert_equal(vendor_id_basic_information, vid, f"Vendor ID is {vid} and it should be {vendor_id_basic_information}") + + # Check ProductID + pid = response_data["Payload"]["ProductID"] + product_id_basic_information = await self.read_single_attribute_check_success( + dev_ctrl=th1, + cluster=Clusters.BasicInformation, + attribute=Clusters.BasicInformation.Attributes.ProductID + ) + + asserts.assert_equal(product_id_basic_information, pid, + f"Product ID is {pid} and it should be {product_id_basic_information}") + + # Check HardwareVersion + hwv = response_data["Payload"]["HardwareVersion"] + hardware_version_basic_information = await self.read_single_attribute_check_success( + dev_ctrl=th1, + cluster=Clusters.BasicInformation, + attribute=Clusters.BasicInformation.Attributes.HardwareVersion + ) + + asserts.assert_equal(hardware_version_basic_information, hwv, + f"Hardware Version is {hwv} and it should be {hardware_version_basic_information}") + + # Check SoftwareVersion + swv = response_data["Payload"]["SoftwareVersion"] + software_version_basic_information = await self.read_single_attribute_check_success( + dev_ctrl=th1, + cluster=Clusters.BasicInformation, + attribute=Clusters.BasicInformation.Attributes.SoftwareVersion + ) + + asserts.assert_equal(software_version_basic_information, swv, + f"Software Version is {swv} and it should be {software_version_basic_information}") + + # Check ProtocolsSupported protocols_supported + protocols_supported = response_data["Payload"]["ProtocolsSupported"] + asserts.assert_true(Clusters.OtaSoftwareUpdateProvider.Enums.DownloadProtocolEnum.kBDXSynchronous in protocols_supported, + f"kBDXSynchronous: {Clusters.OtaSoftwareUpdateProvider.Enums.DownloadProtocolEnum.kBDXSynchronous} is not part of ProtocolsSupporter: {protocols_supported}") + + # Check MCORE.OTA.HTTPS + if self.check_pics("MCORE.OTA.HTTPS"): + asserts.assert_true(Clusters.OtaSoftwareUpdateProvider.Enums.DownloadProtocolEnum.kHttps in protocols_supported, + f"kHttps: {Clusters.OtaSoftwareUpdateProvider.Enums.DownloadProtocolEnum.kHttps} is not part of ProtocolsSupporter: {protocols_supported}") + + # Check RequestorCanConsent + requestor_can_consent = response_data["Payload"]["RequestorCanConsent"] + expected_requestor_can_consent = 0 + asserts.assert_equal(requestor_can_consent, expected_requestor_can_consent, + f"Requestor Can Consent is {requestor_can_consent} instead of {expected_requestor_can_consent}") + + # Check Location + location = response_data["Payload"]["Location"] + if location: + location_basic_information = await self.read_single_attribute_check_success( + dev_ctrl=th1, + cluster=Clusters.BasicInformation, + attribute=Clusters.BasicInformation.Attributes.Location + ) + + asserts.assert_equal(location_basic_information, location, + f"Location is {location} and it should be {location_basic_information}") + + # Event Handler + event_cb = EventSubscriptionHandler(expected_cluster=Clusters.Objects.OtaSoftwareUpdateRequestor) + await event_cb.start(dev_ctrl=th1, node_id=requestor_node_id, endpoint=endpoint, + fabric_filtered=False, min_interval_sec=0, max_interval_sec=5) + + # Stop provider + await asyncio.sleep(2) + try: + provider_1.terminate() + except Exception as e: + logging.warning(f"Provider termination raised: {e}") + + th1.ExpireSessions(nodeid=p1_node) + await asyncio.sleep(2) + + event_cb.reset() + await event_cb.cancel() + + # Configure DefaultOTAProviders with invalid node ID. DUT tries to send a QueryImage command to TH1/OTA-P. DUT sends QueryImage command to TH2/OTA-P + self.step(2) + + provider_ota_file = OtaImagePath(path="firmware_v2.ota") + + logging.info("Starting OTA Provider 2") + provider_2 = OTAProviderSubprocess( + app=app_path, + storage_dir='/tmp', + port=provider_port, + discriminator=p2_disc, + passcode=p_pass, + ota_source=provider_ota_file, + kvs_path='/tmp/chip_kvs_provider_2', + log_file='/tmp/provider_2.log', + err_log_file='/tmp/provider_2.log' + ) + + provider_2.start() + + # Create TH2 + logging.info("Setting up TH2.") + th2_certificate_auth = self.certificate_authority_manager.NewCertificateAuthority() + th2_fabric_admin = th2_certificate_auth.NewFabricAdmin(vendorId=vendor_id, fabricId=fabric_id_th2) + th2 = th2_fabric_admin.NewController(nodeId=2, useTestCommissioner=True) + + logging.info("Opening commissioning window on DUT.") + params = await self.open_commissioning_window(th1, dut_node_id) + + # Commission TH2/DUT (requestor) + resp = await th2.CommissionOnNetwork( + nodeId=requestor_node_id, + setupPinCode=params.commissioningParameters.setupPinCode, + filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, + filter=params.randomDiscriminator + ) + + logging.info(f"TH2 commissioned: {resp}.") + + # Commissioning Provider-TH2 + logging.info("Commissioning OTA Provider to TH2") + + resp = await th2.CommissionOnNetwork( + nodeId=p2_node, + setupPinCode=p_pass, + filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, + filter=p2_disc + ) + + logging.info(f"Commissioning response: {resp}.") + + # ACL permissions are not required + + if fabric_id_th2 == th1.fabricId: + raise AssertionError(f"Fabric IDs are the same for TH1: {th1.fabricId} and TH2: {fabric_id_th2}.") + + # Event Handler + event_cb = EventSubscriptionHandler(expected_cluster=Clusters.Objects.OtaSoftwareUpdateRequestor) + await event_cb.start(dev_ctrl=th1, node_id=requestor_node_id, endpoint=endpoint, + fabric_filtered=False, min_interval_sec=0, max_interval_sec=5) + + # Write default OTA providers TH1 with p1_node which does not exist + await self.write_ota_providers(th1, p1_node_invalid, endpoint) + + # Announce TH1-OTA Provider + await self.announce_ota_provider(controller=th1, provider_node_id=p1_node_invalid, requestor_node_id=requestor_node_id, vendor_id=vendor_id, endpoint=endpoint) + + # Expect events idle to querying, downloadError and then back to idle + querying_event = event_cb.wait_for_event_report( + Clusters.Objects.OtaSoftwareUpdateRequestor.Events.StateTransition, 5000) + asserts.assert_equal(querying, querying_event.newState, + f"New state is {querying_event.newState} and it should be {querying}") + + download_error = event_cb.wait_for_event_report( + Clusters.Objects.OtaSoftwareUpdateRequestor.Events.DownloadError, 50000) + + logging.info(f"Download Error: {download_error}") + + idle_event = event_cb.wait_for_event_report( + Clusters.Objects.OtaSoftwareUpdateRequestor.Events.StateTransition, 50000) + asserts.assert_equal(idle, idle_event.newState, + f"New state is {idle_event.newState} and it should be {idle}") + + event_cb.reset() + await event_cb.cancel() + await asyncio.sleep(2) + + # Subscribe to events + event_cb = EventSubscriptionHandler(expected_cluster=Clusters.Objects.OtaSoftwareUpdateRequestor) + await event_cb.start(dev_ctrl=th2, node_id=requestor_node_id, endpoint=endpoint, + fabric_filtered=False, min_interval_sec=0, max_interval_sec=5) + + # Define ACL entry + await self.create_acl_entry(dev_ctrl=th2, provider_node_id=p2_node, requestor_node_id=requestor_node_id) + + # Write default OTA providers TH2 + await self.write_ota_providers(th2, p2_node, endpoint) + + # Announce after subscription + await self.announce_ota_provider(controller=th2, provider_node_id=p2_node, requestor_node_id=requestor_node_id, vendor_id=vendor_id, endpoint=endpoint) + + event_idle_to_querying = event_cb.wait_for_event_report( + Clusters.Objects.OtaSoftwareUpdateRequestor.Events.StateTransition, 5000) + + self.verify_state_transition_event(event_report=event_idle_to_querying, previous_state=idle, new_state=querying) + + event_querying_to_downloading = event_cb.wait_for_event_report( + Clusters.Objects.OtaSoftwareUpdateRequestor.Events.StateTransition, 5000) + + self.verify_state_transition_event(event_report=event_querying_to_downloading, + previous_state=querying, new_state=downloading, target_version=target_version) + + event_cb.reset() + await event_cb.cancel() + + logging.info("Cleaning DefaultOTAProviders.") + resp = await th2.WriteAttribute( + dut_node_id, + [(endpoint, Clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.DefaultOTAProviders([]))] + ) + asserts.assert_equal(resp[0].Status, Status.Success, "Clean DefaultOTAProviders failed.") + + await asyncio.sleep(2) + try: + provider_2.terminate() + except Exception as e: + logging.warning(f"Provider termination raised: {e}") + + th2.ExpireSessions(nodeid=p2_node) + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/apps.py b/src/python_testing/matter_testing_infrastructure/matter/testing/apps.py index 3ea649e9a9b9a1..96fb19e80490f9 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/apps.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/apps.py @@ -69,6 +69,15 @@ def __init__(self, app: str, storage_dir: str, discriminator: int, if extra_args: command.extend(extra_args) + if app_pipe is not None: + command.extend([ + "--app-pipe", str(app_pipe) + ]) + if app_pipe_out is not None: + command.extend([ + "--app-pipe-out", str(app_pipe_out) + ]) + command.extend([ "--KVS", kvs_path, '--secured-device-port', str(port), @@ -157,7 +166,7 @@ class OTAProviderSubprocess(AppServerSubprocess): def __init__(self, app: str, storage_dir: str, discriminator: int, passcode: int, ota_source: Union[OtaImagePath, ImageListPath], - port: int = 5541, extra_args: list[str] = [], kvs_path: Optional[str] = None, log_file: Union[str, BinaryIO] = stdout.buffer, err_log_file: Union[str, BinaryIO] = stderr.buffer): + port: int = 5541, extra_args: list[str] = [], kvs_path: Optional[str] = None, log_file: Union[str, BinaryIO] = stdout.buffer, err_log_file: Union[str, BinaryIO] = stderr.buffer, app_pipe: Optional[str] = None, app_pipe_out: Optional[str] = None): """Initialize the OTA Provider subprocess. Args: diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/event_attribute_reporting.py b/src/python_testing/matter_testing_infrastructure/matter/testing/event_attribute_reporting.py index 3b21c5d7a32b9a..921a765bf98892 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/event_attribute_reporting.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/event_attribute_reporting.py @@ -188,6 +188,15 @@ def get_size(self) -> int: def get_event_from_queue(self, block: bool, timeout: int): return self._q.get(block, timeout) + async def cancel(self): + """This cancels a subscription.""" + # Wait for the asyncio.CancelledError to be called before returning + try: + self._subscription.Shutdown() + await asyncio.sleep(5) + except asyncio.CancelledError: + pass + class AttributeSubscriptionHandler: """ diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/matter_test_config.py b/src/python_testing/matter_testing_infrastructure/matter/testing/matter_test_config.py index 83dbad25b08ef8..e7d456bdacd755 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/matter_test_config.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/matter_test_config.py @@ -40,6 +40,7 @@ class MatterTestConfig: endpoint: typing.Union[int, None] = 0 app_pid: int = 0 pipe_name: typing.Union[str, None] = None + pipe_name_out: typing.Union[str, None] = None fail_on_skipped_tests: bool = False commissioning_method: Optional[str] = None diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/matter_testing.py b/src/python_testing/matter_testing_infrastructure/matter/testing/matter_testing.py index 639c51f7079d06..ed41160447aeda 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/matter_testing.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/matter_testing.py @@ -970,6 +970,28 @@ async def write_single_attribute(self, attribute_value: ClusterObjects.ClusterAt f"Expected write success for write to attribute {attribute_value} on endpoint {endpoint_id}") return write_result[0].Status + def read_from_app_pipe(self, app_pipe_out: Optional[str] = None): + BUFFER_SIZE = 1024 + + # If is not empty from the args, verify if the fifo file exists. + if app_pipe_out is not None and not os.path.exists(app_pipe_out): + LOGGER.error("Named pipe %r does NOT exist" % app_pipe_out) + raise FileNotFoundError("CANNOT FIND %r" % app_pipe_out) + + if app_pipe_out is None: + app_pipe_out = self.matter_test_config.pipe_name_out + + if not isinstance(app_pipe_out, str): + raise TypeError("The named pipe must be provided as a string value") + + data = "" + with open(app_pipe_out, "r") as app_pipe_out_fp: + LOGGER.info(f"Reading out-of-band command response to file: {app_pipe_out}") + data = app_pipe_out_fp.read(BUFFER_SIZE) + LOGGER.info(data) + data = json.loads(data) + return data + def write_to_app_pipe(self, command_dict: dict, app_pipe: Optional[str] = None): """ Send an out-of-band command to a Matter app. diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/runner.py b/src/python_testing/matter_testing_infrastructure/matter/testing/runner.py index ace5fd66098ced..ed28d4140227b3 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/runner.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/runner.py @@ -794,6 +794,13 @@ def convert_args_to_matter_config(args: argparse.Namespace): LOGGER.error("Named pipe %r does NOT exist" % config.pipe_name) raise FileNotFoundError("CANNOT FIND %r" % config.pipe_name) + config.pipe_name_out = args.app_pipe_out + if config.pipe_name_out is not None and not os.path.exists(config.pipe_name_out): + # Named pipes are unique, so we MUST have consistent paths + # Verify from start the named pipe exists. + LOGGER.error("Named pipe %r does NOT exist" % config.pipe_name_out) + raise FileNotFoundError("CANNOT FIND %r" % config.pipe_name_out) + config.fail_on_skipped_tests = args.fail_on_skipped config.legacy = args.use_legacy_test_event_triggers @@ -841,7 +848,10 @@ def parse_matter_test_args(argv: Optional[List[str]] = None): help='Node ID for primary DUT communication, ' 'and NodeID to assign if commissioning (default: %d)' % TestingDefaults.DUT_NODE_ID, nargs="+") basic_group.add_argument('--endpoint', type=int, default=None, help="Endpoint under test") - basic_group.add_argument('--app-pipe', type=str, default=None, help="The full path of the app to send an out-of-band command") + basic_group.add_argument('--app-pipe', type=str, default=None, + help="The full path of the app to receive an out-of-band command") + basic_group.add_argument('--app-pipe-out', type=str, default=None, + help="The full path of the app to send an out-of-band command") basic_group.add_argument('--restart-flag-file', type=str, default=None, help="The full path of the file to use to signal a restart to the app") basic_group.add_argument('--timeout', type=int, help="Test timeout in seconds") diff --git a/src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/apps.pyi b/src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/apps.pyi index a385593e5de7ab..90e71d6b54e0a0 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/apps.pyi +++ b/src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/apps.pyi @@ -6,6 +6,7 @@ from typing import Any, BinaryIO, List, Optional, Union from matter.testing.tasks import Subprocess + @dataclass class OtaImagePath: path: str @@ -25,7 +26,7 @@ class AppServerSubprocess(Subprocess): log_file = "" err_log_file = "" def __init__(self, app: str, storage_dir: str, discriminator: int, - passcode: int, port: int = 5540, extra_args: List[str] = ...) -> None: ... + passcode: int, port: int = 5540, app_pipe: Optional[str] = None, app_pipe_out: Optional[str] = None, extra_args: List[str] = ...) -> None: ... class IcdAppServerSubprocess(AppServerSubprocess): @@ -44,11 +45,13 @@ class JFControllerSubprocess(Subprocess): class OTAProviderSubprocess(AppServerSubprocess): DEFAULT_ADMIN_NODE_ID: int PREFIX: bytes + log_file = "" + err_log_file = "" def __init__(self, app: str, storage_dir: str, discriminator: int, passcode: int, ota_source: Union[OtaImagePath, ImageListPath], port: int = 5541, extra_args: list[str] = [], kvs_path: Optional[str] = None, - log_file: Union[str, BinaryIO] = stdout.buffer, err_log_file: Union[str, BinaryIO] = stderr.buffer): ... + log_file: Union[str, BinaryIO] = stdout.buffer, err_log_file: Union[str, BinaryIO] = stderr.buffer, app_pipe: Optional[str] = None, app_pipe_out: Optional[str] = None): ... def kill(self) -> None: ... diff --git a/src/python_testing/test_metadata.yaml b/src/python_testing/test_metadata.yaml index 125da91e42fc1d..fc6b4512d9482f 100644 --- a/src/python_testing/test_metadata.yaml +++ b/src/python_testing/test_metadata.yaml @@ -27,6 +27,8 @@ not_automated: reason: Can not run thread CNET tests in CI - name: TC_CNET_4_22.py reason: It has no CI execution block, is not executed in CI + - name: TC_SU_2_8.py + reason: CI script not implemented - name: TC_DGGEN_3_2.py reason: src/python_testing/test_testing/test_TC_DGGEN_3_2.py is the Unit test