# HTTP Client Component for Calendink Client **Authored by Antigravity (Claude Opus 4.6)** **Date:** 2026-03-28 --- ## 1. What (Goal) Create a reusable ESP-IDF component (`components/http_client/`) that provides a simple, synchronous HTTP client API for the Calendink Client firmware. The component must: - Wrap ESP-IDF's `esp_http_client` in a clean C++ API with ergonomic result types. - Support **GET** requests returning either text (JSON) or binary (PNG image) data. - Support **POST** requests sending a JSON body and receiving a JSON response. - Build full URLs from path fragments using the Provider's address resolved via **mDNS** (`calendink.local`). - Be shared across projects via the `components/` directory, usable by any Calendink device firmware. The initial test integration will run in the Client's `main.cpp` after WiFi connects: 1. **Register the device** — `POST /api/devices/register` with the Client's own WiFi MAC address. 2. **Fetch the screen image** — `GET /api/devices/screen.png?mac=XX:XX:XX:XX:XX:XX`, download the PNG and log the received size. ## 2. Why (Reasoning) ### 2.1. Why a Shared Component? The Client firmware needs to talk to the Provider over HTTP. This is a fundamental capability that will be used by every Client feature going forward (screen polling, status reporting, configuration sync). Placing it in `components/http_client/` follows the established pattern (`network`, `led`) and makes it available to any future Calendink firmware project. ### 2.2. Why Synchronous? The Client firmware has a simple execution model: boot → connect WiFi → register → poll screen → sleep → repeat. There is no concurrent UI or web server to keep responsive. A synchronous (blocking) API is: - Simpler to use correctly — no callbacks, no state machines. - Easier to reason about memory lifetime — buffers are allocated and freed in the same scope. - Sufficient for the single-task polling loop the Client will run. If async requests are needed later (e.g., for a multi-task architecture), `_async` variants can be added without breaking the existing API. ### 2.3. Why mDNS Instead of Hardcoded IP? The Provider already advertises itself as `calendink.local` via mDNS (`CONFIG_CALENDINK_MDNS_HOSTNAME`). Using mDNS for discovery: - **Eliminates configuration coupling** — no need to update Client firmware when the Provider's DHCP lease changes. - **Plug-and-play** — a freshly flashed Client finds the Provider automatically on the same network. - **Fallback** — if mDNS resolution fails, the Client can fall back to a Kconfig-configured static IP (configured in the Client project, not the component). The mDNS resolution happens once at startup and is cached. The `mdns` component dependency is lightweight (~15 KB flash). ### 2.4. Why Download the Full Image? The ESP32-C6 has ~320 KB of internal SRAM. A typical screen PNG is 50–200 KB depending on content complexity. For the 4-level grayscale 480×800 display, most PNGs land around 20–80 KB (LodePNG with simple content compresses well). This fits comfortably in internal RAM. If a particular PNG exceeds available heap, the `http_get_binary()` function will return `ESP_ERR_NO_MEM` — the caller can handle this gracefully. For the initial test, we'll attempt the full download and log the result either way. ## 3. How (Implementation Details) ### 3.1. Component File Structure ``` components/http_client/ ├── CMakeLists.txt # Component registration ├── idf_component.yml # Component manifest ├── http_client.hpp # Public API header └── http_client.cpp # Implementation ``` No `Kconfig.projbuild` in the component itself — the Provider address configuration belongs to the consumer (Client project). The component only provides the URL builder and HTTP functions. ### 3.2. API Surface ```cpp // ── Result Types ────────────────────────────────────────────── struct http_text_response_t { int status_code; // HTTP status (200, 404, 500, etc.) char *body; // Heap-allocated, null-terminated (caller frees) size_t body_len; // Byte length (excluding null terminator) }; struct http_binary_response_t { int status_code; uint8_t *data; // Heap-allocated binary buffer (caller frees) size_t data_len; // Byte length }; // ── Functions ───────────────────────────────────────────────── // Build "http://:" from a host, port, and path. // Returns 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. esp_err_t http_get_text(const char *url, http_text_response_t *out); // GET a binary resource (e.g. PNG). Blocks until complete. esp_err_t http_get_binary(const char *url, http_binary_response_t *out); // POST JSON body, receive JSON response. Blocks until complete. esp_err_t http_post_json(const char *url, const char *json_body, http_text_response_t *out); ``` **Design note: `http_build_url` takes host/port as parameters** rather than reading Kconfig directly. This keeps the component generic — the caller (Client `main.cpp`) passes its own Kconfig values or mDNS-resolved addresses. The component has zero Kconfig of its own. ### 3.3. Implementation Details #### Event Handler — Accumulating Response Body `esp_http_client` delivers data in chunks via an event callback. We use a single internal handler that accumulates chunks into a dynamically growing buffer: ``` HTTP_EVENT_ON_DATA → realloc(buffer, current_len + new_len) → memcpy ``` The buffer pointer is passed through the `user_data` field of `esp_http_client_config_t`. A small internal struct tracks the buffer, length, and capacity: ```cpp struct http_receive_buffer_t { char *buf; size_t len; size_t capacity; }; ``` #### Timeout Default: **10 seconds** (`timeout_ms = 10000`). Sufficient for even the largest PNG response from the Provider (encoding + transfer). Can be made configurable later if needed. #### Memory Safety - Output struct is zeroed at function entry — on any error path, the caller gets a clean struct with null pointers. - On `esp_http_client_perform()` failure, any partially accumulated buffer is freed immediately. - On success, ownership of the buffer transfers to the caller (documented: "caller must `free()`"). #### Logging Uses `kTagHttpClient = "HTTP_CLIENT"` — unique tag per Unity Build convention (though this component is not Unity Build, keeping the convention consistent is good practice for when the Client eventually adopts it). Key log points: - `ESP_LOGI` — request start (method + URL), response status + size - `ESP_LOGW` — non-2xx status codes - `ESP_LOGE` — connection failures, allocation failures ### 3.4. Client Integration #### Kconfig (in `Client/main/Kconfig.projbuild` — NOT in the component) ```kconfig menu "Calendink Client" config CALENDINK_PROVIDER_MDNS_HOSTNAME string "Provider mDNS Hostname" default "calendink" help The mDNS hostname of the Provider device. The Client resolves .local to find the Provider IP. config CALENDINK_PROVIDER_PORT int "Provider HTTP Port" default 80 help The HTTP port of the Calendink Provider. config CALENDINK_PROVIDER_FALLBACK_IP string "Provider Fallback IP (if mDNS fails)" default "" help Static IP to use if mDNS resolution fails. Leave empty to disable fallback (the Client will retry mDNS). endmenu ``` #### main.cpp Test Flow After WiFi connects successfully: ``` 1. Resolve Provider IP via mdns_query_host("calendink", timeout) - On success: use resolved IP - On failure: fall back to CONFIG_CALENDINK_PROVIDER_FALLBACK_IP if set, else abort 2. Get own MAC: esp_wifi_get_mac(WIFI_IF_STA, mac_bytes) → format "XX:XX:XX:XX:XX:XX" 3. POST /api/devices/register - Body: {"mac": "XX:XX:XX:XX:XX:XX"} - Log: status code + response body - Free response 4. GET /api/devices/screen.png?mac=XX:XX:XX:XX:XX:XX - Log: status code + received byte count - Free response (we don't use the image data yet) ``` #### Dependencies Added to Client | File | Change | |---|---| | `Client/main/idf_component.yml` | Add `http_client: path: "../../components/http_client"` | | `Client/main/CMakeLists.txt` | Add `http_client` and `mdns` to `PRIV_REQUIRES` | ### 3.5. Component Build Configuration ```cmake idf_component_register(SRCS "http_client.cpp" INCLUDE_DIRS "." "../shared" PRIV_REQUIRES esp_http_client) ``` - `INCLUDE_DIRS "."` — consumers `#include "http_client.hpp"` - `"../shared"` — for `types.hpp` (type aliases, `internal` macro) - `PRIV_REQUIRES esp_http_client` — ESP-IDF's built-in HTTP client The `mdns` dependency is on the **Client**, not the component — the component is agnostic to how the caller discovers the host. ### 3.6. Provider API Contracts (Reference) These are the two endpoints the Client will call. They already exist and require no changes. **POST /api/devices/register** ``` Request: {"mac": "AA:BB:CC:DD:EE:FF"} Response: {"status": "ok", "mac": "AA:BB:CC:DD:EE:FF"} (201-equivalent, new device) {"status": "already_registered", "mac": "AA:BB:..."} (200, already known) Error: 400 if missing mac, 500 if device slots full ``` **GET /api/devices/screen.png?mac=AA:BB:CC:DD:EE:FF** ``` Response: image/png binary (4-level grayscale, 480×800) Error: 400 if missing mac param, 404 if device not registered, 500 on render failure ``` ## 4. Summary We add a new shared component `http_client` that wraps `esp_http_client` in a simple synchronous API with `http_get_text()`, `http_get_binary()`, and `http_post_json()`. The component is generic — it takes host/port/path and knows nothing about mDNS or Kconfig. The Client firmware integrates it by resolving the Provider via mDNS (`calendink.local`), registering itself, and fetching its screen image as a connectivity test. No Provider-side changes are needed. --- *Created by Antigravity (Claude Opus 4.6) - 2026-03-28*