feat(mcp): restore MCP service with display charging toggle

- Restore MCP system (McpSystem 1900+ LOC, McpHandler, McpScreensaver, McpSettings, McpOverride apps)
- Use audio-stream instead of direct I2S for MP3 playback (fixes fast playback, uses native resampler)
- Register McpSettings + McpOverride in Tactility.cpp so Settings shows MCP Screen
- DisplaySettings: add ScreensaverType::McpScreen and disableScreensaverWhenCharging flag
- Display app: add 'Disable on charging' toggle, handles McpScreen screensaver type
- DisplayIdle: skip screensaver while charging, support McpScreen screensaver, isDeviceCharging check via PowerDevice
- WebServer: coexistence with MCP (McpHandler)
- Avoid formatting churn from previous commits

Squashed from range eaa248b0..8970143f (6 commits) into single clean commit
This commit is contained in:
Adolfo Reyna
2026-07-21 11:47:38 -04:00
parent 2fbc44466a
commit a158fe1696
18 changed files with 5646 additions and 17 deletions
@@ -5,6 +5,8 @@
#include <Tactility/service/webserver/AssetVersion.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/settings/WebServerSettings.h>
#include <Tactility/settings/McpSettings.h>
#include <Tactility/mcp/McpSystem.h>
#include <Tactility/MountPoints.h>
#include <Tactility/file/File.h>
#include <Tactility/lvgl/Statusbar.h>
@@ -215,7 +217,8 @@ bool WebServerService::onStart(ServiceContext& service) {
lock.lock();
g_cachedSettings = settings::webserver::loadOrGetDefault();
g_settingsCached = true;
serverEnabled = g_cachedSettings.webServerEnabled;
auto mcpSettings = settings::mcp::loadOrGetDefault();
serverEnabled = g_cachedSettings.webServerEnabled || mcpSettings.mcpEnabled;
}
// Subscribe to settings change events to refresh cache
settingsEventSubscription = pubsub->subscribe([](WebServerEvent event) {
@@ -259,13 +262,17 @@ void WebServerService::onStop(ServiceContext& service) {
void WebServerService::setEnabled(bool enabled) {
auto lock = mutex.asScopedLock();
lock.lock();
if (enabled) {
if (!httpServer || !httpServer->isStarted()) {
startServer();
}
} else {
if (httpServer && httpServer->isStarted()) {
// Stop only if both web server and MCP are disabled
auto wsSettings = settings::webserver::loadOrGetDefault();
auto mcpSettings = settings::mcp::loadOrGetDefault();
bool anyEnabled = wsSettings.webServerEnabled || mcpSettings.mcpEnabled;
if (!anyEnabled && httpServer && httpServer->isStarted()) {
stopServer();
}
}
@@ -514,6 +521,11 @@ bool WebServerService::startServer() {
LOG_I(TAG, "HTTP server started successfully on port %u", (unsigned)settings.webServerPort);
publish_event(this, WebServerEvent::WebServerStarted);
auto mcpSettings = settings::mcp::loadOrGetDefault();
if (mcpSettings.mcpEnabled) {
mcp::startVideoStreamServer();
}
// Show statusbar icon
if (statusbarIconId >= 0) {
lvgl::statusbar_icon_set_image(statusbarIconId, LVGL_ICON_STATUSBAR_CLOUD);
@@ -533,6 +545,8 @@ void WebServerService::stopServer() {
httpServer->stop();
httpServer.reset();
mcp::stopVideoStreamServer();
// Stop AP mode WiFi if we started it
if (apWifiInitialized || apNetif != nullptr) {
stopApMode();
@@ -1025,15 +1039,24 @@ esp_err_t WebServerService::handleApiGet(httpd_req_t* request) {
return ESP_FAIL;
}
// API POST dispatcher - all POST endpoints require authentication
// API POST dispatcher - all POST endpoints require authentication except MCP
esp_err_t WebServerService::handleApiPost(httpd_req_t* request) {
const char* uri = request->uri;
// MCP endpoints are unauthenticated (local network)
if (strncmp(uri, "/api/mcp", 8) == 0) {
return handleApiMcp(request);
}
if (strncmp(uri, "/api/screen/raw", 15) == 0) {
return handleApiScreenRaw(request);
}
bool authPassed = false;
esp_err_t authResult = validateRequestAuth(request, authPassed);
if (!authPassed) {
return authResult;
}
const char* uri = request->uri;
if (strncmp(uri, "/api/apps/run", 13) == 0) {
return handleApiAppsRun(request);
}