237 lines
10 KiB
Markdown
237 lines
10 KiB
Markdown
# 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://<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*
|