Device implementations and touch calibration updates

This commit is contained in:
Ken Van Hoeylandt 2026-07-13 21:38:28 +02:00
parent fa4a6e255c
commit 8f5e92d6e4
27 changed files with 400 additions and 264 deletions

View File

@ -1,7 +1,6 @@
file(GLOB_RECURSE SOURCE_FILES Source/*.c*) file(GLOB_RECURSE SOURCE_FILES source/*.c*)
idf_component_register( idf_component_register(
SRCS ${SOURCE_FILES} SRCS ${SOURCE_FILES}
INCLUDE_DIRS "Source" REQUIRES TactilityKernel driver
REQUIRES Tactility esp_lvgl_port ILI934x XPT2046 PwmBacklight driver vfs fatfs
) )

View File

@ -1,21 +0,0 @@
#include "devices/Display.h"
#include <Tactility/hal/Configuration.h>
#include <PwmBacklight.h>
using namespace tt::hal;
static bool initBoot() {
return driver::pwmbacklight::init(LCD_PIN_BACKLIGHT);
}
static DeviceVector createDevices() {
return {
createDisplay()
};
}
extern const Configuration hardwareConfiguration = {
.initBoot = initBoot,
.createDevices = createDevices
};

View File

@ -1,46 +0,0 @@
#include "Display.h"
#include "Xpt2046Touch.h"
#include <Ili934xDisplay.h>
#include <PwmBacklight.h>
static std::shared_ptr<tt::hal::touch::TouchDevice> createTouch(esp_lcd_spi_bus_handle_t spiDevice) {
auto configuration = std::make_unique<Xpt2046Touch::Configuration>(
spiDevice,
TOUCH_CS_PIN,
LCD_HORIZONTAL_RESOLUTION,
LCD_VERTICAL_RESOLUTION,
true, // swapXY
false, // mirrorX
true // mirrorY
);
return std::make_shared<Xpt2046Touch>(std::move(configuration));
}
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay() {
auto spi_configuration = std::make_shared<Ili934xDisplay::SpiConfiguration>(Ili934xDisplay::SpiConfiguration {
.spiHostDevice = LCD_SPI_HOST,
.csPin = LCD_PIN_CS,
.dcPin = LCD_PIN_DC,
.pixelClockFrequency = 40'000'000,
.transactionQueueDepth = 10
});
Ili934xDisplay::Configuration panel_configuration = {
.horizontalResolution = LCD_HORIZONTAL_RESOLUTION,
.verticalResolution = LCD_VERTICAL_RESOLUTION,
.gapX = 0,
.gapY = 0,
.swapXY = true,
.mirrorX = true,
.mirrorY = true,
.invertColor = false,
.swapBytes = true,
.bufferSize = LCD_BUFFER_SIZE,
.touch = createTouch(spi_configuration->spiHostDevice),
.backlightDutyFunction = driver::pwmbacklight::setBacklightDuty,
.resetPin = LCD_PIN_RST,
.rgbElementOrder = LCD_RGB_ELEMENT_ORDER_RGB
};
return std::make_shared<Ili934xDisplay>(panel_configuration, spi_configuration, true);
}

View File

@ -1,28 +0,0 @@
#pragma once
#include <Tactility/hal/display/DisplayDevice.h>
#include <driver/gpio.h>
#include <driver/spi_common.h>
#include <memory>
// Display
constexpr auto LCD_SPI_HOST = SPI2_HOST;
constexpr auto LCD_PIN_CS = GPIO_NUM_15;
constexpr auto LCD_PIN_DC = GPIO_NUM_2;
constexpr auto LCD_PIN_RST = GPIO_NUM_NC; // tied to ESP32 RST
constexpr auto LCD_PIN_CLK = GPIO_NUM_14;
constexpr auto LCD_PIN_MOSI = GPIO_NUM_13;
constexpr auto LCD_PIN_MISO = GPIO_NUM_12;
constexpr auto LCD_HORIZONTAL_RESOLUTION = 240;
constexpr auto LCD_VERTICAL_RESOLUTION = 320;
constexpr auto LCD_BUFFER_HEIGHT = LCD_VERTICAL_RESOLUTION / 10;
constexpr auto LCD_BUFFER_SIZE = LCD_HORIZONTAL_RESOLUTION * LCD_BUFFER_HEIGHT;
// Backlight
constexpr auto LCD_PIN_BACKLIGHT = GPIO_NUM_27;
// Touch
constexpr auto TOUCH_CS_PIN = GPIO_NUM_33;
constexpr auto TOUCH_IRQ_PIN = GPIO_NUM_36;
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay();

View File

@ -5,9 +5,10 @@
#include <tactility/bindings/esp32_gpio.h> #include <tactility/bindings/esp32_gpio.h>
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/display_placeholder.h>
#include <tactility/bindings/pointer_placeholder.h>
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_ledc_backlight.h>
#include <bindings/ili9341.h>
#include <bindings/xpt2046.h>
/ { / {
compatible = "root"; compatible = "root";
@ -23,6 +24,15 @@
gpio-count = <40>; gpio-count = <40>;
}; };
display_backlight {
compatible = "espressif,esp32-ledc-backlight";
// Off by default so display power-on won't show the screen from before the last power loss.
// The display backlight is turned on during the boot process.
status = "disabled";
pin-backlight = <&gpio0 27 GPIO_FLAG_NONE>;
frequency-hz = <512>;
};
spi0 { spi0 {
compatible = "espressif,esp32-spi"; compatible = "espressif,esp32-spi";
host = <SPI2_HOST>; host = <SPI2_HOST>;
@ -33,11 +43,23 @@
<&gpio0 33 GPIO_FLAG_NONE>; // Touch <&gpio0 33 GPIO_FLAG_NONE>; // Touch
display@0 { display@0 {
compatible = "display-placeholder"; compatible = "ilitek,ili9341";
horizontal-resolution = <240>;
vertical-resolution = <320>;
swap-xy;
mirror-x;
mirror-y;
pixel-clock-hz = <40000000>;
pin-dc = <&gpio0 2 GPIO_FLAG_NONE>;
backlight = <&display_backlight>;
}; };
touch@1 { touch@1 {
compatible = "pointer-placeholder"; compatible = "xptek,xpt2046";
x-max = <240>;
y-max = <320>;
swap-xy;
mirror-y;
}; };
}; };
@ -61,4 +83,4 @@
pin-tx = <&gpio0 1 GPIO_FLAG_NONE>; pin-tx = <&gpio0 1 GPIO_FLAG_NONE>;
pin-rx = <&gpio0 3 GPIO_FLAG_NONE>; pin-rx = <&gpio0 3 GPIO_FLAG_NONE>;
}; };
}; };

View File

@ -7,6 +7,8 @@ hardware.target=ESP32
hardware.flashSize=4MB hardware.flashSize=4MB
hardware.spiRam=false hardware.spiRam=false
dependencies.useDeprecatedHal=false
storage.userDataLocation=SD storage.userDataLocation=SD
display.size=2.4" display.size=2.4"

View File

@ -1,3 +1,5 @@
dependencies: dependencies:
- Platforms/platform-esp32 - Platforms/platform-esp32
- Drivers/ili9341-module
- Drivers/xpt2046-module
dts: cyd,2432s024r.dts dts: cyd,2432s024r.dts

View File

@ -13,6 +13,9 @@ display.size=2.8"
display.shape=rectangle display.shape=rectangle
display.dpi=143 display.dpi=143
touch.calibrationSupported=true
touch.calibrationRequired=false
cdn.warningMessage=There are 3 hardware variants of this board. This build works on the original variant only ("v1"). cdn.warningMessage=There are 3 hardware variants of this board. This build works on the original variant only ("v1").
lvgl.colorDepth=16 lvgl.colorDepth=16

View File

@ -16,6 +16,9 @@ display.size=3.5"
display.shape=rectangle display.shape=rectangle
display.dpi=165 display.dpi=165
touch.calibrationSupported=true
touch.calibrationRequired=true
cdn.warningMessage=Put the device into bootloader mode by pressing the center nav button and reset for 2-3 seconds, then release reset, then release the nav button.<br/>After flashing is finished, press the reset button to reboot. cdn.warningMessage=Put the device into bootloader mode by pressing the center nav button and reset for 2-3 seconds, then release reset, then release the nav button.<br/>After flashing is finished, press the reset button to reboot.
lvgl.colorDepth=24 lvgl.colorDepth=24

View File

@ -1,30 +1,10 @@
#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;
@ -49,7 +29,7 @@ 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 = processCoordinates, .process_coordinates = nullptr,
.interrupt_callback = nullptr, .interrupt_callback = nullptr,
.user_data = configuration.get(), .user_data = configuration.get(),
.driver_data = nullptr .driver_data = nullptr

View File

@ -56,6 +56,4 @@ public:
std::string getName() const final { return "XPT2046"; } std::string getName() const final { return "XPT2046"; }
std::string getDescription() const final { return "XPT2046 SPI touch driver"; } std::string getDescription() const final { return "XPT2046 SPI touch driver"; }
bool supportsCalibration() const override { return true; }
}; };

View File

@ -1,7 +1,6 @@
#include "Xpt2046SoftSpi.h" #include "Xpt2046SoftSpi.h"
#include <tactility/log.h> #include <tactility/log.h>
#include <Tactility/settings/TouchCalibrationSettings.h>
#include <algorithm> #include <algorithm>
@ -198,9 +197,6 @@ bool Xpt2046SoftSpi::getTouchPoint(Point& point) {
uint16_t x = static_cast<uint16_t>(std::clamp(mappedX, 0, static_cast<int>(configuration->xMax))); uint16_t x = static_cast<uint16_t>(std::clamp(mappedX, 0, static_cast<int>(configuration->xMax)));
uint16_t y = static_cast<uint16_t>(std::clamp(mappedY, 0, static_cast<int>(configuration->yMax))); uint16_t y = static_cast<uint16_t>(std::clamp(mappedY, 0, static_cast<int>(configuration->yMax)));
const auto calibration = tt::settings::touch::getActive();
tt::settings::touch::applyCalibration(calibration, configuration->xMax, configuration->yMax, x, y);
point.x = x; point.x = x;
point.y = y; point.y = y;
return true; return true;

View File

@ -99,7 +99,6 @@ 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; }

View File

@ -102,4 +102,11 @@ menu "Tactility App"
help help
The minimum time to show the splash screen in milliseconds. The minimum time to show the splash screen in milliseconds.
When set to 0, startup will continue to desktop as soon as boot operations are finished. When set to 0, startup will continue to desktop as soon as boot operations are finished.
config TT_TOUCH_CALIBRATION_SUPPORTED
bool "Set true when a touch screen calibration app should be included"
default n
config TT_TOUCH_CALIBRATION_REQUIRED
bool "Set true when a touch screen calibration is required before the device is usable"
default n
depends on TT_TOUCH_CALIBRATION_SUPPORTED
endmenu endmenu

View File

@ -10,6 +10,56 @@ extern "C" {
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/error.h> #include <tactility/error.h>
/**
* @brief Linear per-axis calibration range for raw pointer coordinates.
*
* Values are the raw (pre-calibration) coordinates that should map to the display's
* [0, hor_res-1] / [0, ver_res-1] range. Corrects scale+offset error only; axis
* swap/mirror is handled separately by PointerApi and applied by the driver before
* lvgl_pointer_read_cb() sees the coordinates.
*/
struct LvglPointerCalibration {
int32_t x_min;
int32_t x_max;
int32_t y_min;
int32_t y_max;
};
/**
* @brief Sets (or clears, when calibration is NULL) the calibration applied to raw coordinates
* read from the device before they are written into LVGL indev data, on an indev previously
* created with lvgl_pointer_add().
*
* @warning Caller must hold the LVGL lock (see lvgl_lock() in lvgl_module.h).
*
* @param[in] indev an indev previously created by lvgl_pointer_add()
* @param[in] calibration the calibration range to apply, or NULL to clear/disable calibration
* @retval ERROR_NONE on success
* @retval ERROR_INVALID_ARGUMENT if indev is NULL, or calibration is non-NULL but invalid
* (x_max <= x_min, y_max <= y_min, or either span smaller than the minimum allowed range)
*/
error_t lvgl_pointer_set_calibration(lv_indev_t* indev, const struct LvglPointerCalibration* calibration);
/**
* @brief Retrieves the calibration currently active on indev, if any.
* @warning Caller must hold the LVGL lock.
* @return true when a calibration is currently set on indev (out_calibration is filled), false otherwise
*/
bool lvgl_pointer_get_calibration(lv_indev_t* indev, struct LvglPointerCalibration* out_calibration);
/**
* @brief Returns the first indev created by lvgl_pointer_add() that hasn't been removed yet.
*
* Unlike iterating LVGL's own indev list, this only ever returns an indev created by
* lvgl_pointer_add() safe to pass to lvgl_pointer_set_calibration()/lvgl_pointer_get_calibration()
* without risking a foreign indev (e.g. one registered by the deprecated HAL layer) whose driver
* data isn't a struct LvglPointerCtx*.
*
* @warning Caller must hold the LVGL lock.
* @return the indev, or NULL if none is currently registered.
*/
lv_indev_t* lvgl_pointer_get_default(void);
/** /**
* @brief Creates an lv_indev_t bound to the given POINTER_TYPE device and registers a read callback * @brief Creates an lv_indev_t bound to the given POINTER_TYPE device and registers a read callback
* that polls the device through its PointerApi. * that polls the device through its PointerApi.

View File

@ -7,11 +7,52 @@
struct LvglPointerCtx { struct LvglPointerCtx {
struct Device* device; struct Device* device;
bool calibration_enabled;
struct LvglPointerCalibration calibration;
}; };
// Bus reads are expected to complete quickly; bound the wait so a stalled controller can't block the LVGL indev poll. // 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); 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;
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;
}
static void lvgl_pointer_read_cb(lv_indev_t* indev, lv_indev_data_t* data) { static void lvgl_pointer_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)lv_indev_get_driver_data(indev); struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)lv_indev_get_driver_data(indev);
@ -26,6 +67,16 @@ static void lvgl_pointer_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
bool touched = pointer_get_touched_points(ctx->device, &x, &y, NULL, &point_count, 1); bool touched = pointer_get_touched_points(ctx->device, &x, &y, NULL, &point_count, 1);
if (touched && point_count > 0) { if (touched && point_count > 0) {
if (ctx->calibration_enabled) {
lv_display_t* display = lv_indev_get_display(indev);
if (display != NULL) {
int32_t target_x_max = lv_display_get_horizontal_resolution(display) - 1;
int32_t target_y_max = lv_display_get_vertical_resolution(display) - 1;
if (target_x_max > 0 && target_y_max > 0) {
lvgl_pointer_calibration_apply(&ctx->calibration, target_x_max, target_y_max, &x, &y);
}
}
}
data->point.x = x; data->point.x = x;
data->point.y = y; data->point.y = y;
data->state = LV_INDEV_STATE_PRESSED; data->state = LV_INDEV_STATE_PRESSED;
@ -42,7 +93,7 @@ error_t lvgl_pointer_add(struct Device* device, lv_display_t* display, lv_indev_
return ERROR_INVALID_ARGUMENT; return ERROR_INVALID_ARGUMENT;
} }
struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)malloc(sizeof(struct LvglPointerCtx)); struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)calloc(1, sizeof(struct LvglPointerCtx));
if (ctx == NULL) { if (ctx == NULL) {
return ERROR_OUT_OF_MEMORY; return ERROR_OUT_OF_MEMORY;
} }
@ -61,16 +112,58 @@ error_t lvgl_pointer_add(struct Device* device, lv_display_t* display, lv_indev_
lv_indev_set_display(indev, display); lv_indev_set_display(indev, display);
} }
if (default_pointer_indev == NULL) {
default_pointer_indev = indev;
}
*out_indev = indev; *out_indev = indev;
return ERROR_NONE; return ERROR_NONE;
} }
lv_indev_t* lvgl_pointer_get_default(void) {
return default_pointer_indev;
}
error_t lvgl_pointer_set_calibration(lv_indev_t* indev, const struct LvglPointerCalibration* calibration) {
if (indev == NULL) {
return ERROR_INVALID_ARGUMENT;
}
struct LvglPointerCtx* ctx = lv_indev_get_driver_data(indev);
if (calibration == NULL) {
ctx->calibration_enabled = false;
return ERROR_NONE;
}
if (!lvgl_pointer_calibration_is_valid(calibration)) {
return ERROR_INVALID_ARGUMENT;
}
ctx->calibration = *calibration;
ctx->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) {
return false;
}
struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)lv_indev_get_driver_data(indev);
if (!ctx->calibration_enabled) {
return false;
}
*out_calibration = ctx->calibration;
return true;
}
void lvgl_pointer_remove(lv_indev_t* indev) { void lvgl_pointer_remove(lv_indev_t* indev) {
if (indev == NULL) { if (indev == NULL) {
return; return;
} }
struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)lv_indev_get_driver_data(indev); struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)lv_indev_get_driver_data(indev);
if (default_pointer_indev == indev) {
default_pointer_indev = NULL;
}
lv_indev_delete(indev); lv_indev_delete(indev);
free(ctx); free(ctx);
} }

View File

@ -1,5 +1,9 @@
#pragma once #pragma once
#include <sdkconfig.h>
#if defined(CONFIG_TT_TOUCH_CALIBRATION_SUPPORTED)
#include <Tactility/app/App.h> #include <Tactility/app/App.h>
namespace tt::app::touchcalibration { namespace tt::app::touchcalibration {
@ -7,3 +11,5 @@ namespace tt::app::touchcalibration {
LaunchId start(); LaunchId start();
} // namespace tt::app::touchcalibration } // namespace tt::app::touchcalibration
#endif

View File

@ -4,6 +4,14 @@
namespace tt::settings::touch { namespace tt::settings::touch {
/**
* @brief Persisted touch calibration coefficients.
*
* Shape mirrors struct LvglPointerCalibration (tactility/lvgl_pointer.h): xMin/xMax/yMin/yMax are
* the raw touch coordinate range that should map onto the display's full resolution. This struct
* only concerns itself with persistence - applying it to a live pointer indev is the caller's
* responsibility (see lvgl_pointer_set_calibration()).
*/
struct TouchCalibrationSettings { struct TouchCalibrationSettings {
bool enabled = false; bool enabled = false;
int32_t xMin = 0; int32_t xMin = 0;
@ -14,20 +22,12 @@ struct TouchCalibrationSettings {
TouchCalibrationSettings getDefault(); TouchCalibrationSettings getDefault();
bool isValid(const TouchCalibrationSettings& settings);
bool load(TouchCalibrationSettings& settings); bool load(TouchCalibrationSettings& settings);
TouchCalibrationSettings loadOrGetDefault(); TouchCalibrationSettings loadOrGetDefault();
bool save(const TouchCalibrationSettings& settings); 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 } // namespace tt::settings::touch

View File

@ -132,7 +132,9 @@ namespace app {
namespace setup { extern const AppManifest manifest; } namespace setup { 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; }
#ifdef CONFIG_TT_TOUCH_CALIBRATION_SUPPORTED
namespace touchcalibration { extern const AppManifest manifest; } namespace touchcalibration { extern const AppManifest manifest; }
#endif
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 btmanage { extern const AppManifest manifest; } namespace btmanage { extern const AppManifest manifest; }
@ -193,7 +195,9 @@ static void registerInternalApps() {
addAppManifest(app::setup::manifest); addAppManifest(app::setup::manifest);
addAppManifest(app::systeminfo::manifest); addAppManifest(app::systeminfo::manifest);
addAppManifest(app::timedatesettings::manifest); addAppManifest(app::timedatesettings::manifest);
#ifdef CONFIG_TT_TOUCH_CALIBRATION_SUPPORTED
addAppManifest(app::touchcalibration::manifest); addAppManifest(app::touchcalibration::manifest);
#endif
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

@ -11,10 +11,12 @@
#include <Tactility/service/displayidle/DisplayIdleService.h> #include <Tactility/service/displayidle/DisplayIdleService.h>
#endif #endif
#include <Tactility/app/App.h> #include <Tactility/app/App.h>
#include <Tactility/app/touchcalibration/TouchCalibration.h>
#include <Tactility/lvgl/Toolbar.h> #include <Tactility/lvgl/Toolbar.h>
#include <Tactility/settings/DisplaySettings.h> #include <Tactility/settings/DisplaySettings.h>
#include <lvgl.h> #include <lvgl.h>
#include <tactility/lvgl_pointer.h>
namespace tt::app::kerneldisplay { namespace tt::app::kerneldisplay {
@ -98,6 +100,10 @@ class KernelDisplayApp final : public App {
} }
} }
static void onCalibrateTouchClicked(lv_event_t*) {
app::touchcalibration::start();
}
static void onScreensaverChanged(lv_event_t* event) { static void onScreensaverChanged(lv_event_t* event) {
auto* app = static_cast<KernelDisplayApp*>(lv_event_get_user_data(event)); auto* app = static_cast<KernelDisplayApp*>(lv_event_get_user_data(event));
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event)); auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
@ -251,8 +257,24 @@ public:
} }
} }
// Note: no touch calibration section here - unlike HalDisplayApp, the kernel PointerApi has if (lvgl_pointer_get_default() != nullptr) {
// no calibration support yet. 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

@ -17,6 +17,12 @@
#include <functional> #include <functional>
#include <vector> #include <vector>
#include <sdkconfig.h>
#if defined(CONFIG_TT_TOUCH_CALIBRATION_REQUIRED)
#include <Tactility/app/touchcalibration/TouchCalibration.h>
#endif
namespace tt::app::setup { namespace tt::app::setup {
extern const AppManifest manifest; extern const AppManifest manifest;
@ -139,6 +145,13 @@ public:
void onCreate(AppContext& app) override { void onCreate(AppContext& app) override {
steps = { steps = {
#if defined(CONFIG_TT_TOUCH_CALIBRATION_REQUIRED)
{
.title = "Touch Calibration",
.description = "Let's calibrate the touch screen.",
.run = [] { touchcalibration::start(); }
},
#endif
{ {
.title = "Time Zone Setup", .title = "Time Zone Setup",
.description = "Let's set the time zone.", .description = "Let's set the time zone.",

View File

@ -166,16 +166,16 @@ class TimeZoneApp final : public App {
} }
void updateList() { void updateList() {
if (lvgl::lock(100 / portTICK_PERIOD_MS)) { if (lvgl::lock(200 / portTICK_PERIOD_MS)) {
std::string filter = string::lowercase(std::string(lv_textarea_get_text(filterTextareaWidget))); std::string filter = string::lowercase(std::string(lv_textarea_get_text(filterTextareaWidget)));
readTimeZones(filter);
lvgl::unlock(); lvgl::unlock();
readTimeZones(filter);
} else { } else {
LOG_E(LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL"); LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "TimeZone LVGL");
return; return;
} }
if (lvgl::lock(100 / portTICK_PERIOD_MS)) { if (lvgl::lock(200 / portTICK_PERIOD_MS)) {
if (mutex.lock(100 / portTICK_PERIOD_MS)) { if (mutex.lock(100 / portTICK_PERIOD_MS)) {
lv_obj_clean(listWidget); lv_obj_clean(listWidget);
@ -189,6 +189,8 @@ class TimeZoneApp final : public App {
} }
lvgl::unlock(); lvgl::unlock();
} else {
LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "TimeZone LVGL");
} }
} }

View File

@ -1,10 +1,13 @@
#include <Tactility/Tactility.h>
#include <Tactility/app/touchcalibration/TouchCalibration.h> #include <Tactility/app/touchcalibration/TouchCalibration.h>
#if defined(CONFIG_TT_TOUCH_CALIBRATION_SUPPORTED)
#include <Tactility/Tactility.h>
#include <Tactility/settings/TouchCalibrationSettings.h> #include <Tactility/settings/TouchCalibrationSettings.h>
#include <tactility/log.h> #include <tactility/log.h>
#include <tactility/lvgl_module.h>
#include <tactility/lvgl_pointer.h>
#include <algorithm> #include <algorithm>
#include <lvgl.h> #include <lvgl.h>
@ -30,6 +33,7 @@ class TouchCalibrationApp final : public App {
Sample samples[4] = {}; Sample samples[4] = {};
uint8_t sampleCount = 0; uint8_t sampleCount = 0;
bool calibrationApplied = false;
lv_obj_t* root = nullptr; lv_obj_t* root = nullptr;
lv_obj_t* target = nullptr; lv_obj_t* target = nullptr;
@ -73,12 +77,33 @@ class TouchCalibrationApp final : public App {
} }
void finishCalibration() { void finishCalibration() {
constexpr int32_t MIN_RANGE = 20; const int32_t xLow = (static_cast<int32_t>(samples[0].x) + static_cast<int32_t>(samples[3].x)) / 2;
const int32_t xHigh = (static_cast<int32_t>(samples[1].x) + static_cast<int32_t>(samples[2].x)) / 2;
const int32_t yLow = (static_cast<int32_t>(samples[0].y) + static_cast<int32_t>(samples[1].y)) / 2;
const int32_t yHigh = (static_cast<int32_t>(samples[2].y) + static_cast<int32_t>(samples[3].y)) / 2;
const int32_t xMin = (static_cast<int32_t>(samples[0].x) + static_cast<int32_t>(samples[3].x)) / 2; // Targets sit TARGET_MARGIN in from each edge (see getTargetPoint()), not at the screen
const int32_t xMax = (static_cast<int32_t>(samples[1].x) + static_cast<int32_t>(samples[2].x)) / 2; // edges themselves - xLow/xHigh/yLow/yHigh are raw samples at those inset positions, not
const int32_t yMin = (static_cast<int32_t>(samples[0].y) + static_cast<int32_t>(samples[1].y)) / 2; // at 0/width or 0/height. Extrapolate them out to the true edges so the saved range (which
const int32_t yMax = (static_cast<int32_t>(samples[2].y) + static_cast<int32_t>(samples[3].y)) / 2; // lvgl_pointer.h maps onto the full [0, resolution) display range) lines up correctly
// across the whole screen instead of being off by a margin's worth of scale and offset.
const auto width = lv_obj_get_content_width(root);
const auto height = lv_obj_get_content_height(root);
const int32_t xSpan = static_cast<int32_t>(width) - 2 * TARGET_MARGIN;
const int32_t ySpan = static_cast<int32_t>(height) - 2 * TARGET_MARGIN;
if (xSpan <= 0 || ySpan <= 0) {
lv_label_set_text(titleLabel, "Calibration Failed");
lv_label_set_text(hintLabel, "Screen too small. Tap to close.");
lv_obj_add_flag(target, LV_OBJ_FLAG_HIDDEN);
setResult(Result::Error);
return;
}
const int32_t xMin = xLow - (xHigh - xLow) * TARGET_MARGIN / xSpan;
const int32_t xMax = xHigh + (xHigh - xLow) * TARGET_MARGIN / xSpan;
const int32_t yMin = yLow - (yHigh - yLow) * TARGET_MARGIN / ySpan;
const int32_t yMax = yHigh + (yHigh - yLow) * TARGET_MARGIN / ySpan;
settings::touch::TouchCalibrationSettings settings = settings::touch::getDefault(); settings::touch::TouchCalibrationSettings settings = settings::touch::getDefault();
settings.enabled = true; settings.enabled = true;
@ -87,7 +112,7 @@ class TouchCalibrationApp final : public App {
settings.yMin = yMin; settings.yMin = yMin;
settings.yMax = yMax; settings.yMax = yMax;
if ((xMax - xMin) < MIN_RANGE || (yMax - yMin) < MIN_RANGE || !settings::touch::isValid(settings)) { if (!settings::touch::isValid(settings)) {
lv_label_set_text(titleLabel, "Calibration Failed"); lv_label_set_text(titleLabel, "Calibration Failed");
lv_label_set_text(hintLabel, "Range invalid. Tap to close."); lv_label_set_text(hintLabel, "Range invalid. Tap to close.");
lv_obj_add_flag(target, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(target, LV_OBJ_FLAG_HIDDEN);
@ -103,6 +128,20 @@ class TouchCalibrationApp final : public App {
return; return;
} }
LvglPointerCalibration calibration = {
.x_min = xMin,
.x_max = xMax,
.y_min = yMin,
.y_max = yMax,
};
lvgl_lock();
auto* indev = lvgl_pointer_get_default();
if (indev != nullptr) {
lvgl_pointer_set_calibration(indev, &calibration);
}
lvgl_unlock();
calibrationApplied = true;
LOG_I(TAG, "Saved calibration x=[%d, %d] y=[%d, %d]", xMin, xMax, yMin, yMax); LOG_I(TAG, "Saved calibration x=[%d, %d] y=[%d, %d]", xMin, xMax, yMin, yMax);
lv_label_set_text(titleLabel, "Calibration Complete"); lv_label_set_text(titleLabel, "Calibration Complete");
lv_label_set_text(hintLabel, "Touch anywhere to continue."); lv_label_set_text(hintLabel, "Touch anywhere to continue.");
@ -141,14 +180,36 @@ public:
void onCreate(AppContext& app) override { void onCreate(AppContext& app) override {
(void)app; (void)app;
settings::touch::setRuntimeCalibrationEnabled(false); // Clear any active calibration so the taps sampled below are raw, uncalibrated coordinates.
settings::touch::invalidateCache(); lvgl_lock();
auto* indev = lvgl_pointer_get_default();
if (indev != nullptr) {
lvgl_pointer_set_calibration(indev, nullptr);
}
lvgl_unlock();
} }
void onDestroy(AppContext& app) override { void onDestroy(AppContext& app) override {
(void)app; (void)app;
settings::touch::setRuntimeCalibrationEnabled(true); // finishCalibration() already applied a new calibration on success. On cancel/failure,
settings::touch::invalidateCache(); // restore whatever calibration was on disk before onCreate() cleared it above.
if (calibrationApplied) {
return;
}
settings::touch::TouchCalibrationSettings settings;
lvgl_lock();
auto* indev = lvgl_pointer_get_default();
if (indev != nullptr && settings::touch::load(settings) && settings.enabled && settings::touch::isValid(settings)) {
LvglPointerCalibration calibration = {
.x_min = settings.xMin,
.x_max = settings.xMax,
.y_min = settings.yMin,
.y_max = settings.yMax,
};
lvgl_pointer_set_calibration(indev, &calibration);
}
lvgl_unlock();
} }
void onShow(AppContext& app, lv_obj_t* parent) override { void onShow(AppContext& app, lv_obj_t* parent) override {
@ -196,9 +257,11 @@ public:
extern const AppManifest manifest = { extern const AppManifest manifest = {
.appId = "TouchCalibration", .appId = "TouchCalibration",
.appName = "Touch Calibration", .appName = "Touch Calibration",
.appCategory = Category::System, .appCategory = Category::Settings,
.appFlags = AppManifest::Flags::HideStatusBar | AppManifest::Flags::Hidden, .appFlags = AppManifest::Flags::HideStatusBar,
.createApp = create<TouchCalibrationApp> .createApp = create<TouchCalibrationApp>
}; };
} // namespace tt::app::touchcalibration } // namespace tt::app::touchcalibration
#endif // defined(CONFIG_TT_TOUCH_CALIBRATION_SUPPORTED)

View File

@ -8,11 +8,13 @@
#include <Tactility/lvgl/LvglSync.h> #include <Tactility/lvgl/LvglSync.h>
#include <Tactility/service/ServiceRegistration.h> #include <Tactility/service/ServiceRegistration.h>
#include <Tactility/settings/DisplaySettings.h> #include <Tactility/settings/DisplaySettings.h>
#include <Tactility/settings/TouchCalibrationSettings.h>
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/drivers/backlight.h> #include <tactility/drivers/backlight.h>
#include <tactility/log.h> #include <tactility/log.h>
#include <tactility/lvgl_module.h> #include <tactility/lvgl_module.h>
#include <tactility/lvgl_pointer.h>
#include <tactility/module.h> #include <tactility/module.h>
#include <lvgl.h> #include <lvgl.h>
@ -71,6 +73,24 @@ void attachDevices() {
} }
} }
// Apply touch calibration (kernel POINTER_TYPE model only - see tactility/lvgl_pointer.h)
LOG_I(TAG, "Apply touch calibration");
auto touch_calibration_settings = settings::touch::loadOrGetDefault();
if (touch_calibration_settings.enabled && settings::touch::isValid(touch_calibration_settings)) {
auto* pointer_indev = lvgl_pointer_get_default();
if (pointer_indev != nullptr) {
struct LvglPointerCalibration calibration = {
.x_min = touch_calibration_settings.xMin,
.x_max = touch_calibration_settings.xMax,
.y_min = touch_calibration_settings.yMin,
.y_max = touch_calibration_settings.yMax,
};
if (lvgl_pointer_set_calibration(pointer_indev, &calibration) != ERROR_NONE) {
LOG_E(TAG, "Failed to apply saved touch calibration");
}
}
}
// Start keyboards // Start keyboards
LOG_I(TAG, "Start keyboards"); LOG_I(TAG, "Start keyboards");
auto keyboards = hal::findDevices<hal::keyboard::KeyboardDevice>(hal::Device::Type::Keyboard); auto keyboards = hal::findDevices<hal::keyboard::KeyboardDevice>(hal::Device::Type::Keyboard);

View File

@ -2,10 +2,8 @@
#include <Tactility/file/File.h> #include <Tactility/file/File.h>
#include <Tactility/file/PropertiesFile.h> #include <Tactility/file/PropertiesFile.h>
#include <Tactility/Mutex.h>
#include <Tactility/Paths.h> #include <Tactility/Paths.h>
#include <algorithm>
#include <cstdlib> #include <cstdlib>
#include <cerrno> #include <cerrno>
#include <climits> #include <climits>
@ -24,11 +22,6 @@ constexpr auto* SETTINGS_KEY_X_MAX = "xMax";
constexpr auto* SETTINGS_KEY_Y_MIN = "yMin"; constexpr auto* SETTINGS_KEY_Y_MIN = "yMin";
constexpr auto* SETTINGS_KEY_Y_MAX = "yMax"; 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) { static bool toBool(const std::string& value) {
return value == "1" || value == "true" || value == "True"; return value == "1" || value == "true" || value == "True";
} }
@ -127,62 +120,7 @@ bool save(const TouchCalibrationSettings& settings) {
return false; return false;
} }
if (!file::savePropertiesFile(settings_path, map)) { return file::savePropertiesFile(settings_path, 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 } // namespace tt::settings::touch

View File

@ -62,20 +62,19 @@ def has_group(properties: dict, group: str):
prefix = f"{group}." prefix = f"{group}."
return any(key.startswith(prefix) for key in properties) return any(key.startswith(prefix) for key in properties)
def get_property_or_exit(properties: dict, group: str, key: str): def get_property_or_exit(properties: dict, key: str):
full_key = f"{group}.{key}" if key not in properties:
if full_key not in properties: exit_with_error(f"Device properties does not contain key: {key}")
exit_with_error(f"Device properties does not contain key: {full_key}") return properties[key]
return properties[full_key]
def get_property_or_default(properties: dict, group: str, key: str, default): def get_property_or_default(properties: dict, key: str, default):
return properties.get(f"{group}.{key}", default) return properties.get(key, default)
def get_property_or_none(properties: dict, group: str, key: str): def get_property_or_none(properties: dict, key: str):
return get_property_or_default(properties, group, key, None) return get_property_or_default(properties, key, None)
def get_boolean_property_or_false(properties: dict, group: str, key: str): def get_boolean_property_or_false(properties: dict, key: str):
return properties.get(f"{group}.{key}") == "true" return properties.get(key) == "true"
def safe_int(value: str, error_message: str): def safe_int(value: str, error_message: str):
try: try:
@ -95,13 +94,13 @@ def write_defaults(output_file):
output_file.write(default_properties) output_file.write(default_properties)
def get_user_data_location(device_properties: dict): def get_user_data_location(device_properties: dict):
user_data_location = get_property_or_exit(device_properties, "storage", "userDataLocation") user_data_location = get_property_or_exit(device_properties, "storage.userDataLocation")
if user_data_location not in ("SD", "Internal"): if user_data_location not in ("SD", "Internal"):
exit_with_error(f"storage.userDataLocation must be 'SD' or 'Internal', but was: '{user_data_location}'") exit_with_error(f"storage.userDataLocation must be 'SD' or 'Internal', but was: '{user_data_location}'")
return user_data_location return user_data_location
def write_partition_table(output_file, device_properties: dict, is_dev: bool): def write_partition_table(output_file, device_properties: dict, is_dev: bool):
flash_size = get_property_or_exit(device_properties, "hardware", "flashSize") flash_size = get_property_or_exit(device_properties, "hardware.flashSize")
if not flash_size.endswith("MB"): if not flash_size.endswith("MB"):
exit_with_error("Flash size should be written as xMB or xxMB (e.g. 4MB, 16MB)") exit_with_error("Flash size should be written as xMB or xxMB (e.g. 4MB, 16MB)")
flash_size_number = flash_size[:-2] flash_size_number = flash_size[:-2]
@ -122,8 +121,8 @@ def write_partition_table(output_file, device_properties: dict, is_dev: bool):
def write_tactility_variables(output_file, device_properties: dict, device_id: str): def write_tactility_variables(output_file, device_properties: dict, device_id: str):
# Board and vendor # Board and vendor
board_vendor = get_property_or_exit(device_properties, "general", "vendor").replace("\"", "\\\"") board_vendor = get_property_or_exit(device_properties, "general.vendor").replace("\"", "\\\"")
board_name = get_property_or_exit(device_properties, "general", "name").replace("\"", "\\\"") board_name = get_property_or_exit(device_properties, "general.name").replace("\"", "\\\"")
if board_name == board_vendor or board_vendor == "": if board_name == board_vendor or board_vendor == "":
output_file.write(f"CONFIG_TT_DEVICE_NAME=\"{board_name}\"\n") output_file.write(f"CONFIG_TT_DEVICE_NAME=\"{board_name}\"\n")
else: else:
@ -134,10 +133,10 @@ def write_tactility_variables(output_file, device_properties: dict, device_id: s
if device_id == "lilygo-tdeck": if device_id == "lilygo-tdeck":
output_file.write("CONFIG_TT_TDECK_WORKAROUND=y\n") output_file.write("CONFIG_TT_TDECK_WORKAROUND=y\n")
# Launcher app id # Launcher app id
launcher_app_id = get_property_or_exit(device_properties, "apps", "launcherAppId").replace("\"", "\\\"") launcher_app_id = get_property_or_exit(device_properties, "apps.launcherAppId").replace("\"", "\\\"")
output_file.write(f"CONFIG_TT_LAUNCHER_APP_ID=\"{launcher_app_id}\"\n") output_file.write(f"CONFIG_TT_LAUNCHER_APP_ID=\"{launcher_app_id}\"\n")
# Auto start app id # Auto start app id
auto_start_app_id = get_property_or_none(device_properties, "apps", "autoStartAppId") auto_start_app_id = get_property_or_none(device_properties, "apps.autoStartAppId")
if auto_start_app_id is not None: if auto_start_app_id is not None:
safe_auto_start_app_id = auto_start_app_id.replace("\"", "\\\"") safe_auto_start_app_id = auto_start_app_id.replace("\"", "\\\"")
output_file.write(f"CONFIG_TT_AUTO_START_APP_ID=\"{safe_auto_start_app_id}\"\n") output_file.write(f"CONFIG_TT_AUTO_START_APP_ID=\"{safe_auto_start_app_id}\"\n")
@ -148,7 +147,7 @@ def write_tactility_variables(output_file, device_properties: dict, device_id: s
output_file.write("CONFIG_TT_USER_DATA_LOCATION_INTERNAL=y\n") output_file.write("CONFIG_TT_USER_DATA_LOCATION_INTERNAL=y\n")
def write_core_variables(output_file, device_properties: dict): def write_core_variables(output_file, device_properties: dict):
idf_target = get_property_or_exit(device_properties, "hardware", "target").lower() idf_target = get_property_or_exit(device_properties, "hardware.target").lower()
output_file.write("# Target\n") output_file.write("# Target\n")
output_file.write(f"CONFIG_IDF_TARGET=\"{idf_target}\"\n") output_file.write(f"CONFIG_IDF_TARGET=\"{idf_target}\"\n")
output_file.write("# CPU\n") output_file.write("# CPU\n")
@ -171,28 +170,28 @@ def write_core_variables(output_file, device_properties: dict):
output_file.write("CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH=y\n") output_file.write("CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH=y\n")
output_file.write("CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH=y\n") output_file.write("CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH=y\n")
# Usage of tt::hal can be disabled to simplify dependency wiring (device depends on TactilityKernel instead of Tactility) # Usage of tt::hal can be disabled to simplify dependency wiring (device depends on TactilityKernel instead of Tactility)
use_deprecated_hal = get_property_or_none(device_properties, "dependencies", "useDeprecatedHal") use_deprecated_hal = get_property_or_none(device_properties, "dependencies.useDeprecatedHal")
if use_deprecated_hal is None or use_deprecated_hal.lower() == "true": if use_deprecated_hal is None or use_deprecated_hal.lower() == "true":
output_file.write("CONFIG_TT_USE_DEPRECATED_HAL=y\n") output_file.write("CONFIG_TT_USE_DEPRECATED_HAL=y\n")
else: else:
output_file.write("CONFIG_TT_USE_DEPRECATED_HAL=n\n") output_file.write("CONFIG_TT_USE_DEPRECATED_HAL=n\n")
def write_flash_variables(output_file, device_properties: dict): def write_flash_variables(output_file, device_properties: dict):
flash_size = get_property_or_exit(device_properties, "hardware", "flashSize") flash_size = get_property_or_exit(device_properties, "hardware.flashSize")
if not flash_size.endswith("MB"): if not flash_size.endswith("MB"):
exit_with_error("Flash size should be written as xMB or xxMB (e.g. 4MB, 16MB)") exit_with_error("Flash size should be written as xMB or xxMB (e.g. 4MB, 16MB)")
output_file.write("# Flash\n") output_file.write("# Flash\n")
flash_size_number = flash_size[:-2] flash_size_number = flash_size[:-2]
output_file.write(f"CONFIG_ESPTOOLPY_FLASHSIZE_{flash_size_number}MB=y\n") output_file.write(f"CONFIG_ESPTOOLPY_FLASHSIZE_{flash_size_number}MB=y\n")
flash_mode = get_property_or_default(device_properties, "hardware", "flashMode", 'QIO') flash_mode = get_property_or_default(device_properties, "hardware.flashMode", 'QIO')
output_file.write(f"CONFIG_FLASHMODE_{flash_mode}=y\n") output_file.write(f"CONFIG_FLASHMODE_{flash_mode}=y\n")
esptool_flash_freq = get_property_or_none(device_properties, "hardware", "esptoolFlashFreq") esptool_flash_freq = get_property_or_none(device_properties, "hardware.esptoolFlashFreq")
if esptool_flash_freq is not None: if esptool_flash_freq is not None:
output_file.write(f"CONFIG_ESPTOOLPY_FLASHFREQ_{esptool_flash_freq}=y\n") output_file.write(f"CONFIG_ESPTOOLPY_FLASHFREQ_{esptool_flash_freq}=y\n")
def write_spiram_variables(output_file, device_properties: dict): def write_spiram_variables(output_file, device_properties: dict):
idf_target = get_property_or_exit(device_properties, "hardware", "target").lower() idf_target = get_property_or_exit(device_properties, "hardware.target").lower()
has_spiram = get_property_or_exit(device_properties, "hardware", "spiRam") has_spiram = get_property_or_exit(device_properties, "hardware.spiRam")
if has_spiram != "true": if has_spiram != "true":
return return
output_file.write("# SPIRAM\n") output_file.write("# SPIRAM\n")
@ -201,7 +200,7 @@ def write_spiram_variables(output_file, device_properties: dict):
# Enable # Enable
output_file.write("CONFIG_SPIRAM=y\n") output_file.write("CONFIG_SPIRAM=y\n")
output_file.write(f"CONFIG_{idf_target.upper()}_SPIRAM_SUPPORT=y\n") output_file.write(f"CONFIG_{idf_target.upper()}_SPIRAM_SUPPORT=y\n")
mode = get_property_or_exit(device_properties, "hardware", "spiRamMode") mode = get_property_or_exit(device_properties, "hardware.spiRamMode")
if mode == "OPI": if mode == "OPI":
mode = "OCT" mode = "OCT"
# Mode # Mode
@ -209,7 +208,7 @@ def write_spiram_variables(output_file, device_properties: dict):
output_file.write(f"CONFIG_SPIRAM_MODE_{mode}=y\n") output_file.write(f"CONFIG_SPIRAM_MODE_{mode}=y\n")
else: else:
output_file.write("CONFIG_SPIRAM_TYPE_AUTO=y\n") output_file.write("CONFIG_SPIRAM_TYPE_AUTO=y\n")
speed = get_property_or_exit(device_properties, "hardware", "spiRamSpeed") speed = get_property_or_exit(device_properties, "hardware.spiRamSpeed")
# Speed # Speed
output_file.write(f"CONFIG_SPIRAM_SPEED_{speed}=y\n") output_file.write(f"CONFIG_SPIRAM_SPEED_{speed}=y\n")
output_file.write(f"CONFIG_SPIRAM_SPEED={speed}\n") output_file.write(f"CONFIG_SPIRAM_SPEED={speed}\n")
@ -223,7 +222,7 @@ def write_spiram_variables(output_file, device_properties: dict):
output_file.write("CONFIG_SPIRAM_XIP_FROM_PSRAM=y\n") output_file.write("CONFIG_SPIRAM_XIP_FROM_PSRAM=y\n")
def write_performance_improvements(output_file, device_properties: dict): def write_performance_improvements(output_file, device_properties: dict):
idf_target = get_property_or_exit(device_properties, "hardware", "target").lower() idf_target = get_property_or_exit(device_properties, "hardware.target").lower()
if idf_target == "esp32s3": if idf_target == "esp32s3":
output_file.write("# Performance improvement: Fixes glitches in the RGB display driver when rendering new screens/apps\n") output_file.write("# Performance improvement: Fixes glitches in the RGB display driver when rendering new screens/apps\n")
output_file.write("CONFIG_ESP32S3_DATA_CACHE_LINE_64B=y\n") output_file.write("CONFIG_ESP32S3_DATA_CACHE_LINE_64B=y\n")
@ -246,16 +245,16 @@ def write_lvgl_variables(output_file, device_properties: dict):
write_lvgl_variable_placeholders(output_file) write_lvgl_variable_placeholders(output_file)
return return
# LVGL DPI overrides the real DPI settings # LVGL DPI overrides the real DPI settings
dpi_text = get_property_or_none(device_properties, "lvgl", "dpi") dpi_text = get_property_or_none(device_properties, "lvgl.dpi")
if dpi_text is None: if dpi_text is None:
dpi_text = get_property_or_exit(device_properties, "display", "dpi") dpi_text = get_property_or_exit(device_properties, "display.dpi")
dpi = safe_int(dpi_text, f"DPI must be an integer, but was: '{dpi_text}'") dpi = safe_int(dpi_text, f"DPI must be an integer, but was: '{dpi_text}'")
output_file.write(f"CONFIG_LV_DPI_DEF={dpi}\n") output_file.write(f"CONFIG_LV_DPI_DEF={dpi}\n")
color_depth = get_property_or_exit(device_properties, "lvgl", "colorDepth") color_depth = get_property_or_exit(device_properties, "lvgl.colorDepth")
output_file.write(f"CONFIG_LV_COLOR_DEPTH={color_depth}\n") output_file.write(f"CONFIG_LV_COLOR_DEPTH={color_depth}\n")
output_file.write(f"CONFIG_LV_COLOR_DEPTH_{color_depth}=y\n") output_file.write(f"CONFIG_LV_COLOR_DEPTH_{color_depth}=y\n")
output_file.write("CONFIG_LV_DISP_DEF_REFR_PERIOD=10\n") output_file.write("CONFIG_LV_DISP_DEF_REFR_PERIOD=10\n")
theme = get_property_or_default(device_properties, "lvgl", "theme", "DefaultDark") theme = get_property_or_default(device_properties, "lvgl.theme", "DefaultDark")
if theme == "DefaultDark": if theme == "DefaultDark":
output_file.write("CONFIG_LV_THEME_DEFAULT_DARK=y\n") output_file.write("CONFIG_LV_THEME_DEFAULT_DARK=y\n")
elif theme == "DefaultLight": elif theme == "DefaultLight":
@ -264,7 +263,7 @@ def write_lvgl_variables(output_file, device_properties: dict):
output_file.write("CONFIG_LV_USE_THEME_MONO=y\n") output_file.write("CONFIG_LV_USE_THEME_MONO=y\n")
else: else:
exit_with_error(f"Unknown theme: {theme}") exit_with_error(f"Unknown theme: {theme}")
font_height_text = get_property_or_default(device_properties, "lvgl", "fontSize", "14") font_height_text = get_property_or_default(device_properties, "lvgl.fontSize", "14")
font_height = safe_int(font_height_text, f"Font height must be an integer, but was: '{font_height_text}'") font_height = safe_int(font_height_text, f"Font height must be an integer, but was: '{font_height_text}'")
if font_height <= 12: if font_height <= 12:
output_file.write("CONFIG_LV_FONT_MONTSERRAT_8=y\n") output_file.write("CONFIG_LV_FONT_MONTSERRAT_8=y\n")
@ -333,14 +332,23 @@ def write_lvgl_variables(output_file, device_properties: dict):
output_file.write("CONFIG_TT_LVGL_LAUNCHER_ICON_SIZE=72\n") output_file.write("CONFIG_TT_LVGL_LAUNCHER_ICON_SIZE=72\n")
output_file.write("CONFIG_TT_LVGL_SHARED_ICON_SIZE=32\n") output_file.write("CONFIG_TT_LVGL_SHARED_ICON_SIZE=32\n")
def write_touch_calibration_variables(output_file, device_properties: dict):
calibration_supported = get_property_or_none(device_properties, "touch.calibrationSupported")
if calibration_supported is not None and calibration_supported.lower() == "true":
output_file.write("# Touch calibration\n")
output_file.write("CONFIG_TT_TOUCH_CALIBRATION_SUPPORTED=y\n")
calibration_required = get_property_or_none(device_properties, "touch.calibrationRequired")
if calibration_required is not None and calibration_required.lower() == "true":
output_file.write("CONFIG_TT_TOUCH_CALIBRATION_REQUIRED=y\n")
def write_usb_variables(output_file, device_properties: dict): def write_usb_variables(output_file, device_properties: dict):
has_tiny_usb = get_boolean_property_or_false(device_properties, "hardware", "tinyUsb") has_tiny_usb = get_boolean_property_or_false(device_properties, "hardware.tinyUsb")
if has_tiny_usb: if has_tiny_usb:
output_file.write("# TinyUSB\n") output_file.write("# TinyUSB\n")
output_file.write("CONFIG_TINYUSB_MSC_ENABLED=y\n") output_file.write("CONFIG_TINYUSB_MSC_ENABLED=y\n")
output_file.write("CONFIG_TINYUSB_MSC_MOUNT_PATH=\"/sdcard\"\n") output_file.write("CONFIG_TINYUSB_MSC_MOUNT_PATH=\"/sdcard\"\n")
idf_target = get_property_or_exit(device_properties, "hardware", "target").lower() idf_target = get_property_or_exit(device_properties, "hardware.target").lower()
if idf_target == "esp32p4": if idf_target == "esp32p4":
# P4 has two USB-DWC controllers (HS/UTMI and FS/FSLS). esp_tinyusb defaults to # P4 has two USB-DWC controllers (HS/UTMI and FS/FSLS). esp_tinyusb defaults to
# RHPORT_HS (UTMI), which is the same controller claimed by usbhost0's # RHPORT_HS (UTMI), which is the same controller claimed by usbhost0's
@ -349,8 +357,8 @@ def write_usb_variables(output_file, device_properties: dict):
output_file.write("CONFIG_TINYUSB_RHPORT_FS=y\n") output_file.write("CONFIG_TINYUSB_RHPORT_FS=y\n")
def write_bluetooth_variables(output_file, device_properties: dict): def write_bluetooth_variables(output_file, device_properties: dict):
idf_target = get_property_or_exit(device_properties, "hardware", "target").lower() idf_target = get_property_or_exit(device_properties, "hardware.target").lower()
has_bluetooth = get_boolean_property_or_false(device_properties, "hardware", "bluetooth") has_bluetooth = get_boolean_property_or_false(device_properties, "hardware.bluetooth")
if has_bluetooth: if has_bluetooth:
output_file.write("# Bluetooth (NimBLE)\n") output_file.write("# Bluetooth (NimBLE)\n")
output_file.write("CONFIG_BT_ENABLED=y\n") output_file.write("CONFIG_BT_ENABLED=y\n")
@ -365,7 +373,7 @@ def write_bluetooth_variables(output_file, device_properties: dict):
# and does not suffer from the same fragmentation — enabling reliable re-init. # and does not suffer from the same fragmentation — enabling reliable re-init.
# Also frees significant internal RAM on memory-constrained targets (e.g. S3). # Also frees significant internal RAM on memory-constrained targets (e.g. S3).
# Dependency: CONFIG_SPIRAM_USE_CAPS_ALLOC || CONFIG_SPIRAM_USE_MALLOC (set by write_spiram_variables). # Dependency: CONFIG_SPIRAM_USE_CAPS_ALLOC || CONFIG_SPIRAM_USE_MALLOC (set by write_spiram_variables).
has_spiram = get_boolean_property_or_false(device_properties, "hardware", "spiRam") has_spiram = get_boolean_property_or_false(device_properties, "hardware.spiRam")
if has_spiram: if has_spiram:
output_file.write("CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_EXTERNAL=y\n") output_file.write("CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_EXTERNAL=y\n")
# Expand NimBLE's GAP device name buffer to match BLE_DEVICE_NAME_MAX. # Expand NimBLE's GAP device name buffer to match BLE_DEVICE_NAME_MAX.
@ -382,7 +390,7 @@ def write_bluetooth_variables(output_file, device_properties: dict):
output_file.write("CONFIG_BT_NIMBLE_NVS_PERSIST=y\n") output_file.write("CONFIG_BT_NIMBLE_NVS_PERSIST=y\n")
def write_usbhost_variables(output_file, device_properties: dict): def write_usbhost_variables(output_file, device_properties: dict):
has_usbhost = get_boolean_property_or_false(device_properties, "hardware", "usbHostEnabled") has_usbhost = get_boolean_property_or_false(device_properties, "hardware.usbHostEnabled")
if has_usbhost: if has_usbhost:
output_file.write("# USB Host\n") output_file.write("# USB Host\n")
output_file.write("CONFIG_FATFS_VOLUME_COUNT=6\n") output_file.write("CONFIG_FATFS_VOLUME_COUNT=6\n")
@ -416,6 +424,7 @@ def write_properties(output_file, device_properties: dict, device_id: str, is_de
write_usbhost_variables(output_file, device_properties) write_usbhost_variables(output_file, device_properties)
write_custom_sdkconfig(output_file, device_properties) write_custom_sdkconfig(output_file, device_properties)
write_lvgl_variables(output_file, device_properties) write_lvgl_variables(output_file, device_properties)
write_touch_calibration_variables(output_file, device_properties)
def get_current_sdkconfig_target(sdkconfig_path: str): def get_current_sdkconfig_target(sdkconfig_path: str):
if not os.path.isfile(sdkconfig_path): if not os.path.isfile(sdkconfig_path):
@ -448,7 +457,7 @@ def main(device_id: str, is_dev: bool):
output_file_path = "sdkconfig" output_file_path = "sdkconfig"
# Clean build dirs if target changes # Clean build dirs if target changes
device_properties = read_device_properties(device_id) device_properties = read_device_properties(device_id)
new_target = get_property_or_exit(device_properties, "hardware", "target").lower() new_target = get_property_or_exit(device_properties, "hardware.target").lower()
sdkconfig_target = get_current_sdkconfig_target(output_file_path) sdkconfig_target = get_current_sdkconfig_target(output_file_path)
clean_build_dirs_on_platform_change(sdkconfig_target, new_target) clean_build_dirs_on_platform_change(sdkconfig_target, new_target)
if os.path.isfile(output_file_path): if os.path.isfile(output_file_path):