diff options
| author | Luke Hoersten <[email protected]> | 2026-06-13 23:01:23 -0500 |
|---|---|---|
| committer | Luke Hoersten <[email protected]> | 2026-06-13 23:01:23 -0500 |
| commit | 1d6cbb222ced226fcb482c0b19130774d181c8f8 (patch) | |
| tree | 768d964dc962e111c8c596e10e2da95748bc738a /main/state_client.c | |
| parent | 57c93fd2978ed5b5e64f4620803b7a2cd7767ed2 (diff) | |
M7: touch + outbound /state POST to Scrypted
state_client.{h,c}:
- Worker task drains a depth-1 queue (xQueueOverwrite gives the
replace-on-full semantics from the spec — in-flight POST is never
cancelled; the next queued entry is overwritten by newer state).
- esp_http_client POST to <scrypted>/state with Content-Type application/
json, User-Agent ScryptedViewport/<version>, Connection: close, 1s
timeout. Body: {"viewport":"<name>","state":"wake"|"sleep"}.
- Any non-2xx or transport error increments state_post_failures and is
otherwise ignored. Silently drops if no Scrypted URL is configured.
touch.{h,c}:
- FT5426 capacitive touch on the shared I2C bus at 0x38.
- 30ms polling task; tracks down/up transitions, detects taps as
down-then-up within 500ms with a 150ms debounce.
- On tap, toggles wake/sleep via state_machine_set_local(), which drives
the local transition AND fires state_client_post() at Scrypted.
state_machine adds state_machine_set_local(): runs the same transition
as state_machine_set() then enqueues an outbound POST. Idle-timer
expiry now uses this path so Scrypted sees idle-driven sleeps too
(the spec's "tighten the race with /frame 409" path stays in place as
the fallback).
display.h exposes display_i2c_bus(); touch.c uses it instead of
re-initializing the same I2C port.
app_main starts state_client right after the state machine and starts
touch after display init (touch is skipped if display isn't up since
they share the bus).
CMakeLists.txt: add esp_http_client to REQUIRES.
Build clean against ESP-IDF 5.4 (binary ~860 KB; jumped ~210 KB from
M6 because esp_http_client + cJSON path pulls in tcp_transport, mbedtls,
http_parser).
TESTING.md M7: flask-based test receiver, tap dispatch verification,
failure-path check (kill receiver, confirm counter increments),
queue-coalescing behavior, no-POST-on-Scrypted-initiated, no-POST-
when-unconfigured.
Diffstat (limited to 'main/state_client.c')
| -rw-r--r-- | main/state_client.c | 105 |
1 files changed, 105 insertions, 0 deletions
diff --git a/main/state_client.c b/main/state_client.c new file mode 100644 index 0000000..2d55bf9 --- /dev/null +++ b/main/state_client.c @@ -0,0 +1,105 @@ +#include "state_client.h" + +#include <stdio.h> +#include <string.h> + +#include "cJSON.h" +#include "esp_http_client.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" +#include "freertos/task.h" + +#include "viewport_state.h" + +static const char *TAG = "state_client"; + +#define WORKER_STACK 4096 +#define WORKER_PRIO 5 +#define HTTP_TIMEOUT_MS 1000 +#define USER_AGENT "ScryptedViewport/" VIEWPORT_VERSION + +// Depth-1 queue holding the next desired state. xQueueOverwrite() gives us +// the replace-on-full semantics from the spec. +static QueueHandle_t s_queue; + +static void perform_post(viewport_run_state_t state) +{ + char url[320]; + char body[160]; + char viewport_name[64]; + + viewport_state_lock(); + viewport_state_t *st = viewport_state_get(); + if (st->scrypted_url[0] == '\0') { + viewport_state_unlock(); + return; // not configured — silently drop + } + snprintf(url, sizeof(url), "%s/state", st->scrypted_url); + strncpy(viewport_name, st->viewport_name, sizeof(viewport_name) - 1); + viewport_name[sizeof(viewport_name) - 1] = '\0'; + viewport_state_unlock(); + + const char *state_str = (state == VIEWPORT_STATE_AWAKE) ? "wake" : "sleep"; + snprintf(body, sizeof(body), + "{\"viewport\":\"%s\",\"state\":\"%s\"}", + viewport_name, state_str); + + esp_http_client_config_t cfg = { + .url = url, + .method = HTTP_METHOD_POST, + .timeout_ms = HTTP_TIMEOUT_MS, + .disable_auto_redirect = true, + }; + esp_http_client_handle_t client = esp_http_client_init(&cfg); + if (!client) { + viewport_state_lock(); + viewport_state_get()->state_post_failures++; + viewport_state_unlock(); + return; + } + esp_http_client_set_header(client, "Content-Type", "application/json"); + esp_http_client_set_header(client, "User-Agent", USER_AGENT); + esp_http_client_set_header(client, "Connection", "close"); + esp_http_client_set_post_field(client, body, strlen(body)); + + esp_err_t err = esp_http_client_perform(client); + int status = (err == ESP_OK) ? esp_http_client_get_status_code(client) : -1; + + if (err != ESP_OK || status < 200 || status >= 300) { + ESP_LOGW(TAG, "POST %s failed: err=%s status=%d", url, esp_err_to_name(err), status); + viewport_state_lock(); + viewport_state_get()->state_post_failures++; + viewport_state_unlock(); + } else { + ESP_LOGI(TAG, "POST %s {state:%s} -> %d", url, state_str, status); + } + esp_http_client_cleanup(client); +} + +static void worker(void *arg) +{ + for (;;) { + viewport_run_state_t pending; + if (xQueueReceive(s_queue, &pending, portMAX_DELAY) == pdTRUE) { + perform_post(pending); + } + } +} + +esp_err_t state_client_init(void) +{ + s_queue = xQueueCreate(1, sizeof(viewport_run_state_t)); + if (!s_queue) return ESP_ERR_NO_MEM; + BaseType_t ok = xTaskCreate(worker, "state_client", WORKER_STACK, + NULL, WORKER_PRIO, NULL); + return (ok == pdPASS) ? ESP_OK : ESP_FAIL; +} + +void state_client_post(viewport_run_state_t state) +{ + if (state != VIEWPORT_STATE_AWAKE && state != VIEWPORT_STATE_ASLEEP) return; + // xQueueOverwrite is depth-1 replace-on-full. The in-flight POST in the + // worker is unaffected — it finishes naturally, then drains the new entry. + xQueueOverwrite(s_queue, &state); +} |
