Papers3 improvements (#611)

- 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
This commit is contained in:
Shadowtrance 2026-08-08 23:02:49 +10:00 committed by GitHub
parent 564d8af64c
commit dc3f6104b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 251 additions and 92 deletions

View File

@ -11,46 +11,27 @@
#include <epd_board.h> #include <epd_board.h>
#include <epdiy.h> #include <epdiy.h>
#include <esp_heap_caps.h>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#define TAG "Papers3Display" #define TAG "Papers3Display"
#define GET_CONFIG(device) (static_cast<const Papers3DisplayConfig*>((device)->config)) #define GET_CONFIG(device) (static_cast<const Papers3DisplayConfig*>((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<uint8_t>((p1 << 4U) | p0);
}
memcpy(&s_unpack_lut[byte], dst, sizeof(dst));
}
}
extern "C" { extern "C" {
extern Module m5stack_papers3_module; extern Module m5stack_papers3_module;
// epd_hl_init() sets an internal already_initialized flag and has no matching deinit, so the // epd_hl_init() has no matching deinit and sets an internal already_initialized flag, so the
// highlevel state (and the framebuffer it owns) must persist across stop()/start() cycles and be // highlevel state must persist across stop()/start() cycles and be reused rather than recreated.
// reused rather than recreated - ported from the old deprecated-HAL EpdiyDisplay's identical
// s_hlInitialized/s_hlState statics.
static bool s_hl_initialized = false; static bool s_hl_initialized = false;
static EpdiyHighlevelState s_hl_state = {}; static EpdiyHighlevelState s_hl_state = {};
struct Papers3DisplayInternal { struct Papers3DisplayInternal {
EpdiyHighlevelState hl_state; EpdiyHighlevelState hl_state;
uint8_t* framebuffer; 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; uint8_t* packed_buffer;
bool powered; bool powered;
}; };
@ -76,16 +57,19 @@ static error_t papers3_display_reset(Device* device) {
} }
static error_t papers3_display_init(Device* device) { static error_t papers3_display_init(Device* device) {
const auto* config = GET_CONFIG(device);
auto* internal = static_cast<Papers3DisplayInternal*>(device_get_driver_data(device)); auto* internal = static_cast<Papers3DisplayInternal*>(device_get_driver_data(device));
power_on(internal); power_on(internal);
epd_clear(); // The bootloader/boot-logo splash draws via partial refreshes that never get a real quality
epd_hl_set_all_white(&internal->hl_state); // 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; return ERROR_NONE;
} }
// LVGL only ever calls this with the full frame: DISPLAY_COLOR_FORMAT_MONOCHROME forces // Reports GRAYSCALE8 (not MONOCHROME) so LVGL uses partial/tile updates instead of forcing
// LV_DISPLAY_RENDER_MODE_FULL in the generic kernel LVGL bridge (lvgl_display.c), and FULL mode // full-frame - the bridge hardcodes full-frame for MONOCHROME/I1 regardless of capability flags.
// only presents (calls draw_bitmap) once per render cycle, with the complete 0,0..hres,vres rect. // 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) { 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<Papers3DisplayInternal*>(device_get_driver_data(device)); auto* internal = static_cast<Papers3DisplayInternal*>(device_get_driver_data(device));
const auto* config = GET_CONFIG(device); const auto* config = GET_CONFIG(device);
@ -93,32 +77,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 width = x_end - x_start;
const int32_t height = y_end - y_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 // color_data is DISPLAY_COLOR_FORMAT_GRAYSCALE8: row-major, 1 byte/pixel luminance
// with the palette header already stripped by the caller). Bit 1 = white/lit (LVGL's I1 blend // (0x00=black..0xFF=white, matching LVGL's L8). EPDiy wants 4bpp packed (2px/byte, 0x0=black,
// sets a bit when the source luminance is above its threshold), bit 0 = 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<const uint8_t*>(color_data); const auto* src = static_cast<const uint8_t*>(color_data);
const size_t src_stride = static_cast<size_t>(width + 7) / 8; const size_t src_stride = static_cast<size_t>(width);
const size_t packed_stride = static_cast<size_t>(width + 1) / 2; const size_t packed_stride = static_cast<size_t>(width + 1) / 2;
for (int32_t row = 0; row < height; row++) { for (int32_t row = 0; row < height; row++) {
const uint8_t* src_row = src + static_cast<size_t>(row) * src_stride; const uint8_t* src_row = src + static_cast<size_t>(row) * src_stride;
uint8_t* dst_row = internal->packed_buffer + static_cast<size_t>(row) * packed_stride; uint8_t* dst_row = internal->packed_buffer + static_cast<size_t>(row) * packed_stride;
int32_t col = 0; int32_t col = 0;
// Bulk path: one LUT lookup + 4-byte copy per 8 source pixels. for (; col + 2 <= width; col += 2) {
for (; col + 8 <= width; col += 8) { const uint8_t p0 = src_row[col] >> 4U;
memcpy(dst_row + col / 2, &s_unpack_lut[src_row[col / 8]], 4); const uint8_t p1 = src_row[col + 1] >> 4U;
}
// 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;
}
dst_row[col / 2] = static_cast<uint8_t>((p1 << 4U) | p0); dst_row[col / 2] = static_cast<uint8_t>((p1 << 4U) | p0);
} }
if (col < width) { // odd width: last column has no pair, low nibble unused
dst_row[col / 2] = static_cast<uint8_t>(src_row[col] >> 4U);
}
} }
const EpdRect update_area = { const EpdRect update_area = {
@ -152,7 +130,7 @@ static error_t papers3_display_disp_on_off(Device* device, bool on_off) {
} }
static DisplayColorFormat papers3_display_get_color_format(Device*) { 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 // epd_width()/epd_height() are the panel's native, unrotated dimensions (display->width/height in
@ -172,7 +150,7 @@ static uint16_t papers3_display_get_resolution_y(Device*) {
static void papers3_display_get_frame_buffer(Device*, uint8_t, void** out_buffer) { 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 // 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. // get_frame_buffer_count() and draw_bitmap()'s conversion.
*out_buffer = nullptr; *out_buffer = nullptr;
} }
@ -184,7 +162,9 @@ static uint8_t papers3_display_get_frame_buffer_count(Device*) {
// endregion // endregion
static const DisplayApi papers3_display_api = { static const DisplayApi papers3_display_api = {
.capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME | DISPLAY_CAPABILITY_SLOW_REFRESH, // 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_PREFER_EXTERNAL_RAM,
.reset = papers3_display_reset, .reset = papers3_display_reset,
.init = papers3_display_init, .init = papers3_display_init,
.draw_bitmap = papers3_display_draw_bitmap, .draw_bitmap = papers3_display_draw_bitmap,
@ -213,12 +193,6 @@ static const DisplayApi papers3_display_api = {
static error_t start(Device* device) { static error_t start(Device* device) {
const auto* config = GET_CONFIG(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<Papers3DisplayInternal*>(malloc(sizeof(Papers3DisplayInternal))); auto* internal = static_cast<Papers3DisplayInternal*>(malloc(sizeof(Papers3DisplayInternal)));
if (internal == nullptr) { if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY; return ERROR_OUT_OF_MEMORY;
@ -245,8 +219,12 @@ static error_t start(Device* device) {
internal->framebuffer = epd_hl_get_framebuffer(&internal->hl_state); internal->framebuffer = epd_hl_get_framebuffer(&internal->hl_state);
// Sized for the rotated (LVGL-facing) resolution - see get_resolution_x()/y()'s comment. // 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<size_t>((epd_rotated_display_width() + 1) / 2) * static_cast<size_t>(epd_rotated_display_height()); const size_t packed_buffer_size = static_cast<size_t>((epd_rotated_display_width() + 1) / 2) * static_cast<size_t>(epd_rotated_display_height());
internal->packed_buffer = static_cast<uint8_t*>(malloc(packed_buffer_size)); internal->packed_buffer = static_cast<uint8_t*>(heap_caps_malloc(packed_buffer_size, MALLOC_CAP_SPIRAM));
if (internal->packed_buffer == nullptr) { if (internal->packed_buffer == nullptr) {
LOG_E(TAG, "Failed to allocate packed pixel buffer"); LOG_E(TAG, "Failed to allocate packed pixel buffer");
epd_deinit(); epd_deinit();

View File

@ -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. // REG_INT_STAT polling (when no IRQ pin) and software key-repeat ticking.
static constexpr uint32_t POLL_INTERVAL_MS = 20; 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 // 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 // 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 // constants - matching KeyboardKeyData::key's driver-defined contract and the same convention
// m5stack-module's cardputer_keyboard.cpp kernel driver already uses. // 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) { static uint32_t tab5_translate_key(uint8_t keycode, uint8_t modifier, bool ctrl) {
const bool shift = (modifier & 0x22U) != 0U; const bool shift = (modifier & 0x22U) != 0U;
@ -172,6 +184,15 @@ static uint32_t now_ms() {
return static_cast<uint32_t>(esp_timer_get_time() / 1000); return static_cast<uint32_t>(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;
bool alt;
};
struct Tab5KeyboardInternal { struct Tab5KeyboardInternal {
QueueHandle_t queue; QueueHandle_t queue;
@ -181,6 +202,7 @@ struct Tab5KeyboardInternal {
bool aa_held; bool aa_held;
bool aa_tapped; bool aa_tapped;
bool ctrl_held; bool ctrl_held;
bool alt_held;
// IRQ-driven event gating // IRQ-driven event gating
volatile bool irq_pending; volatile bool irq_pending;
@ -193,7 +215,7 @@ struct Tab5KeyboardInternal {
uint32_t last_poll_ms; uint32_t last_poll_ms;
// Software key-repeat state (tracked by position to survive modifier changes) // 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_row;
uint8_t repeat_col; uint8_t repeat_col;
uint32_t repeat_start_ms; uint32_t repeat_start_ms;
@ -289,16 +311,22 @@ static void remove_irq_pin(Tab5KeyboardInternal* internal) {
// drain_events - reads all pending events from the device queue // drain_events - reads all pending events from the device queue
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
static void drain_events(Device* device, Tab5KeyboardInternal* internal) { static void drain_events(Device* device, Tab5KeyboardInternal* internal) {
// 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; uint8_t count = 0;
if (!read_reg(device, REG_EVENT_NUM, &count) || count == 0) { if (!read_reg(device, REG_EVENT_NUM, &count) || count == 0) {
return; break;
} }
while (count > 0) {
uint8_t raw = 0; uint8_t raw = 0;
if (!read_reg(device, REG_KEY_EVENT, &raw) || raw == KEY_EVENT_EMPTY) { if (!read_reg(device, REG_KEY_EVENT, &raw) || raw == KEY_EVENT_EMPTY) {
break; break;
} }
drained++;
const bool pressed = (raw & 0x80U) != 0U; const bool pressed = (raw & 0x80U) != 0U;
const uint8_t row = (raw >> 4U) & 0x07U; const uint8_t row = (raw >> 4U) & 0x07U;
@ -308,7 +336,6 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) {
if (row == MOD_ROW_SYM && col == MOD_COL_SYM) { if (row == MOD_ROW_SYM && col == MOD_COL_SYM) {
internal->sym_active = pressed; internal->sym_active = pressed;
update_leds(device, internal); update_leds(device, internal);
count--;
continue; continue;
} }
if (row == MOD_ROW_AA && col == MOD_COL_AA) { if (row == MOD_ROW_AA && col == MOD_COL_AA) {
@ -324,16 +351,14 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) {
internal->aa_tapped = false; internal->aa_tapped = false;
} }
update_leds(device, internal); update_leds(device, internal);
count--;
continue; continue;
} }
if (row == MOD_ROW_CTRL && col == MOD_COL_CTRL) { if (row == MOD_ROW_CTRL && col == MOD_COL_CTRL) {
internal->ctrl_held = pressed; internal->ctrl_held = pressed;
count--;
continue; continue;
} }
if (row == MOD_ROW_ALT && col == MOD_COL_ALT) { if (row == MOD_ROW_ALT && col == MOD_COL_ALT) {
count--; internal->alt_held = pressed;
continue; continue;
} }
@ -354,10 +379,11 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) {
// no business reaching into, so ESC is now just queued as a normal key // 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 // 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). // 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, internal->alt_held };
xQueueSend(internal->queue, &event, 0);
// Arm software repeat tracking by row/col to survive modifier changes // Arm software repeat tracking by row/col to survive modifier changes
const uint32_t now = now_ms(); const uint32_t now = now_ms();
internal->repeat_key = lv_key; internal->repeat_event = event;
internal->repeat_row = row; internal->repeat_row = row;
internal->repeat_col = col; internal->repeat_col = col;
internal->repeat_start_ms = now; internal->repeat_start_ms = now;
@ -370,12 +396,11 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) {
} }
} else if (row == internal->repeat_row && col == internal->repeat_col) { } else if (row == internal->repeat_row && col == internal->repeat_col) {
// Match release by position, not translated value — survives sticky Aa clear // 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 // Clear INT status after draining so the line de-asserts
@ -434,13 +459,19 @@ static void poll_if_due(Device* device, Tab5KeyboardInternal* internal) {
drain_events(device, internal); drain_events(device, internal);
} }
// Software key-repeat (runs every tick regardless of IRQ) // Software key-repeat (runs every tick regardless of IRQ).
if (internal->repeat_key != 0U) { //
if ((now - internal->repeat_start_ms) >= REPEAT_INITIAL_MS) { // 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; const uint32_t last = internal->repeat_last_ms;
if (last == 0 || (now - last) >= REPEAT_RATE_MS) { if (last == 0 || (repeat_now - last) >= REPEAT_RATE_MS) {
internal->repeat_last_ms = now; internal->repeat_last_ms = repeat_now;
xQueueSend(internal->queue, &internal->repeat_key, 0); xQueueSend(internal->queue, &internal->repeat_event, 0);
} }
} }
} }
@ -464,7 +495,7 @@ static error_t start(Device* device) {
} }
memset(internal, 0, sizeof(Tab5KeyboardInternal)); memset(internal, 0, sizeof(Tab5KeyboardInternal));
internal->queue = xQueueCreate(20, sizeof(uint32_t)); internal->queue = xQueueCreate(20, sizeof(Tab5KeyEvent));
if (internal->queue == nullptr) { if (internal->queue == nullptr) {
free(internal); free(internal);
return ERROR_OUT_OF_MEMORY; return ERROR_OUT_OF_MEMORY;
@ -522,15 +553,19 @@ static error_t tab5_keyboard_read_key(Device* device, KeyboardKeyData* data) {
poll_if_due(device, internal); poll_if_due(device, internal);
uint32_t lv_key = 0; Tab5KeyEvent event = {};
if (xQueueReceive(internal->queue, &lv_key, 0) == pdTRUE) { if (xQueueReceive(internal->queue, &event, 0) == pdTRUE) {
data->key = lv_key; data->key = event.key;
data->pressed = true; data->pressed = true;
data->continue_reading = uxQueueMessagesWaiting(internal->queue) > 0; data->continue_reading = uxQueueMessagesWaiting(internal->queue) > 0;
data->ctrl = event.ctrl;
data->alt = event.alt;
} else { } else {
data->key = 0; data->key = 0;
data->pressed = false; data->pressed = false;
data->continue_reading = false; data->continue_reading = false;
data->ctrl = false;
data->alt = false;
} }
return ERROR_NONE; return ERROR_NONE;

View File

@ -53,6 +53,13 @@ struct LvglDisplayConfig {
* the LV_COLOR_FORMAT_I1 path (already always-full-frame). * the LV_COLOR_FORMAT_I1 path (already always-full-frame).
*/ */
bool force_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 prefer_external_ram;
}; };
/** /**

View File

@ -67,11 +67,13 @@ void lvgl_devices_attach() {
// itself is never asked to do something it can't. // 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) && 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); display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_CAP_MIRROR);
bool prefer_external_ram_buffer = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_PREFER_EXTERNAL_RAM);
struct LvglDisplayConfig lvgl_display_config = { struct LvglDisplayConfig lvgl_display_config = {
.buffer_height = (uint16_t)(vres > 10 ? vres / 10 : vres), .buffer_height = (uint16_t)(vres > 10 ? vres / 10 : vres),
.sw_rotate = !can_hw_rotate, .sw_rotate = !can_hw_rotate,
.swap_bytes = swap_bytes, .swap_bytes = swap_bytes,
.force_full_frame = display_requires_full_frame .force_full_frame = display_requires_full_frame,
.prefer_external_ram = prefer_external_ram_buffer
}; };
lv_disp_t* added_display = NULL; lv_disp_t* added_display = NULL;
if (lvgl_display_add(kernel_display_device, &lvgl_display_config, &added_display) == ERROR_NONE) { if (lvgl_display_add(kernel_display_device, &lvgl_display_config, &added_display) == ERROR_NONE) {

View File

@ -59,13 +59,18 @@ struct LvglDisplayCtx {
bool byte_swap; bool byte_swap;
}; };
static void* lvgl_display_alloc_buffer(size_t size_bytes) { static void* lvgl_display_alloc_buffer(size_t size_bytes, bool prefer_external_ram) {
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
// Must match LV_DRAW_BUF_ALIGN (can be > 4 - e.g. 64, tied to the cache line size for // 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 // 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 // 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. // 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 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 = 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) { if (buf == NULL) {
buf = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size_bytes, MALLOC_CAP_DEFAULT); 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). // (e.g. ssd1306_draw_bitmap()'s row-to-page transpose).
*out = LV_COLOR_FORMAT_I1; *out = LV_COLOR_FORMAT_I1;
return true; 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: default:
return false; 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 // 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. // owned buffer instead of computing partial-region byte offsets against that packing.
buf_size_bytes = (size_t)((hres + 7) / 8) * vres + 8; 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->prefer_external_ram);
if (ctx->buf1 == NULL) { if (ctx->buf1 == NULL) {
delete wrapper; delete wrapper;
return ERROR_OUT_OF_MEMORY; return ERROR_OUT_OF_MEMORY;
@ -416,13 +428,13 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig*
? vres : config->buffer_height; ? vres : config->buffer_height;
buf_size_bytes = (size_t)hres * buf_height * bpp; 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->prefer_external_ram);
if (ctx->buf1 == NULL) { if (ctx->buf1 == NULL) {
delete wrapper; delete wrapper;
return ERROR_OUT_OF_MEMORY; return ERROR_OUT_OF_MEMORY;
} }
if (config->double_buffer) { if (config->double_buffer) {
ctx->buf2 = lvgl_display_alloc_buffer(buf_size_bytes); ctx->buf2 = lvgl_display_alloc_buffer(buf_size_bytes, config->prefer_external_ram);
if (ctx->buf2 == NULL) { if (ctx->buf2 == NULL) {
lvgl_display_free_buffer(ctx->buf1); lvgl_display_free_buffer(ctx->buf1);
delete wrapper; delete wrapper;
@ -436,7 +448,7 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig*
ctx->buf_size_bytes = buf_size_bytes; ctx->buf_size_bytes = buf_size_bytes;
if (ctx->sw_rotate) { 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->prefer_external_ram);
if (ctx->rotate_buf == NULL) { if (ctx->rotate_buf == NULL) {
if (ctx->owns_buffers) { if (ctx->owns_buffers) {
lvgl_display_free_buffer(ctx->buf1); lvgl_display_free_buffer(ctx->buf1);

View File

@ -108,7 +108,16 @@ static uint32_t hid_keycode_to_key(uint8_t modifier, uint8_t key_code,
default: break; 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]))) { if (key_code < (sizeof(keycode2ascii) / sizeof(keycode2ascii[0]))) {
bool is_letter = (key_code >= 0x04 && key_code <= 0x1D); bool is_letter = (key_code >= 0x04 && key_code <= 0x1D);
@ -157,6 +166,10 @@ static void hid_interface_callback(hid_host_device_handle_t handle,
if (params.proto == HID_PROTOCOL_KEYBOARD) { if (params.proto == HID_PROTOCOL_KEYBOARD) {
if (data_len < sizeof(hid_keyboard_input_report_boot_t)) break; if (data_len < sizeof(hid_keyboard_input_report_boot_t)) break;
auto* kb = reinterpret_cast<const hid_keyboard_input_report_boot_t*>(data); auto* kb = reinterpret_cast<const hid_keyboard_input_report_boot_t*>(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++) { for (int i = 0; i < HID_KEYBOARD_KEY_MAX; i++) {
uint8_t prev_hid = ctx->prev_keys[i]; uint8_t prev_hid = ctx->prev_keys[i];
@ -169,7 +182,7 @@ static void hid_interface_callback(hid_host_device_handle_t handle,
uint32_t lv_key = ctx->pressed_lv_keys[prev_hid]; uint32_t lv_key = ctx->pressed_lv_keys[prev_hid];
ctx->pressed_lv_keys[prev_hid] = 0; ctx->pressed_lv_keys[prev_hid] = 0;
if (lv_key) { 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); publish_event(ctx, &evt);
} }
} }
@ -208,7 +221,10 @@ static void hid_interface_callback(hid_host_device_handle_t handle,
uint32_t lv_key = hid_keycode_to_key(kb->modifier.val, hid_code, uint32_t lv_key = hid_keycode_to_key(kb->modifier.val, hid_code,
ctx->caps_lock_active, ctx->num_lock_active); ctx->caps_lock_active, ctx->num_lock_active);
if (lv_key) { if (lv_key) {
UsbHidEvent evt = { .type = USB_HID_EVENT_KEY, .key = { lv_key, true } }; UsbHidEvent evt = {
.type = USB_HID_EVENT_KEY,
.key = { lv_key, true, with_ctrl, with_alt }
};
publish_event(ctx, &evt); publish_event(ctx, &evt);
ctx->pressed_lv_keys[hid_code] = lv_key; ctx->pressed_lv_keys[hid_code] = lv_key;
} }

View File

@ -3,9 +3,18 @@
#include <symbols/esp_http_client.h> #include <symbols/esp_http_client.h>
#include <sdkconfig.h>
#include <esp_http_client.h> #include <esp_http_client.h>
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
#include <esp_crt_bundle.h>
#endif
const esp_elfsym esp_http_client_symbols[] = { 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_init),
ESP_ELFSYM_EXPORT(esp_http_client_perform), ESP_ELFSYM_EXPORT(esp_http_client_perform),
ESP_ELFSYM_EXPORT(esp_http_client_cancel_request), ESP_ELFSYM_EXPORT(esp_http_client_cancel_request),

View File

@ -1,3 +1,4 @@
#include <sdkconfig.h>
#include <private/elf_symbol.h> #include <private/elf_symbol.h>
#include <cstddef> #include <cstddef>
@ -40,6 +41,7 @@ const esp_elfsym freertos_symbols[] = {
ESP_ELFSYM_EXPORT(xTaskGenericNotify), ESP_ELFSYM_EXPORT(xTaskGenericNotify),
ESP_ELFSYM_EXPORT(xTaskGenericNotifyFromISR), ESP_ELFSYM_EXPORT(xTaskGenericNotifyFromISR),
ESP_ELFSYM_EXPORT(ulTaskGenericNotifyTake), ESP_ELFSYM_EXPORT(ulTaskGenericNotifyTake),
ESP_ELFSYM_EXPORT(xTaskGetCurrentTaskHandle),
ESP_ELFSYM_EXPORT(xTaskGetTickCount), ESP_ELFSYM_EXPORT(xTaskGetTickCount),
ESP_ELFSYM_EXPORT(xTaskGetTickCountFromISR), ESP_ELFSYM_EXPORT(xTaskGetTickCountFromISR),
ESP_ELFSYM_EXPORT(pvTaskGetThreadLocalStoragePointer), ESP_ELFSYM_EXPORT(pvTaskGetThreadLocalStoragePointer),
@ -101,6 +103,12 @@ const esp_elfsym freertos_symbols[] = {
ESP_ELFSYM_EXPORT(vPortYield), ESP_ELFSYM_EXPORT(vPortYield),
ESP_ELFSYM_EXPORT(vPortEnterCritical), ESP_ELFSYM_EXPORT(vPortEnterCritical),
ESP_ELFSYM_EXPORT(vPortExitCritical), ESP_ELFSYM_EXPORT(vPortExitCritical),
#if defined(CONFIG_IDF_TARGET_ESP32P4) || defined(CONFIG_IDF_TARGET_ESP32S3)
ESP_ELFSYM_EXPORT(xPortEnterCriticalTimeout),
#endif
#if defined(CONFIG_IDF_TARGET_ESP32P4)
ESP_ELFSYM_EXPORT(vPortExitCriticalMultiCore),
#endif
ESP_ELFSYM_EXPORT(xPortInIsrContext), ESP_ELFSYM_EXPORT(xPortInIsrContext),
ESP_ELFSYM_EXPORT(xPortCanYield), ESP_ELFSYM_EXPORT(xPortCanYield),
ESP_ELFSYM_EXPORT(xPortGetCoreID), ESP_ELFSYM_EXPORT(xPortGetCoreID),

View File

@ -33,6 +33,7 @@
#include <esp_heap_caps.h> #include <esp_heap_caps.h>
#include <esp_timer.h> #include <esp_timer.h>
#include <esp_system.h> #include <esp_system.h>
#include <esp_vfs.h>
#include <fcntl.h> #include <fcntl.h>
#include <lwip/sockets.h> #include <lwip/sockets.h>
#include <lwip/netdb.h> #include <lwip/netdb.h>
@ -89,6 +90,7 @@ const esp_elfsym main_symbols[] {
ESP_ELFSYM_EXPORT(close), ESP_ELFSYM_EXPORT(close),
ESP_ELFSYM_EXPORT(rmdir), ESP_ELFSYM_EXPORT(rmdir),
ESP_ELFSYM_EXPORT(unlink), ESP_ELFSYM_EXPORT(unlink),
ESP_ELFSYM_EXPORT(open),
// strings.h // strings.h
ESP_ELFSYM_EXPORT(explicit_bzero), ESP_ELFSYM_EXPORT(explicit_bzero),
ESP_ELFSYM_EXPORT(strcasecmp), ESP_ELFSYM_EXPORT(strcasecmp),
@ -194,6 +196,10 @@ const esp_elfsym main_symbols[] {
ESP_ELFSYM_EXPORT(fgets), ESP_ELFSYM_EXPORT(fgets),
ESP_ELFSYM_EXPORT(fopen), ESP_ELFSYM_EXPORT(fopen),
ESP_ELFSYM_EXPORT(freopen), 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(fputc),
ESP_ELFSYM_EXPORT(fputs), ESP_ELFSYM_EXPORT(fputs),
ESP_ELFSYM_EXPORT(fprintf), ESP_ELFSYM_EXPORT(fprintf),
@ -239,6 +245,7 @@ const esp_elfsym main_symbols[] {
ESP_ELFSYM_EXPORT(memchr), ESP_ELFSYM_EXPORT(memchr),
ESP_ELFSYM_EXPORT(memmove), ESP_ELFSYM_EXPORT(memmove),
ESP_ELFSYM_EXPORT(strdup), ESP_ELFSYM_EXPORT(strdup),
ESP_ELFSYM_EXPORT(stpcpy),
// ctype // ctype
ESP_ELFSYM_EXPORT(isalnum), ESP_ELFSYM_EXPORT(isalnum),
@ -256,6 +263,13 @@ const esp_elfsym main_symbols[] {
ESP_ELFSYM_EXPORT(toupper), ESP_ELFSYM_EXPORT(toupper),
// ESP-IDF // ESP-IDF
ESP_ELFSYM_EXPORT(esp_log), 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_write),
ESP_ELFSYM_EXPORT(esp_log_timestamp), ESP_ELFSYM_EXPORT(esp_log_timestamp),
ESP_ELFSYM_EXPORT(esp_err_to_name), ESP_ELFSYM_EXPORT(esp_err_to_name),
@ -327,9 +341,26 @@ const esp_elfsym main_symbols[] {
ESP_ELFSYM_EXPORT(ipaddr_addr), ESP_ELFSYM_EXPORT(ipaddr_addr),
// POSIX socket names (VFS wrappers used when apps include <sys/socket.h>) // POSIX socket names (VFS wrappers used when apps include <sys/socket.h>)
ESP_ELFSYM_EXPORT(select), 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 // sys/stat.h
ESP_ELFSYM_EXPORT(stat), ESP_ELFSYM_EXPORT(stat),
ESP_ELFSYM_EXPORT(mkdir), 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_netif.h
ESP_ELFSYM_EXPORT(esp_netif_get_ip_info), ESP_ELFSYM_EXPORT(esp_netif_get_ip_info),
ESP_ELFSYM_EXPORT(esp_netif_get_handle_from_ifkey), 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),
ESP_ELFSYM_EXPORT(tinfl_decompress_mem_to_callback), ESP_ELFSYM_EXPORT(tinfl_decompress_mem_to_callback),
ESP_ELFSYM_EXPORT(tinfl_decompress_mem_to_mem), 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 // ledc
ESP_ELFSYM_EXPORT(ledc_update_duty), ESP_ELFSYM_EXPORT(ledc_update_duty),
ESP_ELFSYM_EXPORT(ledc_set_freq), ESP_ELFSYM_EXPORT(ledc_set_freq),

View File

@ -23,7 +23,13 @@ enum DisplayCapability {
DISPLAY_CAPABILITY_SLEEP = 1 << 6, DISPLAY_CAPABILITY_SLEEP = 1 << 6,
DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME = 1 << 7, DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME = 1 << 7,
/** Can be used by e-paper with pointer devices for long click timing changes. */ /** 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_PREFER_EXTERNAL_RAM = 1 << 9
}; };
/** /**
@ -36,6 +42,11 @@ enum DisplayColorFormat {
DISPLAY_COLOR_FORMAT_RGB565 = 0x3, DISPLAY_COLOR_FORMAT_RGB565 = 0x3,
DISPLAY_COLOR_FORMAT_RGB565_SWAPPED = 0x4, DISPLAY_COLOR_FORMAT_RGB565_SWAPPED = 0x4,
DISPLAY_COLOR_FORMAT_RGB888 = 0x5, 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,
}; };
/** /**

View File

@ -24,6 +24,25 @@ struct KeyboardKeyData {
* immediately to drain it. False if this was the last pending event. * immediately to drain it. False if this was the last pending event.
*/ */
bool continue_reading; 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 >= '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;
}; };
/** /**

View File

@ -43,7 +43,18 @@ typedef enum {
typedef struct { typedef struct {
UsbHidEventType type; UsbHidEventType type;
union { 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 >= '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 { int32_t dx; int32_t dy; } mouse_move;
struct { bool button1; bool button2; } mouse_btn; struct { bool button1; bool button2; } mouse_btn;
struct { int32_t delta; } scroll; struct { int32_t delta; } scroll;

View File

@ -9,6 +9,12 @@ extern "C" {
error_t keyboard_read_key(Device* device, KeyboardKeyData* data) { error_t keyboard_read_key(Device* device, KeyboardKeyData* data) {
const auto* driver = device_get_driver(device); 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); return KEYBOARD_DRIVER_API(driver)->read_key(device, data);
} }

View File

@ -171,6 +171,7 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(file_system_unmount), DEFINE_MODULE_SYMBOL(file_system_unmount),
DEFINE_MODULE_SYMBOL(file_system_is_mounted), DEFINE_MODULE_SYMBOL(file_system_is_mounted),
DEFINE_MODULE_SYMBOL(file_system_get_path), DEFINE_MODULE_SYMBOL(file_system_get_path),
DEFINE_MODULE_SYMBOL(file_system_for_each),
// memory // memory
DEFINE_MODULE_SYMBOL(MEMORY_POLICY_DEFAULT), DEFINE_MODULE_SYMBOL(MEMORY_POLICY_DEFAULT),
DEFINE_MODULE_SYMBOL(memory_print_stats), DEFINE_MODULE_SYMBOL(memory_print_stats),