#ifdef ESP_PLATFORM #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace tt::service::webserver { static void* psram_malloc(size_t size) { void* ptr = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if (ptr != nullptr) { return ptr; } return malloc(size); } static const auto LOGGER = Logger("McpHandler"); #define MCP_JSON_BODY_LIMIT (256 * 1024) #define MCP_RAW_BODY_LIMIT (512 * 1024) static const char* TOOLS_JSON = "[" "{" "\"name\":\"get_capabilities\"," "\"description\":\"Get the Tactility display capabilities.\"," "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}" "}," "{" "\"name\":\"clear_screen\"," "\"description\":\"Clear the MCP drawing area to white (0) or black (1).\"," "\"inputSchema\":{" "\"type\":\"object\"," "\"properties\":{\"color\":{\"type\":\"integer\",\"enum\":[0,1]}}," "\"required\":[\"color\"]" "}" "}," "{" "\"name\":\"draw_text\"," "\"description\":\"Draw text in the MCP drawing area at x,y coordinates.\"," "\"inputSchema\":{" "\"type\":\"object\"," "\"properties\":{" "\"text\":{\"type\":\"string\"}," "\"x\":{\"type\":\"integer\"}," "\"y\":{\"type\":\"integer\"}," "\"size\":{\"type\":\"integer\",\"enum\":[1,2],\"default\":1}" "}," "\"required\":[\"text\",\"x\",\"y\"]" "}" "}," "{" "\"name\":\"draw_image\"," "\"description\":\"Draw a bridge-preprocessed binary PBM image.\"," "\"inputSchema\":{" "\"type\":\"object\"," "\"properties\":{" "\"pbm_base64\":{\"type\":\"string\"}," "\"x\":{\"type\":\"integer\",\"default\":0}," "\"y\":{\"type\":\"integer\",\"default\":0}," "\"dither\":{\"type\":\"boolean\",\"default\":true}" "}," "\"required\":[\"pbm_base64\"]" "}" "}," "{" "\"name\":\"draw_color_bmp\"," "\"description\":\"Draw an uncompressed 24-bit or 32-bit BMP image.\"," "\"inputSchema\":{" "\"type\":\"object\"," "\"properties\":{" "\"bmp_base64\":{\"type\":\"string\"}," "\"x\":{\"type\":\"integer\",\"default\":0}," "\"y\":{\"type\":\"integer\",\"default\":0}" "}," "\"required\":[\"bmp_base64\"]" "}" "}," "{" "\"name\":\"draw_raw_rgb565\"," "\"description\":\"Draw big-endian raw RGB565 pixels.\"," "\"inputSchema\":{" "\"type\":\"object\"," "\"properties\":{" "\"rgb565_base64\":{\"type\":\"string\"}," "\"x\":{\"type\":\"integer\"}," "\"y\":{\"type\":\"integer\"}," "\"w\":{\"type\":\"integer\"}," "\"h\":{\"type\":\"integer\"}" "}," "\"required\":[\"rgb565_base64\",\"x\",\"y\",\"w\",\"h\"]" "}" "}," "{" "\"name\":\"get_screenshot\"," "\"description\":\"Capture the MCP framebuffer as a PBM image.\"," "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}" "}," "{" "\"name\":\"play_tone\"," "\"description\":\"Play a pure sine wave tone on the board speaker.\"," "\"inputSchema\":{\"type\":\"object\",\"properties\":{" "\"frequency\":{\"type\":\"integer\",\"default\":440}," "\"duration_ms\":{\"type\":\"integer\",\"default\":1000}," "\"volume\":{\"type\":\"integer\",\"default\":50}" "}}" "}," "{" "\"name\":\"play_audio\"," "\"description\":\"Play a 16 kHz 16-bit PCM WAV file from app user data.\"," "\"inputSchema\":{\"type\":\"object\",\"properties\":{" "\"filename\":{\"type\":\"string\"}," "\"volume\":{\"type\":\"integer\",\"default\":50}" "},\"required\":[\"filename\"]}" "}," "{" "\"name\":\"record_voice\"," "\"description\":\"Record 16 kHz 16-bit mono PCM to app user data.\"," "\"inputSchema\":{\"type\":\"object\",\"properties\":{" "\"duration_sec\":{\"type\":\"integer\",\"default\":4}," "\"filename\":{\"type\":\"string\",\"default\":\"recording.pcm\"}" "}}" "}," "{" "\"name\":\"play_audio_base64\"," "\"description\":\"Decode and play a base64-encoded 16 kHz 16-bit PCM WAV.\"," "\"inputSchema\":{\"type\":\"object\",\"properties\":{" "\"wav_base64\":{\"type\":\"string\"}," "\"volume\":{\"type\":\"integer\",\"default\":50}" "},\"required\":[\"wav_base64\"]}" "}," "{" "\"name\":\"play_mp3\"," "\"description\":\"Stream an MP3 file from app user data to the speaker.\"," "\"inputSchema\":{\"type\":\"object\",\"properties\":{" "\"filename\":{\"type\":\"string\"}," "\"volume\":{\"type\":\"integer\",\"default\":50}" "},\"required\":[\"filename\"]}" "}," "{" "\"name\":\"play_mp3_base64\"," "\"description\":\"Decode and play a base64-encoded MP3.\"," "\"inputSchema\":{\"type\":\"object\",\"properties\":{" "\"mp3_base64\":{\"type\":\"string\"}," "\"volume\":{\"type\":\"integer\",\"default\":50}" "},\"required\":[\"mp3_base64\"]}" "}" "]"; static uint8_t* base64_decode(const char* input, size_t* output_size) { if (input == NULL || output_size == NULL) { return NULL; } const char* payload = input; const char* marker = strstr(input, ";base64,"); if (marker != NULL) { payload = marker + 8; } size_t input_size = strlen(payload); size_t approx_output_len = (input_size * 3) / 4; uint8_t* output = (uint8_t*)psram_malloc(approx_output_len + 4); if (output == NULL) { return NULL; } size_t actual_len = 0; int ret = mbedtls_base64_decode(output, approx_output_len + 4, &actual_len, (const unsigned char*)payload, input_size); if (ret != 0) { free(output); return NULL; } *output_size = actual_len; return output; } static cJSON* make_error(cJSON* id, int code, const char* message) { cJSON* root = cJSON_CreateObject(); cJSON_AddStringToObject(root, "jsonrpc", "2.0"); cJSON* error = cJSON_AddObjectToObject(root, "error"); cJSON_AddNumberToObject(error, "code", code); cJSON_AddStringToObject(error, "message", message); if (id != NULL) { cJSON_AddItemToObject(root, "id", cJSON_Duplicate(id, true)); } else { cJSON_AddNullToObject(root, "id"); } return root; } static cJSON* make_tool_result(cJSON* id, const char* text) { cJSON* root = cJSON_CreateObject(); cJSON_AddStringToObject(root, "jsonrpc", "2.0"); cJSON* result = cJSON_AddObjectToObject(root, "result"); cJSON_AddArrayToObject(result, "content"); cJSON* item = cJSON_CreateObject(); cJSON_AddStringToObject(item, "type", "text"); cJSON_AddStringToObject(item, "text", text); cJSON_AddItemToArray(cJSON_GetObjectItem(result, "content"), item); if (id != NULL) { cJSON_AddItemToObject(root, "id", cJSON_Duplicate(id, true)); } else { cJSON_AddNullToObject(root, "id"); } return root; } static cJSON* handle_tool_call(cJSON* id, cJSON* params) { if (!cJSON_IsObject(params)) { return make_error(id, -32602, "Invalid params"); } cJSON* name_item = cJSON_GetObjectItemCaseSensitive(params, "name"); cJSON* arguments = cJSON_GetObjectItemCaseSensitive(params, "arguments"); if (!cJSON_IsString(name_item) || name_item->valuestring == NULL) { return make_error(id, -32602, "Missing tool name"); } if (arguments != NULL && !cJSON_IsObject(arguments)) { return make_error(id, -32602, "Tool arguments must be an object"); } const char* name = name_item->valuestring; auto& state = mcp::getState(); if (strcmp(name, "get_capabilities") == 0) { char capabilities[384]; uint16_t width = state.drawWidth > 0 ? state.drawWidth : state.displayWidth; uint16_t height = state.drawHeight > 0 ? state.drawHeight : state.displayHeight; snprintf( capabilities, sizeof(capabilities), "{\"color\":true,\"width\":%u,\"height\":%u," "\"formats\":[\"rgb565_base64\",\"bmp_base64\",\"pbm_base64\"]," "\"audio\":{\"sampleRate\":16000,\"bitsPerSample\":16,\"channels\":1}," "\"platform\":\"tactility\",\"appVersion\":\"0.1.0\"," "\"uiVisible\":%s}", width, height, (state.drawArea != nullptr) ? "true" : "false" ); return make_tool_result(id, capabilities); } if (strcmp(name, "clear_screen") == 0) { cJSON* color_item = cJSON_GetObjectItemCaseSensitive(arguments, "color"); if (!cJSON_IsNumber(color_item) || (color_item->valueint != 0 && color_item->valueint != 1)) { return make_error(id, -32602, "color must be 0 or 1"); } if (!mcp::clearScreen(color_item->valueint)) { return make_error(id, -32010, "Failed to clear screen"); } return make_tool_result(id, "Screen cleared."); } if (strcmp(name, "draw_text") == 0) { cJSON* text_item = cJSON_GetObjectItemCaseSensitive(arguments, "text"); cJSON* x_item = cJSON_GetObjectItemCaseSensitive(arguments, "x"); cJSON* y_item = cJSON_GetObjectItemCaseSensitive(arguments, "y"); cJSON* size_item = cJSON_GetObjectItemCaseSensitive(arguments, "size"); if (!cJSON_IsString(text_item) || text_item->valuestring == NULL || !cJSON_IsNumber(x_item) || !cJSON_IsNumber(y_item)) { return make_error(id, -32602, "draw_text requires text, x, and y"); } if (strlen(text_item->valuestring) > 512) { return make_error(id, -32602, "text exceeds 512 characters"); } int size = cJSON_IsNumber(size_item) ? size_item->valueint : 1; if (size != 1 && size != 2) { return make_error(id, -32602, "size must be 1 or 2"); } if (!mcp::drawText(text_item->valuestring, x_item->valueint, y_item->valueint, size)) { return make_error(id, -32010, "Failed to draw text"); } char message[160]; snprintf( message, sizeof(message), "Successfully drew text at (%d, %d) with size %d.", x_item->valueint, y_item->valueint, size ); return make_tool_result(id, message); } if (strcmp(name, "draw_raw_rgb565") == 0) { cJSON* encoded_item = cJSON_GetObjectItemCaseSensitive(arguments, "rgb565_base64"); cJSON* x_item = cJSON_GetObjectItemCaseSensitive(arguments, "x"); cJSON* y_item = cJSON_GetObjectItemCaseSensitive(arguments, "y"); cJSON* width_item = cJSON_GetObjectItemCaseSensitive(arguments, "w"); cJSON* height_item = cJSON_GetObjectItemCaseSensitive(arguments, "h"); if (!cJSON_IsString(encoded_item) || !cJSON_IsNumber(x_item) || !cJSON_IsNumber(y_item) || !cJSON_IsNumber(width_item) || !cJSON_IsNumber(height_item)) { return make_error(id, -32602, "draw_raw_rgb565 requires rgb565_base64, x, y, w, and h"); } size_t decoded_size = 0; uint8_t* decoded = base64_decode(encoded_item->valuestring, &decoded_size); if (decoded == NULL) { return make_error(id, -32602, "Invalid RGB565 base64 data"); } bool drawn = mcp::drawRgb565( decoded, decoded_size, x_item->valueint, y_item->valueint, width_item->valueint, height_item->valueint ); free(decoded); return drawn ? make_tool_result(id, "Raw RGB565 data displayed successfully.") : make_error(id, -32010, "Failed to draw RGB565 data"); } if (strcmp(name, "draw_color_bmp") == 0) { cJSON* encoded_item = cJSON_GetObjectItemCaseSensitive(arguments, "bmp_base64"); cJSON* x_item = cJSON_GetObjectItemCaseSensitive(arguments, "x"); cJSON* y_item = cJSON_GetObjectItemCaseSensitive(arguments, "y"); if (!cJSON_IsString(encoded_item)) { return make_error(id, -32602, "Missing bmp_base64"); } size_t decoded_size = 0; uint8_t* decoded = base64_decode(encoded_item->valuestring, &decoded_size); if (decoded == NULL) { return make_error(id, -32602, "Invalid BMP base64 data"); } bool drawn = mcp::drawBmp( decoded, decoded_size, cJSON_IsNumber(x_item) ? x_item->valueint : 0, cJSON_IsNumber(y_item) ? y_item->valueint : 0 ); free(decoded); return drawn ? make_tool_result(id, "Color BMP image displayed successfully.") : make_error(id, -32602, "Unsupported or invalid BMP image"); } if (strcmp(name, "draw_image") == 0) { cJSON* encoded_item = cJSON_GetObjectItemCaseSensitive(arguments, "pbm_base64"); cJSON* x_item = cJSON_GetObjectItemCaseSensitive(arguments, "x"); cJSON* y_item = cJSON_GetObjectItemCaseSensitive(arguments, "y"); if (!cJSON_IsString(encoded_item)) { return make_error( id, -32602, "draw_image requires bridge-preprocessed pbm_base64" ); } size_t decoded_size = 0; uint8_t* decoded = base64_decode(encoded_item->valuestring, &decoded_size); if (decoded == NULL) { return make_error(id, -32602, "Invalid PBM base64 data"); } bool drawn = mcp::drawPbm( decoded, decoded_size, cJSON_IsNumber(x_item) ? x_item->valueint : 0, cJSON_IsNumber(y_item) ? y_item->valueint : 0 ); free(decoded); return drawn ? make_tool_result(id, "Image displayed successfully.") : make_error(id, -32602, "Unsupported or invalid PBM image"); } if (strcmp(name, "get_screenshot") == 0) { std::string encoded = mcp::getScreenshotPbmBase64(); if (encoded.empty()) { return make_error(id, -32010, "Screenshot capture failed"); } std::string result = "__PBM_BASE64__:" + encoded; return make_tool_result(id, result.c_str()); } if (strcmp(name, "play_tone") == 0) { cJSON* frequency_item = cJSON_GetObjectItemCaseSensitive(arguments, "frequency"); cJSON* duration_item = cJSON_GetObjectItemCaseSensitive(arguments, "duration_ms"); cJSON* volume_item = cJSON_GetObjectItemCaseSensitive(arguments, "volume"); int frequency = cJSON_IsNumber(frequency_item) ? frequency_item->valueint : 440; int duration = cJSON_IsNumber(duration_item) ? duration_item->valueint : 1000; int volume = cJSON_IsNumber(volume_item) ? volume_item->valueint : 50; if (frequency < 50 || frequency > 10000 || duration < 50 || duration > 5000 || volume < 0 || volume > 100) { return make_error(id, -32602, "frequency, duration_ms, or volume is out of range"); } std::string error; if (!mcp::playTone(frequency, duration, volume, error)) { return make_error(id, -32020, error.c_str()); } char message[128]; snprintf( message, sizeof(message), "Played tone of %dHz for %dms at volume %d.", frequency, duration, volume ); return make_tool_result(id, message); } if (strcmp(name, "record_voice") == 0) { cJSON* duration_item = cJSON_GetObjectItemCaseSensitive(arguments, "duration_sec"); cJSON* filename_item = cJSON_GetObjectItemCaseSensitive(arguments, "filename"); int duration = cJSON_IsNumber(duration_item) ? duration_item->valueint : 4; const char* filename = cJSON_IsString(filename_item) ? filename_item->valuestring : "recording.pcm"; if (duration < 1 || duration > 15) { return make_error(id, -32602, "duration_sec must be between 1 and 15"); } std::string error; size_t recorded_bytes = 0; if (!mcp::recordVoice(duration, filename, recorded_bytes, error)) { return make_error(id, -32020, error.c_str()); } char message[180]; snprintf( message, sizeof(message), "Successfully recorded %d seconds of audio to '%s' (%u bytes).", duration, filename, (unsigned)recorded_bytes ); return make_tool_result(id, message); } if (strcmp(name, "play_audio") == 0) { cJSON* filename_item = cJSON_GetObjectItemCaseSensitive(arguments, "filename"); cJSON* volume_item = cJSON_GetObjectItemCaseSensitive(arguments, "volume"); int volume = cJSON_IsNumber(volume_item) ? volume_item->valueint : 50; if (!cJSON_IsString(filename_item) || filename_item->valuestring == NULL) { return make_error(id, -32602, "play_audio requires filename"); } if (volume < 0 || volume > 100) { return make_error(id, -32602, "volume must be between 0 and 100"); } std::string error; if (!mcp::playAudioFile(filename_item->valuestring, volume, error)) { return make_error(id, -32020, error.c_str()); } char message[160]; snprintf( message, sizeof(message), "Successfully played audio file '%s'.", filename_item->valuestring ); return make_tool_result(id, message); } if (strcmp(name, "play_audio_base64") == 0) { cJSON* encoded_item = cJSON_GetObjectItemCaseSensitive(arguments, "wav_base64"); cJSON* volume_item = cJSON_GetObjectItemCaseSensitive(arguments, "volume"); int volume = cJSON_IsNumber(volume_item) ? volume_item->valueint : 50; if (!cJSON_IsString(encoded_item) || encoded_item->valuestring == NULL) { return make_error(id, -32602, "play_audio_base64 requires wav_base64"); } if (volume < 0 || volume > 100) { return make_error(id, -32602, "volume must be between 0 and 100"); } size_t decoded_size = 0; uint8_t* decoded = base64_decode(encoded_item->valuestring, &decoded_size); if (decoded == NULL) { return make_error(id, -32602, "Invalid WAV base64 data"); } std::string error; bool played = mcp::playWavMemory( decoded, decoded_size, volume, error ); free(decoded); return played ? make_tool_result(id, "Successfully played base64 audio.") : make_error(id, -32020, error.c_str()); } if (strcmp(name, "play_mp3") == 0) { cJSON* filename_item = cJSON_GetObjectItemCaseSensitive(arguments, "filename"); cJSON* volume_item = cJSON_GetObjectItemCaseSensitive(arguments, "volume"); int volume = cJSON_IsNumber(volume_item) ? volume_item->valueint : 50; if (!cJSON_IsString(filename_item) || filename_item->valuestring == NULL) { return make_error(id, -32602, "play_mp3 requires filename"); } if (volume < 0 || volume > 100) { return make_error(id, -32602, "volume must be between 0 and 100"); } std::string error; if (!mcp::playMp3File(filename_item->valuestring, volume, error)) { return make_error(id, -32020, error.c_str()); } char message[160]; snprintf( message, sizeof(message), "Successfully played MP3 file '%s'.", filename_item->valuestring ); return make_tool_result(id, message); } if (strcmp(name, "play_mp3_base64") == 0) { cJSON* encoded_item = cJSON_GetObjectItemCaseSensitive(arguments, "mp3_base64"); cJSON* volume_item = cJSON_GetObjectItemCaseSensitive(arguments, "volume"); int volume = cJSON_IsNumber(volume_item) ? volume_item->valueint : 50; if (!cJSON_IsString(encoded_item) || encoded_item->valuestring == NULL) { return make_error(id, -32602, "play_mp3_base64 requires mp3_base64"); } if (volume < 0 || volume > 100) { return make_error(id, -32602, "volume must be between 0 and 100"); } size_t decoded_size = 0; uint8_t* decoded = base64_decode(encoded_item->valuestring, &decoded_size); if (decoded == NULL) { return make_error(id, -32602, "Invalid MP3 base64 data"); } std::string error; bool played = mcp::playMp3Memory( decoded, decoded_size, volume, error ); free(decoded); return played ? make_tool_result(id, "Successfully played base64 MP3 audio.") : make_error(id, -32020, error.c_str()); } return make_error(id, -32602, "Unknown tool"); } static cJSON* handle_rpc(const char* body) { cJSON* request = cJSON_Parse(body); if (request == NULL) { return make_error(NULL, -32700, "Parse error"); } cJSON* id = cJSON_GetObjectItemCaseSensitive(request, "id"); cJSON* jsonrpc = cJSON_GetObjectItemCaseSensitive(request, "jsonrpc"); cJSON* method = cJSON_GetObjectItemCaseSensitive(request, "method"); cJSON* params = cJSON_GetObjectItemCaseSensitive(request, "params"); if (!cJSON_IsObject(request) || !cJSON_IsString(jsonrpc) || strcmp(jsonrpc->valuestring, "2.0") != 0 || !cJSON_IsString(method)) { cJSON* response = make_error(id, -32600, "Invalid Request"); cJSON_Delete(request); return response; } cJSON* response = NULL; if (strcmp(method->valuestring, "tools/list") == 0) { cJSON* tools = cJSON_Parse(TOOLS_JSON); if (tools == NULL) { response = make_error(id, -32603, "Failed to construct tool list"); } else { response = cJSON_CreateObject(); cJSON_AddStringToObject(response, "jsonrpc", "2.0"); cJSON* result = cJSON_AddObjectToObject(response, "result"); cJSON_AddItemToObject(result, "tools", tools); if (id != NULL) { cJSON_AddItemToObject(response, "id", cJSON_Duplicate(id, true)); } else { cJSON_AddNullToObject(response, "id"); } } } else if (strcmp(method->valuestring, "tools/call") == 0) { response = handle_tool_call(id, params); } else { response = make_error(id, -32601, "Method not found"); } cJSON_Delete(request); return response; } // POST /api/mcp esp_err_t WebServerService::handleApiMcp(httpd_req_t* request) { LOGGER.info("POST /api/mcp"); // Load MCP Settings and check if enabled auto mcpSettings = settings::mcp::loadOrGetDefault(); if (!mcpSettings.mcpEnabled) { httpd_resp_send_err(request, HTTPD_404_NOT_FOUND, "MCP server is disabled"); return ESP_FAIL; } int content_len = request->content_len; if (content_len <= 0 || content_len > MCP_JSON_BODY_LIMIT) { httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Invalid Content-Length"); return ESP_FAIL; } char* body = (char*)psram_malloc(content_len + 1); if (body == NULL) { httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Out of memory"); return ESP_FAIL; } int received = 0; while (received < content_len) { int ret = httpd_req_recv(request, body + received, content_len - received); if (ret <= 0) { if (ret == HTTPD_SOCK_ERR_TIMEOUT) { continue; } free(body); httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Incomplete request body"); return ESP_FAIL; } received += ret; } body[content_len] = '\0'; cJSON* response = handle_rpc(body); free(body); char* response_text = cJSON_PrintUnformatted(response); cJSON_Delete(response); if (response_text == NULL) { httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Serialization failed"); return ESP_FAIL; } httpd_resp_set_type(request, "application/json"); httpd_resp_set_hdr(request, "Access-Control-Allow-Origin", "*"); httpd_resp_sendstr(request, response_text); cJSON_free(response_text); return ESP_OK; } static int query_integer(const char* path, const char* name, int default_value) { const char* query = strchr(path, '?'); if (query == NULL) { return default_value; } query++; size_t name_size = strlen(name); while (*query != '\0') { if (strncmp(query, name, name_size) == 0 && query[name_size] == '=') { return atoi(query + name_size + 1); } query = strchr(query, '&'); if (query == NULL) { break; } query++; } return default_value; } // POST /api/screen/raw esp_err_t WebServerService::handleApiScreenRaw(httpd_req_t* request) { LOGGER.info("POST /api/screen/raw"); // Load MCP Settings and check if enabled auto mcpSettings = settings::mcp::loadOrGetDefault(); if (!mcpSettings.mcpEnabled) { httpd_resp_send_err(request, HTTPD_404_NOT_FOUND, "MCP server is disabled"); return ESP_FAIL; } int content_len = request->content_len; if (content_len <= 0 || content_len > MCP_RAW_BODY_LIMIT) { httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Invalid Content-Length"); return ESP_FAIL; } uint8_t* body = (uint8_t*)psram_malloc(content_len); if (body == NULL) { httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Out of memory"); return ESP_FAIL; } int received = 0; while (received < content_len) { int ret = httpd_req_recv(request, (char*)body + received, content_len - received); if (ret <= 0) { if (ret == HTTPD_SOCK_ERR_TIMEOUT) { continue; } free(body); httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Incomplete request body"); return ESP_FAIL; } received += ret; } auto& state = mcp::getState(); const char* path = request->uri; int x = query_integer(path, "x", 0); int y = query_integer(path, "y", 0); int width = query_integer(path, "w", state.drawWidth); int height = query_integer(path, "h", state.drawHeight); bool valid_size = width > 0 && height > 0 && (size_t)width * height * 2 == (size_t)content_len; bool drawn = valid_size && mcp::drawRgb565( body, content_len, x, y, width, height ); free(body); if (drawn) { httpd_resp_set_type(request, "text/plain"); httpd_resp_set_hdr(request, "Access-Control-Allow-Origin", "*"); httpd_resp_sendstr(request, "OK"); return ESP_OK; } else { httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Invalid RGB565 payload or size"); return ESP_FAIL; } } } // namespace tt::service::webserver #endif