#ifndef ESP_PLATFORM #include #include #include #include #include #include #include #include #include #include #include #include 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 manifests; app_manager_for_each_manifest([](const AppManifest* manifest, void* context) { static_cast*>(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(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(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