src.nth.io/

summaryrefslogtreecommitdiff
path: root/main/http_api.c
diff options
context:
space:
mode:
Diffstat (limited to 'main/http_api.c')
-rw-r--r--main/http_api.c233
1 files changed, 223 insertions, 10 deletions
diff --git a/main/http_api.c b/main/http_api.c
index 4c0008d..7e97407 100644
--- a/main/http_api.c
+++ b/main/http_api.c
@@ -11,11 +11,17 @@
#include "esp_log.h"
#include "esp_timer.h"
+#include "display.h"
+#include "mdns_service.h"
#include "net_eth.h"
+#include "nvs_config.h"
#include "viewport_state.h"
static const char *TAG = "http_api";
+#define MAX_BODY_BYTES 2048
+#define MIN_IDLE_TIMEOUT 5000
+
static const char *state_name(viewport_run_state_t s)
{
switch (s) {
@@ -25,6 +31,14 @@ static const char *state_name(viewport_run_state_t s)
}
}
+static const char *orientation_name(viewport_orientation_t o)
+{
+ return (o == VIEWPORT_ORIENTATION_LANDSCAPE) ? "landscape" : "portrait";
+}
+
+// ============================================================================
+// GET /state
+// ============================================================================
static esp_err_t state_get_handler(httpd_req_t *req)
{
viewport_state_lock();
@@ -32,7 +46,7 @@ static esp_err_t state_get_handler(httpd_req_t *req)
uint64_t now_us = (uint64_t)esp_timer_get_time();
uint64_t up_ms = (now_us - st->boot_us) / 1000;
- int64_t last_age_ms = (st->last_frame_us < 0)
+ int64_t last_age_ms = (st->last_frame_us < 0)
? -1
: (int64_t)((now_us - (uint64_t)st->last_frame_us) / 1000);
@@ -41,10 +55,10 @@ static esp_err_t state_get_handler(httpd_req_t *req)
st->viewport_name[0] ? cJSON_CreateString(st->viewport_name)
: cJSON_CreateNull());
cJSON_AddStringToObject(root, "version", VIEWPORT_VERSION);
- cJSON_AddBoolToObject(root, "configured", st->configured);
+ cJSON_AddBoolToObject (root, "configured", st->configured);
cJSON_AddStringToObject(root, "state", state_name(st->state));
cJSON_AddNumberToObject(root, "uptime_ms", (double)up_ms);
- cJSON_AddItemToObject(root, "last_frame_ms_ago",
+ cJSON_AddItemToObject (root, "last_frame_ms_ago",
last_age_ms < 0 ? cJSON_CreateNull()
: cJSON_CreateNumber((double)last_age_ms));
cJSON_AddNumberToObject(root, "frames_received", (double)st->frames_received);
@@ -69,25 +83,224 @@ static esp_err_t state_get_handler(httpd_req_t *req)
return err;
}
+// ============================================================================
+// GET /config
+// ============================================================================
+static esp_err_t config_get_handler(httpd_req_t *req)
+{
+ viewport_state_lock();
+ viewport_state_t *st = viewport_state_get();
+
+ cJSON *root = cJSON_CreateObject();
+ cJSON_AddItemToObject(root, "viewport",
+ st->viewport_name[0] ? cJSON_CreateString(st->viewport_name)
+ : cJSON_CreateNull());
+ cJSON_AddItemToObject(root, "scrypted",
+ st->scrypted_url[0] ? cJSON_CreateString(st->scrypted_url)
+ : cJSON_CreateNull());
+ cJSON_AddNumberToObject(root, "idle_timeout_ms", (double)st->idle_timeout_ms);
+ cJSON_AddStringToObject(root, "orientation", orientation_name(st->orientation));
+ cJSON_AddNumberToObject(root, "brightness", (double)st->brightness);
+
+ viewport_state_unlock();
+
+ char *body = cJSON_PrintUnformatted(root);
+ cJSON_Delete(root);
+ if (!body) return ESP_ERR_NO_MEM;
+
+ httpd_resp_set_type(req, "application/json");
+ esp_err_t err = httpd_resp_send(req, body, HTTPD_RESP_USE_STRLEN);
+ cJSON_free(body);
+ return err;
+}
+
+// ============================================================================
+// POST /config — partial-update, atomic, validated
+// ============================================================================
+static esp_err_t respond_400(httpd_req_t *req, const char *reason)
+{
+ ESP_LOGW(TAG, "/config 400: %s", reason);
+ httpd_resp_set_status(req, "400 Bad Request");
+ httpd_resp_set_type(req, "text/plain");
+ return httpd_resp_send(req, reason, HTTPD_RESP_USE_STRLEN);
+}
+
+static esp_err_t read_body(httpd_req_t *req, char *buf, size_t cap)
+{
+ if (req->content_len == 0 || req->content_len >= cap) return ESP_FAIL;
+ size_t got = 0;
+ while (got < req->content_len) {
+ int n = httpd_req_recv(req, buf + got, req->content_len - got);
+ if (n <= 0) return ESP_FAIL;
+ got += n;
+ }
+ buf[got] = '\0';
+ return ESP_OK;
+}
+
+static esp_err_t config_post_handler(httpd_req_t *req)
+{
+ char body[MAX_BODY_BYTES];
+ if (read_body(req, body, sizeof(body)) != ESP_OK)
+ return respond_400(req, "missing or oversized body");
+
+ cJSON *root = cJSON_Parse(body);
+ if (!root) return respond_400(req, "invalid JSON");
+
+ // Stage validated values; commit atomically at the end. Each "have_*"
+ // flag records whether the field was present in the request.
+ bool have_vp = false, have_sc = false, have_idle = false;
+ bool have_orient = false, have_bright = false;
+
+ char vp[64] = {0};
+ char sc[256] = {0};
+ uint32_t idle_ms = 0;
+ viewport_orientation_t orient = VIEWPORT_ORIENTATION_PORTRAIT;
+ uint8_t bright = 80;
+
+ cJSON *j;
+
+ if ((j = cJSON_GetObjectItemCaseSensitive(root, "viewport"))) {
+ if (!cJSON_IsString(j) || j->valuestring[0] == '\0' ||
+ strlen(j->valuestring) >= sizeof(vp)) {
+ cJSON_Delete(root);
+ return respond_400(req, "viewport must be a non-empty string < 64 chars");
+ }
+ strncpy(vp, j->valuestring, sizeof(vp) - 1);
+ have_vp = true;
+ }
+
+ if ((j = cJSON_GetObjectItemCaseSensitive(root, "scrypted"))) {
+ if (!cJSON_IsString(j) || strncmp(j->valuestring, "http://", 7) != 0 ||
+ strlen(j->valuestring) >= sizeof(sc)) {
+ cJSON_Delete(root);
+ return respond_400(req, "scrypted must be http://... and < 256 chars");
+ }
+ strncpy(sc, j->valuestring, sizeof(sc) - 1);
+ have_sc = true;
+ }
+
+ if ((j = cJSON_GetObjectItemCaseSensitive(root, "idle_timeout_ms"))) {
+ if (!cJSON_IsNumber(j) || j->valuedouble < 0 ||
+ j->valuedouble > 0xFFFFFFFFULL) {
+ cJSON_Delete(root);
+ return respond_400(req, "idle_timeout_ms must be a u32");
+ }
+ idle_ms = (uint32_t)j->valuedouble;
+ if (idle_ms != 0 && idle_ms < MIN_IDLE_TIMEOUT) {
+ cJSON_Delete(root);
+ return respond_400(req, "idle_timeout_ms must be 0 or >= 5000");
+ }
+ have_idle = true;
+ }
+
+ if ((j = cJSON_GetObjectItemCaseSensitive(root, "orientation"))) {
+ if (!cJSON_IsString(j)) {
+ cJSON_Delete(root);
+ return respond_400(req, "orientation must be a string");
+ }
+ if (strcmp(j->valuestring, "portrait") == 0) {
+ orient = VIEWPORT_ORIENTATION_PORTRAIT;
+ } else if (strcmp(j->valuestring, "landscape") == 0) {
+ orient = VIEWPORT_ORIENTATION_LANDSCAPE;
+ } else {
+ cJSON_Delete(root);
+ return respond_400(req, "orientation must be portrait or landscape");
+ }
+ have_orient = true;
+ }
+
+ if ((j = cJSON_GetObjectItemCaseSensitive(root, "brightness"))) {
+ if (!cJSON_IsNumber(j) || j->valuedouble < 0 || j->valuedouble > 100) {
+ cJSON_Delete(root);
+ return respond_400(req, "brightness must be 0..100");
+ }
+ bright = (uint8_t)j->valuedouble;
+ have_bright = true;
+ }
+
+ cJSON_Delete(root);
+
+ // Apply atomically.
+ bool brightness_changed = false;
+ bool name_or_orient_changed = false;
+
+ viewport_state_lock();
+ viewport_state_t *st = viewport_state_get();
+
+ if (have_vp) {
+ if (strcmp(st->viewport_name, vp) != 0) name_or_orient_changed = true;
+ strncpy(st->viewport_name, vp, sizeof(st->viewport_name) - 1);
+ }
+ if (have_sc) strncpy(st->scrypted_url, sc, sizeof(st->scrypted_url) - 1);
+ if (have_idle) st->idle_timeout_ms = idle_ms;
+ if (have_orient) {
+ if (st->orientation != orient) name_or_orient_changed = true;
+ st->orientation = orient;
+ }
+ if (have_bright) {
+ if (st->brightness != bright) brightness_changed = true;
+ st->brightness = bright;
+ }
+
+ // A configured device has both a viewport name and a scrypted URL.
+ if (st->viewport_name[0] && st->scrypted_url[0] && !st->configured) {
+ st->configured = true;
+ if (st->state == VIEWPORT_STATE_UNCONFIGURED) st->state = VIEWPORT_STATE_ASLEEP;
+ }
+
+ viewport_state_unlock();
+
+ esp_err_t save_err = nvs_config_save();
+ if (save_err != ESP_OK) {
+ ESP_LOGE(TAG, "nvs_config_save failed: %s", esp_err_to_name(save_err));
+ // Don't fail the request — config is applied in RAM and will reapply
+ // on next save. Caller can re-POST.
+ }
+
+ if (brightness_changed && display_is_up()) {
+ viewport_state_lock();
+ uint8_t b = viewport_state_get()->brightness;
+ viewport_state_unlock();
+ display_set_brightness(b);
+ }
+ if (name_or_orient_changed) {
+ mdns_service_refresh();
+ }
+
+ httpd_resp_set_status(req, "204 No Content");
+ return httpd_resp_send(req, NULL, 0);
+}
+
+// ============================================================================
+// Route table + start
+// ============================================================================
static const httpd_uri_t s_state_get = {
- .uri = "/state",
- .method = HTTP_GET,
- .handler = state_get_handler,
- .user_ctx = NULL,
+ .uri = "/state", .method = HTTP_GET, .handler = state_get_handler,
+};
+static const httpd_uri_t s_config_get = {
+ .uri = "/config", .method = HTTP_GET, .handler = config_get_handler,
+};
+static const httpd_uri_t s_config_post = {
+ .uri = "/config", .method = HTTP_POST, .handler = config_post_handler,
};
esp_err_t http_api_start(void)
{
httpd_config_t cfg = HTTPD_DEFAULT_CONFIG();
- cfg.server_port = 80;
+ cfg.server_port = 80;
cfg.max_uri_handlers = 8;
cfg.lru_purge_enable = true;
httpd_handle_t server = NULL;
ESP_RETURN_ON_ERROR(httpd_start(&server, &cfg), TAG, "httpd_start");
ESP_RETURN_ON_ERROR(httpd_register_uri_handler(server, &s_state_get),
- TAG, "register /state");
+ TAG, "register GET /state");
+ ESP_RETURN_ON_ERROR(httpd_register_uri_handler(server, &s_config_get),
+ TAG, "register GET /config");
+ ESP_RETURN_ON_ERROR(httpd_register_uri_handler(server, &s_config_post),
+ TAG, "register POST /config");
- ESP_LOGI(TAG, "http server listening on :80 (GET /state)");
+ ESP_LOGI(TAG, "http server listening on :80 (GET /state, GET/POST /config)");
return ESP_OK;
}