diff --git a/Buildscripts/sdkconfig/default.properties b/Buildscripts/sdkconfig/default.properties index 4bb2689b..29498bbf 100644 --- a/Buildscripts/sdkconfig/default.properties +++ b/Buildscripts/sdkconfig/default.properties @@ -2,6 +2,8 @@ CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072 # Ensure large enough stack for network operations CONFIG_ESP_MAIN_TASK_STACK_SIZE=6144 +# HTTP server: need enough URI handler slots for WebServer (7 + internal) + DevServer +CONFIG_HTTPD_MAX_URI_HANDLERS=20 # Fixes static assertion: FLASH and PSRAM Mode configuration are not supported CONFIG_IDF_EXPERIMENTAL_FEATURES=y # Free up IRAM diff --git a/Devices/es3c28p/CMakeLists.txt b/Devices/es3c28p/CMakeLists.txt index 3c5bef0d..e7405fc8 100644 --- a/Devices/es3c28p/CMakeLists.txt +++ b/Devices/es3c28p/CMakeLists.txt @@ -3,5 +3,5 @@ file(GLOB_RECURSE SOURCE_FILES Source/*.c*) idf_component_register( SRCS ${SOURCE_FILES} INCLUDE_DIRS "Source" - REQUIRES Tactility esp_lvgl_port ILI934x FT6x36 PwmBacklight driver vfs fatfs + REQUIRES Tactility esp_lvgl_port ILI934x FT6x36 PwmBacklight driver vfs fatfs esp_adc ) diff --git a/Devices/es3c28p/Source/Configuration.cpp b/Devices/es3c28p/Source/Configuration.cpp index dd2582e9..a0c9d059 100644 --- a/Devices/es3c28p/Source/Configuration.cpp +++ b/Devices/es3c28p/Source/Configuration.cpp @@ -1,5 +1,6 @@ #include "PwmBacklight.h" #include "devices/Display.h" +#include "devices/Power.h" #include #include @@ -82,7 +83,8 @@ static bool initBoot() { static DeviceVector createDevices() { return { - createDisplay() + createDisplay(), + createPower() }; } diff --git a/Devices/es3c28p/Source/devices/Power.cpp b/Devices/es3c28p/Source/devices/Power.cpp new file mode 100644 index 00000000..fb95572d --- /dev/null +++ b/Devices/es3c28p/Source/devices/Power.cpp @@ -0,0 +1,156 @@ +#include "Power.h" +#include +#include +#include +#include +#include +#include + +static const auto* TAG = "ES3C28P_Power"; + +// HW: BAT+ -> R14 200K + R15 200K -> GND => BAT_ADC middle, 2x divider, to GPIO9 ADC1_CH8 +// Charger TP4054 CHRG pin only drives LED, not connected to ESP32 GPIO. +// So we infer charging by voltage behavior: rising trend and high voltage. + +class Es3c28pPower final : public tt::hal::power::PowerDevice { +public: + Es3c28pPower() { + adc_oneshot_unit_init_cfg_t init_cfg = { + .unit_id = ADC_UNIT_1, + .clk_src = ADC_RTC_CLK_SRC_DEFAULT, + .ulp_mode = ADC_ULP_MODE_DISABLE, + }; + if (adc_oneshot_new_unit(&init_cfg, &adc_handle) != ESP_OK) { + ESP_LOGE(TAG, "ADC unit init failed"); + return; + } + adc_oneshot_chan_cfg_t chan_cfg = { + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_12, + }; + if (adc_oneshot_config_channel(adc_handle, ADC_CHANNEL_8, &chan_cfg) != ESP_OK) { + ESP_LOGE(TAG, "ADC channel config failed"); + adc_oneshot_del_unit(adc_handle); + adc_handle = nullptr; + return; + } + adc_cali_curve_fitting_config_t cali_cfg = { + .unit_id = ADC_UNIT_1, + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_12, + }; + if (adc_cali_create_scheme_curve_fitting(&cali_cfg, &cali_handle) != ESP_OK) { + ESP_LOGW(TAG, "ADC cali scheme failed"); + cali_handle = nullptr; + } + ESP_LOGI(TAG, "Battery ADC GPIO9 ADC1_CH8, 2x divider, cali %s", cali_handle ? "OK" : "none"); + } + + ~Es3c28pPower() override { + if (cali_handle) adc_cali_delete_scheme_curve_fitting(cali_handle); + if (adc_handle) adc_oneshot_del_unit(adc_handle); + } + + std::string getName() const override { return "ES3C28P Power"; } + std::string getDescription() const override { return "Battery GPIO9 ADC1_CH8 + TP4054 inferred charging"; } + + bool supportsMetric(MetricType type) const override { + switch (type) { + case MetricType::BatteryVoltage: + case MetricType::ChargeLevel: + case MetricType::IsCharging: + return adc_handle != nullptr; + default: return false; + } + } + + bool getMetric(MetricType type, MetricData& data) override { + if (!adc_handle) return false; + + int raw = 0; + int mv = 0; + if (adc_oneshot_read(adc_handle, ADC_CHANNEL_8, &raw) != ESP_OK) return false; + if (cali_handle) { + if (adc_cali_raw_to_voltage(cali_handle, raw, &mv) != ESP_OK) return false; + } else { + mv = (raw * 3300) / 4095; + } + int vbat_mv = mv * 2; // 200K/200K divider + + // Track voltage trend for charging detection + const int now_ms = xTaskGetTickCount() * portTICK_PERIOD_MS; + if (last_read_ms == 0) { + last_vbat_mv = vbat_mv; + last_read_ms = now_ms; + } + + // Update trend every 2 seconds to avoid noise + if (now_ms - last_read_ms > 2000) { + // If voltage increased by at least 10mV in 2s, likely charging + if (vbat_mv > last_vbat_mv + 10) { + rising_count++; + falling_count = 0; + } else if (vbat_mv < last_vbat_mv - 10) { + falling_count++; + rising_count = 0; + if (falling_count > 3) is_charging_trend = false; + } + last_vbat_mv = vbat_mv; + last_read_ms = now_ms; + } + + switch (type) { + case MetricType::BatteryVoltage: + data.valueAsUint32 = (uint32_t)vbat_mv; + return true; + case MetricType::ChargeLevel: { + uint8_t pct; + if (vbat_mv <= 2500) pct = 0; + else if (vbat_mv >= 4200) pct = 100; + else pct = (uint8_t)((vbat_mv - 2500) / 17); + data.valueAsUint8 = pct; + return true; + } + case MetricType::IsCharging: { + // Cases: + // 1. No battery / wall powered: vbat <500mV or >5000mV (VBUS via MOSFET) => charging true + if (vbat_mv < 500 || vbat_mv > 5000) { + data.valueAsBool = true; + is_charging_trend = true; + return true; + } + // 2. High voltage near full with charger attached: >=4150mV => charging (charger holds at 4.2V) + if (vbat_mv >= 4150) { + data.valueAsBool = true; + is_charging_trend = true; + return true; + } + // 3. Rising trend indicates charging + if (rising_count >= 2) { + is_charging_trend = true; + } + // 4. If previously charging and voltage still >=4000, keep charging until drop + if (is_charging_trend && vbat_mv >= 4000) { + data.valueAsBool = true; + return true; + } + data.valueAsBool = is_charging_trend; + return true; + } + default: return false; + } + } + +private: + adc_oneshot_unit_handle_t adc_handle = nullptr; + adc_cali_handle_t cali_handle = nullptr; + int last_vbat_mv = 0; + int last_read_ms = 0; + int rising_count = 0; + int falling_count = 0; + bool is_charging_trend = false; +}; + +std::shared_ptr createPower() { + return std::make_shared(); +} diff --git a/Devices/es3c28p/Source/devices/Power.h b/Devices/es3c28p/Source/devices/Power.h new file mode 100644 index 00000000..4366bc61 --- /dev/null +++ b/Devices/es3c28p/Source/devices/Power.h @@ -0,0 +1,5 @@ +#pragma once +#include +#include + +std::shared_ptr createPower(); diff --git a/Tactility/Include/Tactility/settings/DisplaySettings.h b/Tactility/Include/Tactility/settings/DisplaySettings.h index f89a0fd5..d6b35f2e 100644 --- a/Tactility/Include/Tactility/settings/DisplaySettings.h +++ b/Tactility/Include/Tactility/settings/DisplaySettings.h @@ -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 */ diff --git a/Tactility/Private/Tactility/service/development/DevelopmentService.h b/Tactility/Private/Tactility/service/development/DevelopmentService.h index 0d74dbbd..8be33bce 100644 --- a/Tactility/Private/Tactility/service/development/DevelopmentService.h +++ b/Tactility/Private/Tactility/service/development/DevelopmentService.h @@ -43,7 +43,8 @@ class DevelopmentService final : public Service { .handler = handleAppUninstall, .user_ctx = this } - } + }, + 8192 ); void startServer(); diff --git a/Tactility/Source/app/display/Display.cpp b/Tactility/Source/app/display/Display.cpp index ca2314ab..eac98140 100644 --- a/Tactility/Source/app/display/Display.cpp +++ b/Tactility/Source/app/display/Display.cpp @@ -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_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(lv_event_get_user_data(event)); + auto* sw = static_cast(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(lv_event_get_user_data(event)); auto* dropdown = static_cast(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()) { diff --git a/Tactility/Source/mcp/McpSystem.cpp b/Tactility/Source/mcp/McpSystem.cpp index 3572f204..552f7e80 100644 --- a/Tactility/Source/mcp/McpSystem.cpp +++ b/Tactility/Source/mcp/McpSystem.cpp @@ -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, diff --git a/Tactility/Source/network/HttpServer.cpp b/Tactility/Source/network/HttpServer.cpp index d6b9b673..cd9c92d3 100644 --- a/Tactility/Source/network/HttpServer.cpp +++ b/Tactility/Source/network/HttpServer.cpp @@ -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(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; } diff --git a/Tactility/Source/service/displayidle/DisplayIdle.cpp b/Tactility/Source/service/displayidle/DisplayIdle.cpp index a7164033..3c0c9051 100644 --- a/Tactility/Source/service/displayidle/DisplayIdle.cpp +++ b/Tactility/Source/service/displayidle/DisplayIdle.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -30,6 +31,22 @@ static std::shared_ptr getDisplay() { return hal::findFirstDevice(hal::Device::Type::Display); } +static bool isDeviceCharging() { + bool charging = false; + hal::findDevices(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(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(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::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); diff --git a/Tactility/Source/settings/DisplaySettings.cpp b/Tactility/Source/settings/DisplaySettings.cpp index 56dd55e4..c4f593b4 100644 --- a/Tactility/Source/settings/DisplaySettings.cpp +++ b/Tactility/Source/settings/DisplaySettings.cpp @@ -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; diff --git a/Tactility/Source/settings/McpSettings.cpp b/Tactility/Source/settings/McpSettings.cpp index 8986dfe4..88b70c18 100644 --- a/Tactility/Source/settings/McpSettings.cpp +++ b/Tactility/Source/settings/McpSettings.cpp @@ -2,19 +2,27 @@ #include #include #include +#include #include #include 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 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 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;