USB device-mode support (#613)

This commit is contained in:
Shadowtrance
2026-08-13 05:29:35 +10:00
committed by GitHub
parent 37c507544b
commit cc8be3faef
94 changed files with 3466 additions and 399 deletions
+261 -75
View File
@@ -2,14 +2,13 @@
#include <lvgl/devices/pointer.h>
#include <lvgl/devices/device_context.h>
#include <tactility/device.h>
#include <tactility/drivers/pointer.h>
constexpr auto* TAG = "lvgl_pointer";
#include <cstring>
#include <new>
struct LvglPointerCtx {
bool calibration_enabled;
struct LvglPointerCalibration calibration;
};
constexpr auto* TAG = "lvgl_pointer";
// Bus reads are expected to complete quickly; bound the wait so a stalled controller can't block the LVGL indev poll.
static const TickType_t LVGL_POINTER_READ_TIMEOUT = pdMS_TO_TICKS(10);
@@ -22,6 +21,61 @@ static lv_indev_t* default_pointer_indev = NULL;
// Mirrors Tactility/Source/settings/TouchCalibrationSettings.cpp's isValid().
static const int32_t LVGL_POINTER_CALIBRATION_MIN_RANGE = 20;
// Caps nearest-neighbor slot tracking (lvgl_pointer_pool_assign) to a fraction of screen width,
// so a lifted finger's slot doesn't jump to grab an unrelated new touch elsewhere on screen.
// Scaled by resolution, not a flat pixel value, so it stays proportionate on any display size.
// Set generously \wide: a too-tight cap misreads a fast scroll's own motion as release+re-press,
// firing whatever is under the finger mid-drag.
//
// \wide False positives (treating one continued drag as two separate touches) are far more
// disruptive than false negatives (merging an unrelated same-spot lift+relanding), which
// favors erring toward a larger cap.
static const int32_t LVGL_POINTER_MAX_TRACK_DIST_FRACTION = 3; // 1/3 of screen width
// One physical touch device backs LVGL_POINTER_MAX_SLOTS independent lv_indev_t instances (a
// "pool"), so LVGL can track that many simultaneous fingers - LVGL v9 has no multi-point indev
// concept (confirmed against lv_indev.h/lv_indev.c: lv_indev_data_t carries exactly one
// lv_point_t/state pair), so N simultaneous independently-clickable widgets requires N indevs.
// Each slot's read callback shares one raw multi-touch read per round rather than each slot
// hitting the bus independently - see lvgl_pointer_read_cb().
struct LvglPointerPool {
struct Device* device;
bool calibration_enabled;
struct LvglPointerCalibration calibration;
uint8_t slot_count;
lv_indev_t* slot_indev[LVGL_POINTER_MAX_SLOTS];
// Per-slot "which finger" tracking. A slot with active=false reports RELEASED and is up for
// grabs by any unmatched raw point next round.
bool slot_active[LVGL_POINTER_MAX_SLOTS];
lv_point_t slot_point[LVGL_POINTER_MAX_SLOTS];
// Shared raw-read cache for this round. Refreshed by whichever slot's read callback runs
// first each round (see round_pos); the rest just consume it - one bus transaction per
// slot_count read callbacks, not one per slot.
uint8_t round_pos;
uint16_t raw_x[LVGL_POINTER_MAX_SLOTS];
uint16_t raw_y[LVGL_POINTER_MAX_SLOTS];
uint8_t raw_count;
};
// Safely narrows an arbitrary lv_indev_t's driver data down to this module's LvglPointerPool,
// or NULL if indev is NULL, wasn't created by lvgl_pointer_add() (LvglDeviceContext is shared by
// every LVGL indev/display this module creates - pointer, trackball, keyboard - so driver_data
// alone doesn't prove it's a pointer pool), or is mid-teardown (context nulled by
// lvgl_pointer_remove()/lvgl_pointer_add()'s cleanup path before the wrapper itself is freed).
static struct LvglPointerPool* lvgl_pointer_pool_from_indev(lv_indev_t* indev) {
if (indev == NULL) {
return NULL;
}
auto* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev);
if (wrapper == NULL || wrapper->device == NULL || device_get_type(wrapper->device) != &POINTER_TYPE) {
return NULL;
}
return (struct LvglPointerPool*)wrapper->context;
}
static bool lvgl_pointer_calibration_is_valid(const struct LvglPointerCalibration* calibration) {
return calibration->x_max > calibration->x_min &&
calibration->y_max > calibration->y_min &&
@@ -53,91 +107,193 @@ static void lvgl_pointer_calibration_apply(
*y = (uint16_t)mapped_y;
}
// Reads the touch controller and applies calibration entirely in the graphics driver's own
// native (LV_DISPLAY_ROTATION_0) coordinate space - native_x_max/native_y_max are just the panel's
// fixed pixel dimensions, not a rotation. This function has no notion of LVGL rotation at all:
// calibration corrects the raw sensor's fixed physical mapping, which never changes with on-screen
// orientation, so it doesn't belong anywhere near rotation math.
static bool lvgl_pointer_read_calibrated(struct Device* device, struct LvglPointerCtx* ctx, int32_t native_x_max, int32_t native_y_max, uint16_t* x, uint16_t* y) {
if (pointer_read_data(device, LVGL_POINTER_READ_TIMEOUT) != ERROR_NONE) {
return false;
// Reads all currently-touched points from the device into pool->raw_x/raw_y/raw_count, applying
// calibration in the graphics driver's own native (LV_DISPLAY_ROTATION_0) coordinate space -
// native_x_max/native_y_max are just the panel's fixed pixel dimensions, not a rotation. This
// function has no notion of LVGL rotation at all: calibration corrects the raw sensor's fixed
// physical mapping, which never changes with on-screen orientation, so it doesn't belong anywhere
// near rotation math.
static void lvgl_pointer_pool_refresh(struct LvglPointerPool* pool, int32_t native_x_max, int32_t native_y_max) {
pool->raw_count = 0;
if (pointer_read_data(pool->device, LVGL_POINTER_READ_TIMEOUT) != ERROR_NONE) {
return;
}
uint8_t point_count = 0;
if (!pointer_get_touched_points(device, x, y, NULL, &point_count, 1) || point_count == 0) {
return false;
if (!pointer_get_touched_points(pool->device, pool->raw_x, pool->raw_y, NULL, &point_count, LVGL_POINTER_MAX_SLOTS) || point_count == 0) {
return;
}
if (point_count > LVGL_POINTER_MAX_SLOTS) point_count = LVGL_POINTER_MAX_SLOTS;
if (pool->calibration_enabled && native_x_max > 0 && native_y_max > 0) {
for (uint8_t i = 0; i < point_count; i++) {
lvgl_pointer_calibration_apply(&pool->calibration, native_x_max, native_y_max, &pool->raw_x[i], &pool->raw_y[i]);
}
}
if (ctx->calibration_enabled && native_x_max > 0 && native_y_max > 0) {
lvgl_pointer_calibration_apply(&ctx->calibration, native_x_max, native_y_max, x, y);
}
return true;
pool->raw_count = point_count;
}
// The actual LVGL indev read callback: wraps lvgl_pointer_read_calibrated() and, only here, applies
// the rotation needed to place the (still native-space) point into the currently active LVGL
// logical space - unconditionally, since native-space coordinates always need this regardless of
// whether calibration is enabled.
// Matches this round's raw points onto pool slots by nearest-neighbor to each slot's last known
// position, so a slot "follows" the same physical finger across rounds instead of jumping when
// the touch controller reports points in a different order (no touch-ID/tracking field exists
// anywhere in this stack - see esp_lcd_touch_get_coordinates()/PointerApi.get_touched_points()).
// Unmatched raw points (new touches) claim the nearest inactive slot. Slots with no matching
// point this round go inactive (RELEASED).
static void lvgl_pointer_pool_assign(struct LvglPointerPool* pool, int32_t native_x_max) {
// Touch drivers clamp raw coordinates to the panel's configured native resolution regardless
// of calibration, so native_x_max is a valid scale reference for the distance cap even when calibration is disabled.
// Falls back to a conservative fixed pixel value if the display/resolution isn't available for some reason.
const int32_t max_track_dist = native_x_max > 0 ? (native_x_max / LVGL_POINTER_MAX_TRACK_DIST_FRACTION) : 150;
const int32_t max_track_dist_sq = max_track_dist * max_track_dist;
bool raw_claimed[LVGL_POINTER_MAX_SLOTS] = {};
// A slot released this round must report RELEASED for at least one round before it can host
// a new touch - otherwise pass 2 immediately reassigns it, and LVGL sees a jump instead of a
// release-then-press.
bool slot_released_now[LVGL_POINTER_MAX_SLOTS] = {};
// First pass: let already-active slots keep following their nearest raw point, so a held
// finger doesn't get reshuffled onto a different slot just because another finger moved.
for (uint8_t s = 0; s < pool->slot_count; s++) {
if (!pool->slot_active[s]) continue;
int32_t best_dist = -1;
int8_t best_raw = -1;
for (uint8_t r = 0; r < pool->raw_count; r++) {
if (raw_claimed[r]) continue;
int32_t dx = (int32_t)pool->raw_x[r] - pool->slot_point[s].x;
int32_t dy = (int32_t)pool->raw_y[r] - pool->slot_point[s].y;
int32_t dist = dx * dx + dy * dy;
if (best_raw < 0 || dist < best_dist) {
best_dist = dist;
best_raw = (int8_t)r;
}
}
if (best_raw >= 0 && best_dist <= max_track_dist_sq) {
raw_claimed[best_raw] = true;
pool->slot_point[s].x = (lv_coord_t)pool->raw_x[best_raw];
pool->slot_point[s].y = (lv_coord_t)pool->raw_y[best_raw];
} else {
pool->slot_active[s] = false;
slot_released_now[s] = true;
}
}
// Second pass: any unclaimed raw point is a new touch - hand it to the first inactive slot
// that wasn't just released this round.
for (uint8_t r = 0; r < pool->raw_count; r++) {
if (raw_claimed[r]) continue;
for (uint8_t s = 0; s < pool->slot_count; s++) {
if (pool->slot_active[s] || slot_released_now[s]) continue;
pool->slot_active[s] = true;
pool->slot_point[s].x = (lv_coord_t)pool->raw_x[r];
pool->slot_point[s].y = (lv_coord_t)pool->raw_y[r];
raw_claimed[r] = true;
break;
}
}
}
// The actual LVGL indev read callback, shared by every slot in the pool. Only the first slot to
// be read each round (round_pos wraps 0..slot_count-1) triggers the real bus read + reassignment;
// the rest just report whatever lvgl_pointer_pool_assign() decided for their slot. Which slot
// happens to run first varies (LVGL calls each indev's timer independently), but since all slots
// share one timer period they complete one full round every slot_count calls regardless of order.
static void lvgl_pointer_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev);
struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)wrapper->context;
lv_display_t* display = lv_indev_get_display(indev);
// lv_display_get_original_*_resolution() is the native (LV_DISPLAY_ROTATION_0) size,
// unaffected by the display's current rotation - no rotation lookup needed to get it.
int32_t native_x_max = display != NULL ? lv_display_get_original_horizontal_resolution(display) - 1 : 0;
int32_t native_y_max = display != NULL ? lv_display_get_original_vertical_resolution(display) - 1 : 0;
uint16_t x = 0;
uint16_t y = 0;
if (!lvgl_pointer_read_calibrated(wrapper->device, ctx, native_x_max, native_y_max, &x, &y)) {
struct LvglPointerPool* pool = lvgl_pointer_pool_from_indev(indev);
if (pool == NULL) {
data->state = LV_INDEV_STATE_RELEASED;
return;
}
data->point.x = x;
data->point.y = y;
data->state = LV_INDEV_STATE_PRESSED;
uint8_t slot = 0;
for (; slot < pool->slot_count; slot++) {
if (pool->slot_indev[slot] == indev) break;
}
if (pool->round_pos == 0) {
lv_display_t* display = lv_indev_get_display(indev);
// lv_display_get_original_*_resolution() is the native (LV_DISPLAY_ROTATION_0) size,
// unaffected by the display's current rotation - no rotation lookup needed to get it.
int32_t native_x_max = display != NULL ? lv_display_get_original_horizontal_resolution(display) - 1 : 0;
int32_t native_y_max = display != NULL ? lv_display_get_original_vertical_resolution(display) - 1 : 0;
lvgl_pointer_pool_refresh(pool, native_x_max, native_y_max);
lvgl_pointer_pool_assign(pool, native_x_max);
}
pool->round_pos = (uint8_t)((pool->round_pos + 1) % pool->slot_count);
if (slot < pool->slot_count && pool->slot_active[slot]) {
data->point = pool->slot_point[slot];
data->state = LV_INDEV_STATE_PRESSED;
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
}
error_t lvgl_pointer_add(struct Device* device, lv_display_t* display, lv_indev_t** out_indev) {
if (device == NULL || out_indev == NULL) {
error_t lvgl_pointer_add(struct Device* device, lv_display_t* display, uint8_t max_touch_points, lv_indev_t** out_indevs) {
if (device == NULL || out_indevs == NULL) {
return ERROR_INVALID_ARGUMENT;
}
if (device_get_type(device) != &POINTER_TYPE) {
return ERROR_INVALID_ARGUMENT;
}
if (max_touch_points == 0) max_touch_points = 1;
if (max_touch_points > LVGL_POINTER_MAX_SLOTS) max_touch_points = LVGL_POINTER_MAX_SLOTS;
struct LvglPointerCtx* ctx = new(std::nothrow) LvglPointerCtx();
if (ctx == NULL) {
auto* pool = new(std::nothrow) LvglPointerPool();
if (pool == NULL) {
return ERROR_OUT_OF_MEMORY;
}
struct LvglDeviceContext* wrapper = new(std::nothrow) LvglDeviceContext(ctx);
if (wrapper == NULL) {
delete ctx;
return ERROR_OUT_OF_MEMORY;
}
wrapper->device = device;
pool->device = device;
pool->slot_count = max_touch_points;
lv_indev_t* indev = lv_indev_create();
if (indev == NULL) {
delete wrapper;
return ERROR_OUT_OF_MEMORY;
// Every slot gets its own LvglDeviceContext wrapper, but all of them point at this same pool.
// On the way out - success or failure - every wrapper's context pointer is nulled before any
// of them are deleted, then the pool itself is freed exactly once: LvglDeviceContext's
// destructor does `::operator delete(context)`, so leaving more than one wrapper owning the
// same pool pointer would double-free it (see lvgl_pointer_remove(), same pattern).
uint8_t created = 0;
for (; created < max_touch_points; created++) {
auto* wrapper = new(std::nothrow) LvglDeviceContext(pool);
if (wrapper == NULL) {
break;
}
wrapper->device = device;
lv_indev_t* indev = lv_indev_create();
if (indev == NULL) {
delete wrapper;
break;
}
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev, lvgl_pointer_read_cb);
lv_indev_set_driver_data(indev, wrapper);
if (display != NULL) {
lv_indev_set_display(indev, display);
}
pool->slot_indev[created] = indev;
out_indevs[created] = indev;
}
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev, lvgl_pointer_read_cb);
lv_indev_set_driver_data(indev, wrapper);
if (display != NULL) {
lv_indev_set_display(indev, display);
if (created < max_touch_points) {
for (uint8_t j = 0; j < created; j++) {
lv_indev_t* slot_indev = pool->slot_indev[j];
auto* slot_wrapper = (LvglDeviceContext*)lv_indev_get_driver_data(slot_indev);
slot_wrapper->context = NULL;
lv_indev_delete(slot_indev);
delete slot_wrapper;
}
delete pool;
return ERROR_OUT_OF_MEMORY;
}
if (default_pointer_indev == NULL) {
default_pointer_indev = indev;
default_pointer_indev = pool->slot_indev[0];
}
*out_indev = indev;
return ERROR_NONE;
}
@@ -145,48 +301,78 @@ lv_indev_t* lvgl_pointer_get_default(void) {
return default_pointer_indev;
}
// Any slot indev's wrapper->context points at the same shared pool, so calibration set through
// any one of them applies to the whole physical device/all its slots (one panel, one calibration).
error_t lvgl_pointer_set_calibration(lv_indev_t* indev, const struct LvglPointerCalibration* calibration) {
if (indev == NULL) {
struct LvglPointerPool* pool = lvgl_pointer_pool_from_indev(indev);
if (pool == NULL) {
return ERROR_INVALID_ARGUMENT;
}
struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev);
struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)wrapper->context;
if (calibration == NULL) {
ctx->calibration_enabled = false;
pool->calibration_enabled = false;
return ERROR_NONE;
}
if (!lvgl_pointer_calibration_is_valid(calibration)) {
return ERROR_INVALID_ARGUMENT;
}
ctx->calibration = *calibration;
ctx->calibration_enabled = true;
pool->calibration = *calibration;
pool->calibration_enabled = true;
return ERROR_NONE;
}
bool lvgl_pointer_get_calibration(lv_indev_t* indev, struct LvglPointerCalibration* out_calibration) {
if (indev == NULL || out_calibration == NULL) {
if (out_calibration == NULL) {
return false;
}
struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev);
struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)wrapper->context;
if (!ctx->calibration_enabled) {
struct LvglPointerPool* pool = lvgl_pointer_pool_from_indev(indev);
if (pool == NULL || !pool->calibration_enabled) {
return false;
}
*out_calibration = ctx->calibration;
*out_calibration = pool->calibration;
return true;
}
int8_t lvgl_pointer_get_slot_index(lv_indev_t* indev) {
struct LvglPointerPool* pool = lvgl_pointer_pool_from_indev(indev);
if (pool == NULL) {
return -1;
}
for (uint8_t i = 0; i < pool->slot_count; i++) {
if (pool->slot_indev[i] == indev) return (int8_t)i;
}
return -1;
}
// Removes every slot indev belonging to the same pool as `indev` (a partial pool removal isn't a
// case that comes up: apps ask for "the pointer device" and get every slot back from
// lvgl_pointer_add(), so they hold either all of a pool's indevs or none).
void lvgl_pointer_remove(lv_indev_t* indev) {
if (indev == NULL) {
return;
}
struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev);
if (default_pointer_indev == indev) {
default_pointer_indev = NULL;
struct LvglPointerPool* pool = (struct LvglPointerPool*)wrapper->context;
uint8_t slot_count = pool->slot_count;
lv_indev_t* slot_indevs[LVGL_POINTER_MAX_SLOTS];
memcpy(slot_indevs, pool->slot_indev, sizeof(lv_indev_t*) * slot_count);
// Every LvglDeviceContext wrapper points at the same pool; null `context` out on all of them
// before deleting any (LvglDeviceContext's destructor frees `context`, and pool must stay
// valid for every wrapper's own delete to run safely) - then free the pool exactly once
// ourselves at the end.
for (uint8_t i = 0; i < slot_count; i++) {
lv_indev_t* slot_indev = slot_indevs[i];
if (slot_indev == NULL) continue;
if (default_pointer_indev == slot_indev) {
default_pointer_indev = NULL;
}
struct LvglDeviceContext* slot_wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(slot_indev);
slot_wrapper->context = NULL;
lv_indev_delete(slot_indev);
delete slot_wrapper;
}
lv_indev_delete(indev);
delete wrapper;
delete pool;
}