diff --git a/examples/darwin-framework-tool/BUILD.gn b/examples/darwin-framework-tool/BUILD.gn index 0cdc445fd9ffa5..81c886fe8a0d47 100644 --- a/examples/darwin-framework-tool/BUILD.gn +++ b/examples/darwin-framework-tool/BUILD.gn @@ -93,6 +93,9 @@ executable("darwin-framework-tool") { "commands/pairing/PairingCommandBridge.mm", "commands/pairing/PairingDelegateBridge.mm", "commands/payload/SetupPayloadParseCommand.mm", + "commands/provider/OTAProviderDelegate.mm", + "commands/provider/OTASoftwareUpdateInteractive.mm", + "commands/provider/Commands.h", "commands/storage/Commands.h", "commands/storage/StorageManagementCommand.mm", "main.mm", diff --git a/examples/darwin-framework-tool/commands/provider/Commands.h b/examples/darwin-framework-tool/commands/provider/Commands.h new file mode 100644 index 00000000000000..9233911fc90a5e --- /dev/null +++ b/examples/darwin-framework-tool/commands/provider/Commands.h @@ -0,0 +1,14 @@ + #include "OTASoftwareUpdateInteractive.h" + +void registerClusterOtaSoftwareUpdateProviderInteractive(Commands & commands) +{ + + const char * clusterName = "OtaSoftwareUpdateApp"; + + commands_list clusterCommands = { + make_unique(), // + + }; + + commands.Register(clusterName, clusterCommands); +} \ No newline at end of file diff --git a/examples/darwin-framework-tool/commands/provider/OTAProviderDelegate.h b/examples/darwin-framework-tool/commands/provider/OTAProviderDelegate.h new file mode 100644 index 00000000000000..3c20e0ecd3d4a7 --- /dev/null +++ b/examples/darwin-framework-tool/commands/provider/OTAProviderDelegate.h @@ -0,0 +1,33 @@ +/* + * + * Copyright (c) 2022 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. + */ +#pragma once +#import + +@interface OTAProviderDelegate : NSObject +- (void)handleQueryImage:(MTROtaSoftwareUpdateProviderClusterQueryImageParams * _Nonnull)params + completionHandler:(void (^ _Nonnull)(MTROtaSoftwareUpdateProviderClusterQueryImageResponseParams * _Nullable data, + NSError * _Nullable error))completionHandler; + +- (void)handleApplyUpdateRequest:(MTROtaSoftwareUpdateProviderClusterApplyUpdateRequestParams * _Nonnull)params + completionHandler:(void (^ _Nonnull)(MTROtaSoftwareUpdateProviderClusterApplyUpdateResponseParams * _Nullable data, + NSError * _Nullable error))completionHandler; + + +- (void)handleNotifyUpdateApplied:(MTROtaSoftwareUpdateProviderClusterNotifyUpdateAppliedParams * _Nonnull)params + completionHandler:(StatusCompletion _Nonnull)completionHandler; + +@end diff --git a/examples/darwin-framework-tool/commands/provider/OTAProviderDelegate.mm b/examples/darwin-framework-tool/commands/provider/OTAProviderDelegate.mm new file mode 100644 index 00000000000000..49f64a3881c044 --- /dev/null +++ b/examples/darwin-framework-tool/commands/provider/OTAProviderDelegate.mm @@ -0,0 +1,47 @@ +/* + * + * Copyright (c) 2022 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "OTAProviderDelegate.h" +#import + + +@interface OTAProviderDelegate () +@end + +@implementation OTAProviderDelegate +- (void)handleQueryImage:(MTROtaSoftwareUpdateProviderClusterQueryImageParams * _Nonnull)params + completionHandler:(void (^ _Nonnull)(MTROtaSoftwareUpdateProviderClusterQueryImageResponseParams * _Nullable data, + NSError * _Nullable error))completionHandler +{ + NSLog(@"handleQueryImage: %@", params); + +} + +- (void)handleApplyUpdateRequest:(MTROtaSoftwareUpdateProviderClusterApplyUpdateRequestParams * _Nonnull)params + completionHandler:(void (^ _Nonnull)(MTROtaSoftwareUpdateProviderClusterApplyUpdateResponseParams * _Nullable data, + NSError * _Nullable error))completionHandler +{ + NSLog(@"handleApplyUpdateRequest: %@", params); +} + +- (void)handleNotifyUpdateApplied:(MTROtaSoftwareUpdateProviderClusterNotifyUpdateAppliedParams * _Nonnull)params + completionHandler:(StatusCompletion _Nonnull)completionHandler +{ + NSLog(@"handleNotifyUpdateApplied: %@", params); +} + +@end diff --git a/examples/darwin-framework-tool/commands/provider/OTASoftwareUpdateInteractive.h b/examples/darwin-framework-tool/commands/provider/OTASoftwareUpdateInteractive.h new file mode 100644 index 00000000000000..5bbe86d90842d2 --- /dev/null +++ b/examples/darwin-framework-tool/commands/provider/OTASoftwareUpdateInteractive.h @@ -0,0 +1,65 @@ +/* + * + * Copyright (c) 2022 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. + */ +#pragma once +#include "../common/CHIPCommandBridge.h" + +static constexpr uint16_t SW_VER_STR_MAX_LEN = 64; +static constexpr uint16_t OTA_URL_MAX_LEN = 512; + +typedef struct DeviceSoftwareVersionModel +{ + chip::VendorId vendorId; + uint16_t productId; + uint32_t softwareVersion; + char softwareVersionString[SW_VER_STR_MAX_LEN]; + uint16_t cDVersionNumber; + bool softwareVersionValid; + uint32_t minApplicableSoftwareVersion; + uint32_t maxApplicableSoftwareVersion; + char otaURL[OTA_URL_MAX_LEN]; +} DeviceSoftwareVersionModel; + +class OTASoftwareUpdateBase : public CHIPCommandBridge { +public: + OTASoftwareUpdateBase(const char * _Nonnull commandName) + : CHIPCommandBridge(commandName) {} + void SetCandidatesFromFilePath(char * _Nonnull filePath); + bool SelectOTACandidate(const uint16_t requestorVendorID, + const uint16_t requestorProductID, + const uint32_t requestorSoftwareVersion, + DeviceSoftwareVersionModel & finalCandidate); + + /////////// CHIPCommandBridge Interface ///////// + chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(120); } +private: + std::vector mCandidates; + +}; + +class OTASoftwareUpdateSetFilePath : public OTASoftwareUpdateBase { +public: + OTASoftwareUpdateSetFilePath() + : OTASoftwareUpdateBase("candidate-file-path") + { + AddArgument("path", &mOTACandidatesFilePath); + } + + /////////// CHIPCommandBridge Interface ///////// + CHIP_ERROR RunCommand() override; +private: + char * _Nonnull mOTACandidatesFilePath; +}; diff --git a/examples/darwin-framework-tool/commands/provider/OTASoftwareUpdateInteractive.mm b/examples/darwin-framework-tool/commands/provider/OTASoftwareUpdateInteractive.mm new file mode 100644 index 00000000000000..8eb2d2da52bcdb --- /dev/null +++ b/examples/darwin-framework-tool/commands/provider/OTASoftwareUpdateInteractive.mm @@ -0,0 +1,268 @@ +/* + * + * Copyright (c) 2022 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "OTASoftwareUpdateInteractive.h" + + #include + #include + +// TODO: Objective-C Matter.framework needs to expose this. + #include + +// constexpr uint8_t kUpdateTokenLen = 32; // must be between 8 and 32 +// constexpr uint8_t kUpdateTokenStrLen = kUpdateTokenLen * 2 + 1; // Hex string needs 2 hex chars for every byte +constexpr size_t kOtaHeaderMaxSize = 1024; + +static bool CompareSoftwareVersions(const DeviceSoftwareVersionModel & a, + const DeviceSoftwareVersionModel & b) +{ + return (a.softwareVersion < b.softwareVersion); +} + + bool ParseOTAHeader(chip::OTAImageHeaderParser & parser, const char * otaFilePath, chip::OTAImageHeader & header) +{ + uint8_t otaFileContent[kOtaHeaderMaxSize]; + chip::ByteSpan buffer(otaFileContent); + + std::ifstream otaFile(otaFilePath, std::ifstream::in); + if (!otaFile.is_open() || !otaFile.good()) + { + ChipLogError(SoftwareUpdate, "Error opening OTA image file: %s", otaFilePath); + return false; + } + + otaFile.read(reinterpret_cast(otaFileContent), kOtaHeaderMaxSize); + if (otaFile.bad()) + { + ChipLogError(SoftwareUpdate, "Error reading OTA image file: %s", otaFilePath); + return false; + } + + parser.Init(); + if (!parser.IsInitialized()) + { + return false; + } + + CHIP_ERROR error = parser.AccumulateAndDecode(buffer, header); + if (error != CHIP_NO_ERROR) + { + ChipLogError(SoftwareUpdate, "Error parsing OTA image header: %" CHIP_ERROR_FORMAT, error.Format()); + return false; + } + + return true; +} + + // Parses the JSON filepath and extracts DeviceSoftwareVersionModel parameters +static bool ParseJsonFileAndPopulateCandidates(const char * filepath, + std::vector & candidates) +{ + bool ret = false; + Json::Value root; + Json::CharReaderBuilder builder; + JSONCPP_STRING errs; + std::ifstream ifs; + + builder["collectComments"] = true; // allow C/C++ type comments in JSON file + ifs.open(filepath); + + if (!ifs.good()) + { + ChipLogError(SoftwareUpdate, "Error opening ifstream with file: \"%s\"", filepath); + return ret; + } + + if (!parseFromStream(builder, ifs, &root, &errs)) + { + ChipLogError(SoftwareUpdate, "Error parsing JSON from file: \"%s\"", filepath); + return ret; + } + + const Json::Value devSofVerModValue = root["deviceSoftwareVersionModel"]; + if (!devSofVerModValue || !devSofVerModValue.isArray()) + { + ChipLogError(SoftwareUpdate, "Error: Key deviceSoftwareVersionModel not found or its value is not of type Array"); + } + else + { + for (auto iter : devSofVerModValue) + { + DeviceSoftwareVersionModel candidate; + candidate.vendorId = static_cast(iter.get("vendorId", 1).asUInt()); + candidate.productId = static_cast(iter.get("productId", 1).asUInt()); + candidate.softwareVersion = static_cast(iter.get("softwareVersion", 10).asUInt64()); + strncpy(candidate.softwareVersionString, + iter.get("softwareVersionString", "1.0.0").asCString(), + SW_VER_STR_MAX_LEN); + candidate.cDVersionNumber = static_cast(iter.get("cDVersionNumber", 0).asUInt()); + candidate.softwareVersionValid = iter.get("softwareVersionValid", true).asBool() ? true : false; + candidate.minApplicableSoftwareVersion = static_cast(iter.get("minApplicableSoftwareVersion", 0).asUInt64()); + candidate.maxApplicableSoftwareVersion = + static_cast(iter.get("maxApplicableSoftwareVersion", 1000).asUInt64()); + strncpy(candidate.otaURL, iter.get("otaURL", "https://test.com").asCString(), OTA_URL_MAX_LEN); + candidates.push_back(candidate); + ret = true; + } + } + return ret; +} + +CHIP_ERROR OTASoftwareUpdateSetFilePath::RunCommand() +{ + if (!IsInteractive()){ + ChipLogError(chipTool, "OTA Software Can only be ran in Interactive mode."); + return CHIP_ERROR_INTERNAL; + } + + SetCandidatesFromFilePath(mOTACandidatesFilePath); + SetCommandExitStatus(nil); + + return CHIP_NO_ERROR; +} + +void OTASoftwareUpdateBase::SetCandidatesFromFilePath(char * _Nonnull filePath) +{ + std::vector candidates; + ChipLogDetail(chipTool, "Setting candidates from file path: %s", filePath); + ParseJsonFileAndPopulateCandidates(filePath, candidates); + mCandidates = std::move(candidates); + for (auto candidate : mCandidates) + { + chip::OTAImageHeaderParser parser; + chip::OTAImageHeader header; + ParseOTAHeader(parser, candidate.otaURL, header); + ChipLogDetail(chipTool, "Validating image list candidate %s: ", candidate.otaURL); + VerifyOrDie(candidate.vendorId == header.mVendorId); + VerifyOrDie(candidate.productId == header.mProductId); + VerifyOrDie(candidate.softwareVersion == header.mSoftwareVersion); + VerifyOrDie(strlen(candidate.softwareVersionString) == header.mSoftwareVersionString.size()); + VerifyOrDie(memcmp(candidate.softwareVersionString, header.mSoftwareVersionString.data(), + header.mSoftwareVersionString.size()) == 0); + if (header.mMinApplicableVersion.HasValue()) + { + VerifyOrDie(candidate.minApplicableSoftwareVersion == header.mMinApplicableVersion.Value()); + } + if (header.mMaxApplicableVersion.HasValue()) + { + VerifyOrDie(candidate.maxApplicableSoftwareVersion == header.mMaxApplicableVersion.Value()); + } + parser.Clear(); + } + // if (mQueryImageStatus == OTAQueryStatus::kUpdateAvailable) + // { + // GenerateUpdateToken(updateToken, kUpdateTokenLen); + // GetUpdateTokenString(ByteSpan(updateToken), strBuf, kUpdateTokenStrLen); + // ChipLogDetail(SoftwareUpdate, "Generated updateToken: %s", strBuf); + + // // TODO: This uses the current node as the provider to supply the OTA image. This can be configurable such that the + // // provider supplying the response is not the provider supplying the OTA image. + // FabricIndex fabricIndex = commandObj->GetAccessingFabricIndex(); + // const FabricInfo * fabricInfo = Server::GetInstance().GetFabricTable().FindFabricWithIndex(fabricIndex); + // NodeId nodeId = fabricInfo->GetPeerId().GetNodeId(); + + // // Generate the ImageURI if one is not already preset + // if (strlen(mImageUri) == 0) + // { + // // Only supporting BDX protocol for now + // MutableCharSpan uri(mImageUri); + // CHIP_ERROR error = chip::bdx::MakeURI(nodeId, CharSpan::fromCharString(mOTACandidatesFilePath), uri); + // if (error != CHIP_NO_ERROR) + // { + // ChipLogError(SoftwareUpdate, "Cannot generate URI"); + // memset(mImageUri, 0, sizeof(mImageUri)); + // } + // else + // { + // ChipLogDetail(SoftwareUpdate, "Generated URI: %s", mImageUri); + // } + // } + + // // Initialize the transfer session in prepartion for a BDX transfer + // BitFlags bdxFlags; + // bdxFlags.Set(TransferControlFlags::kReceiverDrive); + // if (mBdxOtaSender.InitializeTransfer(commandObj->GetSubjectDescriptor().fabricIndex, + // commandObj->GetSubjectDescriptor().subject) == CHIP_NO_ERROR) + // { + // CHIP_ERROR error = + // mBdxOtaSender.PrepareForTransfer(&chip::DeviceLayer::SystemLayer(), chip::bdx::TransferRole::kSender, bdxFlags, + // kMaxBdxBlockSize, kBdxTimeout, chip::System::Clock::Milliseconds32(mPollInterval)); + // if (error != CHIP_NO_ERROR) + // { + // ChipLogError(SoftwareUpdate, "Cannot prepare for transfer: %" CHIP_ERROR_FORMAT, error.Format()); + // commandObj->AddStatus(commandPath, Status::Failure); + // return; + // } + + // response.imageURI.Emplace(chip::CharSpan::fromCharString(mImageUri)); + // response.softwareVersion.Emplace(mSoftwareVersion); + // response.softwareVersionString.Emplace(chip::CharSpan::fromCharString(mSoftwareVersionString)); + // response.updateToken.Emplace(chip::ByteSpan(updateToken)); + // } + // else + // { + // // Another BDX transfer in progress + // mQueryImageStatus = OTAQueryStatus::kBusy; + // } + // } + + // // Delay action time is only applicable when the provider is busy + // if (mQueryImageStatus == OTAQueryStatus::kBusy) + // { + // response.delayedActionTime.Emplace(mDelayedQueryActionTimeSec); + // } + + // // Set remaining fields common to all status types + // response.status = mQueryImageStatus; + // if (mUserConsentNeeded && requestorCanConsent) + // { + // response.userConsentNeeded.Emplace(true); + // } + // else + // { + // response.userConsentNeeded.Emplace(false); + // } + // // For test coverage, sending empty metadata when (requestorNodeId % 2) == 0 and not sending otherwise. + // if (commandObj->GetSubjectDescriptor().subject % 2 == 0) + // { + // response.metadataForRequestor.Emplace(chip::ByteSpan()); + // } + + // // Either sends the response or an error status + // commandObj->AddResponse(commandPath, response); + +} + +bool OTASoftwareUpdateBase::SelectOTACandidate(const uint16_t requestorVendorID, const uint16_t requestorProductID, + const uint32_t requestorSoftwareVersion, + DeviceSoftwareVersionModel & finalCandidate) +{ + bool candidateFound = false; + std::sort(mCandidates.begin(), mCandidates.end(), CompareSoftwareVersions); + for (auto candidate : mCandidates) + { + // VendorID and ProductID will be the primary key when querying + // the DCL servers. If not we can add the vendor/product ID checks here. + if (candidate.softwareVersionValid && requestorSoftwareVersion < candidate.softwareVersion && + requestorSoftwareVersion >= candidate.minApplicableSoftwareVersion && + requestorSoftwareVersion <= candidate.maxApplicableSoftwareVersion) + { + candidateFound = true; + finalCandidate = candidate; + } + } + return candidateFound; +} \ No newline at end of file diff --git a/examples/darwin-framework-tool/main.mm b/examples/darwin-framework-tool/main.mm index 8159f91df78ac0..09885d08eb3178 100644 --- a/examples/darwin-framework-tool/main.mm +++ b/examples/darwin-framework-tool/main.mm @@ -22,6 +22,7 @@ #include "commands/interactive/Commands.h" #include "commands/pairing/Commands.h" #include "commands/payload/Commands.h" +#include "commands/provider/Commands.h" #include "commands/storage/Commands.h" #include @@ -33,6 +34,7 @@ int main(int argc, const char * argv[]) registerCommandsPairing(commands); registerCommandsInteractive(commands); registerCommandsPayload(commands); + registerClusterOtaSoftwareUpdateProviderInteractive(commands); registerCommandsStorage(commands); registerCommandsTests(commands); registerClusters(commands);