From 934eaa055a42ad0a765a696e96f6087b98926e73 Mon Sep 17 00:00:00 2001 From: Eric Dahl Date: Wed, 15 Jul 2026 11:21:20 -0400 Subject: [PATCH 1/9] =?UTF-8?q?spike(ESP-026):=20display=20increment=201?= =?UTF-8?q?=20=E2=80=94=20panel=20bring-up=20on=20ESP-IDF=20(S3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the KitchenSync Touch 1.47" LCD to the ESP-IDF firmware. Increment 1 proves the LovyanGFX Panel_ST7789 + Bus_SPI config comes up under ESP-IDF's SPI on the S3. - ks_display.{h,cpp}: LGFX config copied verbatim from ktouch_display.cpp; backlight on ledc (was analogWrite); render task draws a test frame. No touch / live data yet. - idf_component.yml: LovyanGFX managed component, esp32s3 target only. - Kconfig: KS_TOUCH_DISPLAY (depends on IDF_TARGET_ESP32S3, default n). - CMakeLists: always compile ks_display.cpp (no-op stub when flag off). - ks_main.c: one guarded call + include; P4 build unaffected. Spike — not for merge until verified on glass. --- KitchenSync/main/CMakeLists.txt | 4 + KitchenSync/main/Kconfig.projbuild | 10 +++ KitchenSync/main/idf_component.yml | 8 ++ KitchenSync/main/ks_display.cpp | 124 +++++++++++++++++++++++++++++ KitchenSync/main/ks_display.h | 29 +++++++ KitchenSync/main/ks_main.c | 2 + 6 files changed, 177 insertions(+) create mode 100644 KitchenSync/main/ks_display.cpp create mode 100644 KitchenSync/main/ks_display.h diff --git a/KitchenSync/main/CMakeLists.txt b/KitchenSync/main/CMakeLists.txt index f4ad013..7836734 100644 --- a/KitchenSync/main/CMakeLists.txt +++ b/KitchenSync/main/CMakeLists.txt @@ -24,6 +24,10 @@ idf_component_register( "midi_clock_in.c" "${X32LINK_DIR}/midi_bpm_calc.c" "ks_web.cpp" + # ESP-026 spike: 1.47" touch LCD glue. Always compiled; real only under + # CONFIG_KS_TOUCH_DISPLAY (S3), a no-op otherwise, so ks_main calls it + # unconditionally. LovyanGFX comes in via idf_component.yml (S3 target only). + "ks_display.cpp" # ESP-029: /status is now a SHARED module. It moved to X32Link/ and is symlinked # into KitchenSyncTouch/, so both firmwares emit /status from ONE implementation # and cannot drift apart again (ARC-024). diff --git a/KitchenSync/main/Kconfig.projbuild b/KitchenSync/main/Kconfig.projbuild index 7a4d7f2..4e763c4 100644 --- a/KitchenSync/main/Kconfig.projbuild +++ b/KitchenSync/main/Kconfig.projbuild @@ -14,4 +14,14 @@ menu "KitchenSync Configuration" help Password for the WiFi network. Set locally; do not commit. + config KS_TOUCH_DISPLAY + bool "Enable the 1.47\" touch LCD (Waveshare S3-Touch-LCD-1.47)" + depends on IDF_TARGET_ESP32S3 + default n + help + ESP-026 spike: drive the KitchenSync Touch 1.47" LCD + AXS5106L touch from + this ESP-IDF firmware (see ks_display.cpp). S3-only — the panel exists on the + Waveshare S3-Touch-LCD board; a headless S3 (Super Mini) leaves this off. + Pulls in the LovyanGFX managed component (idf_component.yml, S3 target only). + endmenu diff --git a/KitchenSync/main/idf_component.yml b/KitchenSync/main/idf_component.yml index 94ba77f..e30a50f 100644 --- a/KitchenSync/main/idf_component.yml +++ b/KitchenSync/main/idf_component.yml @@ -17,3 +17,11 @@ dependencies: - if: "target in [esp32p4]" espressif/led_strip: "^2.5.5" espressif/mdns: "^1.8.0" + # ESP-026 spike: the 1.47" touch LCD lives only on the Waveshare S3 board. LovyanGFX + # drives ESP-IDF's SPI natively, so the same Panel_ST7789 config as the Arduino Touch + # sketch carries over. Pulled only for the S3 target; gated further by the + # CONFIG_KS_TOUCH_DISPLAY Kconfig flag (a headless S3 leaves it off). + lovyan03/LovyanGFX: + version: "^1.1.16" + rules: + - if: "target in [esp32s3]" diff --git a/KitchenSync/main/ks_display.cpp b/KitchenSync/main/ks_display.cpp new file mode 100644 index 0000000..44f2203 --- /dev/null +++ b/KitchenSync/main/ks_display.cpp @@ -0,0 +1,124 @@ +// ESP-026 spike, increment 1: bring the Waveshare ESP32-S3-Touch-LCD-1.47 panel up +// under ESP-IDF. Goal of this increment is narrow and deliberate: prove that the +// LovyanGFX Panel_ST7789 + Bus_SPI config that works under Arduino also works under +// ESP-IDF's SPI driver on the S3. No touch, no live tempo yet — those are increments +// 2 and 3. If pixels appear, the one real unknown of the whole migration is dead. +// +// See ks_display.h and ADR-0009. Ported from KitchenSyncTouch/ktouch_display.cpp. +#include "sdkconfig.h" +#include "ks_display.h" + +#if CONFIG_KS_TOUCH_DISPLAY + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "driver/ledc.h" +#include "esp_log.h" + +#define LGFX_USE_V1 +#include + +#include "fw_version.h" + +static const char *TAG = "ks_display"; + +// ---- Panel: JD9853 via Panel_ST7789, 172x320 visible in 240x320 RAM. ------------- +// IDENTICAL to KitchenSyncTouch/ktouch_display.cpp (validated on glass, LNK-014). +// LovyanGFX's LGFX_Device/Panel/Bus classes are framework-agnostic and drive ESP-IDF's +// SPI2_HOST directly — this class body is copied verbatim from the Arduino build. +class LGFX : public lgfx::LGFX_Device { + lgfx::Panel_ST7789 _panel; + lgfx::Bus_SPI _bus; +public: + LGFX() { + { auto c = _bus.config(); + c.spi_host = SPI2_HOST; c.spi_mode = 0; + c.freq_write = 40000000; c.freq_read = 16000000; + c.pin_sclk = 38; c.pin_mosi = 39; c.pin_miso = -1; c.pin_dc = 45; + c.dma_channel = SPI_DMA_CH_AUTO; + _bus.config(c); _panel.setBus(&_bus); } + { auto c = _panel.config(); + c.pin_cs = 21; c.pin_rst = 40; c.pin_busy = -1; + c.memory_width = 240; c.memory_height = 320; + c.panel_width = 172; c.panel_height = 320; + c.offset_x = 34; c.offset_y = 0; + c.invert = false; c.rgb_order = false; + _panel.config(c); } + setPanel(&_panel); + } +}; + +#define LCD_BL 46 +#define SCR_W 320 +#define SCR_H 172 + +static LGFX s_lcd; + +// ---- Backlight: ledc PWM on GPIO46 (replaces Arduino analogWrite). ---------------- +#define BL_TIMER LEDC_TIMER_0 +#define BL_CHANNEL LEDC_CHANNEL_0 +#define BL_MODE LEDC_LOW_SPEED_MODE + +static void backlight_init(void) { + ledc_timer_config_t t = {}; + t.speed_mode = BL_MODE; + t.duty_resolution = LEDC_TIMER_8_BIT; + t.timer_num = BL_TIMER; + t.freq_hz = 5000; + t.clk_cfg = LEDC_AUTO_CLK; + ledc_timer_config(&t); + + ledc_channel_config_t c = {}; + c.gpio_num = LCD_BL; + c.speed_mode = BL_MODE; + c.channel = BL_CHANNEL; + c.timer_sel = BL_TIMER; + c.duty = 0; + c.hpoint = 0; + ledc_channel_config(&c); +} + +extern "C" void ks_display_set_brightness(int pct) { + if (pct < 0) pct = 0; if (pct > 100) pct = 100; + ledc_set_duty(BL_MODE, BL_CHANNEL, (uint32_t)(pct * 255 / 100)); + ledc_update_duty(BL_MODE, BL_CHANNEL); +} + +// ---- Increment 1 test frame: header + a big "OK" so a working panel is unmistakable. +static void draw_hello(void) { + s_lcd.fillScreen(TFT_BLACK); + s_lcd.setTextColor(0x838d95u, TFT_BLACK); s_lcd.setTextSize(1); + s_lcd.setCursor(8, 4); s_lcd.println("KITCHENSYNC TOUCH"); + + s_lcd.setTextColor(0xB6FF36u, TFT_BLACK); s_lcd.setTextSize(4); + s_lcd.setCursor(8, 40); s_lcd.println("ESP-IDF"); + s_lcd.setCursor(8, 84); s_lcd.println("DISPLAY OK"); + + s_lcd.setTextColor(0x6f8a4du, TFT_BLACK); s_lcd.setTextSize(1); + s_lcd.setCursor(8, SCR_H - 14); s_lcd.print("fw "); s_lcd.println(FW_VERSION); +} + +static void display_task(void *arg) { + (void)arg; + ESP_LOGI(TAG, "display_task: init panel"); + s_lcd.init(); + s_lcd.setRotation(7); // landscape 320x172, same as the Arduino build + ks_display_set_brightness(80); + draw_hello(); + ESP_LOGI(TAG, "display_task: panel up, %dx%d", (int)s_lcd.width(), (int)s_lcd.height()); + + // Increment 2 will replace this idle loop with live BPM/sync from beat_source. + for (;;) vTaskDelay(pdMS_TO_TICKS(100)); +} + +extern "C" void ks_display_start(void) { + backlight_init(); + xTaskCreate(display_task, "display", 4096, NULL, 3, NULL); +} + +#else // !CONFIG_KS_TOUCH_DISPLAY -> no display on this build + +extern "C" void ks_display_start(void) {} +extern "C" void ks_display_set_brightness(int pct) { (void)pct; } + +#endif diff --git a/KitchenSync/main/ks_display.h b/KitchenSync/main/ks_display.h new file mode 100644 index 0000000..bddc44a --- /dev/null +++ b/KitchenSync/main/ks_display.h @@ -0,0 +1,29 @@ +// ESP-026 spike: the KitchenSync Touch 1.47" LCD, driven from the ESP-IDF firmware +// instead of the Arduino KitchenSyncTouch sketch. This is the ONE genuinely new piece +// of the Touch -> ESP-IDF convergence (ADR-0009); everything else the Touch needs +// (wifi_link, ks_web, config, transport, the clock engine) already exists here. +// +// Ported from KitchenSyncTouch/ktouch_display.cpp. The panel/touch config and every +// draw call are pure LovyanGFX and carry over unchanged; only the Arduino primitives +// (analogWrite -> ledc, Wire -> i2c_master, pinMode/delay -> gpio/vTaskDelay, +// millis -> esp_timer, WiFi.status -> wifi_link) are replaced. +// +// Compiled into every target, but real only when CONFIG_KS_TOUCH_DISPLAY is set +// (Kconfig: depends on IDF_TARGET_ESP32S3). On the P4 and headless builds it is a +// no-op, so ks_main can call it unconditionally. +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +// Initialise the panel + backlight and spawn the render task. Idempotent-safe to call +// once from app_main. No-op unless CONFIG_KS_TOUCH_DISPLAY. +void ks_display_start(void); + +// Set backlight brightness (percent). No-op unless CONFIG_KS_TOUCH_DISPLAY. +void ks_display_set_brightness(int pct); + +#ifdef __cplusplus +} +#endif diff --git a/KitchenSync/main/ks_main.c b/KitchenSync/main/ks_main.c index 48e4de5..90f599b 100644 --- a/KitchenSync/main/ks_main.c +++ b/KitchenSync/main/ks_main.c @@ -30,6 +30,7 @@ #include "wifi_link.h" #include "usb_midi_host.h" #include "ks_web.h" +#include "ks_display.h" /* ESP-026: 1.47" touch LCD glue (no-op unless CONFIG_KS_TOUCH_DISPLAY) */ #include "ks_tick_health.h" /* P4-038: publish the 1 ms clock task's probe in /status */ #include "ks_config.h" #include "ks_config_nvs.h" @@ -472,6 +473,7 @@ void app_main(void) follow_beat_io_start(); /* mic-based tempo detection, display-only v1 (P4-020) */ ks_led_start(LED_GPIO, LED_PIXELS); /* WS2812 visual metronome; harmless if nothing wired (P4-018) */ midi_uart_out_start(MIDI_TX_GPIO, MIDI_STROBE_GPIO); /* ESP-015: DIN MIDI out + analyzer strobe */ + ks_display_start(); /* ESP-026: 1.47" touch LCD (no-op unless CONFIG_KS_TOUCH_DISPLAY, S3-only) */ /* P4-038: priority 19, core 1 — the ESP-018 fix, ported to the LAST firmware that * never got it. This task ran at 6, which is BELOW lwIP's tcpip_task (18): the * network stack could legally preempt the clock generator, and it did. Measured on From ff837f33d00ec5318276a4dab9b7b5d88c030957 Mon Sep 17 00:00:00 2001 From: Eric Dahl Date: Wed, 15 Jul 2026 11:36:33 -0400 Subject: [PATCH 2/9] =?UTF-8?q?spike(ESP-026):=20S3-only=20sdkconfig=20def?= =?UTF-8?q?aults=20=E2=80=94=20enable=20KS=5FTOUCH=5FDISPLAY?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sdkconfig.defaults.esp32s3 auto-loads only for the S3 target, so a fresh 'idf.py set-target esp32s3' comes up display-enabled and P4 is untouched. --- KitchenSync/sdkconfig.defaults.esp32s3 | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 KitchenSync/sdkconfig.defaults.esp32s3 diff --git a/KitchenSync/sdkconfig.defaults.esp32s3 b/KitchenSync/sdkconfig.defaults.esp32s3 new file mode 100644 index 0000000..7237f83 --- /dev/null +++ b/KitchenSync/sdkconfig.defaults.esp32s3 @@ -0,0 +1,16 @@ +# ESP-026 spike — S3-only sdkconfig defaults. +# +# ESP-IDF auto-loads this file ON TOP OF sdkconfig.defaults when, and only when, the +# target is esp32s3 (i.e. after `idf.py set-target esp32s3`). It never affects the P4 +# build. The base sdkconfig.defaults is P4-tuned; its P4-only symbols (silicon rev, +# the ESP-Hosted C6 slave, USB-host transfer size) are unknown on the S3 and are +# ignored with harmless "unknown CONFIG_*" warnings. The 16MB flash + custom OTA +# partition table it sets DO apply here and match the Waveshare S3-Touch-LCD-1.47 +# board (16MB N16R8) — the same layout KitchenSyncTouch ships (partitions_devkit.csv +# is the separate 4MB DevKit table, not this board). + +# Bring the 1.47" touch LCD up by default on the S3 so a fresh `set-target esp32s3` +# comes up display-enabled and only WiFi credentials need entering in menuconfig. +# Spike convenience: productization will gate the display on a per-board Kconfig, not +# on "every S3" — a headless S3 Super Mini has no panel. See ADR-0009 / ESP-026. +CONFIG_KS_TOUCH_DISPLAY=y From 766ba2ac7976c5de6a1d981a5160583328906697 Mon Sep 17 00:00:00 2001 From: Eric Dahl Date: Wed, 15 Jul 2026 11:54:41 -0400 Subject: [PATCH 3/9] spike(ESP-026): pull LovyanGFX as pinned git dep (1.2.21), not a registry component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LovyanGFX is not in the Espressif component registry (the ^1.1.16 registry pin failed to resolve). Pull it from GitHub pinned to tag 1.2.21 — the exact version the Arduino firmware already ships (THIRD_PARTY.md, BSD-2-Clause). Gated to the S3 target. --- KitchenSync/main/idf_component.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/KitchenSync/main/idf_component.yml b/KitchenSync/main/idf_component.yml index e30a50f..c7b9ce8 100644 --- a/KitchenSync/main/idf_component.yml +++ b/KitchenSync/main/idf_component.yml @@ -19,9 +19,12 @@ dependencies: espressif/mdns: "^1.8.0" # ESP-026 spike: the 1.47" touch LCD lives only on the Waveshare S3 board. LovyanGFX # drives ESP-IDF's SPI natively, so the same Panel_ST7789 config as the Arduino Touch - # sketch carries over. Pulled only for the S3 target; gated further by the - # CONFIG_KS_TOUCH_DISPLAY Kconfig flag (a headless S3 leaves it off). - lovyan03/LovyanGFX: - version: "^1.1.16" + # sketch carries over. It is NOT in the Espressif component registry, so pull it as a + # git dependency PINNED to tag 1.2.21 — the exact version the Arduino firmware already + # ships (THIRD_PARTY.md, BSD-2-Clause). Cloned into managed_components/ at build time, + # gated to the S3 target; CONFIG_KS_TOUCH_DISPLAY gates it further (headless S3 off). + lovyangfx: + git: https://github.com/lovyan03/LovyanGFX.git + version: "1.2.21" rules: - if: "target in [esp32s3]" From 48b1a4af6e1e8e33e8573c2765c0138076df41e5 Mon Sep 17 00:00:00 2001 From: Eric Dahl Date: Wed, 15 Jul 2026 12:03:35 -0400 Subject: [PATCH 4/9] spike(ESP-026): fix -Werror=misleading-indentation in ks_display Split the two-statement brightness clamp onto separate lines. ESP-IDF builds with -Werror=misleading-indentation (Arduino did not). Full esp32s3 build now green. --- KitchenSync/main/ks_display.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/KitchenSync/main/ks_display.cpp b/KitchenSync/main/ks_display.cpp index 44f2203..a7df0a7 100644 --- a/KitchenSync/main/ks_display.cpp +++ b/KitchenSync/main/ks_display.cpp @@ -79,7 +79,8 @@ static void backlight_init(void) { } extern "C" void ks_display_set_brightness(int pct) { - if (pct < 0) pct = 0; if (pct > 100) pct = 100; + if (pct < 0) pct = 0; + if (pct > 100) pct = 100; ledc_set_duty(BL_MODE, BL_CHANNEL, (uint32_t)(pct * 255 / 100)); ledc_update_duty(BL_MODE, BL_CHANNEL); } From 2e61f19b2ddfa912c5bcd75036e25d528c273898 Mon Sep 17 00:00:00 2001 From: Eric Dahl Date: Wed, 15 Jul 2026 12:09:53 -0400 Subject: [PATCH 5/9] =?UTF-8?q?tasks(ESP-026):=20record=20spike=20result?= =?UTF-8?q?=20=E2=80=94=20display=20up=20on=20ESP-IDF=20(verified=20on=20g?= =?UTF-8?q?lass)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tasks/ESP-026.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tasks/ESP-026.md b/tasks/ESP-026.md index a7230a8..168b450 100644 --- a/tasks/ESP-026.md +++ b/tasks/ESP-026.md @@ -174,3 +174,23 @@ The 2026-07 architecture initiative recorded why it is the highest-leverage stru [`docs/architecture/alignment-report.md`](../docs/architecture/alignment-report.md) §2.1. - It is the prerequisite that makes extracting `ks-core` out of `X32Link/` symlink-neutral — [`docs/architecture/repository-roadmap.md`](../docs/architecture/repository-roadmap.md) steps 1–2. + +## SPIKE RESULT 2026-07-15 — the display comes up on ESP-IDF + +**The one real unknown is dead.** LovyanGFX drives the Waveshare S3-Touch-LCD-1.47 +panel from the KitchenSync ESP-IDF firmware — verified on glass ("ESP-IDF DISPLAY OK"). + +What it took (spike branch `spike/esp-026-display-esp-idf`): +- `ks_display.{h,cpp}` — the LGFX `Panel_ST7789` + `Bus_SPI` config from + `ktouch_display.cpp` carries over VERBATIM; only Arduino primitives change + (analogWrite → ledc). Increment 1 is panel bring-up only (no touch/live data). +- LovyanGFX is NOT in the Espressif registry → pulled as a git dep pinned to tag + 1.2.21 (the version the Arduino firmware already ships; THIRD_PARTY.md), gated to + the esp32s3 target in `idf_component.yml`. +- `CONFIG_KS_TOUCH_DISPLAY` Kconfig flag (depends on IDF_TARGET_ESP32S3); + auto-enabled for S3 via `sdkconfig.defaults.esp32s3`. P4 build untouched. +- Full esp32s3 build green; `ks_main.c` needs one guarded call. + +Remaining (mechanical, no known unknowns): inc2 live BPM/sync from beat_source; +inc3 AXS5106L touch via i2c_master (reuse the host-tested axs5106l.c parser); +then transport intents, delete KitchenSyncTouch/, move shared C out of X32Link/. From 3423fc4c0bd1f3b307e3181c478bba3524e08da9 Mon Sep 17 00:00:00 2001 From: Eric Dahl Date: Wed, 15 Jul 2026 12:22:12 -0400 Subject: [PATCH 6/9] =?UTF-8?q?spike(ESP-026):=20display=20increment=203?= =?UTF-8?q?=20=E2=80=94=20AXS5106L=20touch=20+=20USB-Serial-JTAG=20console?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ks_display.cpp: AXS5106L over i2c_master (SDA42/SCL41, 0x63) + GPIO47 reset, reusing the host-tested axs5106l.c parser (added to CMake SRCS by path). Taps are echoed on screen ('TAP x,y') and to the log. - sdkconfig.defaults.esp32s3: route the console to USB-Serial-JTAG (the Arduino build's hwcdc). ESP-IDF's UART default left the USB port dark after boot, so the cu.usbmodem port vanished and esptool could not auto-reset. Now it persists + serial logs work. (Builds green; hardware verification pending the next flash.) --- KitchenSync/main/CMakeLists.txt | 1 + KitchenSync/main/ks_display.cpp | 68 +++++++++++++++++++++++++- KitchenSync/sdkconfig.defaults.esp32s3 | 7 +++ 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/KitchenSync/main/CMakeLists.txt b/KitchenSync/main/CMakeLists.txt index 7836734..e46fa1e 100644 --- a/KitchenSync/main/CMakeLists.txt +++ b/KitchenSync/main/CMakeLists.txt @@ -28,6 +28,7 @@ idf_component_register( # CONFIG_KS_TOUCH_DISPLAY (S3), a no-op otherwise, so ks_main calls it # unconditionally. LovyanGFX comes in via idf_component.yml (S3 target only). "ks_display.cpp" + "${X32LINK_DIR}/axs5106l.c" # ESP-026 inc3: pure AXS5106L touch-report parser (shared, host-tested) # ESP-029: /status is now a SHARED module. It moved to X32Link/ and is symlinked # into KitchenSyncTouch/, so both firmwares emit /status from ONE implementation # and cannot drift apart again (ARC-024). diff --git a/KitchenSync/main/ks_display.cpp b/KitchenSync/main/ks_display.cpp index a7df0a7..d08cfe8 100644 --- a/KitchenSync/main/ks_display.cpp +++ b/KitchenSync/main/ks_display.cpp @@ -13,12 +13,15 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/ledc.h" +#include "driver/gpio.h" +#include "driver/i2c_master.h" #include "esp_log.h" #define LGFX_USE_V1 #include #include "fw_version.h" +#include "axs5106l.h" // ESP-026 inc3: pure, host-tested AXS5106L report parser (X32Link) static const char *TAG = "ks_display"; @@ -52,7 +55,14 @@ class LGFX : public lgfx::LGFX_Device { #define SCR_W 320 #define SCR_H 172 +// Touch (AXS5106L, I2C 0x63) — same pins as KitchenSyncTouch/ktouch_display.cpp. +#define TP_SDA 42 +#define TP_SCL 41 +#define TP_RST 47 + static LGFX s_lcd; +static i2c_master_bus_handle_t s_i2c_bus = NULL; +static i2c_master_dev_handle_t s_touch = NULL; // ---- Backlight: ledc PWM on GPIO46 (replaces Arduino analogWrite). ---------------- #define BL_TIMER LEDC_TIMER_0 @@ -99,6 +109,44 @@ static void draw_hello(void) { s_lcd.setCursor(8, SCR_H - 14); s_lcd.print("fw "); s_lcd.println(FW_VERSION); } +// ---- AXS5106L touch over i2c_master (replaces Arduino Wire + pinMode/delay). ------ +static void touch_init(void) { + // Reset pulse on TP_RST, then bring up the I2C bus (order matches the Arduino glue). + gpio_config_t rst = {}; + rst.pin_bit_mask = 1ULL << TP_RST; + rst.mode = GPIO_MODE_OUTPUT; + gpio_config(&rst); + gpio_set_level((gpio_num_t)TP_RST, 0); vTaskDelay(pdMS_TO_TICKS(20)); + gpio_set_level((gpio_num_t)TP_RST, 1); vTaskDelay(pdMS_TO_TICKS(80)); + + i2c_master_bus_config_t bus = {}; + bus.i2c_port = I2C_NUM_0; + bus.sda_io_num = (gpio_num_t)TP_SDA; + bus.scl_io_num = (gpio_num_t)TP_SCL; + bus.clk_source = I2C_CLK_SRC_DEFAULT; + bus.glitch_ignore_cnt = 7; + bus.flags.enable_internal_pullup = true; + if (i2c_new_master_bus(&bus, &s_i2c_bus) != ESP_OK) { ESP_LOGE(TAG, "i2c bus init failed"); return; } + + i2c_device_config_t dev = {}; + dev.dev_addr_length = I2C_ADDR_BIT_LEN_7; + dev.device_address = AXS5106L_I2C_ADDR; + dev.scl_speed_hz = 400000; + if (i2c_master_bus_add_device(s_i2c_bus, &dev, &s_touch) != ESP_OK) + ESP_LOGE(TAG, "i2c add device failed"); +} + +// I2C read (write reg 0x01, then read the 14-byte report) + pure parse. The decode +// lives in the host-tested axs5106l.c; this glue owns only the bus transaction. +static bool read_touch(axs_touch_t *t) { + if (!s_touch) return false; + uint8_t reg = AXS5106L_TOUCH_REG; + if (i2c_master_transmit(s_touch, ®, 1, 50) != ESP_OK) return false; + uint8_t buf[AXS5106L_REPORT_LEN]; + if (i2c_master_receive(s_touch, buf, sizeof(buf), 50) != ESP_OK) return false; + return axs5106l_parse(buf, sizeof(buf), t) == 0; +} + static void display_task(void *arg) { (void)arg; ESP_LOGI(TAG, "display_task: init panel"); @@ -108,8 +156,24 @@ static void display_task(void *arg) { draw_hello(); ESP_LOGI(TAG, "display_task: panel up, %dx%d", (int)s_lcd.width(), (int)s_lcd.height()); - // Increment 2 will replace this idle loop with live BPM/sync from beat_source. - for (;;) vTaskDelay(pdMS_TO_TICKS(100)); + touch_init(); + ESP_LOGI(TAG, "display_task: touch ready — tap the glass"); + + // Increment 3: echo the last tap (raw controller coords) on screen + serial, to + // prove the AXS5106L i2c path and the shared axs5106l.c parser. Coordinate rotation + // and hit-testing (the pure ktouch_ui.c) come in a later increment. + axs_touch_t t; + for (;;) { + if (read_touch(&t) && t.points_len > 0) { + ESP_LOGI(TAG, "touch: count=%d p0=(%u,%u)", t.count, + (unsigned)t.points[0].x, (unsigned)t.points[0].y); + s_lcd.fillRect(0, 120, SCR_W, 30, TFT_BLACK); + s_lcd.setTextColor(0x36B6FFu, TFT_BLACK); s_lcd.setTextSize(2); + s_lcd.setCursor(8, 124); + s_lcd.printf("TAP %u,%u", (unsigned)t.points[0].x, (unsigned)t.points[0].y); + } + vTaskDelay(pdMS_TO_TICKS(33)); + } } extern "C" void ks_display_start(void) { diff --git a/KitchenSync/sdkconfig.defaults.esp32s3 b/KitchenSync/sdkconfig.defaults.esp32s3 index 7237f83..bc3f17d 100644 --- a/KitchenSync/sdkconfig.defaults.esp32s3 +++ b/KitchenSync/sdkconfig.defaults.esp32s3 @@ -14,3 +14,10 @@ # Spike convenience: productization will gate the display on a per-board Kconfig, not # on "every S3" — a headless S3 Super Mini has no panel. See ADR-0009 / ESP-026. CONFIG_KS_TOUCH_DISPLAY=y + +# Route the console to the S3's USB-Serial-JTAG (the same controller the Arduino +# build reached via "hardware CDC/JTAG"/hwcdc). ESP-IDF defaults the console to UART, +# which leaves the board's USB port dark once the app boots — so the cu.usbmodem port +# vanishes after boot and esptool can no longer auto-reset for the next flash. Driving +# USB-Serial-JTAG keeps the port alive (normal flash + monitor) and gives serial logs. +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y From f1b7306669a6480493826f1103513d2e99d85c23 Mon Sep 17 00:00:00 2001 From: Eric Dahl Date: Wed, 15 Jul 2026 12:28:03 -0400 Subject: [PATCH 7/9] tasks(ESP-026): touch + USB-Serial-JTAG console verified on hardware --- tasks/ESP-026.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tasks/ESP-026.md b/tasks/ESP-026.md index 168b450..9a72d2f 100644 --- a/tasks/ESP-026.md +++ b/tasks/ESP-026.md @@ -194,3 +194,19 @@ What it took (spike branch `spike/esp-026-display-esp-idf`): Remaining (mechanical, no known unknowns): inc2 live BPM/sync from beat_source; inc3 AXS5106L touch via i2c_master (reuse the host-tested axs5106l.c parser); then transport intents, delete KitchenSyncTouch/, move shared C out of X32Link/. + +### Update 2026-07-15 (later) — touch + console proven too + +- **Increment 3 (touch) verified on glass.** AXS5106L over `i2c_master` (SDA42/SCL41, + addr 0x63, GPIO47 reset) + the host-tested `axs5106l.c` parser — taps echo "TAP x,y" + on screen and track the finger. The last hardware interface is proven under ESP-IDF. +- **Console/flash path solved.** ESP-IDF's default UART console left the S3's USB port + dark after boot (port vanished, esptool couldn't auto-reset). Set + `CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y` (sdkconfig.defaults.esp32s3) — the same + controller the Arduino build used via hwcdc. Port now persists after boot; normal + flash + serial monitor restored. One-time download-mode flash was needed to apply it. + +Net: display, touch, and console all work on the ESP-IDF firmware on the S3 touch +board. No hardware unknowns remain for the migration. Remaining is mechanical: inc2 +live BPM/sync from beat_source; post transport intents into transport_intent.c; then +retire KitchenSyncTouch/ and move shared C out of X32Link/ (the ADR-0009 endgame). From 9dab29ce12e51e395dd1b33a4664d4da92830462 Mon Sep 17 00:00:00 2001 From: Eric Dahl Date: Wed, 15 Jul 2026 12:46:28 -0400 Subject: [PATCH 8/9] spike(ESP-026): map touch to landscape screen coords + dot under finger Portrait-native AXS5106L coords -> setRotation(7) landscape: sx=319-raw_y, sy=171-raw_x (verified against a corner tap). Draws a dot at the mapped point. --- KitchenSync/main/ks_display.cpp | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/KitchenSync/main/ks_display.cpp b/KitchenSync/main/ks_display.cpp index d08cfe8..a41181a 100644 --- a/KitchenSync/main/ks_display.cpp +++ b/KitchenSync/main/ks_display.cpp @@ -159,18 +159,26 @@ static void display_task(void *arg) { touch_init(); ESP_LOGI(TAG, "display_task: touch ready — tap the glass"); - // Increment 3: echo the last tap (raw controller coords) on screen + serial, to - // prove the AXS5106L i2c path and the shared axs5106l.c parser. Coordinate rotation - // and hit-testing (the pure ktouch_ui.c) come in a later increment. + // Increment 3: echo the last tap on screen + serial to prove the AXS5106L i2c path + // and the shared axs5106l.c parser. Raw controller coords are portrait-native; the + // landscape setRotation(7) maps them screen_x = 319 - raw_y, screen_y = 171 - raw_x + // (verified against a corner tap). A dot is drawn at the mapped point so it lands + // under the finger. Real hit-testing (the pure ktouch_ui.c) comes with the UI port. axs_touch_t t; + int prev_dx = -1, prev_dy = -1; for (;;) { if (read_touch(&t) && t.points_len > 0) { - ESP_LOGI(TAG, "touch: count=%d p0=(%u,%u)", t.count, - (unsigned)t.points[0].x, (unsigned)t.points[0].y); - s_lcd.fillRect(0, 120, SCR_W, 30, TFT_BLACK); + int rx = t.points[0].x, ry = t.points[0].y; + int sx = 319 - ry; if (sx < 0) sx = 0; if (sx > 319) sx = 319; + int sy = 171 - rx; if (sy < 0) sy = 0; if (sy > 171) sy = 171; + ESP_LOGI(TAG, "touch: count=%d raw=(%d,%d) screen=(%d,%d)", t.count, rx, ry, sx, sy); + if (prev_dx >= 0) s_lcd.fillCircle(prev_dx, prev_dy, 6, TFT_BLACK); + s_lcd.fillCircle(sx, sy, 6, 0x36B6FFu); + prev_dx = sx; prev_dy = sy; + s_lcd.fillRect(0, 120, 160, 26, TFT_BLACK); s_lcd.setTextColor(0x36B6FFu, TFT_BLACK); s_lcd.setTextSize(2); s_lcd.setCursor(8, 124); - s_lcd.printf("TAP %u,%u", (unsigned)t.points[0].x, (unsigned)t.points[0].y); + s_lcd.printf("%d,%d", sx, sy); } vTaskDelay(pdMS_TO_TICKS(33)); } From 9b326fc207ea6ce58ef24d867ea5e89739baafb8 Mon Sep 17 00:00:00 2001 From: Eric Dahl Date: Fri, 17 Jul 2026 00:28:53 -0400 Subject: [PATCH 9/9] docs(ESP-026): re-verify display spike on master + record the download-mode gotcha MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebased the display spike onto master (clean), rebuilt esp32s3, reflashed the Waveshare S3-Touch — panel shows "ESP-IDF DISPLAY OK". Landing the spike. Note the board latches DOWNLOAD mode on every host-issued USB reset (esptool/monitor/DTR), so a fresh flash boots dark and silent until a physical power-cycle — looks like a crash, isn't one. Claude-Session: https://claude.ai/code/session_01F7YsQgso3KNpeXbP5vNVj6 --- tasks/ESP-026.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tasks/ESP-026.md b/tasks/ESP-026.md index 9a72d2f..f4ff262 100644 --- a/tasks/ESP-026.md +++ b/tasks/ESP-026.md @@ -210,3 +210,18 @@ Net: display, touch, and console all work on the ESP-IDF firmware on the S3 touc board. No hardware unknowns remain for the migration. Remaining is mechanical: inc2 live BPM/sync from beat_source; post transport intents into transport_intent.c; then retire KitchenSyncTouch/ and move shared C out of X32Link/ (the ADR-0009 endgame). + +### Re-verified 2026-07-17 — spike rebased onto master, still green on glass + +Rebased `spike/esp-026-display-esp-idf` onto current master (clean, no conflicts), rebuilt +esp32s3, reflashed the Waveshare S3-Touch — panel shows "ESP-IDF DISPLAY OK". Landing the +spike (it had sat unmerged). One hardware gotcha cost real time and is worth flagging for +anyone bringing this board up headless: + +- **This S3-Touch board latches DOWNLOAD mode on every host-issued USB reset.** `esptool + --after hard_reset`, `idf.py monitor`'s connect-reset, and raw DTR/RTS pulses all landed + it in `boot:0x22 (DOWNLOAD(USB/UART0))` — the app never ran, screen dark, serial silent. + It looked exactly like a firmware crash and is not one. **Only a physical power-cycle (or + RST with BOOT released) reads the straps clean and runs the app.** No software-over-USB + path gets past a wedged BOOT strap. If a fresh flash boots dark, power-cycle before + debugging firmware.