From 69b477fe1ca2587469624b33574d5d43a3d12b8d Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Sat, 8 Aug 2026 02:04:20 +1000 Subject: [PATCH 1/6] PaperS3 Improvements And Tab5 keyboard improvements Usb host keyboard improvements SYMBOLS Requires the device.py change from CL-32 PR but that only affects visuals and not build breaking --- .../source/drivers/papers3_display.cpp | 98 +++++++++---------- .../Source/devices/tab5_keyboard.cpp | 80 ++++++++++----- .../include/lvgl/devices/display.h | 7 ++ .../lvgl-module/source/devices/devices.cpp | 4 +- .../lvgl-module/source/devices/display.cpp | 24 +++-- .../source/drivers/usb/esp32_usbhost_hid.cpp | 20 +++- TactilityC/Source/symbols/esp_http_client.cpp | 4 + TactilityC/Source/symbols/freertos.cpp | 6 ++ TactilityC/Source/tt_init.cpp | 44 +++++++++ .../include/tactility/drivers/display.h | 13 ++- .../include/tactility/drivers/keyboard.h | 17 ++++ .../include/tactility/drivers/usb_host_hid.h | 11 ++- TactilityKernel/source/drivers/keyboard.cpp | 6 ++ TactilityKernel/source/symbols.c | 1 + 14 files changed, 246 insertions(+), 89 deletions(-) diff --git a/Devices/m5stack-papers3/source/drivers/papers3_display.cpp b/Devices/m5stack-papers3/source/drivers/papers3_display.cpp index d127bafce..7a6a0f77c 100644 --- a/Devices/m5stack-papers3/source/drivers/papers3_display.cpp +++ b/Devices/m5stack-papers3/source/drivers/papers3_display.cpp @@ -11,48 +11,35 @@ #include #include +#include + #include #include #define TAG "Papers3Display" #define GET_CONFIG(device) (static_cast((device)->config)) -// Maps each src byte (8px, MSB-first, bit=1 -> white/0x0F) to the 4 packed dst bytes -// (2px/byte, EPDiy MODE_PACKING_2PPB nibble order) it produces, replacing a per-pixel -// branch loop with a table lookup. -static uint32_t s_unpack_lut[256]; - -static void init_unpack_lut() { - for (uint32_t byte = 0; byte < 256; byte++) { - uint8_t dst[4]; - for (int32_t pair = 0; pair < 4; pair++) { - const uint8_t bit0 = (byte >> (7 - pair * 2)) & 0x01U; - const uint8_t bit1 = (byte >> (7 - pair * 2 - 1)) & 0x01U; - const uint8_t p0 = bit0 ? 0x0FU : 0x00U; - const uint8_t p1 = bit1 ? 0x0FU : 0x00U; - dst[pair] = static_cast((p1 << 4U) | p0); - } - memcpy(&s_unpack_lut[byte], dst, sizeof(dst)); - } -} - extern "C" { extern Module m5stack_papers3_module; -// epd_hl_init() sets an internal already_initialized flag and has no matching deinit, so the -// highlevel state (and the framebuffer it owns) must persist across stop()/start() cycles and be -// reused rather than recreated - ported from the old deprecated-HAL EpdiyDisplay's identical -// s_hlInitialized/s_hlState statics. +// epd_hl_init() has no matching deinit and sets an internal already_initialized flag, so the +// highlevel state must persist across stop()/start() cycles and be reused rather than recreated. static bool s_hl_initialized = false; static EpdiyHighlevelState s_hl_state = {}; +// Partial tile updates (see draw_bitmap()) never get a real quality pass, so a faint boot-logo +// ghost lingers after boot. One epd_fullclear() shortly after clears it. +static constexpr int DRAWS_UNTIL_POST_BOOT_CLEAR = 12; + struct Papers3DisplayInternal { EpdiyHighlevelState hl_state; uint8_t* framebuffer; - // Scratch buffer for the I1(1bpp)->EPDiy(4bpp packed, 2px/byte) conversion in draw_bitmap(). + // Scratch buffer for the grayscale8->EPDiy(4bpp packed, 2px/byte) conversion in draw_bitmap(). uint8_t* packed_buffer; bool powered; + int draw_count; + bool post_boot_clear_done; }; static void power_on(Papers3DisplayInternal* internal) { @@ -83,9 +70,9 @@ static error_t papers3_display_init(Device* device) { return ERROR_NONE; } -// LVGL only ever calls this with the full frame: DISPLAY_COLOR_FORMAT_MONOCHROME forces -// LV_DISPLAY_RENDER_MODE_FULL in the generic kernel LVGL bridge (lvgl_display.c), and FULL mode -// only presents (calls draw_bitmap) once per render cycle, with the complete 0,0..hres,vres rect. +// Reports GRAYSCALE8 (not MONOCHROME) so LVGL uses partial/tile updates instead of forcing +// full-frame - the bridge hardcodes full-frame for MONOCHROME/I1 regardless of capability flags. +// So draw_bitmap is called once per changed tile, not necessarily the whole panel. static error_t papers3_display_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) { auto* internal = static_cast(device_get_driver_data(device)); const auto* config = GET_CONFIG(device); @@ -93,32 +80,26 @@ static error_t papers3_display_draw_bitmap(Device* device, int32_t x_start, int3 const int32_t width = x_end - x_start; const int32_t height = y_end - y_start; - // color_data is DISPLAY_COLOR_FORMAT_MONOCHROME: row-major, MSB-first 1bpp (LVGL's LV_COLOR_FORMAT_I1 - // with the palette header already stripped by the caller). Bit 1 = white/lit (LVGL's I1 blend - // sets a bit when the source luminance is above its threshold), bit 0 = black. + // color_data is DISPLAY_COLOR_FORMAT_GRAYSCALE8: row-major, 1 byte/pixel luminance + // (0x00=black..0xFF=white, matching LVGL's L8). EPDiy wants 4bpp packed (2px/byte, 0x0=black, + // 0xF=white) - a plain >>4 truncation preserves all 16 real gray levels the panel supports + // (this panel is not B/W-only; see MODE_GC16/GL16 in papers3-display.yaml's draw-mode doc). const auto* src = static_cast(color_data); - const size_t src_stride = static_cast(width + 7) / 8; + const size_t src_stride = static_cast(width); const size_t packed_stride = static_cast(width + 1) / 2; for (int32_t row = 0; row < height; row++) { const uint8_t* src_row = src + static_cast(row) * src_stride; uint8_t* dst_row = internal->packed_buffer + static_cast(row) * packed_stride; int32_t col = 0; - // Bulk path: one LUT lookup + 4-byte copy per 8 source pixels. - for (; col + 8 <= width; col += 8) { - memcpy(dst_row + col / 2, &s_unpack_lut[src_row[col / 8]], 4); - } - // Tail: fewer than 8 pixels left (width not a multiple of 8). - for (; col < width; col += 2) { - const uint8_t bit0 = (src_row[col / 8] >> (7 - (col % 8))) & 0x01U; - const uint8_t p0 = bit0 ? 0x0FU : 0x00U; - uint8_t p1 = 0; - if (col + 1 < width) { - const uint8_t bit1 = (src_row[(col + 1) / 8] >> (7 - ((col + 1) % 8))) & 0x01U; - p1 = bit1 ? 0x0FU : 0x00U; - } + for (; col + 2 <= width; col += 2) { + const uint8_t p0 = src_row[col] >> 4U; + const uint8_t p1 = src_row[col + 1] >> 4U; dst_row[col / 2] = static_cast((p1 << 4U) | p0); } + if (col < width) { // odd width: last column has no pair, low nibble unused + dst_row[col / 2] = static_cast(src_row[col] >> 4U); + } } const EpdRect update_area = { @@ -137,6 +118,13 @@ static error_t papers3_display_draw_bitmap(Device* device, int32_t x_start, int3 update_area ); + // See DRAWS_UNTIL_POST_BOOT_CLEAR's comment: run the ghost-clearing full pass after boot's + // draws so it doesn't delay the boot splash/launcher content from appearing first. + if (!internal->post_boot_clear_done && ++internal->draw_count >= DRAWS_UNTIL_POST_BOOT_CLEAR) { + internal->post_boot_clear_done = true; + epd_fullclear(&internal->hl_state, config->temperature_celsius); + } + return draw_result == EPD_DRAW_SUCCESS ? ERROR_NONE : ERROR_RESOURCE; } @@ -152,7 +140,7 @@ static error_t papers3_display_disp_on_off(Device* device, bool on_off) { } static DisplayColorFormat papers3_display_get_color_format(Device*) { - return DISPLAY_COLOR_FORMAT_MONOCHROME; + return DISPLAY_COLOR_FORMAT_GRAYSCALE8; } // epd_width()/epd_height() are the panel's native, unrotated dimensions (display->width/height in @@ -172,7 +160,7 @@ static uint16_t papers3_display_get_resolution_y(Device*) { static void papers3_display_get_frame_buffer(Device*, uint8_t, void** out_buffer) { // Not exposed via the generic fb-direct path: EPDiy's framebuffer is its own 4bpp packed - // format, not the DISPLAY_COLOR_FORMAT_MONOCHROME (1bpp) this driver reports - see + // format, not the DISPLAY_COLOR_FORMAT_GRAYSCALE8 (1 byte/pixel) this driver reports - see // get_frame_buffer_count() and draw_bitmap()'s conversion. *out_buffer = nullptr; } @@ -184,7 +172,9 @@ static uint8_t papers3_display_get_frame_buffer_count(Device*) { // endregion static const DisplayApi papers3_display_api = { - .capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME | DISPLAY_CAPABILITY_SLOW_REFRESH, + // NO_DMA_BUFFER_NEEDED: draw_bitmap() converts into packed_buffer before touching hardware, + // never DMAs from LVGL's pointer directly - frees LVGL's draw buffers from forced internal RAM. + .capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLOW_REFRESH | DISPLAY_CAPABILITY_NO_DMA_BUFFER_NEEDED, .reset = papers3_display_reset, .init = papers3_display_init, .draw_bitmap = papers3_display_draw_bitmap, @@ -213,17 +203,13 @@ static const DisplayApi papers3_display_api = { static error_t start(Device* device) { const auto* config = GET_CONFIG(device); - static bool s_lut_initialized = false; - if (!s_lut_initialized) { - init_unpack_lut(); - s_lut_initialized = true; - } - auto* internal = static_cast(malloc(sizeof(Papers3DisplayInternal))); if (internal == nullptr) { return ERROR_OUT_OF_MEMORY; } internal->powered = false; + internal->draw_count = 0; + internal->post_boot_clear_done = false; epd_init(&epd_board_m5papers3, &ED047TC1, static_cast(EPD_LUT_1K | EPD_FEED_QUEUE_32)); epd_set_rotation(config->rotation); @@ -245,8 +231,12 @@ static error_t start(Device* device) { internal->framebuffer = epd_hl_get_framebuffer(&internal->hl_state); // Sized for the rotated (LVGL-facing) resolution - see get_resolution_x()/y()'s comment. + // ~260KB for this panel - a plain malloc() would land in scarce internal RAM. This buffer is + // only ever read once per draw_bitmap() call by epd_draw_rotated_image() (into epdiy's own + // SPIRAM-backed framebuffers, see highlevel.c), so it has no internal-RAM/DMA requirement and + // belongs in PSRAM instead, matching epdiy's own front_fb/back_fb/difference_fb allocations. const size_t packed_buffer_size = static_cast((epd_rotated_display_width() + 1) / 2) * static_cast(epd_rotated_display_height()); - internal->packed_buffer = static_cast(malloc(packed_buffer_size)); + internal->packed_buffer = static_cast(heap_caps_malloc(packed_buffer_size, MALLOC_CAP_SPIRAM)); if (internal->packed_buffer == nullptr) { LOG_E(TAG, "Failed to allocate packed pixel buffer"); epd_deinit(); diff --git a/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp b/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp index eed327756..817484c66 100644 --- a/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp +++ b/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp @@ -35,6 +35,12 @@ static constexpr uint32_t REPEAT_RATE_MS = 80; // REG_INT_STAT polling (when no IRQ pin) and software key-repeat ticking. static constexpr uint32_t POLL_INTERVAL_MS = 20; +// Upper bound on events consumed per drain_events() call. Since the loop re-reads REG_EVENT_NUM +// each iteration rather than counting down a latched value, this caps the damage if the device +// ever reports a non-zero count that never drains - without it, that would spin forever holding +// the I2C bus. The device's own queue is far smaller than this, so it never limits normal bursts. +static constexpr uint8_t MAX_EVENTS_PER_DRAIN = 32; + // --------------------------------------------------------------------------- // Register addresses // --------------------------------------------------------------------------- @@ -117,6 +123,12 @@ static constexpr HidMapping KEY_MATRIX_HID_SYM[70] = { // Covers all codes present in the Tab5 matrix tables above. LV_KEY_* are plain uint32_t // constants - matching KeyboardKeyData::key's driver-defined contract and the same convention // m5stack-module's cardputer_keyboard.cpp kernel driver already uses. +// +// `ctrl` only selects the LVGL focus-navigation aliases for the arrow keys. Ctrl chords on +// ordinary keys are NOT folded into the returned value - the C0 control codes a terminal wants +// (Ctrl+C = 0x03, Ctrl+K = 0x0B, ...) collide with the LVGL constants returned here (LV_KEY_END = 3, +// LV_KEY_PREV = 11, ...), so Ctrl is reported out-of-band via KeyboardKeyData::ctrl instead and +// consumers that want control codes derive them themselves. // --------------------------------------------------------------------------- static uint32_t tab5_translate_key(uint8_t keycode, uint8_t modifier, bool ctrl) { const bool shift = (modifier & 0x22U) != 0U; @@ -172,6 +184,14 @@ static uint32_t now_ms() { return static_cast(esp_timer_get_time() / 1000); } +// Queued key event. Modifier state is captured here at enqueue time rather than read back from +// Tab5KeyboardInternal at dequeue time, since the user can release Ctrl before read_key() drains +// the event - and software key-repeat replays this same struct, so a held chord keeps its modifiers. +struct Tab5KeyEvent { + uint32_t key; + bool ctrl; +}; + struct Tab5KeyboardInternal { QueueHandle_t queue; @@ -193,7 +213,7 @@ struct Tab5KeyboardInternal { uint32_t last_poll_ms; // Software key-repeat state (tracked by position to survive modifier changes) - uint32_t repeat_key; + Tab5KeyEvent repeat_event; uint8_t repeat_row; uint8_t repeat_col; uint32_t repeat_start_ms; @@ -289,16 +309,22 @@ static void remove_irq_pin(Tab5KeyboardInternal* internal) { // drain_events - reads all pending events from the device queue // --------------------------------------------------------------------------- static void drain_events(Device* device, Tab5KeyboardInternal* internal) { - uint8_t count = 0; - if (!read_reg(device, REG_EVENT_NUM, &count) || count == 0) { - return; - } + // REG_EVENT_NUM is re-read every iteration rather than latched once and counted down, matching + // M5's own UnitTab5Keyboard::drain_events(). Each REG_KEY_EVENT read consumes one event from the + // device queue, so a count latched up front can go stale mid-drain; re-reading makes the loop + // self-correcting and lets it stop as soon as the device says the queue is actually empty. + uint8_t drained = 0; + while (drained < MAX_EVENTS_PER_DRAIN) { + uint8_t count = 0; + if (!read_reg(device, REG_EVENT_NUM, &count) || count == 0) { + break; + } - while (count > 0) { uint8_t raw = 0; if (!read_reg(device, REG_KEY_EVENT, &raw) || raw == KEY_EVENT_EMPTY) { break; } + drained++; const bool pressed = (raw & 0x80U) != 0U; const uint8_t row = (raw >> 4U) & 0x07U; @@ -308,7 +334,6 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) { if (row == MOD_ROW_SYM && col == MOD_COL_SYM) { internal->sym_active = pressed; update_leds(device, internal); - count--; continue; } if (row == MOD_ROW_AA && col == MOD_COL_AA) { @@ -324,16 +349,13 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) { internal->aa_tapped = false; } update_leds(device, internal); - count--; continue; } if (row == MOD_ROW_CTRL && col == MOD_COL_CTRL) { internal->ctrl_held = pressed; - count--; continue; } if (row == MOD_ROW_ALT && col == MOD_COL_ALT) { - count--; continue; } @@ -354,10 +376,11 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) { // no business reaching into, so ESC is now just queued as a normal key // like everything else (LVGL/app code already handles ESC via focus/group // navigation the same way a dedicated ESC key on any other keyboard would). - xQueueSend(internal->queue, &lv_key, 0); + const Tab5KeyEvent event = { lv_key, internal->ctrl_held }; + xQueueSend(internal->queue, &event, 0); // Arm software repeat tracking by row/col to survive modifier changes const uint32_t now = now_ms(); - internal->repeat_key = lv_key; + internal->repeat_event = event; internal->repeat_row = row; internal->repeat_col = col; internal->repeat_start_ms = now; @@ -370,12 +393,11 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) { } } else if (row == internal->repeat_row && col == internal->repeat_col) { // Match release by position, not translated value — survives sticky Aa clear - internal->repeat_key = 0; + internal->repeat_event.key = 0; } } } } - count--; } // Clear INT status after draining so the line de-asserts @@ -434,13 +456,19 @@ static void poll_if_due(Device* device, Tab5KeyboardInternal* internal) { drain_events(device, internal); } - // Software key-repeat (runs every tick regardless of IRQ) - if (internal->repeat_key != 0U) { - if ((now - internal->repeat_start_ms) >= REPEAT_INITIAL_MS) { + // Software key-repeat (runs every tick regardless of IRQ). + // + // The clock is re-read here rather than reusing `now` from the top of the function: a press + // handled by the drain above sets repeat_start_ms to a timestamp taken *during* the drain, which + // is later than `now`. The unsigned subtraction below would then wrap to a huge value and clear + // the REPEAT_INITIAL_MS gate immediately, emitting one spurious repeat ~1ms after every press. + const uint32_t repeat_now = now_ms(); + if (internal->repeat_event.key != 0U) { + if ((repeat_now - internal->repeat_start_ms) >= REPEAT_INITIAL_MS) { const uint32_t last = internal->repeat_last_ms; - if (last == 0 || (now - last) >= REPEAT_RATE_MS) { - internal->repeat_last_ms = now; - xQueueSend(internal->queue, &internal->repeat_key, 0); + if (last == 0 || (repeat_now - last) >= REPEAT_RATE_MS) { + internal->repeat_last_ms = repeat_now; + xQueueSend(internal->queue, &internal->repeat_event, 0); } } } @@ -464,7 +492,7 @@ static error_t start(Device* device) { } memset(internal, 0, sizeof(Tab5KeyboardInternal)); - internal->queue = xQueueCreate(20, sizeof(uint32_t)); + internal->queue = xQueueCreate(20, sizeof(Tab5KeyEvent)); if (internal->queue == nullptr) { free(internal); return ERROR_OUT_OF_MEMORY; @@ -522,15 +550,19 @@ static error_t tab5_keyboard_read_key(Device* device, KeyboardKeyData* data) { poll_if_due(device, internal); - uint32_t lv_key = 0; - if (xQueueReceive(internal->queue, &lv_key, 0) == pdTRUE) { - data->key = lv_key; + Tab5KeyEvent event = {}; + if (xQueueReceive(internal->queue, &event, 0) == pdTRUE) { + data->key = event.key; data->pressed = true; data->continue_reading = uxQueueMessagesWaiting(internal->queue) > 0; + data->ctrl = event.ctrl; + data->alt = false; // Alt is not tracked by this driver - see drain_events() } else { data->key = 0; data->pressed = false; data->continue_reading = false; + data->ctrl = false; + data->alt = false; } return ERROR_NONE; diff --git a/Modules/lvgl-module/include/lvgl/devices/display.h b/Modules/lvgl-module/include/lvgl/devices/display.h index 2e7b26232..a6feacecf 100644 --- a/Modules/lvgl-module/include/lvgl/devices/display.h +++ b/Modules/lvgl-module/include/lvgl/devices/display.h @@ -53,6 +53,13 @@ struct LvglDisplayConfig { * the LV_COLOR_FORMAT_I1 path (already always-full-frame). */ bool force_full_frame; + + /** + * Opts owned draw buffer(s) OUT of DMA-capable memory, falling back to PSRAM instead of + * scarce internal RAM. Default false keeps existing behavior. Only set true if the driver + * never DMAs directly from the buffer pointer LVGL hands it in the flush callback. + */ + bool skip_dma_capable_buffer; }; /** diff --git a/Modules/lvgl-module/source/devices/devices.cpp b/Modules/lvgl-module/source/devices/devices.cpp index 6b70b80ed..ab0c631af 100644 --- a/Modules/lvgl-module/source/devices/devices.cpp +++ b/Modules/lvgl-module/source/devices/devices.cpp @@ -67,11 +67,13 @@ void lvgl_devices_attach() { // itself is never asked to do something it can't. bool can_hw_rotate = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_CAP_SWAP_XY) && display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_CAP_MIRROR); + bool skip_dma_capable_buffer = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_NO_DMA_BUFFER_NEEDED); struct LvglDisplayConfig lvgl_display_config = { .buffer_height = (uint16_t)(vres > 10 ? vres / 10 : vres), .sw_rotate = !can_hw_rotate, .swap_bytes = swap_bytes, - .force_full_frame = display_requires_full_frame + .force_full_frame = display_requires_full_frame, + .skip_dma_capable_buffer = skip_dma_capable_buffer }; lv_disp_t* added_display = NULL; if (lvgl_display_add(kernel_display_device, &lvgl_display_config, &added_display) == ERROR_NONE) { diff --git a/Modules/lvgl-module/source/devices/display.cpp b/Modules/lvgl-module/source/devices/display.cpp index b43a7927e..837e2cc1c 100644 --- a/Modules/lvgl-module/source/devices/display.cpp +++ b/Modules/lvgl-module/source/devices/display.cpp @@ -59,13 +59,18 @@ struct LvglDisplayCtx { bool byte_swap; }; -static void* lvgl_display_alloc_buffer(size_t size_bytes) { +static void* lvgl_display_alloc_buffer(size_t size_bytes, bool skip_dma_capable) { #ifdef ESP_PLATFORM // Must match LV_DRAW_BUF_ALIGN (can be > 4 - e.g. 64, tied to the cache line size for // DMA2D/PPA coherency on some targets - see sdkconfig's CONFIG_LV_DRAW_BUF_ALIGN). A buffer // allocated less strictly than that fails lv_display_set_buffers()'s alignment assert, which // is configured to LV_ASSERT_HANDLER (while(1);) rather than a clean abort - i.e. a silent hang. - void* buf = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size_bytes, MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + // MALLOC_CAP_DMA is scarce internal RAM - skip it for displays that don't DMA directly from + // this buffer (see skip_dma_capable_buffer). Dropping MALLOC_CAP_DMA alone isn't enough to + // land in PSRAM though: MALLOC_CAP_8BIT alone is still satisfied by internal RAM, so + // MALLOC_CAP_SPIRAM must be requested explicitly (confirmed on real hardware). + uint32_t caps = skip_dma_capable ? (MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT) : (MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + void* buf = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size_bytes, caps); if (buf == NULL) { buf = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size_bytes, MALLOC_CAP_DEFAULT); } @@ -104,6 +109,13 @@ static bool lvgl_display_map_color_format(enum DisplayColorFormat in, lv_color_f // (e.g. ssd1306_draw_bitmap()'s row-to-page transpose). *out = LV_COLOR_FORMAT_I1; return true; + case DISPLAY_COLOR_FORMAT_GRAYSCALE8: + // Row-major, 1 byte/pixel luminance (0x00=black, 0xFF=white) - matches LV_COLOR_FORMAT_L8 + // directly, no repacking needed. Deliberately NOT routed through the I1 branch below in + // lvgl_display_add(): I1 is hardcoded to LV_DISPLAY_RENDER_MODE_FULL there, which is what + // this format exists to avoid for panels that want real partial/tile updates. + *out = LV_COLOR_FORMAT_L8; + return true; default: return false; } @@ -396,7 +408,7 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* // buffer's start (see lvgl_display_flush_cb()). Always redraw the whole frame in one // owned buffer instead of computing partial-region byte offsets against that packing. buf_size_bytes = (size_t)((hres + 7) / 8) * vres + 8; - ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes); + ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes, config->skip_dma_capable_buffer); if (ctx->buf1 == NULL) { delete wrapper; return ERROR_OUT_OF_MEMORY; @@ -416,13 +428,13 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* ? vres : config->buffer_height; buf_size_bytes = (size_t)hres * buf_height * bpp; - ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes); + ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes, config->skip_dma_capable_buffer); if (ctx->buf1 == NULL) { delete wrapper; return ERROR_OUT_OF_MEMORY; } if (config->double_buffer) { - ctx->buf2 = lvgl_display_alloc_buffer(buf_size_bytes); + ctx->buf2 = lvgl_display_alloc_buffer(buf_size_bytes, config->skip_dma_capable_buffer); if (ctx->buf2 == NULL) { lvgl_display_free_buffer(ctx->buf1); delete wrapper; @@ -436,7 +448,7 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* ctx->buf_size_bytes = buf_size_bytes; if (ctx->sw_rotate) { - ctx->rotate_buf = lvgl_display_alloc_buffer(buf_size_bytes); + ctx->rotate_buf = lvgl_display_alloc_buffer(buf_size_bytes, config->skip_dma_capable_buffer); if (ctx->rotate_buf == NULL) { if (ctx->owns_buffers) { lvgl_display_free_buffer(ctx->buf1); diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp index dd20bb8e6..dfb8d19ab 100644 --- a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp @@ -108,7 +108,16 @@ static uint32_t hid_keycode_to_key(uint8_t modifier, uint8_t key_code, default: break; } - if (ctrl || alt) return 0; + /* + * Ctrl and Alt no longer suppress the key. + * + * They used to return 0 here, which meant a chord like Ctrl+C produced nothing at all and a + * terminal application could never see it. The modifiers are now reported alongside the key in + * UsbHidEvent instead, so the plain character still comes through and a consumer that wants a + * control code derives it. Alt is passed through on the same basis. + */ + (void)ctrl; + (void)alt; if (key_code < (sizeof(keycode2ascii) / sizeof(keycode2ascii[0]))) { bool is_letter = (key_code >= 0x04 && key_code <= 0x1D); @@ -208,7 +217,14 @@ static void hid_interface_callback(hid_host_device_handle_t handle, uint32_t lv_key = hid_keycode_to_key(kb->modifier.val, hid_code, ctx->caps_lock_active, ctx->num_lock_active); if (lv_key) { - UsbHidEvent evt = { .type = USB_HID_EVENT_KEY, .key = { lv_key, true } }; + const bool with_ctrl = (kb->modifier.val & HID_LEFT_CONTROL) || + (kb->modifier.val & HID_RIGHT_CONTROL); + const bool with_alt = (kb->modifier.val & HID_LEFT_ALT) || + (kb->modifier.val & HID_RIGHT_ALT); + UsbHidEvent evt = { + .type = USB_HID_EVENT_KEY, + .key = { lv_key, true, with_ctrl, with_alt } + }; publish_event(ctx, &evt); ctx->pressed_lv_keys[hid_code] = lv_key; } diff --git a/TactilityC/Source/symbols/esp_http_client.cpp b/TactilityC/Source/symbols/esp_http_client.cpp index 99a6fe225..2b8e71f66 100644 --- a/TactilityC/Source/symbols/esp_http_client.cpp +++ b/TactilityC/Source/symbols/esp_http_client.cpp @@ -4,8 +4,12 @@ #include #include +#include const esp_elfsym esp_http_client_symbols[] = { + // Needed for HTTPS: an app passes this as crt_bundle_attach to validate certificates against + // the bundle already compiled into the firmware (CONFIG_MBEDTLS_CERTIFICATE_BUNDLE). + ESP_ELFSYM_EXPORT(esp_crt_bundle_attach), ESP_ELFSYM_EXPORT(esp_http_client_init), ESP_ELFSYM_EXPORT(esp_http_client_perform), ESP_ELFSYM_EXPORT(esp_http_client_cancel_request), diff --git a/TactilityC/Source/symbols/freertos.cpp b/TactilityC/Source/symbols/freertos.cpp index ae98521af..def9180c2 100644 --- a/TactilityC/Source/symbols/freertos.cpp +++ b/TactilityC/Source/symbols/freertos.cpp @@ -40,6 +40,7 @@ const esp_elfsym freertos_symbols[] = { ESP_ELFSYM_EXPORT(xTaskGenericNotify), ESP_ELFSYM_EXPORT(xTaskGenericNotifyFromISR), ESP_ELFSYM_EXPORT(ulTaskGenericNotifyTake), + ESP_ELFSYM_EXPORT(xTaskGetCurrentTaskHandle), ESP_ELFSYM_EXPORT(xTaskGetTickCount), ESP_ELFSYM_EXPORT(xTaskGetTickCountFromISR), ESP_ELFSYM_EXPORT(pvTaskGetThreadLocalStoragePointer), @@ -101,6 +102,11 @@ const esp_elfsym freertos_symbols[] = { ESP_ELFSYM_EXPORT(vPortYield), ESP_ELFSYM_EXPORT(vPortEnterCritical), ESP_ELFSYM_EXPORT(vPortExitCritical), + // On multicore targets portENTER_CRITICAL/taskENTER_CRITICAL expand to these spinlock-taking + // variants rather than to vPortEnterCritical/vPortExitCritical above, so both pairs are needed + // for an app to use a critical section at all. + ESP_ELFSYM_EXPORT(xPortEnterCriticalTimeout), + ESP_ELFSYM_EXPORT(vPortExitCriticalMultiCore), ESP_ELFSYM_EXPORT(xPortInIsrContext), ESP_ELFSYM_EXPORT(xPortCanYield), ESP_ELFSYM_EXPORT(xPortGetCoreID), diff --git a/TactilityC/Source/tt_init.cpp b/TactilityC/Source/tt_init.cpp index 296b9f68b..fd5dd2954 100644 --- a/TactilityC/Source/tt_init.cpp +++ b/TactilityC/Source/tt_init.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -89,6 +90,7 @@ const esp_elfsym main_symbols[] { ESP_ELFSYM_EXPORT(close), ESP_ELFSYM_EXPORT(rmdir), ESP_ELFSYM_EXPORT(unlink), + ESP_ELFSYM_EXPORT(open), // strings.h ESP_ELFSYM_EXPORT(explicit_bzero), ESP_ELFSYM_EXPORT(strcasecmp), @@ -194,6 +196,10 @@ const esp_elfsym main_symbols[] { ESP_ELFSYM_EXPORT(fgets), ESP_ELFSYM_EXPORT(fopen), ESP_ELFSYM_EXPORT(freopen), + // Lets an app find the descriptor behind a stream. Needed when stdin/stdout have been pointed + // somewhere other than descriptors 0 and 1, which is the case for an app that owns a terminal. + ESP_ELFSYM_EXPORT(fileno), + ESP_ELFSYM_EXPORT(setvbuf), ESP_ELFSYM_EXPORT(fputc), ESP_ELFSYM_EXPORT(fputs), ESP_ELFSYM_EXPORT(fprintf), @@ -239,6 +245,7 @@ const esp_elfsym main_symbols[] { ESP_ELFSYM_EXPORT(memchr), ESP_ELFSYM_EXPORT(memmove), ESP_ELFSYM_EXPORT(strdup), + ESP_ELFSYM_EXPORT(stpcpy), // ctype ESP_ELFSYM_EXPORT(isalnum), @@ -256,6 +263,13 @@ const esp_elfsym main_symbols[] { ESP_ELFSYM_EXPORT(toupper), // ESP-IDF ESP_ELFSYM_EXPORT(esp_log), + // Lets an app that has taken over the display quieten firmware logging: with stdout redirected + // to a terminal the app owns, anything logged appears on screen as if the app had printed it. + ESP_ELFSYM_EXPORT(esp_log_level_set), + // Lets an app redirect firmware logging somewhere other than stdout. An app that has pointed + // stdout at its own terminal needs this: without it, every ESP_LOG from any component in the + // system - NTP, RTC, TLS - paints over the app's display. + ESP_ELFSYM_EXPORT(esp_log_set_vprintf), ESP_ELFSYM_EXPORT(esp_log_write), ESP_ELFSYM_EXPORT(esp_log_timestamp), ESP_ELFSYM_EXPORT(esp_err_to_name), @@ -327,9 +341,26 @@ const esp_elfsym main_symbols[] { ESP_ELFSYM_EXPORT(ipaddr_addr), // POSIX socket names (VFS wrappers used when apps include ) ESP_ELFSYM_EXPORT(select), + // stdlib.h - environment and sorting + ESP_ELFSYM_EXPORT(getenv), + ESP_ELFSYM_EXPORT(setenv), + ESP_ELFSYM_EXPORT(unsetenv), + ESP_ELFSYM_EXPORT(qsort), + // unistd.h + ESP_ELFSYM_EXPORT(access), + ESP_ELFSYM_EXPORT(isatty), + ESP_ELFSYM_EXPORT(read), + ESP_ELFSYM_EXPORT(write), + ESP_ELFSYM_EXPORT(lseek), // sys/stat.h ESP_ELFSYM_EXPORT(stat), ESP_ELFSYM_EXPORT(mkdir), + // esp_vfs.h - lets an app register a device node (e.g. a terminal at /dev/...) so that plain + // printf/stdout reaches it. Note the registration is global and outlives the app unless it + // unregisters: an app that takes this must release it on shutdown, or the next one inherits a + // path whose callbacks point into unloaded memory. + ESP_ELFSYM_EXPORT(esp_vfs_register), + ESP_ELFSYM_EXPORT(esp_vfs_unregister), // esp_netif.h ESP_ELFSYM_EXPORT(esp_netif_get_ip_info), ESP_ELFSYM_EXPORT(esp_netif_get_handle_from_ifkey), @@ -360,6 +391,19 @@ const esp_elfsym main_symbols[] { ESP_ELFSYM_EXPORT(tinfl_decompress), ESP_ELFSYM_EXPORT(tinfl_decompress_mem_to_callback), ESP_ELFSYM_EXPORT(tinfl_decompress_mem_to_mem), + // Compression, the counterpart to the tinfl_* decompression above. Like those, these live in + // the chip's ROM rather than in flash, so exporting them costs nothing. + // + // Note this is miniz, not zlib: ESP-IDF builds it with MINIZ_NO_ZLIB_APIS, so deflate/inflate, + // crc32 and the gz* file API do not exist anywhere in the firmware. An app wanting gzip files + // has to use the tdefl_/tinfl_ interfaces directly, or vendor zlib itself. + ESP_ELFSYM_EXPORT(tdefl_init), + ESP_ELFSYM_EXPORT(tdefl_compress), + ESP_ELFSYM_EXPORT(tdefl_compress_buffer), + ESP_ELFSYM_EXPORT(tdefl_compress_mem_to_mem), + ESP_ELFSYM_EXPORT(tdefl_compress_mem_to_output), + ESP_ELFSYM_EXPORT(tdefl_get_adler32), + ESP_ELFSYM_EXPORT(tdefl_get_prev_return_status), // ledc ESP_ELFSYM_EXPORT(ledc_update_duty), ESP_ELFSYM_EXPORT(ledc_set_freq), diff --git a/TactilityKernel/include/tactility/drivers/display.h b/TactilityKernel/include/tactility/drivers/display.h index 02b90f151..49a2a4d97 100644 --- a/TactilityKernel/include/tactility/drivers/display.h +++ b/TactilityKernel/include/tactility/drivers/display.h @@ -23,7 +23,13 @@ enum DisplayCapability { DISPLAY_CAPABILITY_SLEEP = 1 << 6, DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME = 1 << 7, /** Can be used by e-paper with pointer devices for long click timing changes. */ - DISPLAY_CAPABILITY_SLOW_REFRESH = 1 << 8 + DISPLAY_CAPABILITY_SLOW_REFRESH = 1 << 8, + /** + * Promise that draw_bitmap() never DMAs directly from the color_data pointer it's given (e.g. + * it copies/converts into its own buffer first). Lets the LVGL bridge allocate this display's + * draw buffer(s) from non-DMA-capable memory instead of forcing scarce internal RAM. + */ + DISPLAY_CAPABILITY_NO_DMA_BUFFER_NEEDED = 1 << 9 }; /** @@ -36,6 +42,11 @@ enum DisplayColorFormat { DISPLAY_COLOR_FORMAT_RGB565 = 0x3, DISPLAY_COLOR_FORMAT_RGB565_SWAPPED = 0x4, DISPLAY_COLOR_FORMAT_RGB888 = 0x5, + // 8 bpp luminance, 0x00 = black, 0xFF = white (matches LVGL's LV_COLOR_FORMAT_L8). Unlike + // MONOCHROME, the LVGL bridge does not force full-frame rendering for this format, so drivers + // that want real partial/tile updates (e.g. grayscale e-paper panels) should report this + // instead of MONOCHROME even if they intend to threshold down to black/white themselves. + DISPLAY_COLOR_FORMAT_GRAYSCALE8 = 0x6, }; /** diff --git a/TactilityKernel/include/tactility/drivers/keyboard.h b/TactilityKernel/include/tactility/drivers/keyboard.h index a4af4c788..420dabdfa 100644 --- a/TactilityKernel/include/tactility/drivers/keyboard.h +++ b/TactilityKernel/include/tactility/drivers/keyboard.h @@ -24,6 +24,23 @@ struct KeyboardKeyData { * immediately to drain it. False if this was the last pending event. */ bool continue_reading; + /** + * @brief True if Ctrl was held when this key was pressed. + * + * Reported separately rather than folded into `key` because the two encodings collide: the C0 + * control codes a terminal expects for Ctrl chords (Ctrl+C is 0x03, Ctrl+K is 0x0B, ...) overlap + * the LVGL key constants drivers emit in the same field (LV_KEY_END is 3, LV_KEY_PREV is 11, + * LV_KEY_UP is 17, ...), so a single uint32_t cannot express both. Consumers that want control + * codes derive them here, e.g. `(key >= 'a' && key <= 'z') ? (key & 0x1F) : key` when ctrl is set. + * + * Drivers whose hardware cannot report Ctrl leave this false. + */ + bool ctrl; + /** + * @brief True if Alt was held when this key was pressed. See ctrl for why modifiers are reported + * separately. Drivers whose hardware cannot report Alt leave this false. + */ + bool alt; }; /** diff --git a/TactilityKernel/include/tactility/drivers/usb_host_hid.h b/TactilityKernel/include/tactility/drivers/usb_host_hid.h index c7b2b111b..ac5b7aa1f 100644 --- a/TactilityKernel/include/tactility/drivers/usb_host_hid.h +++ b/TactilityKernel/include/tactility/drivers/usb_host_hid.h @@ -43,7 +43,16 @@ typedef enum { typedef struct { UsbHidEventType type; union { - struct { uint32_t key_code; bool pressed; } key; + /** + * @brief A key press or release. + * + * `ctrl` and `alt` report the modifiers separately rather than folding them into + * `key_code`, because the two encodings collide: the C0 control codes a terminal expects + * for Ctrl chords (Ctrl+C is 0x03, Ctrl+K is 0x0B) overlap the UsbHidKey constants above + * (USB_HID_KEY_END is 3, USB_HID_KEY_PREV is 11). A consumer wanting control codes derives + * them here, e.g. `(key_code >= 'a' && key_code <= 'z') ? (key_code & 0x1F) : key_code`. + */ + struct { uint32_t key_code; bool pressed; bool ctrl; bool alt; } key; struct { int32_t dx; int32_t dy; } mouse_move; struct { bool button1; bool button2; } mouse_btn; struct { int32_t delta; } scroll; diff --git a/TactilityKernel/source/drivers/keyboard.cpp b/TactilityKernel/source/drivers/keyboard.cpp index 6bb02bf40..8c14bcd28 100644 --- a/TactilityKernel/source/drivers/keyboard.cpp +++ b/TactilityKernel/source/drivers/keyboard.cpp @@ -9,6 +9,12 @@ extern "C" { error_t keyboard_read_key(Device* device, KeyboardKeyData* data) { const auto* driver = device_get_driver(device); + + // Default the modifier fields here rather than in each driver: only drivers whose hardware can + // report modifiers set them, and the rest would otherwise leave whatever the caller's stack held. + data->ctrl = false; + data->alt = false; + return KEYBOARD_DRIVER_API(driver)->read_key(device, data); } diff --git a/TactilityKernel/source/symbols.c b/TactilityKernel/source/symbols.c index 48c500737..418b116f8 100644 --- a/TactilityKernel/source/symbols.c +++ b/TactilityKernel/source/symbols.c @@ -171,6 +171,7 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(file_system_unmount), DEFINE_MODULE_SYMBOL(file_system_is_mounted), DEFINE_MODULE_SYMBOL(file_system_get_path), + DEFINE_MODULE_SYMBOL(file_system_for_each), // memory DEFINE_MODULE_SYMBOL(MEMORY_POLICY_DEFAULT), DEFINE_MODULE_SYMBOL(memory_print_stats), From 5ea3b8ab9360586db1362889a10514e37f0d6d9a Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Sat, 8 Aug 2026 02:15:21 +1000 Subject: [PATCH 2/6] fix this guy! --- TactilityC/Source/symbols/freertos.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TactilityC/Source/symbols/freertos.cpp b/TactilityC/Source/symbols/freertos.cpp index def9180c2..26749ca0a 100644 --- a/TactilityC/Source/symbols/freertos.cpp +++ b/TactilityC/Source/symbols/freertos.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -102,11 +103,10 @@ const esp_elfsym freertos_symbols[] = { ESP_ELFSYM_EXPORT(vPortYield), ESP_ELFSYM_EXPORT(vPortEnterCritical), ESP_ELFSYM_EXPORT(vPortExitCritical), - // On multicore targets portENTER_CRITICAL/taskENTER_CRITICAL expand to these spinlock-taking - // variants rather than to vPortEnterCritical/vPortExitCritical above, so both pairs are needed - // for an app to use a critical section at all. ESP_ELFSYM_EXPORT(xPortEnterCriticalTimeout), +#ifdef CONFIG_IDF_TARGET_ESP32P4 ESP_ELFSYM_EXPORT(vPortExitCriticalMultiCore), +#endif ESP_ELFSYM_EXPORT(xPortInIsrContext), ESP_ELFSYM_EXPORT(xPortCanYield), ESP_ELFSYM_EXPORT(xPortGetCoreID), From f804c580686fb34bc71e7e88e92973d01cbd803a Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Sat, 8 Aug 2026 02:18:43 +1000 Subject: [PATCH 3/6] name --- Modules/lvgl-module/include/lvgl/devices/display.h | 2 +- Modules/lvgl-module/source/devices/devices.cpp | 4 ++-- Modules/lvgl-module/source/devices/display.cpp | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Modules/lvgl-module/include/lvgl/devices/display.h b/Modules/lvgl-module/include/lvgl/devices/display.h index a6feacecf..3dfcc3926 100644 --- a/Modules/lvgl-module/include/lvgl/devices/display.h +++ b/Modules/lvgl-module/include/lvgl/devices/display.h @@ -59,7 +59,7 @@ struct LvglDisplayConfig { * scarce internal RAM. Default false keeps existing behavior. Only set true if the driver * never DMAs directly from the buffer pointer LVGL hands it in the flush callback. */ - bool skip_dma_capable_buffer; + bool prefer_external_ram; }; /** diff --git a/Modules/lvgl-module/source/devices/devices.cpp b/Modules/lvgl-module/source/devices/devices.cpp index ab0c631af..6aa507fac 100644 --- a/Modules/lvgl-module/source/devices/devices.cpp +++ b/Modules/lvgl-module/source/devices/devices.cpp @@ -67,13 +67,13 @@ void lvgl_devices_attach() { // itself is never asked to do something it can't. bool can_hw_rotate = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_CAP_SWAP_XY) && display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_CAP_MIRROR); - bool skip_dma_capable_buffer = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_NO_DMA_BUFFER_NEEDED); + bool prefer_external_ram_buffer = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_NO_DMA_BUFFER_NEEDED); struct LvglDisplayConfig lvgl_display_config = { .buffer_height = (uint16_t)(vres > 10 ? vres / 10 : vres), .sw_rotate = !can_hw_rotate, .swap_bytes = swap_bytes, .force_full_frame = display_requires_full_frame, - .skip_dma_capable_buffer = skip_dma_capable_buffer + .prefer_external_ram = prefer_external_ram_buffer }; lv_disp_t* added_display = NULL; if (lvgl_display_add(kernel_display_device, &lvgl_display_config, &added_display) == ERROR_NONE) { diff --git a/Modules/lvgl-module/source/devices/display.cpp b/Modules/lvgl-module/source/devices/display.cpp index 837e2cc1c..82d9eb529 100644 --- a/Modules/lvgl-module/source/devices/display.cpp +++ b/Modules/lvgl-module/source/devices/display.cpp @@ -59,17 +59,17 @@ struct LvglDisplayCtx { bool byte_swap; }; -static void* lvgl_display_alloc_buffer(size_t size_bytes, bool skip_dma_capable) { +static void* lvgl_display_alloc_buffer(size_t size_bytes, bool prefer_external_ram) { #ifdef ESP_PLATFORM // Must match LV_DRAW_BUF_ALIGN (can be > 4 - e.g. 64, tied to the cache line size for // DMA2D/PPA coherency on some targets - see sdkconfig's CONFIG_LV_DRAW_BUF_ALIGN). A buffer // allocated less strictly than that fails lv_display_set_buffers()'s alignment assert, which // is configured to LV_ASSERT_HANDLER (while(1);) rather than a clean abort - i.e. a silent hang. // MALLOC_CAP_DMA is scarce internal RAM - skip it for displays that don't DMA directly from - // this buffer (see skip_dma_capable_buffer). Dropping MALLOC_CAP_DMA alone isn't enough to + // this buffer (see prefer_external_ram_buffer). Dropping MALLOC_CAP_DMA alone isn't enough to // land in PSRAM though: MALLOC_CAP_8BIT alone is still satisfied by internal RAM, so // MALLOC_CAP_SPIRAM must be requested explicitly (confirmed on real hardware). - uint32_t caps = skip_dma_capable ? (MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT) : (MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + uint32_t caps = prefer_external_ram ? (MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT) : (MALLOC_CAP_DMA | MALLOC_CAP_8BIT); void* buf = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size_bytes, caps); if (buf == NULL) { buf = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size_bytes, MALLOC_CAP_DEFAULT); @@ -408,7 +408,7 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* // buffer's start (see lvgl_display_flush_cb()). Always redraw the whole frame in one // owned buffer instead of computing partial-region byte offsets against that packing. buf_size_bytes = (size_t)((hres + 7) / 8) * vres + 8; - ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes, config->skip_dma_capable_buffer); + ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes, config->prefer_external_ram); if (ctx->buf1 == NULL) { delete wrapper; return ERROR_OUT_OF_MEMORY; @@ -428,13 +428,13 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* ? vres : config->buffer_height; buf_size_bytes = (size_t)hres * buf_height * bpp; - ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes, config->skip_dma_capable_buffer); + ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes, config->prefer_external_ram); if (ctx->buf1 == NULL) { delete wrapper; return ERROR_OUT_OF_MEMORY; } if (config->double_buffer) { - ctx->buf2 = lvgl_display_alloc_buffer(buf_size_bytes, config->skip_dma_capable_buffer); + ctx->buf2 = lvgl_display_alloc_buffer(buf_size_bytes, config->prefer_external_ram); if (ctx->buf2 == NULL) { lvgl_display_free_buffer(ctx->buf1); delete wrapper; @@ -448,7 +448,7 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* ctx->buf_size_bytes = buf_size_bytes; if (ctx->sw_rotate) { - ctx->rotate_buf = lvgl_display_alloc_buffer(buf_size_bytes, config->skip_dma_capable_buffer); + ctx->rotate_buf = lvgl_display_alloc_buffer(buf_size_bytes, config->prefer_external_ram); if (ctx->rotate_buf == NULL) { if (ctx->owns_buffers) { lvgl_display_free_buffer(ctx->buf1); From aacadc4f9c4832d2f12b63bb46d95392a989bd5f Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Sat, 8 Aug 2026 02:27:53 +1000 Subject: [PATCH 4/6] name again --- Devices/m5stack-papers3/source/drivers/papers3_display.cpp | 4 ++-- Modules/lvgl-module/source/devices/devices.cpp | 2 +- TactilityKernel/include/tactility/drivers/display.h | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Devices/m5stack-papers3/source/drivers/papers3_display.cpp b/Devices/m5stack-papers3/source/drivers/papers3_display.cpp index 7a6a0f77c..cc628a605 100644 --- a/Devices/m5stack-papers3/source/drivers/papers3_display.cpp +++ b/Devices/m5stack-papers3/source/drivers/papers3_display.cpp @@ -172,9 +172,9 @@ static uint8_t papers3_display_get_frame_buffer_count(Device*) { // endregion static const DisplayApi papers3_display_api = { - // NO_DMA_BUFFER_NEEDED: draw_bitmap() converts into packed_buffer before touching hardware, + // PREFER_EXTERNAL_RAM: draw_bitmap() converts into packed_buffer before touching hardware, // never DMAs from LVGL's pointer directly - frees LVGL's draw buffers from forced internal RAM. - .capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLOW_REFRESH | DISPLAY_CAPABILITY_NO_DMA_BUFFER_NEEDED, + .capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLOW_REFRESH | DISPLAY_CAPABILITY_PREFER_EXTERNAL_RAM, .reset = papers3_display_reset, .init = papers3_display_init, .draw_bitmap = papers3_display_draw_bitmap, diff --git a/Modules/lvgl-module/source/devices/devices.cpp b/Modules/lvgl-module/source/devices/devices.cpp index 6aa507fac..196cc26c7 100644 --- a/Modules/lvgl-module/source/devices/devices.cpp +++ b/Modules/lvgl-module/source/devices/devices.cpp @@ -67,7 +67,7 @@ void lvgl_devices_attach() { // itself is never asked to do something it can't. bool can_hw_rotate = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_CAP_SWAP_XY) && display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_CAP_MIRROR); - bool prefer_external_ram_buffer = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_NO_DMA_BUFFER_NEEDED); + bool prefer_external_ram_buffer = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_PREFER_EXTERNAL_RAM); struct LvglDisplayConfig lvgl_display_config = { .buffer_height = (uint16_t)(vres > 10 ? vres / 10 : vres), .sw_rotate = !can_hw_rotate, diff --git a/TactilityKernel/include/tactility/drivers/display.h b/TactilityKernel/include/tactility/drivers/display.h index 49a2a4d97..97de217e4 100644 --- a/TactilityKernel/include/tactility/drivers/display.h +++ b/TactilityKernel/include/tactility/drivers/display.h @@ -29,7 +29,7 @@ enum DisplayCapability { * it copies/converts into its own buffer first). Lets the LVGL bridge allocate this display's * draw buffer(s) from non-DMA-capable memory instead of forcing scarce internal RAM. */ - DISPLAY_CAPABILITY_NO_DMA_BUFFER_NEEDED = 1 << 9 + DISPLAY_CAPABILITY_PREFER_EXTERNAL_RAM = 1 << 9 }; /** From 2c1085cac1978a35d5708b7d1ae8d1d5d4d06241 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Sat, 8 Aug 2026 03:32:38 +1000 Subject: [PATCH 5/6] fix ze things! --- .../source/drivers/papers3_display.cpp | 21 +++++-------------- .../Source/devices/tab5_keyboard.cpp | 7 +++++-- .../source/drivers/usb/esp32_usbhost_hid.cpp | 10 ++++----- TactilityC/Source/symbols/esp_http_client.cpp | 5 +++++ TactilityC/Source/symbols/freertos.cpp | 4 +++- .../include/tactility/drivers/keyboard.h | 4 +++- .../include/tactility/drivers/usb_host_hid.h | 4 +++- 7 files changed, 29 insertions(+), 26 deletions(-) diff --git a/Devices/m5stack-papers3/source/drivers/papers3_display.cpp b/Devices/m5stack-papers3/source/drivers/papers3_display.cpp index cc628a605..d21e5af2f 100644 --- a/Devices/m5stack-papers3/source/drivers/papers3_display.cpp +++ b/Devices/m5stack-papers3/source/drivers/papers3_display.cpp @@ -28,18 +28,12 @@ extern Module m5stack_papers3_module; static bool s_hl_initialized = false; static EpdiyHighlevelState s_hl_state = {}; -// Partial tile updates (see draw_bitmap()) never get a real quality pass, so a faint boot-logo -// ghost lingers after boot. One epd_fullclear() shortly after clears it. -static constexpr int DRAWS_UNTIL_POST_BOOT_CLEAR = 12; - struct Papers3DisplayInternal { EpdiyHighlevelState hl_state; uint8_t* framebuffer; // Scratch buffer for the grayscale8->EPDiy(4bpp packed, 2px/byte) conversion in draw_bitmap(). uint8_t* packed_buffer; bool powered; - int draw_count; - bool post_boot_clear_done; }; static void power_on(Papers3DisplayInternal* internal) { @@ -63,10 +57,14 @@ static error_t papers3_display_reset(Device* device) { } static error_t papers3_display_init(Device* device) { + const auto* config = GET_CONFIG(device); auto* internal = static_cast(device_get_driver_data(device)); power_on(internal); epd_clear(); - epd_hl_set_all_white(&internal->hl_state); + // The bootloader/boot-logo splash draws via partial refreshes that never get a real quality + // pass, leaving a faint ghost. Run a full clear now, before LVGL's first flush ever reaches + // draw_bitmap(), so it never has to undo content LVGL already put on screen. + epd_fullclear(&internal->hl_state, config->temperature_celsius); return ERROR_NONE; } @@ -118,13 +116,6 @@ static error_t papers3_display_draw_bitmap(Device* device, int32_t x_start, int3 update_area ); - // See DRAWS_UNTIL_POST_BOOT_CLEAR's comment: run the ghost-clearing full pass after boot's - // draws so it doesn't delay the boot splash/launcher content from appearing first. - if (!internal->post_boot_clear_done && ++internal->draw_count >= DRAWS_UNTIL_POST_BOOT_CLEAR) { - internal->post_boot_clear_done = true; - epd_fullclear(&internal->hl_state, config->temperature_celsius); - } - return draw_result == EPD_DRAW_SUCCESS ? ERROR_NONE : ERROR_RESOURCE; } @@ -208,8 +199,6 @@ static error_t start(Device* device) { return ERROR_OUT_OF_MEMORY; } internal->powered = false; - internal->draw_count = 0; - internal->post_boot_clear_done = false; epd_init(&epd_board_m5papers3, &ED047TC1, static_cast(EPD_LUT_1K | EPD_FEED_QUEUE_32)); epd_set_rotation(config->rotation); diff --git a/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp b/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp index 817484c66..3de918af2 100644 --- a/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp +++ b/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp @@ -190,6 +190,7 @@ static uint32_t now_ms() { struct Tab5KeyEvent { uint32_t key; bool ctrl; + bool alt; }; struct Tab5KeyboardInternal { @@ -201,6 +202,7 @@ struct Tab5KeyboardInternal { bool aa_held; bool aa_tapped; bool ctrl_held; + bool alt_held; // IRQ-driven event gating volatile bool irq_pending; @@ -356,6 +358,7 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) { continue; } if (row == MOD_ROW_ALT && col == MOD_COL_ALT) { + internal->alt_held = pressed; continue; } @@ -376,7 +379,7 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) { // no business reaching into, so ESC is now just queued as a normal key // like everything else (LVGL/app code already handles ESC via focus/group // navigation the same way a dedicated ESC key on any other keyboard would). - const Tab5KeyEvent event = { lv_key, internal->ctrl_held }; + const Tab5KeyEvent event = { lv_key, internal->ctrl_held, internal->alt_held }; xQueueSend(internal->queue, &event, 0); // Arm software repeat tracking by row/col to survive modifier changes const uint32_t now = now_ms(); @@ -556,7 +559,7 @@ static error_t tab5_keyboard_read_key(Device* device, KeyboardKeyData* data) { data->pressed = true; data->continue_reading = uxQueueMessagesWaiting(internal->queue) > 0; data->ctrl = event.ctrl; - data->alt = false; // Alt is not tracked by this driver - see drain_events() + data->alt = event.alt; } else { data->key = 0; data->pressed = false; diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp index dfb8d19ab..dd55b1f73 100644 --- a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp @@ -166,6 +166,10 @@ static void hid_interface_callback(hid_host_device_handle_t handle, if (params.proto == HID_PROTOCOL_KEYBOARD) { if (data_len < sizeof(hid_keyboard_input_report_boot_t)) break; auto* kb = reinterpret_cast(data); + const bool with_ctrl = (kb->modifier.val & HID_LEFT_CONTROL) || + (kb->modifier.val & HID_RIGHT_CONTROL); + const bool with_alt = (kb->modifier.val & HID_LEFT_ALT) || + (kb->modifier.val & HID_RIGHT_ALT); for (int i = 0; i < HID_KEYBOARD_KEY_MAX; i++) { uint8_t prev_hid = ctx->prev_keys[i]; @@ -178,7 +182,7 @@ static void hid_interface_callback(hid_host_device_handle_t handle, uint32_t lv_key = ctx->pressed_lv_keys[prev_hid]; ctx->pressed_lv_keys[prev_hid] = 0; if (lv_key) { - UsbHidEvent evt = { .type = USB_HID_EVENT_KEY, .key = { lv_key, false } }; + UsbHidEvent evt = { .type = USB_HID_EVENT_KEY, .key = { lv_key, false, with_ctrl, with_alt } }; publish_event(ctx, &evt); } } @@ -217,10 +221,6 @@ static void hid_interface_callback(hid_host_device_handle_t handle, uint32_t lv_key = hid_keycode_to_key(kb->modifier.val, hid_code, ctx->caps_lock_active, ctx->num_lock_active); if (lv_key) { - const bool with_ctrl = (kb->modifier.val & HID_LEFT_CONTROL) || - (kb->modifier.val & HID_RIGHT_CONTROL); - const bool with_alt = (kb->modifier.val & HID_LEFT_ALT) || - (kb->modifier.val & HID_RIGHT_ALT); UsbHidEvent evt = { .type = USB_HID_EVENT_KEY, .key = { lv_key, true, with_ctrl, with_alt } diff --git a/TactilityC/Source/symbols/esp_http_client.cpp b/TactilityC/Source/symbols/esp_http_client.cpp index 2b8e71f66..89b5791bf 100644 --- a/TactilityC/Source/symbols/esp_http_client.cpp +++ b/TactilityC/Source/symbols/esp_http_client.cpp @@ -3,13 +3,18 @@ #include +#include #include +#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE #include +#endif const esp_elfsym esp_http_client_symbols[] = { +#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE // Needed for HTTPS: an app passes this as crt_bundle_attach to validate certificates against // the bundle already compiled into the firmware (CONFIG_MBEDTLS_CERTIFICATE_BUNDLE). ESP_ELFSYM_EXPORT(esp_crt_bundle_attach), +#endif ESP_ELFSYM_EXPORT(esp_http_client_init), ESP_ELFSYM_EXPORT(esp_http_client_perform), ESP_ELFSYM_EXPORT(esp_http_client_cancel_request), diff --git a/TactilityC/Source/symbols/freertos.cpp b/TactilityC/Source/symbols/freertos.cpp index 26749ca0a..26389e1ae 100644 --- a/TactilityC/Source/symbols/freertos.cpp +++ b/TactilityC/Source/symbols/freertos.cpp @@ -103,8 +103,10 @@ const esp_elfsym freertos_symbols[] = { ESP_ELFSYM_EXPORT(vPortYield), ESP_ELFSYM_EXPORT(vPortEnterCritical), ESP_ELFSYM_EXPORT(vPortExitCritical), +#if defined(CONFIG_IDF_TARGET_ESP32P4) || defined(CONFIG_IDF_TARGET_ESP32S3) ESP_ELFSYM_EXPORT(xPortEnterCriticalTimeout), -#ifdef CONFIG_IDF_TARGET_ESP32P4 +#endif +#if defined(CONFIG_IDF_TARGET_ESP32P4) ESP_ELFSYM_EXPORT(vPortExitCriticalMultiCore), #endif ESP_ELFSYM_EXPORT(xPortInIsrContext), diff --git a/TactilityKernel/include/tactility/drivers/keyboard.h b/TactilityKernel/include/tactility/drivers/keyboard.h index 420dabdfa..f1390f22d 100644 --- a/TactilityKernel/include/tactility/drivers/keyboard.h +++ b/TactilityKernel/include/tactility/drivers/keyboard.h @@ -31,7 +31,9 @@ struct KeyboardKeyData { * control codes a terminal expects for Ctrl chords (Ctrl+C is 0x03, Ctrl+K is 0x0B, ...) overlap * the LVGL key constants drivers emit in the same field (LV_KEY_END is 3, LV_KEY_PREV is 11, * LV_KEY_UP is 17, ...), so a single uint32_t cannot express both. Consumers that want control - * codes derive them here, e.g. `(key >= 'a' && key <= 'z') ? (key & 0x1F) : key` when ctrl is set. + * codes derive them here, e.g. + * `((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z')) ? (key & 0x1F) : key` + * when ctrl is set. * * Drivers whose hardware cannot report Ctrl leave this false. */ diff --git a/TactilityKernel/include/tactility/drivers/usb_host_hid.h b/TactilityKernel/include/tactility/drivers/usb_host_hid.h index ac5b7aa1f..abba9c95c 100644 --- a/TactilityKernel/include/tactility/drivers/usb_host_hid.h +++ b/TactilityKernel/include/tactility/drivers/usb_host_hid.h @@ -50,7 +50,9 @@ typedef struct { * `key_code`, because the two encodings collide: the C0 control codes a terminal expects * for Ctrl chords (Ctrl+C is 0x03, Ctrl+K is 0x0B) overlap the UsbHidKey constants above * (USB_HID_KEY_END is 3, USB_HID_KEY_PREV is 11). A consumer wanting control codes derives - * them here, e.g. `(key_code >= 'a' && key_code <= 'z') ? (key_code & 0x1F) : key_code`. + * them here, e.g. + * `((key_code >= 'a' && key_code <= 'z') || (key_code >= 'A' && key_code <= 'Z')) ? + * (key_code & 0x1F) : key_code`. */ struct { uint32_t key_code; bool pressed; bool ctrl; bool alt; } key; struct { int32_t dx; int32_t dy; } mouse_move; From 4261407c7a4cbfba67f4e06a17f354b0fa65e288 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Sat, 8 Aug 2026 03:43:14 +1000 Subject: [PATCH 6/6] always miss something.... --- Devices/m5stack-papers3/source/drivers/papers3_display.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/Devices/m5stack-papers3/source/drivers/papers3_display.cpp b/Devices/m5stack-papers3/source/drivers/papers3_display.cpp index d21e5af2f..949c1d435 100644 --- a/Devices/m5stack-papers3/source/drivers/papers3_display.cpp +++ b/Devices/m5stack-papers3/source/drivers/papers3_display.cpp @@ -60,7 +60,6 @@ static error_t papers3_display_init(Device* device) { const auto* config = GET_CONFIG(device); auto* internal = static_cast(device_get_driver_data(device)); power_on(internal); - epd_clear(); // The bootloader/boot-logo splash draws via partial refreshes that never get a real quality // pass, leaving a faint ghost. Run a full clear now, before LVGL's first flush ever reaches // draw_bitmap(), so it never has to undo content LVGL already put on screen.