Add calibration methods and app (#520)
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/app/App.h>
|
||||
|
||||
namespace tt::app::touchcalibration {
|
||||
|
||||
LaunchId start();
|
||||
|
||||
} // namespace tt::app::touchcalibration
|
||||
@@ -27,6 +27,8 @@ public:
|
||||
|
||||
virtual bool supportsTouchDriver() = 0;
|
||||
|
||||
virtual bool supportsCalibration() const { return false; }
|
||||
|
||||
/** Could return nullptr if not supported */
|
||||
virtual std::shared_ptr<TouchDriver> getTouchDriver() = 0;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace tt::settings::touch {
|
||||
|
||||
struct TouchCalibrationSettings {
|
||||
bool enabled = false;
|
||||
int32_t xMin = 0;
|
||||
int32_t xMax = 0;
|
||||
int32_t yMin = 0;
|
||||
int32_t yMax = 0;
|
||||
};
|
||||
|
||||
TouchCalibrationSettings getDefault();
|
||||
|
||||
bool load(TouchCalibrationSettings& settings);
|
||||
|
||||
TouchCalibrationSettings loadOrGetDefault();
|
||||
|
||||
bool save(const TouchCalibrationSettings& settings);
|
||||
|
||||
bool isValid(const TouchCalibrationSettings& settings);
|
||||
|
||||
TouchCalibrationSettings getActive();
|
||||
|
||||
void setRuntimeCalibrationEnabled(bool enabled);
|
||||
|
||||
void invalidateCache();
|
||||
|
||||
bool applyCalibration(const TouchCalibrationSettings& settings, uint16_t xMax, uint16_t yMax, uint16_t& x, uint16_t& y);
|
||||
|
||||
} // namespace tt::settings::touch
|
||||
@@ -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);
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
#endif
|
||||
|
||||
#include <Tactility/Logger.h>
|
||||
#include <Tactility/app/App.h>
|
||||
#include <Tactility/hal/display/DisplayDevice.h>
|
||||
#include <Tactility/hal/touch/TouchDevice.h>
|
||||
#include <Tactility/lvgl/Toolbar.h>
|
||||
#include <Tactility/settings/DisplaySettings.h>
|
||||
|
||||
@@ -22,6 +24,16 @@ static std::shared_ptr<hal::display::DisplayDevice> getHalDisplay() {
|
||||
return hal::findFirstDevice<hal::display::DisplayDevice>(hal::Device::Type::Display);
|
||||
}
|
||||
|
||||
static bool hasCalibratableTouchDevice() {
|
||||
auto touch_devices = hal::findDevices<hal::touch::TouchDevice>(hal::Device::Type::Touch);
|
||||
for (const auto& touch_device : touch_devices) {
|
||||
if (touch_device != nullptr && touch_device->supportsCalibration()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
class DisplayApp final : public App {
|
||||
|
||||
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 @@ public:
|
||||
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 {
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
#include <Tactility/Tactility.h>
|
||||
|
||||
#include <Tactility/app/touchcalibration/TouchCalibration.h>
|
||||
|
||||
#include <Tactility/Logger.h>
|
||||
#include <Tactility/settings/TouchCalibrationSettings.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <lvgl.h>
|
||||
|
||||
namespace tt::app::touchcalibration {
|
||||
|
||||
static const auto LOGGER = Logger("TouchCalibration");
|
||||
|
||||
extern const AppManifest manifest;
|
||||
|
||||
LaunchId start() {
|
||||
return app::start(manifest.appId);
|
||||
}
|
||||
|
||||
class TouchCalibrationApp final : public App {
|
||||
|
||||
static constexpr int32_t TARGET_MARGIN = 24;
|
||||
|
||||
struct Sample {
|
||||
uint16_t x;
|
||||
uint16_t y;
|
||||
};
|
||||
|
||||
Sample samples[4] = {};
|
||||
uint8_t sampleCount = 0;
|
||||
|
||||
lv_obj_t* root = nullptr;
|
||||
lv_obj_t* target = nullptr;
|
||||
lv_obj_t* titleLabel = nullptr;
|
||||
lv_obj_t* hintLabel = nullptr;
|
||||
|
||||
static void onPress(lv_event_t* event) {
|
||||
auto* self = static_cast<TouchCalibrationApp*>(lv_event_get_user_data(event));
|
||||
if (self != nullptr) {
|
||||
self->onPressInternal(event);
|
||||
}
|
||||
}
|
||||
|
||||
static lv_point_t getTargetPoint(uint8_t index, lv_coord_t width, lv_coord_t height) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return {.x = TARGET_MARGIN, .y = TARGET_MARGIN};
|
||||
case 1:
|
||||
return {.x = width - TARGET_MARGIN, .y = TARGET_MARGIN};
|
||||
case 2:
|
||||
return {.x = width - TARGET_MARGIN, .y = height - TARGET_MARGIN};
|
||||
default:
|
||||
return {.x = TARGET_MARGIN, .y = height - TARGET_MARGIN};
|
||||
}
|
||||
}
|
||||
|
||||
void updateUi() {
|
||||
if (target == nullptr || root == nullptr || titleLabel == nullptr || hintLabel == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto width = lv_obj_get_content_width(root);
|
||||
const auto height = lv_obj_get_content_height(root);
|
||||
|
||||
if (sampleCount < 4) {
|
||||
const auto point = getTargetPoint(sampleCount, width, height);
|
||||
lv_obj_set_pos(target, point.x - 14, point.y - 14);
|
||||
lv_label_set_text(titleLabel, "Touchscreen Calibration");
|
||||
lv_label_set_text_fmt(hintLabel, "Tap target %u/4", static_cast<unsigned>(sampleCount + 1));
|
||||
}
|
||||
}
|
||||
|
||||
void finishCalibration() {
|
||||
constexpr int32_t MIN_RANGE = 20;
|
||||
|
||||
const int32_t xMin = (static_cast<int32_t>(samples[0].x) + static_cast<int32_t>(samples[3].x)) / 2;
|
||||
const int32_t xMax = (static_cast<int32_t>(samples[1].x) + static_cast<int32_t>(samples[2].x)) / 2;
|
||||
const int32_t yMin = (static_cast<int32_t>(samples[0].y) + static_cast<int32_t>(samples[1].y)) / 2;
|
||||
const int32_t yMax = (static_cast<int32_t>(samples[2].y) + static_cast<int32_t>(samples[3].y)) / 2;
|
||||
|
||||
settings::touch::TouchCalibrationSettings settings = settings::touch::getDefault();
|
||||
settings.enabled = true;
|
||||
settings.xMin = xMin;
|
||||
settings.xMax = xMax;
|
||||
settings.yMin = yMin;
|
||||
settings.yMax = yMax;
|
||||
|
||||
if ((xMax - xMin) < MIN_RANGE || (yMax - yMin) < MIN_RANGE || !settings::touch::isValid(settings)) {
|
||||
lv_label_set_text(titleLabel, "Calibration Failed");
|
||||
lv_label_set_text(hintLabel, "Range invalid. Tap to close.");
|
||||
lv_obj_add_flag(target, LV_OBJ_FLAG_HIDDEN);
|
||||
setResult(Result::Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!settings::touch::save(settings)) {
|
||||
lv_label_set_text(titleLabel, "Calibration Failed");
|
||||
lv_label_set_text(hintLabel, "Unable to save settings. Tap to close.");
|
||||
lv_obj_add_flag(target, LV_OBJ_FLAG_HIDDEN);
|
||||
setResult(Result::Error);
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.info("Saved calibration x=[{}, {}] y=[{}, {}]", xMin, xMax, yMin, yMax);
|
||||
lv_label_set_text(titleLabel, "Calibration Complete");
|
||||
lv_label_set_text(hintLabel, "Touch anywhere to continue.");
|
||||
lv_obj_add_flag(target, LV_OBJ_FLAG_HIDDEN);
|
||||
setResult(Result::Ok);
|
||||
}
|
||||
|
||||
void onPressInternal(lv_event_t* event) {
|
||||
auto* indev = lv_event_get_indev(event);
|
||||
if (indev == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
lv_point_t point = {0, 0};
|
||||
lv_indev_get_point(indev, &point);
|
||||
|
||||
if (sampleCount < 4) {
|
||||
samples[sampleCount] = {
|
||||
.x = static_cast<uint16_t>(std::max(static_cast<lv_coord_t>(0), point.x)),
|
||||
.y = static_cast<uint16_t>(std::max(static_cast<lv_coord_t>(0), point.y)),
|
||||
};
|
||||
sampleCount++;
|
||||
|
||||
if (sampleCount < 4) {
|
||||
updateUi();
|
||||
} else {
|
||||
finishCalibration();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
stop(manifest.appId);
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
void onCreate(AppContext& app) override {
|
||||
(void)app;
|
||||
settings::touch::setRuntimeCalibrationEnabled(false);
|
||||
settings::touch::invalidateCache();
|
||||
}
|
||||
|
||||
void onDestroy(AppContext& app) override {
|
||||
(void)app;
|
||||
settings::touch::setRuntimeCalibrationEnabled(true);
|
||||
settings::touch::invalidateCache();
|
||||
}
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
(void)app;
|
||||
|
||||
lv_obj_set_style_bg_color(parent, lv_color_black(), LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_opa(parent, LV_OPA_COVER, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(parent, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_radius(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
root = lv_obj_create(parent);
|
||||
lv_obj_set_size(root, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_bg_opa(root, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(root, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_all(root, 0, LV_STATE_DEFAULT);
|
||||
|
||||
titleLabel = lv_label_create(root);
|
||||
lv_obj_align(titleLabel, LV_ALIGN_TOP_MID, 0, 14);
|
||||
lv_obj_set_style_text_color(titleLabel, lv_color_white(), LV_STATE_DEFAULT);
|
||||
lv_label_set_text(titleLabel, "Touchscreen Calibration");
|
||||
|
||||
hintLabel = lv_label_create(root);
|
||||
lv_obj_align(hintLabel, LV_ALIGN_BOTTOM_MID, 0, -14);
|
||||
lv_obj_set_style_text_color(hintLabel, lv_color_white(), LV_STATE_DEFAULT);
|
||||
lv_label_set_text(hintLabel, "Tap target 1/4");
|
||||
|
||||
target = lv_button_create(root);
|
||||
lv_obj_set_size(target, 28, 28);
|
||||
lv_obj_set_style_radius(target, LV_RADIUS_CIRCLE, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_color(target, lv_palette_main(LV_PALETTE_RED), LV_STATE_DEFAULT);
|
||||
// Ensure root receives all presses for sampling.
|
||||
lv_obj_remove_flag(target, LV_OBJ_FLAG_CLICKABLE);
|
||||
|
||||
auto* targetLabel = lv_label_create(target);
|
||||
lv_label_set_text(targetLabel, "+");
|
||||
lv_obj_center(targetLabel);
|
||||
|
||||
lv_obj_add_flag(root, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_add_event_cb(root, onPress, LV_EVENT_PRESSED, this);
|
||||
|
||||
updateUi();
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.appId = "TouchCalibration",
|
||||
.appName = "Touch Calibration",
|
||||
.appCategory = Category::System,
|
||||
.appFlags = AppManifest::Flags::HideStatusBar | AppManifest::Flags::Hidden,
|
||||
.createApp = create<TouchCalibrationApp>
|
||||
};
|
||||
|
||||
} // namespace tt::app::touchcalibration
|
||||
@@ -0,0 +1,173 @@
|
||||
#include <Tactility/settings/TouchCalibrationSettings.h>
|
||||
|
||||
#include <Tactility/file/PropertiesFile.h>
|
||||
#include <Tactility/Mutex.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cerrno>
|
||||
#include <climits>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace tt::settings::touch {
|
||||
|
||||
constexpr auto* SETTINGS_FILE = "/data/settings/touch-calibration.properties";
|
||||
constexpr auto* SETTINGS_KEY_ENABLED = "enabled";
|
||||
constexpr auto* SETTINGS_KEY_X_MIN = "xMin";
|
||||
constexpr auto* SETTINGS_KEY_X_MAX = "xMax";
|
||||
constexpr auto* SETTINGS_KEY_Y_MIN = "yMin";
|
||||
constexpr auto* SETTINGS_KEY_Y_MAX = "yMax";
|
||||
|
||||
static bool runtimeCalibrationEnabled = true;
|
||||
static bool cacheInitialized = false;
|
||||
static TouchCalibrationSettings cachedSettings;
|
||||
static tt::Mutex cacheMutex;
|
||||
|
||||
static bool toBool(const std::string& value) {
|
||||
return value == "1" || value == "true" || value == "True";
|
||||
}
|
||||
|
||||
static bool parseInt32(const std::string& value, int32_t& out) {
|
||||
errno = 0;
|
||||
char* end_ptr = nullptr;
|
||||
const long parsed = std::strtol(value.c_str(), &end_ptr, 10);
|
||||
if (errno != 0 || end_ptr == value.c_str() || *end_ptr != '\0') {
|
||||
return false;
|
||||
}
|
||||
if (parsed < INT32_MIN || parsed > INT32_MAX) {
|
||||
return false;
|
||||
}
|
||||
out = static_cast<int32_t>(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
TouchCalibrationSettings getDefault() {
|
||||
return {
|
||||
.enabled = false,
|
||||
.xMin = 0,
|
||||
.xMax = 0,
|
||||
.yMin = 0,
|
||||
.yMax = 0,
|
||||
};
|
||||
}
|
||||
|
||||
bool isValid(const TouchCalibrationSettings& settings) {
|
||||
constexpr auto MIN_RANGE = 20;
|
||||
return settings.xMax > settings.xMin && settings.yMax > settings.yMin &&
|
||||
(settings.xMax - settings.xMin) >= MIN_RANGE &&
|
||||
(settings.yMax - settings.yMin) >= MIN_RANGE;
|
||||
}
|
||||
|
||||
bool load(TouchCalibrationSettings& settings) {
|
||||
std::map<std::string, std::string> map;
|
||||
if (!file::loadPropertiesFile(SETTINGS_FILE, map)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto enabled_it = map.find(SETTINGS_KEY_ENABLED);
|
||||
auto x_min_it = map.find(SETTINGS_KEY_X_MIN);
|
||||
auto x_max_it = map.find(SETTINGS_KEY_X_MAX);
|
||||
auto y_min_it = map.find(SETTINGS_KEY_Y_MIN);
|
||||
auto y_max_it = map.find(SETTINGS_KEY_Y_MAX);
|
||||
|
||||
if (enabled_it == map.end() || x_min_it == map.end() || x_max_it == map.end() || y_min_it == map.end() || y_max_it == map.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
TouchCalibrationSettings loaded = getDefault();
|
||||
loaded.enabled = toBool(enabled_it->second);
|
||||
if (!parseInt32(x_min_it->second, loaded.xMin) ||
|
||||
!parseInt32(x_max_it->second, loaded.xMax) ||
|
||||
!parseInt32(y_min_it->second, loaded.yMin) ||
|
||||
!parseInt32(y_max_it->second, loaded.yMax)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (loaded.enabled && !isValid(loaded)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
settings = loaded;
|
||||
return true;
|
||||
}
|
||||
|
||||
TouchCalibrationSettings loadOrGetDefault() {
|
||||
TouchCalibrationSettings settings;
|
||||
if (!load(settings)) {
|
||||
settings = getDefault();
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
bool save(const TouchCalibrationSettings& settings) {
|
||||
if (settings.enabled && !isValid(settings)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> map;
|
||||
map[SETTINGS_KEY_ENABLED] = settings.enabled ? "1" : "0";
|
||||
map[SETTINGS_KEY_X_MIN] = std::to_string(settings.xMin);
|
||||
map[SETTINGS_KEY_X_MAX] = std::to_string(settings.xMax);
|
||||
map[SETTINGS_KEY_Y_MIN] = std::to_string(settings.yMin);
|
||||
map[SETTINGS_KEY_Y_MAX] = std::to_string(settings.yMax);
|
||||
|
||||
if (!file::savePropertiesFile(SETTINGS_FILE, map)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto lock = cacheMutex.asScopedLock();
|
||||
lock.lock();
|
||||
cachedSettings = settings;
|
||||
cacheInitialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
TouchCalibrationSettings getActive() {
|
||||
auto lock = cacheMutex.asScopedLock();
|
||||
lock.lock();
|
||||
if (!cacheInitialized) {
|
||||
cachedSettings = loadOrGetDefault();
|
||||
cacheInitialized = true;
|
||||
}
|
||||
if (!runtimeCalibrationEnabled) {
|
||||
auto disabled = cachedSettings;
|
||||
disabled.enabled = false;
|
||||
return disabled;
|
||||
}
|
||||
return cachedSettings;
|
||||
}
|
||||
|
||||
void setRuntimeCalibrationEnabled(bool enabled) {
|
||||
auto lock = cacheMutex.asScopedLock();
|
||||
lock.lock();
|
||||
runtimeCalibrationEnabled = enabled;
|
||||
}
|
||||
|
||||
void invalidateCache() {
|
||||
auto lock = cacheMutex.asScopedLock();
|
||||
lock.lock();
|
||||
cacheInitialized = false;
|
||||
}
|
||||
|
||||
bool applyCalibration(const TouchCalibrationSettings& settings, uint16_t xMax, uint16_t yMax, uint16_t& x, uint16_t& y) {
|
||||
if (!settings.enabled || !isValid(settings)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int32_t in_x = static_cast<int32_t>(x);
|
||||
const int32_t in_y = static_cast<int32_t>(y);
|
||||
|
||||
const int64_t mapped_x = (static_cast<int64_t>(in_x) - static_cast<int64_t>(settings.xMin)) *
|
||||
static_cast<int64_t>(xMax) /
|
||||
(static_cast<int64_t>(settings.xMax) - static_cast<int64_t>(settings.xMin));
|
||||
const int64_t mapped_y = (static_cast<int64_t>(in_y) - static_cast<int64_t>(settings.yMin)) *
|
||||
static_cast<int64_t>(yMax) /
|
||||
(static_cast<int64_t>(settings.yMax) - static_cast<int64_t>(settings.yMin));
|
||||
|
||||
x = static_cast<uint16_t>(std::clamp<int64_t>(mapped_x, 0, static_cast<int64_t>(xMax)));
|
||||
y = static_cast<uint16_t>(std::clamp<int64_t>(mapped_y, 0, static_cast<int64_t>(yMax)));
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace tt::settings::touch
|
||||
Reference in New Issue
Block a user