2026-08-12 21:29:35 +02:00

379 lines
17 KiB
C++

// SPDX-License-Identifier: Apache-2.0
#include <lvgl/devices/pointer.h>
#include <lvgl/devices/device_context.h>
#include <tactility/device.h>
#include <tactility/drivers/pointer.h>
#include <cstring>
#include <new>
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);
// Tracks the first indev created by lvgl_pointer_add() still alive, for lvgl_pointer_get_default().
// Only ever set/cleared by lvgl_pointer_add()/lvgl_pointer_remove(), so it can never point at an
// indev created by other code (e.g. the deprecated HAL's own LVGL pointer registration).
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 &&
(calibration->x_max - calibration->x_min) >= LVGL_POINTER_CALIBRATION_MIN_RANGE &&
(calibration->y_max - calibration->y_min) >= LVGL_POINTER_CALIBRATION_MIN_RANGE;
}
// Linear per-axis rescale of [x_min,x_max]/[y_min,y_max] onto [0,target_x_max]/[0,target_y_max],
// clamped. Mirrors TouchCalibrationSettings.cpp's applyCalibration(). Kept as a standalone
// function (not inlined into the read callback) so the math is isolated and easy to reason about.
static void lvgl_pointer_calibration_apply(
const struct LvglPointerCalibration* calibration,
int32_t target_x_max,
int32_t target_y_max,
uint16_t* x,
uint16_t* y
) {
int64_t mapped_x = ((int64_t)*x - calibration->x_min) * target_x_max /
((int64_t)calibration->x_max - calibration->x_min);
int64_t mapped_y = ((int64_t)*y - calibration->y_min) * target_y_max /
((int64_t)calibration->y_max - calibration->y_min);
if (mapped_x < 0) mapped_x = 0;
if (mapped_x > target_x_max) mapped_x = target_x_max;
if (mapped_y < 0) mapped_y = 0;
if (mapped_y > target_y_max) mapped_y = target_y_max;
*x = (uint16_t)mapped_x;
*y = (uint16_t)mapped_y;
}
// 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(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]);
}
}
pool->raw_count = point_count;
}
// 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 LvglPointerPool* pool = lvgl_pointer_pool_from_indev(indev);
if (pool == NULL) {
data->state = LV_INDEV_STATE_RELEASED;
return;
}
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, 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;
auto* pool = new(std::nothrow) LvglPointerPool();
if (pool == NULL) {
return ERROR_OUT_OF_MEMORY;
}
pool->device = device;
pool->slot_count = max_touch_points;
// 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;
}
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 = pool->slot_indev[0];
}
return ERROR_NONE;
}
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) {
struct LvglPointerPool* pool = lvgl_pointer_pool_from_indev(indev);
if (pool == NULL) {
return ERROR_INVALID_ARGUMENT;
}
if (calibration == NULL) {
pool->calibration_enabled = false;
return ERROR_NONE;
}
if (!lvgl_pointer_calibration_is_valid(calibration)) {
return ERROR_INVALID_ARGUMENT;
}
pool->calibration = *calibration;
pool->calibration_enabled = true;
return ERROR_NONE;
}
bool lvgl_pointer_get_calibration(lv_indev_t* indev, struct LvglPointerCalibration* out_calibration) {
if (out_calibration == NULL) {
return false;
}
struct LvglPointerPool* pool = lvgl_pointer_pool_from_indev(indev);
if (pool == NULL || !pool->calibration_enabled) {
return false;
}
*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);
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;
}
delete pool;
}