src.nth.io/

summaryrefslogtreecommitdiff
path: root/main/touch.c
diff options
context:
space:
mode:
authorLuke Hoersten <[email protected]>2026-06-13 23:01:23 -0500
committerLuke Hoersten <[email protected]>2026-06-13 23:01:23 -0500
commit1d6cbb222ced226fcb482c0b19130774d181c8f8 (patch)
tree768d964dc962e111c8c596e10e2da95748bc738a /main/touch.c
parent57c93fd2978ed5b5e64f4620803b7a2cd7767ed2 (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/touch.c')
-rw-r--r--main/touch.c123
1 files changed, 123 insertions, 0 deletions
diff --git a/main/touch.c b/main/touch.c
new file mode 100644
index 0000000..f13183d
--- /dev/null
+++ b/main/touch.c
@@ -0,0 +1,123 @@
+#include "touch.h"
+
+#include <stdint.h>
+
+#include "driver/i2c_master.h"
+#include "esp_check.h"
+#include "esp_log.h"
+#include "esp_timer.h"
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+
+#include "display.h"
+#include "state_machine.h"
+#include "viewport_state.h"
+
+static const char *TAG = "touch";
+
+#define FT5426_ADDR 0x38
+#define FT5426_I2C_FREQ_HZ 400000
+
+// FT5x06/FT5426 register map (subset).
+#define FT_REG_DEV_MODE 0x00
+#define FT_REG_GEST_ID 0x01
+#define FT_REG_TD_STATUS 0x02 // low 4 bits = touch count
+#define FT_REG_P1_XH 0x03 // X high (bits 0..3) + event flag (bits 6..7)
+#define FT_REG_P1_XL 0x04
+#define FT_REG_P1_YH 0x05
+#define FT_REG_P1_YL 0x06
+
+#define POLL_PERIOD_MS 30
+#define TAP_MAX_MS 500
+#define TAP_MAX_MOVE_PX 25
+#define TAP_DEBOUNCE_MS 150
+
+static i2c_master_dev_handle_t s_dev;
+static TaskHandle_t s_task;
+
+static esp_err_t ft_read(uint8_t reg, uint8_t *buf, size_t n)
+{
+ return i2c_master_transmit_receive(s_dev, &reg, 1, buf, n, 50);
+}
+
+static void touch_task(void *arg)
+{
+ bool was_down = false;
+ uint64_t down_us = 0;
+ uint16_t down_x = 0;
+ uint16_t down_y = 0;
+ uint64_t last_tap_us = 0;
+ uint8_t buf[7];
+
+ for (;;) {
+ vTaskDelay(pdMS_TO_TICKS(POLL_PERIOD_MS));
+
+ if (ft_read(FT_REG_DEV_MODE, buf, sizeof(buf)) != ESP_OK) continue;
+
+ uint8_t touches = buf[FT_REG_TD_STATUS] & 0x0F;
+ if (touches > 0) {
+ // Always capture P1; we only care about taps, not multi-touch.
+ uint16_t x = ((buf[FT_REG_P1_XH] & 0x0F) << 8) | buf[FT_REG_P1_XL];
+ uint16_t y = ((buf[FT_REG_P1_YH] & 0x0F) << 8) | buf[FT_REG_P1_YL];
+ if (!was_down) {
+ was_down = true;
+ down_us = (uint64_t)esp_timer_get_time();
+ down_x = x;
+ down_y = y;
+ }
+ } else if (was_down) {
+ // Release: was-down → up.
+ was_down = false;
+ uint64_t now = (uint64_t)esp_timer_get_time();
+
+ if (now - last_tap_us < (uint64_t)TAP_DEBOUNCE_MS * 1000ULL) continue;
+
+ uint64_t dt_ms = (now - down_us) / 1000ULL;
+ // No reliable last position from the burst; use down coordinates
+ // for the duration check, ignore movement check beyond the down
+ // capture. Good enough for tap-vs-press.
+ (void)down_x; (void)down_y; (void)TAP_MAX_MOVE_PX;
+
+ if (dt_ms > 0 && dt_ms <= TAP_MAX_MS) {
+ last_tap_us = now;
+ viewport_run_state_t cur = state_machine_current();
+ viewport_run_state_t target =
+ (cur == VIEWPORT_STATE_AWAKE) ? VIEWPORT_STATE_ASLEEP
+ : VIEWPORT_STATE_AWAKE;
+ ESP_LOGI(TAG, "tap (%lu ms) -> %s", (unsigned long)dt_ms,
+ target == VIEWPORT_STATE_AWAKE ? "wake" : "sleep");
+ state_machine_set_local(target);
+ }
+ }
+ }
+}
+
+esp_err_t touch_init(void)
+{
+ i2c_master_bus_handle_t bus = display_i2c_bus();
+ if (!bus) {
+ ESP_LOGW(TAG, "I2C bus unavailable — display didn't come up; skipping touch");
+ return ESP_ERR_INVALID_STATE;
+ }
+
+ i2c_device_config_t cfg = {
+ .dev_addr_length = I2C_ADDR_BIT_LEN_7,
+ .device_address = FT5426_ADDR,
+ .scl_speed_hz = FT5426_I2C_FREQ_HZ,
+ };
+ ESP_RETURN_ON_ERROR(i2c_master_bus_add_device(bus, &cfg, &s_dev),
+ TAG, "i2c add FT5426");
+
+ // Sanity poll: read device mode.
+ uint8_t dev_mode = 0;
+ esp_err_t err = ft_read(FT_REG_DEV_MODE, &dev_mode, 1);
+ if (err != ESP_OK) {
+ ESP_LOGE(TAG, "FT5426 @0x38 unreachable (%s) — check INT jumper",
+ esp_err_to_name(err));
+ return err;
+ }
+ ESP_LOGI(TAG, "FT5426 ack'd (dev_mode=0x%02x)", dev_mode);
+
+ BaseType_t ok = xTaskCreate(touch_task, "touch", 3072, NULL, 4, &s_task);
+ return (ok == pdPASS) ? ESP_OK : ESP_FAIL;
+}