From b1fe519e59fc45c9b88dd85a973c13c9ee7aec3a Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Mon, 26 Oct 2026 18:23:00 +0100 Subject: [PATCH 1/5] More compact logo --- Buildscripts/logo.cmake | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/Buildscripts/logo.cmake b/Buildscripts/logo.cmake index cfb5d3454..0c617326b 100644 --- a/Buildscripts/logo.cmake +++ b/Buildscripts/logo.cmake @@ -3,9 +3,9 @@ get_filename_component(VERSION_TEXT_FILE version.txt ABSOLUTE) file(READ ${VERSION_TEXT_FILE} TACTILITY_VERSION) if (DEFINED ENV{ESP_IDF_VERSION}) - set(TACTILITY_TARGET " @ ESP-IDF") + set(TACTILITY_TARGET "ESP-IDF") else () - set(TACTILITY_TARGET " @ Simulator") + set(TACTILITY_TARGET "Simulator") endif () string(ASCII 27 Esc) @@ -18,19 +18,12 @@ set(White "${Esc}[1;37m") # Some terminals (e.g. GitHub Actions) reset colour for every in a multiline message(), # so we add the colour to each line instead of assuming it would automatically be re-used. message("\n\n\ - ${LightPurple}@@\n\ - ${LightPurple}@@@\n\ - ${LightPurple}@@@\n\ - ${LightPurple}@@@\n\ - ${LightPurple}@@@\n\ - ${Cyan}@@@@@@@@@@@@@@@@${LightPurple}@@@\n\ - ${Cyan}@@@@@@@@@@@@@@@@@${LightPurple}@@@\n\ - ${Cyan}@@@ ${LightPurple}@@@ ${White}Tactility ${TACTILITY_VERSION}\n\ - ${Cyan}@@@ ${LightPurple}@@@ ${Grey}${TACTILITY_TARGET}\n\ - ${Cyan}@@@${LightPurple}@@@@@@@@@@@@@@@@@\n\ - ${Cyan}@@@${LightPurple}@@@@@@@@@@@@@@@@\n\ - ${Cyan}@@@\n\ - ${Cyan}@@@\n\ - ${Cyan}@@@\n\ - ${Cyan}@@@\n\ - ${Cyan}@@\n\n${ColorReset}") + ${LightPurple}▙\n\ + ${LightPurple}█\n\ + ${LightPurple}█\n\ + ${Cyan}▂▄▄▄▄▄▄▄▄▄${LightPurple}█\n\ + ${Cyan}█ ${LightPurple}█ ${White}Tactility ${Cyan}${TACTILITY_VERSION}${Grey} with ${Cyan}${TACTILITY_TARGET}\n\ + ${Cyan}█${LightPurple}▀▀▀▀▀▀▀▀▀🮂\n\ + ${Cyan}█\n\ + ${Cyan}█\n\ + ${Cyan}▜${ColorReset}\n") From 5cd0738005ad2e9c214c7d847b1f906e5d06e0d9 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Mon, 26 Oct 2026 22:43:29 +0100 Subject: [PATCH 2/5] ideas.md cleanup --- Documentation/ideas.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Documentation/ideas.md b/Documentation/ideas.md index 5f6b52312..204c88fa5 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -22,13 +22,9 @@ - Pick different font to improve monochrome screen readability (or change the weight). - Use GPS time to set/update the current time: when enabled, at boot, when wifi is not enabled, start the gps device and wait until a lock is acquired, then get the time and sync it. Then stop the GPS device. - Try https://github.com/bootdsc/Fastwave-Eink-Driver with CL-32 e-paper. -- Power Off with e-paper should trigger full refresh before power off. - Inspect code for memory improvements: replace `std::*` with `tt::std::*` -- Tactility/Source/bluetooth move into bluetooth module, make it work generically for all bluetooth apis -- DisplayApi: resolution x/y shouldn't be functions but struct values - Tactility/Source/app/imageviewer/ImageViewer.cpp: Limit memory used to open an image. loadFile() now resizes fileData to the entire file size before displaying the image. start accepts a file path without a size limit, so opening a large image can exhaust memory on an embedded device before LVGL decodes it. Reject files above a supported limit or retain a bounded file-backed decoding path. -- A device profile selection screen for Simulator: select device profiles such as T-Lora Pager, T-Deck Plus, CL-32, Cardputer and T-Embed which changes the resolution and theming defaults after restarting. - A fullscreen tiny keyboard that works with single stroke gestures (similar to Palm OS Graffiti) - Move more window management code from Tactility/ to window-manager - Implement signal/raise for terminal apps (for CTRL+C and window resize signals) @@ -47,7 +43,6 @@ Create some kind of "intent" handler like on Android. The intent can have an action (e.g. view), a URL and an optional bundle. The manifest can provide the intent handler -- LilyGO T-Dongle S3: 1 button control, stop auto-launching web server - Core2: support power off via software - Try out speed optimizations: https://docs.espressif.com/projects/esp-faq/en/latest/software-framework/peripherals/lcd.html (relates to CONFIG_ESP32S3_DATA_CACHE_LINE_64B that is in use for RGB displays via the `device.properties` fix/workaround) From af53c332a6c5050af30b5c22049faac45a02fa98 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Tue, 27 Oct 2026 18:15:58 +0100 Subject: [PATCH 3/5] elf_loader 2.0.0 --- .../source/app_esp32_loader_service.cpp | 2 +- Modules/app-esp32-module/source/elf_cache.cpp | 14 +++++++++----- Tactility/idf_component.yml | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Modules/app-esp32-module/source/app_esp32_loader_service.cpp b/Modules/app-esp32-module/source/app_esp32_loader_service.cpp index b7283b809..cb1dd7973 100644 --- a/Modules/app-esp32-module/source/app_esp32_loader_service.cpp +++ b/Modules/app-esp32-module/source/app_esp32_loader_service.cpp @@ -156,7 +156,7 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) { } module_refs_begin(&runtime->module_refs); - const int relocate_result = esp_elf_relocate(&runtime->elf, runtime->file_data); + const int relocate_result = esp_elf_relocate(&runtime->elf, runtime->file_data, size); module_refs_end(); if (relocate_result != 0) { // esp_elf_relocate() already frees elf->pdata/ptext itself on a relocation failure diff --git a/Modules/app-esp32-module/source/elf_cache.cpp b/Modules/app-esp32-module/source/elf_cache.cpp index c39e4d76b..dc06a05c0 100644 --- a/Modules/app-esp32-module/source/elf_cache.cpp +++ b/Modules/app-esp32-module/source/elf_cache.cpp @@ -3,6 +3,8 @@ #ifdef CONFIG_ELF_LOADER_LOAD_PSRAM +#include +#include #include #include @@ -15,14 +17,15 @@ // only runs when its core is not in a critical section, which is where esp_cache_msync() does its work. extern "C" { -void __real_esp_elf_arch_flush(void); +int __real_esp_elf_arch_flush(esp_elf_t* elf); void Cache_WriteBack_All(void); void spi_flash_disable_interrupts_caches_and_other_cpu(void); void spi_flash_enable_interrupts_caches_and_other_cpu(void); } -#if !CONFIG_FREERTOS_UNICORE && !CONFIG_IDF_TARGET_ESP32S31 +// ESP32-S3 on ESP-IDF >= 5.2 syncs only the text range with esp_cache_msync(), which takes its lock +#if !CONFIG_FREERTOS_UNICORE && !CONFIG_IDF_TARGET_ESP32S31 && !(CONFIG_IDF_TARGET_ESP32S3 && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 2, 0)) namespace { @@ -52,17 +55,18 @@ void IRAM_ATTR write_back_with_core1_parked(void*) { } // namespace // IRAM: spi_flash_disable_interrupts_caches_and_other_cpu() returns here with the caches disabled -extern "C" void IRAM_ATTR __wrap_esp_elf_arch_flush(void) { +extern "C" int IRAM_ATTR __wrap_esp_elf_arch_flush(esp_elf_t*) { esp_ipc_call_blocking(0, write_back_with_core1_parked, nullptr); // Same as elf_loader's own esp_elf_arch_flush() after its write-back spi_flash_disable_interrupts_caches_and_other_cpu(); spi_flash_enable_interrupts_caches_and_other_cpu(); + return 0; } #else -extern "C" void IRAM_ATTR __wrap_esp_elf_arch_flush(void) { - __real_esp_elf_arch_flush(); +extern "C" int IRAM_ATTR __wrap_esp_elf_arch_flush(esp_elf_t* elf) { + return __real_esp_elf_arch_flush(elf); } #endif diff --git a/Tactility/idf_component.yml b/Tactility/idf_component.yml index 12f7e7b27..4720fd875 100644 --- a/Tactility/idf_component.yml +++ b/Tactility/idf_component.yml @@ -30,7 +30,7 @@ dependencies: version: "2.0.0" rules: - if: "target in [esp32, esp32s3]" - espressif/elf_loader: "1.3.3" + espressif/elf_loader: "2.0.0" espressif/esp_lcd_touch: "1.2.1" atanisoft/esp_lcd_touch_xpt2046: version: "1.0.6" From cf6df29c1f6f35a32025c52575ef64fcac9d9b1f Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Tue, 27 Oct 2026 18:16:20 +0100 Subject: [PATCH 4/5] Update ideas.md --- Documentation/ideas.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Documentation/ideas.md b/Documentation/ideas.md index 204c88fa5..63784fb7e 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -7,6 +7,7 @@ ## Higher Priority +- Fix system icon selection on mono display - Find out why the first build of an app with a clean build cache folder always fails - Improve posix support: use user home path and system /tmp folder. - Drivers/audio-codec-module is not a module. Move it somewhere else. Or make it an actual module. @@ -19,9 +20,9 @@ ## Medium Priority +- Try https://github.com/bootdsc/Fastwave-Eink-Driver with CL-32 e-paper. - Pick different font to improve monochrome screen readability (or change the weight). - Use GPS time to set/update the current time: when enabled, at boot, when wifi is not enabled, start the gps device and wait until a lock is acquired, then get the time and sync it. Then stop the GPS device. -- Try https://github.com/bootdsc/Fastwave-Eink-Driver with CL-32 e-paper. - Inspect code for memory improvements: replace `std::*` with `tt::std::*` - Tactility/Source/app/imageviewer/ImageViewer.cpp: Limit memory used to open an image. loadFile() now resizes fileData to the entire file size before displaying the image. start accepts a file path without a size limit, so opening a large image can exhaust memory on an embedded device before LVGL decodes it. Reject files above a supported limit or retain a bounded file-backed decoding path. From 386be93ee10b12e53acdc2d1fe6528f2e05dd102 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Tue, 27 Oct 2026 18:17:34 +0100 Subject: [PATCH 5/5] Auto-detect display for CL-32 v0.4 --- Devices/cl32-v04/CMakeLists.txt | 2 +- Devices/cl32-v04/cl32-v04.dts | 13 +- Devices/cl32-v04/device.properties | 2 +- Devices/cl32-v04/module.yaml | 1 + Devices/cl32-v04/source/cl32_v4_display.cpp | 223 ++++++++++++++++++++ Devices/cl32-v04/source/cl32_v4_display.h | 7 + Devices/cl32-v04/source/module.cpp | 16 +- 7 files changed, 249 insertions(+), 15 deletions(-) create mode 100644 Devices/cl32-v04/source/cl32_v4_display.cpp create mode 100644 Devices/cl32-v04/source/cl32_v4_display.h diff --git a/Devices/cl32-v04/CMakeLists.txt b/Devices/cl32-v04/CMakeLists.txt index 09a2fdee3..bbc2ded00 100644 --- a/Devices/cl32-v04/CMakeLists.txt +++ b/Devices/cl32-v04/CMakeLists.txt @@ -3,5 +3,5 @@ file(GLOB_RECURSE SOURCE_FILES source/*.c*) idf_component_register( SRCS ${SOURCE_FILES} INCLUDE_DIRS "source" - REQUIRES TactilityKernel + REQUIRES TactilityKernel platform-esp32 ssd16xx-module st7305-module driver ) diff --git a/Devices/cl32-v04/cl32-v04.dts b/Devices/cl32-v04/cl32-v04.dts index 8365b152d..5ec12862e 100644 --- a/Devices/cl32-v04/cl32-v04.dts +++ b/Devices/cl32-v04/cl32-v04.dts @@ -14,7 +14,6 @@ #include #include #include -#include #include / { @@ -93,6 +92,8 @@ i2cClockFrequency = <100000>; }; + // The display is either an ST7305 reflective LCD or an SSD16xx e-paper panel. It is created + // dynamically based on hardware detection, see cl32_v4_display.cpp. spi1 { compatible = "espressif,esp32-spi"; host = ; @@ -107,16 +108,6 @@ compatible = "espressif,esp32-sdspi"; frequency-khz = <20000>; }; - - display@1 { - compatible = "sitronix,st7305"; - pin-dc = <&gpio0 13 GPIO_FLAG_NONE>; - pin-reset = <&gpio0 12 GPIO_FLAG_NONE>; - pixel-clock-hz = <80000000>; - mirror-x; - mirror-y; - backlight = <&back_light>; - }; }; // Optional Mesh Card with a Seeed Wio-SX1262 module, detected at runtime. diff --git a/Devices/cl32-v04/device.properties b/Devices/cl32-v04/device.properties index 0e18f75a0..a20930527 100644 --- a/Devices/cl32-v04/device.properties +++ b/Devices/cl32-v04/device.properties @@ -20,4 +20,4 @@ lvgl.colorDepth=8 lvgl.theme=Mono lvgl.uiDensity=compact -cdn.infoMessage=This variant supports hardware revision v0.4 with the reflective LCD. +cdn.infoMessage=This variant supports hardware revision v0.4 with the reflective LCD or e-paper display. diff --git a/Devices/cl32-v04/module.yaml b/Devices/cl32-v04/module.yaml index 8058e3f65..da570449a 100644 --- a/Devices/cl32-v04/module.yaml +++ b/Devices/cl32-v04/module.yaml @@ -1,6 +1,7 @@ dependencies: - Platforms/platform-esp32 - Drivers/bm8563-module + - Drivers/ssd16xx-module - Drivers/st7305-module - Drivers/sx126x-module bindings: bindings diff --git a/Devices/cl32-v04/source/cl32_v4_display.cpp b/Devices/cl32-v04/source/cl32_v4_display.cpp new file mode 100644 index 000000000..ea2635c26 --- /dev/null +++ b/Devices/cl32-v04/source/cl32_v4_display.cpp @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "cl32_v4_display.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +constexpr auto* TAG = "cl32-v4-display"; + +constexpr gpio_num_t PIN_CS = GPIO_NUM_6; +constexpr gpio_num_t PIN_RESET = GPIO_NUM_12; +constexpr gpio_num_t PIN_DC = GPIO_NUM_13; +constexpr gpio_num_t PIN_BUSY = GPIO_NUM_14; +constexpr uint32_t DISPLAY_CS_INDEX = 1; + +static St7305Config st7305_config {}; +static Ssd16xxConfig ssd16xx_config {}; +static Device display_device {}; +static bool display_created = false; + +// The probe-once latch for on_spi1_started(). File-scope so the teardown can reset it for a later start/probe cycle. +static bool did_probe = false; + +static void send_command(spi_device_handle_t spi, uint8_t command) { + spi_transaction_t transaction = {}; + transaction.flags = SPI_TRANS_USE_TXDATA; + transaction.length = 8; + transaction.tx_data[0] = command; + gpio_set_level(PIN_CS, 0); + gpio_set_level(PIN_DC, 0); + spi_device_polling_transmit(spi, &transaction); + gpio_set_level(PIN_CS, 1); +} + +static bool wait_for_busy_high(TickType_t timeout) { + const TickType_t start = xTaskGetTickCount(); + while (xTaskGetTickCount() - start <= timeout) { + if (gpio_get_level(PIN_BUSY)) { + return true; + } + vTaskDelay(1); + } + return false; +} + +// Only the SSD16xx drives the BUSY line: it goes high during the software reset. +// The pin is pulled down so it stays low when an ST7305 is connected. +static bool detect_ssd16xx(Device* spi1) { + const auto* spi_config = static_cast(spi1->config); + + spi_device_interface_config_t device_config = {}; + device_config.clock_speed_hz = 4000000; + device_config.mode = 0; + device_config.spics_io_num = -1; + device_config.queue_size = 1; + spi_device_handle_t spi; + if (spi_bus_add_device(spi_config->host, &device_config, &spi) != ESP_OK) { + LOG_E(TAG, "Failed to add SPI device for detection"); + return false; + } + + gpio_config_t outputs = { + .pin_bit_mask = (1ULL << PIN_CS) | (1ULL << PIN_DC) | (1ULL << PIN_RESET), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + gpio_config(&outputs); + gpio_set_level(PIN_CS, 1); + + gpio_config_t input = { + .pin_bit_mask = 1ULL << PIN_BUSY, + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_ENABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + gpio_config(&input); + + gpio_set_level(PIN_RESET, 0); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(PIN_RESET, 1); + vTaskDelay(pdMS_TO_TICKS(10)); + + spi_controller_lock(spi1); + send_command(spi, 0x12); + const bool busy = wait_for_busy_high(pdMS_TO_TICKS(20)); + spi_controller_unlock(spi1); + + spi_bus_remove_device(spi); + gpio_reset_pin(PIN_BUSY); + return busy; +} + +static bool create_display(Device* spi1, bool is_ssd16xx) { + Device* gpio0 = nullptr; + if (device_get_by_name("gpio0", &gpio0) != ERROR_NONE) { + LOG_E(TAG, "gpio0 not found"); + return false; + } + + const char* compatible; + void* config; + if (is_ssd16xx) { + ssd16xx_config = Ssd16xxConfig { + .pin_dc = { gpio0, PIN_DC, GPIO_FLAG_NONE }, + .pin_reset = { gpio0, PIN_RESET, GPIO_FLAG_NONE }, + .pin_busy = { gpio0, PIN_BUSY, GPIO_FLAG_NONE }, + .pin_cs = { gpio0, PIN_CS, GPIO_FLAG_NONE }, + .clock_speed_hz = 4000000, + .panel_type = "gdey029t71h", + .rotation = 1, + }; + compatible = "solomon-systech,ssd16xx"; + config = &ssd16xx_config; + } else { + Device* backlight = nullptr; + if (device_get_by_name("back_light", &backlight) != ERROR_NONE) { + LOG_W(TAG, "back_light not found"); + } + st7305_config = St7305Config { + .horizontal_resolution = 384, + .vertical_resolution = 168, + .column_offset = 23, + .mirror_x = true, + .mirror_y = true, + .invert_color = false, + .pixel_clock_hz = 80000000, + .pin_dc = { gpio0, PIN_DC, GPIO_FLAG_NONE }, + .pin_reset = { gpio0, PIN_RESET, GPIO_FLAG_NONE }, + .backlight = backlight, + }; + compatible = "sitronix,st7305"; + config = &st7305_config; + } + + display_device = Device { + .address = DISPLAY_CS_INDEX, + .name = "display", + .config = config, + .parent = nullptr, + .flags = 0, + .internal = nullptr, + }; + + error_t error = device_construct(&display_device); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to construct display: %s", error_to_string(error)); + return false; + } + + device_set_parent(&display_device, spi1); + + Driver* driver = driver_find_compatible(compatible); + if (driver == nullptr) { + LOG_E(TAG, "No driver registered for %s", compatible); + device_destruct(&display_device); + return false; + } + device_set_driver(&display_device, driver); + + error = device_add(&display_device); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to add display: %s", error_to_string(error)); + device_destruct(&display_device); + return false; + } + + error = device_start(&display_device); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to start display: %s", error_to_string(error)); + device_remove(&display_device); + device_destruct(&display_device); + return false; + } + + display_created = true; + return true; +} + +// Fires for every device's start/stop in the system. +static void on_spi1_started(Device* device, DeviceEvent event, void* context) { + (void)context; + + if (did_probe || event != DEVICE_EVENT_STARTED || strcmp(device->name, "spi1") != 0) { + return; + } + did_probe = true; + + const bool is_ssd16xx = detect_ssd16xx(device); + LOG_I(TAG, "Detected %s display", is_ssd16xx ? "SSD16xx e-paper" : "ST7305 LCD"); + create_display(device, is_ssd16xx); +} + +void cl32_v4_display_detect_start() { + device_listener_add(on_spi1_started, nullptr); +} + +void cl32_v4_display_detect_stop() { + device_listener_remove(on_spi1_started, nullptr); +} + +void cl32_v4_display_teardown() { + if (display_created) { + device_stop(&display_device); + device_remove(&display_device); + device_destruct(&display_device); + display_created = false; + } + did_probe = false; +} diff --git a/Devices/cl32-v04/source/cl32_v4_display.h b/Devices/cl32-v04/source/cl32_v4_display.h new file mode 100644 index 000000000..6913c9514 --- /dev/null +++ b/Devices/cl32-v04/source/cl32_v4_display.h @@ -0,0 +1,7 @@ +#pragma once + +void cl32_v4_display_detect_start(); +void cl32_v4_display_detect_stop(); + +/** Stops, removes and destructs the detected display device. */ +void cl32_v4_display_teardown(); diff --git a/Devices/cl32-v04/source/module.cpp b/Devices/cl32-v04/source/module.cpp index cea5887ef..f94608fa8 100644 --- a/Devices/cl32-v04/source/module.cpp +++ b/Devices/cl32-v04/source/module.cpp @@ -1,11 +1,23 @@ #include +#include "cl32_v4_display.h" #include "cl32_v4_light.h" #include "cl32_v4_keyboard.h" #include "cl32_v4_power.h" extern "C" { +static error_t start() { + cl32_v4_display_detect_start(); + return ERROR_NONE; +} + +static error_t stop() { + cl32_v4_display_detect_stop(); + cl32_v4_display_teardown(); + return ERROR_NONE; +} + static Driver* const cl32_drivers[] = { &cl32_v4_power_driver, &cl32_v4_power_supply_driver, @@ -16,8 +28,8 @@ static Driver* const cl32_drivers[] = { Module cl32_v04_module = { .name = "cl32", - .start = nullptr, - .stop = nullptr, + .start = start, + .stop = stop, .drivers = cl32_drivers, .symbols = nullptr, .internal = nullptr