PR-09: HTTP REST/JSON API v1#15
Merged
Merged
Conversation
Spec-kit artifacts for PR-09 (esp_http_server /api/v1/ REST/JSON API + frozen OpenAPI sketch), approved at Checkpoint 2. Records: pure cJSON serialize/parse/ route-table (host-tested) + target-only ApiServer split; separate STA-mode server; non-blocking cached reads (QUIRK 5); soil/power served last-good with valid=false until PR-11; mode->wateringEnabled; LockedWifiManager; no auth v1; OTA stub. No implementation yet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ifiManager Phase 1+2 of PR-09: - api component (linux-guarded CMake): pure ApiEnvelope + ApiRoutes(matchRoute) + ApiDtos, host-tested; ApiServer target-only slot reserved - cJSON: IDF v6 has no built-in json component -> pinned managed dep espressif/cjson ==1.7.19~2 (components/api/idf_component.yml); dependencies.lock updated (esp-modbus pins untouched) - LockedWifiManager (network) for the cross-task API/console snapshot reader (resolves WifiState.h TODO(PR-09)) - host api suites registered (stubs); fixed /api/* -Werror=comment trap in headers Verified: host 191/0, rev1 + rev2 build (0 errors) + board-config verified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sted) Phase 3 / US1 (pure, host-tested): - ApiSerialize: serializeStatus/serializeSensors/serializePower(+unavailable) over cJSON via the success envelope; non-finite/last-good floats -> JSON null; NPK keys only when present; not-set clock -> null timestamp; rev1 power -> null; no wifi password - 15 host tests (11 serialize + 4 route-table full-set); fixed another /*-in-comment (-Werror=comment) trap in ApiDtos.h ApiServer + app_main wiring is the next mission (US1 target side). Verified: host 206/0 (+15), rev1 + rev2 build + board-config verified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ors/power Phase 3 / US1 target side: - ApiServer (target-only): esp_http_server in STA mode, opaque handle; GET /api/v1/ status, /sensors, /power handlers reading ONLY non-blocking cached getters via the Locked* wrappers (QUIRK 5) + LockedWifiManager snapshot + esp_netif IP; JSON 404 via httpd_register_err_handler. Board-gated power (rev2 INA226 / rev1 unavailable). - app_main: LockedWifiManager wraps wifi_manager_inst; ApiServer constructed after sensors, started once on first WifiState::Connected via SystemObserver (mirrors SNTP; object-ordering). Not WDT-subscribed; pumps_force_off() still first. - validity derived non-blockingly (getLastError()==0 && isfinite) -> soil valid=false until PR-11, honest last-good elsewhere. Verified: rev1 + rev2 build (0 errors) + board-config verified; host unchanged 206/0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (host-tested) Phase 4 / US2 (pure, host-tested): - ApiRequests: parseConfigSet (all-or-nothing range validation per IConfigStore constants; malformed/wrong-type -> ok=false, nothing applied) + parsePumpCommand (action start/run/stop; duration 1..300) -> typed result structs, no throwing - ApiSerialize: serializePump / serializePumpList / serializeConfig (never wifi password) - 19 host tests (16 requests + 3 serialize) ApiServer US2 handlers (pumps/config) are the next mission. Verified: host 225/0 (+19), rev1 + rev2 build + board-config verified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 4 / US2 target side (ApiServer): - GET /api/v1/pumps (capability-enumerated); POST /api/v1/pumps/* (name from URI, parsePumpCommand -> runFor/stop; already-running -> 409; unknown pump -> 404) - GET/POST /api/v1/config (parseConfigSet all-or-nothing -> setters; invalid -> 400, persist-fail -> 500). Mode = wateringEnabled. Never serializes the wifi password. - non-blocking (pump status + config getters/setters only); no watering decisions (cap/no-restart live in IWaterPump); wildcard uri_match for /pumps/*. Verified: rev1 + rev2 build + board-config verified; host unchanged 225/0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nge helper Phase 5 / US3 (pure, host-tested): - ApiSerialize: serializeHistory (timestamps/values + echo; empty series -> empty arrays, not error), serializeEvents (newest-first), serializeSelfTest (overall+checks) - namedRangeToWindow (ApiRequests): 1h/6h/24h/7d/30d -> [now-span, now]; unknown -> false - 5 host tests ApiServer US3 handlers + OpenAPI sketch + docs are the next mission. Verified: host 230/0 (+5), rev1 + rev2 build + board-config verified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 5 / US3 target side + polish: - ApiServer: GET /api/v1/history (query metric/reading/range|start-end -> getSensorReadings, empty=200 not error), GET /events (bounded count), POST /selftest (the one documented bounded blocking read of env+soil), POST /ota (501 stub) - docs/api/openapi.yaml: OpenAPI 3 sketch of all 11 endpoints (FROZEN at merge) - firmware/CLAUDE.md: HTTP API v1 section - specs/009 HIL curl checklist Verified: host 230/0; rev1 1057920B (33% slot free) + rev2 build, both config-verified; esp-modbus pins unchanged; only cjson added to dependencies.lock. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses CP3 findings A1-A9 + doc corrections (host 230/0, both boards green): - A1: system_observer starts SNTP + API server on the Connected EDGE (once per transition), not 10x/s on steady Connected (was a log flood + heavyweight httpd_start in the watchdog-fed watering loop) - A3: ApiStatus wired in (added 409/500, dropped 503, statusLine()); ApiResponse.status is now the enum, no raw status literals - A4: deleted LockedWifiManager (reader-only mutex gave no real guarantee); ApiServer reads WifiManager::snapshot() directly; WifiState.h comment corrected - A5: JSON error body for 405 (was httpd default HTML — parity violation) - A6: resolveEventCount + resolveWindow extracted to pure ApiRequests; history/events return 400 on present-but-invalid params instead of silently defaulting - A7: clamp window underflow (unset clock can't wrap uint32) - A8: ESP_LOGE the config-persist 500 with the failing field - A9: cJSON OOM guards (no empty-body 200; no leak) - serializeStatus emits epoch null when unsynced; corrected route/dispatch/US1-only/ LockedWifiManager comments + CLAUDE.md + openapi header New host tests for the extracted logic + coverage gaps land in the next commit. Verified: host 230/0, rev1 + rev2 build (0 errors) + board-config verified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- route/openapi drift guard: apiRoutes() set == checked-in expected mirror (A2; covers the previously-dead apiRoutes()/apiRouteCount()) - resolveEventCount / resolveWindow (range precedence, explicit both/start-only/ end-only, default-24h, unknown->ok=false) + namedRangeToWindow underflow clamp (A6/A7) - parser gaps: pump missing/wrong-type duration + missing/non-string action; config wrong-type bool, moisture 0/100 bounds + negative reject, interval floors - envelope: errorBody/notFoundBody shape + statusLine mapping Verified: host 252/0 (+22). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR-09 — HTTP REST/JSON API v1
Delivers the frozen
/api/v1/REST/JSON contract (FR7, FR6, FR12) overesp_http_server, plus a committedOpenAPI 3 sketch (
docs/api/openapi.yaml). Behavioral coverage ofdocs/parity-checklist.md§4. Spec-kitartifacts in
specs/009-http-server-api-v1/. Depends on PR-06/07 (merged); live data from PR-03/04/05.What it does
A dedicated
esp_http_serverin station mode serving/api/v1/:GET /status(mode, wifi+IP, time-sync, uptime, reset reason, firmware, storage[, power]),GET /sensors(env + soil + level [+ power] with per-sectionvalidflags, epoch timestamp),GET /history(metric/reading/range|start-end → aligned series),GET /power(rev2),GET /events.GET/POST /pumps(capability-enumerated; start/run/stop; 300 s cap; already-running → 409),GET/POST /config(validated, all-or-nothing, persisted), mode ↔wateringEnabled.POST /selftest(bounded env+soil self-test),POST /ota(501 stub — PR-13).{success,…}/{success:false,error}envelope; JSON 404 and 405; unset clock → null epoch.Architecture
apicomponent, split like sensors/network: pure serialize/parse/route-table/envelope(host-tested with cJSON) + target-only
ApiServer(the soleesp_http_servertouchpoint, opaquehandle, excluded from the linux build). Handlers call ONLY the
Locked*interfaces, are non-blocking(cached getters;
/selftestis the one bounded exception), make no watering decisions, and neverserialize the wifi password (Constitution I / FR-015). The server runs on the httpd task, not
watchdog-subscribed; SNTP + API start once on the WiFi Connected edge.
jsoncomponent → pinned managed depespressif/cjson ==1.7.19~2(
components/api/idf_component.yml); it links on the linux host so the pure layer is host-tested.Verification
wifi-password-absent), request parse/validate (config all-or-nothing + bounds; pump action/duration),
route-table↔OpenAPI drift guard,
resolveEventCount/resolveWindow/DST-safe range helper, envelope.in the 1.5 MiB OTA slot).
dependencies.lockadds only cjson; esp-modbus pins unchanged. No frozen-legacy edits.Review (CP3)
Five specialist agents; all core invariants PASS. Findings A1–A9 + coverage/comment fixes applied and
re-verified (see
docs/checkpoints/cp3-009-http-server-api-v1.md): start-on-Connected-edge (was a 10 Hzretry storm), JSON 405,
ApiStatuswired in,LockedWifiManagerremoved (reader-only lock gave noguarantee), 400 on malformed query params, window-underflow clamp, config-500 logging, cJSON OOM guards.
Deferred (by design)
SensorReadingsDto.timestamp→optional,PumpCommandResult→optional); driving theApiServer registration array from the route table — a later cleanup.
valid:falseuntil PR-11's periodic reader lands.specs/009-http-server-api-v1/checklists/hil.md) runs on the rev1 rig: scripted curl over everyendpoint incl. non-blocking sensors, pump timed-run+cap, config reject, robustness (bad-JSON 400,
unknown 404, stalled-client watering-isolation), rev2 power.
🤖 Generated with Claude Code