Add calibration methods and app (#520)

This commit is contained in:
NellowTCS 2026-04-13 12:58:27 -06:00 committed by GitHub
parent 4dd2762b79
commit 10ba4f58e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 724 additions and 380 deletions

View File

@ -1,10 +1,30 @@
#include "Xpt2046Touch.h" #include "Xpt2046Touch.h"
#include <Tactility/settings/TouchCalibrationSettings.h>
#include <Tactility/lvgl/LvglSync.h> #include <Tactility/lvgl/LvglSync.h>
#include <algorithm>
#include <esp_err.h> #include <esp_err.h>
#include <esp_lcd_touch_xpt2046.h> #include <esp_lcd_touch_xpt2046.h>
static void processCoordinates(esp_lcd_touch_handle_t tp, uint16_t* x, uint16_t* y, uint16_t* strength, uint8_t* pointCount, uint8_t maxPointCount) {
(void)strength;
if (tp == nullptr || x == nullptr || y == nullptr || pointCount == nullptr || *pointCount == 0) {
return;
}
auto* config = static_cast<Xpt2046Touch::Configuration*>(tp->config.user_data);
if (config == nullptr) {
return;
}
const auto settings = tt::settings::touch::getActive();
const auto points = std::min<uint8_t>(*pointCount, maxPointCount);
for (uint8_t i = 0; i < points; i++) {
tt::settings::touch::applyCalibration(settings, config->xMax, config->yMax, x[i], y[i]);
}
}
bool Xpt2046Touch::createIoHandle(esp_lcd_panel_io_handle_t& outHandle) { bool Xpt2046Touch::createIoHandle(esp_lcd_panel_io_handle_t& outHandle) {
const esp_lcd_panel_io_spi_config_t io_config = ESP_LCD_TOUCH_IO_SPI_XPT2046_CONFIG(configuration->spiPinCs); const esp_lcd_panel_io_spi_config_t io_config = ESP_LCD_TOUCH_IO_SPI_XPT2046_CONFIG(configuration->spiPinCs);
return esp_lcd_new_panel_io_spi(configuration->spiDevice, &io_config, &outHandle) == ESP_OK; return esp_lcd_new_panel_io_spi(configuration->spiDevice, &io_config, &outHandle) == ESP_OK;
@ -29,9 +49,9 @@ esp_lcd_touch_config_t Xpt2046Touch::createEspLcdTouchConfig() {
.mirror_x = configuration->mirrorX, .mirror_x = configuration->mirrorX,
.mirror_y = configuration->mirrorY, .mirror_y = configuration->mirrorY,
}, },
.process_coordinates = nullptr, .process_coordinates = processCoordinates,
.interrupt_callback = nullptr, .interrupt_callback = nullptr,
.user_data = nullptr, .user_data = configuration.get(),
.driver_data = nullptr .driver_data = nullptr
}; };
} }

View File

@ -55,5 +55,7 @@ public:
std::string getName() const final { return "XPT2046"; } std::string getName() const final { return "XPT2046"; }
std::string getDescription() const final { return "XPT2046 I2C touch driver"; } std::string getDescription() const final { return "XPT2046 SPI touch driver"; }
bool supportsCalibration() const override { return true; }
}; };

View File

@ -1,373 +1,240 @@
#include "Xpt2046SoftSpi.h" #include "Xpt2046SoftSpi.h"
#include <Tactility/Logger.h> #include <Tactility/Logger.h>
#include <Tactility/lvgl/LvglSync.h> #include <Tactility/settings/TouchCalibrationSettings.h>
#include <driver/gpio.h> #include <algorithm>
#include <esp_err.h>
#include <esp_lvgl_port.h> #include <driver/gpio.h>
#include <freertos/FreeRTOS.h> #include <esp_err.h>
#include <freertos/task.h> #include <freertos/FreeRTOS.h>
#include <inttypes.h> #include <freertos/task.h>
#include <nvs.h> #include <rom/ets_sys.h>
#include <nvs_flash.h>
#include <rom/ets_sys.h> static const auto LOGGER = tt::Logger("Xpt2046SoftSpi");
static const auto LOGGER = tt::Logger("Xpt2046SoftSpi"); constexpr auto CMD_READ_Y = 0x90;
constexpr auto CMD_READ_X = 0xD0;
constexpr auto RERUN_CALIBRATE = false;
constexpr auto CMD_READ_Y = 0x90; // Try different commands if these don't work constexpr int RAW_MIN_DEFAULT = 100;
constexpr auto CMD_READ_X = 0xD0; // Alternative: 0x98 for Y, 0xD8 for X constexpr int RAW_MAX_DEFAULT = 1900;
constexpr int RAW_VALID_MIN = 100;
struct Calibration { constexpr int RAW_VALID_MAX = 3900;
int xMin;
int xMax; Xpt2046SoftSpi::Xpt2046SoftSpi(std::unique_ptr<Configuration> inConfiguration)
int yMin; : configuration(std::move(inConfiguration)) {
int yMax; assert(configuration != nullptr);
}; }
Calibration cal = { bool Xpt2046SoftSpi::start() {
.xMin = 100, LOGGER.info("Starting Xpt2046SoftSpi touch driver");
.xMax = 1900,
.yMin = 100, // Configure GPIO pins
.yMax = 1900 gpio_config_t io_conf = {};
};
// Configure MOSI, CLK, CS as outputs
Xpt2046SoftSpi::Xpt2046SoftSpi(std::unique_ptr<Configuration> inConfiguration) io_conf.intr_type = GPIO_INTR_DISABLE;
: configuration(std::move(inConfiguration)) { io_conf.mode = GPIO_MODE_OUTPUT;
assert(configuration != nullptr); io_conf.pin_bit_mask = (1ULL << configuration->mosiPin) |
} (1ULL << configuration->clkPin) |
(1ULL << configuration->csPin);
// Defensive check for NVS, put here just in case NVS is init after touch setup. io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
static void ensureNvsInitialized() { io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
static bool initialized = false;
if (initialized) return; if (gpio_config(&io_conf) != ESP_OK) {
LOGGER.error("Failed to configure output pins");
esp_err_t result = nvs_flash_init(); return false;
if (result == ESP_ERR_NVS_NO_FREE_PAGES || result == ESP_ERR_NVS_NEW_VERSION_FOUND) { }
nvs_flash_erase(); // ignore error for safety
result = nvs_flash_init(); // Configure MISO as input
} io_conf.mode = GPIO_MODE_INPUT;
io_conf.pin_bit_mask = (1ULL << configuration->misoPin);
initialized = (result == ESP_OK); io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
}
if (gpio_config(&io_conf) != ESP_OK) {
bool Xpt2046SoftSpi::start() { LOGGER.error("Failed to configure input pin");
ensureNvsInitialized(); return false;
}
LOGGER.info("Starting Xpt2046SoftSpi touch driver");
// Initialize pin states
// Configure GPIO pins gpio_set_level(configuration->csPin, 1); // CS high
gpio_config_t io_conf = {}; gpio_set_level(configuration->clkPin, 0); // CLK low
gpio_set_level(configuration->mosiPin, 0); // MOSI low
// Configure MOSI, CLK, CS as outputs
io_conf.intr_type = GPIO_INTR_DISABLE; LOGGER.info(
io_conf.mode = GPIO_MODE_OUTPUT; "GPIO configured: MOSI={}, MISO={}, CLK={}, CS={}",
io_conf.pin_bit_mask = (1ULL << configuration->mosiPin) | static_cast<int>(configuration->mosiPin),
(1ULL << configuration->clkPin) | static_cast<int>(configuration->misoPin),
(1ULL << configuration->csPin); static_cast<int>(configuration->clkPin),
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; static_cast<int>(configuration->csPin)
io_conf.pull_up_en = GPIO_PULLUP_DISABLE; );
if (gpio_config(&io_conf) != ESP_OK) { return true;
LOGGER.error("Failed to configure output pins"); }
return false;
} bool Xpt2046SoftSpi::stop() {
LOGGER.info("Stopping Xpt2046SoftSpi touch driver");
// Configure MISO as input
io_conf.mode = GPIO_MODE_INPUT; // Stop LVLG if needed
io_conf.pin_bit_mask = (1ULL << configuration->misoPin); if (lvglDevice != nullptr) {
io_conf.pull_up_en = GPIO_PULLUP_ENABLE; stopLvgl();
}
if (gpio_config(&io_conf) != ESP_OK) {
LOGGER.error("Failed to configure input pin"); return true;
return false; }
}
bool Xpt2046SoftSpi::startLvgl(lv_display_t* display) {
// Initialize pin states (void)display;
gpio_set_level(configuration->csPin, 1); // CS high if (lvglDevice != nullptr) {
gpio_set_level(configuration->clkPin, 0); // CLK low LOGGER.error("LVGL was already started");
gpio_set_level(configuration->mosiPin, 0); // MOSI low return false;
}
LOGGER.info("GPIO configured: MOSI={}, MISO={}, CLK={}, CS={}",
static_cast<int>(configuration->mosiPin), lvglDevice = lv_indev_create();
static_cast<int>(configuration->misoPin), if (lvglDevice == nullptr) {
static_cast<int>(configuration->clkPin), LOGGER.error("Failed to create LVGL input device");
static_cast<int>(configuration->csPin) return false;
); }
// Load or perform calibration lv_indev_set_type(lvglDevice, LV_INDEV_TYPE_POINTER);
bool calibrationValid = true; //loadCalibration() && !RERUN_CALIBRATE; lv_indev_set_read_cb(lvglDevice, touchReadCallback);
if (calibrationValid) { lv_indev_set_user_data(lvglDevice, this);
// Check if calibration values are valid (xMin != xMax, yMin != yMax)
if (cal.xMin == cal.xMax || cal.yMin == cal.yMax) { LOGGER.info("Xpt2046SoftSpi touch driver started successfully");
LOGGER.warn("Invalid calibration detected: xMin={}, xMax={}, yMin={}, yMax={}", cal.xMin, cal.xMax, cal.yMin, cal.yMax); return true;
calibrationValid = false; }
}
} bool Xpt2046SoftSpi::stopLvgl() {
if (lvglDevice != nullptr) {
if (!calibrationValid) { lv_indev_delete(lvglDevice);
LOGGER.warn("Calibration data not found, invalid, or forced recalibration"); lvglDevice = nullptr;
calibrate(); }
saveCalibration(); return true;
} else { }
LOGGER.info("Loaded calibration: xMin={}, yMin={}, xMax={}, yMax={}", cal.xMin, cal.yMin, cal.xMax, cal.yMax);
} int Xpt2046SoftSpi::readSPI(uint8_t command) {
int result = 0;
return true;
} // Pull CS low for this transaction
gpio_set_level(configuration->csPin, 0);
bool Xpt2046SoftSpi::stop() { ets_delay_us(1);
LOGGER.info("Stopping Xpt2046SoftSpi touch driver");
// Send 8-bit command
// Stop LVLG if needed for (int i = 7; i >= 0; i--) {
if (lvglDevice != nullptr) { gpio_set_level(configuration->mosiPin, (command & (1 << i)) ? 1 : 0);
stopLvgl(); gpio_set_level(configuration->clkPin, 1);
} ets_delay_us(1);
gpio_set_level(configuration->clkPin, 0);
return true; ets_delay_us(1);
} }
bool Xpt2046SoftSpi::startLvgl(lv_display_t* display) { for (int i = 11; i >= 0; i--) {
if (lvglDevice != nullptr) { gpio_set_level(configuration->clkPin, 1);
LOGGER.error("LVGL was already started"); ets_delay_us(1);
return false; if (gpio_get_level(configuration->misoPin)) {
} result |= (1 << i);
}
lvglDevice = lv_indev_create(); gpio_set_level(configuration->clkPin, 0);
if (!lvglDevice) { ets_delay_us(1);
LOGGER.error("Failed to create LVGL input device"); }
return false;
} // Pull CS high for this transaction
gpio_set_level(configuration->csPin, 1);
lv_indev_set_type(lvglDevice, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(lvglDevice, touchReadCallback); return result;
lv_indev_set_user_data(lvglDevice, this); }
LOGGER.info("Xpt2046SoftSpi touch driver started successfully"); bool Xpt2046SoftSpi::readRawPoint(uint16_t& x, uint16_t& y) {
return true; constexpr int sampleCount = 8;
} int totalX = 0;
int totalY = 0;
bool Xpt2046SoftSpi::stopLvgl() { int validSamples = 0;
if (lvglDevice != nullptr) {
lv_indev_delete(lvglDevice); for (int i = 0; i < sampleCount; i++) {
lvglDevice = nullptr; const int rawX = readSPI(CMD_READ_X);
} const int rawY = readSPI(CMD_READ_Y);
return true;
} if (rawX > RAW_VALID_MIN && rawX < RAW_VALID_MAX && rawY > RAW_VALID_MIN && rawY < RAW_VALID_MAX) {
totalX += rawX;
int Xpt2046SoftSpi::readSPI(uint8_t command) { totalY += rawY;
int result = 0; validSamples++;
}
// Pull CS low for this transaction
gpio_set_level(configuration->csPin, 0); vTaskDelay(pdMS_TO_TICKS(1));
ets_delay_us(1); }
// Send 8-bit command if (validSamples < 3) {
for (int i = 7; i >= 0; i--) { return false;
gpio_set_level(configuration->mosiPin, command & (1 << i)); }
gpio_set_level(configuration->clkPin, 1);
ets_delay_us(1); x = static_cast<uint16_t>(totalX / validSamples);
gpio_set_level(configuration->clkPin, 0); y = static_cast<uint16_t>(totalY / validSamples);
ets_delay_us(1); return true;
} }
for (int i = 11; i >= 0; i--) { bool Xpt2046SoftSpi::getTouchPoint(Point& point) {
gpio_set_level(configuration->clkPin, 1); uint16_t rawX = 0;
ets_delay_us(1); uint16_t rawY = 0;
if (gpio_get_level(configuration->misoPin)) { if (!readRawPoint(rawX, rawY)) {
result |= (1 << i); return false;
} }
gpio_set_level(configuration->clkPin, 0);
ets_delay_us(1); int mappedX = (static_cast<int>(rawX) - RAW_MIN_DEFAULT) * static_cast<int>(configuration->xMax) /
} (RAW_MAX_DEFAULT - RAW_MIN_DEFAULT);
int mappedY = (static_cast<int>(rawY) - RAW_MIN_DEFAULT) * static_cast<int>(configuration->yMax) /
// Pull CS high for this transaction (RAW_MAX_DEFAULT - RAW_MIN_DEFAULT);
gpio_set_level(configuration->csPin, 1);
if (configuration->swapXy) {
return result; std::swap(mappedX, mappedY);
} }
if (configuration->mirrorX) {
void Xpt2046SoftSpi::calibrate() { mappedX = static_cast<int>(configuration->xMax) - mappedX;
const int samples = 8; // More samples for better accuracy }
if (configuration->mirrorY) {
LOGGER.info("Calibration starting..."); mappedY = static_cast<int>(configuration->yMax) - mappedY;
}
LOGGER.info("Touch TOP-LEFT corner");
uint16_t x = static_cast<uint16_t>(std::clamp(mappedX, 0, static_cast<int>(configuration->xMax)));
while (!isTouched()) { uint16_t y = static_cast<uint16_t>(std::clamp(mappedY, 0, static_cast<int>(configuration->yMax)));
vTaskDelay(pdMS_TO_TICKS(50));
} const auto calibration = tt::settings::touch::getActive();
tt::settings::touch::applyCalibration(calibration, configuration->xMax, configuration->yMax, x, y);
int sumX = 0, sumY = 0;
for (int i = 0; i < samples; i++) { point.x = x;
sumX += readSPI(CMD_READ_X); point.y = y;
sumY += readSPI(CMD_READ_Y); return true;
vTaskDelay(pdMS_TO_TICKS(10)); }
}
cal.xMin = sumX / samples; bool Xpt2046SoftSpi::isTouched() {
cal.yMin = sumY / samples; uint16_t x = 0;
uint16_t y = 0;
LOGGER.info("Top-left calibrated: xMin={}, yMin={}", cal.xMin, cal.yMin); return readRawPoint(x, y);
}
LOGGER.info("Touch BOTTOM-RIGHT corner");
void Xpt2046SoftSpi::touchReadCallback(lv_indev_t* indev, lv_indev_data_t* data) {
while (!isTouched()) { auto* touch = static_cast<Xpt2046SoftSpi*>(lv_indev_get_user_data(indev));
vTaskDelay(pdMS_TO_TICKS(50)); if (touch == nullptr) {
} data->state = LV_INDEV_STATE_RELEASED;
return;
sumX = sumY = 0; }
for (int i = 0; i < samples; i++) {
sumX += readSPI(CMD_READ_X); Point point;
sumY += readSPI(CMD_READ_Y); if (touch->getTouchPoint(point)) {
vTaskDelay(pdMS_TO_TICKS(10)); data->point.x = point.x;
} data->point.y = point.y;
cal.xMax = sumX / samples; data->state = LV_INDEV_STATE_PRESSED;
cal.yMax = sumY / samples; } else {
data->state = LV_INDEV_STATE_RELEASED;
LOGGER.info("Bottom-right calibrated: xMax={}, yMax={}", cal.xMax, cal.yMax); }
}
LOGGER.info("Calibration completed! xMin={}, yMin={}, xMax={}, yMax={}", cal.xMin, cal.yMin, cal.xMax, cal.yMax);
} // Return driver instance if any
std::shared_ptr<tt::hal::touch::TouchDriver> Xpt2046SoftSpi::getTouchDriver() {
bool Xpt2046SoftSpi::loadCalibration() { assert(lvglDevice == nullptr); // Still attached to LVGL context. Call stopLvgl() first.
LOGGER.warn("Calibration load disabled (using fresh calibration only).");
return false; if (touchDriver == nullptr) {
} touchDriver = std::make_shared<Xpt2046SoftSpiDriver>(this);
}
void Xpt2046SoftSpi::saveCalibration() {
nvs_handle_t handle; return touchDriver;
esp_err_t err = nvs_open("xpt2046", NVS_READWRITE, &handle); }
if (err != ESP_OK) {
LOGGER.error("Failed to open NVS for writing ({})", esp_err_to_name(err));
return;
}
err = nvs_set_blob(handle, "cal", &cal, sizeof(cal));
if (err == ESP_OK) {
nvs_commit(handle);
LOGGER.info("Calibration saved to NVS");
} else {
LOGGER.error("Failed to write calibration data to NVS ({})", esp_err_to_name(err));
}
nvs_close(handle);
}
void Xpt2046SoftSpi::setCalibration(int xMin, int yMin, int xMax, int yMax) {
cal.xMin = xMin;
cal.yMin = yMin;
cal.xMax = xMax;
cal.yMax = yMax;
LOGGER.info("Manual calibration set: xMin={}, yMin={}, xMax={}, yMax={}", xMin, yMin, xMax, yMax);
}
bool Xpt2046SoftSpi::getTouchPoint(Point& point) {
const int samples = 8; // More samples for better accuracy
int totalX = 0, totalY = 0;
int validSamples = 0;
gpio_set_level(configuration->csPin, 0);
for (int i = 0; i < samples; i++) {
int rawX = readSPI(CMD_READ_X);
int rawY = readSPI(CMD_READ_Y);
// Only use valid readings
if (rawX > 100 && rawX < 3900 && rawY > 100 && rawY < 3900) {
totalX += rawX;
totalY += rawY;
validSamples++;
}
vTaskDelay(pdMS_TO_TICKS(1));
}
gpio_set_level(configuration->csPin, 1);
if (validSamples == 0) {
return false;
}
int rawX = totalX / validSamples;
int rawY = totalY / validSamples;
const int xRange = cal.xMax - cal.xMin;
const int yRange = cal.yMax - cal.yMin;
if (xRange <= 0 || yRange <= 0) {
LOGGER.warn("Invalid calibration: xRange={}, yRange={}", xRange, yRange);
return false;
}
int x = (rawX - cal.xMin) * configuration->xMax / xRange;
int y = (rawY - cal.yMin) * configuration->yMax / yRange;
if (configuration->swapXy) std::swap(x, y);
if (configuration->mirrorX) x = configuration->xMax - x;
if (configuration->mirrorY) y = configuration->yMax - y;
point.x = std::clamp(x, 0, (int)configuration->xMax);
point.y = std::clamp(y, 0, (int)configuration->yMax);
return true;
}
// TODO: Merge isTouched() and getTouchPoint() into 1 method
bool Xpt2046SoftSpi::isTouched() {
const int samples = 3;
int xTotal = 0, yTotal = 0;
int validSamples = 0;
gpio_set_level(configuration->csPin, 0);
for (int i = 0; i < samples; i++) {
int x = readSPI(CMD_READ_X);
int y = readSPI(CMD_READ_Y);
// Basic validity check - XPT2046 typically returns values in range 100-3900 when touched
if (x > 100 && x < 3900 && y > 100 && y < 3900) {
xTotal += x;
yTotal += y;
validSamples++;
}
vTaskDelay(pdMS_TO_TICKS(1)); // Small delay between samples
}
gpio_set_level(configuration->csPin, 1);
// Consider touched if we got valid readings
bool touched = validSamples >= 2;
// Debug logging (remove this once working)
if (touched) {
LOGGER.debug("Touch detected: validSamples={}, avgX={}, avgY={}", validSamples, xTotal / validSamples, yTotal / validSamples);
}
return touched;
}
void Xpt2046SoftSpi::touchReadCallback(lv_indev_t* indev, lv_indev_data_t* data) {
Xpt2046SoftSpi* touch = static_cast<Xpt2046SoftSpi*>(lv_indev_get_user_data(indev));
Point point;
if (touch && touch->isTouched() && touch->getTouchPoint(point)) {
data->point.x = point.x;
data->point.y = point.y;
data->state = LV_INDEV_STATE_PRESSED;
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
}
// Return driver instance if any
std::shared_ptr<tt::hal::touch::TouchDriver> Xpt2046SoftSpi::getTouchDriver() {
assert(lvglDevice == nullptr); // Still attached to LVGL context. Call stopLvgl() first.
if (touchDriver == nullptr) {
touchDriver = std::make_shared<Xpt2046SoftSpiDriver>(this);
}
return touchDriver;
}

View File

@ -81,8 +81,7 @@ private:
std::shared_ptr<tt::hal::touch::TouchDriver> touchDriver; std::shared_ptr<tt::hal::touch::TouchDriver> touchDriver;
int readSPI(uint8_t command); int readSPI(uint8_t command);
bool loadCalibration(); bool readRawPoint(uint16_t& x, uint16_t& y);
void saveCalibration();
static void touchReadCallback(lv_indev_t* indev, lv_indev_data_t* data); static void touchReadCallback(lv_indev_t* indev, lv_indev_data_t* data);
public: public:
@ -100,12 +99,11 @@ public:
bool stopLvgl() override; bool stopLvgl() override;
bool supportsTouchDriver() override { return true; } bool supportsTouchDriver() override { return true; }
bool supportsCalibration() const override { return true; }
std::shared_ptr<tt::hal::touch::TouchDriver> getTouchDriver() override; std::shared_ptr<tt::hal::touch::TouchDriver> getTouchDriver() override;
lv_indev_t* getLvglIndev() override { return lvglDevice; } lv_indev_t* getLvglIndev() override { return lvglDevice; }
// XPT2046-specific methods // XPT2046-specific methods
bool getTouchPoint(Point& point); bool getTouchPoint(Point& point);
void calibrate();
void setCalibration(int xMin, int yMin, int xMax, int yMax);
bool isTouched(); bool isTouched();
}; };

View File

@ -0,0 +1,9 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::touchcalibration {
LaunchId start();
} // namespace tt::app::touchcalibration

View File

@ -27,6 +27,8 @@ public:
virtual bool supportsTouchDriver() = 0; virtual bool supportsTouchDriver() = 0;
virtual bool supportsCalibration() const { return false; }
/** Could return nullptr if not supported */ /** Could return nullptr if not supported */
virtual std::shared_ptr<TouchDriver> getTouchDriver() = 0; virtual std::shared_ptr<TouchDriver> getTouchDriver() = 0;
}; };

View File

@ -0,0 +1,33 @@
#pragma once
#include <cstdint>
namespace tt::settings::touch {
struct TouchCalibrationSettings {
bool enabled = false;
int32_t xMin = 0;
int32_t xMax = 0;
int32_t yMin = 0;
int32_t yMax = 0;
};
TouchCalibrationSettings getDefault();
bool load(TouchCalibrationSettings& settings);
TouchCalibrationSettings loadOrGetDefault();
bool save(const TouchCalibrationSettings& settings);
bool isValid(const TouchCalibrationSettings& settings);
TouchCalibrationSettings getActive();
void setRuntimeCalibrationEnabled(bool enabled);
void invalidateCache();
bool applyCalibration(const TouchCalibrationSettings& settings, uint16_t xMax, uint16_t yMax, uint16_t& x, uint16_t& y);
} // namespace tt::settings::touch

View File

@ -103,6 +103,7 @@ namespace app {
namespace settings { extern const AppManifest manifest; } namespace settings { extern const AppManifest manifest; }
namespace systeminfo { extern const AppManifest manifest; } namespace systeminfo { extern const AppManifest manifest; }
namespace timedatesettings { extern const AppManifest manifest; } namespace timedatesettings { extern const AppManifest manifest; }
namespace touchcalibration { extern const AppManifest manifest; }
namespace timezone { extern const AppManifest manifest; } namespace timezone { extern const AppManifest manifest; }
namespace usbsettings { extern const AppManifest manifest; } namespace usbsettings { extern const AppManifest manifest; }
namespace wifiapsettings { extern const AppManifest manifest; } namespace wifiapsettings { extern const AppManifest manifest; }
@ -153,6 +154,7 @@ static void registerInternalApps() {
addAppManifest(app::selectiondialog::manifest); addAppManifest(app::selectiondialog::manifest);
addAppManifest(app::systeminfo::manifest); addAppManifest(app::systeminfo::manifest);
addAppManifest(app::timedatesettings::manifest); addAppManifest(app::timedatesettings::manifest);
addAppManifest(app::touchcalibration::manifest);
addAppManifest(app::timezone::manifest); addAppManifest(app::timezone::manifest);
addAppManifest(app::wifiapsettings::manifest); addAppManifest(app::wifiapsettings::manifest);
addAppManifest(app::wificonnect::manifest); addAppManifest(app::wificonnect::manifest);

View File

@ -7,7 +7,9 @@
#endif #endif
#include <Tactility/Logger.h> #include <Tactility/Logger.h>
#include <Tactility/app/App.h>
#include <Tactility/hal/display/DisplayDevice.h> #include <Tactility/hal/display/DisplayDevice.h>
#include <Tactility/hal/touch/TouchDevice.h>
#include <Tactility/lvgl/Toolbar.h> #include <Tactility/lvgl/Toolbar.h>
#include <Tactility/settings/DisplaySettings.h> #include <Tactility/settings/DisplaySettings.h>
@ -22,6 +24,16 @@ static std::shared_ptr<hal::display::DisplayDevice> getHalDisplay() {
return hal::findFirstDevice<hal::display::DisplayDevice>(hal::Device::Type::Display); return hal::findFirstDevice<hal::display::DisplayDevice>(hal::Device::Type::Display);
} }
static bool hasCalibratableTouchDevice() {
auto touch_devices = hal::findDevices<hal::touch::TouchDevice>(hal::Device::Type::Touch);
for (const auto& touch_device : touch_devices) {
if (touch_device != nullptr && touch_device->supportsCalibration()) {
return true;
}
}
return false;
}
class DisplayApp final : public App { class DisplayApp final : public App {
settings::display::DisplaySettings displaySettings; settings::display::DisplaySettings displaySettings;
@ -119,6 +131,10 @@ class DisplayApp final : public App {
} }
} }
static void onCalibrateTouchClicked(lv_event_t*) {
app::start("TouchCalibration");
}
public: public:
void onShow(AppContext& app, lv_obj_t* parent) override { void onShow(AppContext& app, lv_obj_t* parent) override {
@ -278,6 +294,25 @@ public:
lv_obj_add_state(screensaverDropdown, LV_STATE_DISABLED); lv_obj_add_state(screensaverDropdown, LV_STATE_DISABLED);
} }
} }
if (hasCalibratableTouchDevice()) {
auto* calibrate_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(calibrate_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(calibrate_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(calibrate_wrapper, 0, LV_STATE_DEFAULT);
auto* calibrate_label = lv_label_create(calibrate_wrapper);
lv_label_set_text(calibrate_label, "Touch calibration");
lv_obj_align(calibrate_label, LV_ALIGN_LEFT_MID, 0, 0);
auto* calibrate_button = lv_button_create(calibrate_wrapper);
lv_obj_align(calibrate_button, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_event_cb(calibrate_button, onCalibrateTouchClicked, LV_EVENT_SHORT_CLICKED, this);
auto* calibrate_button_label = lv_label_create(calibrate_button);
lv_label_set_text(calibrate_button_label, "Calibrate");
lv_obj_center(calibrate_button_label);
}
} }
void onHide(AppContext& app) override { void onHide(AppContext& app) override {

View File

@ -0,0 +1,203 @@
#include <Tactility/Tactility.h>
#include <Tactility/app/touchcalibration/TouchCalibration.h>
#include <Tactility/Logger.h>
#include <Tactility/settings/TouchCalibrationSettings.h>
#include <algorithm>
#include <lvgl.h>
namespace tt::app::touchcalibration {
static const auto LOGGER = Logger("TouchCalibration");
extern const AppManifest manifest;
LaunchId start() {
return app::start(manifest.appId);
}
class TouchCalibrationApp final : public App {
static constexpr int32_t TARGET_MARGIN = 24;
struct Sample {
uint16_t x;
uint16_t y;
};
Sample samples[4] = {};
uint8_t sampleCount = 0;
lv_obj_t* root = nullptr;
lv_obj_t* target = nullptr;
lv_obj_t* titleLabel = nullptr;
lv_obj_t* hintLabel = nullptr;
static void onPress(lv_event_t* event) {
auto* self = static_cast<TouchCalibrationApp*>(lv_event_get_user_data(event));
if (self != nullptr) {
self->onPressInternal(event);
}
}
static lv_point_t getTargetPoint(uint8_t index, lv_coord_t width, lv_coord_t height) {
switch (index) {
case 0:
return {.x = TARGET_MARGIN, .y = TARGET_MARGIN};
case 1:
return {.x = width - TARGET_MARGIN, .y = TARGET_MARGIN};
case 2:
return {.x = width - TARGET_MARGIN, .y = height - TARGET_MARGIN};
default:
return {.x = TARGET_MARGIN, .y = height - TARGET_MARGIN};
}
}
void updateUi() {
if (target == nullptr || root == nullptr || titleLabel == nullptr || hintLabel == nullptr) {
return;
}
const auto width = lv_obj_get_content_width(root);
const auto height = lv_obj_get_content_height(root);
if (sampleCount < 4) {
const auto point = getTargetPoint(sampleCount, width, height);
lv_obj_set_pos(target, point.x - 14, point.y - 14);
lv_label_set_text(titleLabel, "Touchscreen Calibration");
lv_label_set_text_fmt(hintLabel, "Tap target %u/4", static_cast<unsigned>(sampleCount + 1));
}
}
void finishCalibration() {
constexpr int32_t MIN_RANGE = 20;
const int32_t xMin = (static_cast<int32_t>(samples[0].x) + static_cast<int32_t>(samples[3].x)) / 2;
const int32_t xMax = (static_cast<int32_t>(samples[1].x) + static_cast<int32_t>(samples[2].x)) / 2;
const int32_t yMin = (static_cast<int32_t>(samples[0].y) + static_cast<int32_t>(samples[1].y)) / 2;
const int32_t yMax = (static_cast<int32_t>(samples[2].y) + static_cast<int32_t>(samples[3].y)) / 2;
settings::touch::TouchCalibrationSettings settings = settings::touch::getDefault();
settings.enabled = true;
settings.xMin = xMin;
settings.xMax = xMax;
settings.yMin = yMin;
settings.yMax = yMax;
if ((xMax - xMin) < MIN_RANGE || (yMax - yMin) < MIN_RANGE || !settings::touch::isValid(settings)) {
lv_label_set_text(titleLabel, "Calibration Failed");
lv_label_set_text(hintLabel, "Range invalid. Tap to close.");
lv_obj_add_flag(target, LV_OBJ_FLAG_HIDDEN);
setResult(Result::Error);
return;
}
if (!settings::touch::save(settings)) {
lv_label_set_text(titleLabel, "Calibration Failed");
lv_label_set_text(hintLabel, "Unable to save settings. Tap to close.");
lv_obj_add_flag(target, LV_OBJ_FLAG_HIDDEN);
setResult(Result::Error);
return;
}
LOGGER.info("Saved calibration x=[{}, {}] y=[{}, {}]", xMin, xMax, yMin, yMax);
lv_label_set_text(titleLabel, "Calibration Complete");
lv_label_set_text(hintLabel, "Touch anywhere to continue.");
lv_obj_add_flag(target, LV_OBJ_FLAG_HIDDEN);
setResult(Result::Ok);
}
void onPressInternal(lv_event_t* event) {
auto* indev = lv_event_get_indev(event);
if (indev == nullptr) {
return;
}
lv_point_t point = {0, 0};
lv_indev_get_point(indev, &point);
if (sampleCount < 4) {
samples[sampleCount] = {
.x = static_cast<uint16_t>(std::max(static_cast<lv_coord_t>(0), point.x)),
.y = static_cast<uint16_t>(std::max(static_cast<lv_coord_t>(0), point.y)),
};
sampleCount++;
if (sampleCount < 4) {
updateUi();
} else {
finishCalibration();
}
return;
}
stop(manifest.appId);
}
public:
void onCreate(AppContext& app) override {
(void)app;
settings::touch::setRuntimeCalibrationEnabled(false);
settings::touch::invalidateCache();
}
void onDestroy(AppContext& app) override {
(void)app;
settings::touch::setRuntimeCalibrationEnabled(true);
settings::touch::invalidateCache();
}
void onShow(AppContext& app, lv_obj_t* parent) override {
(void)app;
lv_obj_set_style_bg_color(parent, lv_color_black(), LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(parent, LV_OPA_COVER, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(parent, 0, LV_STATE_DEFAULT);
lv_obj_set_style_radius(parent, 0, LV_STATE_DEFAULT);
root = lv_obj_create(parent);
lv_obj_set_size(root, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_bg_opa(root, LV_OPA_TRANSP, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(root, 0, LV_STATE_DEFAULT);
lv_obj_set_style_pad_all(root, 0, LV_STATE_DEFAULT);
titleLabel = lv_label_create(root);
lv_obj_align(titleLabel, LV_ALIGN_TOP_MID, 0, 14);
lv_obj_set_style_text_color(titleLabel, lv_color_white(), LV_STATE_DEFAULT);
lv_label_set_text(titleLabel, "Touchscreen Calibration");
hintLabel = lv_label_create(root);
lv_obj_align(hintLabel, LV_ALIGN_BOTTOM_MID, 0, -14);
lv_obj_set_style_text_color(hintLabel, lv_color_white(), LV_STATE_DEFAULT);
lv_label_set_text(hintLabel, "Tap target 1/4");
target = lv_button_create(root);
lv_obj_set_size(target, 28, 28);
lv_obj_set_style_radius(target, LV_RADIUS_CIRCLE, LV_STATE_DEFAULT);
lv_obj_set_style_bg_color(target, lv_palette_main(LV_PALETTE_RED), LV_STATE_DEFAULT);
// Ensure root receives all presses for sampling.
lv_obj_remove_flag(target, LV_OBJ_FLAG_CLICKABLE);
auto* targetLabel = lv_label_create(target);
lv_label_set_text(targetLabel, "+");
lv_obj_center(targetLabel);
lv_obj_add_flag(root, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(root, onPress, LV_EVENT_PRESSED, this);
updateUi();
}
};
extern const AppManifest manifest = {
.appId = "TouchCalibration",
.appName = "Touch Calibration",
.appCategory = Category::System,
.appFlags = AppManifest::Flags::HideStatusBar | AppManifest::Flags::Hidden,
.createApp = create<TouchCalibrationApp>
};
} // namespace tt::app::touchcalibration

View File

@ -0,0 +1,173 @@
#include <Tactility/settings/TouchCalibrationSettings.h>
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/Mutex.h>
#include <algorithm>
#include <cstdlib>
#include <cerrno>
#include <climits>
#include <map>
#include <string>
namespace tt::settings::touch {
constexpr auto* SETTINGS_FILE = "/data/settings/touch-calibration.properties";
constexpr auto* SETTINGS_KEY_ENABLED = "enabled";
constexpr auto* SETTINGS_KEY_X_MIN = "xMin";
constexpr auto* SETTINGS_KEY_X_MAX = "xMax";
constexpr auto* SETTINGS_KEY_Y_MIN = "yMin";
constexpr auto* SETTINGS_KEY_Y_MAX = "yMax";
static bool runtimeCalibrationEnabled = true;
static bool cacheInitialized = false;
static TouchCalibrationSettings cachedSettings;
static tt::Mutex cacheMutex;
static bool toBool(const std::string& value) {
return value == "1" || value == "true" || value == "True";
}
static bool parseInt32(const std::string& value, int32_t& out) {
errno = 0;
char* end_ptr = nullptr;
const long parsed = std::strtol(value.c_str(), &end_ptr, 10);
if (errno != 0 || end_ptr == value.c_str() || *end_ptr != '\0') {
return false;
}
if (parsed < INT32_MIN || parsed > INT32_MAX) {
return false;
}
out = static_cast<int32_t>(parsed);
return true;
}
TouchCalibrationSettings getDefault() {
return {
.enabled = false,
.xMin = 0,
.xMax = 0,
.yMin = 0,
.yMax = 0,
};
}
bool isValid(const TouchCalibrationSettings& settings) {
constexpr auto MIN_RANGE = 20;
return settings.xMax > settings.xMin && settings.yMax > settings.yMin &&
(settings.xMax - settings.xMin) >= MIN_RANGE &&
(settings.yMax - settings.yMin) >= MIN_RANGE;
}
bool load(TouchCalibrationSettings& settings) {
std::map<std::string, std::string> map;
if (!file::loadPropertiesFile(SETTINGS_FILE, map)) {
return false;
}
auto enabled_it = map.find(SETTINGS_KEY_ENABLED);
auto x_min_it = map.find(SETTINGS_KEY_X_MIN);
auto x_max_it = map.find(SETTINGS_KEY_X_MAX);
auto y_min_it = map.find(SETTINGS_KEY_Y_MIN);
auto y_max_it = map.find(SETTINGS_KEY_Y_MAX);
if (enabled_it == map.end() || x_min_it == map.end() || x_max_it == map.end() || y_min_it == map.end() || y_max_it == map.end()) {
return false;
}
TouchCalibrationSettings loaded = getDefault();
loaded.enabled = toBool(enabled_it->second);
if (!parseInt32(x_min_it->second, loaded.xMin) ||
!parseInt32(x_max_it->second, loaded.xMax) ||
!parseInt32(y_min_it->second, loaded.yMin) ||
!parseInt32(y_max_it->second, loaded.yMax)) {
return false;
}
if (loaded.enabled && !isValid(loaded)) {
return false;
}
settings = loaded;
return true;
}
TouchCalibrationSettings loadOrGetDefault() {
TouchCalibrationSettings settings;
if (!load(settings)) {
settings = getDefault();
}
return settings;
}
bool save(const TouchCalibrationSettings& settings) {
if (settings.enabled && !isValid(settings)) {
return false;
}
std::map<std::string, std::string> map;
map[SETTINGS_KEY_ENABLED] = settings.enabled ? "1" : "0";
map[SETTINGS_KEY_X_MIN] = std::to_string(settings.xMin);
map[SETTINGS_KEY_X_MAX] = std::to_string(settings.xMax);
map[SETTINGS_KEY_Y_MIN] = std::to_string(settings.yMin);
map[SETTINGS_KEY_Y_MAX] = std::to_string(settings.yMax);
if (!file::savePropertiesFile(SETTINGS_FILE, map)) {
return false;
}
auto lock = cacheMutex.asScopedLock();
lock.lock();
cachedSettings = settings;
cacheInitialized = true;
return true;
}
TouchCalibrationSettings getActive() {
auto lock = cacheMutex.asScopedLock();
lock.lock();
if (!cacheInitialized) {
cachedSettings = loadOrGetDefault();
cacheInitialized = true;
}
if (!runtimeCalibrationEnabled) {
auto disabled = cachedSettings;
disabled.enabled = false;
return disabled;
}
return cachedSettings;
}
void setRuntimeCalibrationEnabled(bool enabled) {
auto lock = cacheMutex.asScopedLock();
lock.lock();
runtimeCalibrationEnabled = enabled;
}
void invalidateCache() {
auto lock = cacheMutex.asScopedLock();
lock.lock();
cacheInitialized = false;
}
bool applyCalibration(const TouchCalibrationSettings& settings, uint16_t xMax, uint16_t yMax, uint16_t& x, uint16_t& y) {
if (!settings.enabled || !isValid(settings)) {
return false;
}
const int32_t in_x = static_cast<int32_t>(x);
const int32_t in_y = static_cast<int32_t>(y);
const int64_t mapped_x = (static_cast<int64_t>(in_x) - static_cast<int64_t>(settings.xMin)) *
static_cast<int64_t>(xMax) /
(static_cast<int64_t>(settings.xMax) - static_cast<int64_t>(settings.xMin));
const int64_t mapped_y = (static_cast<int64_t>(in_y) - static_cast<int64_t>(settings.yMin)) *
static_cast<int64_t>(yMax) /
(static_cast<int64_t>(settings.yMax) - static_cast<int64_t>(settings.yMin));
x = static_cast<uint16_t>(std::clamp<int64_t>(mapped_x, 0, static_cast<int64_t>(xMax)));
y = static_cast<uint16_t>(std::clamp<int64_t>(mapped_y, 0, static_cast<int64_t>(yMax)));
return true;
}
} // namespace tt::settings::touch