Files
tactility/Tactility/Source/app/mcpoverride/McpOverrideApp.cpp
T
Adolfo Reyna 41004a3c6f feat: integrate MCP server and screen override into system firmware
- Add McpSettings (load/save mcpEnabled, mcpOverrideEnabled to /data/service/mcp/settings.properties)
- Add McpSystem: global state, canvas drawing (RGB565, BMP, PBM, text), I2S audio (WAV, MP3 via minimp3, tone, record)
- Add McpHandler: unauthenticated POST /api/mcp (JSON-RPC 2.0, 13 tools) and POST /api/screen/raw endpoints
- Route MCP endpoints before Basic Auth check in WebServerService::handleApiPost
- Start HTTP server at boot if either webServerEnabled OR mcpEnabled is set
- Fix setEnabled: start unconditionally when called with true; only stop if both WS and MCP are disabled
- Add McpSettingsApp (Settings category): toggle MCP service + screen override, show live endpoint URL
- Add McpOverrideApp (appId one.tactility.mcpscreen): full-screen LVGL canvas for LLM-driven display override
- Register both new apps in Tactility.cpp
2026-06-26 23:02:09 -04:00

141 lines
5.3 KiB
C++

#ifdef ESP_PLATFORM
#include <Tactility/Tactility.h>
#include <Tactility/mcp/McpSystem.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/Logger.h>
#include <lvgl.h>
#include <tactility/lvgl_icon_shared.h>
#include <esp_heap_caps.h>
namespace tt::app::mcpoverride {
static const auto LOGGER = Logger("McpOverrideApp");
class McpOverrideApp final : public App {
public:
void onCreate(AppContext& app) override {
// Prepare global state
auto& state = mcp::getState();
state.overrideActive = false;
}
void onShow(AppContext& app, lv_obj_t* parent) override {
LOGGER.info("onShow: Starting MCP Override display canvas");
auto& state = mcp::getState();
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_all(parent, 0, LV_PART_MAIN);
lv_obj_set_style_pad_row(parent, 0, LV_PART_MAIN);
// Standard toolbar so the user can navigate back
lv_obj_t* toolbar = lvgl::toolbar_create(parent, app);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_t* title_label = lv_label_create(toolbar);
lv_label_set_text(title_label, "MCP Override Screen");
// Create drawing canvas
state.drawArea = lv_canvas_create(parent);
lv_obj_set_width(state.drawArea, LV_PCT(100));
lv_obj_set_flex_grow(state.drawArea, 1);
lv_obj_set_style_radius(state.drawArea, 0, LV_PART_MAIN);
lv_obj_set_style_border_width(state.drawArea, 0, LV_PART_MAIN);
lv_obj_set_style_pad_all(state.drawArea, 0, LV_PART_MAIN);
lv_obj_remove_flag(state.drawArea, LV_OBJ_FLAG_SCROLLABLE);
// Get display metrics
lv_display_t* display = lv_obj_get_display(parent);
state.displayWidth = lv_display_get_horizontal_resolution(display);
state.displayHeight = lv_display_get_vertical_resolution(display);
lv_obj_update_layout(parent);
state.drawWidth = lv_obj_get_content_width(state.drawArea);
state.drawHeight = lv_obj_get_content_height(state.drawArea);
// Allocate framebuffer
size_t required_size = (size_t)state.drawWidth * state.drawHeight * sizeof(uint16_t);
if (state.framebuffer == nullptr || state.framebufferSize != required_size) {
if (state.framebuffer != nullptr) {
heap_caps_free(state.framebuffer);
state.framebuffer = nullptr;
}
state.framebuffer = (uint16_t*)heap_caps_malloc(required_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (state.framebuffer == nullptr) {
state.framebuffer = (uint16_t*)heap_caps_malloc(required_size, MALLOC_CAP_8BIT);
}
state.framebufferSize = state.framebuffer == nullptr ? 0 : required_size;
}
if (state.framebuffer == nullptr) {
LOGGER.error("Failed to allocate {} bytes framebuffer", (unsigned)required_size);
lv_obj_t* error = lv_label_create(state.drawArea);
lv_label_set_text(error, "Framebuffer allocation failed");
lv_obj_center(error);
return;
}
lv_canvas_set_buffer(
state.drawArea,
state.framebuffer,
state.drawWidth,
state.drawHeight,
LV_COLOR_FORMAT_RGB565
);
// Initialize welcome/waiting screen if LLM hasn't written anything yet
if (!state.overrideActive) {
// Fill with a nice dark blue/slate color
for (size_t i = 0; i < (size_t)state.drawWidth * state.drawHeight; ++i) {
state.framebuffer[i] = 0x18E3;
}
state.drawColor = 1;
lv_obj_t* welcome_label = lv_label_create(state.drawArea);
lv_label_set_text(welcome_label, "Waiting for LLM...");
lv_obj_set_style_text_color(welcome_label, lv_color_white(), LV_PART_MAIN);
lv_obj_align(welcome_label, LV_ALIGN_CENTER, 0, -20);
lv_obj_t* desc_label = lv_label_create(state.drawArea);
lv_label_set_text_fmt(desc_label, "Display Resolution: %ux%u", state.drawWidth, state.drawHeight);
lv_obj_set_style_text_color(desc_label, lv_palette_lighten(LV_PALETTE_BLUE, 3), LV_PART_MAIN);
lv_obj_align(desc_label, LV_ALIGN_CENTER, 0, 10);
}
}
void onHide(AppContext& app) override {
LOGGER.info("onHide: Tearing down MCP Override canvas");
auto& state = mcp::getState();
state.drawArea = nullptr;
if (state.framebuffer != nullptr) {
heap_caps_free(state.framebuffer);
state.framebuffer = nullptr;
state.framebufferSize = 0;
}
state.overrideActive = false;
// Stop any running tone or recording to prevent stuck state
state.audioRunning = false;
}
void onDestroy(AppContext& app) override {
onHide(app);
}
};
extern const AppManifest manifest = {
.appId = "one.tactility.mcpscreen", // Keep the original appId for compatibility
.appName = "MCP Override Screen",
.appIcon = LVGL_ICON_SHARED_TOOLBAR,
.appCategory = Category::System,
.createApp = create<McpOverrideApp>
};
} // namespace
#endif // ESP_PLATFORM