| /* |
| * |
| * 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. |
| */ |
| |
| // This file contains an implementation of the OTARequestorDriver interface class. |
| // Individual platforms may supply their own implementations of OTARequestorDriver |
| // or use this one. There are no requirements or assumptions on the implementation other |
| // than adherence to the OTA Image Update Requestor part of the Matter specification; the |
| // aspects of the functionality not mandated by the specification are considered implementation choices. |
| // |
| // This particular implementation of the OTARequestorDriver makes the following choices: |
| // - Only a single timer can be active at any given moment |
| // - The periodic query timer is running if and only if there is no update in progress (the core logic |
| // UpdateState is kIdle) |
| // - AnnounceOTAProviders command is ignored if an update is in progress |
| // - The provider location passed in AnnounceOTAProviders is used in a single query (possibly retried) and then discarded |
| // - Explicitly triggering a query through TriggerImmediateQuery() cancels any in-progress update |
| // - A QueryImage call results in the driver iterating through the list of default OTA providers, from beginning to the end, until a |
| // provider successfully transfers the OTA image. If a provider is busy, it will be retried a set number of times before moving |
| // to the next available one. If all else fails, the periodic query timer is kicked off again. |
| |
| #include <platform/CHIPDeviceLayer.h> |
| #include <platform/OTAImageProcessor.h> |
| |
| #include "DefaultOTARequestorDriver.h" |
| #include "OTARequestorInterface.h" |
| |
| namespace chip { |
| namespace DeviceLayer { |
| namespace { |
| |
| using namespace app::Clusters::OtaSoftwareUpdateRequestor; |
| using namespace app::Clusters::OtaSoftwareUpdateRequestor::Structs; |
| |
| constexpr uint8_t kMaxInvalidSessionRetries = 1; // Max # of query image retries to perform on invalid session error |
| constexpr uint32_t kDelayQueryUponCommissioningSec = 30; // Delay before sending the initial image query after commissioning |
| constexpr uint32_t kImmediateStartDelaySec = 1; // Delay before sending a query in response to UrgentUpdateAvailable |
| constexpr System::Clock::Seconds32 kDefaultDelayedActionTime = System::Clock::Seconds32(120); |
| |
| DefaultOTARequestorDriver * ToDriver(void * context) |
| { |
| return static_cast<DefaultOTARequestorDriver *>(context); |
| } |
| |
| } // namespace |
| |
| void DefaultOTARequestorDriver::Init(OTARequestorInterface * requestor, OTAImageProcessorInterface * processor) |
| { |
| mRequestor = requestor; |
| mImageProcessor = processor; |
| mProviderRetryCount = 0; |
| mInvalidSessionRetryCount = 0; |
| |
| if (mImageProcessor->IsFirstImageRun()) |
| { |
| SystemLayer().ScheduleLambda([this] { |
| CHIP_ERROR error = mImageProcessor->ConfirmCurrentImage(); |
| |
| if (error != CHIP_NO_ERROR) |
| { |
| ChipLogError(SoftwareUpdate, "Failed to confirm image: %" CHIP_ERROR_FORMAT, error.Format()); |
| mRequestor->Reset(); |
| return; |
| } |
| |
| if (mSendNotifyUpdateApplied) |
| { |
| mRequestor->NotifyUpdateApplied(); |
| } |
| }); |
| } |
| else if ((mRequestor->GetCurrentUpdateState() != OTAUpdateStateEnum::kIdle)) |
| { |
| // Not running a new image for the first time but also not in the idle state may indicate there is a problem |
| mRequestor->Reset(); |
| } |
| else |
| { |
| // Start the first periodic query timer |
| StartSelectedTimer(SelectedTimer::kPeriodicQueryTimer); |
| } |
| } |
| |
| bool DefaultOTARequestorDriver::CanConsent() |
| { |
| return false; |
| } |
| |
| uint16_t DefaultOTARequestorDriver::GetMaxDownloadBlockSize() |
| { |
| return maxDownloadBlockSize; |
| } |
| |
| void DefaultOTARequestorDriver::SetMaxDownloadBlockSize(uint16_t blockSize) |
| { |
| maxDownloadBlockSize = blockSize; |
| } |
| |
| void StartDelayTimerHandler(System::Layer * systemLayer, void * appState) |
| { |
| ToDriver(appState)->SendQueryImage(); |
| } |
| |
| bool DefaultOTARequestorDriver::ProviderLocationsEqual(const ProviderLocationType & a, const ProviderLocationType & b) |
| { |
| if ((a.fabricIndex == b.fabricIndex) && (a.providerNodeID == b.providerNodeID) && (a.endpoint == b.endpoint)) |
| { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| void DefaultOTARequestorDriver::HandleIdleStateExit() |
| { |
| // Start watchdog timer to monitor new Query Image session |
| StartSelectedTimer(SelectedTimer::kWatchdogTimer); |
| } |
| |
| void DefaultOTARequestorDriver::HandleIdleStateEnter(IdleStateReason reason) |
| { |
| if (reason != IdleStateReason::kInvalidSession) |
| { |
| mInvalidSessionRetryCount = 0; |
| } |
| |
| switch (reason) |
| { |
| case IdleStateReason::kUnknown: |
| ChipLogProgress(SoftwareUpdate, "Unknown idle state reason so set the periodic timer for a next attempt"); |
| StartSelectedTimer(SelectedTimer::kPeriodicQueryTimer); |
| break; |
| case IdleStateReason::kIdle: |
| // There is no current OTA update in progress so start the periodic query timer |
| StartSelectedTimer(SelectedTimer::kPeriodicQueryTimer); |
| break; |
| case IdleStateReason::kInvalidSession: |
| if (mInvalidSessionRetryCount < kMaxInvalidSessionRetries) |
| { |
| // An invalid session is detected which may be temporary (such as provider being restarted) |
| // so try to query the same provider again. Since the session has already been disconnected prior to |
| // getting here, this new query should trigger an attempt to re-establish CASE. If that subsequently fails, |
| // we conclusively know the provider is not available, and will fall into the else clause below on that attempt. |
| SendQueryImage(); |
| mInvalidSessionRetryCount++; |
| } |
| else |
| { |
| mInvalidSessionRetryCount = 0; |
| StartSelectedTimer(SelectedTimer::kPeriodicQueryTimer); |
| } |
| break; |
| } |
| } |
| |
| void DefaultOTARequestorDriver::DownloadUpdateTimerHandler(System::Layer * systemLayer, void * appState) |
| { |
| DefaultOTARequestorDriver * driver = ToDriver(appState); |
| |
| VerifyOrDie(driver->mRequestor != nullptr); |
| driver->mRequestor->DownloadUpdate(); |
| } |
| |
| void DefaultOTARequestorDriver::ApplyUpdateTimerHandler(System::Layer * systemLayer, void * appState) |
| { |
| DefaultOTARequestorDriver * driver = ToDriver(appState); |
| |
| VerifyOrDie(driver->mRequestor != nullptr); |
| driver->mRequestor->ApplyUpdate(); |
| } |
| |
| void DefaultOTARequestorDriver::ApplyTimerHandler(System::Layer * systemLayer, void * appState) |
| { |
| DefaultOTARequestorDriver * driver = ToDriver(appState); |
| |
| VerifyOrDie(driver->mImageProcessor != nullptr); |
| driver->mImageProcessor->Apply(); |
| } |
| |
| void DefaultOTARequestorDriver::UpdateAvailable(const UpdateDescription & update, System::Clock::Seconds32 delay) |
| { |
| // IMPLEMENTATION CHOICE: |
| // This implementation unconditionally downloads an available update |
| |
| VerifyOrDie(mRequestor != nullptr); |
| ScheduleDelayedAction(delay, DownloadUpdateTimerHandler, this); |
| } |
| |
| CHIP_ERROR DefaultOTARequestorDriver::UpdateNotFound(UpdateNotFoundReason reason, System::Clock::Seconds32 delay) |
| { |
| CHIP_ERROR status = CHIP_NO_ERROR; |
| |
| switch (reason) |
| { |
| case UpdateNotFoundReason::kUpToDate: |
| break; |
| case UpdateNotFoundReason::kBusy: { |
| status = ScheduleQueryRetry(true, chip::max(kDefaultDelayedActionTime, delay)); |
| if (status == CHIP_ERROR_MAX_RETRY_EXCEEDED) |
| { |
| // If max retry exceeded with current provider, try a different provider |
| status = ScheduleQueryRetry(false, chip::max(kDefaultDelayedActionTime, delay)); |
| } |
| break; |
| } |
| case UpdateNotFoundReason::kNotAvailable: { |
| // Schedule a query only if a different provider is available |
| status = ScheduleQueryRetry(false, chip::max(kDefaultDelayedActionTime, delay)); |
| break; |
| } |
| } |
| return status; |
| } |
| |
| void DefaultOTARequestorDriver::UpdateDownloaded() |
| { |
| VerifyOrDie(mRequestor != nullptr); |
| mRequestor->ApplyUpdate(); |
| } |
| |
| void DefaultOTARequestorDriver::UpdateConfirmed(System::Clock::Seconds32 delay) |
| { |
| VerifyOrDie(mImageProcessor != nullptr); |
| ScheduleDelayedAction(delay, ApplyTimerHandler, this); |
| } |
| |
| void DefaultOTARequestorDriver::UpdateSuspended(System::Clock::Seconds32 delay) |
| { |
| VerifyOrDie(mRequestor != nullptr); |
| |
| if (delay < kDefaultDelayedActionTime) |
| { |
| delay = kDefaultDelayedActionTime; |
| } |
| |
| ScheduleDelayedAction(delay, ApplyUpdateTimerHandler, this); |
| } |
| |
| void DefaultOTARequestorDriver::UpdateDiscontinued() |
| { |
| VerifyOrDie(mImageProcessor != nullptr); |
| mImageProcessor->Abort(); |
| |
| // Cancel all update timers |
| UpdateCancelled(); |
| } |
| |
| // Cancel all OTA update timers |
| void DefaultOTARequestorDriver::UpdateCancelled() |
| { |
| // Cancel all OTA Update timers started by OTARequestorDriver regardless of whether thery are running or not |
| CancelDelayedAction(DownloadUpdateTimerHandler, this); |
| CancelDelayedAction(StartDelayTimerHandler, this); |
| CancelDelayedAction(ApplyTimerHandler, this); |
| CancelDelayedAction(ApplyUpdateTimerHandler, this); |
| } |
| |
| void DefaultOTARequestorDriver::ScheduleDelayedAction(System::Clock::Seconds32 delay, System::TimerCompleteCallback action, |
| void * aAppState) |
| { |
| VerifyOrDie(SystemLayer().StartTimer(std::chrono::duration_cast<System::Clock::Timeout>(delay), action, aAppState) == |
| CHIP_NO_ERROR); |
| } |
| |
| void DefaultOTARequestorDriver::CancelDelayedAction(System::TimerCompleteCallback action, void * aAppState) |
| { |
| SystemLayer().CancelTimer(action, aAppState); |
| } |
| |
| // Device commissioning has completed, schedule a provider query |
| void DefaultOTARequestorDriver::OTACommissioningCallback() |
| { |
| // Schedule a query. At the end of this query/update process the Default Provider timer is started |
| ScheduleDelayedAction(System::Clock::Seconds32(kDelayQueryUponCommissioningSec), StartDelayTimerHandler, this); |
| } |
| |
| void DefaultOTARequestorDriver::ProcessAnnounceOTAProviders( |
| const ProviderLocationType & providerLocation, |
| app::Clusters::OtaSoftwareUpdateRequestor::OTAAnnouncementReason announcementReason) |
| { |
| // If reason is URGENT_UPDATE_AVAILABLE, we start OTA immediately. Otherwise, respect the timer value set in mOtaStartDelaySec. |
| // This is done to exemplify what a real-world OTA Requestor might do while also being configurable enough to use as a test app. |
| uint32_t secToStart = 0; |
| switch (announcementReason) |
| { |
| case OTAAnnouncementReason::kSimpleAnnouncement: |
| case OTAAnnouncementReason::kUpdateAvailable: |
| secToStart = mOtaStartDelaySec; |
| break; |
| case OTAAnnouncementReason::kUrgentUpdateAvailable: |
| // TODO: Implement random delay per spec |
| secToStart = kImmediateStartDelaySec; |
| break; |
| default: |
| ChipLogError(SoftwareUpdate, "Unexpected announcementReason: %u", static_cast<uint8_t>(announcementReason)); |
| return; |
| } |
| |
| // IMPLEMENTATION CHOICE: |
| // This implementation of the OTARequestor driver ignores the announcement if an update is in progress, |
| // otherwise it queries the provider passed in the announcement |
| |
| if (mRequestor->GetCurrentUpdateState() != OTAUpdateStateEnum::kIdle) |
| { |
| ChipLogProgress(SoftwareUpdate, "State is not kIdle, ignoring the AnnounceOTAProviders. State: %d", |
| (int) mRequestor->GetCurrentUpdateState()); |
| return; |
| } |
| |
| // Point to the announced provider |
| mRequestor->SetCurrentProviderLocation(providerLocation); |
| |
| ScheduleDelayedAction(System::Clock::Seconds32(secToStart), StartDelayTimerHandler, this); |
| } |
| |
| void DefaultOTARequestorDriver::SendQueryImage() |
| { |
| OTAUpdateStateEnum currentUpdateState; |
| Optional<ProviderLocationType> lastUsedProvider; |
| mRequestor->GetProviderLocation(lastUsedProvider); |
| if (!lastUsedProvider.HasValue()) |
| { |
| ProviderLocationType providerLocation; |
| bool listExhausted = false; |
| if (GetNextProviderLocation(providerLocation, listExhausted) == true) |
| { |
| mRequestor->SetCurrentProviderLocation(providerLocation); |
| } |
| else |
| { |
| ChipLogProgress(SoftwareUpdate, "No provider available"); |
| return; |
| } |
| } |
| |
| currentUpdateState = mRequestor->GetCurrentUpdateState(); |
| if ((currentUpdateState == OTAUpdateStateEnum::kIdle) || (currentUpdateState == OTAUpdateStateEnum::kDelayedOnQuery)) |
| { |
| mProviderRetryCount++; |
| DeviceLayer::SystemLayer().ScheduleLambda([this] { mRequestor->TriggerImmediateQueryInternal(); }); |
| } |
| else |
| { |
| ChipLogProgress(SoftwareUpdate, "Query already in progress"); |
| } |
| } |
| |
| void DefaultOTARequestorDriver::PeriodicQueryTimerHandler(System::Layer * systemLayer, void * appState) |
| { |
| ChipLogProgress(SoftwareUpdate, "Default Provider timer handler is invoked"); |
| |
| DefaultOTARequestorDriver * driver = ToDriver(appState); |
| |
| // Determine which provider to query next |
| ProviderLocationType providerLocation; |
| bool listExhausted = false; |
| if (driver->GetNextProviderLocation(providerLocation, listExhausted) != true) |
| { |
| driver->StartSelectedTimer(SelectedTimer::kPeriodicQueryTimer); |
| return; |
| } |
| |
| driver->mRequestor->SetCurrentProviderLocation(providerLocation); |
| |
| driver->SendQueryImage(); |
| } |
| |
| void DefaultOTARequestorDriver::StartPeriodicQueryTimer() |
| { |
| ChipLogProgress(SoftwareUpdate, "Starting the periodic query timer, timeout: %u seconds", |
| (unsigned int) mPeriodicQueryTimeInterval); |
| ScheduleDelayedAction(System::Clock::Seconds32(mPeriodicQueryTimeInterval), PeriodicQueryTimerHandler, this); |
| } |
| |
| void DefaultOTARequestorDriver::StopPeriodicQueryTimer() |
| { |
| ChipLogProgress(SoftwareUpdate, "Stopping the Periodic Query timer"); |
| CancelDelayedAction(PeriodicQueryTimerHandler, this); |
| } |
| |
| void DefaultOTARequestorDriver::RekickPeriodicQueryTimer() |
| { |
| ChipLogProgress(SoftwareUpdate, "Rekicking the Periodic Query timer"); |
| StopPeriodicQueryTimer(); |
| StartPeriodicQueryTimer(); |
| } |
| |
| void DefaultOTARequestorDriver::WatchdogTimerHandler(System::Layer * systemLayer, void * appState) |
| { |
| DefaultOTARequestorDriver * driver = ToDriver(appState); |
| |
| ChipLogError(SoftwareUpdate, "Watchdog timer detects state stuck at %u. Cancelling download and resetting state.", |
| to_underlying(driver->mRequestor->GetCurrentUpdateState())); |
| |
| // Something went wrong and OTA requestor is stuck in a non-idle state for too long. |
| // Let's just cancel download, reset state, and re-start periodic query timer. |
| driver->UpdateDiscontinued(); |
| driver->mRequestor->CancelImageUpdate(); |
| driver->StartPeriodicQueryTimer(); |
| } |
| |
| void DefaultOTARequestorDriver::StartWatchdogTimer() |
| { |
| ChipLogProgress(SoftwareUpdate, "Starting the watchdog timer, timeout: %u seconds", (unsigned int) mWatchdogTimeInterval); |
| ScheduleDelayedAction(System::Clock::Seconds32(mWatchdogTimeInterval), WatchdogTimerHandler, this); |
| } |
| |
| void DefaultOTARequestorDriver::StopWatchdogTimer() |
| { |
| ChipLogProgress(SoftwareUpdate, "Stopping the watchdog timer"); |
| CancelDelayedAction(WatchdogTimerHandler, this); |
| } |
| |
| void DefaultOTARequestorDriver::StartSelectedTimer(SelectedTimer timer) |
| { |
| switch (timer) |
| { |
| case SelectedTimer::kPeriodicQueryTimer: |
| StopWatchdogTimer(); |
| StartPeriodicQueryTimer(); |
| break; |
| case SelectedTimer::kWatchdogTimer: |
| StopPeriodicQueryTimer(); |
| StartWatchdogTimer(); |
| break; |
| } |
| } |
| |
| /** |
| * Returns the next available Provider location. The algorithm is to simply loop through the list of DefaultOtaProviders as a |
| * circular list and return the next value (based on the last used provider). If the list of DefaultOtaProviders is empty, FALSE is |
| * returned. |
| */ |
| bool DefaultOTARequestorDriver::GetNextProviderLocation(ProviderLocationType & providerLocation, bool & listExhausted) |
| { |
| Optional<ProviderLocationType> lastUsedProvider; |
| mRequestor->GetProviderLocation(lastUsedProvider); |
| mProviderRetryCount = 0; // Reset provider retry count |
| listExhausted = false; |
| |
| // Iterate through the default providers list and find the last used provider. If found, return the provider after it |
| auto iterator = mRequestor->GetDefaultOTAProviderListIterator(); |
| while (lastUsedProvider.HasValue() && iterator.Next()) |
| { |
| if (ProviderLocationsEqual(iterator.GetValue(), lastUsedProvider.Value())) |
| { |
| if (iterator.Next()) |
| { |
| providerLocation = iterator.GetValue(); |
| return true; |
| } |
| } |
| } |
| |
| // If no suitable candidate found, return the first element of the default providers list or an error |
| iterator = mRequestor->GetDefaultOTAProviderListIterator(); |
| if (iterator.Next()) |
| { |
| providerLocation = iterator.GetValue(); |
| listExhausted = true; |
| return true; |
| } |
| |
| ChipLogError(SoftwareUpdate, "No suitable OTA Provider candidate found"); |
| return false; |
| } |
| |
| CHIP_ERROR DefaultOTARequestorDriver::ScheduleQueryRetry(bool trySameProvider, System::Clock::Seconds32 delay) |
| { |
| CHIP_ERROR status = CHIP_NO_ERROR; |
| |
| if (trySameProvider == false) |
| { |
| VerifyOrDie(mRequestor != nullptr); |
| |
| ProviderLocationType providerLocation; |
| bool listExhausted = false; |
| |
| // Note that the "listExhausted" being set to TRUE, implies that the entire list of |
| // defaultOTAProviders has been traversed. On bootup, the last provider is reset |
| // which ensures that every QueryImage call will ensure that the list is traversed from |
| // start to end, until an OTA is successfully completed. |
| if ((GetNextProviderLocation(providerLocation, listExhausted) != true) || (listExhausted == true)) |
| { |
| status = CHIP_ERROR_PROVIDER_LIST_EXHAUSTED; |
| } |
| else |
| { |
| mRequestor->SetCurrentProviderLocation(providerLocation); |
| } |
| } |
| |
| if (mProviderRetryCount > kMaxBusyProviderRetryCount) |
| { |
| ChipLogProgress(SoftwareUpdate, "Max retry of %u exceeded. Will not retry", kMaxBusyProviderRetryCount); |
| status = CHIP_ERROR_MAX_RETRY_EXCEEDED; |
| } |
| |
| if (status == CHIP_NO_ERROR) |
| { |
| ChipLogProgress(SoftwareUpdate, "Scheduling a retry"); |
| ScheduleDelayedAction(delay, StartDelayTimerHandler, this); |
| } |
| |
| return status; |
| } |
| |
| } // namespace DeviceLayer |
| } // namespace chip |