e7fb5fb38f
- RLCD device.properties: DefaultDark (focus UI visible), fontSize 18
(+28% vs 14: small 10->14, default 14->18, large 18->24, icons 16->20/36->48)
- ST7305 driver: honor invertColor config (was ignored), call
esp_lcd_panel_invert_color after init
- DisplayDevice: add supportsInvertColor + set/get, and
supportsMonochromeThreshold/set/get (ST7305 only)
- EspLcdDisplay: implement invert via panelHandle, and mono threshold via
global g_mono_threshold shared with esp_lvgl_port
- DisplaySettings: new fields invertColor (default true to preserve current
inverted look) and monochromeThreshold (default 60 best for DefaultDark per
user testing, range 20-230)
- Lvgl.cpp: apply invert + threshold at boot from settings
- Display app: add Invert colors switch (if supportsInvertColor) + Mono
threshold slider with live preview and persistence
- esp_lvgl_port_disp.c (components override for hash mismatch): fix B/W
conversion
BEFORE: blue >16 only -> crushed colors, no gray handling
AFTER: proper luminance Y=0.299R+0.587G+0.114B with adjustable threshold
(default 60), no Bayer dithering dots. Crisp text on reflective
- Launcher/button/list wrappers reverted to stock DefaultDark (no white
focus hacks) per user request - focus UI now uses default outline which
survives luminance threshold
Verified on waveshare-esp32-s3-rlcd /dev/cu.usbmodem1101:
- Build 2877200 bytes, flash hash verified
- Invert toggle works, threshold slider live (60 best per test)
- Font bump visible, focus UI visible for button navigation
- No dithering dots
Docs: LVGL mono https://docs.lvgl.io/9.2/porting/display.html#monochrome-displays
Themes: https://docs.lvgl.io/9.2/details/common-widget-features/styles/themes.html
Co-authored-by: Hermes Agent <hermes@noreply>
286 lines
10 KiB
C++
286 lines
10 KiB
C++
#include <Tactility/settings/DisplaySettings.h>
|
|
|
|
#include <Tactility/file/File.h>
|
|
#include <Tactility/file/PropertiesFile.h>
|
|
#include <Tactility/Paths.h>
|
|
#include <tactility/hal/Device.h>
|
|
#include <Tactility/hal/display/DisplayDevice.h>
|
|
|
|
#include <map>
|
|
#include <string>
|
|
#include <utility>
|
|
|
|
namespace tt::settings::display {
|
|
|
|
static std::string getSettingsFilePath() {
|
|
return getUserDataPath() + "/settings/display.properties";
|
|
}
|
|
|
|
constexpr auto* SETTINGS_KEY_ORIENTATION = "orientation";
|
|
constexpr auto* SETTINGS_KEY_GAMMA_CURVE = "gammaCurve";
|
|
constexpr auto* SETTINGS_KEY_BACKLIGHT_DUTY = "backlightDuty";
|
|
constexpr auto* SETTINGS_KEY_TIMEOUT_ENABLED = "backlightTimeoutEnabled";
|
|
constexpr auto* SETTINGS_KEY_TIMEOUT_MS = "backlightTimeoutMs";
|
|
constexpr auto* SETTINGS_KEY_SCREENSAVER_TYPE = "screensaverType";
|
|
constexpr auto* SETTINGS_KEY_DISABLE_WHEN_CHARGING = "disableScreensaverWhenCharging";
|
|
constexpr auto* SETTINGS_KEY_INVERT_COLOR = "invertColor";
|
|
constexpr auto* SETTINGS_KEY_MONO_THRESHOLD = "monochromeThreshold";
|
|
|
|
static Orientation getDefaultOrientation() {
|
|
auto* display = lv_display_get_default();
|
|
if (display == nullptr) {
|
|
return Orientation::Landscape;
|
|
}
|
|
|
|
if (lv_display_get_physical_horizontal_resolution(display) > lv_display_get_physical_vertical_resolution(display)) {
|
|
return Orientation::Landscape;
|
|
} else {
|
|
return Orientation::Portrait;
|
|
}
|
|
}
|
|
|
|
static std::string toString(Orientation orientation) {
|
|
switch (orientation) {
|
|
using enum Orientation;
|
|
case Portrait:
|
|
return "Portrait";
|
|
case Landscape:
|
|
return "Landscape";
|
|
case PortraitFlipped:
|
|
return "PortraitFlipped";
|
|
case LandscapeFlipped:
|
|
return "LandscapeFlipped";
|
|
default:
|
|
std::unreachable();
|
|
}
|
|
}
|
|
|
|
static bool fromString(const std::string& str, Orientation& orientation) {
|
|
if (str == "Portrait") {
|
|
orientation = Orientation::Portrait;
|
|
return true;
|
|
} else if (str == "Landscape") {
|
|
orientation = Orientation::Landscape;
|
|
return true;
|
|
} else if (str == "PortraitFlipped") {
|
|
orientation = Orientation::PortraitFlipped;
|
|
return true;
|
|
} else if (str == "LandscapeFlipped") {
|
|
orientation = Orientation::LandscapeFlipped;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static std::string toString(ScreensaverType type) {
|
|
switch (type) {
|
|
using enum ScreensaverType;
|
|
case None:
|
|
return "None";
|
|
case BouncingBalls:
|
|
return "BouncingBalls";
|
|
case Mystify:
|
|
return "Mystify";
|
|
case MatrixRain:
|
|
return "MatrixRain";
|
|
case StackChan:
|
|
return "StackChan";
|
|
case McpScreen:
|
|
return "McpScreen";
|
|
default:
|
|
std::unreachable();
|
|
}
|
|
}
|
|
|
|
static bool fromString(const std::string& str, ScreensaverType& type) {
|
|
if (str == "None") {
|
|
type = ScreensaverType::None;
|
|
return true;
|
|
} else if (str == "BouncingBalls") {
|
|
type = ScreensaverType::BouncingBalls;
|
|
return true;
|
|
} else if (str == "Mystify") {
|
|
type = ScreensaverType::Mystify;
|
|
return true;
|
|
} else if (str == "MatrixRain") {
|
|
type = ScreensaverType::MatrixRain;
|
|
return true;
|
|
} else if (str == "StackChan") {
|
|
type = ScreensaverType::StackChan;
|
|
return true;
|
|
} else if (str == "McpScreen") {
|
|
type = ScreensaverType::McpScreen;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool load(DisplaySettings& settings) {
|
|
auto settings_path = getSettingsFilePath();
|
|
if (!file::isFile(settings_path)) {
|
|
return false;
|
|
}
|
|
|
|
std::map<std::string, std::string> map;
|
|
if (!file::loadPropertiesFile(settings_path, map)) {
|
|
return false;
|
|
}
|
|
|
|
auto orientation_entry = map.find(SETTINGS_KEY_ORIENTATION);
|
|
Orientation orientation;
|
|
if (orientation_entry == map.end() || !fromString(orientation_entry->second, orientation)) {
|
|
orientation = getDefaultOrientation();
|
|
}
|
|
|
|
auto gamma_entry = map.find(SETTINGS_KEY_GAMMA_CURVE);
|
|
int gamma_curve = 0;
|
|
if (gamma_entry != map.end()) {
|
|
gamma_curve = atoi(gamma_entry->second.c_str());
|
|
}
|
|
|
|
auto backlight_duty_entry = map.find(SETTINGS_KEY_BACKLIGHT_DUTY);
|
|
int backlight_duty = 200; // default
|
|
if (backlight_duty_entry != map.end()) {
|
|
backlight_duty = atoi(backlight_duty_entry->second.c_str());
|
|
if (backlight_duty_entry->second != "0" && backlight_duty == 0) {
|
|
backlight_duty = 200;
|
|
}
|
|
}
|
|
|
|
bool timeout_enabled = false;
|
|
auto timeout_enabled_entry = map.find(SETTINGS_KEY_TIMEOUT_ENABLED);
|
|
if (timeout_enabled_entry != map.end()) {
|
|
timeout_enabled = (timeout_enabled_entry->second == "1" || timeout_enabled_entry->second == "true" || timeout_enabled_entry->second == "True");
|
|
}
|
|
|
|
uint32_t timeout_ms = 60000; // default 60s
|
|
auto timeout_ms_entry = map.find(SETTINGS_KEY_TIMEOUT_MS);
|
|
if (timeout_ms_entry != map.end()) {
|
|
timeout_ms = static_cast<uint32_t>(std::strtoul(timeout_ms_entry->second.c_str(), nullptr, 10));
|
|
}
|
|
|
|
auto screensaver_entry = map.find(SETTINGS_KEY_SCREENSAVER_TYPE);
|
|
ScreensaverType screensaver_type = ScreensaverType::BouncingBalls;
|
|
if (screensaver_entry != map.end()) {
|
|
fromString(screensaver_entry->second, screensaver_type);
|
|
}
|
|
|
|
bool disable_when_charging = false;
|
|
auto disable_charging_entry = map.find(SETTINGS_KEY_DISABLE_WHEN_CHARGING);
|
|
if (disable_charging_entry != map.end()) {
|
|
disable_when_charging = (disable_charging_entry->second == "1" || disable_charging_entry->second == "true" || disable_charging_entry->second == "True");
|
|
}
|
|
|
|
bool invert_color = false;
|
|
auto invert_entry = map.find(SETTINGS_KEY_INVERT_COLOR);
|
|
if (invert_entry != map.end()) {
|
|
invert_color = (invert_entry->second == "1" || invert_entry->second == "true" || invert_entry->second == "True");
|
|
}
|
|
|
|
int mono_thresh = 60;
|
|
auto mono_entry = map.find(SETTINGS_KEY_MONO_THRESHOLD);
|
|
if (mono_entry != map.end()) {
|
|
mono_thresh = atoi(mono_entry->second.c_str());
|
|
if (mono_thresh < 1) mono_thresh = 1;
|
|
if (mono_thresh > 255) mono_thresh = 255;
|
|
}
|
|
|
|
settings.orientation = orientation;
|
|
settings.gammaCurve = gamma_curve;
|
|
settings.backlightDuty = backlight_duty;
|
|
settings.backlightTimeoutEnabled = timeout_enabled;
|
|
settings.backlightTimeoutMs = timeout_ms;
|
|
settings.screensaverType = screensaver_type;
|
|
settings.disableScreensaverWhenCharging = disable_when_charging;
|
|
settings.invertColor = invert_color;
|
|
settings.monochromeThreshold = static_cast<uint8_t>(mono_thresh);
|
|
|
|
return true;
|
|
}
|
|
|
|
DisplaySettings getDefault() {
|
|
return DisplaySettings {
|
|
.orientation = getDefaultOrientation(),
|
|
.gammaCurve = 1,
|
|
.backlightDuty = 200,
|
|
.backlightTimeoutEnabled = false,
|
|
.backlightTimeoutMs = 60000,
|
|
.screensaverType = ScreensaverType::BouncingBalls,
|
|
.disableScreensaverWhenCharging = false,
|
|
.invertColor = true,
|
|
.monochromeThreshold = 60
|
|
};
|
|
}
|
|
|
|
DisplaySettings loadOrGetDefault() {
|
|
DisplaySettings settings;
|
|
if (!load(settings)) {
|
|
settings = getDefault();
|
|
}
|
|
return settings;
|
|
}
|
|
|
|
bool save(const DisplaySettings& settings) {
|
|
std::map<std::string, std::string> map;
|
|
map[SETTINGS_KEY_BACKLIGHT_DUTY] = std::to_string(settings.backlightDuty);
|
|
map[SETTINGS_KEY_GAMMA_CURVE] = std::to_string(settings.gammaCurve);
|
|
map[SETTINGS_KEY_ORIENTATION] = toString(settings.orientation);
|
|
map[SETTINGS_KEY_TIMEOUT_ENABLED] = settings.backlightTimeoutEnabled ? "1" : "0";
|
|
map[SETTINGS_KEY_TIMEOUT_MS] = std::to_string(settings.backlightTimeoutMs);
|
|
map[SETTINGS_KEY_SCREENSAVER_TYPE] = toString(settings.screensaverType);
|
|
map[SETTINGS_KEY_DISABLE_WHEN_CHARGING] = settings.disableScreensaverWhenCharging ? "1" : "0";
|
|
map[SETTINGS_KEY_INVERT_COLOR] = settings.invertColor ? "1" : "0";
|
|
map[SETTINGS_KEY_MONO_THRESHOLD] = std::to_string(settings.monochromeThreshold);
|
|
auto settings_path = getSettingsFilePath();
|
|
if (!file::findOrCreateParentDirectory(settings_path, 0755)) {
|
|
return false;
|
|
}
|
|
return file::savePropertiesFile(settings_path, map);
|
|
}
|
|
|
|
lv_display_rotation_t toLvglDisplayRotation(Orientation orientation) {
|
|
auto* lvgl_display = lv_display_get_default();
|
|
auto rotation = lv_display_get_rotation(lvgl_display);
|
|
bool is_originally_landscape;
|
|
// The lvgl resolution code compensates for rotation. We have to revert the compensation to get the real display resolution
|
|
// TODO: Use info from display driver
|
|
if (rotation == LV_DISPLAY_ROTATION_0 || rotation == LV_DISPLAY_ROTATION_180) {
|
|
is_originally_landscape = lv_display_get_physical_horizontal_resolution(lvgl_display) > lv_display_get_physical_vertical_resolution(lvgl_display);
|
|
} else {
|
|
is_originally_landscape = lv_display_get_physical_horizontal_resolution(lvgl_display) < lv_display_get_physical_vertical_resolution(lvgl_display);
|
|
}
|
|
if (is_originally_landscape) {
|
|
// Landscape display
|
|
switch (orientation) {
|
|
case Orientation::Landscape:
|
|
return LV_DISPLAY_ROTATION_0;
|
|
case Orientation::Portrait:
|
|
return LV_DISPLAY_ROTATION_90;
|
|
case Orientation::LandscapeFlipped:
|
|
return LV_DISPLAY_ROTATION_180;
|
|
case Orientation::PortraitFlipped:
|
|
return LV_DISPLAY_ROTATION_270;
|
|
default:
|
|
return LV_DISPLAY_ROTATION_0;
|
|
}
|
|
} else {
|
|
// Portrait display
|
|
switch (orientation) {
|
|
case Orientation::Landscape:
|
|
return LV_DISPLAY_ROTATION_90;
|
|
case Orientation::Portrait:
|
|
return LV_DISPLAY_ROTATION_0;
|
|
case Orientation::LandscapeFlipped:
|
|
return LV_DISPLAY_ROTATION_270;
|
|
case Orientation::PortraitFlipped:
|
|
return LV_DISPLAY_ROTATION_180;
|
|
default:
|
|
return LV_DISPLAY_ROTATION_0;
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace
|