mirror of
https://github.com/espressif/esp-matter.git
synced 2026-04-27 19:13:13 +00:00
ota-provider: Add DCL OTA Provider support
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
if (CONFIG_ESP_MATTER_OTA_PROVIDER_ENABLED)
|
||||
set(srcs "src/esp_matter_ota_bdx_sender.cpp"
|
||||
"src/esp_matter_ota_candidates.cpp"
|
||||
"src/esp_matter_ota_http_downloader.cpp"
|
||||
"src/esp_matter_ota_provider.cpp")
|
||||
|
||||
set(include_dirs "include")
|
||||
|
||||
set(priv_include_dirs "private_include")
|
||||
endif()
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
INCLUDE_DIRS "${include_dirs}"
|
||||
PRIV_INCLUDE_DIRS "${priv_include_dirs}"
|
||||
REQUIRES esp_matter esp_http_client json_parser)
|
||||
@@ -0,0 +1,52 @@
|
||||
menu "ESP Matter OTA Provider"
|
||||
|
||||
config ESP_MATTER_OTA_PROVIDER_ENABLED
|
||||
bool "Enable OTA provider"
|
||||
default n
|
||||
help
|
||||
Enable OTA provider. The OTA Provider will fetch the OTA candidates from DCL MainNet or TestNet
|
||||
and establish https connection with the ImageURI. Pass the download data to the Requestor with BDX
|
||||
Protocol.
|
||||
|
||||
choice ESP_MATTER_OTA_PROVIDER_DCL_OPTION
|
||||
prompt "OTA Provider DCL options"
|
||||
depends on ESP_MATTER_OTA_PROVIDER_ENABLED
|
||||
default ESP_MATTER_OTA_PROVIDER_DCL_MAINNET
|
||||
help
|
||||
The DCL used by the OTA Provider to fetch the OTA candidates.
|
||||
|
||||
config ESP_MATTER_OTA_PROVIDER_DCL_MAINNET
|
||||
bool "DCL - MainNet"
|
||||
help
|
||||
MainNet DCL, the REST URL for it is 'https://on.dcl.csa-iot.org/'
|
||||
|
||||
config ESP_MATTER_OTA_PROVIDER_DCL_TESTNET
|
||||
bool "DCL - TestNet"
|
||||
help
|
||||
TestNet DCL, the REST URL for it is 'https://on.test-net.dcl.csa-iot.org/'
|
||||
|
||||
endchoice
|
||||
|
||||
config ESP_MATTER_MAX_OTA_CANDIDATES_COUNT
|
||||
int "OTA Provider Max Candidates Count"
|
||||
depends on ESP_MATTER_OTA_PROVIDER_ENABLED
|
||||
default 8
|
||||
help
|
||||
This value indicates the maximum count of the OTA candidates cache.
|
||||
|
||||
config ESP_MATTER_OTA_CANDIDATES_UPDATE_PERIODICALLY
|
||||
bool "Update OTA Candidates Periodically"
|
||||
depends on ESP_MATTER_OTA_PROVIDER_ENABLED
|
||||
default true
|
||||
help
|
||||
Update the OTA candidates cache periodically.
|
||||
|
||||
config ESP_MATTER_OTA_CANDIDATES_UPDATE_PERIOD
|
||||
int "OTA Candidates Update Period (hours)"
|
||||
depends on ESP_MATTER_OTA_CANDIDATES_UPDATE_PERIODICALLY
|
||||
range 1 480
|
||||
default 36
|
||||
help
|
||||
OTA Candidates Update Period in Hours
|
||||
|
||||
endmenu
|
||||
@@ -0,0 +1,21 @@
|
||||
## ESP-Matter OTA Provider
|
||||
|
||||
The OTA Provider will maintain a cache array of OTA candidates, which is used to store previous results of QueryImage command.
|
||||
|
||||
1. After receiving the QueryImage command from the OTA Requestor, the OTA Provider will handle the command asynchronously.
|
||||
|
||||
a. If there is an existing backend command processing, the OTA provider will reply a response with Busy status.
|
||||
|
||||
2. The OTA Provider will look up the OTA candidates cache array to find whether there is an available update for the specific VendorID and ProductID in the command data.
|
||||
|
||||
a. If there is already a candidate record for the specific VendorID and ProductID with valid SoftwareVersion, the OTA Provider will reply a UpdateAvailable reponse and start BDXTransfer.
|
||||
|
||||
b. If there is no record for the specific VendorID, ProductID, and SoftwareVersion, the OTA Provider will try to fetch the candidate from the MainNet or TestNet DCL (Distributed Compliance Ledger).
|
||||
b1. If there is an error during candidate fetching, the OTA provider will reply a response with NotAvailable status.
|
||||
b2. If finishing candidate fetching, the OTA provider will reply a response with UpdateAvailable status and start BDXTransfer.
|
||||
|
||||
3. When the BDXTransfer of the OTA Provider receives a BDXInit message, it will establish an HTTP(S) connection to the URL of the OTA candidate and start downloading the image.
|
||||
|
||||
4. When the BDXTransfer of the OTA Provider receives a QueryBlock message, it will read the HTTP response for the HTTP(S) connection, prepare a Block message, and send it to the Requestor.\
|
||||
|
||||
Note: For the first QueryBlock message, the OTA Provider will verify the header of the image from the HTTP response.
|
||||
@@ -0,0 +1,3 @@
|
||||
## IDF Component Manager Manifest File
|
||||
dependencies:
|
||||
espressif/json_parser: "~1.0.0"
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2023 Espressif Systems (Shanghai) PTE LTD
|
||||
//
|
||||
// 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 <esp_err.h>
|
||||
#include <esp_http_client.h>
|
||||
#include <protocols/bdx/BdxTransferSession.h>
|
||||
#include <protocols/bdx/TransferFacilitator.h>
|
||||
|
||||
#define OTA_URL_MAX_LEN 256
|
||||
|
||||
namespace esp_matter {
|
||||
namespace ota_provider {
|
||||
|
||||
class OtaBdxSender : public chip::bdx::Responder {
|
||||
public:
|
||||
enum BdxSenderErr {
|
||||
kErrBdxSenderNone = 0,
|
||||
kErrBdxSenderStatusReceived,
|
||||
kErrBdxSenderInternal,
|
||||
kErrBdxSenderTimeout,
|
||||
};
|
||||
|
||||
OtaBdxSender()
|
||||
{
|
||||
memset(mOtaImageUrl, 0, sizeof(mOtaImageUrl));
|
||||
mOtaImageSize = 0;
|
||||
}
|
||||
|
||||
// Initializes BDX transfer-related metadata. Should always be called first.
|
||||
esp_err_t InitializeTransfer(chip::FabricIndex fabricIndex, chip::NodeId nodeId);
|
||||
|
||||
uint16_t GetTransferBlockSize(void);
|
||||
|
||||
uint64_t GetTransferLength(void);
|
||||
|
||||
void SetOtaImageUrl(const char *otaImageUrl)
|
||||
{
|
||||
strncpy(mOtaImageUrl, otaImageUrl, strnlen(otaImageUrl, OTA_URL_MAX_LEN));
|
||||
}
|
||||
|
||||
const char *GetOtaImageUrl() const { return mOtaImageUrl; }
|
||||
|
||||
private:
|
||||
void HandleTransferSessionOutput(chip::bdx::TransferSession::OutputEvent &event) override;
|
||||
|
||||
esp_err_t ParseOtaImageHeader(const uint8_t *header_buf, size_t header_buf_size);
|
||||
|
||||
void Reset();
|
||||
|
||||
uint64_t mNumBytesSent = 0;
|
||||
|
||||
bool mInitialized = false;
|
||||
|
||||
chip::Optional<chip::FabricIndex> mFabricIndex;
|
||||
chip::Optional<chip::NodeId> mNodeId;
|
||||
|
||||
char mOtaImageUrl[OTA_URL_MAX_LEN];
|
||||
uint64_t mOtaImageSize;
|
||||
esp_http_client_handle_t mHttpDownloader;
|
||||
};
|
||||
|
||||
} // namespace ota_provider
|
||||
} // namespace esp_matter
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright 2023 Espressif Systems (Shanghai) PTE LTD
|
||||
//
|
||||
// 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 <access/SubjectDescriptor.h>
|
||||
#include <app-common/zap-generated/cluster-objects.h>
|
||||
#include <app/CommandHandler.h>
|
||||
#include <app/clusters/ota-provider/OTAProviderUserConsentDelegate.h>
|
||||
#include <app/clusters/ota-provider/ota-provider-delegate.h>
|
||||
#include <cstdint>
|
||||
#include <esp_matter_ota_bdx_sender.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <lib/core/OTAImageHeader.h>
|
||||
|
||||
#define SOFTWARE_VERSION_STR_MAX_LEN 64
|
||||
|
||||
namespace esp_matter {
|
||||
namespace ota_provider {
|
||||
|
||||
class EspOtaProvider : public chip::app::Clusters::OTAProviderDelegate {
|
||||
public:
|
||||
using OTAQueryStatus = chip::app::Clusters::OtaSoftwareUpdateProvider::OTAQueryStatus;
|
||||
using OTAApplyUpdateAction = chip::app::Clusters::OtaSoftwareUpdateProvider::OTAApplyUpdateAction;
|
||||
|
||||
static constexpr size_t kUriMaxLen = 256;
|
||||
static constexpr uint8_t kUpdateTokenLen = 32;
|
||||
static constexpr uint8_t kUpdateTokenStrLen = kUpdateTokenLen * 2 + 1;
|
||||
struct EspOtaRequestorEntry {
|
||||
chip::ScopedNodeId mNodeId;
|
||||
bool mOtaAllowed;
|
||||
bool mOtaAllowedOnce;
|
||||
bool mHasNewVersion;
|
||||
uint8_t mUpdateToken[kUpdateTokenLen];
|
||||
char mImageUri[kUriMaxLen];
|
||||
char mOtaImageUrl[OTA_URL_MAX_LEN];
|
||||
size_t mOtaImageSize;
|
||||
uint32_t mSoftwareVersion;
|
||||
char mSoftwareVersionString[SOFTWARE_VERSION_STR_MAX_LEN];
|
||||
EspOtaRequestorEntry *mNext;
|
||||
};
|
||||
|
||||
// OTAProviderDelegate Implementation
|
||||
void HandleQueryImage(chip::app::CommandHandler *commandObj, const chip::app::ConcreteCommandPath &commandPath,
|
||||
const chip::app::Clusters::OtaSoftwareUpdateProvider::Commands::QueryImage::DecodableType
|
||||
&commandData) override;
|
||||
|
||||
void HandleApplyUpdateRequest(
|
||||
chip::app::CommandHandler *commandObj, const chip::app::ConcreteCommandPath &commandPath,
|
||||
const chip::app::Clusters::OtaSoftwareUpdateProvider::Commands::ApplyUpdateRequest::DecodableType &commandData)
|
||||
override;
|
||||
|
||||
void HandleNotifyUpdateApplied(
|
||||
chip::app::CommandHandler *commandObj, const chip::app::ConcreteCommandPath &commandPath,
|
||||
const chip::app::Clusters::OtaSoftwareUpdateProvider::Commands::NotifyUpdateApplied::DecodableType &commandData)
|
||||
override;
|
||||
|
||||
// OTAProviderImpl public APIs
|
||||
static EspOtaProvider &GetInstance()
|
||||
{
|
||||
static EspOtaProvider instance;
|
||||
return instance;
|
||||
}
|
||||
void Init(bool otaAllowedDefault);
|
||||
void SetApplyUpdateAction(OTAApplyUpdateAction action) { mUpdateAction = action; }
|
||||
void SetDelayedQueryActionTimeSec(uint32_t time) { mDelayedQueryActionTimeSec = time; }
|
||||
void SetDelayedApplyActionTimeSec(uint32_t time) { mDelayedApplyActionTimeSec = time; }
|
||||
void SetPollInterval(uint32_t interval) { mPollInterval = (interval != 0) ? interval : mPollInterval; }
|
||||
|
||||
static void FetchImageDoneCallback(OTAQueryStatus status, const char *imageUrl, size_t imageSize,
|
||||
uint32_t softwareVersion, const char *softwareVersionStr, void *arg);
|
||||
|
||||
// When the OTA Provider receives a QueryImage command from an OTA Requestor and there is no existing entry for the
|
||||
// Requestor node, the Provider will create an OTA Requestor Entry for the requestor, and set the entry's
|
||||
// mOtaAllowed to mOtaAllowedDefault.
|
||||
void SetOtaAllowedDefault(bool otaAllowed) { mOtaAllowedDefault = otaAllowed; }
|
||||
// When there is a Requestor entry for the nodeId, we can call the EnableOtaForNode/DisableOtaForNode to make the
|
||||
// provider allow whether the requestor proceed the OTA process.
|
||||
esp_err_t EnableOtaForNode(const chip::ScopedNodeId &nodeId, bool forOnlyOnce);
|
||||
esp_err_t DisableOtaForNode(const chip::ScopedNodeId &nodeId);
|
||||
// This should be called when the OTA Provider is notified that one node is removed from the Fabric.
|
||||
esp_err_t RemoveOtaRequestorEntry(const chip::ScopedNodeId &nodeId);
|
||||
EspOtaRequestorEntry *FindOtaRequestorEntry(const chip::ScopedNodeId &nodeId);
|
||||
|
||||
private:
|
||||
EspOtaProvider() {}
|
||||
~EspOtaProvider() {}
|
||||
|
||||
void SendQueryImageResponse(OTAQueryStatus status);
|
||||
|
||||
esp_err_t CreateOtaRequestorEntry(const chip::ScopedNodeId &nodeId);
|
||||
|
||||
OtaBdxSender mOtaBdxSender;
|
||||
uint32_t mDelayedQueryActionTimeSec;
|
||||
OTAApplyUpdateAction mUpdateAction;
|
||||
uint32_t mDelayedApplyActionTimeSec;
|
||||
uint32_t mPollInterval;
|
||||
bool mOtaAllowedDefault;
|
||||
EspOtaRequestorEntry *mOtaRequestorList;
|
||||
|
||||
// Use async command handler for QueryImage command
|
||||
chip::app::CommandHandler::Handle mAsyncCommandHandle;
|
||||
chip::app::ConcreteCommandPath mPath = chip::app::ConcreteCommandPath(0, 0, 0);
|
||||
chip::Access::SubjectDescriptor mSubjectDescriptor;
|
||||
chip::ScopedNodeId mPeerNodeId;
|
||||
};
|
||||
} // namespace ota_provider
|
||||
} // namespace esp_matter
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2023 Espressif Systems (Shanghai) PTE LTD
|
||||
//
|
||||
// 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 <esp_err.h>
|
||||
#include <esp_matter_ota_provider.h>
|
||||
|
||||
namespace esp_matter {
|
||||
namespace ota_provider {
|
||||
|
||||
typedef struct {
|
||||
uint16_t vendor_id;
|
||||
uint16_t product_id;
|
||||
uint32_t software_version;
|
||||
char software_version_str[SOFTWARE_VERSION_STR_MAX_LEN];
|
||||
uint16_t cd_version_number;
|
||||
uint32_t min_applicable_software_version;
|
||||
uint32_t max_applicable_software_version;
|
||||
char ota_url[OTA_URL_MAX_LEN];
|
||||
uint32_t ota_file_size;
|
||||
uint32_t lifetime;
|
||||
} model_version_t;
|
||||
|
||||
typedef void (*fetch_ota_image_done_callback_t)(EspOtaProvider::OTAQueryStatus status, const char *imageUrl,
|
||||
size_t imageSize, uint32_t softwareVersion,
|
||||
const char *softwareVersionStr, void *ctx);
|
||||
|
||||
esp_err_t fetch_ota_candidate(const uint16_t vendor_id, const uint16_t product_id, const uint32_t software_version,
|
||||
fetch_ota_image_done_callback_t callback, void *callback_args);
|
||||
|
||||
esp_err_t init_ota_candidates();
|
||||
|
||||
} // namespace ota_provider
|
||||
} // namespace esp_matter
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2023 Espressif Systems (Shanghai) PTE LTD
|
||||
//
|
||||
// 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 <esp_err.h>
|
||||
#include <esp_http_client.h>
|
||||
|
||||
namespace esp_matter {
|
||||
namespace ota_provider {
|
||||
|
||||
struct ota_image_header_prefix {
|
||||
uint32_t file_identifier;
|
||||
uint64_t total_size;
|
||||
uint32_t header_size;
|
||||
} __attribute__((packed));
|
||||
|
||||
typedef struct ota_image_header_prefix ota_image_header_prefix_t;
|
||||
|
||||
constexpr uint32_t k_ota_image_file_identifier = 0x1BEEF11E;
|
||||
|
||||
int http_downloader_read(esp_http_client_handle_t http_client, char *buf, size_t size);
|
||||
|
||||
void http_downloader_abort(esp_http_client_handle_t http_client);
|
||||
|
||||
esp_err_t http_downloader_start(esp_http_client_config_t *config, esp_http_client_handle_t *http_client);
|
||||
|
||||
} // namespace ota_provider
|
||||
} // namespace esp_matter
|
||||
@@ -0,0 +1,232 @@
|
||||
// Copyright 2023 Espressif Systems (Shanghai) PTE LTD
|
||||
//
|
||||
// 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 <esp_crt_bundle.h>
|
||||
#include <esp_log.h>
|
||||
#include <esp_matter_ota_bdx_sender.h>
|
||||
#include <esp_matter_ota_http_downloader.h>
|
||||
|
||||
#include <lib/core/CHIPError.h>
|
||||
#include <lib/support/BitFlags.h>
|
||||
#include <lib/support/CHIPMemString.h>
|
||||
#include <messaging/ExchangeContext.h>
|
||||
#include <messaging/Flags.h>
|
||||
#include <protocols/bdx/BdxTransferSession.h>
|
||||
|
||||
static constexpr char TAG[] = "ota_provider";
|
||||
|
||||
using chip::bdx::StatusCode;
|
||||
using chip::bdx::TransferControlFlags;
|
||||
using chip::bdx::TransferSession;
|
||||
|
||||
namespace esp_matter {
|
||||
namespace ota_provider {
|
||||
|
||||
esp_err_t OtaBdxSender::InitializeTransfer(chip::FabricIndex fabricIndex, chip::NodeId nodeId)
|
||||
{
|
||||
if (mInitialized) {
|
||||
if ((mFabricIndex.HasValue() && mFabricIndex.Value() == fabricIndex) &&
|
||||
(mNodeId.HasValue() && mNodeId.Value() == nodeId)) {
|
||||
Reset();
|
||||
} else if ((mFabricIndex.HasValue() && mFabricIndex.Value() != fabricIndex) ||
|
||||
(mNodeId.HasValue() && mNodeId.Value() != nodeId)) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
} else {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
}
|
||||
mFabricIndex.SetValue(fabricIndex);
|
||||
mNodeId.SetValue(nodeId);
|
||||
mInitialized = true;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t OtaBdxSender::ParseOtaImageHeader(const uint8_t *header_buf, size_t header_buf_size)
|
||||
{
|
||||
if (header_buf_size < sizeof(ota_image_header_prefix_t)) {
|
||||
ESP_LOGE(TAG, "Invalid header buffer size");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
ota_image_header_prefix_t *prefix = (ota_image_header_prefix_t *)header_buf;
|
||||
if (prefix->file_identifier != k_ota_image_file_identifier) {
|
||||
ESP_LOGE(TAG, "Invalid OTA image file identifier");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (prefix->total_size <= prefix->header_size + sizeof(ota_image_header_prefix_t)) {
|
||||
ESP_LOGE(TAG, "Invalid payload size");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
mOtaImageSize = prefix->total_size;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void OtaBdxSender::HandleTransferSessionOutput(TransferSession::OutputEvent &event)
|
||||
{
|
||||
CHIP_ERROR err = CHIP_NO_ERROR;
|
||||
|
||||
if (event.EventType != TransferSession::OutputEventType::kNone) {
|
||||
ESP_LOGD(TAG, "OutputEvent type: %s", event.ToString(event.EventType));
|
||||
}
|
||||
switch (event.EventType) {
|
||||
case TransferSession::OutputEventType::kNone:
|
||||
break;
|
||||
case TransferSession::OutputEventType::kMsgToSend: {
|
||||
chip::Messaging::SendFlags sendFlags;
|
||||
if (!event.msgTypeData.HasMessageType(chip::Protocols::SecureChannel::MsgType::StatusReport)) {
|
||||
sendFlags.Set(chip::Messaging::SendMessageFlags::kExpectResponse);
|
||||
}
|
||||
if (mExchangeCtx == nullptr) {
|
||||
ESP_LOGE(TAG, "mExchangeCtx cannot be NULL");
|
||||
return;
|
||||
}
|
||||
err = mExchangeCtx->SendMessage(event.msgTypeData.ProtocolId, event.msgTypeData.MessageType,
|
||||
std::move(event.MsgData), sendFlags);
|
||||
if (err == CHIP_NO_ERROR) {
|
||||
if (!sendFlags.Has(chip::Messaging::SendMessageFlags::kExpectResponse)) {
|
||||
// After sending the StatusReport, exchange context gets closed so, set mExchangeCtx to null
|
||||
mExchangeCtx = nullptr;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGE(TAG, "SendMessage failed: %" CHIP_ERROR_FORMAT, err.Format());
|
||||
Reset();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TransferSession::OutputEventType::kInitReceived: {
|
||||
// TransferSession will automatically reject a transfer if there are no
|
||||
// common supported control modes. It will also default to the smaller
|
||||
// block size.
|
||||
TransferSession::TransferAcceptData acceptData;
|
||||
acceptData.ControlMode = TransferControlFlags::kReceiverDrive; // OTA must use receiver drive
|
||||
acceptData.MaxBlockSize = mTransfer.GetTransferBlockSize();
|
||||
acceptData.StartOffset = mTransfer.GetStartOffset();
|
||||
acceptData.Length = mTransfer.GetTransferLength();
|
||||
if (mTransfer.AcceptTransfer(acceptData) != CHIP_NO_ERROR) {
|
||||
ESP_LOGE(TAG, "AcceptTransfter failed error:%" CHIP_ERROR_FORMAT, err.Format());
|
||||
return;
|
||||
}
|
||||
// Establish http connection
|
||||
esp_http_client_config_t config = {
|
||||
.url = mOtaImageUrl,
|
||||
.event_handler = NULL,
|
||||
.transport_type = HTTP_TRANSPORT_OVER_SSL,
|
||||
.skip_cert_common_name_check = false,
|
||||
.crt_bundle_attach = esp_crt_bundle_attach,
|
||||
.keep_alive_enable = true,
|
||||
};
|
||||
if (http_downloader_start(&config, &mHttpDownloader) != ESP_OK) {
|
||||
mTransfer.AbortTransfer(StatusCode::kUnknown);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TransferSession::OutputEventType::kQueryReceived: {
|
||||
TransferSession::BlockData blockData;
|
||||
uint16_t bytesToRead = mTransfer.GetTransferBlockSize();
|
||||
|
||||
chip::System::PacketBufferHandle blockBuf = chip::System::PacketBufferHandle::New(bytesToRead);
|
||||
if (blockBuf.IsNull()) {
|
||||
mTransfer.AbortTransfer(StatusCode::kUnknown);
|
||||
return;
|
||||
}
|
||||
// Read http response
|
||||
int bytes_read = http_downloader_read(mHttpDownloader, reinterpret_cast<char *>(blockBuf->Start()), bytesToRead);
|
||||
if (bytes_read < 0) {
|
||||
ESP_LOGE(TAG, "http_downloader_read failed");
|
||||
mTransfer.AbortTransfer(StatusCode::kUnknown);
|
||||
break;
|
||||
}
|
||||
if (mOtaImageSize == 0 && mNumBytesSent == 0) {
|
||||
if (ParseOtaImageHeader(blockBuf->Start(), static_cast<size_t>(bytes_read)) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to Parse OTA image header");
|
||||
mTransfer.AbortTransfer(StatusCode::kUnknown);
|
||||
break;
|
||||
}
|
||||
}
|
||||
blockData.Data = blockBuf->Start();
|
||||
blockData.Length =
|
||||
static_cast<size_t>(std::min(static_cast<uint64_t>(bytes_read), (mOtaImageSize - mNumBytesSent)));
|
||||
blockData.IsEof = (blockData.Length < bytesToRead) ||
|
||||
(mNumBytesSent + static_cast<uint64_t>(blockData.Length) == mOtaImageSize);
|
||||
mNumBytesSent = static_cast<uint64_t>(mNumBytesSent + blockData.Length);
|
||||
|
||||
if (CHIP_NO_ERROR != mTransfer.PrepareBlock(blockData)) {
|
||||
ESP_LOGE(TAG, "PrepareBlock failed: %" CHIP_ERROR_FORMAT, err.Format());
|
||||
mTransfer.AbortTransfer(StatusCode::kUnknown);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TransferSession::OutputEventType::kAckReceived:
|
||||
break;
|
||||
case TransferSession::OutputEventType::kAckEOFReceived: {
|
||||
ESP_LOGD(TAG, "Transfer completed, got AckEOF");
|
||||
mStopPolling = true; // Stop polling the TransferSession only after receiving BlockAckEOF
|
||||
Reset();
|
||||
break;
|
||||
}
|
||||
case TransferSession::OutputEventType::kStatusReceived: {
|
||||
ESP_LOGE(TAG, "Got StatusReport %x", static_cast<uint16_t>(event.statusData.statusCode));
|
||||
http_downloader_abort(mHttpDownloader);
|
||||
Reset();
|
||||
break;
|
||||
}
|
||||
case TransferSession::OutputEventType::kInternalError: {
|
||||
ESP_LOGE(TAG, "InternalError");
|
||||
http_downloader_abort(mHttpDownloader);
|
||||
Reset();
|
||||
break;
|
||||
}
|
||||
case TransferSession::OutputEventType::kTransferTimeout: {
|
||||
ESP_LOGE(TAG, "TransferTimeout");
|
||||
http_downloader_abort(mHttpDownloader);
|
||||
Reset();
|
||||
break;
|
||||
}
|
||||
case TransferSession::OutputEventType::kAcceptReceived:
|
||||
case TransferSession::OutputEventType::kBlockReceived:
|
||||
default:
|
||||
ESP_LOGE(TAG, "unsupported event type");
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
void OtaBdxSender::Reset()
|
||||
{
|
||||
mFabricIndex.ClearValue();
|
||||
mNodeId.ClearValue();
|
||||
mTransfer.Reset();
|
||||
if (mExchangeCtx != nullptr) {
|
||||
mExchangeCtx->Close();
|
||||
mExchangeCtx = nullptr;
|
||||
}
|
||||
|
||||
mInitialized = false;
|
||||
mNumBytesSent = 0;
|
||||
mOtaImageSize = 0;
|
||||
mHttpDownloader = nullptr;
|
||||
memset(mOtaImageUrl, 0, sizeof(mOtaImageUrl));
|
||||
}
|
||||
|
||||
uint16_t OtaBdxSender::GetTransferBlockSize(void)
|
||||
{
|
||||
return mTransfer.GetTransferBlockSize();
|
||||
}
|
||||
|
||||
uint64_t OtaBdxSender::GetTransferLength()
|
||||
{
|
||||
return mTransfer.GetTransferLength();
|
||||
}
|
||||
|
||||
} // namespace ota_provider
|
||||
} // namespace esp_matter
|
||||
@@ -0,0 +1,472 @@
|
||||
// Copyright 2023 Espressif Systems (Shanghai) PTE LTD
|
||||
//
|
||||
// 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 <algorithm>
|
||||
#include <esp_check.h>
|
||||
#include <esp_crt_bundle.h>
|
||||
#include <esp_err.h>
|
||||
#include <esp_http_client.h>
|
||||
#include <esp_log.h>
|
||||
#include <esp_matter_mem.h>
|
||||
#include <esp_matter_ota_candidates.h>
|
||||
#include <esp_matter_ota_provider.h>
|
||||
#include <esp_timer.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/queue.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
#include <functional>
|
||||
#include <json_parser.h>
|
||||
|
||||
#include <lib/support/ScopedBuffer.h>
|
||||
|
||||
#include <string.h>
|
||||
#include "core/DataModelTypes.h"
|
||||
|
||||
using chip::Platform::ScopedMemoryBufferWithSize;
|
||||
|
||||
namespace esp_matter {
|
||||
namespace ota_provider {
|
||||
|
||||
static constexpr char TAG[] = "ota_provider";
|
||||
#if CONFIG_ESP_MATTER_OTA_PROVIDER_DCL_MAINNET
|
||||
static constexpr char dcl_rest_url[] = "https://on.dcl.csa-iot.org/dcl/model/versions";
|
||||
#elif CONFIG_ESP_MATTER_OTA_PROVIDER_DCL_TESTNET
|
||||
static constexpr char dcl_rest_url[] = "https://on.test-net.dcl.csa-iot.org/dcl/model/versions";
|
||||
#endif
|
||||
static constexpr size_t max_ota_candidate_count = CONFIG_ESP_MATTER_MAX_OTA_CANDIDATES_COUNT;
|
||||
|
||||
static model_version_t *_ota_candidates_cache[max_ota_candidate_count];
|
||||
static QueueHandle_t _ota_candidate_task_queue = NULL;
|
||||
#ifdef CONFIG_ESP_MATTER_OTA_CANDIDATES_UPDATE_PERIODICALLY
|
||||
static esp_timer_handle_t _ota_candidates_update_timer = NULL;
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
uint16_t vendor_id;
|
||||
uint16_t product_id;
|
||||
uint32_t software_version;
|
||||
fetch_ota_image_done_callback_t callback;
|
||||
void *callback_args;
|
||||
} ota_candidate_fetch_action_t;
|
||||
|
||||
static bool _is_ota_candidate_valid(model_version_t *model, uint32_t current_software_version)
|
||||
{
|
||||
return model->software_version > current_software_version &&
|
||||
model->max_applicable_software_version >= current_software_version &&
|
||||
model->min_applicable_software_version <= current_software_version;
|
||||
}
|
||||
|
||||
static int _search_ota_candidate(uint16_t vendor_id, uint16_t product_id, uint32_t software_ver)
|
||||
{
|
||||
for (size_t index = 0; index < max_ota_candidate_count; ++index) {
|
||||
model_version_t *cur_model = _ota_candidates_cache[index];
|
||||
if (cur_model) {
|
||||
if (cur_model->vendor_id == vendor_id && cur_model->product_id == product_id) {
|
||||
if (_is_ota_candidate_valid(cur_model, software_ver)) {
|
||||
return index;
|
||||
} else {
|
||||
// This candidate is not valid, expire it.
|
||||
esp_matter_mem_free(cur_model);
|
||||
_ota_candidates_cache[index] = nullptr;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static size_t _find_empty_ota_candidates()
|
||||
{
|
||||
uint8_t oldest_candidate_lifetime = 0;
|
||||
size_t oldest_candidate_index = 0;
|
||||
for (size_t index = 0; index < max_ota_candidate_count; ++index) {
|
||||
if (!_ota_candidates_cache[index]) {
|
||||
return index;
|
||||
} else {
|
||||
if (oldest_candidate_lifetime < _ota_candidates_cache[index]->lifetime) {
|
||||
oldest_candidate_lifetime = _ota_candidates_cache[index]->lifetime;
|
||||
oldest_candidate_index = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
// expire the oldest candidate
|
||||
esp_matter_mem_free(_ota_candidates_cache[oldest_candidate_index]);
|
||||
_ota_candidates_cache[oldest_candidate_index] = nullptr;
|
||||
return oldest_candidate_index;
|
||||
}
|
||||
|
||||
static void _increase_ota_candidates_lifetime()
|
||||
{
|
||||
for (size_t index = 0; index < max_ota_candidate_count; ++index) {
|
||||
if (_ota_candidates_cache[index]) {
|
||||
_ota_candidates_cache[index]->lifetime++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static esp_err_t _query_software_version_array(const uint16_t vendor_id, const uint16_t product_id,
|
||||
uint32_t **software_version_array, size_t &software_version_count)
|
||||
{
|
||||
if (!software_version_array) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
esp_err_t ret = ESP_OK;
|
||||
int sw_ver_count = 0, sw_ver_index = 0, sw_ver_tmp;
|
||||
;
|
||||
char url[100];
|
||||
snprintf(url, sizeof(url), "%s/%d/%d", dcl_rest_url, vendor_id, product_id);
|
||||
esp_http_client_config_t config = {
|
||||
.url = url,
|
||||
.transport_type = HTTP_TRANSPORT_OVER_SSL,
|
||||
.buffer_size = 1024,
|
||||
.skip_cert_common_name_check = false,
|
||||
.crt_bundle_attach = esp_crt_bundle_attach,
|
||||
};
|
||||
esp_http_client_handle_t client = NULL;
|
||||
ScopedMemoryBufferWithSize<char> http_payload;
|
||||
int http_len, http_status_code;
|
||||
jparse_ctx_t jctx;
|
||||
|
||||
client = esp_http_client_init(&config);
|
||||
if (!client) {
|
||||
ESP_LOGE(TAG, "Failed to initialise HTTP Client.");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
ESP_GOTO_ON_ERROR(esp_http_client_set_header(client, "accept", "application/json"), cleanup, TAG,
|
||||
"Failed to set http header accept");
|
||||
ESP_GOTO_ON_ERROR(esp_http_client_set_method(client, HTTP_METHOD_GET), cleanup, TAG, "Failed to set http method");
|
||||
|
||||
// HTTP GET
|
||||
ESP_GOTO_ON_ERROR(esp_http_client_open(client, 0), cleanup, TAG, "Failed to open http connection");
|
||||
|
||||
// Read Response
|
||||
http_len = esp_http_client_fetch_headers(client);
|
||||
http_status_code = esp_http_client_get_status_code(client);
|
||||
http_payload.Calloc(1024);
|
||||
if ((http_len > 0) && (http_status_code == 200)) {
|
||||
ESP_GOTO_ON_FALSE(http_payload.Get(), ESP_ERR_NO_MEM, close, TAG, "Failed to alloc memory for http_payload");
|
||||
http_len = esp_http_client_read_response(client, http_payload.Get(), http_payload.AllocatedSize());
|
||||
http_payload[http_len] = '\0';
|
||||
} else {
|
||||
http_len = esp_http_client_read_response(client, http_payload.Get(), http_payload.AllocatedSize());
|
||||
http_payload[http_len] = '\0';
|
||||
ESP_LOGE(TAG, "Invalid response for %s", url);
|
||||
ESP_LOGE(TAG, "Status = %d, Data = %s", http_status_code, http_len > 0 ? http_payload.Get() : "None");
|
||||
ret = ESP_FAIL;
|
||||
goto close;
|
||||
}
|
||||
ESP_LOGD(TAG, "http_response:\n%s", http_payload.Get());
|
||||
|
||||
// Parse the response payload
|
||||
ESP_GOTO_ON_FALSE(json_parse_start(&jctx, http_payload.Get(), http_len) == 0, ESP_FAIL, close, TAG,
|
||||
"Failed to parse the http response json on json_parse_start");
|
||||
if (json_obj_get_object(&jctx, "modelVersions") == 0) {
|
||||
if (json_obj_get_array(&jctx, "softwareVersions", &sw_ver_count) == 0 && sw_ver_count > 0) {
|
||||
*software_version_array = (uint32_t *)esp_matter_mem_calloc(sw_ver_count, sizeof(uint32_t));
|
||||
if (*software_version_array) {
|
||||
software_version_count = sw_ver_count;
|
||||
for (sw_ver_index = 0; sw_ver_index < sw_ver_count; ++sw_ver_index) {
|
||||
if (json_arr_get_int(&jctx, sw_ver_index, &sw_ver_tmp) == 0) {
|
||||
(*software_version_array)[sw_ver_index] = sw_ver_tmp;
|
||||
} else {
|
||||
(*software_version_array)[sw_ver_index] = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ret = ESP_ERR_NO_MEM;
|
||||
}
|
||||
json_obj_leave_array(&jctx);
|
||||
} else {
|
||||
ret = ESP_FAIL;
|
||||
}
|
||||
json_obj_leave_object(&jctx);
|
||||
} else {
|
||||
ret = ESP_FAIL;
|
||||
}
|
||||
json_parse_end(&jctx);
|
||||
|
||||
close:
|
||||
esp_http_client_close(client);
|
||||
cleanup:
|
||||
esp_http_client_cleanup(client);
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
if (*software_version_array) {
|
||||
esp_matter_mem_free(*software_version_array);
|
||||
*software_version_array = nullptr;
|
||||
software_version_count = 0;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t _query_ota_candidate(model_version_t *model, uint32_t new_software_version,
|
||||
uint32_t current_software_version)
|
||||
{
|
||||
if (!model) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
esp_err_t ret = ESP_OK;
|
||||
char url[128];
|
||||
snprintf(url, sizeof(url), "%s/%d/%d/%ld", dcl_rest_url, model->vendor_id, model->product_id, new_software_version);
|
||||
esp_http_client_config_t config = {
|
||||
.url = url,
|
||||
.transport_type = HTTP_TRANSPORT_OVER_SSL,
|
||||
.buffer_size = 1024,
|
||||
.skip_cert_common_name_check = false,
|
||||
.crt_bundle_attach = esp_crt_bundle_attach,
|
||||
};
|
||||
esp_http_client_handle_t client = NULL;
|
||||
ScopedMemoryBufferWithSize<char> http_payload;
|
||||
int http_len, http_status_code;
|
||||
int max_applicable_software_version, min_applicable_software_version, cd_version_number, string_len;
|
||||
bool software_version_valid;
|
||||
jparse_ctx_t jctx;
|
||||
|
||||
client = esp_http_client_init(&config);
|
||||
ESP_RETURN_ON_FALSE(client, ESP_FAIL, TAG, "Failed to initialise HTTP Client.");
|
||||
ESP_GOTO_ON_ERROR(esp_http_client_set_header(client, "accept", "application/json"), cleanup, TAG,
|
||||
"Failed to set http header accept");
|
||||
ESP_GOTO_ON_ERROR(esp_http_client_set_method(client, HTTP_METHOD_GET), cleanup, TAG, "Failed to set http method");
|
||||
|
||||
// HTTP GET
|
||||
ESP_GOTO_ON_ERROR(esp_http_client_open(client, 0), cleanup, TAG, "Failed to open http connection");
|
||||
|
||||
// Read Response
|
||||
http_len = esp_http_client_fetch_headers(client);
|
||||
http_status_code = esp_http_client_get_status_code(client);
|
||||
http_payload.Calloc(1024);
|
||||
if ((http_len > 0) && (http_status_code == 200)) {
|
||||
ESP_GOTO_ON_FALSE(http_payload.Get(), ESP_ERR_NO_MEM, close, TAG, "Failed to alloc memory for http_payload");
|
||||
http_len = esp_http_client_read_response(client, http_payload.Get(), http_payload.AllocatedSize());
|
||||
http_payload[http_len] = '\0';
|
||||
} else {
|
||||
http_len = esp_http_client_read_response(client, http_payload.Get(), http_payload.AllocatedSize());
|
||||
http_payload[http_len] = '\0';
|
||||
ESP_LOGE(TAG, "Invalid response for %s", url);
|
||||
ESP_LOGE(TAG, "Status = %d, Data = %s", http_status_code, http_len > 0 ? http_payload.Get() : "None");
|
||||
ret = ESP_FAIL;
|
||||
goto close;
|
||||
}
|
||||
ESP_LOGD(TAG, "http_response:\n%s", http_payload.Get());
|
||||
|
||||
ESP_GOTO_ON_FALSE(json_parse_start(&jctx, http_payload.Get(), http_len) == 0, ESP_FAIL, close, TAG,
|
||||
"Failed to parse the http response json on json_parse_start");
|
||||
if (json_obj_get_object(&jctx, "modelVersion") == 0) {
|
||||
if (json_obj_get_int(&jctx, "maxApplicableSoftwareVersion", &max_applicable_software_version) == 0 &&
|
||||
json_obj_get_int(&jctx, "minApplicableSoftwareVersion", &min_applicable_software_version) == 0 &&
|
||||
json_obj_get_bool(&jctx, "softwareVersionValid", &software_version_valid) == 0 && software_version_valid &&
|
||||
max_applicable_software_version >= current_software_version &&
|
||||
min_applicable_software_version <= current_software_version &&
|
||||
new_software_version > current_software_version) {
|
||||
model->max_applicable_software_version = max_applicable_software_version;
|
||||
model->min_applicable_software_version = min_applicable_software_version;
|
||||
model->software_version = new_software_version;
|
||||
if (json_obj_get_int(&jctx, "cdVersionNumber", &cd_version_number) == 0) {
|
||||
model->cd_version_number = cd_version_number;
|
||||
}
|
||||
if (json_obj_get_strlen(&jctx, "softwareVersionString", &string_len) == 0 &&
|
||||
json_obj_get_string(&jctx, "softwareVersionString", model->software_version_str,
|
||||
sizeof(model->software_version_str)) == 0) {
|
||||
string_len = string_len < sizeof(model->software_version_str) - 1
|
||||
? string_len
|
||||
: sizeof(model->software_version_str) - 1;
|
||||
model->software_version_str[string_len] = 0;
|
||||
}
|
||||
if (json_obj_get_strlen(&jctx, "otaUrl", &string_len) == 0 &&
|
||||
json_obj_get_string(&jctx, "otaUrl", model->ota_url, sizeof(model->ota_url)) == 0) {
|
||||
model->ota_url[string_len] = 0;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGI(TAG, "This result is not valid for software version %ld, skip it", current_software_version);
|
||||
ret = ESP_ERR_NOT_FINISHED;
|
||||
}
|
||||
json_obj_leave_object(&jctx);
|
||||
} else {
|
||||
ret = ESP_FAIL;
|
||||
}
|
||||
json_parse_end(&jctx);
|
||||
|
||||
close:
|
||||
esp_http_client_close(client);
|
||||
cleanup:
|
||||
esp_http_client_cleanup(client);
|
||||
return ret;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_ESP_MATTER_OTA_CANDIDATES_UPDATE_PERIODICALLY
|
||||
static void _update_all_ota_candidates_cache()
|
||||
{
|
||||
for (size_t index = 0; index < max_ota_candidate_count; ++index) {
|
||||
model_version_t *candidate = _ota_candidates_cache[index];
|
||||
if (candidate) {
|
||||
uint32_t *software_version_array;
|
||||
size_t software_version_count;
|
||||
esp_err_t err = _query_software_version_array(candidate->vendor_id, candidate->product_id,
|
||||
&software_version_array, software_version_count);
|
||||
if (err == ESP_OK && software_version_array && software_version_count > 0) {
|
||||
std::sort(&software_version_array[0], &software_version_array[software_version_count - 1],
|
||||
std::greater<uint32_t>());
|
||||
for (size_t index = 0;
|
||||
index < software_version_count && software_version_array[index] > candidate->software_version;
|
||||
++index) {
|
||||
err = _query_ota_candidate(candidate, software_version_array[index], candidate->software_version);
|
||||
if (err == ESP_OK) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
esp_matter_mem_free(software_version_array);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void _ota_candidates_periodic_update_handler(void *arg)
|
||||
{
|
||||
ota_candidate_fetch_action_t action;
|
||||
action.vendor_id = chip::kMaxVendorId;
|
||||
if (xQueueSend(_ota_candidate_task_queue, &action, portMAX_DELAY) != pdTRUE) {
|
||||
ESP_LOGE(TAG, "Failed send search ota candidate action");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static void _ota_candidate_fetch_handler(ota_candidate_fetch_action_t &action)
|
||||
{
|
||||
model_version_t *candidate = nullptr;
|
||||
assert(action.callback);
|
||||
int candate_index = _search_ota_candidate(action.vendor_id, action.product_id, action.software_version);
|
||||
if (candate_index >= 0 && candate_index < max_ota_candidate_count && _ota_candidates_cache[candate_index]) {
|
||||
candidate = _ota_candidates_cache[candate_index];
|
||||
action.callback(EspOtaProvider::OTAQueryStatus::kUpdateAvailable, candidate->ota_url, candidate->ota_file_size,
|
||||
candidate->software_version, candidate->software_version_str, action.callback_args);
|
||||
return;
|
||||
} else {
|
||||
// Cannot find the candidate from cache, we need to query DCL for a new candidate;
|
||||
uint32_t *software_version_array;
|
||||
size_t software_version_count;
|
||||
esp_err_t err = _query_software_version_array(action.vendor_id, action.product_id, &software_version_array,
|
||||
software_version_count);
|
||||
if (err == ESP_OK && software_version_array && software_version_count > 0) {
|
||||
// Sort the software version array
|
||||
std::sort(&software_version_array[0], &software_version_array[software_version_count - 1],
|
||||
std::greater<uint32_t>());
|
||||
candidate = (model_version_t *)esp_matter_mem_calloc(1, sizeof(model_version_t));
|
||||
candidate->vendor_id = action.vendor_id;
|
||||
candidate->product_id = action.product_id;
|
||||
for (size_t index = 0;
|
||||
index < software_version_count && software_version_array[index] > action.software_version; ++index) {
|
||||
err = _query_ota_candidate(candidate, software_version_array[index], action.software_version);
|
||||
if (err == ESP_OK) {
|
||||
size_t empty_index = _find_empty_ota_candidates();
|
||||
_increase_ota_candidates_lifetime();
|
||||
candidate->lifetime = 0;
|
||||
// Add this candidate to cache
|
||||
_ota_candidates_cache[empty_index] = candidate;
|
||||
action.callback(EspOtaProvider::OTAQueryStatus::kUpdateAvailable, candidate->ota_url,
|
||||
candidate->ota_file_size, candidate->software_version,
|
||||
candidate->software_version_str, action.callback_args);
|
||||
esp_matter_mem_free(software_version_array);
|
||||
return;
|
||||
}
|
||||
}
|
||||
esp_matter_mem_free(candidate);
|
||||
esp_matter_mem_free(software_version_array);
|
||||
}
|
||||
}
|
||||
// Cannot fetch the candidate
|
||||
action.callback(EspOtaProvider::OTAQueryStatus::kNotAvailable, nullptr, 0, 0, nullptr, action.callback_args);
|
||||
}
|
||||
|
||||
static void ota_candidate_task(void *ctx)
|
||||
{
|
||||
ota_candidate_fetch_action_t action;
|
||||
while (true) {
|
||||
if (xQueueReceive(_ota_candidate_task_queue, &action, portMAX_DELAY) == pdTRUE) {
|
||||
if (action.vendor_id != chip::kMaxVendorId) {
|
||||
_ota_candidate_fetch_handler(action);
|
||||
}
|
||||
#ifdef CONFIG_ESP_MATTER_OTA_CANDIDATES_UPDATE_PERIODICALLY
|
||||
else {
|
||||
// If receiving an action with Max VendorId, try to update all the candidates cache.
|
||||
_update_all_ota_candidates_cache();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
vQueueDelete(_ota_candidate_task_queue);
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
esp_err_t fetch_ota_candidate(const uint16_t vendor_id, const uint16_t product_id, const uint32_t software_version,
|
||||
fetch_ota_image_done_callback_t callback, void *ctx)
|
||||
{
|
||||
if (!_ota_candidate_task_queue) {
|
||||
ESP_LOGE(TAG, "Failed to search ota candidate as the task queue is not initialized");
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
if (!callback) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
ota_candidate_fetch_action_t action;
|
||||
action.vendor_id = vendor_id;
|
||||
action.product_id = product_id;
|
||||
action.software_version = software_version;
|
||||
action.callback = callback;
|
||||
action.callback_args = ctx;
|
||||
if (xQueueSend(_ota_candidate_task_queue, &action, portMAX_DELAY) != pdTRUE) {
|
||||
ESP_LOGE(TAG, "Failed send search ota candidate action");
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t init_ota_candidates()
|
||||
{
|
||||
memset(_ota_candidates_cache, 0, sizeof(_ota_candidates_cache));
|
||||
if (_ota_candidate_task_queue) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
_ota_candidate_task_queue = xQueueCreate(8, sizeof(ota_candidate_fetch_action_t));
|
||||
if (!_ota_candidate_task_queue) {
|
||||
ESP_LOGE(TAG, "Failed to create ota_candidate task queue");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
static TaskHandle_t task_handle = NULL;
|
||||
if (task_handle) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (xTaskCreate(ota_candidate_task, "ota_candidate", 8192, NULL, 5, NULL) != pdTRUE) {
|
||||
ESP_LOGE(TAG, "Failed to create ota_candidate task");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
#ifdef CONFIG_ESP_MATTER_OTA_CANDIDATES_UPDATE_PERIODICALLY
|
||||
if (!_ota_candidates_update_timer) {
|
||||
// start a timer which will update the candidates cache everyday.
|
||||
esp_timer_init();
|
||||
const esp_timer_create_args_t timer_args = {
|
||||
.callback = _ota_candidates_periodic_update_handler, .arg = nullptr, .name = "ota_candidates_update_timer"};
|
||||
esp_timer_create(&timer_args, &_ota_candidates_update_timer);
|
||||
esp_timer_start_periodic(_ota_candidates_update_timer,
|
||||
(uint64_t)CONFIG_ESP_MATTER_OTA_CANDIDATES_UPDATE_PERIOD * 3600 * 1000 * 1000);
|
||||
}
|
||||
#endif
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
} // namespace ota_provider
|
||||
} // namespace esp_matter
|
||||
@@ -0,0 +1,184 @@
|
||||
// Copyright 2023 Espressif Systems (Shanghai) PTE LTD
|
||||
//
|
||||
// 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 "esp_err.h"
|
||||
#include <esp_check.h>
|
||||
#include <esp_http_client.h>
|
||||
#include <esp_log.h>
|
||||
#include <esp_matter_mem.h>
|
||||
#include <esp_matter_ota_http_downloader.h>
|
||||
#include <sdkconfig.h>
|
||||
|
||||
static constexpr char TAG[] = "ota_provider";
|
||||
|
||||
namespace esp_matter {
|
||||
namespace ota_provider {
|
||||
|
||||
static bool _process_again(int status_code)
|
||||
{
|
||||
switch (status_code) {
|
||||
case HttpStatus_MovedPermanently:
|
||||
case HttpStatus_Found:
|
||||
case HttpStatus_TemporaryRedirect:
|
||||
case HttpStatus_Unauthorized:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static esp_err_t _http_handle_response_code(esp_http_client_handle_t http_client, int status_code)
|
||||
{
|
||||
esp_err_t err = ESP_OK;
|
||||
if (status_code == HttpStatus_MovedPermanently || status_code == HttpStatus_Found ||
|
||||
status_code == HttpStatus_TemporaryRedirect) {
|
||||
err = esp_http_client_set_redirection(http_client);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "URL redirection Failed");
|
||||
return err;
|
||||
}
|
||||
} else if (status_code == HttpStatus_Unauthorized) {
|
||||
esp_http_client_add_auth(http_client);
|
||||
} else if (status_code == HttpStatus_NotFound || status_code == HttpStatus_Forbidden) {
|
||||
ESP_LOGE(TAG, "File not found(%d)", status_code);
|
||||
return ESP_FAIL;
|
||||
} else if (status_code >= HttpStatus_BadRequest && status_code < HttpStatus_InternalError) {
|
||||
ESP_LOGE(TAG, "Client error (%d)", status_code);
|
||||
return ESP_FAIL;
|
||||
} else if (status_code >= HttpStatus_InternalError) {
|
||||
ESP_LOGE(TAG, "Server error (%d)", status_code);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
char upgrade_data_buf[256];
|
||||
// process_again() returns true only in case of redirection.
|
||||
if (_process_again(status_code)) {
|
||||
while (1) {
|
||||
// In case of redirection, esp_http_client_read() is called
|
||||
// to clear the response buffer of http_client.
|
||||
int data_read = esp_http_client_read(http_client, upgrade_data_buf, sizeof(upgrade_data_buf));
|
||||
if (data_read <= 0) {
|
||||
return ESP_OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t _http_connect(esp_http_client_handle_t http_client)
|
||||
{
|
||||
esp_err_t err = ESP_OK;
|
||||
int status_code, header_ret;
|
||||
do {
|
||||
char *post_data = NULL;
|
||||
/* Send POST request if body is set.
|
||||
* Note: Sending POST request is not supported if partial_http_download
|
||||
* is enabled
|
||||
*/
|
||||
int post_len = esp_http_client_get_post_field(http_client, &post_data);
|
||||
err = esp_http_client_open(http_client, post_len);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
if (post_len) {
|
||||
int write_len = 0;
|
||||
while (post_len > 0) {
|
||||
write_len = esp_http_client_write(http_client, post_data, post_len);
|
||||
if (write_len < 0) {
|
||||
ESP_LOGE(TAG, "Write failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
post_len -= write_len;
|
||||
post_data += write_len;
|
||||
}
|
||||
}
|
||||
header_ret = esp_http_client_fetch_headers(http_client);
|
||||
if (header_ret < 0) {
|
||||
return header_ret;
|
||||
}
|
||||
status_code = esp_http_client_get_status_code(http_client);
|
||||
err = _http_handle_response_code(http_client, status_code);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
} while (_process_again(status_code));
|
||||
return err;
|
||||
}
|
||||
|
||||
static int _http_client_read_check_connection(esp_http_client_handle_t client, char *data, size_t size)
|
||||
{
|
||||
int len = esp_http_client_read(client, data, size);
|
||||
if (len == 0 && !esp_http_client_is_complete_data_received(client) &&
|
||||
(errno == ENOTCONN || errno == ECONNRESET || errno == ECONNABORTED)) {
|
||||
return -1;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
static int _http_client_read(esp_http_client_handle_t http_client, char *data, size_t size)
|
||||
{
|
||||
int read_len = 0;
|
||||
while (read_len < size) {
|
||||
int len = _http_client_read_check_connection(http_client, data, size - read_len);
|
||||
if (esp_http_client_is_complete_data_received(http_client)) {
|
||||
ESP_LOGI(TAG, "Finish downloading");
|
||||
return read_len + len;
|
||||
} else if (len < 0) {
|
||||
ESP_LOGE(TAG, "Failed to read image");
|
||||
return read_len;
|
||||
}
|
||||
read_len += len;
|
||||
}
|
||||
return read_len;
|
||||
}
|
||||
|
||||
static void _http_client_cleanup(esp_http_client_handle_t client)
|
||||
{
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
}
|
||||
|
||||
int http_downloader_read(esp_http_client_handle_t http_client, char *buf, size_t size)
|
||||
{
|
||||
if (!http_client) {
|
||||
return -1;
|
||||
}
|
||||
return _http_client_read(http_client, buf, size);
|
||||
}
|
||||
|
||||
void http_downloader_abort(esp_http_client_handle_t http_client)
|
||||
{
|
||||
if (http_client) {
|
||||
_http_client_cleanup(http_client);
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t http_downloader_start(esp_http_client_config_t *config, esp_http_client_handle_t *http_client)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
ESP_RETURN_ON_FALSE(http_client, ESP_ERR_INVALID_ARG, TAG, "http_client cannot be NULL");
|
||||
*http_client = esp_http_client_init(config);
|
||||
ESP_RETURN_ON_FALSE(*http_client, ESP_ERR_NO_MEM, TAG, "Failed to initialize http client");
|
||||
ESP_GOTO_ON_ERROR(_http_connect(*http_client), exit, TAG, "Failed to connect to HTTP server");
|
||||
return ESP_OK;
|
||||
exit:
|
||||
_http_client_cleanup(*http_client);
|
||||
*http_client = nullptr;
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace ota_provider
|
||||
} // namespace esp_matter
|
||||
@@ -0,0 +1,355 @@
|
||||
// Copyright 2023 Espressif Systems (Shanghai) PTE LTD
|
||||
//
|
||||
// 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 <cstring>
|
||||
#include <esp_check.h>
|
||||
#include <esp_http_client.h>
|
||||
#include <esp_log.h>
|
||||
#include <esp_matter_mem.h>
|
||||
#include <esp_matter_ota_candidates.h>
|
||||
#include <esp_matter_ota_provider.h>
|
||||
#include <json_parser.h>
|
||||
|
||||
#include <app/server/Server.h>
|
||||
#include <platform/PlatformManager.h>
|
||||
#include <protocols/bdx/BdxUri.h>
|
||||
#include <protocols/interaction_model/StatusCode.h>
|
||||
|
||||
using namespace chip;
|
||||
using namespace chip::app::Clusters::OtaSoftwareUpdateProvider::Commands;
|
||||
using chip::BitFlags;
|
||||
using chip::ByteSpan;
|
||||
using chip::app::CommandHandler;
|
||||
using chip::app::ConcreteCommandPath;
|
||||
using chip::bdx::TransferControlFlags;
|
||||
using chip::Protocols::InteractionModel::Status;
|
||||
|
||||
static constexpr char TAG[] = "ota_provider";
|
||||
|
||||
namespace esp_matter {
|
||||
namespace ota_provider {
|
||||
|
||||
// Arbitrary BDX Transfer Params
|
||||
constexpr uint32_t kMaxBdxBlockSize = 1024;
|
||||
constexpr chip::System::Clock::Timeout kBdxTimeout =
|
||||
chip::System::Clock::Seconds16(5 * 60); // OTA Spec mandates >= 5 minutes
|
||||
constexpr uint32_t kBdxServerPollIntervalMillis = 50;
|
||||
|
||||
static void GenerateUpdateToken(uint8_t *buf, size_t bufSize)
|
||||
{
|
||||
for (size_t i = 0; i < bufSize; ++i) {
|
||||
buf[i] = chip::Crypto::GetRandU8();
|
||||
}
|
||||
}
|
||||
|
||||
static void GetUpdateTokenString(const ByteSpan &token, char *buf, size_t bufSize)
|
||||
{
|
||||
const uint8_t *tokenData = static_cast<const uint8_t *>(token.data());
|
||||
size_t minLength = chip::min(token.size(), bufSize);
|
||||
for (size_t i = 0; i < (minLength / 2) - 1; ++i) {
|
||||
snprintf(&buf[i * 2], bufSize, "%02X", tokenData[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void EspOtaProvider::Init(bool otaAllowedDefault)
|
||||
{
|
||||
mDelayedQueryActionTimeSec = 0;
|
||||
mUpdateAction = OTAApplyUpdateAction::kProceed;
|
||||
mDelayedApplyActionTimeSec = 0;
|
||||
mPollInterval = kBdxServerPollIntervalMillis;
|
||||
mOtaRequestorList = nullptr;
|
||||
mOtaAllowedDefault = otaAllowedDefault;
|
||||
init_ota_candidates();
|
||||
chip::Server::GetInstance().GetExchangeManager().RegisterUnsolicitedMessageHandlerForProtocol(
|
||||
chip::Protocols::BDX::Id, &mOtaBdxSender);
|
||||
}
|
||||
|
||||
void EspOtaProvider::SendQueryImageResponse(OTAQueryStatus status)
|
||||
{
|
||||
auto commandHandleRef = std::move(mAsyncCommandHandle);
|
||||
auto commandHandle = commandHandleRef.Get();
|
||||
if (commandHandle == nullptr || commandHandle->GetExchangeContext()->GetSessionHandle()->GetPeer() != mPeerNodeId) {
|
||||
ESP_LOGE(TAG, "Invalid commandHandle, cannot send QueryImageResponse");
|
||||
return;
|
||||
}
|
||||
EspOtaRequestorEntry *requestor = FindOtaRequestorEntry(mPeerNodeId);
|
||||
if (requestor) {
|
||||
if ((!requestor->mOtaAllowed) && (!requestor->mOtaAllowedOnce)) {
|
||||
if (status == OTAQueryStatus::kUpdateAvailable) {
|
||||
requestor->mHasNewVersion = true;
|
||||
}
|
||||
status = OTAQueryStatus::kNotAvailable;
|
||||
}
|
||||
} else {
|
||||
status = OTAQueryStatus::kNotAvailable;
|
||||
}
|
||||
|
||||
QueryImageResponse::Type response;
|
||||
char strBuf[kUpdateTokenStrLen] = {0};
|
||||
|
||||
// Set fields specific for an available status response
|
||||
if (status == OTAQueryStatus::kUpdateAvailable) {
|
||||
FabricIndex fabricIndex = mSubjectDescriptor.fabricIndex;
|
||||
const FabricInfo *fabricInfo = Server::GetInstance().GetFabricTable().FindFabricWithIndex(fabricIndex);
|
||||
NodeId providerNodeId = fabricInfo->GetPeerId().GetNodeId();
|
||||
|
||||
// Generate the ImageURI
|
||||
MutableCharSpan uri(requestor->mImageUri);
|
||||
char otaFileName[128] = {0};
|
||||
char *ptr = strrchr(requestor->mOtaImageUrl, '/');
|
||||
strncpy(otaFileName, ptr + 1, strnlen(ptr + 1, 255));
|
||||
CHIP_ERROR error = chip::bdx::MakeURI(providerNodeId, CharSpan::fromCharString(otaFileName), uri);
|
||||
if (error != CHIP_NO_ERROR) {
|
||||
ESP_LOGE(TAG, "Cannot generate URI");
|
||||
memset(requestor->mImageUri, 0, sizeof(requestor->mImageUri));
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Generated URI: %s", requestor->mImageUri);
|
||||
}
|
||||
|
||||
// Initialize the transfer session in prepartion for a BDX transfer
|
||||
BitFlags<TransferControlFlags> bdxFlags;
|
||||
bdxFlags.Set(TransferControlFlags::kReceiverDrive);
|
||||
if (mOtaBdxSender.InitializeTransfer(mSubjectDescriptor.fabricIndex, mSubjectDescriptor.subject) == ESP_OK) {
|
||||
mOtaBdxSender.SetOtaImageUrl(requestor->mOtaImageUrl);
|
||||
ESP_LOGI(TAG, "Bdx Sender will query the OTA image from %s", requestor->mOtaImageUrl);
|
||||
CHIP_ERROR error = mOtaBdxSender.PrepareForTransfer(
|
||||
&chip::DeviceLayer::SystemLayer(), chip::bdx::TransferRole::kSender, bdxFlags, kMaxBdxBlockSize,
|
||||
kBdxTimeout, chip::System::Clock::Milliseconds32(mPollInterval));
|
||||
if (error != CHIP_NO_ERROR) {
|
||||
ESP_LOGE(TAG, "Cannot prepare for transfer: %" CHIP_ERROR_FORMAT, error.Format());
|
||||
commandHandle->AddStatus(mPath, Status::Failure);
|
||||
return;
|
||||
}
|
||||
GenerateUpdateToken(requestor->mUpdateToken, kUpdateTokenLen);
|
||||
GetUpdateTokenString(ByteSpan(requestor->mUpdateToken), strBuf, kUpdateTokenStrLen);
|
||||
ESP_LOGD(TAG, "Generated updateToken: %s", strBuf);
|
||||
|
||||
response.imageURI.Emplace(chip::CharSpan::fromCharString(requestor->mImageUri));
|
||||
response.softwareVersion.Emplace(requestor->mSoftwareVersion);
|
||||
response.softwareVersionString.Emplace(chip::CharSpan::fromCharString(requestor->mSoftwareVersionString));
|
||||
response.updateToken.Emplace(chip::ByteSpan(requestor->mUpdateToken));
|
||||
} else {
|
||||
// Another BDX transfer in progress
|
||||
status = OTAQueryStatus::kBusy;
|
||||
}
|
||||
}
|
||||
|
||||
// Delay action time is only applicable when the provider is busy
|
||||
if (status == OTAQueryStatus::kBusy) {
|
||||
if (mDelayedApplyActionTimeSec == 0) {
|
||||
mDelayedQueryActionTimeSec = 120;
|
||||
}
|
||||
response.delayedActionTime.Emplace(mDelayedQueryActionTimeSec);
|
||||
}
|
||||
|
||||
// Set remaining fields common to all status types
|
||||
response.status = status;
|
||||
// Either sends the response or an error status
|
||||
commandHandle->AddResponse(mPath, response);
|
||||
}
|
||||
|
||||
void EspOtaProvider::FetchImageDoneCallback(OTAQueryStatus status, const char *imageUrl, size_t imageSize,
|
||||
uint32_t softwareVersion, const char *softwareVersionStr, void *arg)
|
||||
{
|
||||
EspOtaProvider *provider = (EspOtaProvider *)arg;
|
||||
assert(provider);
|
||||
EspOtaRequestorEntry *requestor = provider->FindOtaRequestorEntry(provider->mPeerNodeId);
|
||||
if (requestor && status == OTAQueryStatus::kUpdateAvailable) {
|
||||
strncpy(requestor->mOtaImageUrl, imageUrl, sizeof(requestor->mOtaImageUrl) - 1);
|
||||
requestor->mOtaImageSize = imageSize;
|
||||
requestor->mSoftwareVersion = softwareVersion;
|
||||
strncpy(requestor->mSoftwareVersionString, softwareVersionStr, sizeof(requestor->mSoftwareVersionString) - 1);
|
||||
}
|
||||
DeviceLayer::PlatformMgr().LockChipStack();
|
||||
provider->SendQueryImageResponse(status);
|
||||
DeviceLayer::PlatformMgr().UnlockChipStack();
|
||||
}
|
||||
|
||||
void EspOtaProvider::HandleQueryImage(CommandHandler *commandObj, const ConcreteCommandPath &commandPath,
|
||||
const QueryImage::DecodableType &commandData)
|
||||
{
|
||||
uint16_t vendor_id = commandData.vendorID;
|
||||
uint16_t product_id = commandData.productID;
|
||||
uint32_t software_version = commandData.softwareVersion;
|
||||
if (CreateOtaRequestorEntry(commandObj->GetExchangeContext()->GetSessionHandle()->GetPeer()) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to create Ota Pending Entry");
|
||||
commandObj->AddStatus(commandPath, Status::ResourceExhausted);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mAsyncCommandHandle.Get() != nullptr) {
|
||||
// We have a command processing in the backend, reject query image command.
|
||||
QueryImageResponse::Type response;
|
||||
response.status = OTAQueryStatus::kBusy;
|
||||
if (mDelayedApplyActionTimeSec == 0) {
|
||||
mDelayedQueryActionTimeSec = 120;
|
||||
}
|
||||
response.delayedActionTime.Emplace(mDelayedQueryActionTimeSec);
|
||||
commandObj->AddResponse(commandPath, response);
|
||||
return;
|
||||
}
|
||||
// The OTA provider might need some time to query the image information from DCL.
|
||||
commandObj->FlushAcksRightAwayOnSlowCommand();
|
||||
// Use a command handle to hold the CommandHandler so that it will not be released.
|
||||
mSubjectDescriptor = commandObj->GetSubjectDescriptor();
|
||||
mPeerNodeId = commandObj->GetExchangeContext()->GetSessionHandle()->GetPeer();
|
||||
mAsyncCommandHandle = chip::app::CommandHandler::Handle(commandObj);
|
||||
mPath = commandPath;
|
||||
if (fetch_ota_candidate(vendor_id, product_id, software_version, FetchImageDoneCallback, this) != ESP_OK) {
|
||||
SendQueryImageResponse(OTAQueryStatus::kNotAvailable);
|
||||
}
|
||||
}
|
||||
|
||||
void EspOtaProvider::HandleApplyUpdateRequest(app::CommandHandler *commandObj,
|
||||
const app::ConcreteCommandPath &commandPath,
|
||||
const ApplyUpdateRequest::DecodableType &commandData)
|
||||
{
|
||||
if (commandObj == nullptr) {
|
||||
ESP_LOGE(TAG, "Invalid commandObj, cannot handle ApplyUpdateRequest");
|
||||
return;
|
||||
}
|
||||
EspOtaRequestorEntry *requestor =
|
||||
FindOtaRequestorEntry(commandObj->GetExchangeContext()->GetSessionHandle()->GetPeer());
|
||||
char tokenBuf[kUpdateTokenStrLen] = {0};
|
||||
GetUpdateTokenString(commandData.updateToken, tokenBuf, kUpdateTokenStrLen);
|
||||
ESP_LOGD(TAG, "%s: token: %s, version: %" PRIu32, __FUNCTION__, tokenBuf, commandData.newVersion);
|
||||
if (requestor && commandData.updateToken.data_equal(ByteSpan(requestor->mUpdateToken)) &&
|
||||
commandData.newVersion == requestor->mSoftwareVersion) {
|
||||
ApplyUpdateResponse::Type response;
|
||||
response.action = mUpdateAction;
|
||||
response.delayedActionTime = mDelayedApplyActionTimeSec;
|
||||
|
||||
// Reset delay back to 0 for subsequent uses
|
||||
mDelayedApplyActionTimeSec = 0;
|
||||
// Reset back to success case for subsequent uses
|
||||
mUpdateAction = OTAApplyUpdateAction::kProceed;
|
||||
|
||||
// Either sends the response or an error status
|
||||
commandObj->AddResponse(commandPath, response);
|
||||
} else {
|
||||
commandObj->AddStatus(commandPath, Status::InvalidCommand);
|
||||
}
|
||||
}
|
||||
|
||||
void EspOtaProvider::HandleNotifyUpdateApplied(app::CommandHandler *commandObj,
|
||||
const app::ConcreteCommandPath &commandPath,
|
||||
const NotifyUpdateApplied::DecodableType &commandData)
|
||||
{
|
||||
if (commandObj == nullptr) {
|
||||
ESP_LOGE(TAG, "Invalid commandObj, cannot handle ApplyUpdateRequest");
|
||||
return;
|
||||
}
|
||||
EspOtaRequestorEntry *requestor =
|
||||
FindOtaRequestorEntry(commandObj->GetExchangeContext()->GetSessionHandle()->GetPeer());
|
||||
char tokenBuf[kUpdateTokenStrLen] = {0};
|
||||
GetUpdateTokenString(commandData.updateToken, tokenBuf, kUpdateTokenStrLen);
|
||||
ESP_LOGD(TAG, "%s: token: %s, version: %" PRIu32, __FUNCTION__, tokenBuf, commandData.softwareVersion);
|
||||
if (requestor && commandData.updateToken.data_equal(ByteSpan(requestor->mUpdateToken)) &&
|
||||
commandData.softwareVersion == requestor->mSoftwareVersion) {
|
||||
commandObj->AddStatus(commandPath, Status::Success);
|
||||
// Finish OTA, set the set OtaAllowedOnce to false.
|
||||
requestor->mOtaAllowedOnce = false;
|
||||
} else {
|
||||
commandObj->AddStatus(commandPath, Status::InvalidCommand);
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t EspOtaProvider::EnableOtaForNode(const chip::ScopedNodeId &nodeId, bool forOnlyOnce)
|
||||
{
|
||||
EspOtaRequestorEntry *iter = mOtaRequestorList;
|
||||
bool found = false;
|
||||
while (iter) {
|
||||
if (iter->mNodeId == nodeId ||
|
||||
(nodeId.GetNodeId() == chip::kUndefinedNodeId &&
|
||||
(nodeId.GetFabricIndex() == iter->mNodeId.GetFabricIndex() ||
|
||||
nodeId.GetFabricIndex() == chip::kUndefinedFabricIndex))) {
|
||||
if (!forOnlyOnce) {
|
||||
iter->mOtaAllowed = true;
|
||||
} else {
|
||||
iter->mOtaAllowedOnce = true;
|
||||
}
|
||||
found = true;
|
||||
}
|
||||
iter = iter->mNext;
|
||||
}
|
||||
return found ? ESP_OK : ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
esp_err_t EspOtaProvider::DisableOtaForNode(const chip::ScopedNodeId &nodeId)
|
||||
{
|
||||
EspOtaRequestorEntry *iter = mOtaRequestorList;
|
||||
bool found = false;
|
||||
while (iter) {
|
||||
if (iter->mNodeId == nodeId ||
|
||||
(nodeId.GetNodeId() == chip::kUndefinedNodeId &&
|
||||
(nodeId.GetFabricIndex() == iter->mNodeId.GetFabricIndex() ||
|
||||
nodeId.GetFabricIndex() == chip::kUndefinedFabricIndex))) {
|
||||
iter->mOtaAllowed = false;
|
||||
iter->mOtaAllowedOnce = false;
|
||||
found = true;
|
||||
}
|
||||
iter = iter->mNext;
|
||||
}
|
||||
return found ? ESP_OK : ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
EspOtaProvider::EspOtaRequestorEntry *EspOtaProvider::FindOtaRequestorEntry(const chip::ScopedNodeId &nodeId)
|
||||
{
|
||||
EspOtaRequestorEntry *iter = mOtaRequestorList;
|
||||
while (iter) {
|
||||
if (iter->mNodeId == nodeId) {
|
||||
return iter;
|
||||
}
|
||||
iter = iter->mNext;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
esp_err_t EspOtaProvider::CreateOtaRequestorEntry(const chip::ScopedNodeId &nodeId)
|
||||
{
|
||||
EspOtaRequestorEntry *entry = FindOtaRequestorEntry(nodeId);
|
||||
if (!entry) {
|
||||
entry = chip::Platform::New<EspOtaRequestorEntry>();
|
||||
if (!entry) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
entry->mNodeId = nodeId;
|
||||
entry->mOtaAllowed = mOtaAllowedDefault;
|
||||
entry->mNext = mOtaRequestorList;
|
||||
mOtaRequestorList = entry;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t EspOtaProvider::RemoveOtaRequestorEntry(const chip::ScopedNodeId &nodeId)
|
||||
{
|
||||
EspOtaRequestorEntry *prev = nullptr;
|
||||
EspOtaRequestorEntry *iter = mOtaRequestorList;
|
||||
while (iter && iter->mNodeId != nodeId) {
|
||||
prev = iter;
|
||||
iter = iter->mNext;
|
||||
}
|
||||
if (iter) {
|
||||
if (!prev) {
|
||||
mOtaRequestorList = iter->mNext;
|
||||
} else {
|
||||
prev->mNext = iter->mNext;
|
||||
}
|
||||
chip::Platform::Delete(iter);
|
||||
return ESP_OK;
|
||||
}
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
} // namespace ota_provider
|
||||
} // namespace esp_matter
|
||||
@@ -98,3 +98,9 @@ examples/door_lock:
|
||||
- if: IDF_TARGET in ["esp32", "esp32c3", "esp32c2", "esp32c6", "esp32h2"]
|
||||
temporary: true
|
||||
reason: the other targets are not tested yet
|
||||
|
||||
examples/ota_provider:
|
||||
enable:
|
||||
- if: IDF_TARGET in ["esp32s3"]
|
||||
temporary: true
|
||||
reason: the other targets are not tested yet
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# The following lines of boilerplate have to be in your project's
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
|
||||
if(NOT DEFINED ENV{ESP_MATTER_PATH})
|
||||
message(FATAL_ERROR "Please set ESP_MATTER_PATH to the path of esp-matter repo")
|
||||
endif(NOT DEFINED ENV{ESP_MATTER_PATH})
|
||||
|
||||
if(NOT DEFINED ENV{ESP_MATTER_DEVICE_PATH})
|
||||
if("${IDF_TARGET}" STREQUAL "esp32" OR "${IDF_TARGET}" STREQUAL "")
|
||||
set(ENV{ESP_MATTER_DEVICE_PATH} $ENV{ESP_MATTER_PATH}/device_hal/device/esp32_devkit_c)
|
||||
elseif("${IDF_TARGET}" STREQUAL "esp32c3")
|
||||
set(ENV{ESP_MATTER_DEVICE_PATH} $ENV{ESP_MATTER_PATH}/device_hal/device/esp32c3_devkit_m)
|
||||
elseif("${IDF_TARGET}" STREQUAL "esp32s3")
|
||||
set(ENV{ESP_MATTER_DEVICE_PATH} $ENV{ESP_MATTER_PATH}/device_hal/device/esp32s3_devkit_c)
|
||||
else()
|
||||
message(FATAL_ERROR "Unsupported IDF_TARGET")
|
||||
endif()
|
||||
endif(NOT DEFINED ENV{ESP_MATTER_DEVICE_PATH})
|
||||
|
||||
set(ESP_MATTER_PATH $ENV{ESP_MATTER_PATH})
|
||||
set(MATTER_SDK_PATH ${ESP_MATTER_PATH}/connectedhomeip/connectedhomeip)
|
||||
|
||||
# This should be done before using the IDF_TARGET variable.
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
include($ENV{ESP_MATTER_DEVICE_PATH}/esp_matter_device.cmake)
|
||||
|
||||
set(EXTRA_COMPONENT_DIRS
|
||||
"../common"
|
||||
"${MATTER_SDK_PATH}/config/esp32/components"
|
||||
"${ESP_MATTER_PATH}/components"
|
||||
"${ESP_MATTER_PATH}/device_hal/device"
|
||||
${extra_components_dirs_append})
|
||||
|
||||
project(ota_provider)
|
||||
|
||||
idf_build_set_property(CXX_COMPILE_OPTIONS "-std=gnu++17;-Os;-DCHIP_HAVE_CONFIG_H" APPEND)
|
||||
idf_build_set_property(C_COMPILE_OPTIONS "-Os" APPEND)
|
||||
|
||||
# For RISCV chips, project_include.cmake sets -Wno-format, but does not clear various
|
||||
# flags that depend on -Wformat
|
||||
idf_build_set_property(COMPILE_OPTIONS "-Wno-format-nonliteral;-Wno-format-security" APPEND)
|
||||
@@ -0,0 +1,18 @@
|
||||
# Controller
|
||||
|
||||
This example creates a Matter OTA Provider using the ESP Matter data model.
|
||||
|
||||
|
||||
See the [docs](https://docs.espressif.com/projects/esp-matter/en/latest/esp32/developing.html) for more information about building and flashing the firmware.
|
||||
|
||||
## 1. Additional Environment Setup
|
||||
|
||||
No additional setup is required.
|
||||
|
||||
## 2. OTA Provider Example
|
||||
|
||||
To test this OTA Provider example, you need to upload your ota candidate to the DCL(Distributed Compliance Ledger) [TestNet](https://testnet.iotledger.io/models). This candidate should include an OTA image URL which can be used for downloading the OTA image.
|
||||
|
||||
For offical products, the ota candidate should be uploaded to the DCL [MainNet](https://dcl.iotledger.io/models).
|
||||
|
||||
The Matter OTA instruction can be found in [docs](https://github.com/project-chip/connectedhomeip/blob/master/docs/guides/esp32/ota.md).
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
idf_component_register(SRC_DIRS ".")
|
||||
|
||||
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
|
||||
target_compile_options(${COMPONENT_LIB} PRIVATE "-DCHIP_HAVE_CONFIG_H")
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
This example code is in the Public Domain (or CC0 licensed, at your option.)
|
||||
|
||||
Unless required by applicable law or agreed to in writing, this
|
||||
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
CONDITIONS OF ANY KIND, either express or implied.
|
||||
*/
|
||||
|
||||
#include <esp_err.h>
|
||||
#include <esp_log.h>
|
||||
#include <nvs_flash.h>
|
||||
|
||||
#include <esp_matter.h>
|
||||
#include <esp_matter_console.h>
|
||||
#include <esp_matter_ota_provider.h>
|
||||
|
||||
#include <app_reset.h>
|
||||
|
||||
#include <app/clusters/ota-provider/ota-provider.h>
|
||||
#include <app/server/Server.h>
|
||||
#include <credentials/FabricTable.h>
|
||||
|
||||
static const char *TAG = "app_main";
|
||||
uint16_t switch_endpoint_id = 0;
|
||||
|
||||
using namespace esp_matter;
|
||||
using namespace esp_matter::attribute;
|
||||
using namespace esp_matter::endpoint;
|
||||
using namespace esp_matter::ota_provider;
|
||||
using namespace chip::app::Clusters;
|
||||
using chip::app::Clusters::OTAProviderDelegate;
|
||||
|
||||
static void app_event_cb(const ChipDeviceEvent *event, intptr_t arg)
|
||||
{
|
||||
switch (event->Type) {
|
||||
case chip::DeviceLayer::DeviceEventType::PublicEventTypes::kInterfaceIpAddressChanged:
|
||||
ESP_LOGI(TAG, "Interface IP Address changed");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void app_main()
|
||||
{
|
||||
esp_err_t err = ESP_OK;
|
||||
|
||||
/* Initialize the ESP NVS layer */
|
||||
nvs_flash_init();
|
||||
// If there is no commissioner in the controller, we need a default node so that the controller can be commissioned
|
||||
// to a specific fabric.
|
||||
node::config_t node_config;
|
||||
node_t *node = node::create(&node_config, NULL, NULL);
|
||||
endpoint_t *root_node_endpoint = endpoint::get(node, 0);
|
||||
cluster::ota_provider::config_t config;
|
||||
cluster_t *ota_provider_cluster = cluster::ota_provider::create(root_node_endpoint, &config, CLUSTER_FLAG_SERVER);
|
||||
if (!node || !root_node_endpoint || !ota_provider_cluster) {
|
||||
ESP_LOGE(TAG, "Failed to create data model");
|
||||
return;
|
||||
}
|
||||
/* Matter start */
|
||||
err = esp_matter::start(app_event_cb);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Matter start failed: %d", err);
|
||||
}
|
||||
EspOtaProvider::GetInstance().Init(true);
|
||||
OTAProvider::SetDelegate(0, reinterpret_cast<OTAProviderDelegate *>(&EspOtaProvider::GetInstance()));
|
||||
#if CONFIG_ENABLE_CHIP_SHELL
|
||||
esp_matter::console::diagnostics_register_commands();
|
||||
esp_matter::console::wifi_register_commands();
|
||||
esp_matter::console::init();
|
||||
#endif // CONFIG_ENABLE_CHIP_SHELL
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
# Note: Firmware partition offset needs to be 64K aligned, initial 36K (9 sectors) are reserved for bootloader and partition table
|
||||
esp_secure_cert, 0x3F, ,0xd000, 0x2000, encrypted
|
||||
nvs, data, nvs, 0x10000, 0xC000,
|
||||
nvs_keys, data, nvs_keys,, 0x1000, encrypted
|
||||
otadata, data, ota, , 0x2000
|
||||
phy_init, data, phy, , 0x1000,
|
||||
ota_0, app, ota_0, 0x20000, 0x1E0000,
|
||||
ota_1, app, ota_1, 0x200000, 0x1E0000,
|
||||
|
@@ -0,0 +1,47 @@
|
||||
# Default to 921600 baud when flashing and monitoring device
|
||||
CONFIG_ESPTOOLPY_BAUD_921600B=y
|
||||
CONFIG_ESPTOOLPY_BAUD=921600
|
||||
CONFIG_ESPTOOLPY_COMPRESSED=y
|
||||
CONFIG_ESPTOOLPY_MONITOR_BAUD_115200B=y
|
||||
CONFIG_ESPTOOLPY_MONITOR_BAUD=115200
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||
|
||||
#enable BT
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
|
||||
#enable lwip ipv6 autoconfig
|
||||
CONFIG_LWIP_IPV6_AUTOCONFIG=y
|
||||
|
||||
# Use a custom partition table
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
|
||||
|
||||
# Testing Options
|
||||
CONFIG_USE_TEST_SETUP_PIN_CODE=20212020
|
||||
CONFIG_USE_TEST_SETUP_DISCRIMINATOR=0xF0
|
||||
|
||||
# Enable chip shell
|
||||
CONFIG_ENABLE_CHIP_SHELL=y
|
||||
CONFIG_ESP_MATTER_CONSOLE_TASK_STACK=4096
|
||||
|
||||
#enable lwIP route hooks
|
||||
CONFIG_LWIP_HOOK_IP6_ROUTE_DEFAULT=y
|
||||
CONFIG_LWIP_HOOK_ND6_GET_GW_DEFAULT=y
|
||||
|
||||
# Increase udp endpoints num for commissioner
|
||||
CONFIG_NUM_UDP_ENDPOINTS=16
|
||||
|
||||
# Use compact attribute storage mode
|
||||
CONFIG_ESP_MATTER_NVS_USE_COMPACT_ATTR_STORAGE=y
|
||||
|
||||
# Enable HKDF in mbedtls
|
||||
CONFIG_MBEDTLS_HKDF_C=y
|
||||
|
||||
# Increase LwIP IPv6 address number to 6 (MAX_FABRIC + 1)
|
||||
# unique local addresses for fabrics(MAX_FABRIC), a link local address(1)
|
||||
CONFIG_LWIP_IPV6_NUM_ADDRESSES=6
|
||||
|
||||
# Enable OTA provider, use TestNet
|
||||
CONFIG_ESP_MATTER_OTA_PROVIDER_ENABLED=y
|
||||
CONFIG_ESP_MATTER_OTA_PROVIDER_DCL_TESTNET=y
|
||||
Reference in New Issue
Block a user