ota-provider: Add DCL OTA Provider support

This commit is contained in:
WanqQixiang
2023-10-09 19:31:34 +08:00
parent 489d78196e
commit 133d360591
19 changed files with 1815 additions and 0 deletions
@@ -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
+6
View File
@@ -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
+42
View File
@@ -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)
+18
View File
@@ -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")
+73
View File
@@ -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
}
+9
View File
@@ -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,
1 # Name, Type, SubType, Offset, Size, Flags
2 # Note: Firmware partition offset needs to be 64K aligned, initial 36K (9 sectors) are reserved for bootloader and partition table
3 esp_secure_cert, 0x3F, ,0xd000, 0x2000, encrypted
4 nvs, data, nvs, 0x10000, 0xC000,
5 nvs_keys, data, nvs_keys,, 0x1000, encrypted
6 otadata, data, ota, , 0x2000
7 phy_init, data, phy, , 0x1000,
8 ota_0, app, ota_0, 0x20000, 0x1E0000,
9 ota_1, app, ota_1, 0x200000, 0x1E0000,
+47
View File
@@ -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