diff --git a/KitchenSync/main/CMakeLists.txt b/KitchenSync/main/CMakeLists.txt index f4ad013..e46fa1e 100644 --- a/KitchenSync/main/CMakeLists.txt +++ b/KitchenSync/main/CMakeLists.txt @@ -24,6 +24,11 @@ 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" + "${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/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..c7b9ce8 100644 --- a/KitchenSync/main/idf_component.yml +++ b/KitchenSync/main/idf_component.yml @@ -17,3 +17,14 @@ 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. 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]" diff --git a/KitchenSync/main/ks_display.cpp b/KitchenSync/main/ks_display.cpp new file mode 100644 index 0000000..a41181a --- /dev/null +++ b/KitchenSync/main/ks_display.cpp @@ -0,0 +1,197 @@ +// 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 "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"; + +// ---- 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 + +// 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 +#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); +} + +// ---- 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"); + 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()); + + touch_init(); + ESP_LOGI(TAG, "display_task: touch ready — tap the glass"); + + // 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) { + 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("%d,%d", sx, sy); + } + vTaskDelay(pdMS_TO_TICKS(33)); + } +} + +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 diff --git a/KitchenSync/sdkconfig.defaults.esp32s3 b/KitchenSync/sdkconfig.defaults.esp32s3 new file mode 100644 index 0000000..bc3f17d --- /dev/null +++ b/KitchenSync/sdkconfig.defaults.esp32s3 @@ -0,0 +1,23 @@ +# 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 + +# 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 diff --git a/tasks/ESP-026.md b/tasks/ESP-026.md index a7230a8..f4ff262 100644 --- a/tasks/ESP-026.md +++ b/tasks/ESP-026.md @@ -174,3 +174,54 @@ 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/. + +### 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). + +### 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.