From 133d360591d47c7f3c2be4aa6fc893fea6e276b6 Mon Sep 17 00:00:00 2001 From: WanqQixiang Date: Mon, 9 Oct 2023 19:31:34 +0800 Subject: [PATCH] ota-provider: Add DCL OTA Provider support --- .../esp_matter_ota_provider/CMakeLists.txt | 15 + components/esp_matter_ota_provider/Kconfig | 52 ++ components/esp_matter_ota_provider/README.md | 21 + .../esp_matter_ota_provider/idf_component.yml | 3 + .../include/esp_matter_ota_bdx_sender.h | 76 +++ .../include/esp_matter_ota_provider.h | 119 +++++ .../esp_matter_ota_candidates.h | 46 ++ .../esp_matter_ota_http_downloader.h | 40 ++ .../src/esp_matter_ota_bdx_sender.cpp | 232 +++++++++ .../src/esp_matter_ota_candidates.cpp | 472 ++++++++++++++++++ .../src/esp_matter_ota_http_downloader.cpp | 184 +++++++ .../src/esp_matter_ota_provider.cpp | 355 +++++++++++++ examples/.build-rules.yml | 6 + examples/ota_provider/CMakeLists.txt | 42 ++ examples/ota_provider/README.md | 18 + examples/ota_provider/main/CMakeLists.txt | 5 + examples/ota_provider/main/app_main.cpp | 73 +++ examples/ota_provider/partitions.csv | 9 + examples/ota_provider/sdkconfig.defaults | 47 ++ 19 files changed, 1815 insertions(+) create mode 100644 components/esp_matter_ota_provider/CMakeLists.txt create mode 100644 components/esp_matter_ota_provider/Kconfig create mode 100644 components/esp_matter_ota_provider/README.md create mode 100644 components/esp_matter_ota_provider/idf_component.yml create mode 100644 components/esp_matter_ota_provider/include/esp_matter_ota_bdx_sender.h create mode 100644 components/esp_matter_ota_provider/include/esp_matter_ota_provider.h create mode 100644 components/esp_matter_ota_provider/private_include/esp_matter_ota_candidates.h create mode 100644 components/esp_matter_ota_provider/private_include/esp_matter_ota_http_downloader.h create mode 100644 components/esp_matter_ota_provider/src/esp_matter_ota_bdx_sender.cpp create mode 100644 components/esp_matter_ota_provider/src/esp_matter_ota_candidates.cpp create mode 100644 components/esp_matter_ota_provider/src/esp_matter_ota_http_downloader.cpp create mode 100644 components/esp_matter_ota_provider/src/esp_matter_ota_provider.cpp create mode 100644 examples/ota_provider/CMakeLists.txt create mode 100644 examples/ota_provider/README.md create mode 100644 examples/ota_provider/main/CMakeLists.txt create mode 100644 examples/ota_provider/main/app_main.cpp create mode 100644 examples/ota_provider/partitions.csv create mode 100644 examples/ota_provider/sdkconfig.defaults diff --git a/components/esp_matter_ota_provider/CMakeLists.txt b/components/esp_matter_ota_provider/CMakeLists.txt new file mode 100644 index 000000000..9e9721a77 --- /dev/null +++ b/components/esp_matter_ota_provider/CMakeLists.txt @@ -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) diff --git a/components/esp_matter_ota_provider/Kconfig b/components/esp_matter_ota_provider/Kconfig new file mode 100644 index 000000000..3ff2fa779 --- /dev/null +++ b/components/esp_matter_ota_provider/Kconfig @@ -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 diff --git a/components/esp_matter_ota_provider/README.md b/components/esp_matter_ota_provider/README.md new file mode 100644 index 000000000..bc4a85f16 --- /dev/null +++ b/components/esp_matter_ota_provider/README.md @@ -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. diff --git a/components/esp_matter_ota_provider/idf_component.yml b/components/esp_matter_ota_provider/idf_component.yml new file mode 100644 index 000000000..edf5cf7d4 --- /dev/null +++ b/components/esp_matter_ota_provider/idf_component.yml @@ -0,0 +1,3 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/json_parser: "~1.0.0" diff --git a/components/esp_matter_ota_provider/include/esp_matter_ota_bdx_sender.h b/components/esp_matter_ota_provider/include/esp_matter_ota_bdx_sender.h new file mode 100644 index 000000000..d96d1fc52 --- /dev/null +++ b/components/esp_matter_ota_provider/include/esp_matter_ota_bdx_sender.h @@ -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 +#include +#include +#include + +#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 mFabricIndex; + chip::Optional mNodeId; + + char mOtaImageUrl[OTA_URL_MAX_LEN]; + uint64_t mOtaImageSize; + esp_http_client_handle_t mHttpDownloader; +}; + +} // namespace ota_provider +} // namespace esp_matter diff --git a/components/esp_matter_ota_provider/include/esp_matter_ota_provider.h b/components/esp_matter_ota_provider/include/esp_matter_ota_provider.h new file mode 100644 index 000000000..5d74099d7 --- /dev/null +++ b/components/esp_matter_ota_provider/include/esp_matter_ota_provider.h @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +#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 diff --git a/components/esp_matter_ota_provider/private_include/esp_matter_ota_candidates.h b/components/esp_matter_ota_provider/private_include/esp_matter_ota_candidates.h new file mode 100644 index 000000000..d86788925 --- /dev/null +++ b/components/esp_matter_ota_provider/private_include/esp_matter_ota_candidates.h @@ -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 +#include + +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 diff --git a/components/esp_matter_ota_provider/private_include/esp_matter_ota_http_downloader.h b/components/esp_matter_ota_provider/private_include/esp_matter_ota_http_downloader.h new file mode 100644 index 000000000..1318714e9 --- /dev/null +++ b/components/esp_matter_ota_provider/private_include/esp_matter_ota_http_downloader.h @@ -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 +#include + +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 diff --git a/components/esp_matter_ota_provider/src/esp_matter_ota_bdx_sender.cpp b/components/esp_matter_ota_provider/src/esp_matter_ota_bdx_sender.cpp new file mode 100644 index 000000000..97aecebad --- /dev/null +++ b/components/esp_matter_ota_provider/src/esp_matter_ota_bdx_sender.cpp @@ -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 +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +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(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(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(std::min(static_cast(bytes_read), (mOtaImageSize - mNumBytesSent))); + blockData.IsEof = (blockData.Length < bytesToRead) || + (mNumBytesSent + static_cast(blockData.Length) == mOtaImageSize); + mNumBytesSent = static_cast(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(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 diff --git a/components/esp_matter_ota_provider/src/esp_matter_ota_candidates.cpp b/components/esp_matter_ota_provider/src/esp_matter_ota_candidates.cpp new file mode 100644 index 000000000..9fbf9c7d2 --- /dev/null +++ b/components/esp_matter_ota_provider/src/esp_matter_ota_candidates.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#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 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 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()); + 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()); + 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 diff --git a/components/esp_matter_ota_provider/src/esp_matter_ota_http_downloader.cpp b/components/esp_matter_ota_provider/src/esp_matter_ota_http_downloader.cpp new file mode 100644 index 000000000..881569d4c --- /dev/null +++ b/components/esp_matter_ota_provider/src/esp_matter_ota_http_downloader.cpp @@ -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 +#include +#include +#include +#include +#include + +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 diff --git a/components/esp_matter_ota_provider/src/esp_matter_ota_provider.cpp b/components/esp_matter_ota_provider/src/esp_matter_ota_provider.cpp new file mode 100644 index 000000000..ddcf8b30a --- /dev/null +++ b/components/esp_matter_ota_provider/src/esp_matter_ota_provider.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +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(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 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(); + 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 diff --git a/examples/.build-rules.yml b/examples/.build-rules.yml index c4c093664..111d44436 100644 --- a/examples/.build-rules.yml +++ b/examples/.build-rules.yml @@ -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 diff --git a/examples/ota_provider/CMakeLists.txt b/examples/ota_provider/CMakeLists.txt new file mode 100644 index 000000000..5d9511621 --- /dev/null +++ b/examples/ota_provider/CMakeLists.txt @@ -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) diff --git a/examples/ota_provider/README.md b/examples/ota_provider/README.md new file mode 100644 index 000000000..59fb1100d --- /dev/null +++ b/examples/ota_provider/README.md @@ -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). diff --git a/examples/ota_provider/main/CMakeLists.txt b/examples/ota_provider/main/CMakeLists.txt new file mode 100644 index 000000000..4692938c0 --- /dev/null +++ b/examples/ota_provider/main/CMakeLists.txt @@ -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") diff --git a/examples/ota_provider/main/app_main.cpp b/examples/ota_provider/main/app_main.cpp new file mode 100644 index 000000000..913f4407e --- /dev/null +++ b/examples/ota_provider/main/app_main.cpp @@ -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 +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include + +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(&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 +} diff --git a/examples/ota_provider/partitions.csv b/examples/ota_provider/partitions.csv new file mode 100644 index 000000000..ba4a188a4 --- /dev/null +++ b/examples/ota_provider/partitions.csv @@ -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, diff --git a/examples/ota_provider/sdkconfig.defaults b/examples/ota_provider/sdkconfig.defaults new file mode 100644 index 000000000..df304dadb --- /dev/null +++ b/examples/ota_provider/sdkconfig.defaults @@ -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