diff --git a/Drivers/XPT2046/Source/Xpt2046Touch.cpp b/Drivers/XPT2046/Source/Xpt2046Touch.cpp index 1695777ab..325d7a05b 100644 --- a/Drivers/XPT2046/Source/Xpt2046Touch.cpp +++ b/Drivers/XPT2046/Source/Xpt2046Touch.cpp @@ -1,10 +1,30 @@ #include "Xpt2046Touch.h" +#include #include +#include #include #include +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(tp->config.user_data); + if (config == nullptr) { + return; + } + + const auto settings = tt::settings::touch::getActive(); + const auto points = std::min(*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) { 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; @@ -29,9 +49,9 @@ esp_lcd_touch_config_t Xpt2046Touch::createEspLcdTouchConfig() { .mirror_x = configuration->mirrorX, .mirror_y = configuration->mirrorY, }, - .process_coordinates = nullptr, + .process_coordinates = processCoordinates, .interrupt_callback = nullptr, - .user_data = nullptr, + .user_data = configuration.get(), .driver_data = nullptr }; } diff --git a/Drivers/XPT2046/Source/Xpt2046Touch.h b/Drivers/XPT2046/Source/Xpt2046Touch.h index 91404e4fb..eb65787f2 100644 --- a/Drivers/XPT2046/Source/Xpt2046Touch.h +++ b/Drivers/XPT2046/Source/Xpt2046Touch.h @@ -55,5 +55,7 @@ class Xpt2046Touch : public EspLcdTouch { 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; } }; diff --git a/Drivers/XPT2046SoftSPI/Source/Xpt2046SoftSpi.cpp b/Drivers/XPT2046SoftSPI/Source/Xpt2046SoftSpi.cpp index 7b8f771d1..6fb66aab3 100644 --- a/Drivers/XPT2046SoftSPI/Source/Xpt2046SoftSpi.cpp +++ b/Drivers/XPT2046SoftSPI/Source/Xpt2046SoftSpi.cpp @@ -1,373 +1,240 @@ -#include "Xpt2046SoftSpi.h" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -static const auto LOGGER = tt::Logger("Xpt2046SoftSpi"); - -constexpr auto RERUN_CALIBRATE = false; -constexpr auto CMD_READ_Y = 0x90; // Try different commands if these don't work -constexpr auto CMD_READ_X = 0xD0; // Alternative: 0x98 for Y, 0xD8 for X - -struct Calibration { - int xMin; - int xMax; - int yMin; - int yMax; -}; - -Calibration cal = { - .xMin = 100, - .xMax = 1900, - .yMin = 100, - .yMax = 1900 -}; - -Xpt2046SoftSpi::Xpt2046SoftSpi(std::unique_ptr inConfiguration) - : configuration(std::move(inConfiguration)) { - assert(configuration != nullptr); -} - -// Defensive check for NVS, put here just in case NVS is init after touch setup. -static void ensureNvsInitialized() { - static bool initialized = false; - if (initialized) return; - - esp_err_t result = nvs_flash_init(); - 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(); - } - - initialized = (result == ESP_OK); -} - -bool Xpt2046SoftSpi::start() { - ensureNvsInitialized(); - - LOGGER.info("Starting Xpt2046SoftSpi touch driver"); - - // Configure GPIO pins - gpio_config_t io_conf = {}; - - // Configure MOSI, CLK, CS as outputs - io_conf.intr_type = GPIO_INTR_DISABLE; - io_conf.mode = GPIO_MODE_OUTPUT; - io_conf.pin_bit_mask = (1ULL << configuration->mosiPin) | - (1ULL << configuration->clkPin) | - (1ULL << configuration->csPin); - io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; - io_conf.pull_up_en = GPIO_PULLUP_DISABLE; - - if (gpio_config(&io_conf) != ESP_OK) { - LOGGER.error("Failed to configure output pins"); - return false; - } - - // Configure MISO as input - io_conf.mode = GPIO_MODE_INPUT; - io_conf.pin_bit_mask = (1ULL << configuration->misoPin); - io_conf.pull_up_en = GPIO_PULLUP_ENABLE; - - if (gpio_config(&io_conf) != ESP_OK) { - LOGGER.error("Failed to configure input pin"); - return false; - } - - // Initialize pin states - gpio_set_level(configuration->csPin, 1); // CS high - gpio_set_level(configuration->clkPin, 0); // CLK low - gpio_set_level(configuration->mosiPin, 0); // MOSI low - - LOGGER.info("GPIO configured: MOSI={}, MISO={}, CLK={}, CS={}", - static_cast(configuration->mosiPin), - static_cast(configuration->misoPin), - static_cast(configuration->clkPin), - static_cast(configuration->csPin) - ); - - // Load or perform calibration - bool calibrationValid = true; //loadCalibration() && !RERUN_CALIBRATE; - if (calibrationValid) { - // Check if calibration values are valid (xMin != xMax, yMin != yMax) - if (cal.xMin == cal.xMax || cal.yMin == cal.yMax) { - LOGGER.warn("Invalid calibration detected: xMin={}, xMax={}, yMin={}, yMax={}", cal.xMin, cal.xMax, cal.yMin, cal.yMax); - calibrationValid = false; - } - } - - if (!calibrationValid) { - LOGGER.warn("Calibration data not found, invalid, or forced recalibration"); - calibrate(); - saveCalibration(); - } else { - LOGGER.info("Loaded calibration: xMin={}, yMin={}, xMax={}, yMax={}", cal.xMin, cal.yMin, cal.xMax, cal.yMax); - } - - return true; -} - -bool Xpt2046SoftSpi::stop() { - LOGGER.info("Stopping Xpt2046SoftSpi touch driver"); - - // Stop LVLG if needed - if (lvglDevice != nullptr) { - stopLvgl(); - } - - return true; -} - -bool Xpt2046SoftSpi::startLvgl(lv_display_t* display) { - if (lvglDevice != nullptr) { - LOGGER.error("LVGL was already started"); - return false; - } - - lvglDevice = lv_indev_create(); - if (!lvglDevice) { - LOGGER.error("Failed to create LVGL input device"); - return false; - } - - lv_indev_set_type(lvglDevice, LV_INDEV_TYPE_POINTER); - lv_indev_set_read_cb(lvglDevice, touchReadCallback); - lv_indev_set_user_data(lvglDevice, this); - - LOGGER.info("Xpt2046SoftSpi touch driver started successfully"); - return true; -} - -bool Xpt2046SoftSpi::stopLvgl() { - if (lvglDevice != nullptr) { - lv_indev_delete(lvglDevice); - lvglDevice = nullptr; - } - return true; -} - -int Xpt2046SoftSpi::readSPI(uint8_t command) { - int result = 0; - - // Pull CS low for this transaction - gpio_set_level(configuration->csPin, 0); - ets_delay_us(1); - - // Send 8-bit command - for (int i = 7; i >= 0; i--) { - gpio_set_level(configuration->mosiPin, command & (1 << i)); - gpio_set_level(configuration->clkPin, 1); - ets_delay_us(1); - gpio_set_level(configuration->clkPin, 0); - ets_delay_us(1); - } - - for (int i = 11; i >= 0; i--) { - gpio_set_level(configuration->clkPin, 1); - ets_delay_us(1); - if (gpio_get_level(configuration->misoPin)) { - result |= (1 << i); - } - gpio_set_level(configuration->clkPin, 0); - ets_delay_us(1); - } - - // Pull CS high for this transaction - gpio_set_level(configuration->csPin, 1); - - return result; -} - -void Xpt2046SoftSpi::calibrate() { - const int samples = 8; // More samples for better accuracy - - LOGGER.info("Calibration starting..."); - - LOGGER.info("Touch TOP-LEFT corner"); - - while (!isTouched()) { - vTaskDelay(pdMS_TO_TICKS(50)); - } - - int sumX = 0, sumY = 0; - for (int i = 0; i < samples; i++) { - sumX += readSPI(CMD_READ_X); - sumY += readSPI(CMD_READ_Y); - vTaskDelay(pdMS_TO_TICKS(10)); - } - cal.xMin = sumX / samples; - cal.yMin = sumY / samples; - - LOGGER.info("Top-left calibrated: xMin={}, yMin={}", cal.xMin, cal.yMin); - - LOGGER.info("Touch BOTTOM-RIGHT corner"); - - while (!isTouched()) { - vTaskDelay(pdMS_TO_TICKS(50)); - } - - sumX = sumY = 0; - for (int i = 0; i < samples; i++) { - sumX += readSPI(CMD_READ_X); - sumY += readSPI(CMD_READ_Y); - vTaskDelay(pdMS_TO_TICKS(10)); - } - cal.xMax = sumX / samples; - cal.yMax = sumY / samples; - - 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); -} - -bool Xpt2046SoftSpi::loadCalibration() { - LOGGER.warn("Calibration load disabled (using fresh calibration only)."); - return false; -} - -void Xpt2046SoftSpi::saveCalibration() { - nvs_handle_t handle; - 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(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 Xpt2046SoftSpi::getTouchDriver() { - assert(lvglDevice == nullptr); // Still attached to LVGL context. Call stopLvgl() first. - - if (touchDriver == nullptr) { - touchDriver = std::make_shared(this); - } - - return touchDriver; -} +#include "Xpt2046SoftSpi.h" + +#include +#include + +#include + +#include +#include +#include +#include +#include + +static const auto LOGGER = tt::Logger("Xpt2046SoftSpi"); + +constexpr auto CMD_READ_Y = 0x90; +constexpr auto CMD_READ_X = 0xD0; + +constexpr int RAW_MIN_DEFAULT = 100; +constexpr int RAW_MAX_DEFAULT = 1900; +constexpr int RAW_VALID_MIN = 100; +constexpr int RAW_VALID_MAX = 3900; + +Xpt2046SoftSpi::Xpt2046SoftSpi(std::unique_ptr inConfiguration) + : configuration(std::move(inConfiguration)) { + assert(configuration != nullptr); +} + +bool Xpt2046SoftSpi::start() { + LOGGER.info("Starting Xpt2046SoftSpi touch driver"); + + // Configure GPIO pins + gpio_config_t io_conf = {}; + + // Configure MOSI, CLK, CS as outputs + io_conf.intr_type = GPIO_INTR_DISABLE; + io_conf.mode = GPIO_MODE_OUTPUT; + io_conf.pin_bit_mask = (1ULL << configuration->mosiPin) | + (1ULL << configuration->clkPin) | + (1ULL << configuration->csPin); + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_conf.pull_up_en = GPIO_PULLUP_DISABLE; + + if (gpio_config(&io_conf) != ESP_OK) { + LOGGER.error("Failed to configure output pins"); + return false; + } + + // Configure MISO as input + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pin_bit_mask = (1ULL << configuration->misoPin); + io_conf.pull_up_en = GPIO_PULLUP_ENABLE; + + if (gpio_config(&io_conf) != ESP_OK) { + LOGGER.error("Failed to configure input pin"); + return false; + } + + // Initialize pin states + gpio_set_level(configuration->csPin, 1); // CS high + gpio_set_level(configuration->clkPin, 0); // CLK low + gpio_set_level(configuration->mosiPin, 0); // MOSI low + + LOGGER.info( + "GPIO configured: MOSI={}, MISO={}, CLK={}, CS={}", + static_cast(configuration->mosiPin), + static_cast(configuration->misoPin), + static_cast(configuration->clkPin), + static_cast(configuration->csPin) + ); + + return true; +} + +bool Xpt2046SoftSpi::stop() { + LOGGER.info("Stopping Xpt2046SoftSpi touch driver"); + + // Stop LVLG if needed + if (lvglDevice != nullptr) { + stopLvgl(); + } + + return true; +} + +bool Xpt2046SoftSpi::startLvgl(lv_display_t* display) { + (void)display; + if (lvglDevice != nullptr) { + LOGGER.error("LVGL was already started"); + return false; + } + + lvglDevice = lv_indev_create(); + if (lvglDevice == nullptr) { + LOGGER.error("Failed to create LVGL input device"); + return false; + } + + lv_indev_set_type(lvglDevice, LV_INDEV_TYPE_POINTER); + lv_indev_set_read_cb(lvglDevice, touchReadCallback); + lv_indev_set_user_data(lvglDevice, this); + + LOGGER.info("Xpt2046SoftSpi touch driver started successfully"); + return true; +} + +bool Xpt2046SoftSpi::stopLvgl() { + if (lvglDevice != nullptr) { + lv_indev_delete(lvglDevice); + lvglDevice = nullptr; + } + return true; +} + +int Xpt2046SoftSpi::readSPI(uint8_t command) { + int result = 0; + + // Pull CS low for this transaction + gpio_set_level(configuration->csPin, 0); + ets_delay_us(1); + + // Send 8-bit command + for (int i = 7; i >= 0; i--) { + gpio_set_level(configuration->mosiPin, (command & (1 << i)) ? 1 : 0); + gpio_set_level(configuration->clkPin, 1); + ets_delay_us(1); + gpio_set_level(configuration->clkPin, 0); + ets_delay_us(1); + } + + for (int i = 11; i >= 0; i--) { + gpio_set_level(configuration->clkPin, 1); + ets_delay_us(1); + if (gpio_get_level(configuration->misoPin)) { + result |= (1 << i); + } + gpio_set_level(configuration->clkPin, 0); + ets_delay_us(1); + } + + // Pull CS high for this transaction + gpio_set_level(configuration->csPin, 1); + + return result; +} + +bool Xpt2046SoftSpi::readRawPoint(uint16_t& x, uint16_t& y) { + constexpr int sampleCount = 8; + int totalX = 0; + int totalY = 0; + int validSamples = 0; + + for (int i = 0; i < sampleCount; i++) { + const int rawX = readSPI(CMD_READ_X); + const int rawY = readSPI(CMD_READ_Y); + + if (rawX > RAW_VALID_MIN && rawX < RAW_VALID_MAX && rawY > RAW_VALID_MIN && rawY < RAW_VALID_MAX) { + totalX += rawX; + totalY += rawY; + validSamples++; + } + + vTaskDelay(pdMS_TO_TICKS(1)); + } + + if (validSamples < 3) { + return false; + } + + x = static_cast(totalX / validSamples); + y = static_cast(totalY / validSamples); + return true; +} + +bool Xpt2046SoftSpi::getTouchPoint(Point& point) { + uint16_t rawX = 0; + uint16_t rawY = 0; + if (!readRawPoint(rawX, rawY)) { + return false; + } + + int mappedX = (static_cast(rawX) - RAW_MIN_DEFAULT) * static_cast(configuration->xMax) / + (RAW_MAX_DEFAULT - RAW_MIN_DEFAULT); + int mappedY = (static_cast(rawY) - RAW_MIN_DEFAULT) * static_cast(configuration->yMax) / + (RAW_MAX_DEFAULT - RAW_MIN_DEFAULT); + + if (configuration->swapXy) { + std::swap(mappedX, mappedY); + } + if (configuration->mirrorX) { + mappedX = static_cast(configuration->xMax) - mappedX; + } + if (configuration->mirrorY) { + mappedY = static_cast(configuration->yMax) - mappedY; + } + + uint16_t x = static_cast(std::clamp(mappedX, 0, static_cast(configuration->xMax))); + uint16_t y = static_cast(std::clamp(mappedY, 0, static_cast(configuration->yMax))); + + const auto calibration = tt::settings::touch::getActive(); + tt::settings::touch::applyCalibration(calibration, configuration->xMax, configuration->yMax, x, y); + + point.x = x; + point.y = y; + return true; +} + +bool Xpt2046SoftSpi::isTouched() { + uint16_t x = 0; + uint16_t y = 0; + return readRawPoint(x, y); +} + +void Xpt2046SoftSpi::touchReadCallback(lv_indev_t* indev, lv_indev_data_t* data) { + auto* touch = static_cast(lv_indev_get_user_data(indev)); + if (touch == nullptr) { + data->state = LV_INDEV_STATE_RELEASED; + return; + } + + Point point; + if (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 Xpt2046SoftSpi::getTouchDriver() { + assert(lvglDevice == nullptr); // Still attached to LVGL context. Call stopLvgl() first. + + if (touchDriver == nullptr) { + touchDriver = std::make_shared(this); + } + + return touchDriver; +} diff --git a/Drivers/XPT2046SoftSPI/Source/Xpt2046SoftSpi.h b/Drivers/XPT2046SoftSPI/Source/Xpt2046SoftSpi.h index a861b009d..1bdddcc52 100644 --- a/Drivers/XPT2046SoftSPI/Source/Xpt2046SoftSpi.h +++ b/Drivers/XPT2046SoftSPI/Source/Xpt2046SoftSpi.h @@ -81,8 +81,7 @@ class Xpt2046SoftSpi : public tt::hal::touch::TouchDevice { std::shared_ptr touchDriver; int readSPI(uint8_t command); - bool loadCalibration(); - void saveCalibration(); + bool readRawPoint(uint16_t& x, uint16_t& y); static void touchReadCallback(lv_indev_t* indev, lv_indev_data_t* data); public: @@ -100,12 +99,11 @@ class Xpt2046SoftSpi : public tt::hal::touch::TouchDevice { bool stopLvgl() override; bool supportsTouchDriver() override { return true; } + bool supportsCalibration() const override { return true; } std::shared_ptr getTouchDriver() override; lv_indev_t* getLvglIndev() override { return lvglDevice; } // XPT2046-specific methods bool getTouchPoint(Point& point); - void calibrate(); - void setCalibration(int xMin, int yMin, int xMax, int yMax); bool isTouched(); }; diff --git a/Tactility/Include/Tactility/app/touchcalibration/TouchCalibration.h b/Tactility/Include/Tactility/app/touchcalibration/TouchCalibration.h new file mode 100644 index 000000000..560f2e15e --- /dev/null +++ b/Tactility/Include/Tactility/app/touchcalibration/TouchCalibration.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace tt::app::touchcalibration { + +LaunchId start(); + +} // namespace tt::app::touchcalibration diff --git a/Tactility/Include/Tactility/hal/touch/TouchDevice.h b/Tactility/Include/Tactility/hal/touch/TouchDevice.h index 84c97e313..1206ca1ba 100644 --- a/Tactility/Include/Tactility/hal/touch/TouchDevice.h +++ b/Tactility/Include/Tactility/hal/touch/TouchDevice.h @@ -27,6 +27,8 @@ class TouchDevice : public Device { virtual bool supportsTouchDriver() = 0; + virtual bool supportsCalibration() const { return false; } + /** Could return nullptr if not supported */ virtual std::shared_ptr getTouchDriver() = 0; }; diff --git a/Tactility/Include/Tactility/settings/TouchCalibrationSettings.h b/Tactility/Include/Tactility/settings/TouchCalibrationSettings.h new file mode 100644 index 000000000..1d80f7279 --- /dev/null +++ b/Tactility/Include/Tactility/settings/TouchCalibrationSettings.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +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 diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index 52ff1ef4f..5883970ce 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -103,6 +103,7 @@ namespace app { namespace settings { extern const AppManifest manifest; } namespace systeminfo { extern const AppManifest manifest; } namespace timedatesettings { extern const AppManifest manifest; } + namespace touchcalibration { extern const AppManifest manifest; } namespace timezone { extern const AppManifest manifest; } namespace usbsettings { extern const AppManifest manifest; } namespace wifiapsettings { extern const AppManifest manifest; } @@ -153,6 +154,7 @@ static void registerInternalApps() { addAppManifest(app::selectiondialog::manifest); addAppManifest(app::systeminfo::manifest); addAppManifest(app::timedatesettings::manifest); + addAppManifest(app::touchcalibration::manifest); addAppManifest(app::timezone::manifest); addAppManifest(app::wifiapsettings::manifest); addAppManifest(app::wificonnect::manifest); diff --git a/Tactility/Source/app/display/Display.cpp b/Tactility/Source/app/display/Display.cpp index 00a2a7eaf..080bd1cf7 100644 --- a/Tactility/Source/app/display/Display.cpp +++ b/Tactility/Source/app/display/Display.cpp @@ -7,7 +7,9 @@ #endif #include +#include #include +#include #include #include @@ -22,6 +24,16 @@ static std::shared_ptr getHalDisplay() { return hal::findFirstDevice(hal::Device::Type::Display); } +static bool hasCalibratableTouchDevice() { + auto touch_devices = hal::findDevices(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 { settings::display::DisplaySettings displaySettings; @@ -119,6 +131,10 @@ class DisplayApp final : public App { } } + static void onCalibrateTouchClicked(lv_event_t*) { + app::start("TouchCalibration"); + } + public: void onShow(AppContext& app, lv_obj_t* parent) override { @@ -278,6 +294,25 @@ class DisplayApp final : public App { 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 { diff --git a/Tactility/Source/app/touchcalibration/TouchCalibration.cpp b/Tactility/Source/app/touchcalibration/TouchCalibration.cpp new file mode 100644 index 000000000..5cfd11093 --- /dev/null +++ b/Tactility/Source/app/touchcalibration/TouchCalibration.cpp @@ -0,0 +1,203 @@ +#include + +#include + +#include +#include + +#include +#include + +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(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(sampleCount + 1)); + } + } + + void finishCalibration() { + constexpr int32_t MIN_RANGE = 20; + + const int32_t xMin = (static_cast(samples[0].x) + static_cast(samples[3].x)) / 2; + const int32_t xMax = (static_cast(samples[1].x) + static_cast(samples[2].x)) / 2; + const int32_t yMin = (static_cast(samples[0].y) + static_cast(samples[1].y)) / 2; + const int32_t yMax = (static_cast(samples[2].y) + static_cast(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(std::max(static_cast(0), point.x)), + .y = static_cast(std::max(static_cast(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 +}; + +} // namespace tt::app::touchcalibration diff --git a/Tactility/Source/settings/TouchCalibrationSettings.cpp b/Tactility/Source/settings/TouchCalibrationSettings.cpp new file mode 100644 index 000000000..9621d1633 --- /dev/null +++ b/Tactility/Source/settings/TouchCalibrationSettings.cpp @@ -0,0 +1,173 @@ +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +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(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 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 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(x); + const int32_t in_y = static_cast(y); + + const int64_t mapped_x = (static_cast(in_x) - static_cast(settings.xMin)) * + static_cast(xMax) / + (static_cast(settings.xMax) - static_cast(settings.xMin)); + const int64_t mapped_y = (static_cast(in_y) - static_cast(settings.yMin)) * + static_cast(yMax) / + (static_cast(settings.yMax) - static_cast(settings.yMin)); + + x = static_cast(std::clamp(mapped_x, 0, static_cast(xMax))); + y = static_cast(std::clamp(mapped_y, 0, static_cast(yMax))); + return true; +} + +} // namespace tt::settings::touch