diff --git a/Provider/AGENTS.md b/Provider/AGENTS.md index 93acce2..9bdd9fc 100644 --- a/Provider/AGENTS.md +++ b/Provider/AGENTS.md @@ -81,6 +81,7 @@ Read the relevant TDD before any major architectural change: | `tdd/concurrent_requests.md` | Changing HTTP server socket/connection config | | `tdd/lvgl_image_generation.md` | Touching LVGL headless display or image gen | | `tdd/device_screens.md` | Changing device registration, MAC routing, or XML layout logic | +| `tdd/http_client_component.md` | Changing the shared HTTP client component or Client↔Provider communication | --- diff --git a/Provider/GEMINI.md b/Provider/GEMINI.md index 443ea47..c1b352f 100644 --- a/Provider/GEMINI.md +++ b/Provider/GEMINI.md @@ -78,6 +78,7 @@ Always read the relevant TDD before making major architectural changes: | [concurrent_requests.md](tdd/concurrent_requests.md) | Changing HTTP server socket/connection config | | [lvgl_image_generation.md](tdd/lvgl_image_generation.md) | Touching LVGL headless display or image gen | | [device_screens.md](tdd/device_screens.md) | Changing device registration, MAC routing, or XML layout logic | +| [http_client_component.md](tdd/http_client_component.md) | Changing the shared HTTP client component or Client↔Provider communication | --- diff --git a/Provider/tdd/http_client_component.md b/Provider/tdd/http_client_component.md new file mode 100644 index 0000000..9781fb3 --- /dev/null +++ b/Provider/tdd/http_client_component.md @@ -0,0 +1,236 @@ +# 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*