From 4cb5a599714cd2fe0f7d9673dcb20235f82f432b Mon Sep 17 00:00:00 2001 From: zekageri Date: Tue, 10 Mar 2026 13:11:37 +0100 Subject: [PATCH] chore: align formatter baseline with esptoolkit-template --- .clang-format | 11 + .editorconfig | 11 + .gitignore | 1 - .vscode/bin/clang-format | 19 + .vscode/extensions.json | 9 + .vscode/settings.json | 30 + .vscode/tasks.json | 12 + README.md | 7 + examples/basic_fetch/basic_fetch.ino | 127 +- scripts/format_cpp.sh | 24 + src/esp_fetch/fetch.cpp | 1501 ++++++++++++------------ src/esp_fetch/fetch.h | 233 ++-- src/esp_fetch/fetch_allocator.h | 104 +- test/test_esp_fetch/test_esp_fetch.cpp | 188 ++- 14 files changed, 1255 insertions(+), 1022 deletions(-) create mode 100644 .clang-format create mode 100644 .editorconfig create mode 100755 .vscode/bin/clang-format create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100755 scripts/format_cpp.sh diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..8450693 --- /dev/null +++ b/.clang-format @@ -0,0 +1,11 @@ +BasedOnStyle: LLVM +ColumnLimit: 100 +BinPackArguments: false +BinPackParameters: false +AllowAllArgumentsOnNextLine: false +AlignAfterOpenBracket: BlockIndent +UseTab: ForIndentation +IndentWidth: 4 +TabWidth: 4 +ContinuationIndentWidth: 4 +AllowShortFunctionsOnASingleLine: None diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d89c76d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +[*.{c,cc,cpp,h,hpp,ino}] +indent_style = tab +indent_size = tab +tab_width = 4 diff --git a/.gitignore b/.gitignore index 78f49b6..6346d5c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ .venv build/ build_prev_runner/ -.vscode \ No newline at end of file diff --git a/.vscode/bin/clang-format b/.vscode/bin/clang-format new file mode 100755 index 0000000..0df371f --- /dev/null +++ b/.vscode/bin/clang-format @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if command -v clang-format >/dev/null 2>&1; then + exec clang-format "$@" +fi + +_home_dir="${HOME:-}" +if [ -n "$_home_dir" ]; then + _candidate="$(ls -1d "$_home_dir"/.vscode/extensions/ms-vscode.cpptools-*-linux-x64/LLVM/bin/clang-format 2>/dev/null | tail -n 1 || true)" + if [ -n "$_candidate" ] && [ -x "$_candidate" ]; then + exec "$_candidate" "$@" + fi +fi + +echo "clang-format executable not found." >&2 +echo "Install clang-format system-wide or install/update ms-vscode.cpptools." >&2 +exit 127 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..f814711 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "pioarduino.pioarduino-ide", + "xaver.clang-format" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..24368c8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,30 @@ +{ + "files.associations": { + "*.ino": "cpp" + }, + "editor.defaultFormatter": "xaver.clang-format", + "C_Cpp.formatting": "Disabled", + "clang-format.style": "file", + "clang-format.executable": "${workspaceRoot}/.vscode/bin/clang-format", + "[cpp]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + }, + "[c]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + }, + "[arduino]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..20e66d5 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,12 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Format Firmware Sources", + "type": "shell", + "command": "bash ${workspaceFolder}/scripts/format_cpp.sh", + "group": "build", + "problemMatcher": [] + } + ] +} diff --git a/README.md b/README.md index 676ad94..e3a7819 100644 --- a/README.md +++ b/README.md @@ -377,6 +377,13 @@ struct StreamResult { --- +## Formatting Baseline + +This repository follows the firmware formatting baseline from `esptoolkit-template`: +- `.clang-format` is the source of truth for C/C++/INO layout. +- `.editorconfig` enforces tabs (`tab_width = 4`), LF endings, and final newline. +- Format all tracked firmware sources with `bash scripts/format_cpp.sh`. + ## License MIT — see [LICENSE.md](LICENSE.md) diff --git a/examples/basic_fetch/basic_fetch.ino b/examples/basic_fetch/basic_fetch.ino index 1f1134a..fa03f94 100644 --- a/examples/basic_fetch/basic_fetch.ino +++ b/examples/basic_fetch/basic_fetch.ino @@ -8,65 +8,88 @@ const char *POST_URL = "https://httpbin.org/post"; const char *GET_URL = "https://httpbin.org/get"; void setup() { - Serial.begin(115200); - delay(200); + Serial.begin(115200); + delay(200); - FetchConfig cfg; - cfg.maxConcurrentRequests = 2; - cfg.stackSize = 6144; - cfg.priority = 4; - cfg.defaultTimeoutMs = 12000; - fetch.init(cfg); + FetchConfig cfg; + cfg.maxConcurrentRequests = 2; + cfg.stackSize = 6144; + cfg.priority = 4; + cfg.defaultTimeoutMs = 12000; + fetch.init(cfg); - JsonDocument payload; - payload["hello"] = "world"; + JsonDocument payload; + payload["hello"] = "world"; - bool posting = fetch.post(POST_URL, payload, [](JsonDocument result) { - if (!result["error"].isNull()) { - ESP_LOGE("FETCH_DEMO", "async post failed: %s", result["error"]["message"].as()); - return; - } - ESP_LOGI("FETCH_DEMO", "async post status %d body len %u", - result["status"].as(), - result["body"].as().length()); - }); - if (!posting) { - ESP_LOGE("FETCH_DEMO", "Failed to start http post"); - } + bool posting = fetch.post(POST_URL, payload, [](JsonDocument result) { + if (!result["error"].isNull()) { + ESP_LOGE( + "FETCH_DEMO", + "async post failed: %s", + result["error"]["message"].as() + ); + return; + } + ESP_LOGI( + "FETCH_DEMO", + "async post status %d body len %u", + result["status"].as(), + result["body"].as().length() + ); + }); + if (!posting) { + ESP_LOGE("FETCH_DEMO", "Failed to start http post"); + } - FetchRequestOptions opts; - opts.headers.push_back({"Accept", "application/json"}); - bool getting = fetch.get(GET_URL, [](JsonDocument result) { - if (!result["error"].isNull()) { - ESP_LOGE("FETCH_DEMO", "async get failed: %s", result["error"]["message"].as()); - return; - } - Serial.printf("Server: %s\n", result["headers"]["server"].as()); - }, opts); - if (!getting) { - ESP_LOGE("FETCH_DEMO", "Failed to start http get"); - } + FetchRequestOptions opts; + opts.headers.push_back({"Accept", "application/json"}); + bool getting = fetch.get( + GET_URL, + [](JsonDocument result) { + if (!result["error"].isNull()) { + ESP_LOGE( + "FETCH_DEMO", + "async get failed: %s", + result["error"]["message"].as() + ); + return; + } + Serial.printf("Server: %s\n", result["headers"]["server"].as()); + }, + opts + ); + if (!getting) { + ESP_LOGE("FETCH_DEMO", "Failed to start http get"); + } - JsonDocument postResult = fetch.post(POST_URL, payload, portMAX_DELAY); - if (!postResult["error"].isNull()) { - ESP_LOGW("FETCH_DEMO", "sync post failed: %s", postResult["error"]["message"].as()); - } else { - Serial.printf("Sync POST status %d\n", postResult["status"].as()); - } + JsonDocument postResult = fetch.post(POST_URL, payload, portMAX_DELAY); + if (!postResult["error"].isNull()) { + ESP_LOGW( + "FETCH_DEMO", + "sync post failed: %s", + postResult["error"]["message"].as() + ); + } else { + Serial.printf("Sync POST status %d\n", postResult["status"].as()); + } - JsonDocument getResult = fetch.get(GET_URL, portMAX_DELAY); - if (!getResult["error"].isNull()) { - ESP_LOGW("FETCH_DEMO", "sync get failed: %s", getResult["error"]["message"].as()); - } else { - Serial.printf("Your IP: %s\n", getResult["body"].as()); - } + JsonDocument getResult = fetch.get(GET_URL, portMAX_DELAY); + if (!getResult["error"].isNull()) { + ESP_LOGW( + "FETCH_DEMO", + "sync get failed: %s", + getResult["error"]["message"].as() + ); + } else { + Serial.printf("Your IP: %s\n", getResult["body"].as()); + } } void loop() { - if (!deinitialized && fetch.isInitialized() && millis() > 15000UL) { - fetch.deinit(); - deinitialized = true; - ESP_LOGI("FETCH_DEMO", "ESPFetch deinitialized"); - } - vTaskDelay(pdMS_TO_TICKS(1000)); + if (!deinitialized && fetch.isInitialized() && millis() > 15000UL) { + fetch.deinit(); + deinitialized = true; + ESP_LOGI("FETCH_DEMO", "ESPFetch deinitialized"); + } + vTaskDelay(pdMS_TO_TICKS(1000)); } diff --git a/scripts/format_cpp.sh b/scripts/format_cpp.sh new file mode 100755 index 0000000..7d17b04 --- /dev/null +++ b/scripts/format_cpp.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -euo pipefail + +_repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +_clang_format="${_repo_root}/.vscode/bin/clang-format" + +if [ ! -x "${_clang_format}" ]; then + echo "clang-format wrapper not found: ${_clang_format}" >&2 + exit 1 +fi + +mapfile -d '' _format_files < <( + git -C "${_repo_root}" ls-files -z -- '*.c' '*.cc' '*.cpp' '*.h' '*.hpp' '*.ino' +) + +if [ "${#_format_files[@]}" -eq 0 ]; then + echo "No tracked C/C++/INO files found to format." + exit 0 +fi + +"${_clang_format}" -i --style=file "${_format_files[@]}" + +echo "Formatted ${#_format_files[@]} files." diff --git a/src/esp_fetch/fetch.cpp b/src/esp_fetch/fetch.cpp index ec97065..dfde447 100644 --- a/src/esp_fetch/fetch.cpp +++ b/src/esp_fetch/fetch.cpp @@ -16,826 +16,889 @@ namespace { constexpr const char *TAG = "ESPFetch"; struct InternalFetchHeader { - FetchString name; - FetchString value; - - InternalFetchHeader(const char *headerName, const char *headerValue, const FetchAllocator &allocator) - : name(headerName ? headerName : "", allocator), - value(headerValue ? headerValue : "", allocator) {} + FetchString name; + FetchString value; + + InternalFetchHeader( + const char *headerName, const char *headerValue, const FetchAllocator &allocator + ) + : name(headerName ? headerName : "", allocator), + value(headerValue ? headerValue : "", allocator) { + } }; using InternalFetchHeaderVector = FetchVector; -template -bool equalsIgnoreCase(const TString &lhs, const char *rhs) { - if (!rhs) { - return false; - } - const size_t rhsLen = std::strlen(rhs); - if (lhs.size() != rhsLen) { - return false; - } - for (size_t i = 0; i < lhs.size(); ++i) { - if (std::tolower(static_cast(lhs[i])) != - std::tolower(static_cast(rhs[i]))) { - return false; - } - } - return true; +template bool equalsIgnoreCase(const TString &lhs, const char *rhs) { + if (!rhs) { + return false; + } + const size_t rhsLen = std::strlen(rhs); + if (lhs.size() != rhsLen) { + return false; + } + for (size_t i = 0; i < lhs.size(); ++i) { + if (std::tolower(static_cast(lhs[i])) != + std::tolower(static_cast(rhs[i]))) { + return false; + } + } + return true; } struct InternalFetchRequestOptions { - explicit InternalFetchRequestOptions(bool usePSRAMBuffers = false) - : charAllocator(usePSRAMBuffers), - headerAllocator(usePSRAMBuffers), - headers(headerAllocator) {} - - FetchAllocator charAllocator; - FetchAllocator headerAllocator; - - uint32_t timeoutMs = 0; - size_t maxBodyBytes = 0; - size_t maxHeaderBytes = 0; - size_t rxBufferSize = 0; - size_t txBufferSize = 0; - bool skipTlsCommonNameCheck = false; - bool allowRedirects = true; - InternalFetchHeaderVector headers; - const char *contentType = nullptr; + explicit InternalFetchRequestOptions(bool usePSRAMBuffers = false) + : charAllocator(usePSRAMBuffers), headerAllocator(usePSRAMBuffers), + headers(headerAllocator) { + } + + FetchAllocator charAllocator; + FetchAllocator headerAllocator; + + uint32_t timeoutMs = 0; + size_t maxBodyBytes = 0; + size_t maxHeaderBytes = 0; + size_t rxBufferSize = 0; + size_t txBufferSize = 0; + bool skipTlsCommonNameCheck = false; + bool allowRedirects = true; + InternalFetchHeaderVector headers; + const char *contentType = nullptr; }; struct FetchStringWriter { - explicit FetchStringWriter(FetchString &target) : target_(target) {} - - size_t write(uint8_t value) { - target_.push_back(static_cast(value)); - return 1; - } - - size_t write(const uint8_t *buffer, size_t size) { - if (!buffer || size == 0) { - return 0; - } - target_.append(reinterpret_cast(buffer), size); - return size; - } + explicit FetchStringWriter(FetchString &target) : target_(target) { + } + + size_t write(uint8_t value) { + target_.push_back(static_cast(value)); + return 1; + } + + size_t write(const uint8_t *buffer, size_t size) { + if (!buffer || size == 0) { + return 0; + } + target_.append(reinterpret_cast(buffer), size); + return size; + } private: - FetchString &target_; + FetchString &target_; }; bool startsWithIgnoreCase(const std::string &value, const char *prefix) { - if (!prefix) { - return false; - } - const size_t prefixLen = std::strlen(prefix); - if (value.size() < prefixLen) { - return false; - } - for (size_t i = 0; i < prefixLen; ++i) { - if (std::tolower(static_cast(value[i])) != - std::tolower(static_cast(prefix[i]))) { - return false; - } - } - return true; + if (!prefix) { + return false; + } + const size_t prefixLen = std::strlen(prefix); + if (value.size() < prefixLen) { + return false; + } + for (size_t i = 0; i < prefixLen; ++i) { + if (std::tolower(static_cast(value[i])) != + std::tolower(static_cast(prefix[i]))) { + return false; + } + } + return true; } std::string trimUrl(const std::string &value) { - size_t start = 0; - while (start < value.size() && std::isspace(static_cast(value[start]))) { - ++start; - } - size_t end = value.size(); - while (end > start && std::isspace(static_cast(value[end - 1]))) { - --end; - } - return value.substr(start, end - start); + size_t start = 0; + while (start < value.size() && std::isspace(static_cast(value[start]))) { + ++start; + } + size_t end = value.size(); + while (end > start && std::isspace(static_cast(value[end - 1]))) { + --end; + } + return value.substr(start, end - start); } bool normalizeScheme(std::string &url, const char *scheme) { - const size_t schemeLen = std::strlen(scheme); - if (url.size() <= schemeLen || !startsWithIgnoreCase(url, scheme)) { - return false; - } - if (url[schemeLen] != ':') { - return false; - } - - size_t slashPos = schemeLen + 1; - size_t slashCount = 0; - while (slashPos + slashCount < url.size() && url[slashPos + slashCount] == '/') { - ++slashCount; - } - if (slashCount == 2) { - return false; - } - if (slashCount > 2) { - url.erase(slashPos + 2, slashCount - 2); - return true; - } - if (slashCount == 1) { - url.insert(slashPos, "/"); - return true; - } - url.insert(slashPos, "//"); - return true; + const size_t schemeLen = std::strlen(scheme); + if (url.size() <= schemeLen || !startsWithIgnoreCase(url, scheme)) { + return false; + } + if (url[schemeLen] != ':') { + return false; + } + + size_t slashPos = schemeLen + 1; + size_t slashCount = 0; + while (slashPos + slashCount < url.size() && url[slashPos + slashCount] == '/') { + ++slashCount; + } + if (slashCount == 2) { + return false; + } + if (slashCount > 2) { + url.erase(slashPos + 2, slashCount - 2); + return true; + } + if (slashCount == 1) { + url.insert(slashPos, "/"); + return true; + } + url.insert(slashPos, "//"); + return true; } bool stripLeadingHostColon(std::string &url) { - const size_t schemePos = url.find("://"); - if (schemePos == std::string::npos) { - return false; - } - if (!startsWithIgnoreCase(url, "http") && !startsWithIgnoreCase(url, "https")) { - return false; - } - const size_t hostPos = schemePos + 3; - if (hostPos >= url.size() || url[hostPos] != ':') { - return false; - } - url.erase(hostPos, 1); - return true; + const size_t schemePos = url.find("://"); + if (schemePos == std::string::npos) { + return false; + } + if (!startsWithIgnoreCase(url, "http") && !startsWithIgnoreCase(url, "https")) { + return false; + } + const size_t hostPos = schemePos + 3; + if (hostPos >= url.size() || url[hostPos] != ':') { + return false; + } + url.erase(hostPos, 1); + return true; } std::string normalizeUrl(const std::string &url) { - std::string normalized = trimUrl(url); - bool changed = false; - changed = normalizeScheme(normalized, "https") || changed; - changed = normalizeScheme(normalized, "http") || changed; - changed = stripLeadingHostColon(normalized) || changed; - if (changed) { - ESP_LOGW(TAG, "Normalized URL to %s", normalized.c_str()); - } - return normalized; + std::string normalized = trimUrl(url); + bool changed = false; + changed = normalizeScheme(normalized, "https") || changed; + changed = normalizeScheme(normalized, "http") || changed; + changed = stripLeadingHostColon(normalized) || changed; + if (changed) { + ESP_LOGW(TAG, "Normalized URL to %s", normalized.c_str()); + } + return normalized; } int resolveHttpBufferSize(const char *label, size_t requestValue, size_t configValue) { - const size_t selected = requestValue ? requestValue : configValue; - if (selected == 0) { - return 0; - } - if (selected > static_cast(INT_MAX)) { - ESP_LOGW(TAG, "%s exceeds INT_MAX, clamping to %d bytes", label, INT_MAX); - return INT_MAX; - } - return static_cast(selected); + const size_t selected = requestValue ? requestValue : configValue; + if (selected == 0) { + return 0; + } + if (selected > static_cast(INT_MAX)) { + ESP_LOGW(TAG, "%s exceeds INT_MAX, clamping to %d bytes", label, INT_MAX); + return INT_MAX; + } + return static_cast(selected); } -} // namespace +} // namespace struct ESPFetch::FetchResponse { - explicit FetchResponse(bool usePSRAMBuffers = false) - : charAllocator(usePSRAMBuffers), - headerAllocator(usePSRAMBuffers), - body(charAllocator), - headers(headerAllocator) {} - - esp_err_t error = ESP_OK; - int statusCode = 0; - FetchAllocator charAllocator; - FetchAllocator headerAllocator; - FetchString body; - InternalFetchHeaderVector headers; - bool bodyTruncated = false; - bool headersTruncated = false; - int64_t durationUs = 0; + explicit FetchResponse(bool usePSRAMBuffers = false) + : charAllocator(usePSRAMBuffers), headerAllocator(usePSRAMBuffers), body(charAllocator), + headers(headerAllocator) { + } + + esp_err_t error = ESP_OK; + int statusCode = 0; + FetchAllocator charAllocator; + FetchAllocator headerAllocator; + FetchString body; + InternalFetchHeaderVector headers; + bool bodyTruncated = false; + bool headersTruncated = false; + int64_t durationUs = 0; }; struct ESPFetch::SyncHandle { - SyncHandle() = default; - ~SyncHandle() { - if (done) { - vSemaphoreDelete(done); - done = nullptr; - } - } - - SemaphoreHandle_t done = nullptr; - bool ready = false; - JsonDocument doc; + SyncHandle() = default; + ~SyncHandle() { + if (done) { + vSemaphoreDelete(done); + done = nullptr; + } + } + + SemaphoreHandle_t done = nullptr; + bool ready = false; + JsonDocument doc; }; struct ESPFetch::FetchJob { - explicit FetchJob(bool usePSRAMBuffers = false) - : stringAllocator(usePSRAMBuffers), - url(stringAllocator), - body(stringAllocator), - requestOptions(usePSRAMBuffers), - response(usePSRAMBuffers) {} - - ESPFetch *owner = nullptr; - FetchAllocator stringAllocator; - FetchString url; - esp_http_client_method_t method = HTTP_METHOD_GET; - FetchString body; - InternalFetchRequestOptions requestOptions; - - // JSON mode callback (existing APIs) - FetchCallback callback; - std::shared_ptr syncHandle; - - // Limits (used differently depending on mode) - size_t bodyLimit = 0; - size_t headerLimit = 0; - - // Response bookkeeping - FetchResponse response; - - // Stream mode (new APIs) - bool isStream = false; - FetchChunkCallback onChunk; - FetchStreamCallback onDone; - size_t receivedBytes = 0; - esp_err_t streamAbortError = ESP_OK; + explicit FetchJob(bool usePSRAMBuffers = false) + : stringAllocator(usePSRAMBuffers), url(stringAllocator), body(stringAllocator), + requestOptions(usePSRAMBuffers), response(usePSRAMBuffers) { + } + + ESPFetch *owner = nullptr; + FetchAllocator stringAllocator; + FetchString url; + esp_http_client_method_t method = HTTP_METHOD_GET; + FetchString body; + InternalFetchRequestOptions requestOptions; + + // JSON mode callback (existing APIs) + FetchCallback callback; + std::shared_ptr syncHandle; + + // Limits (used differently depending on mode) + size_t bodyLimit = 0; + size_t headerLimit = 0; + + // Response bookkeeping + FetchResponse response; + + // Stream mode (new APIs) + bool isStream = false; + FetchChunkCallback onChunk; + FetchStreamCallback onDone; + size_t receivedBytes = 0; + esp_err_t streamAbortError = ESP_OK; }; ESPFetch::~ESPFetch() { - deinit(); + deinit(); } bool ESPFetch::init(const FetchConfig &config) { - if (isInitialized()) { - deinit(); - } - - if (config.maxConcurrentRequests == 0) { - ESP_LOGE(TAG, "maxConcurrentRequests must be > 0"); - return false; - } - - _config = config; - _slotSemaphore = xSemaphoreCreateCounting(_config.maxConcurrentRequests, _config.maxConcurrentRequests); - if (!_slotSemaphore) { - ESP_LOGE(TAG, "Failed to create fetch semaphore"); - return false; - } - - _teardownRequested.store(false, std::memory_order_release); - _initialized.store(true, std::memory_order_release); - return true; + if (isInitialized()) { + deinit(); + } + + if (config.maxConcurrentRequests == 0) { + ESP_LOGE(TAG, "maxConcurrentRequests must be > 0"); + return false; + } + + _config = config; + _slotSemaphore = + xSemaphoreCreateCounting(_config.maxConcurrentRequests, _config.maxConcurrentRequests); + if (!_slotSemaphore) { + ESP_LOGE(TAG, "Failed to create fetch semaphore"); + return false; + } + + _teardownRequested.store(false, std::memory_order_release); + _initialized.store(true, std::memory_order_release); + return true; } void ESPFetch::deinit() { - if (!isInitialized() && _activeTasks.load(std::memory_order_acquire) == 0 && _slotSemaphore == nullptr) { - return; - } + if (!isInitialized() && _activeTasks.load(std::memory_order_acquire) == 0 && + _slotSemaphore == nullptr) { + return; + } - _teardownRequested.store(true, std::memory_order_release); - _initialized.store(false, std::memory_order_release); + _teardownRequested.store(true, std::memory_order_release); + _initialized.store(false, std::memory_order_release); - while (_activeTasks.load(std::memory_order_acquire) > 0) { + while (_activeTasks.load(std::memory_order_acquire) > 0) { #if defined(INCLUDE_xTaskGetSchedulerState) && (INCLUDE_xTaskGetSchedulerState == 1) - if (xTaskGetSchedulerState() == taskSCHEDULER_NOT_STARTED) { - break; - } + if (xTaskGetSchedulerState() == taskSCHEDULER_NOT_STARTED) { + break; + } #endif - vTaskDelay(pdMS_TO_TICKS(1)); - } + vTaskDelay(pdMS_TO_TICKS(1)); + } - if (_slotSemaphore) { - vSemaphoreDelete(_slotSemaphore); - _slotSemaphore = nullptr; - } + if (_slotSemaphore) { + vSemaphoreDelete(_slotSemaphore); + _slotSemaphore = nullptr; + } - _teardownRequested.store(false, std::memory_order_release); + _teardownRequested.store(false, std::memory_order_release); } bool ESPFetch::isInitialized() const { - return _initialized.load(std::memory_order_acquire); + return _initialized.load(std::memory_order_acquire); } bool ESPFetch::get(const char *url, FetchCallback callback, const FetchRequestOptions &options) { - if (!url) { - return false; - } - return enqueueRequest( - url, - HTTP_METHOD_GET, - FetchString{FetchAllocator(_config.usePSRAMBuffers)}, - std::move(callback), - nullptr, - options); + if (!url) { + return false; + } + return enqueueRequest( + url, + HTTP_METHOD_GET, + FetchString{FetchAllocator(_config.usePSRAMBuffers)}, + std::move(callback), + nullptr, + options + ); } bool ESPFetch::get(const String &url, FetchCallback callback, const FetchRequestOptions &options) { - return get(url.c_str(), std::move(callback), options); + return get(url.c_str(), std::move(callback), options); } -JsonDocument ESPFetch::get(const char *url, TickType_t waitTicks, const FetchRequestOptions &options) { - if (!url) { - JsonDocument doc; - doc["ok"] = false; - doc["error"]["message"] = "url is null"; - return doc; - } - auto handle = std::make_shared(); - handle->done = xSemaphoreCreateBinary(); - if (!handle->done) { - JsonDocument doc; - doc["ok"] = false; - doc["error"]["message"] = "failed to allocate sync semaphore"; - return doc; - } - - if (!enqueueRequest(url, - HTTP_METHOD_GET, - FetchString{FetchAllocator(_config.usePSRAMBuffers)}, - nullptr, - handle, - options)) { - JsonDocument doc; - doc["ok"] = false; - doc["error"]["message"] = "failed to start http get"; - return doc; - } - - return waitForResult(handle, waitTicks); +JsonDocument +ESPFetch::get(const char *url, TickType_t waitTicks, const FetchRequestOptions &options) { + if (!url) { + JsonDocument doc; + doc["ok"] = false; + doc["error"]["message"] = "url is null"; + return doc; + } + auto handle = std::make_shared(); + handle->done = xSemaphoreCreateBinary(); + if (!handle->done) { + JsonDocument doc; + doc["ok"] = false; + doc["error"]["message"] = "failed to allocate sync semaphore"; + return doc; + } + + if (!enqueueRequest( + url, + HTTP_METHOD_GET, + FetchString{FetchAllocator(_config.usePSRAMBuffers)}, + nullptr, + handle, + options + )) { + JsonDocument doc; + doc["ok"] = false; + doc["error"]["message"] = "failed to start http get"; + return doc; + } + + return waitForResult(handle, waitTicks); } -JsonDocument ESPFetch::get(const String &url, TickType_t waitTicks, const FetchRequestOptions &options) { - return get(url.c_str(), waitTicks, options); +JsonDocument +ESPFetch::get(const String &url, TickType_t waitTicks, const FetchRequestOptions &options) { + return get(url.c_str(), waitTicks, options); } -bool ESPFetch::post(const char *url, - const JsonDocument &payload, - FetchCallback callback, - const FetchRequestOptions &options) { - if (!url) { - return false; - } - FetchString body{FetchAllocator(_config.usePSRAMBuffers)}; - FetchStringWriter writer(body); - serializeJson(payload, writer); - return enqueueRequest(url, HTTP_METHOD_POST, std::move(body), std::move(callback), nullptr, options); +bool ESPFetch::post( + const char *url, + const JsonDocument &payload, + FetchCallback callback, + const FetchRequestOptions &options +) { + if (!url) { + return false; + } + FetchString body{FetchAllocator(_config.usePSRAMBuffers)}; + FetchStringWriter writer(body); + serializeJson(payload, writer); + return enqueueRequest( + url, + HTTP_METHOD_POST, + std::move(body), + std::move(callback), + nullptr, + options + ); } -bool ESPFetch::post(const String &url, - const JsonDocument &payload, - FetchCallback callback, - const FetchRequestOptions &options) { - return post(url.c_str(), payload, std::move(callback), options); +bool ESPFetch::post( + const String &url, + const JsonDocument &payload, + FetchCallback callback, + const FetchRequestOptions &options +) { + return post(url.c_str(), payload, std::move(callback), options); } -JsonDocument ESPFetch::post(const char *url, - const JsonDocument &payload, - TickType_t waitTicks, - const FetchRequestOptions &options) { - if (!url) { - JsonDocument doc; - doc["ok"] = false; - doc["error"]["message"] = "url is null"; - return doc; - } - FetchString body{FetchAllocator(_config.usePSRAMBuffers)}; - FetchStringWriter writer(body); - serializeJson(payload, writer); - - auto handle = std::make_shared(); - handle->done = xSemaphoreCreateBinary(); - if (!handle->done) { - JsonDocument doc; - doc["ok"] = false; - doc["error"]["message"] = "failed to allocate sync semaphore"; - return doc; - } - - if (!enqueueRequest(url, HTTP_METHOD_POST, std::move(body), nullptr, handle, options)) { - JsonDocument doc; - doc["ok"] = false; - doc["error"]["message"] = "failed to start http post"; - return doc; - } - - return waitForResult(handle, waitTicks); +JsonDocument ESPFetch::post( + const char *url, + const JsonDocument &payload, + TickType_t waitTicks, + const FetchRequestOptions &options +) { + if (!url) { + JsonDocument doc; + doc["ok"] = false; + doc["error"]["message"] = "url is null"; + return doc; + } + FetchString body{FetchAllocator(_config.usePSRAMBuffers)}; + FetchStringWriter writer(body); + serializeJson(payload, writer); + + auto handle = std::make_shared(); + handle->done = xSemaphoreCreateBinary(); + if (!handle->done) { + JsonDocument doc; + doc["ok"] = false; + doc["error"]["message"] = "failed to allocate sync semaphore"; + return doc; + } + + if (!enqueueRequest(url, HTTP_METHOD_POST, std::move(body), nullptr, handle, options)) { + JsonDocument doc; + doc["ok"] = false; + doc["error"]["message"] = "failed to start http post"; + return doc; + } + + return waitForResult(handle, waitTicks); } -JsonDocument ESPFetch::post(const String &url, - const JsonDocument &payload, - TickType_t waitTicks, - const FetchRequestOptions &options) { - return post(url.c_str(), payload, waitTicks, options); +JsonDocument ESPFetch::post( + const String &url, + const JsonDocument &payload, + TickType_t waitTicks, + const FetchRequestOptions &options +) { + return post(url.c_str(), payload, waitTicks, options); } // ------------------------------ // Stream API (new) // ------------------------------ -bool ESPFetch::getStream(const char *url, - FetchChunkCallback onChunk, - FetchStreamCallback onDone, - const FetchRequestOptions &options) { - if (!url || !onChunk) { - return false; - } - return enqueueStreamRequest(url, std::move(onChunk), std::move(onDone), options); +bool ESPFetch::getStream( + const char *url, + FetchChunkCallback onChunk, + FetchStreamCallback onDone, + const FetchRequestOptions &options +) { + if (!url || !onChunk) { + return false; + } + return enqueueStreamRequest(url, std::move(onChunk), std::move(onDone), options); } -bool ESPFetch::getStream(const String &url, - FetchChunkCallback onChunk, - FetchStreamCallback onDone, - const FetchRequestOptions &options) { - return getStream(url.c_str(), std::move(onChunk), std::move(onDone), options); +bool ESPFetch::getStream( + const String &url, + FetchChunkCallback onChunk, + FetchStreamCallback onDone, + const FetchRequestOptions &options +) { + return getStream(url.c_str(), std::move(onChunk), std::move(onDone), options); } -bool ESPFetch::enqueueRequest(const std::string &url, - esp_http_client_method_t method, - FetchString &&body, - FetchCallback callback, - std::shared_ptr syncHandle, - const FetchRequestOptions &options) { - if (!isInitialized()) { - ESP_LOGE(TAG, "ESPFetch not initialized"); - return false; - } - - if (xSemaphoreTake(_slotSemaphore, _config.slotAcquireTicks) != pdTRUE) { - ESP_LOGW(TAG, "No available fetch slots"); - return false; - } - - auto job = std::make_unique(_config.usePSRAMBuffers); - job->owner = this; - const std::string normalizedUrl = normalizeUrl(url); - job->url.assign(normalizedUrl.c_str(), normalizedUrl.size()); - job->method = method; - job->body = std::move(body); - job->requestOptions.timeoutMs = options.timeoutMs; - job->requestOptions.maxBodyBytes = options.maxBodyBytes; - job->requestOptions.maxHeaderBytes = options.maxHeaderBytes; - job->requestOptions.rxBufferSize = options.rxBufferSize; - job->requestOptions.txBufferSize = options.txBufferSize; - job->requestOptions.skipTlsCommonNameCheck = options.skipTlsCommonNameCheck; - job->requestOptions.allowRedirects = options.allowRedirects; - job->requestOptions.contentType = options.contentType; - job->requestOptions.headers.clear(); - job->requestOptions.headers.reserve(options.headers.size()); - for (const auto &header : options.headers) { - job->requestOptions.headers.emplace_back(header.name.c_str(), header.value.c_str(), job->stringAllocator); - } - job->callback = std::move(callback); - job->syncHandle = std::move(syncHandle); - - job->bodyLimit = job->requestOptions.maxBodyBytes ? job->requestOptions.maxBodyBytes : _config.maxBodyBytes; - job->headerLimit = - job->requestOptions.maxHeaderBytes ? job->requestOptions.maxHeaderBytes : _config.maxHeaderBytes; - if (job->bodyLimit == 0) { - job->bodyLimit = std::numeric_limits::max(); - } - if (job->headerLimit == 0) { - job->headerLimit = std::numeric_limits::max(); - } - - size_t reserveBytes = - job->bodyLimit == std::numeric_limits::max() - ? static_cast(1024) - : std::min(job->bodyLimit, static_cast(1024)); - job->response.body.reserve(reserveBytes); - - size_t stackSize = _config.stackSize; - if (stackSize == 0) { - ESP_LOGE(TAG, "Invalid stack size for fetch worker"); - xSemaphoreGive(_slotSemaphore); - return false; - } - - _activeTasks.fetch_add(1, std::memory_order_acq_rel); - - FetchJob *jobPtr = job.release(); - TaskHandle_t taskHandle = nullptr; - const BaseType_t created = xTaskCreatePinnedToCore( - &ESPFetch::requestTask, "esp-fetch", stackSize, jobPtr, _config.priority, &taskHandle, _config.coreId); - if (created != pdPASS) { - ESP_LOGE(TAG, "Failed to spawn fetch task"); - _activeTasks.fetch_sub(1, std::memory_order_acq_rel); - delete jobPtr; - xSemaphoreGive(_slotSemaphore); - return false; - } - return true; +bool ESPFetch::enqueueRequest( + const std::string &url, + esp_http_client_method_t method, + FetchString &&body, + FetchCallback callback, + std::shared_ptr syncHandle, + const FetchRequestOptions &options +) { + if (!isInitialized()) { + ESP_LOGE(TAG, "ESPFetch not initialized"); + return false; + } + + if (xSemaphoreTake(_slotSemaphore, _config.slotAcquireTicks) != pdTRUE) { + ESP_LOGW(TAG, "No available fetch slots"); + return false; + } + + auto job = std::make_unique(_config.usePSRAMBuffers); + job->owner = this; + const std::string normalizedUrl = normalizeUrl(url); + job->url.assign(normalizedUrl.c_str(), normalizedUrl.size()); + job->method = method; + job->body = std::move(body); + job->requestOptions.timeoutMs = options.timeoutMs; + job->requestOptions.maxBodyBytes = options.maxBodyBytes; + job->requestOptions.maxHeaderBytes = options.maxHeaderBytes; + job->requestOptions.rxBufferSize = options.rxBufferSize; + job->requestOptions.txBufferSize = options.txBufferSize; + job->requestOptions.skipTlsCommonNameCheck = options.skipTlsCommonNameCheck; + job->requestOptions.allowRedirects = options.allowRedirects; + job->requestOptions.contentType = options.contentType; + job->requestOptions.headers.clear(); + job->requestOptions.headers.reserve(options.headers.size()); + for (const auto &header : options.headers) { + job->requestOptions.headers + .emplace_back(header.name.c_str(), header.value.c_str(), job->stringAllocator); + } + job->callback = std::move(callback); + job->syncHandle = std::move(syncHandle); + + job->bodyLimit = + job->requestOptions.maxBodyBytes ? job->requestOptions.maxBodyBytes : _config.maxBodyBytes; + job->headerLimit = job->requestOptions.maxHeaderBytes ? job->requestOptions.maxHeaderBytes + : _config.maxHeaderBytes; + if (job->bodyLimit == 0) { + job->bodyLimit = std::numeric_limits::max(); + } + if (job->headerLimit == 0) { + job->headerLimit = std::numeric_limits::max(); + } + + size_t reserveBytes = job->bodyLimit == std::numeric_limits::max() + ? static_cast(1024) + : std::min(job->bodyLimit, static_cast(1024)); + job->response.body.reserve(reserveBytes); + + size_t stackSize = _config.stackSize; + if (stackSize == 0) { + ESP_LOGE(TAG, "Invalid stack size for fetch worker"); + xSemaphoreGive(_slotSemaphore); + return false; + } + + _activeTasks.fetch_add(1, std::memory_order_acq_rel); + + FetchJob *jobPtr = job.release(); + TaskHandle_t taskHandle = nullptr; + const BaseType_t created = xTaskCreatePinnedToCore( + &ESPFetch::requestTask, + "esp-fetch", + stackSize, + jobPtr, + _config.priority, + &taskHandle, + _config.coreId + ); + if (created != pdPASS) { + ESP_LOGE(TAG, "Failed to spawn fetch task"); + _activeTasks.fetch_sub(1, std::memory_order_acq_rel); + delete jobPtr; + xSemaphoreGive(_slotSemaphore); + return false; + } + return true; } -bool ESPFetch::enqueueStreamRequest(const std::string &url, - FetchChunkCallback onChunk, - FetchStreamCallback onDone, - const FetchRequestOptions &options) { - if (!isInitialized()) { - ESP_LOGE(TAG, "ESPFetch not initialized"); - return false; - } - - if (!onChunk) { - ESP_LOGE(TAG, "getStream requires onChunk callback"); - return false; - } - - if (xSemaphoreTake(_slotSemaphore, _config.slotAcquireTicks) != pdTRUE) { - ESP_LOGW(TAG, "No available fetch slots"); - return false; - } - - auto job = std::make_unique(_config.usePSRAMBuffers); - job->owner = this; - const std::string normalizedUrl = normalizeUrl(url); - job->url.assign(normalizedUrl.c_str(), normalizedUrl.size()); - job->method = HTTP_METHOD_GET; - job->requestOptions.timeoutMs = options.timeoutMs; - job->requestOptions.maxBodyBytes = options.maxBodyBytes; - job->requestOptions.maxHeaderBytes = options.maxHeaderBytes; - job->requestOptions.rxBufferSize = options.rxBufferSize; - job->requestOptions.txBufferSize = options.txBufferSize; - job->requestOptions.skipTlsCommonNameCheck = options.skipTlsCommonNameCheck; - job->requestOptions.allowRedirects = options.allowRedirects; - job->requestOptions.contentType = options.contentType; - job->requestOptions.headers.clear(); - job->requestOptions.headers.reserve(options.headers.size()); - for (const auto &header : options.headers) { - job->requestOptions.headers.emplace_back(header.name.c_str(), header.value.c_str(), job->stringAllocator); - } - - job->isStream = true; - job->onChunk = std::move(onChunk); - job->onDone = std::move(onDone); - job->receivedBytes = 0; - job->streamAbortError = ESP_OK; - - // For streaming, default to "unlimited" unless the caller explicitly sets maxBodyBytes. - job->bodyLimit = job->requestOptions.maxBodyBytes ? job->requestOptions.maxBodyBytes : std::numeric_limits::max(); - job->headerLimit = - job->requestOptions.maxHeaderBytes ? job->requestOptions.maxHeaderBytes : _config.maxHeaderBytes; - if (job->headerLimit == 0) { - job->headerLimit = std::numeric_limits::max(); - } - - size_t stackSize = _config.stackSize; - if (stackSize == 0) { - ESP_LOGE(TAG, "Invalid stack size for fetch worker"); - xSemaphoreGive(_slotSemaphore); - return false; - } - - _activeTasks.fetch_add(1, std::memory_order_acq_rel); - - FetchJob *jobPtr = job.release(); - TaskHandle_t taskHandle = nullptr; - const BaseType_t created = xTaskCreatePinnedToCore( - &ESPFetch::requestTask, "esp-fetch", stackSize, jobPtr, _config.priority, &taskHandle, _config.coreId); - if (created != pdPASS) { - ESP_LOGE(TAG, "Failed to spawn fetch task"); - _activeTasks.fetch_sub(1, std::memory_order_acq_rel); - delete jobPtr; - xSemaphoreGive(_slotSemaphore); - return false; - } - return true; +bool ESPFetch::enqueueStreamRequest( + const std::string &url, + FetchChunkCallback onChunk, + FetchStreamCallback onDone, + const FetchRequestOptions &options +) { + if (!isInitialized()) { + ESP_LOGE(TAG, "ESPFetch not initialized"); + return false; + } + + if (!onChunk) { + ESP_LOGE(TAG, "getStream requires onChunk callback"); + return false; + } + + if (xSemaphoreTake(_slotSemaphore, _config.slotAcquireTicks) != pdTRUE) { + ESP_LOGW(TAG, "No available fetch slots"); + return false; + } + + auto job = std::make_unique(_config.usePSRAMBuffers); + job->owner = this; + const std::string normalizedUrl = normalizeUrl(url); + job->url.assign(normalizedUrl.c_str(), normalizedUrl.size()); + job->method = HTTP_METHOD_GET; + job->requestOptions.timeoutMs = options.timeoutMs; + job->requestOptions.maxBodyBytes = options.maxBodyBytes; + job->requestOptions.maxHeaderBytes = options.maxHeaderBytes; + job->requestOptions.rxBufferSize = options.rxBufferSize; + job->requestOptions.txBufferSize = options.txBufferSize; + job->requestOptions.skipTlsCommonNameCheck = options.skipTlsCommonNameCheck; + job->requestOptions.allowRedirects = options.allowRedirects; + job->requestOptions.contentType = options.contentType; + job->requestOptions.headers.clear(); + job->requestOptions.headers.reserve(options.headers.size()); + for (const auto &header : options.headers) { + job->requestOptions.headers + .emplace_back(header.name.c_str(), header.value.c_str(), job->stringAllocator); + } + + job->isStream = true; + job->onChunk = std::move(onChunk); + job->onDone = std::move(onDone); + job->receivedBytes = 0; + job->streamAbortError = ESP_OK; + + // For streaming, default to "unlimited" unless the caller explicitly sets maxBodyBytes. + job->bodyLimit = job->requestOptions.maxBodyBytes ? job->requestOptions.maxBodyBytes + : std::numeric_limits::max(); + job->headerLimit = job->requestOptions.maxHeaderBytes ? job->requestOptions.maxHeaderBytes + : _config.maxHeaderBytes; + if (job->headerLimit == 0) { + job->headerLimit = std::numeric_limits::max(); + } + + size_t stackSize = _config.stackSize; + if (stackSize == 0) { + ESP_LOGE(TAG, "Invalid stack size for fetch worker"); + xSemaphoreGive(_slotSemaphore); + return false; + } + + _activeTasks.fetch_add(1, std::memory_order_acq_rel); + + FetchJob *jobPtr = job.release(); + TaskHandle_t taskHandle = nullptr; + const BaseType_t created = xTaskCreatePinnedToCore( + &ESPFetch::requestTask, + "esp-fetch", + stackSize, + jobPtr, + _config.priority, + &taskHandle, + _config.coreId + ); + if (created != pdPASS) { + ESP_LOGE(TAG, "Failed to spawn fetch task"); + _activeTasks.fetch_sub(1, std::memory_order_acq_rel); + delete jobPtr; + xSemaphoreGive(_slotSemaphore); + return false; + } + return true; } -JsonDocument ESPFetch::waitForResult(const std::shared_ptr &handle, TickType_t waitTicks) const { - JsonDocument doc; - if (!handle || !handle->done) { - doc["ok"] = false; - doc["error"]["message"] = "invalid sync handle"; - return doc; - } - - if (xSemaphoreTake(handle->done, waitTicks) == pdTRUE && handle->ready) { - doc = handle->doc; - } else if (handle->ready) { - doc = handle->doc; - } else { - doc["ok"] = false; - doc["error"]["message"] = "timeout waiting for fetch result"; - } - return doc; +JsonDocument +ESPFetch::waitForResult(const std::shared_ptr &handle, TickType_t waitTicks) const { + JsonDocument doc; + if (!handle || !handle->done) { + doc["ok"] = false; + doc["error"]["message"] = "invalid sync handle"; + return doc; + } + + if (xSemaphoreTake(handle->done, waitTicks) == pdTRUE && handle->ready) { + doc = handle->doc; + } else if (handle->ready) { + doc = handle->doc; + } else { + doc["ok"] = false; + doc["error"]["message"] = "timeout waiting for fetch result"; + } + return doc; } void ESPFetch::requestTask(void *arg) { - auto job = std::unique_ptr(static_cast(arg)); - if (!job || !job->owner) { - if (job && job->owner) { - job->owner->_activeTasks.fetch_sub(1, std::memory_order_acq_rel); - } - vTaskDelete(nullptr); - return; - } - job->owner->runJob(std::move(job)); - vTaskDelete(nullptr); + auto job = std::unique_ptr(static_cast(arg)); + if (!job || !job->owner) { + if (job && job->owner) { + job->owner->_activeTasks.fetch_sub(1, std::memory_order_acq_rel); + } + vTaskDelete(nullptr); + return; + } + job->owner->runJob(std::move(job)); + vTaskDelete(nullptr); } esp_err_t ESPFetch::handleHttpEvent(esp_http_client_event_t *event) { - if (!event || !event->user_data) { - return ESP_OK; - } - auto *job = static_cast(event->user_data); - if (job->owner && job->owner->_teardownRequested.load(std::memory_order_acquire)) { - if (job->isStream && job->streamAbortError == ESP_OK) { - job->streamAbortError = ESP_ERR_INVALID_STATE; - } - return ESP_FAIL; - } - - switch (event->event_id) { - case HTTP_EVENT_ON_DATA: - if (event->data && event->data_len > 0) { - // Stream mode: do NOT buffer body; forward chunks directly. - if (job->isStream) { - size_t toSend = static_cast(event->data_len); - - if (job->bodyLimit != std::numeric_limits::max()) { - if (job->receivedBytes >= job->bodyLimit) { - job->streamAbortError = ESP_ERR_INVALID_SIZE; - return ESP_FAIL; - } - const size_t remaining = job->bodyLimit - job->receivedBytes; - toSend = std::min(toSend, remaining); - } - - if (toSend > 0 && job->onChunk) { - const bool keepGoing = job->onChunk(event->data, toSend); - if (!keepGoing) { - // Caller requested abort; stop the stream and propagate a deterministic error. - if (job->streamAbortError == ESP_OK) { - job->streamAbortError = ESP_ERR_INVALID_STATE; - } - return ESP_FAIL; - } - job->receivedBytes += toSend; - } - - // If we had to clip the chunk, we've hit the limit and abort. - if (toSend < static_cast(event->data_len)) { - job->streamAbortError = ESP_ERR_INVALID_SIZE; - return ESP_FAIL; - } - } else { - // JSON mode (existing): buffer into response.body with limit/truncation. - size_t available = - job->response.body.size() < job->bodyLimit ? (job->bodyLimit - job->response.body.size()) : 0; - size_t copyLen = std::min(available, static_cast(event->data_len)); - if (copyLen > 0) { - job->response.body.append(static_cast(event->data), copyLen); - } - if (copyLen < static_cast(event->data_len)) { - job->response.bodyTruncated = true; - } - } - } - break; - - case HTTP_EVENT_ON_HEADER: - if (event->header_key && event->header_value) { - size_t projected = 0; - for (const auto &hdr : job->response.headers) { - projected += hdr.name.size() + hdr.value.size(); - } - projected += strlen(event->header_key) + strlen(event->header_value); - if (projected <= job->headerLimit) { - job->response.headers.emplace_back( - event->header_key, event->header_value, job->response.charAllocator); - } else { - job->response.headersTruncated = true; - } - } - break; - - default: - break; - } - return ESP_OK; + if (!event || !event->user_data) { + return ESP_OK; + } + auto *job = static_cast(event->user_data); + if (job->owner && job->owner->_teardownRequested.load(std::memory_order_acquire)) { + if (job->isStream && job->streamAbortError == ESP_OK) { + job->streamAbortError = ESP_ERR_INVALID_STATE; + } + return ESP_FAIL; + } + + switch (event->event_id) { + case HTTP_EVENT_ON_DATA: + if (event->data && event->data_len > 0) { + // Stream mode: do NOT buffer body; forward chunks directly. + if (job->isStream) { + size_t toSend = static_cast(event->data_len); + + if (job->bodyLimit != std::numeric_limits::max()) { + if (job->receivedBytes >= job->bodyLimit) { + job->streamAbortError = ESP_ERR_INVALID_SIZE; + return ESP_FAIL; + } + const size_t remaining = job->bodyLimit - job->receivedBytes; + toSend = std::min(toSend, remaining); + } + + if (toSend > 0 && job->onChunk) { + const bool keepGoing = job->onChunk(event->data, toSend); + if (!keepGoing) { + // Caller requested abort; stop the stream and propagate a deterministic + // error. + if (job->streamAbortError == ESP_OK) { + job->streamAbortError = ESP_ERR_INVALID_STATE; + } + return ESP_FAIL; + } + job->receivedBytes += toSend; + } + + // If we had to clip the chunk, we've hit the limit and abort. + if (toSend < static_cast(event->data_len)) { + job->streamAbortError = ESP_ERR_INVALID_SIZE; + return ESP_FAIL; + } + } else { + // JSON mode (existing): buffer into response.body with limit/truncation. + size_t available = job->response.body.size() < job->bodyLimit + ? (job->bodyLimit - job->response.body.size()) + : 0; + size_t copyLen = std::min(available, static_cast(event->data_len)); + if (copyLen > 0) { + job->response.body.append(static_cast(event->data), copyLen); + } + if (copyLen < static_cast(event->data_len)) { + job->response.bodyTruncated = true; + } + } + } + break; + + case HTTP_EVENT_ON_HEADER: + if (event->header_key && event->header_value) { + size_t projected = 0; + for (const auto &hdr : job->response.headers) { + projected += hdr.name.size() + hdr.value.size(); + } + projected += strlen(event->header_key) + strlen(event->header_value); + if (projected <= job->headerLimit) { + job->response.headers.emplace_back( + event->header_key, + event->header_value, + job->response.charAllocator + ); + } else { + job->response.headersTruncated = true; + } + } + break; + + default: + break; + } + return ESP_OK; } void ESPFetch::runJob(std::unique_ptr job) { - if (!job) { - return; - } - - const int64_t start = esp_timer_get_time(); - - if (_teardownRequested.load(std::memory_order_acquire)) { - job->response.error = ESP_ERR_INVALID_STATE; - } else { - esp_http_client_config_t config = {}; - config.url = job->url.c_str(); - config.method = job->method; - config.timeout_ms = job->requestOptions.timeoutMs ? job->requestOptions.timeoutMs : _config.defaultTimeoutMs; - config.buffer_size = resolveHttpBufferSize( - "RX buffer size", job->requestOptions.rxBufferSize, _config.rxBufferSize); - config.buffer_size_tx = resolveHttpBufferSize( - "TX buffer size", job->requestOptions.txBufferSize, _config.txBufferSize); - config.event_handler = &ESPFetch::handleHttpEvent; - config.user_data = job.get(); - config.disable_auto_redirect = !(job->requestOptions.allowRedirects && _config.followRedirects); - config.skip_cert_common_name_check = - job->requestOptions.skipTlsCommonNameCheck || _config.skipTlsCommonNameCheck; - - esp_http_client_handle_t client = esp_http_client_init(&config); - if (!client) { - ESP_LOGE(TAG, "esp_http_client_init failed"); - job->response.error = ESP_ERR_NO_MEM; - } else if (_teardownRequested.load(std::memory_order_acquire)) { - job->response.error = ESP_ERR_INVALID_STATE; - esp_http_client_cleanup(client); - } else { - auto hasHeader = [&](const char *key) { - for (const auto &header : job->requestOptions.headers) { - if (equalsIgnoreCase(header.name, key)) { - return true; - } - } - return false; - }; - - if (_config.userAgent && !hasHeader("User-Agent")) { - esp_http_client_set_header(client, "User-Agent", _config.userAgent); - } - - const char *contentType = - job->requestOptions.contentType ? job->requestOptions.contentType : _config.defaultContentType; - if (!job->isStream && job->method == HTTP_METHOD_POST && contentType && !hasHeader("Content-Type")) { - esp_http_client_set_header(client, "Content-Type", contentType); - } - - for (const auto &header : job->requestOptions.headers) { - esp_http_client_set_header(client, header.name.c_str(), header.value.c_str()); - } - - if (!job->body.empty()) { - esp_http_client_set_post_field(client, job->body.c_str(), job->body.length()); - } - - job->response.error = esp_http_client_perform(client); - if (job->response.error == ESP_OK) { - job->response.statusCode = esp_http_client_get_status_code(client); - } - // Preserve intentional stream abort reason (onChunk returned false or maxBodyBytes clipping), - // instead of losing it to generic ESP_FAIL from esp_http_client_perform(). - if (job->isStream && job->response.error != ESP_OK && job->streamAbortError != ESP_OK) { - job->response.error = job->streamAbortError; - } - esp_http_client_cleanup(client); - } - } - - job->response.durationUs = esp_timer_get_time() - start; - - if (job->isStream) { - StreamResult r; - r.error = job->response.error; - r.statusCode = job->response.statusCode; - r.receivedBytes = job->receivedBytes; - if (job->onDone) { - job->onDone(r); - } - } else { - JsonDocument result = buildResult(*job, job->response); - deliverResult(job, result); - } - - if (_slotSemaphore) { - xSemaphoreGive(_slotSemaphore); - } - - _activeTasks.fetch_sub(1, std::memory_order_acq_rel); + if (!job) { + return; + } + + const int64_t start = esp_timer_get_time(); + + if (_teardownRequested.load(std::memory_order_acquire)) { + job->response.error = ESP_ERR_INVALID_STATE; + } else { + esp_http_client_config_t config = {}; + config.url = job->url.c_str(); + config.method = job->method; + config.timeout_ms = job->requestOptions.timeoutMs ? job->requestOptions.timeoutMs + : _config.defaultTimeoutMs; + config.buffer_size = resolveHttpBufferSize( + "RX buffer size", + job->requestOptions.rxBufferSize, + _config.rxBufferSize + ); + config.buffer_size_tx = resolveHttpBufferSize( + "TX buffer size", + job->requestOptions.txBufferSize, + _config.txBufferSize + ); + config.event_handler = &ESPFetch::handleHttpEvent; + config.user_data = job.get(); + config.disable_auto_redirect = + !(job->requestOptions.allowRedirects && _config.followRedirects); + config.skip_cert_common_name_check = + job->requestOptions.skipTlsCommonNameCheck || _config.skipTlsCommonNameCheck; + + esp_http_client_handle_t client = esp_http_client_init(&config); + if (!client) { + ESP_LOGE(TAG, "esp_http_client_init failed"); + job->response.error = ESP_ERR_NO_MEM; + } else if (_teardownRequested.load(std::memory_order_acquire)) { + job->response.error = ESP_ERR_INVALID_STATE; + esp_http_client_cleanup(client); + } else { + auto hasHeader = [&](const char *key) { + for (const auto &header : job->requestOptions.headers) { + if (equalsIgnoreCase(header.name, key)) { + return true; + } + } + return false; + }; + + if (_config.userAgent && !hasHeader("User-Agent")) { + esp_http_client_set_header(client, "User-Agent", _config.userAgent); + } + + const char *contentType = job->requestOptions.contentType + ? job->requestOptions.contentType + : _config.defaultContentType; + if (!job->isStream && job->method == HTTP_METHOD_POST && contentType && + !hasHeader("Content-Type")) { + esp_http_client_set_header(client, "Content-Type", contentType); + } + + for (const auto &header : job->requestOptions.headers) { + esp_http_client_set_header(client, header.name.c_str(), header.value.c_str()); + } + + if (!job->body.empty()) { + esp_http_client_set_post_field(client, job->body.c_str(), job->body.length()); + } + + job->response.error = esp_http_client_perform(client); + if (job->response.error == ESP_OK) { + job->response.statusCode = esp_http_client_get_status_code(client); + } + // Preserve intentional stream abort reason (onChunk returned false or maxBodyBytes + // clipping), instead of losing it to generic ESP_FAIL from esp_http_client_perform(). + if (job->isStream && job->response.error != ESP_OK && job->streamAbortError != ESP_OK) { + job->response.error = job->streamAbortError; + } + esp_http_client_cleanup(client); + } + } + + job->response.durationUs = esp_timer_get_time() - start; + + if (job->isStream) { + StreamResult r; + r.error = job->response.error; + r.statusCode = job->response.statusCode; + r.receivedBytes = job->receivedBytes; + if (job->onDone) { + job->onDone(r); + } + } else { + JsonDocument result = buildResult(*job, job->response); + deliverResult(job, result); + } + + if (_slotSemaphore) { + xSemaphoreGive(_slotSemaphore); + } + + _activeTasks.fetch_sub(1, std::memory_order_acq_rel); } JsonDocument ESPFetch::buildResult(const FetchJob &job, const FetchResponse &response) const { - JsonDocument doc; - auto root = doc.to(); - root["url"] = job.url.c_str(); - root["method"] = job.method == HTTP_METHOD_POST ? "POST" : "GET"; - const bool httpOk = response.statusCode >= 200 && response.statusCode < 400; - root["status"] = response.statusCode; - root["ok"] = response.error == ESP_OK && httpOk; - root["duration_ms"] = static_cast(response.durationUs / 1000); - root["body"] = response.body.c_str(); - root["body_truncated"] = response.bodyTruncated; - root["headers_truncated"] = response.headersTruncated; - - auto headersObj = root["headers"].to(); - for (const auto &header : response.headers) { - headersObj[header.name.c_str()] = header.value.c_str(); - } - - if (response.error == ESP_OK) { - root["error"] = nullptr; - } else { - auto err = root["error"].to(); - err["code"] = static_cast(response.error); - err["message"] = esp_err_to_name(response.error); - } - return doc; + JsonDocument doc; + auto root = doc.to(); + root["url"] = job.url.c_str(); + root["method"] = job.method == HTTP_METHOD_POST ? "POST" : "GET"; + const bool httpOk = response.statusCode >= 200 && response.statusCode < 400; + root["status"] = response.statusCode; + root["ok"] = response.error == ESP_OK && httpOk; + root["duration_ms"] = static_cast(response.durationUs / 1000); + root["body"] = response.body.c_str(); + root["body_truncated"] = response.bodyTruncated; + root["headers_truncated"] = response.headersTruncated; + + auto headersObj = root["headers"].to(); + for (const auto &header : response.headers) { + headersObj[header.name.c_str()] = header.value.c_str(); + } + + if (response.error == ESP_OK) { + root["error"] = nullptr; + } else { + auto err = root["error"].to(); + err["code"] = static_cast(response.error); + err["message"] = esp_err_to_name(response.error); + } + return doc; } void ESPFetch::deliverResult(const std::unique_ptr &job, const JsonDocument &result) { - if (!job) { - return; - } - if (job->callback) { - job->callback(result); - } - if (job->syncHandle) { - job->syncHandle->doc = result; - job->syncHandle->ready = true; - if (job->syncHandle->done) { - xSemaphoreGive(job->syncHandle->done); - } - } + if (!job) { + return; + } + if (job->callback) { + job->callback(result); + } + if (job->syncHandle) { + job->syncHandle->doc = result; + job->syncHandle->ready = true; + if (job->syncHandle->done) { + xSemaphoreGive(job->syncHandle->done); + } + } } diff --git a/src/esp_fetch/fetch.h b/src/esp_fetch/fetch.h index 6eab0a8..43d4a24 100644 --- a/src/esp_fetch/fetch.h +++ b/src/esp_fetch/fetch.h @@ -3,12 +3,12 @@ #include #include +#include #include #include #include -#include #include -#include +#include #include "fetch_allocator.h" @@ -21,38 +21,38 @@ extern "C" { } struct FetchHeader { - std::string name; - std::string value; + std::string name; + std::string value; }; struct FetchRequestOptions { - uint32_t timeoutMs = 0; - size_t maxBodyBytes = 0; - size_t maxHeaderBytes = 0; - size_t rxBufferSize = 0; - size_t txBufferSize = 0; - bool skipTlsCommonNameCheck = false; - bool allowRedirects = true; - std::vector headers; - const char *contentType = nullptr; + uint32_t timeoutMs = 0; + size_t maxBodyBytes = 0; + size_t maxHeaderBytes = 0; + size_t rxBufferSize = 0; + size_t txBufferSize = 0; + bool skipTlsCommonNameCheck = false; + bool allowRedirects = true; + std::vector headers; + const char *contentType = nullptr; }; struct FetchConfig { - size_t maxConcurrentRequests = 4; - size_t stackSize = 6144 * sizeof(StackType_t); - UBaseType_t priority = 4; - BaseType_t coreId = tskNO_AFFINITY; - uint32_t defaultTimeoutMs = 15000; - size_t maxBodyBytes = 16384; - size_t maxHeaderBytes = 4096; - size_t rxBufferSize = 0; - size_t txBufferSize = 0; - TickType_t slotAcquireTicks = pdMS_TO_TICKS(0); - bool skipTlsCommonNameCheck = false; - bool followRedirects = true; - bool usePSRAMBuffers = false; - const char *userAgent = "ESPFetch/1.0"; - const char *defaultContentType = "application/json"; + size_t maxConcurrentRequests = 4; + size_t stackSize = 6144 * sizeof(StackType_t); + UBaseType_t priority = 4; + BaseType_t coreId = tskNO_AFFINITY; + uint32_t defaultTimeoutMs = 15000; + size_t maxBodyBytes = 16384; + size_t maxHeaderBytes = 4096; + size_t rxBufferSize = 0; + size_t txBufferSize = 0; + TickType_t slotAcquireTicks = pdMS_TO_TICKS(0); + bool skipTlsCommonNameCheck = false; + bool followRedirects = true; + bool usePSRAMBuffers = false; + const char *userAgent = "ESPFetch/1.0"; + const char *defaultContentType = "application/json"; }; using FetchCallback = std::function; @@ -61,84 +61,113 @@ using FetchCallback = std::function; // Streaming (binary/any-content) // ------------------------------ struct StreamResult { - esp_err_t error = ESP_OK; - int statusCode = 0; - size_t receivedBytes = 0; + esp_err_t error = ESP_OK; + int statusCode = 0; + size_t receivedBytes = 0; }; using FetchChunkCallback = std::function; using FetchStreamCallback = std::function; class ESPFetch { - public: - ESPFetch() = default; - ~ESPFetch(); - - bool init(const FetchConfig &config = FetchConfig{}); - void deinit(); - bool isInitialized() const; - - bool get(const char *url, FetchCallback callback, const FetchRequestOptions &options = FetchRequestOptions{}); - bool get(const String &url, FetchCallback callback, const FetchRequestOptions &options = FetchRequestOptions{}); - JsonDocument get(const char *url, TickType_t waitTicks, const FetchRequestOptions &options = FetchRequestOptions{}); - JsonDocument get(const String &url, TickType_t waitTicks, const FetchRequestOptions &options = FetchRequestOptions{}); - - bool post(const char *url, - const JsonDocument &payload, - FetchCallback callback, - const FetchRequestOptions &options = FetchRequestOptions{}); - bool post(const String &url, - const JsonDocument &payload, - FetchCallback callback, - const FetchRequestOptions &options = FetchRequestOptions{}); - JsonDocument post(const char *url, - const JsonDocument &payload, - TickType_t waitTicks, - const FetchRequestOptions &options = FetchRequestOptions{}); - JsonDocument post(const String &url, - const JsonDocument &payload, - TickType_t waitTicks, - const FetchRequestOptions &options = FetchRequestOptions{}); - - // Stream download (binary / any kind). No JSON handling. - bool getStream(const char *url, - FetchChunkCallback onChunk, - FetchStreamCallback onDone = nullptr, - const FetchRequestOptions &options = FetchRequestOptions{}); - bool getStream(const String &url, - FetchChunkCallback onChunk, - FetchStreamCallback onDone = nullptr, - const FetchRequestOptions &options = FetchRequestOptions{}); - - private: - struct FetchJob; - struct FetchResponse; - struct SyncHandle; - - bool enqueueRequest(const std::string &url, - esp_http_client_method_t method, - FetchString &&body, - FetchCallback callback, - std::shared_ptr syncHandle, - const FetchRequestOptions &options); - - bool enqueueStreamRequest(const std::string &url, - FetchChunkCallback onChunk, - FetchStreamCallback onDone, - const FetchRequestOptions &options); - - JsonDocument waitForResult(const std::shared_ptr &handle, TickType_t waitTicks) const; - - static void requestTask(void *arg); - static esp_err_t handleHttpEvent(esp_http_client_event_t *event); - - void runJob(std::unique_ptr job); - JsonDocument buildResult(const FetchJob &job, const FetchResponse &response) const; - static void deliverResult(const std::unique_ptr &job, const JsonDocument &result); - - FetchConfig _config{}; - std::atomic _initialized{false}; - std::atomic _teardownRequested{false}; - std::atomic _activeTasks{0}; - SemaphoreHandle_t _slotSemaphore = nullptr; + public: + ESPFetch() = default; + ~ESPFetch(); + + bool init(const FetchConfig &config = FetchConfig{}); + void deinit(); + bool isInitialized() const; + + bool + get(const char *url, + FetchCallback callback, + const FetchRequestOptions &options = FetchRequestOptions{}); + bool + get(const String &url, + FetchCallback callback, + const FetchRequestOptions &options = FetchRequestOptions{}); + JsonDocument + get(const char *url, + TickType_t waitTicks, + const FetchRequestOptions &options = FetchRequestOptions{}); + JsonDocument + get(const String &url, + TickType_t waitTicks, + const FetchRequestOptions &options = FetchRequestOptions{}); + + bool post( + const char *url, + const JsonDocument &payload, + FetchCallback callback, + const FetchRequestOptions &options = FetchRequestOptions{} + ); + bool post( + const String &url, + const JsonDocument &payload, + FetchCallback callback, + const FetchRequestOptions &options = FetchRequestOptions{} + ); + JsonDocument post( + const char *url, + const JsonDocument &payload, + TickType_t waitTicks, + const FetchRequestOptions &options = FetchRequestOptions{} + ); + JsonDocument post( + const String &url, + const JsonDocument &payload, + TickType_t waitTicks, + const FetchRequestOptions &options = FetchRequestOptions{} + ); + + // Stream download (binary / any kind). No JSON handling. + bool getStream( + const char *url, + FetchChunkCallback onChunk, + FetchStreamCallback onDone = nullptr, + const FetchRequestOptions &options = FetchRequestOptions{} + ); + bool getStream( + const String &url, + FetchChunkCallback onChunk, + FetchStreamCallback onDone = nullptr, + const FetchRequestOptions &options = FetchRequestOptions{} + ); + + private: + struct FetchJob; + struct FetchResponse; + struct SyncHandle; + + bool enqueueRequest( + const std::string &url, + esp_http_client_method_t method, + FetchString &&body, + FetchCallback callback, + std::shared_ptr syncHandle, + const FetchRequestOptions &options + ); + + bool enqueueStreamRequest( + const std::string &url, + FetchChunkCallback onChunk, + FetchStreamCallback onDone, + const FetchRequestOptions &options + ); + + JsonDocument + waitForResult(const std::shared_ptr &handle, TickType_t waitTicks) const; + + static void requestTask(void *arg); + static esp_err_t handleHttpEvent(esp_http_client_event_t *event); + + void runJob(std::unique_ptr job); + JsonDocument buildResult(const FetchJob &job, const FetchResponse &response) const; + static void deliverResult(const std::unique_ptr &job, const JsonDocument &result); + + FetchConfig _config{}; + std::atomic _initialized{false}; + std::atomic _teardownRequested{false}; + std::atomic _activeTasks{0}; + SemaphoreHandle_t _slotSemaphore = nullptr; }; diff --git a/src/esp_fetch/fetch_allocator.h b/src/esp_fetch/fetch_allocator.h index 34f3fae..2639189 100644 --- a/src/esp_fetch/fetch_allocator.h +++ b/src/esp_fetch/fetch_allocator.h @@ -20,82 +20,80 @@ namespace fetch_allocator_detail { inline void *allocate(std::size_t bytes, bool usePSRAMBuffers) noexcept { #if ESP_FETCH_HAS_BUFFER_MANAGER - return ESPBufferManager::allocate(bytes, usePSRAMBuffers); + return ESPBufferManager::allocate(bytes, usePSRAMBuffers); #else - (void)usePSRAMBuffers; - return std::malloc(bytes); + (void)usePSRAMBuffers; + return std::malloc(bytes); #endif } inline void deallocate(void *ptr) noexcept { #if ESP_FETCH_HAS_BUFFER_MANAGER - ESPBufferManager::deallocate(ptr); + ESPBufferManager::deallocate(ptr); #else - std::free(ptr); + std::free(ptr); #endif } -} // namespace fetch_allocator_detail - -template -class FetchAllocator { - public: - using value_type = T; - - FetchAllocator() noexcept = default; - explicit FetchAllocator(bool usePSRAMBuffers) noexcept : _usePSRAMBuffers(usePSRAMBuffers) {} - - template - FetchAllocator(const FetchAllocator &other) noexcept : _usePSRAMBuffers(other.usePSRAMBuffers()) {} - - T *allocate(std::size_t n) { - if (n == 0) { - return nullptr; - } - if (n > (std::numeric_limits::max() / sizeof(T))) { +} // namespace fetch_allocator_detail + +template class FetchAllocator { + public: + using value_type = T; + + FetchAllocator() noexcept = default; + explicit FetchAllocator(bool usePSRAMBuffers) noexcept : _usePSRAMBuffers(usePSRAMBuffers) { + } + + template + FetchAllocator(const FetchAllocator &other) noexcept + : _usePSRAMBuffers(other.usePSRAMBuffers()) { + } + + T *allocate(std::size_t n) { + if (n == 0) { + return nullptr; + } + if (n > (std::numeric_limits::max() / sizeof(T))) { #if defined(__cpp_exceptions) - throw std::bad_alloc(); + throw std::bad_alloc(); #else - std::abort(); + std::abort(); #endif - } + } - void *memory = fetch_allocator_detail::allocate(n * sizeof(T), _usePSRAMBuffers); - if (memory == nullptr) { + void *memory = fetch_allocator_detail::allocate(n * sizeof(T), _usePSRAMBuffers); + if (memory == nullptr) { #if defined(__cpp_exceptions) - throw std::bad_alloc(); + throw std::bad_alloc(); #else - std::abort(); + std::abort(); #endif - } - return static_cast(memory); - } + } + return static_cast(memory); + } - void deallocate(T *ptr, std::size_t) noexcept { - fetch_allocator_detail::deallocate(ptr); - } + void deallocate(T *ptr, std::size_t) noexcept { + fetch_allocator_detail::deallocate(ptr); + } - bool usePSRAMBuffers() const noexcept { - return _usePSRAMBuffers; - } + bool usePSRAMBuffers() const noexcept { + return _usePSRAMBuffers; + } - template - bool operator==(const FetchAllocator &other) const noexcept { - return _usePSRAMBuffers == other.usePSRAMBuffers(); - } + template bool operator==(const FetchAllocator &other) const noexcept { + return _usePSRAMBuffers == other.usePSRAMBuffers(); + } - template - bool operator!=(const FetchAllocator &other) const noexcept { - return !(*this == other); - } + template bool operator!=(const FetchAllocator &other) const noexcept { + return !(*this == other); + } - private: - template - friend class FetchAllocator; + private: + template friend class FetchAllocator; - bool _usePSRAMBuffers = false; + bool _usePSRAMBuffers = false; }; -template -using FetchVector = std::vector>; +template using FetchVector = std::vector>; using FetchString = std::basic_string, FetchAllocator>; diff --git a/test/test_esp_fetch/test_esp_fetch.cpp b/test/test_esp_fetch/test_esp_fetch.cpp index 059e5ea..cb980e5 100644 --- a/test/test_esp_fetch/test_esp_fetch.cpp +++ b/test/test_esp_fetch/test_esp_fetch.cpp @@ -3,123 +3,121 @@ #include static void test_init_rejects_zero_concurrency() { - ESPFetch fetch; - FetchConfig cfg{}; - cfg.maxConcurrentRequests = 0; - TEST_ASSERT_FALSE(fetch.init(cfg)); - TEST_ASSERT_FALSE(fetch.isInitialized()); + ESPFetch fetch; + FetchConfig cfg{}; + cfg.maxConcurrentRequests = 0; + TEST_ASSERT_FALSE(fetch.init(cfg)); + TEST_ASSERT_FALSE(fetch.isInitialized()); } static void test_init_and_deinit_cycle_updates_initialized_flag() { - ESPFetch fetch; - TEST_ASSERT_TRUE(fetch.init()); - TEST_ASSERT_TRUE(fetch.isInitialized()); - fetch.deinit(); - TEST_ASSERT_FALSE(fetch.isInitialized()); + ESPFetch fetch; + TEST_ASSERT_TRUE(fetch.init()); + TEST_ASSERT_TRUE(fetch.isInitialized()); + fetch.deinit(); + TEST_ASSERT_FALSE(fetch.isInitialized()); } static void test_init_accepts_psram_buffer_toggle() { - ESPFetch fetch; - FetchConfig cfg{}; - cfg.usePSRAMBuffers = true; - TEST_ASSERT_TRUE(fetch.init(cfg)); - TEST_ASSERT_TRUE(fetch.isInitialized()); - fetch.deinit(); + ESPFetch fetch; + FetchConfig cfg{}; + cfg.usePSRAMBuffers = true; + TEST_ASSERT_TRUE(fetch.init(cfg)); + TEST_ASSERT_TRUE(fetch.isInitialized()); + fetch.deinit(); } static void test_buffer_size_options_default_to_idf_defaults() { - FetchConfig cfg{}; - FetchRequestOptions opts{}; + FetchConfig cfg{}; + FetchRequestOptions opts{}; - TEST_ASSERT_EQUAL_UINT32(0, cfg.rxBufferSize); - TEST_ASSERT_EQUAL_UINT32(0, cfg.txBufferSize); - TEST_ASSERT_EQUAL_UINT32(0, opts.rxBufferSize); - TEST_ASSERT_EQUAL_UINT32(0, opts.txBufferSize); + TEST_ASSERT_EQUAL_UINT32(0, cfg.rxBufferSize); + TEST_ASSERT_EQUAL_UINT32(0, cfg.txBufferSize); + TEST_ASSERT_EQUAL_UINT32(0, opts.rxBufferSize); + TEST_ASSERT_EQUAL_UINT32(0, opts.txBufferSize); } static void test_buffer_size_options_are_assignable() { - FetchConfig cfg{}; - FetchRequestOptions opts{}; + FetchConfig cfg{}; + FetchRequestOptions opts{}; - cfg.rxBufferSize = 8192; - cfg.txBufferSize = 2048; - opts.rxBufferSize = 4096; - opts.txBufferSize = 1024; + cfg.rxBufferSize = 8192; + cfg.txBufferSize = 2048; + opts.rxBufferSize = 4096; + opts.txBufferSize = 1024; - TEST_ASSERT_EQUAL_UINT32(8192, cfg.rxBufferSize); - TEST_ASSERT_EQUAL_UINT32(2048, cfg.txBufferSize); - TEST_ASSERT_EQUAL_UINT32(4096, opts.rxBufferSize); - TEST_ASSERT_EQUAL_UINT32(1024, opts.txBufferSize); + TEST_ASSERT_EQUAL_UINT32(8192, cfg.rxBufferSize); + TEST_ASSERT_EQUAL_UINT32(2048, cfg.txBufferSize); + TEST_ASSERT_EQUAL_UINT32(4096, opts.rxBufferSize); + TEST_ASSERT_EQUAL_UINT32(1024, opts.txBufferSize); } static void test_deinit_is_safe_before_init() { - ESPFetch fetch; - fetch.deinit(); - TEST_ASSERT_FALSE(fetch.isInitialized()); + ESPFetch fetch; + fetch.deinit(); + TEST_ASSERT_FALSE(fetch.isInitialized()); } static void test_deinit_is_idempotent() { - ESPFetch fetch; - TEST_ASSERT_TRUE(fetch.init()); - TEST_ASSERT_TRUE(fetch.isInitialized()); - fetch.deinit(); - fetch.deinit(); - TEST_ASSERT_FALSE(fetch.isInitialized()); + ESPFetch fetch; + TEST_ASSERT_TRUE(fetch.init()); + TEST_ASSERT_TRUE(fetch.isInitialized()); + fetch.deinit(); + fetch.deinit(); + TEST_ASSERT_FALSE(fetch.isInitialized()); } static void test_reinit_after_deinit_is_supported() { - ESPFetch fetch; - TEST_ASSERT_TRUE(fetch.init()); - fetch.deinit(); - TEST_ASSERT_TRUE(fetch.init()); - TEST_ASSERT_TRUE(fetch.isInitialized()); - fetch.deinit(); - TEST_ASSERT_FALSE(fetch.isInitialized()); + ESPFetch fetch; + TEST_ASSERT_TRUE(fetch.init()); + fetch.deinit(); + TEST_ASSERT_TRUE(fetch.init()); + TEST_ASSERT_TRUE(fetch.isInitialized()); + fetch.deinit(); + TEST_ASSERT_FALSE(fetch.isInitialized()); } static void test_async_get_requires_initialization() { - ESPFetch fetch; - volatile bool invoked = false; - auto cb = [&](JsonDocument) { - invoked = true; - }; - bool started = fetch.get("https://example.com", cb); - TEST_ASSERT_FALSE(started); - TEST_ASSERT_FALSE(invoked); + ESPFetch fetch; + volatile bool invoked = false; + auto cb = [&](JsonDocument) { invoked = true; }; + bool started = fetch.get("https://example.com", cb); + TEST_ASSERT_FALSE(started); + TEST_ASSERT_FALSE(invoked); } static void test_sync_get_reports_error_when_not_initialized() { - ESPFetch fetch; - JsonDocument doc = fetch.get("https://example.com", pdMS_TO_TICKS(1)); - auto msg = doc["error"]["message"] | ""; - TEST_ASSERT_EQUAL_STRING("failed to start http get", msg); - TEST_ASSERT_FALSE(doc["ok"] | true); + ESPFetch fetch; + JsonDocument doc = fetch.get("https://example.com", pdMS_TO_TICKS(1)); + auto msg = doc["error"]["message"] | ""; + TEST_ASSERT_EQUAL_STRING("failed to start http get", msg); + TEST_ASSERT_FALSE(doc["ok"] | true); } static void test_sync_get_requires_url() { - ESPFetch fetch; - JsonDocument doc = fetch.get(nullptr, pdMS_TO_TICKS(1)); - auto msg = doc["error"]["message"] | ""; - TEST_ASSERT_EQUAL_STRING("url is null", msg); + ESPFetch fetch; + JsonDocument doc = fetch.get(nullptr, pdMS_TO_TICKS(1)); + auto msg = doc["error"]["message"] | ""; + TEST_ASSERT_EQUAL_STRING("url is null", msg); } static void test_sync_post_requires_url() { - ESPFetch fetch; - JsonDocument payload; - payload["hello"] = "world"; - JsonDocument doc = fetch.post(nullptr, payload, pdMS_TO_TICKS(1)); - auto msg = doc["error"]["message"] | ""; - TEST_ASSERT_EQUAL_STRING("url is null", msg); + ESPFetch fetch; + JsonDocument payload; + payload["hello"] = "world"; + JsonDocument doc = fetch.post(nullptr, payload, pdMS_TO_TICKS(1)); + auto msg = doc["error"]["message"] | ""; + TEST_ASSERT_EQUAL_STRING("url is null", msg); } static void test_sync_post_reports_error_when_not_initialized() { - ESPFetch fetch; - JsonDocument payload; - payload["value"] = 42; - JsonDocument doc = fetch.post("https://example.com", payload, pdMS_TO_TICKS(1)); - auto msg = doc["error"]["message"] | ""; - TEST_ASSERT_EQUAL_STRING("failed to start http post", msg); - TEST_ASSERT_FALSE(doc["ok"] | true); + ESPFetch fetch; + JsonDocument payload; + payload["value"] = 42; + JsonDocument doc = fetch.post("https://example.com", payload, pdMS_TO_TICKS(1)); + auto msg = doc["error"]["message"] | ""; + TEST_ASSERT_EQUAL_STRING("failed to start http post", msg); + TEST_ASSERT_FALSE(doc["ok"] | true); } void setUp() { @@ -129,24 +127,24 @@ void tearDown() { } void setup() { - delay(2000); - UNITY_BEGIN(); - RUN_TEST(test_init_rejects_zero_concurrency); - RUN_TEST(test_init_and_deinit_cycle_updates_initialized_flag); - RUN_TEST(test_init_accepts_psram_buffer_toggle); - RUN_TEST(test_buffer_size_options_default_to_idf_defaults); - RUN_TEST(test_buffer_size_options_are_assignable); - RUN_TEST(test_deinit_is_safe_before_init); - RUN_TEST(test_deinit_is_idempotent); - RUN_TEST(test_reinit_after_deinit_is_supported); - RUN_TEST(test_async_get_requires_initialization); - RUN_TEST(test_sync_get_reports_error_when_not_initialized); - RUN_TEST(test_sync_get_requires_url); - RUN_TEST(test_sync_post_requires_url); - RUN_TEST(test_sync_post_reports_error_when_not_initialized); - UNITY_END(); + delay(2000); + UNITY_BEGIN(); + RUN_TEST(test_init_rejects_zero_concurrency); + RUN_TEST(test_init_and_deinit_cycle_updates_initialized_flag); + RUN_TEST(test_init_accepts_psram_buffer_toggle); + RUN_TEST(test_buffer_size_options_default_to_idf_defaults); + RUN_TEST(test_buffer_size_options_are_assignable); + RUN_TEST(test_deinit_is_safe_before_init); + RUN_TEST(test_deinit_is_idempotent); + RUN_TEST(test_reinit_after_deinit_is_supported); + RUN_TEST(test_async_get_requires_initialization); + RUN_TEST(test_sync_get_reports_error_when_not_initialized); + RUN_TEST(test_sync_get_requires_url); + RUN_TEST(test_sync_post_requires_url); + RUN_TEST(test_sync_post_reports_error_when_not_initialized); + UNITY_END(); } void loop() { - vTaskDelay(pdMS_TO_TICKS(1000)); + vTaskDelay(pdMS_TO_TICKS(1000)); }