tdd for http client

This commit is contained in:
2026-03-28 13:25:22 -04:00
parent 7ba6ab56e7
commit a886b9aa11
3 changed files with 238 additions and 0 deletions
+1
View File
@@ -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 |
---
+1
View File
@@ -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 |
---
+236
View File
@@ -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 50200 KB depending on content complexity. For the 4-level grayscale 480×800 display, most PNGs land around 2080 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://<host>:<port><path>" 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 <hostname>.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*