From 480e9050f5b58682047d9f885fa639c907215508 Mon Sep 17 00:00:00 2001 From: Vivien Nicolas Date: Wed, 20 Jul 2022 15:39:14 +0200 Subject: [PATCH 1/3] [darwin-framework-tool] Do not exit from interactive mode if the file passed as argument to the command otasoftwareupdateapp candidate-file-path contains errors --- .../provider/OTASoftwareUpdateInteractive.h | 2 +- .../provider/OTASoftwareUpdateInteractive.mm | 110 +++++++++++------- 2 files changed, 69 insertions(+), 43 deletions(-) diff --git a/examples/darwin-framework-tool/commands/provider/OTASoftwareUpdateInteractive.h b/examples/darwin-framework-tool/commands/provider/OTASoftwareUpdateInteractive.h index 64af48f8bf5364..f17147c2eb4d45 100644 --- a/examples/darwin-framework-tool/commands/provider/OTASoftwareUpdateInteractive.h +++ b/examples/darwin-framework-tool/commands/provider/OTASoftwareUpdateInteractive.h @@ -27,7 +27,7 @@ class OTASoftwareUpdateBase : public CHIPCommandBridge { : CHIPCommandBridge(commandName) { } - void SetCandidatesFromFilePath(char * _Nonnull filePath); + CHIP_ERROR SetCandidatesFromFilePath(char * _Nonnull filePath); CHIP_ERROR SetUserConsentStatus(char * _Nonnull status); static constexpr size_t kFilepathBufLen = 256; diff --git a/examples/darwin-framework-tool/commands/provider/OTASoftwareUpdateInteractive.mm b/examples/darwin-framework-tool/commands/provider/OTASoftwareUpdateInteractive.mm index fae950f04a91a6..b5e33fc2764b1b 100644 --- a/examples/darwin-framework-tool/commands/provider/OTASoftwareUpdateInteractive.mm +++ b/examples/darwin-framework-tool/commands/provider/OTASoftwareUpdateInteractive.mm @@ -59,7 +59,6 @@ bool ParseOTAHeader(chip::OTAImageHeaderParser & parser, const char * otaFilePat static bool ParseJsonFileAndPopulateCandidates( const char * filepath, NSMutableArray ** _Nonnull candidates) { - bool ret = false; Json::Value root; Json::CharReaderBuilder builder; JSONCPP_STRING errs; @@ -70,85 +69,112 @@ static bool ParseJsonFileAndPopulateCandidates( if (!ifs.good()) { ChipLogError(SoftwareUpdate, "Error opening ifstream with file: \"%s\"", filepath); - return ret; + return false; } if (!parseFromStream(builder, ifs, &root, &errs)) { ChipLogError(SoftwareUpdate, "Error parsing JSON from file: \"%s\"", filepath); - return ret; + return false; } 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 { - *candidates = [[NSMutableArray alloc] init]; - for (auto iter : devSofVerModValue) { - DeviceSoftwareVersionModel * candidate = [[DeviceSoftwareVersionModel alloc] init]; - candidate.deviceModelData.vendorId = [NSNumber numberWithUnsignedInt:iter.get("vendorId", 1).asUInt()]; - candidate.deviceModelData.productId = [NSNumber numberWithUnsignedInt:iter.get("productId", 1).asUInt()]; - candidate.softwareVersion = [NSNumber numberWithUnsignedLong:iter.get("softwareVersion", 10).asUInt64()]; - candidate.softwareVersionString = - [NSString stringWithUTF8String:iter.get("softwareVersionString", "1.0.0").asCString()]; - candidate.deviceModelData.cDVersionNumber = [NSNumber numberWithUnsignedInt:iter.get("cDVersionNumber", 0).asUInt()]; - candidate.deviceModelData.softwareVersionValid = iter.get("softwareVersionValid", true).asBool() ? YES : NO; - candidate.deviceModelData.minApplicableSoftwareVersion = - [NSNumber numberWithUnsignedLong:iter.get("minApplicableSoftwareVersion", 0).asUInt64()]; - candidate.deviceModelData.maxApplicableSoftwareVersion = - [NSNumber numberWithUnsignedLong:iter.get("maxApplicableSoftwareVersion", 1000).asUInt64()]; - candidate.deviceModelData.otaURL = [NSString stringWithUTF8String:iter.get("otaURL", "https://test.com").asCString()]; - [*candidates addObject:candidate]; - ret = true; - } + return false; } + + *candidates = [[NSMutableArray alloc] init]; + + bool ret = false; + for (auto iter : devSofVerModValue) { + DeviceSoftwareVersionModel * candidate = [[DeviceSoftwareVersionModel alloc] init]; + + auto vendorId = [NSNumber numberWithUnsignedInt:iter.get("vendorId", 1).asUInt()]; + auto productId = [NSNumber numberWithUnsignedInt:iter.get("productId", 1).asUInt()]; + auto softwareVersion = [NSNumber numberWithUnsignedLong:iter.get("softwareVersion", 10).asUInt64()]; + auto softwareVersionString = [NSString stringWithUTF8String:iter.get("softwareVersionString", "1.0.0").asCString()]; + auto cDVersionNumber = [NSNumber numberWithUnsignedInt:iter.get("cDVersionNumber", 0).asUInt()]; + auto softwareVersionValid = iter.get("softwareVersionValid", true).asBool() ? YES : NO; + auto minApplicableSoftwareVersion = + [NSNumber numberWithUnsignedLong:iter.get("minApplicableSoftwareVersion", 0).asUInt64()]; + auto maxApplicableSoftwareVersion = + [NSNumber numberWithUnsignedLong:iter.get("maxApplicableSoftwareVersion", 1000).asUInt64()]; + auto otaURL = [NSString stringWithUTF8String:iter.get("otaURL", "https://test.com").asCString()]; + + candidate.deviceModelData.vendorId = vendorId; + candidate.deviceModelData.productId = productId; + candidate.softwareVersion = softwareVersion; + candidate.softwareVersionString = softwareVersionString; + candidate.deviceModelData.cDVersionNumber = cDVersionNumber; + candidate.deviceModelData.softwareVersionValid = softwareVersionValid; + candidate.deviceModelData.minApplicableSoftwareVersion = minApplicableSoftwareVersion; + candidate.deviceModelData.maxApplicableSoftwareVersion = maxApplicableSoftwareVersion; + candidate.deviceModelData.otaURL = otaURL; + [*candidates addObject:candidate]; + ret = true; + } + return ret; } CHIP_ERROR OTASoftwareUpdateSetFilePath::RunCommand() { - SetCandidatesFromFilePath(mOTACandidatesFilePath); + auto error = SetCandidatesFromFilePath(mOTACandidatesFilePath); SetCommandExitStatus(nil); - - return CHIP_NO_ERROR; + return error; } CHIP_ERROR OTASoftwareUpdateSetStatus::RunCommand() { - CHIP_ERROR error = CHIP_NO_ERROR; - error = SetUserConsentStatus(mUserConsentStatus); + auto error = SetUserConsentStatus(mUserConsentStatus); SetCommandExitStatus(nil); - return error; } -void OTASoftwareUpdateBase::SetCandidatesFromFilePath(char * _Nonnull filePath) +CHIP_ERROR OTASoftwareUpdateBase::SetCandidatesFromFilePath(char * _Nonnull filePath) { NSMutableArray * candidates; ChipLogDetail(chipTool, "Setting candidates from file path: %s", filePath); - ParseJsonFileAndPopulateCandidates(filePath, &candidates); + VerifyOrReturnError(ParseJsonFileAndPopulateCandidates(filePath, &candidates), CHIP_ERROR_INTERNAL); + for (DeviceSoftwareVersionModel * candidate : candidates) { chip::OTAImageHeaderParser parser; chip::OTAImageHeader header; - ParseOTAHeader(parser, [candidate.deviceModelData.otaURL UTF8String], header); + + auto otaURL = [candidate.deviceModelData.otaURL UTF8String]; + VerifyOrReturnError(ParseOTAHeader(parser, otaURL, header), CHIP_ERROR_INVALID_ARGUMENT); + ChipLogDetail(chipTool, "Validating image list candidate %s: ", [candidate.deviceModelData.otaURL UTF8String]); - VerifyOrDie([candidate.deviceModelData.vendorId unsignedIntValue] == header.mVendorId); - VerifyOrDie([candidate.deviceModelData.productId unsignedIntValue] == header.mProductId); - VerifyOrDie([candidate.softwareVersion unsignedLongValue] == header.mSoftwareVersion); - VerifyOrDie([candidate.softwareVersionString length] == header.mSoftwareVersionString.size()); - VerifyOrDie(memcmp([candidate.softwareVersionString UTF8String], header.mSoftwareVersionString.data(), - header.mSoftwareVersionString.size()) - == 0); + + auto vendorId = [candidate.deviceModelData.vendorId unsignedIntValue]; + auto productId = [candidate.deviceModelData.productId unsignedIntValue]; + auto softwareVersion = [candidate.softwareVersion unsignedLongValue]; + auto softwareVersionString = [candidate.softwareVersionString UTF8String]; + auto softwareVersionStringLength = [candidate.softwareVersionString length]; + auto minApplicableSoftwareVersion = [candidate.deviceModelData.minApplicableSoftwareVersion unsignedLongValue]; + auto maxApplicableSoftwareVersion = [candidate.deviceModelData.maxApplicableSoftwareVersion unsignedLongValue]; + + VerifyOrReturnError(vendorId == header.mVendorId, CHIP_ERROR_INVALID_ARGUMENT); + VerifyOrReturnError(productId == header.mProductId, CHIP_ERROR_INVALID_ARGUMENT); + VerifyOrReturnError(softwareVersion == header.mSoftwareVersion, CHIP_ERROR_INVALID_ARGUMENT); + VerifyOrReturnError(softwareVersionStringLength == header.mSoftwareVersionString.size(), CHIP_ERROR_INVALID_ARGUMENT); + VerifyOrReturnError( + memcmp(softwareVersionString, header.mSoftwareVersionString.data(), header.mSoftwareVersionString.size()) == 0, + CHIP_ERROR_INVALID_ARGUMENT); + if (header.mMinApplicableVersion.HasValue()) { - VerifyOrDie( - [candidate.deviceModelData.minApplicableSoftwareVersion unsignedLongValue] == header.mMinApplicableVersion.Value()); + VerifyOrReturnError(minApplicableSoftwareVersion == header.mMinApplicableVersion.Value(), CHIP_ERROR_INVALID_ARGUMENT); } + if (header.mMaxApplicableVersion.HasValue()) { - VerifyOrDie( - [candidate.deviceModelData.maxApplicableSoftwareVersion unsignedLongValue] == header.mMaxApplicableVersion.Value()); + VerifyOrReturnError(maxApplicableSoftwareVersion == header.mMaxApplicableVersion.Value(), CHIP_ERROR_INVALID_ARGUMENT); } + parser.Clear(); } + mOTADelegate.candidates = candidates; + return CHIP_NO_ERROR; } CHIP_ERROR OTASoftwareUpdateBase::SetUserConsentStatus(char * _Nonnull otaSTatus) From 2db729f64710d9ddb2ce91efd13b4d4f2a7b18c8 Mon Sep 17 00:00:00 2001 From: Vivien Nicolas Date: Fri, 22 Jul 2022 18:32:41 +0200 Subject: [PATCH 2/3] [documentation] Fix some typos in the documentation of the bdx protocol --- src/protocols/bdx/BdxTransferSession.h | 1 - src/protocols/bdx/TransferFacilitator.h | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/protocols/bdx/BdxTransferSession.h b/src/protocols/bdx/BdxTransferSession.h index 7696d0473c9dc8..f5ef397ca7d9e8 100644 --- a/src/protocols/bdx/BdxTransferSession.h +++ b/src/protocols/bdx/BdxTransferSession.h @@ -174,7 +174,6 @@ class DLL_EXPORT TransferSession * @param initData Data for initializing this object and for populating a TransferInit message * The role parameter will determine whether to populate a ReceiveInit or SendInit * @param timeout The amount of time to wait for a response before considering the transfer failed - * @param curTime The current time since epoch. Needed to set a start time for the transfer timeout. * * @return CHIP_ERROR Result of initialization and preparation of a TransferInit message. May also indicate if the * TransferSession object is unable to handle this request. diff --git a/src/protocols/bdx/TransferFacilitator.h b/src/protocols/bdx/TransferFacilitator.h index 3729b4adec90e3..3cc3b55f7d0717 100644 --- a/src/protocols/bdx/TransferFacilitator.h +++ b/src/protocols/bdx/TransferFacilitator.h @@ -137,8 +137,8 @@ class Initiator : public TransferFacilitator * @param[in] layer A System::Layer pointer to use to start the polling timer * @param[in] role The role of the Initiator: Sender or Receiver of BDX data * @param[in] initData Data needed for preparing a transfer request BDX message - * @param[in] timeoutMs The chosen timeout delay for the BDX transfer in milliseconds - * @param[in] pollFreqMs The period for the TransferSession poll timer in milliseconds + * @param[in] timeout The chosen timeout delay for the BDX transfer in milliseconds + * @param[in] pollFreq The period for the TransferSession poll timer in milliseconds */ CHIP_ERROR InitiateTransfer(System::Layer * layer, TransferRole role, const TransferSession::TransferInitData & initData, System::Clock::Timeout timeout, From 42ccf5e90420776cfb7e1ff76b059e92a4986679 Mon Sep 17 00:00:00 2001 From: Vivien Nicolas Date: Fri, 22 Jul 2022 18:34:11 +0200 Subject: [PATCH 3/3] [BDX][Darwin] Add basic BDX protocol supports for Matter.framework --- .../commands/provider/OTAProviderDelegate.mm | 134 +++++-- .../Framework/CHIP/MTROTAProviderDelegate.h | 22 ++ .../CHIP/MTROTAProviderDelegateBridge.mm | 348 +++++++++++++++--- 3 files changed, 421 insertions(+), 83 deletions(-) diff --git a/examples/darwin-framework-tool/commands/provider/OTAProviderDelegate.mm b/examples/darwin-framework-tool/commands/provider/OTAProviderDelegate.mm index 1ee29bf5da13b6..afedecb14aa97a 100644 --- a/examples/darwin-framework-tool/commands/provider/OTAProviderDelegate.mm +++ b/examples/darwin-framework-tool/commands/provider/OTAProviderDelegate.mm @@ -17,11 +17,14 @@ #include "OTAProviderDelegate.h" #import +#include constexpr uint8_t kUpdateTokenLen = 32; @interface OTAProviderDelegate () @property NSString * mOTAFilePath; +@property NSFileHandle * mFileHandle; +@property NSNumber * mFileOffset; @property DeviceSoftwareVersionModel * candidate; @end @@ -37,15 +40,15 @@ - (instancetype)init return self; } -// TODO: When BDX is added to Matter.framework, update to initialize -// it when there is an update available. - (void)handleQueryImage:(MTROtaSoftwareUpdateProviderClusterQueryImageParams * _Nonnull)params completionHandler:(void (^_Nonnull)(MTROtaSoftwareUpdateProviderClusterQueryImageResponseParams * _Nullable data, NSError * _Nullable error))completionHandler { NSError * error; - _selectedCandidate.status = @(MTROtaSoftwareUpdateProviderOTAQueryStatusNotAvailable); - if (![params.protocolsSupported containsObject:@(MTROtaSoftwareUpdateProviderOTADownloadProtocolBDXSynchronous)]) { + + auto isBDXProtocolSupported = + [params.protocolsSupported containsObject:@(MTROtaSoftwareUpdateProviderOTADownloadProtocolBDXSynchronous)]; + if (!isBDXProtocolSupported) { _selectedCandidate.status = @(MTROtaSoftwareUpdateProviderOTAQueryStatusDownloadProtocolNotSupported); error = [[NSError alloc] initWithDomain:@"OTAProviderDomain" @@ -55,42 +58,47 @@ - (void)handleQueryImage:(MTROtaSoftwareUpdateProviderClusterQueryImageParams * return; } - if ([self SelectOTACandidate:params.vendorId rPID:params.productId rSV:params.softwareVersion]) { - _selectedCandidate.status = @(MTROtaSoftwareUpdateProviderOTAQueryStatusUpdateAvailable); - _selectedCandidate.updateToken = [self generateUpdateToken]; - if (params.requestorCanConsent.integerValue == 1) { - _selectedCandidate.userConsentNeeded - = (_userConsentState == OTAProviderUserUnknown || _userConsentState == OTAProviderUserDenied) ? @(1) : @(0); - NSLog(@"User Consent Needed: %@", _selectedCandidate.userConsentNeeded); - } else { - NSLog(@"Requestor cannot obtain user consent. Our State: %hhu", _userConsentState); - switch (_userConsentState) { - case OTAProviderUserGranted: - NSLog(@"User Consent Granted"); - _queryImageStatus = MTROtaSoftwareUpdateProviderOTAQueryStatusUpdateAvailable; - break; - - case OTAProviderUserObtaining: - NSLog(@"User Consent Obtaining"); - _queryImageStatus = MTROtaSoftwareUpdateProviderOTAQueryStatusBusy; - break; - - case OTAProviderUserDenied: - case OTAProviderUserUnknown: - NSLog(@"User Consent Denied or Uknown"); - _queryImageStatus = MTROtaSoftwareUpdateProviderOTAQueryStatusNotAvailable; - break; - } - _selectedCandidate.status = @(_queryImageStatus); - } - } else { + auto hasCandidate = [self SelectOTACandidate:params.vendorId rPID:params.productId rSV:params.softwareVersion]; + if (!hasCandidate) { NSLog(@"Unable to select OTA Image."); _selectedCandidate.status = @(MTROtaSoftwareUpdateProviderOTAQueryStatusNotAvailable); error = [[NSError alloc] initWithDomain:@"OTAProviderDomain" code:MTRErrorCodeInvalidState userInfo:@{ NSLocalizedDescriptionKey : NSLocalizedString(@"Unable to select Candidate.", nil) }]; + return; + } + + _selectedCandidate.updateToken = [self generateUpdateToken]; + + if (params.requestorCanConsent.integerValue == 1) { + _selectedCandidate.status = @(MTROtaSoftwareUpdateProviderOTAQueryStatusUpdateAvailable); + _selectedCandidate.userConsentNeeded + = (_userConsentState == OTAProviderUserUnknown || _userConsentState == OTAProviderUserDenied) ? @(1) : @(0); + NSLog(@"User Consent Needed: %@", _selectedCandidate.userConsentNeeded); + completionHandler(_selectedCandidate, error); + return; + } + + NSLog(@"Requestor cannot obtain user consent. Our State: %hhu", _userConsentState); + switch (_userConsentState) { + case OTAProviderUserGranted: + NSLog(@"User Consent Granted"); + _queryImageStatus = MTROtaSoftwareUpdateProviderOTAQueryStatusUpdateAvailable; + break; + + case OTAProviderUserObtaining: + NSLog(@"User Consent Obtaining"); + _queryImageStatus = MTROtaSoftwareUpdateProviderOTAQueryStatusBusy; + break; + + case OTAProviderUserDenied: + case OTAProviderUserUnknown: + NSLog(@"User Consent Denied or Uknown"); + _queryImageStatus = MTROtaSoftwareUpdateProviderOTAQueryStatusNotAvailable; + break; } + _selectedCandidate.status = @(_queryImageStatus); completionHandler(_selectedCandidate, error); } @@ -110,6 +118,66 @@ - (void)handleNotifyUpdateApplied:(MTROtaSoftwareUpdateProviderClusterNotifyUpda completionHandler(nil); } +- (void)handleBDXTransferSessionBegin:(NSString * _Nonnull)fileDesignator + offset:(NSNumber * _Nonnull)offset + completionHandler:(void (^)(NSError * error))completionHandler +{ + NSLog(@"BDX TransferSession begin with %@ (offset: %@ )", fileDesignator, offset); + + auto * handle = [NSFileHandle fileHandleForReadingAtPath:fileDesignator]; + if (handle == nil) { + auto errorString = [NSString stringWithFormat:@"Error accessing file at at %@", fileDesignator]; + auto error = [[NSError alloc] initWithDomain:@"OTAProviderDomain" + code:MTRErrorCodeGeneralError + userInfo:@{ NSLocalizedDescriptionKey : NSLocalizedString(errorString, nil) }]; + completionHandler(error); + return; + } + + NSError * seekError = nil; + [handle seekToOffset:[offset unsignedLongValue] error:&seekError]; + if (seekError != nil) { + auto errorString = [NSString stringWithFormat:@"Error seeking file (%@) to offset %@", fileDesignator, offset]; + auto error = [[NSError alloc] initWithDomain:@"OTAProviderDomain" + code:MTRErrorCodeGeneralError + userInfo:@{ NSLocalizedDescriptionKey : NSLocalizedString(errorString, nil) }]; + completionHandler(error); + return; + } + + _mFileHandle = handle; + _mFileOffset = offset; + completionHandler(nil); +} + +- (void)handleBDXTransferSessionEnd:(NSError * _Nullable)error +{ + NSLog(@"BDX TransferSession end with error: %@", error); + _mFileHandle = nil; + _mFileOffset = nil; +} + +- (void)handleBDXQuery:(NSNumber * _Nonnull)blockSize + blockIndex:(NSNumber * _Nonnull)blockIndex + bytesToSkip:(NSNumber * _Nonnull)bytesToSkip + completionHandler:(void (^)(NSData * _Nullable data, BOOL isEOF))completionHandler +{ + NSLog(@"BDX Query received blockSize: %@, blockIndex: %@", blockSize, blockIndex); + + NSError * error = nil; + auto offset = [_mFileOffset unsignedLongValue] + [bytesToSkip unsignedLongLongValue] + + ([blockSize unsignedLongValue] * [blockIndex unsignedLongValue]); + [_mFileHandle seekToOffset:offset error:&error]; + if (error != nil) { + NSLog(@"Error seeking to offset %@", @(offset)); + completionHandler(nil, NO); + return; + } + + NSData * data = [_mFileHandle readDataOfLength:[blockSize unsignedLongValue]]; + completionHandler(data, [[_mFileHandle availableData] length] == 0); +} + - (void)SetOTAFilePath:(const char *)path { _mOTAFilePath = [NSString stringWithUTF8String:path]; diff --git a/src/darwin/Framework/CHIP/MTROTAProviderDelegate.h b/src/darwin/Framework/CHIP/MTROTAProviderDelegate.h index 81aef65dbf5005..9f431a3f8777b1 100644 --- a/src/darwin/Framework/CHIP/MTROTAProviderDelegate.h +++ b/src/darwin/Framework/CHIP/MTROTAProviderDelegate.h @@ -50,6 +50,28 @@ NS_ASSUME_NONNULL_BEGIN - (void)handleNotifyUpdateApplied:(MTROtaSoftwareUpdateProviderClusterNotifyUpdateAppliedParams *)params completionHandler:(StatusCompletion)completionHandler; +/** + * Notify the delegate when a BDX Session starts + * + */ +- (void)handleBDXTransferSessionBegin:(NSString * _Nonnull)fileDesignator + offset:(NSNumber * _Nonnull)offset + completionHandler:(void (^)(NSError * error))completionHandler; + +/** + * Notify the delegate when a BDX Session ends + * + */ +- (void)handleBDXTransferSessionEnd:(NSError * _Nullable)error; + +/** + * Notify the delegate when a BDX Query message has been received + * + */ +- (void)handleBDXQuery:(NSNumber * _Nonnull)blockSize + blockIndex:(NSNumber * _Nonnull)blockIndex + bytesToSkip:(NSNumber * _Nonnull)bytesToSkip + completionHandler:(void (^)(NSData * _Nullable data, BOOL isEOF))completionHandler; @end NS_ASSUME_NONNULL_END diff --git a/src/darwin/Framework/CHIP/MTROTAProviderDelegateBridge.mm b/src/darwin/Framework/CHIP/MTROTAProviderDelegateBridge.mm index 17c990b4c53697..af52d42a31e058 100644 --- a/src/darwin/Framework/CHIP/MTROTAProviderDelegateBridge.mm +++ b/src/darwin/Framework/CHIP/MTROTAProviderDelegateBridge.mm @@ -23,6 +23,245 @@ #include #include +// BDX +#include +#include + +#include // For InteractionModelEngine::GetInstance()->GetExchangeManager(); +#include // For &DeviceLayer::SystemLayer() +// BDX + +using namespace chip; +using namespace chip::app; +using namespace chip::app::Clusters::OtaSoftwareUpdateProvider; + +// TODO Expose a method onto the delegate to make that configurable. +constexpr uint32_t kMaxBdxBlockSize = 1024; +constexpr System::Clock::Timeout kBdxTimeout = System::Clock::Seconds16(5 * 60); // OTA Spec mandates >= 5 minutes +constexpr System::Clock::Timeout kBdxPollIntervalMs = System::Clock::Milliseconds32(50); +constexpr bdx::TransferRole kBdxRole = bdx::TransferRole::kSender; + +class BdxOTASender : public bdx::Responder { +public: + BdxOTASender() {} + + CHIP_ERROR Start(FabricIndex fabricIndex, NodeId nodeId) + { + if (mInitialized) { + VerifyOrReturnError(mFabricIndex.HasValue() && mNodeId.HasValue(), CHIP_ERROR_INCORRECT_STATE); + + // Prevent a new node connection since another is active + VerifyOrReturnError(mFabricIndex.Value() == fabricIndex && mNodeId.Value() == nodeId, CHIP_ERROR_BUSY); + + // Reset stale connection from the Same Node if exists + Reset(); + } + mInitialized = true; + + mFabricIndex.SetValue(fabricIndex); + mNodeId.SetValue(nodeId); + + BitFlags flags(bdx::TransferControlFlags::kReceiverDrive); + // TODO Have a better mechanism to remove the need from getting an instance of the system layer here. + return PrepareForTransfer(&DeviceLayer::SystemLayer(), kBdxRole, flags, kMaxBdxBlockSize, kBdxTimeout, kBdxPollIntervalMs); + } + + void SetDelegate(id delegate, dispatch_queue_t queue) + { + // TODO Have a better mechanism to retrieve the exchange manager instance + // In order to register ourself as a protocol handler for BDX, it needs to be a reference + // to the exchange manager instance. That's not ideal but the reference is retrieved + // from the interaction model engine instance. + auto exchangeMgr = InteractionModelEngine::GetInstance()->GetExchangeManager(); + if (delegate && queue) { + mDelegate = delegate; + mDelegateQueue = queue; + mWorkQueue = DeviceLayer::PlatformMgrImpl().GetWorkQueue(); + exchangeMgr->RegisterUnsolicitedMessageHandlerForProtocol(Protocols::BDX::Id, this); + } else { + Reset(); + exchangeMgr->UnregisterUnsolicitedMessageHandlerForProtocol(Protocols::BDX::Id); + } + } + +private: + CHIP_ERROR OnMessageToSend(bdx::TransferSession::OutputEvent & event) + { + VerifyOrReturnError(mExchangeCtx != nullptr, CHIP_ERROR_INCORRECT_STATE); + VerifyOrReturnError(mDelegate != nil, CHIP_ERROR_INCORRECT_STATE); + VerifyOrReturnError(mDelegateQueue != nil, CHIP_ERROR_INCORRECT_STATE); + + Messaging::SendFlags sendFlags; + + // All messages sent from the Sender expect a response, except for a StatusReport which would indicate an error and + // the end of the transfer. + if (!event.msgTypeData.HasMessageType(Protocols::SecureChannel::MsgType::StatusReport)) { + sendFlags.Set(Messaging::SendMessageFlags::kExpectResponse); + } + + auto & msgTypeData = event.msgTypeData; + return mExchangeCtx->SendMessage(msgTypeData.ProtocolId, msgTypeData.MessageType, std::move(event.MsgData), sendFlags); + } + + CHIP_ERROR OnTransferSessionBegin(bdx::TransferSession::OutputEvent & event) + { + uint16_t fdl = 0; + auto fd = mTransfer.GetFileDesignator(fdl); + VerifyOrReturnError(fdl <= bdx::kMaxFileDesignatorLen, CHIP_ERROR_INVALID_ARGUMENT); + + auto fileDesignator = [[NSString alloc] initWithBytes:fd length:fdl encoding:NSUTF8StringEncoding]; + auto offset = @(mTransfer.GetStartOffset()); + auto completionHandler = ^(NSError * error) { + dispatch_async(mWorkQueue, ^{ + if (error != nil) { + LogErrorOnFailure([MTRError errorToCHIPErrorCode:error]); + LogErrorOnFailure(mTransfer.AbortTransfer(bdx::StatusCode::kUnknown)); + return; + } + + // bdx::TransferSession will automatically reject a transfer if there are no + // common supported control modes. It will also default to the smaller + // block size. + bdx::TransferSession::TransferAcceptData acceptData; + acceptData.ControlMode = bdx::TransferControlFlags::kReceiverDrive; + acceptData.MaxBlockSize = mTransfer.GetTransferBlockSize(); + acceptData.StartOffset = mTransfer.GetStartOffset(); + acceptData.Length = mTransfer.GetTransferLength(); + + LogErrorOnFailure(mTransfer.AcceptTransfer(acceptData)); + }); + }; + + dispatch_async(mDelegateQueue, ^{ + [mDelegate handleBDXTransferSessionBegin:fileDesignator offset:offset completionHandler:completionHandler]; + }); + + return CHIP_NO_ERROR; + } + + CHIP_ERROR OnTransferSessionEnd(bdx::TransferSession::OutputEvent & event) + { + CHIP_ERROR error = CHIP_ERROR_INTERNAL; + if (event.EventType == bdx::TransferSession::OutputEventType::kAckEOFReceived) { + error = CHIP_NO_ERROR; + } else if (event.EventType == bdx::TransferSession::OutputEventType::kTransferTimeout) { + error = CHIP_ERROR_TIMEOUT; + } + + auto delegate = mDelegate; // mDelegate will be set to nil by Reset, so get a strong ref to it. + dispatch_async(mDelegateQueue, ^{ + [delegate handleBDXTransferSessionEnd:[MTRError errorForCHIPErrorCode:error]]; + }); + + Reset(); + return CHIP_NO_ERROR; + } + + CHIP_ERROR OnBlockQuery(bdx::TransferSession::OutputEvent & event) + { + auto blockSize = @(mTransfer.GetTransferBlockSize()); + auto blockIndex = @(mTransfer.GetNextBlockNum()); + + auto bytesToSkip = @(0); + if (event.EventType == bdx::TransferSession::OutputEventType::kQueryWithSkipReceived) { + bytesToSkip = @(event.bytesToSkip.BytesToSkip); + } + + auto completionHandler = ^(NSData * _Nullable data, BOOL isEOF) { + dispatch_async(mWorkQueue, ^{ + if (data == nil) { + LogErrorOnFailure(mTransfer.AbortTransfer(bdx::StatusCode::kUnknown)); + return; + } + + bdx::TransferSession::BlockData blockData; + blockData.Data = static_cast([data bytes]); + blockData.Length = static_cast([data length]); + blockData.IsEof = isEOF; + + CHIP_ERROR err = mTransfer.PrepareBlock(blockData); + if (CHIP_NO_ERROR != err) { + LogErrorOnFailure(err); + LogErrorOnFailure(mTransfer.AbortTransfer(bdx::StatusCode::kUnknown)); + } + }); + }; + + // TODO Handle MaxLength + + dispatch_async(mDelegateQueue, ^{ + [mDelegate handleBDXQuery:blockSize blockIndex:blockIndex bytesToSkip:bytesToSkip completionHandler:completionHandler]; + }); + + return CHIP_NO_ERROR; + } + + void HandleTransferSessionOutput(bdx::TransferSession::OutputEvent & event) override + { + VerifyOrReturn(mDelegate != nil); + VerifyOrReturn(mDelegateQueue != nil); + + CHIP_ERROR err = CHIP_NO_ERROR; + switch (event.EventType) { + case bdx::TransferSession::OutputEventType::kInitReceived: + err = OnTransferSessionBegin(event); + break; + case bdx::TransferSession::OutputEventType::kStatusReceived: + ChipLogError(BDX, "Got StatusReport %x", static_cast(event.statusData.statusCode)); + [[fallthrough]]; + case bdx::TransferSession::OutputEventType::kAckEOFReceived: + case bdx::TransferSession::OutputEventType::kInternalError: + case bdx::TransferSession::OutputEventType::kTransferTimeout: + err = OnTransferSessionEnd(event); + break; + case bdx::TransferSession::OutputEventType::kQueryWithSkipReceived: + case bdx::TransferSession::OutputEventType::kQueryReceived: + err = OnBlockQuery(event); + break; + case bdx::TransferSession::OutputEventType::kMsgToSend: + err = OnMessageToSend(event); + break; + case bdx::TransferSession::OutputEventType::kNone: + case bdx::TransferSession::OutputEventType::kAckReceived: + // Nothing to do. + break; + case bdx::TransferSession::OutputEventType::kAcceptReceived: + case bdx::TransferSession::OutputEventType::kBlockReceived: + default: + // Should never happens. + chipDie(); + break; + } + LogErrorOnFailure(err); + } + + void Reset() + { + mFabricIndex.ClearValue(); + mNodeId.ClearValue(); + mTransfer.Reset(); + if (mExchangeCtx != nullptr) { + mExchangeCtx->Close(); + mExchangeCtx = nullptr; + } + + mDelegate = nil; + mDelegateQueue = nil; + mWorkQueue = nil; + + mInitialized = false; + } + + bool mInitialized = false; + Optional mFabricIndex; + Optional mNodeId; + id mDelegate = nil; + dispatch_queue_t mDelegateQueue = nil; + dispatch_queue_t mWorkQueue = nil; +}; + +BdxOTASender gOtaSender; + static NSInteger const kOtaProviderEndpoint = 0; MTROTAProviderDelegateBridge::MTROTAProviderDelegateBridge(void) @@ -37,37 +276,53 @@ mDelegate = delegate ?: nil; mQueue = queue ?: nil; - chip::app::Clusters::OTAProvider::SetDelegate(kOtaProviderEndpoint, this); + gOtaSender.SetDelegate(delegate, queue); + Clusters::OTAProvider::SetDelegate(kOtaProviderEndpoint, this); } -void MTROTAProviderDelegateBridge::HandleQueryImage(chip::app::CommandHandler * commandObj, - const chip::app::ConcreteCommandPath & commandPath, - const chip::app::Clusters::OtaSoftwareUpdateProvider::Commands::QueryImage::DecodableType & commandData) +void MTROTAProviderDelegateBridge::HandleQueryImage( + CommandHandler * commandObj, const ConcreteCommandPath & commandPath, const Commands::QueryImage::DecodableType & commandData) { id strongDelegate = mDelegate; if (strongDelegate && mQueue) { auto * commandParams = [[MTROtaSoftwareUpdateProviderClusterQueryImageParams alloc] init]; CHIP_ERROR err = ConvertToQueryImageParams(commandData, commandParams); if (err != CHIP_NO_ERROR) { - commandObj->AddStatus(commandPath, chip::Protocols::InteractionModel::Status::InvalidCommand); + commandObj->AddStatus(commandPath, Protocols::InteractionModel::Status::InvalidCommand); return; } // Make sure to hold on to the command handler and command path to be used in the completion block - __block chip::app::CommandHandler::Handle handle(commandObj); - __block chip::app::ConcreteCommandPath cachedCommandPath( - commandPath.mEndpointId, commandPath.mClusterId, commandPath.mCommandId); + __block CommandHandler::Handle handle(commandObj); + __block ConcreteCommandPath cachedCommandPath(commandPath.mEndpointId, commandPath.mClusterId, commandPath.mCommandId); dispatch_async(mQueue, ^{ [strongDelegate handleQueryImage:commandParams completionHandler:^(MTROtaSoftwareUpdateProviderClusterQueryImageResponseParams * _Nullable data, NSError * _Nullable error) { - dispatch_async(chip::DeviceLayer::PlatformMgrImpl().GetWorkQueue(), ^{ - chip::app::Clusters::OtaSoftwareUpdateProvider::Commands::QueryImageResponse::Type response; + dispatch_async(DeviceLayer::PlatformMgrImpl().GetWorkQueue(), ^{ + Commands::QueryImageResponse::Type response; ConvertFromQueryImageResponseParms(data, response); - chip::app::CommandHandler * handler = handle.Get(); + CommandHandler * handler = handle.Get(); if (handler) { + auto hasUpdate = + [data.status isEqual:@(MTROtaSoftwareUpdateProviderOTAQueryStatusUpdateAvailable)]; + auto isBDXProtocolSupported = [commandParams.protocolsSupported + containsObject:@(MTROtaSoftwareUpdateProviderOTADownloadProtocolBDXSynchronous)]; + + if (hasUpdate && isBDXProtocolSupported) { + auto fabricIndex = handler->GetSubjectDescriptor().fabricIndex; + auto nodeId = handler->GetSubjectDescriptor().subject; + CHIP_ERROR err = gOtaSender.Start(fabricIndex, nodeId); + if (CHIP_NO_ERROR != err) { + LogErrorOnFailure(err); + handler->AddStatus(cachedCommandPath, Protocols::InteractionModel::Status::Failure); + handle.Release(); + return; + } + } + handler->AddResponse(cachedCommandPath, response); handle.Release(); } @@ -77,14 +332,12 @@ } } -void MTROTAProviderDelegateBridge::HandleApplyUpdateRequest(chip::app::CommandHandler * commandObj, - const chip::app::ConcreteCommandPath & commandPath, - const chip::app::Clusters::OtaSoftwareUpdateProvider::Commands::ApplyUpdateRequest::DecodableType & commandData) +void MTROTAProviderDelegateBridge::HandleApplyUpdateRequest(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, + const Commands::ApplyUpdateRequest::DecodableType & commandData) { // Make sure to hold on to the command handler and command path to be used in the completion block - __block chip::app::CommandHandler::Handle handle(commandObj); - __block chip::app::ConcreteCommandPath cachedCommandPath( - commandPath.mEndpointId, commandPath.mClusterId, commandPath.mCommandId); + __block CommandHandler::Handle handle(commandObj); + __block ConcreteCommandPath cachedCommandPath(commandPath.mEndpointId, commandPath.mClusterId, commandPath.mCommandId); id strongDelegate = mDelegate; if (strongDelegate && mQueue) { @@ -96,11 +349,11 @@ handleApplyUpdateRequest:commandParams completionHandler:^(MTROtaSoftwareUpdateProviderClusterApplyUpdateResponseParams * _Nullable data, NSError * _Nullable error) { - dispatch_async(chip::DeviceLayer::PlatformMgrImpl().GetWorkQueue(), ^{ - chip::app::Clusters::OtaSoftwareUpdateProvider::Commands::ApplyUpdateResponse::Type response; + dispatch_async(DeviceLayer::PlatformMgrImpl().GetWorkQueue(), ^{ + Commands::ApplyUpdateResponse::Type response; ConvertFromApplyUpdateRequestResponseParms(data, response); - chip::app::CommandHandler * handler = handle.Get(); + CommandHandler * handler = handle.Get(); if (handler) { handler->AddResponse(cachedCommandPath, response); handle.Release(); @@ -111,14 +364,12 @@ } } -void MTROTAProviderDelegateBridge::HandleNotifyUpdateApplied(chip::app::CommandHandler * commandObj, - const chip::app::ConcreteCommandPath & commandPath, - const chip::app::Clusters::OtaSoftwareUpdateProvider::Commands::NotifyUpdateApplied::DecodableType & commandData) +void MTROTAProviderDelegateBridge::HandleNotifyUpdateApplied(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, + const Commands::NotifyUpdateApplied::DecodableType & commandData) { // Make sure to hold on to the command handler and command path to be used in the completion block - __block chip::app::CommandHandler::Handle handle(commandObj); - __block chip::app::ConcreteCommandPath cachedCommandPath( - commandPath.mEndpointId, commandPath.mClusterId, commandPath.mCommandId); + __block CommandHandler::Handle handle(commandObj); + __block ConcreteCommandPath cachedCommandPath(commandPath.mEndpointId, commandPath.mClusterId, commandPath.mCommandId); id strongDelegate = mDelegate; if (strongDelegate && mQueue) { @@ -126,24 +377,22 @@ ConvertToNotifyUpdateAppliedParams(commandData, commandParams); dispatch_async(mQueue, ^{ - [strongDelegate - handleNotifyUpdateApplied:commandParams - completionHandler:^(NSError * _Nullable error) { - dispatch_async(chip::DeviceLayer::PlatformMgrImpl().GetWorkQueue(), ^{ - chip::app::CommandHandler * handler = handle.Get(); - if (handler) { - handler->AddStatus(cachedCommandPath, chip::Protocols::InteractionModel::Status::Success); - handle.Release(); - } - }); - }]; + [strongDelegate handleNotifyUpdateApplied:commandParams + completionHandler:^(NSError * _Nullable error) { + dispatch_async(DeviceLayer::PlatformMgrImpl().GetWorkQueue(), ^{ + CommandHandler * handler = handle.Get(); + if (handler) { + handler->AddStatus(cachedCommandPath, Protocols::InteractionModel::Status::Success); + handle.Release(); + } + }); + }]; }); } } CHIP_ERROR MTROTAProviderDelegateBridge::ConvertToQueryImageParams( - const chip::app::Clusters::OtaSoftwareUpdateProvider::Commands::QueryImage::DecodableType & commandData, - MTROtaSoftwareUpdateProviderClusterQueryImageParams * commandParams) + const Commands::QueryImage::DecodableType & commandData, MTROtaSoftwareUpdateProviderClusterQueryImageParams * commandParams) { commandParams.vendorId = [NSNumber numberWithUnsignedShort:commandData.vendorId]; commandParams.productId = [NSNumber numberWithUnsignedShort:commandData.productId]; @@ -151,8 +400,8 @@ auto iterator = commandData.protocolsSupported.begin(); NSMutableArray * protocolsSupported = [[NSMutableArray alloc] init]; while (iterator.Next()) { - chip::app::Clusters::OtaSoftwareUpdateProvider::OTADownloadProtocol protocol = iterator.GetValue(); - [protocolsSupported addObject:[NSNumber numberWithInt:chip::to_underlying(protocol)]]; + OTADownloadProtocol protocol = iterator.GetValue(); + [protocolsSupported addObject:[NSNumber numberWithInt:to_underlying(protocol)]]; } ReturnErrorOnFailure(iterator.GetStatus()); commandParams.protocolsSupported = protocolsSupported; @@ -179,16 +428,16 @@ void MTROTAProviderDelegateBridge::ConvertFromQueryImageResponseParms( const MTROtaSoftwareUpdateProviderClusterQueryImageResponseParams * responseParams, - chip::app::Clusters::OtaSoftwareUpdateProvider::Commands::QueryImageResponse::Type & response) + Commands::QueryImageResponse::Type & response) { - response.status = static_cast([responseParams.status intValue]); + response.status = static_cast([responseParams.status intValue]); if (responseParams.delayedActionTime) { response.delayedActionTime.SetValue([responseParams.delayedActionTime unsignedIntValue]); } if (responseParams.imageURI) { - response.imageURI.SetValue(chip::CharSpan([responseParams.imageURI UTF8String], responseParams.imageURI.length)); + response.imageURI.SetValue(CharSpan([responseParams.imageURI UTF8String], responseParams.imageURI.length)); } if (responseParams.softwareVersion) { @@ -197,7 +446,7 @@ if (responseParams.softwareVersionString) { response.softwareVersionString.SetValue( - chip::CharSpan([responseParams.softwareVersionString UTF8String], responseParams.softwareVersionString.length)); + CharSpan([responseParams.softwareVersionString UTF8String], responseParams.softwareVersionString.length)); } if (responseParams.updateToken) { @@ -214,7 +463,7 @@ } void MTROTAProviderDelegateBridge::ConvertToApplyUpdateRequestParams( - const chip::app::Clusters::OtaSoftwareUpdateProvider::Commands::ApplyUpdateRequest::DecodableType & commandData, + const Commands::ApplyUpdateRequest::DecodableType & commandData, MTROtaSoftwareUpdateProviderClusterApplyUpdateRequestParams * commandParams) { commandParams.updateToken = AsData(commandData.updateToken); @@ -223,15 +472,14 @@ void MTROTAProviderDelegateBridge::ConvertFromApplyUpdateRequestResponseParms( const MTROtaSoftwareUpdateProviderClusterApplyUpdateResponseParams * responseParams, - chip::app::Clusters::OtaSoftwareUpdateProvider::Commands::ApplyUpdateResponse::Type & response) + Commands::ApplyUpdateResponse::Type & response) { - response.action - = static_cast([responseParams.action intValue]); + response.action = static_cast([responseParams.action intValue]); response.delayedActionTime = [responseParams.delayedActionTime unsignedIntValue]; } void MTROTAProviderDelegateBridge::ConvertToNotifyUpdateAppliedParams( - const chip::app::Clusters::OtaSoftwareUpdateProvider::Commands::NotifyUpdateApplied::DecodableType & commandData, + const Commands::NotifyUpdateApplied::DecodableType & commandData, MTROtaSoftwareUpdateProviderClusterNotifyUpdateAppliedParams * commandParams) { commandParams.updateToken = AsData(commandData.updateToken);