feat(rlcd): invert toggle, mono threshold, font bump, B/W luminance

- 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>
This commit is contained in:
Adolfo Reyna
2026-07-12 15:27:08 -04:00
parent 78382d6deb
commit e7fb5fb38f
123 changed files with 16637 additions and 36 deletions
+95
View File
@@ -120,6 +120,42 @@ class DisplayApp final : public App {
app->displaySettingsUpdated = true;
}
static void onInvertColorChanged(lv_event_t* event) {
auto* app = static_cast<DisplayApp*>(lv_event_get_user_data(event));
auto* sw = static_cast<lv_obj_t*>(lv_event_get_target(event));
bool enabled = lv_obj_has_state(sw, LV_STATE_CHECKED);
app->displaySettings.invertColor = enabled;
app->displaySettingsUpdated = true;
// Apply immediately
auto hal_display = getHalDisplay();
if (hal_display && hal_display->supportsInvertColor()) {
hal_display->setInvertColor(enabled);
}
}
static void onMonoThresholdChanged(lv_event_t* event) {
auto* slider = static_cast<lv_obj_t*>(lv_event_get_target(event));
auto* app = static_cast<DisplayApp*>(lv_event_get_user_data(event));
int32_t val = lv_slider_get_value(slider);
if (val < 1) val = 1;
if (val > 255) val = 255;
app->displaySettings.monochromeThreshold = static_cast<uint8_t>(val);
app->displaySettingsUpdated = true;
auto hal_display = getHalDisplay();
if (hal_display && hal_display->supportsMonochromeThreshold()) {
hal_display->setMonochromeThreshold(static_cast<uint8_t>(val));
}
}
static void onMonoThresholdChanging(lv_event_t* event) {
auto* slider = static_cast<lv_obj_t*>(lv_event_get_target(event));
auto* value_label = static_cast<lv_obj_t*>(lv_event_get_user_data(event));
if (value_label) {
int32_t v = lv_slider_get_value(slider);
lv_label_set_text_fmt(value_label, "%d", (int)v);
}
}
static void onTimeoutChanged(lv_event_t* event) {
auto* app = static_cast<DisplayApp*>(lv_event_get_user_data(event));
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
@@ -332,6 +368,65 @@ public:
}
}
// Invert color toggle (if display supports it - e.g. RLCD ST7305)
{
auto hal_disp = getHalDisplay();
if (hal_disp && hal_disp->supportsInvertColor()) {
auto* invert_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(invert_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(invert_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(invert_wrapper, 0, LV_STATE_DEFAULT);
auto* invert_label = lv_label_create(invert_wrapper);
lv_label_set_text(invert_label, "Invert colors");
lv_obj_align(invert_label, LV_ALIGN_LEFT_MID, 0, 0);
auto* invert_switch = lv_switch_create(invert_wrapper);
if (displaySettings.invertColor) {
lv_obj_add_state(invert_switch, LV_STATE_CHECKED);
}
lv_obj_align(invert_switch, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_event_cb(invert_switch, onInvertColorChanged, LV_EVENT_VALUE_CHANGED, this);
}
}
// Monochrome threshold slider (ST7305 reflective) - adjustable B/W cut
{
auto hal_disp = getHalDisplay();
if (hal_disp && hal_disp->supportsMonochromeThreshold()) {
auto* thresh_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(thresh_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(thresh_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(thresh_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_flex_flow(thresh_wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(thresh_wrapper, 2, LV_STATE_DEFAULT);
auto* header_row = lv_obj_create(thresh_wrapper);
lv_obj_set_size(header_row, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(header_row, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(header_row, 0, LV_STATE_DEFAULT);
auto* thresh_label = lv_label_create(header_row);
lv_label_set_text(thresh_label, "Mono threshold");
lv_obj_align(thresh_label, LV_ALIGN_LEFT_MID, 0, 0);
auto* value_label = lv_label_create(header_row);
lv_label_set_text_fmt(value_label, "%d", displaySettings.monochromeThreshold);
lv_obj_align(value_label, LV_ALIGN_RIGHT_MID, 0, 0);
auto* thresh_slider = lv_slider_create(thresh_wrapper);
lv_obj_set_width(thresh_slider, LV_PCT(100));
lv_slider_set_range(thresh_slider, 20, 230); // avoid extremes that make screen all black/white
lv_slider_set_value(thresh_slider, displaySettings.monochromeThreshold, LV_ANIM_OFF);
// Live label update: pass value_label as user_data
lv_obj_add_event_cb(thresh_slider, onMonoThresholdChanging, LV_EVENT_VALUE_CHANGED, value_label);
// Persist + apply: pass app as user_data
lv_obj_add_event_cb(thresh_slider, onMonoThresholdChanged, LV_EVENT_VALUE_CHANGED, this);
// Apply initial threshold live
hal_disp->setMonochromeThreshold(displaySettings.monochromeThreshold);
}
}
if (hasCalibratableTouchDevice()) {
auto* calibrate_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(calibrate_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
@@ -49,11 +49,6 @@ class LauncherApp final : public App {
lv_obj_set_style_shadow_width(apps_button, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(apps_button, 0, LV_STATE_DEFAULT);
// Add a high-contrast focus outline for monochrome / RLCD screens
lv_obj_set_style_outline_width(apps_button, 2, LV_STATE_FOCUSED);
lv_obj_set_style_outline_pad(apps_button, 2, LV_STATE_FOCUSED);
lv_obj_set_style_outline_color(apps_button, lv_theme_get_color_primary(apps_button), LV_STATE_FOCUSED);
auto* default_group = lv_group_get_default();
if (default_group) {
lv_group_add_obj(default_group, apps_button);
@@ -223,11 +218,6 @@ public:
lv_obj_set_style_shadow_width(power_button, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(power_button, 0, LV_PART_MAIN);
// Add high-contrast focus outline for monochrome / RLCD screens
lv_obj_set_style_outline_width(power_button, 2, LV_STATE_FOCUSED);
lv_obj_set_style_outline_pad(power_button, 2, LV_STATE_FOCUSED);
lv_obj_set_style_outline_color(power_button, lv_theme_get_color_primary(power_button), LV_STATE_FOCUSED);
auto* default_group = lv_group_get_default();
if (default_group) {
lv_group_add_obj(default_group, power_button);
+10
View File
@@ -44,6 +44,16 @@ void attachDevices() {
if (rotation != lv_display_get_rotation(lvgl_display)) {
lv_display_set_rotation(lvgl_display, rotation);
}
// Apply invert setting if display supports it
if (display->supportsInvertColor()) {
display->setInvertColor(settings.invertColor);
LOGGER.info("InvertColor set to {} for {}", settings.invertColor ? "true" : "false", display->getName());
}
// Apply monochrome threshold for ST7305 reflective
if (display->supportsMonochromeThreshold()) {
display->setMonochromeThreshold(settings.monochromeThreshold);
LOGGER.info("MonoThreshold set to {} for {}", settings.monochromeThreshold, display->getName());
}
} else {
LOGGER.error("Start failed for {}", display->getName());
}
+1 -3
View File
@@ -1,9 +1,7 @@
#ifdef ESP_PLATFORM
#include <lvgl.h>
#include <tactility/lvgl_module.h>
extern "C" {
extern lv_obj_t* __real_lv_button_create(lv_obj_t* parent);
@@ -21,4 +19,4 @@ lv_obj_t* __wrap_lv_button_create(lv_obj_t* parent) {
}
#endif // ESP_PLATFORM
#endif // ESP_PLATFORM
+1 -3
View File
@@ -1,9 +1,7 @@
#ifdef ESP_PLATFORM
#include <lvgl.h>
#include <tactility/lvgl_module.h>
extern "C" {
extern lv_obj_t* __real_lv_list_create(lv_obj_t* parent);
@@ -33,4 +31,4 @@ lv_obj_t* __wrap_lv_list_add_button(lv_obj_t* list, const void* icon, const char
}
#endif // ESP_PLATFORM
#endif // ESP_PLATFORM
+23 -1
View File
@@ -23,6 +23,8 @@ 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();
@@ -171,6 +173,20 @@ bool load(DisplaySettings& settings) {
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;
@@ -178,6 +194,8 @@ bool load(DisplaySettings& settings) {
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;
}
@@ -190,7 +208,9 @@ DisplaySettings getDefault() {
.backlightTimeoutEnabled = false,
.backlightTimeoutMs = 60000,
.screensaverType = ScreensaverType::BouncingBalls,
.disableScreensaverWhenCharging = false
.disableScreensaverWhenCharging = false,
.invertColor = true,
.monochromeThreshold = 60
};
}
@@ -211,6 +231,8 @@ bool save(const DisplaySettings& settings) {
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;