Files
tactility/Tactility/Source/service/webserver/McpSimulatorHandler.cpp
2026-09-22 16:51:50 -04:00

259 lines
12 KiB
C++

#ifndef ESP_PLATFORM
#include <Tactility/service/webserver/WebServerService.h>
#include <Tactility/lvgl/Lvgl.h>
#include <lvgl/lvgl.h>
#include <app/manager.h>
#include <app/start.h>
#include <cJSON.h>
#include <http/server.h>
#include <cstdlib>
#include <cstring>
#include <sstream>
#include <string>
#include <vector>
namespace tt::service::webserver {
namespace {
constexpr size_t JSON_BODY_LIMIT = 64 * 1024;
lv_obj_t* gCanvas = nullptr;
bool gCanvasBlack = false;
lv_obj_t* getCanvas() {
if (gCanvas != nullptr) return gCanvas;
lv_display_t* display = lv_display_get_default();
if (display == nullptr) return nullptr;
lv_obj_t* layer = lv_display_get_layer_top(display);
gCanvas = lv_obj_create(layer);
lv_obj_set_pos(gCanvas, 0, 0);
lv_obj_set_size(gCanvas, lv_display_get_horizontal_resolution(display), lv_display_get_vertical_resolution(display));
lv_obj_set_style_pad_all(gCanvas, 0, LV_PART_MAIN);
lv_obj_set_style_border_width(gCanvas, 0, LV_PART_MAIN);
lv_obj_set_style_bg_opa(gCanvas, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_clear_flag(gCanvas, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_clear_flag(gCanvas, LV_OBJ_FLAG_CLICKABLE);
return gCanvas;
}
bool clearCanvas(int color) {
lv_obj_t* canvas = getCanvas();
if (canvas == nullptr) return false;
lv_obj_clean(canvas);
gCanvasBlack = color == 1;
lv_obj_set_style_bg_color(canvas, gCanvasBlack ? lv_color_black() : lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_bg_opa(canvas, LV_OPA_COVER, LV_PART_MAIN);
return true;
}
bool drawCanvasText(const char* text, int x, int y) {
lv_obj_t* canvas = getCanvas();
if (canvas == nullptr) return false;
lv_obj_t* label = lv_label_create(canvas);
lv_label_set_text(label, text);
lv_obj_set_pos(label, x, y);
lv_obj_set_style_text_color(label, gCanvasBlack ? lv_color_white() : lv_color_black(), LV_PART_MAIN);
return true;
}
cJSON* makeError(const cJSON* id, int code, const char* message) {
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "jsonrpc", "2.0");
cJSON* error = cJSON_AddObjectToObject(response, "error");
cJSON_AddNumberToObject(error, "code", code);
cJSON_AddStringToObject(error, "message", message);
if (id != nullptr) cJSON_AddItemToObject(response, "id", cJSON_Duplicate(id, true));
else cJSON_AddNullToObject(response, "id");
return response;
}
cJSON* makeTextResult(const cJSON* id, const char* text, bool isError = false) {
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "jsonrpc", "2.0");
cJSON* result = cJSON_AddObjectToObject(response, "result");
cJSON* content = cJSON_AddArrayToObject(result, "content");
cJSON* item = cJSON_CreateObject();
cJSON_AddStringToObject(item, "type", "text");
cJSON_AddStringToObject(item, "text", text);
cJSON_AddItemToArray(content, item);
if (isError) cJSON_AddBoolToObject(result, "isError", true);
if (id != nullptr) cJSON_AddItemToObject(response, "id", cJSON_Duplicate(id, true));
else cJSON_AddNullToObject(response, "id");
return response;
}
cJSON* makeToolList(const cJSON* id) {
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "jsonrpc", "2.0");
cJSON* result = cJSON_AddObjectToObject(response, "result");
cJSON* tools = cJSON_AddArrayToObject(result, "tools");
cJSON* capabilities = cJSON_CreateObject();
cJSON_AddStringToObject(capabilities, "name", "get_capabilities");
cJSON_AddStringToObject(capabilities, "description", "Get the simulator platform and display capability information.");
cJSON_AddItemToObject(capabilities, "inputSchema", cJSON_Parse("{\"type\":\"object\",\"properties\":{}}"));
cJSON_AddItemToArray(tools, capabilities);
cJSON* listApps = cJSON_CreateObject();
cJSON_AddStringToObject(listApps, "name", "list_apps");
cJSON_AddStringToObject(listApps, "description", "List apps available in the simulator.");
cJSON_AddItemToObject(listApps, "inputSchema", cJSON_Parse("{\"type\":\"object\",\"properties\":{}}"));
cJSON_AddItemToArray(tools, listApps);
cJSON* runApp = cJSON_CreateObject();
cJSON_AddStringToObject(runApp, "name", "run_app");
cJSON_AddStringToObject(runApp, "description", "Launch an installed simulator app by its app ID.");
cJSON_AddItemToObject(runApp, "inputSchema", cJSON_Parse("{\"type\":\"object\",\"properties\":{\"app_id\":{\"type\":\"string\"}},\"required\":[\"app_id\"]}"));
cJSON_AddItemToArray(tools, runApp);
cJSON* clearScreen = cJSON_CreateObject();
cJSON_AddStringToObject(clearScreen, "name", "clear_screen");
cJSON_AddStringToObject(clearScreen, "description", "Clear the MCP drawing canvas to white (0) or black (1).");
cJSON_AddItemToObject(clearScreen, "inputSchema", cJSON_Parse("{\"type\":\"object\",\"properties\":{\"color\":{\"type\":\"integer\",\"enum\":[0,1]}},\"required\":[\"color\"]}"));
cJSON_AddItemToArray(tools, clearScreen);
cJSON* drawText = cJSON_CreateObject();
cJSON_AddStringToObject(drawText, "name", "draw_text");
cJSON_AddStringToObject(drawText, "description", "Draw text on the simulator MCP canvas at x,y coordinates.");
cJSON_AddItemToObject(drawText, "inputSchema", cJSON_Parse("{\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"},\"x\":{\"type\":\"integer\"},\"y\":{\"type\":\"integer\"}},\"required\":[\"text\",\"x\",\"y\"]}"));
cJSON_AddItemToArray(tools, drawText);
if (id != nullptr) cJSON_AddItemToObject(response, "id", cJSON_Duplicate(id, true));
else cJSON_AddNullToObject(response, "id");
return response;
}
std::string listAppsJson() {
std::vector<const AppManifest*> manifests;
app_manager_for_each_manifest([](const AppManifest* manifest, void* context) {
static_cast<std::vector<const AppManifest*>*>(context)->push_back(manifest);
}, &manifests);
cJSON* apps = cJSON_CreateArray();
for (const AppManifest* manifest : manifests) {
cJSON* app = cJSON_CreateObject();
cJSON_AddStringToObject(app, "id", manifest->id);
cJSON_AddStringToObject(app, "name", manifest->name);
cJSON_AddItemToArray(apps, app);
}
char* text = cJSON_PrintUnformatted(apps);
std::string output = text == nullptr ? "[]" : text;
cJSON_free(text);
cJSON_Delete(apps);
return output;
}
cJSON* handleCall(const cJSON* id, const cJSON* params) {
const cJSON* name = cJSON_GetObjectItemCaseSensitive(params, "name");
const cJSON* args = cJSON_GetObjectItemCaseSensitive(params, "arguments");
if (!cJSON_IsString(name) || (args != nullptr && !cJSON_IsObject(args))) {
return makeError(id, -32602, "Invalid params");
}
if (strcmp(name->valuestring, "clear_screen") == 0) {
const cJSON* color = cJSON_GetObjectItemCaseSensitive(args, "color");
if (!cJSON_IsNumber(color) || (color->valueint != 0 && color->valueint != 1)) {
return makeError(id, -32602, "color must be 0 (white) or 1 (black)");
}
lvgl_lock();
const bool cleared = clearCanvas(color->valueint);
lvgl_unlock();
return cleared ? makeTextResult(id, "ok") : makeTextResult(id, "no active display", true);
}
if (strcmp(name->valuestring, "draw_text") == 0) {
const cJSON* text = cJSON_GetObjectItemCaseSensitive(args, "text");
const cJSON* x = cJSON_GetObjectItemCaseSensitive(args, "x");
const cJSON* y = cJSON_GetObjectItemCaseSensitive(args, "y");
if (!cJSON_IsString(text) || text->valuestring == nullptr || strlen(text->valuestring) > 4096 ||
!cJSON_IsNumber(x) || !cJSON_IsNumber(y) || x->valueint < 0 || y->valueint < 0) {
return makeError(id, -32602, "text, non-negative x and y are required (text max 4096 chars)");
}
lvgl_lock();
lv_display_t* display = lv_display_get_default();
const bool inBounds = display != nullptr && x->valueint < lv_display_get_horizontal_resolution(display) &&
y->valueint < lv_display_get_vertical_resolution(display);
const bool drawn = inBounds && drawCanvasText(text->valuestring, x->valueint, y->valueint);
lvgl_unlock();
if (!inBounds) return makeError(id, -32602, "coordinates are outside the simulator display");
return drawn ? makeTextResult(id, "ok") : makeTextResult(id, "could not draw on display", true);
}
if (strcmp(name->valuestring, "get_capabilities") == 0) {
return makeTextResult(id, "{\"platform\":\"tactility-simulator\",\"mcp\":true,\"drawing\":true}");
}
if (strcmp(name->valuestring, "list_apps") == 0) {
const std::string apps = listAppsJson();
return makeTextResult(id, apps.c_str());
}
if (strcmp(name->valuestring, "run_app") == 0) {
const cJSON* appId = cJSON_GetObjectItemCaseSensitive(args, "app_id");
if (!cJSON_IsString(appId) || appId->valuestring == nullptr || appId->valuestring[0] == '\0') {
return makeError(id, -32602, "app_id is required");
}
AppManifest manifest{};
if (app_manager_find_manifest(appId->valuestring, &manifest) != ERROR_NONE) {
return makeTextResult(id, "app not found", true);
}
AppInstanceId instanceId = 0;
const error_t result = app_start(appId->valuestring, 0, nullptr, &instanceId);
if (result != ERROR_NONE) return makeTextResult(id, "app launch failed", true);
return makeTextResult(id, "ok");
}
return makeError(id, -32602, "Unknown simulator MCP tool");
}
cJSON* handleRpc(const char* body) {
cJSON* request = cJSON_Parse(body);
if (request == nullptr) return makeError(nullptr, -32700, "Parse error");
const cJSON* id = cJSON_GetObjectItemCaseSensitive(request, "id");
const cJSON* version = cJSON_GetObjectItemCaseSensitive(request, "jsonrpc");
const cJSON* method = cJSON_GetObjectItemCaseSensitive(request, "method");
const cJSON* params = cJSON_GetObjectItemCaseSensitive(request, "params");
cJSON* response = nullptr;
if (!cJSON_IsObject(request) || !cJSON_IsString(version) || strcmp(version->valuestring, "2.0") != 0 || !cJSON_IsString(method)) {
response = makeError(id, -32600, "Invalid Request");
} else if (strcmp(method->valuestring, "tools/list") == 0) {
response = makeToolList(id);
} else if (strcmp(method->valuestring, "tools/call") == 0) {
response = handleCall(id, params);
} else {
response = makeError(id, -32601, "Method not found");
}
cJSON_Delete(request);
return response;
}
}
error_t WebServerService::handleApiMcp(HttpServerRequest* request, void*) {
const uint64_t length = http_server_request_get_content_length(request);
if (length == 0 || length > JSON_BODY_LIMIT) {
http_server_request_send_error(request, 400, "Invalid Content-Length");
return ERROR_INVALID_ARGUMENT;
}
std::string body(static_cast<size_t>(length), '\0');
size_t received = 0;
while (received < body.size()) {
const int count = http_server_request_receive(request, body.data() + received, body.size() - received);
if (count <= 0) {
http_server_request_send_error(request, 400, "Incomplete request body");
return ERROR_UNDEFINED;
}
received += static_cast<size_t>(count);
}
cJSON* response = handleRpc(body.c_str());
char* text = cJSON_PrintUnformatted(response);
cJSON_Delete(response);
if (text == nullptr) {
http_server_request_send_error(request, 500, "Serialization failed");
return ERROR_RESOURCE;
}
http_server_request_set_content_type(request, "application/json");
http_server_request_send_string(request, text);
cJSON_free(text);
return ERROR_NONE;
}
}
#endif // ESP_PLATFORM