From 46d1ab6358616a20dd457f7a29645e96278fc074 Mon Sep 17 00:00:00 2001 From: Patedam Date: Sat, 28 Mar 2026 13:44:54 -0400 Subject: [PATCH] http client first version made by claude opus --- components/http_client/CMakeLists.txt | 5 +- components/http_client/http_client.c | 7 - components/http_client/http_client.cpp | 259 +++++++++++++++++++++++ components/http_client/http_client.h | 1 - components/http_client/http_client.hpp | 46 ++++ components/http_client/idf_component.yml | 6 + 6 files changed, 314 insertions(+), 10 deletions(-) delete mode 100644 components/http_client/http_client.c create mode 100644 components/http_client/http_client.cpp delete mode 100644 components/http_client/http_client.h create mode 100644 components/http_client/http_client.hpp create mode 100644 components/http_client/idf_component.yml diff --git a/components/http_client/CMakeLists.txt b/components/http_client/CMakeLists.txt index 325b9b4..4e0439d 100644 --- a/components/http_client/CMakeLists.txt +++ b/components/http_client/CMakeLists.txt @@ -1,2 +1,3 @@ -idf_component_register(SRCS "http_client.c" - INCLUDE_DIRS "include") +idf_component_register(SRCS "http_client.cpp" + INCLUDE_DIRS "." "../shared" + PRIV_REQUIRES esp_http_client) diff --git a/components/http_client/http_client.c b/components/http_client/http_client.c deleted file mode 100644 index cc0bf11..0000000 --- a/components/http_client/http_client.c +++ /dev/null @@ -1,7 +0,0 @@ -#include -#include "http_client.h" - -void func(void) -{ - -} diff --git a/components/http_client/http_client.cpp b/components/http_client/http_client.cpp new file mode 100644 index 0000000..214c2c3 --- /dev/null +++ b/components/http_client/http_client.cpp @@ -0,0 +1,259 @@ +// HTTP Client Component — synchronous GET / POST wrapper around esp_http_client. +// See tdd/http_client_component.md for design rationale. + +#include "http_client.hpp" + +#include +#include +#include + +#include "esp_http_client.h" +#include "esp_log.h" + +#include "types.hpp" + +internal const char *kTagHttpClient = "HTTP_CLIENT"; + +// ── Internal receive buffer ───────────────────────────────────────────────── + +struct http_receive_buffer_t +{ + char *buf; + size_t len; + size_t capacity; +}; + +// ── Event handler ─────────────────────────────────────────────────────────── + +internal esp_err_t http_event_handler(esp_http_client_event_t *evt) +{ + auto *recv = (http_receive_buffer_t *)evt->user_data; + + switch (evt->event_id) + { + case HTTP_EVENT_ON_DATA: + { + if (recv == nullptr) + { + break; + } + + size_t new_len = recv->len + evt->data_len; + + // Grow buffer if needed + if (new_len > recv->capacity) + { + // Double or fit, whichever is larger + size_t new_cap = recv->capacity * 2; + if (new_cap < new_len) + { + new_cap = new_len; + } + + // +1 for potential null terminator + char *new_buf = (char *)realloc(recv->buf, new_cap + 1); + if (new_buf == nullptr) + { + ESP_LOGE(kTagHttpClient, "realloc failed (%zu bytes)", new_cap + 1); + return ESP_ERR_NO_MEM; + } + + recv->buf = new_buf; + recv->capacity = new_cap; + } + + memcpy(recv->buf + recv->len, evt->data, evt->data_len); + recv->len = new_len; + break; + } + + case HTTP_EVENT_ERROR: + ESP_LOGE(kTagHttpClient, "HTTP_EVENT_ERROR"); + break; + + case HTTP_EVENT_ON_CONNECTED: + case HTTP_EVENT_HEADERS_SENT: + case HTTP_EVENT_ON_HEADER: + case HTTP_EVENT_ON_FINISH: + case HTTP_EVENT_DISCONNECTED: + case HTTP_EVENT_REDIRECT: + default: + break; + } + + return ESP_OK; +} + +// ── Shared perform helper ─────────────────────────────────────────────────── + +// Performs an HTTP request, accumulates body into recv buffer. +// On success, recv->buf contains the raw response body (not null-terminated). +// Caller is responsible for cleaning up recv->buf on both success and failure. +internal esp_err_t http_perform(const char *url, + esp_http_client_method_t method, + const char *post_data, + size_t post_data_len, + const char *content_type, + http_receive_buffer_t *recv, + int *out_status_code) +{ + *out_status_code = 0; + + esp_http_client_config_t config = {}; + config.url = url; + config.method = method; + config.event_handler = http_event_handler; + config.user_data = recv; + config.timeout_ms = 10000; + config.buffer_size = 1024; + config.buffer_size_tx = 1024; + + esp_http_client_handle_t client = esp_http_client_init(&config); + if (client == nullptr) + { + ESP_LOGE(kTagHttpClient, "Failed to init HTTP client for %s", url); + return ESP_FAIL; + } + + // Set POST body if provided + if (post_data != nullptr && post_data_len > 0) + { + esp_http_client_set_post_field(client, post_data, (int)post_data_len); + } + + // Set Content-Type header if provided + if (content_type != nullptr) + { + esp_http_client_set_header(client, "Content-Type", content_type); + } + + ESP_LOGI(kTagHttpClient, "%s %s", + method == HTTP_METHOD_POST ? "POST" : "GET", url); + + esp_err_t err = esp_http_client_perform(client); + if (err != ESP_OK) + { + ESP_LOGE(kTagHttpClient, "HTTP request failed: %s", esp_err_to_name(err)); + esp_http_client_cleanup(client); + return err; + } + + *out_status_code = esp_http_client_get_status_code(client); + int64_t content_length = esp_http_client_get_content_length(client); + + if (*out_status_code >= 200 && *out_status_code < 300) + { + ESP_LOGI(kTagHttpClient, "Response: %d, %zu bytes received", + *out_status_code, recv->len); + } + else + { + ESP_LOGW(kTagHttpClient, "Response: %d (content-length: %lld)", + *out_status_code, content_length); + } + + esp_http_client_cleanup(client); + return ESP_OK; +} + +// ── Public API ────────────────────────────────────────────────────────────── + +char *http_build_url(const char *host, uint16_t port, const char *path) +{ + // "http://" (7) + host + ":" (1) + port (max 5) + path + null + size_t host_len = strlen(host); + size_t path_len = path ? strlen(path) : 0; + size_t url_len = 7 + host_len + 1 + 5 + path_len + 1; + + char *url = (char *)malloc(url_len); + if (url == nullptr) + { + ESP_LOGE(kTagHttpClient, "Failed to allocate URL buffer"); + return nullptr; + } + + snprintf(url, url_len, "http://%s:%u%s", host, port, path ? path : ""); + return url; +} + +esp_err_t http_get_text(const char *url, http_text_response_t *out) +{ + memset(out, 0, sizeof(*out)); + + http_receive_buffer_t recv = {}; + int status_code = 0; + + esp_err_t err = http_perform(url, HTTP_METHOD_GET, + nullptr, 0, nullptr, + &recv, &status_code); + if (err != ESP_OK) + { + free(recv.buf); + return err; + } + + // Null-terminate the text body + if (recv.buf != nullptr) + { + recv.buf[recv.len] = '\0'; // Safe: we always allocate capacity + 1 + } + + out->status_code = status_code; + out->body = recv.buf; + out->body_len = recv.len; + return ESP_OK; +} + +esp_err_t http_get_binary(const char *url, http_binary_response_t *out) +{ + memset(out, 0, sizeof(*out)); + + http_receive_buffer_t recv = {}; + int status_code = 0; + + esp_err_t err = http_perform(url, HTTP_METHOD_GET, + nullptr, 0, nullptr, + &recv, &status_code); + if (err != ESP_OK) + { + free(recv.buf); + return err; + } + + out->status_code = status_code; + out->data = (uint8_t *)recv.buf; + out->data_len = recv.len; + return ESP_OK; +} + +esp_err_t http_post_json(const char *url, const char *json_body, + http_text_response_t *out) +{ + memset(out, 0, sizeof(*out)); + + http_receive_buffer_t recv = {}; + int status_code = 0; + + size_t body_len = json_body ? strlen(json_body) : 0; + + esp_err_t err = http_perform(url, HTTP_METHOD_POST, + json_body, body_len, + "application/json", + &recv, &status_code); + if (err != ESP_OK) + { + free(recv.buf); + return err; + } + + // Null-terminate the text body + if (recv.buf != nullptr) + { + recv.buf[recv.len] = '\0'; + } + + out->status_code = status_code; + out->body = recv.buf; + out->body_len = recv.len; + return ESP_OK; +} diff --git a/components/http_client/http_client.h b/components/http_client/http_client.h deleted file mode 100644 index f6f3a61..0000000 --- a/components/http_client/http_client.h +++ /dev/null @@ -1 +0,0 @@ -void func(void); diff --git a/components/http_client/http_client.hpp b/components/http_client/http_client.hpp new file mode 100644 index 0000000..66e48d2 --- /dev/null +++ b/components/http_client/http_client.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "esp_err.h" +#include +#include + +// ── Result Types ──────────────────────────────────────────────────────────── + +// Text/JSON response. Caller must free(body) after use. +struct http_text_response_t +{ + int status_code; // HTTP status (200, 404, 500, …) + char *body; // Heap-allocated, null-terminated + size_t body_len; // Byte length (excluding null terminator) +}; + +// Binary response (e.g. PNG image). Caller must free(data) after use. +struct http_binary_response_t +{ + int status_code; + uint8_t *data; // Heap-allocated binary buffer + size_t data_len; // Byte length +}; + +// ── Functions ─────────────────────────────────────────────────────────────── + +// Build "http://:". +// Returns a heap-allocated string; caller must free(). +char *http_build_url(const char *host, uint16_t port, const char *path); + +// GET a text/JSON resource. Blocks until complete. +// On success (ESP_OK): out is filled. Caller must free(out->body). +// On failure: out is zeroed, returns an esp_err_t. +esp_err_t http_get_text(const char *url, http_text_response_t *out); + +// GET a binary resource (e.g. PNG image). Blocks until complete. +// On success (ESP_OK): out is filled. Caller must free(out->data). +// On failure: out is zeroed, returns an esp_err_t. +esp_err_t http_get_binary(const char *url, http_binary_response_t *out); + +// POST a JSON body, receive a JSON response. Blocks until complete. +// json_body must be a null-terminated JSON string. +// On success (ESP_OK): out is filled. Caller must free(out->body). +// On failure: out is zeroed, returns an esp_err_t. +esp_err_t http_post_json(const char *url, const char *json_body, + http_text_response_t *out); diff --git a/components/http_client/idf_component.yml b/components/http_client/idf_component.yml new file mode 100644 index 0000000..bfa13bc --- /dev/null +++ b/components/http_client/idf_component.yml @@ -0,0 +1,6 @@ +version: "1.0.0" +description: "Calendink HTTP Client Component" + +dependencies: + idf: + version: '>=5.0.0'