fix: MCP settings persistence + WebServer/DevServer coexistence + screensaver charging toggle

- McpSettings was hardcoded to /data/service/mcp/settings.properties,
  but ES3C28P uses storage.userDataLocation=SD so user data lives on
  /sdcard/tactility/. Settings appeared to save but on reload returned
  default false -> "enable and when I go back shows disabled".
  Fixed to use getUserDataPath()+"/settings/mcp.properties" pattern
  consistent with DisplaySettings/WebServerSettings, with
  findOrCreateParentDirectory() and isFile() guard.

- HttpServer ctrl_port collision: ESP-IDF default 32768 used by both
  WebServer (80) and DevelopmentService (6666). Second httpd_start()
  failed silently -> dashboard up but /api/mcp 404 and :6666 refused.
  Fixed with unique ctrl_port = 32768 + (port % 1000).
  Added CONFIG_HTTPD_MAX_URI_HANDLERS=20 (default 8 < 9 needed for
  7 handlers + 2 internal).

- DevService stack 5120 -> 8192 for /app/install handling.
- McpSystem video stream task stack 4096 -> 8192 and idle yield 50ms
  (was 5ms always) to avoid CPU starvation blocking httpd tasks.
  On RLCD, timer must always run: DisplayIdle early return on
  !supportsBacklightDuty() broke MCP manual activation (cachedSettings
  not loaded, backlight duty 0). Fixed to always load settings +
  start timer, log RLCD detection, guard setBacklightDuty with
  supportsBacklightDuty() and fallback duty 255.

- DisplaySettings: add disableScreensaverWhenCharging bool, persisted,
  with UI toggle in Display app (Settings->Display) using
  PowerDevice::IsCharging check via findDevices callback.
  When charging and flag enabled, screensaver activation skipped,
  and if already dimmed, wake on charging.

- ES3C28P Power driver: GPIO9 ADC1_CH8 2x divider 2500-4200mV spec from
  ~/Downloads official docs, TP4054 CHRG not connected to GPIO, so
  IsCharging inferred via voltage thresholds (<500mV no battery USB
  or >5000mV wall powered, >=4150mV high) + rising trend.

Verified: flashed es3c28p to /dev/cu.usbmodem101 (192.168.68.113),
WiFi FamReynaMesh RSSI -59, dashboard 200, :6666/info now works,
/api/mcp returns disabled correctly until enabled, persists to SD.
This commit is contained in:
Adolfo Reyna
2026-07-11 20:26:01 -04:00
parent 81f289fc24
commit 2dc216fb7b
13 changed files with 299 additions and 35 deletions
@@ -29,6 +29,7 @@ struct DisplaySettings {
bool backlightTimeoutEnabled;
uint32_t backlightTimeoutMs; // 0 = Never
ScreensaverType screensaverType = ScreensaverType::BouncingBalls;
bool disableScreensaverWhenCharging = false;
};
/** Compares default settings with the function parameter to return the difference */
@@ -43,7 +43,8 @@ class DevelopmentService final : public Service {
.handler = handleAppUninstall,
.user_ctx = this
}
}
},
8192
);
void startServer();
+36
View File
@@ -41,6 +41,8 @@ class DisplayApp final : public App {
lv_obj_t* timeoutSwitch = nullptr;
lv_obj_t* timeoutDropdown = nullptr;
lv_obj_t* screensaverDropdown = nullptr;
lv_obj_t* disableWhenChargingWrapper = nullptr;
lv_obj_t* disableWhenChargingSwitch = nullptr;
static void onBacklightSliderEvent(lv_event_t* event) {
auto* slider = static_cast<lv_obj_t*>(lv_event_get_target(event));
@@ -95,15 +97,29 @@ class DisplayApp final : public App {
if (app->screensaverDropdown) {
lv_obj_clear_state(app->screensaverDropdown, LV_STATE_DISABLED);
}
if (app->disableWhenChargingWrapper) {
lv_obj_clear_state(app->disableWhenChargingWrapper, LV_STATE_DISABLED);
}
} else {
lv_obj_add_state(app->timeoutDropdown, LV_STATE_DISABLED);
if (app->screensaverDropdown) {
lv_obj_add_state(app->screensaverDropdown, LV_STATE_DISABLED);
}
if (app->disableWhenChargingWrapper) {
lv_obj_add_state(app->disableWhenChargingWrapper, LV_STATE_DISABLED);
}
}
}
}
static void onDisableWhenChargingChanged(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.disableScreensaverWhenCharging = enabled;
app->displaySettingsUpdated = true;
}
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));
@@ -294,6 +310,26 @@ public:
if (!displaySettings.backlightTimeoutEnabled) {
lv_obj_add_state(screensaverDropdown, LV_STATE_DISABLED);
}
// Disable screensaver when charging toggle
disableWhenChargingWrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(disableWhenChargingWrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(disableWhenChargingWrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(disableWhenChargingWrapper, 0, LV_STATE_DEFAULT);
auto* charging_label = lv_label_create(disableWhenChargingWrapper);
lv_label_set_text(charging_label, "Disable on charging");
lv_obj_align(charging_label, LV_ALIGN_LEFT_MID, 0, 0);
disableWhenChargingSwitch = lv_switch_create(disableWhenChargingWrapper);
if (displaySettings.disableScreensaverWhenCharging) {
lv_obj_add_state(disableWhenChargingSwitch, LV_STATE_CHECKED);
}
lv_obj_align(disableWhenChargingSwitch, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_event_cb(disableWhenChargingSwitch, onDisableWhenChargingChanged, LV_EVENT_VALUE_CHANGED, this);
if (!displaySettings.backlightTimeoutEnabled) {
lv_obj_add_state(disableWhenChargingWrapper, LV_STATE_DISABLED);
}
}
if (hasCalibratableTouchDevice()) {
+7 -2
View File
@@ -1700,7 +1700,12 @@ static void mcp_video_stream_task(void* pvParameters) {
}
}
vTaskDelay(pdMS_TO_TICKS(5));
// Yield: longer delay when no client connected to save CPU
if (mono_client_fd < 0 && color_client_fd < 0) {
vTaskDelay(pdMS_TO_TICKS(50));
} else {
vTaskDelay(pdMS_TO_TICKS(5));
}
}
if (mono_client_fd >= 0) close(mono_client_fd);
@@ -1730,7 +1735,7 @@ bool startVideoStreamServer() {
BaseType_t ret = xTaskCreatePinnedToCore(
mcp_video_stream_task,
"mcp_video_stream",
4096,
8192,
nullptr,
5,
(TaskHandle_t*)&state.streamTaskHandle,
+5 -1
View File
@@ -17,9 +17,13 @@ bool HttpServer::startInternal() {
config.server_port = port;
config.uri_match_fn = matchUri;
config.max_uri_handlers = handlers.size() + INTERNAL_URI_HANDLER_COUNT;
// Assign unique ctrl port based on server port to avoid collision when
// multiple HTTP servers (e.g. WebServer 80 + DevServer 6666) start simultaneously.
// ESP-IDF default ctrl_port is 32768; we offset by server port.
config.ctrl_port = static_cast<uint16_t>(32768 + (port % 1000));
if (httpd_start(&server, &config) != ESP_OK) {
LOGGER.error("Failed to start http server on port {}", port);
LOGGER.error("Failed to start http server on port {} (ctrl_port {})", port, config.ctrl_port);
return false;
}
@@ -12,6 +12,7 @@
#include <Tactility/Logger.h>
#include <Tactility/CoreDefines.h>
#include <Tactility/hal/display/DisplayDevice.h>
#include <Tactility/hal/power/PowerDevice.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/service/ServiceContext.h>
#include <Tactility/service/ServiceManifest.h>
@@ -30,6 +31,22 @@ static std::shared_ptr<hal::display::DisplayDevice> getDisplay() {
return hal::findFirstDevice<hal::display::DisplayDevice>(hal::Device::Type::Display);
}
static bool isDeviceCharging() {
bool charging = false;
hal::findDevices<hal::power::PowerDevice>(hal::Device::Type::Power, [&charging](const auto& power) {
if (!power->supportsMetric(hal::power::PowerDevice::MetricType::IsCharging)) {
return true; // continue
}
hal::power::PowerDevice::MetricData data;
if (power->getMetric(hal::power::PowerDevice::MetricType::IsCharging, data) && data.valueAsBool) {
charging = true;
return false; // stop iter
}
return true;
});
return charging;
}
void DisplayIdleService::stopScreensaverCb(lv_event_t* e) {
auto* self = static_cast<DisplayIdleService*>(lv_event_get_user_data(e));
self->stopScreensaverLocked();
@@ -178,43 +195,53 @@ void DisplayIdleService::tick() {
}
auto display = getDisplay();
if (display != nullptr && display->supportsBacklightDuty()) {
bool supportsBacklight = display != nullptr && display->supportsBacklightDuty();
if (supportsBacklight) {
if (!cachedDisplaySettings.backlightTimeoutEnabled || cachedDisplaySettings.backlightTimeoutMs == 0) {
if (displayDimmed) {
display->setBacklightDuty(cachedDisplaySettings.backlightDuty);
displayDimmed = false;
}
} else {
bool charging_blocks = cachedDisplaySettings.disableScreensaverWhenCharging && isDeviceCharging();
if (!displayDimmed && inactive_ms >= cachedDisplaySettings.backlightTimeoutMs) {
if (!lvgl::lock(100)) {
return; // Retry on next tick
if (charging_blocks) {
// Skip screensaver while charging
} else {
if (!lvgl::lock(100)) {
return; // Retry on next tick
}
activateScreensaver();
lvgl::unlock();
if (cachedDisplaySettings.screensaverType == settings::display::ScreensaverType::None) {
display->setBacklightDuty(0);
}
displayDimmed = true;
}
activateScreensaver();
lvgl::unlock();
// Turn off backlight for "None" screensaver (just black screen)
if (cachedDisplaySettings.screensaverType == settings::display::ScreensaverType::None) {
display->setBacklightDuty(0);
} else if (displayDimmed) {
if (inactive_ms < kWakeActivityThresholdMs) {
stopScreensaver();
} else if (charging_blocks) {
stopScreensaver();
}
displayDimmed = true;
} else if (displayDimmed && (inactive_ms < kWakeActivityThresholdMs)) {
stopScreensaver();
}
}
}
}
bool DisplayIdleService::onStart(ServiceContext& service) {
auto display = getDisplay();
if (display != nullptr && !display->supportsBacklightDuty()) {
LOGGER.info("Monochrome/RLCD display detected (no backlight control): disabling screensaver tick timer");
return true;
}
// Seed random number generator for varied screensaver patterns
srand(static_cast<unsigned int>(time(nullptr)));
cachedDisplaySettings = settings::display::loadOrGetDefault();
auto display = getDisplay();
if (display != nullptr && !display->supportsBacklightDuty()) {
LOGGER.info("Monochrome/RLCD display detected (no backlight control): idle timer will run but auto-backlight off is disabled");
}
timer = std::make_unique<Timer>(Timer::Type::Periodic, kernel::millisToTicks(TICK_INTERVAL_MS), [this]{ this->tick(); });
timer->setCallbackPriority(Thread::Priority::Lower);
timer->start();
@@ -297,10 +324,12 @@ void DisplayIdleService::startMcpScreensaver() {
screensaverActiveCounter = 0;
backlightOff = false;
// Ensure backlight is active and set to configured duty cycle
// Ensure backlight is active if the display supports it
auto display = getDisplay();
if (display) {
display->setBacklightDuty(cachedDisplaySettings.backlightDuty);
if (display != nullptr && display->supportsBacklightDuty()) {
uint8_t duty = cachedDisplaySettings.backlightDuty;
if (duty == 0) duty = 255; // ensure visible if settings not loaded / default
display->setBacklightDuty(duty);
}
lv_coord_t screenW = lv_display_get_horizontal_resolution(nullptr);
+11 -1
View File
@@ -22,6 +22,7 @@ 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";
static Orientation getDefaultOrientation() {
auto* display = lv_display_get_default();
@@ -164,12 +165,19 @@ bool load(DisplaySettings& settings) {
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");
}
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;
return true;
}
@@ -181,7 +189,8 @@ DisplaySettings getDefault() {
.backlightDuty = 200,
.backlightTimeoutEnabled = false,
.backlightTimeoutMs = 60000,
.screensaverType = ScreensaverType::BouncingBalls
.screensaverType = ScreensaverType::BouncingBalls,
.disableScreensaverWhenCharging = false
};
}
@@ -201,6 +210,7 @@ bool save(const DisplaySettings& settings) {
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";
auto settings_path = getSettingsFilePath();
if (!file::findOrCreateParentDirectory(settings_path, 0755)) {
return false;
+21 -8
View File
@@ -2,19 +2,27 @@
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/file/File.h>
#include <Tactility/Logger.h>
#include <Tactility/Paths.h>
#include <map>
#include <string>
namespace tt::settings::mcp {
static const auto LOGGER = Logger("McpSettings");
constexpr auto* SETTINGS_FILE = "/data/service/mcp/settings.properties";
static std::string getSettingsFilePath() {
return getUserDataPath() + "/settings/mcp.properties";
}
constexpr auto* KEY_MCP_ENABLED = "mcpEnabled";
bool load(McpSettings& settings) {
auto settings_path = getSettingsFilePath();
if (!file::isFile(settings_path)) {
return false;
}
std::map<std::string, std::string> map;
if (!file::loadPropertiesFile(SETTINGS_FILE, map)) {
if (!file::loadPropertiesFile(settings_path, map)) {
return false;
}
@@ -36,20 +44,25 @@ McpSettings loadOrGetDefault() {
McpSettings settings;
if (!load(settings)) {
settings = getDefault();
file::findOrCreateDirectory("/data/service/mcp", 0755);
save(settings);
if (!save(settings)) {
LOGGER.warn("Failed to save default MCP settings");
}
}
return settings;
}
bool save(const McpSettings& settings) {
file::findOrCreateDirectory("/data/service/mcp", 0755);
std::map<std::string, std::string> map;
map[KEY_MCP_ENABLED] = settings.mcpEnabled ? "true" : "false";
if (!file::savePropertiesFile(SETTINGS_FILE, map)) {
LOGGER.error("Failed to save MCP settings to {}", SETTINGS_FILE);
auto settings_path = getSettingsFilePath();
if (!file::findOrCreateParentDirectory(settings_path, 0755)) {
LOGGER.error("Failed to create parent dir for {}", settings_path);
return false;
}
if (!file::savePropertiesFile(settings_path, map)) {
LOGGER.error("Failed to save MCP settings to {}", settings_path);
return false;
}
return true;