Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions KitchenSync/main/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
10 changes: 10 additions & 0 deletions KitchenSync/main/Kconfig.projbuild
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 11 additions & 0 deletions KitchenSync/main/idf_component.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]"
197 changes: 197 additions & 0 deletions KitchenSync/main/ks_display.cpp
Original file line number Diff line number Diff line change
@@ -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 <LovyanGFX.hpp>

#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, &reg, 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
29 changes: 29 additions & 0 deletions KitchenSync/main/ks_display.h
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions KitchenSync/main/ks_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions KitchenSync/sdkconfig.defaults.esp32s3
Original file line number Diff line number Diff line change
@@ -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
51 changes: 51 additions & 0 deletions tasks/ESP-026.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading