feat: add McpScreen phase 2 display tools
Main / Build (Brainfuck) (push) Has been cancelled
Main / Build (Breakout) (push) Has been cancelled
Main / Build (Calculator) (push) Has been cancelled
Main / Build (Diceware) (push) Has been cancelled
Main / Build (EpubReader) (push) Has been cancelled
Main / Build (GPIO) (push) Has been cancelled
Main / Build (GraphicsDemo) (push) Has been cancelled
Main / Build (HelloWorld) (push) Has been cancelled
Main / Build (M5UnitTest) (push) Has been cancelled
Main / Build (Magic8Ball) (push) Has been cancelled
Main / Build (MediaKeys) (push) Has been cancelled
Main / Build (MystifyDemo) (push) Has been cancelled
Main / Build (SerialConsole) (push) Has been cancelled
Main / Build (Snake) (push) Has been cancelled
Main / Build (TamaTac) (push) Has been cancelled
Main / Build (TodoList) (push) Has been cancelled
Main / Build (TwoEleven) (push) Has been cancelled
Main / Bundle (push) Has been cancelled
Main / Build (Brainfuck) (push) Has been cancelled
Main / Build (Breakout) (push) Has been cancelled
Main / Build (Calculator) (push) Has been cancelled
Main / Build (Diceware) (push) Has been cancelled
Main / Build (EpubReader) (push) Has been cancelled
Main / Build (GPIO) (push) Has been cancelled
Main / Build (GraphicsDemo) (push) Has been cancelled
Main / Build (HelloWorld) (push) Has been cancelled
Main / Build (M5UnitTest) (push) Has been cancelled
Main / Build (Magic8Ball) (push) Has been cancelled
Main / Build (MediaKeys) (push) Has been cancelled
Main / Build (MystifyDemo) (push) Has been cancelled
Main / Build (SerialConsole) (push) Has been cancelled
Main / Build (Snake) (push) Has been cancelled
Main / Build (TamaTac) (push) Has been cancelled
Main / Build (TodoList) (push) Has been cancelled
Main / Build (TwoEleven) (push) Has been cancelled
Main / Bundle (push) Has been cancelled
This commit is contained in:
@@ -15,8 +15,9 @@
|
||||
|
||||
#define MCP_HTTP_PORT 80
|
||||
#define MCP_HEADER_LIMIT 2048
|
||||
#define MCP_BODY_LIMIT 8192
|
||||
#define MCP_RESPONSE_LIMIT 8192
|
||||
#define MCP_JSON_BODY_LIMIT (256 * 1024)
|
||||
#define MCP_RAW_BODY_LIMIT (512 * 1024)
|
||||
#define MCP_RESPONSE_LIMIT (64 * 1024)
|
||||
|
||||
static const char* TAG = "McpServer";
|
||||
|
||||
@@ -49,9 +50,110 @@ static const char* TOOLS_JSON =
|
||||
"},"
|
||||
"\"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\":{}}"
|
||||
"}"
|
||||
"]";
|
||||
|
||||
static int base64_value(char character) {
|
||||
if (character >= 'A' && character <= 'Z') return character - 'A';
|
||||
if (character >= 'a' && character <= 'z') return character - 'a' + 26;
|
||||
if (character >= '0' && character <= '9') return character - '0' + 52;
|
||||
if (character == '+') return 62;
|
||||
if (character == '/') return 63;
|
||||
return -1;
|
||||
}
|
||||
|
||||
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);
|
||||
uint8_t* output = malloc((input_size / 4) * 3 + 3);
|
||||
if (output == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
uint32_t accumulator = 0;
|
||||
int bits = 0;
|
||||
size_t written = 0;
|
||||
for (size_t index = 0; index < input_size; ++index) {
|
||||
char character = payload[index];
|
||||
if (character == '=') {
|
||||
break;
|
||||
}
|
||||
int value = base64_value(character);
|
||||
if (value < 0) {
|
||||
if (character == '\r' || character == '\n' ||
|
||||
character == ' ' || character == '\t') {
|
||||
continue;
|
||||
}
|
||||
free(output);
|
||||
return NULL;
|
||||
}
|
||||
accumulator = (accumulator << 6) | (uint32_t)value;
|
||||
bits += 6;
|
||||
if (bits >= 8) {
|
||||
bits -= 8;
|
||||
output[written++] = (uint8_t)((accumulator >> bits) & 0xFF);
|
||||
}
|
||||
}
|
||||
*output_size = written;
|
||||
return output;
|
||||
}
|
||||
|
||||
static void close_socket(int* socket_fd) {
|
||||
if (socket_fd != NULL && *socket_fd >= 0) {
|
||||
close(*socket_fd);
|
||||
@@ -125,15 +227,17 @@ static cJSON* handle_tool_call(McpScreenState* state, cJSON* id, cJSON* params)
|
||||
|
||||
if (strcmp(name, "get_capabilities") == 0) {
|
||||
char capabilities[320];
|
||||
uint16_t width = state->draw_width > 0 ? state->draw_width : state->display_width;
|
||||
uint16_t height = state->draw_height > 0 ? state->draw_height : state->display_height;
|
||||
snprintf(
|
||||
capabilities,
|
||||
sizeof(capabilities),
|
||||
"{\"color\":true,\"width\":%u,\"height\":%u,"
|
||||
"\"formats\":[\"rgb565_base64\",\"bmp_base64\"],"
|
||||
"\"formats\":[\"rgb565_base64\",\"bmp_base64\",\"pbm_base64\"],"
|
||||
"\"platform\":\"tactility\",\"appVersion\":\"0.1.0\","
|
||||
"\"uiVisible\":%s}",
|
||||
state->display_width,
|
||||
state->display_height,
|
||||
width,
|
||||
height,
|
||||
state->visible ? "true" : "false"
|
||||
);
|
||||
return make_tool_result(id, capabilities);
|
||||
@@ -191,6 +295,110 @@ static cJSON* handle_tool_call(McpScreenState* state, cJSON* id, cJSON* params)
|
||||
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_ui_draw_rgb565_be(
|
||||
state,
|
||||
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_ui_draw_bmp(
|
||||
state,
|
||||
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_ui_draw_pbm(
|
||||
state,
|
||||
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) {
|
||||
char* encoded = mcp_ui_get_screenshot_pbm_base64(state);
|
||||
if (encoded == NULL) {
|
||||
return make_error(id, -32010, "Screenshot capture failed");
|
||||
}
|
||||
size_t result_size = strlen(encoded) + 17;
|
||||
char* result = malloc(result_size);
|
||||
if (result == NULL) {
|
||||
free(encoded);
|
||||
return make_error(id, -32603, "Out of memory");
|
||||
}
|
||||
snprintf(result, result_size, "__PBM_BASE64__:%s", encoded);
|
||||
cJSON* response = make_tool_result(id, result);
|
||||
free(result);
|
||||
free(encoded);
|
||||
return response;
|
||||
}
|
||||
|
||||
return make_error(id, -32602, "Unknown tool");
|
||||
}
|
||||
|
||||
@@ -289,94 +497,164 @@ static void send_http_response(int client, int status, const char* content_type,
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static void handle_http_client(McpScreenState* state, int client) {
|
||||
struct timeval timeout = { .tv_sec = 0, .tv_usec = 500000 };
|
||||
struct timeval timeout = { .tv_sec = 0, .tv_usec = 250000 };
|
||||
setsockopt(client, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
||||
setsockopt(client, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
|
||||
|
||||
char* request = calloc(1, MCP_HEADER_LIMIT + MCP_BODY_LIMIT + 1);
|
||||
if (request == NULL) {
|
||||
char* headers = calloc(1, MCP_HEADER_LIMIT + 1);
|
||||
if (headers == NULL) {
|
||||
send_http_response(client, 400, "text/plain", "Out of memory");
|
||||
return;
|
||||
}
|
||||
|
||||
int received = 0;
|
||||
int header_end = -1;
|
||||
while (received < MCP_HEADER_LIMIT) {
|
||||
int count = recv(client, request + received, MCP_HEADER_LIMIT - received, 0);
|
||||
while (state->running && received < MCP_HEADER_LIMIT) {
|
||||
int count = recv(client, headers + received, MCP_HEADER_LIMIT - received, 0);
|
||||
if (count <= 0) {
|
||||
free(request);
|
||||
free(headers);
|
||||
return;
|
||||
}
|
||||
received += count;
|
||||
header_end = find_header_end(request, received);
|
||||
header_end = find_header_end(headers, received);
|
||||
if (header_end >= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (header_end < 0) {
|
||||
send_http_response(client, 400, "text/plain", "Invalid HTTP headers");
|
||||
free(request);
|
||||
if (!state->running) {
|
||||
free(headers);
|
||||
return;
|
||||
}
|
||||
|
||||
request[header_end - 4] = '\0';
|
||||
if (header_end < 0) {
|
||||
send_http_response(client, 400, "text/plain", "Invalid HTTP headers");
|
||||
free(headers);
|
||||
return;
|
||||
}
|
||||
|
||||
headers[header_end - 4] = '\0';
|
||||
char method[12] = {0};
|
||||
char path[128] = {0};
|
||||
if (sscanf(request, "%11s %127s", method, path) != 2) {
|
||||
if (sscanf(headers, "%11s %127s", method, path) != 2) {
|
||||
send_http_response(client, 400, "text/plain", "Invalid request line");
|
||||
free(request);
|
||||
free(headers);
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(method, "POST") != 0) {
|
||||
send_http_response(client, 405, "text/plain", "POST required");
|
||||
free(request);
|
||||
return;
|
||||
}
|
||||
if (strcmp(path, "/api/mcp") != 0) {
|
||||
send_http_response(client, 404, "text/plain", "Not found");
|
||||
free(request);
|
||||
free(headers);
|
||||
return;
|
||||
}
|
||||
|
||||
int content_length = parse_content_length(request);
|
||||
if (content_length <= 0 || content_length > MCP_BODY_LIMIT) {
|
||||
bool is_mcp = strcmp(path, "/api/mcp") == 0;
|
||||
bool is_raw = strncmp(path, "/api/screen/raw", 15) == 0;
|
||||
if (!is_mcp && !is_raw) {
|
||||
send_http_response(client, 404, "text/plain", "Not found");
|
||||
free(headers);
|
||||
return;
|
||||
}
|
||||
|
||||
int body_limit = is_raw ? MCP_RAW_BODY_LIMIT : MCP_JSON_BODY_LIMIT;
|
||||
int content_length = parse_content_length(headers);
|
||||
if (content_length <= 0 || content_length > body_limit) {
|
||||
send_http_response(
|
||||
client,
|
||||
content_length > MCP_BODY_LIMIT ? 413 : 400,
|
||||
content_length > body_limit ? 413 : 400,
|
||||
"text/plain",
|
||||
"Invalid Content-Length"
|
||||
);
|
||||
free(request);
|
||||
free(headers);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t* body = malloc((size_t)content_length + (is_mcp ? 1 : 0));
|
||||
if (body == NULL) {
|
||||
send_http_response(client, 400, "text/plain", "Out of memory");
|
||||
free(headers);
|
||||
return;
|
||||
}
|
||||
|
||||
int body_received = received - header_end;
|
||||
memmove(request, request + header_end, body_received);
|
||||
while (body_received < content_length) {
|
||||
int count = recv(client, request + body_received, content_length - body_received, 0);
|
||||
if (body_received > content_length) {
|
||||
body_received = content_length;
|
||||
}
|
||||
memcpy(body, headers + header_end, body_received);
|
||||
free(headers);
|
||||
|
||||
while (state->running && body_received < content_length) {
|
||||
int count = recv(client, body + body_received, content_length - body_received, 0);
|
||||
if (count <= 0) {
|
||||
send_http_response(client, 400, "text/plain", "Incomplete request body");
|
||||
free(request);
|
||||
free(body);
|
||||
return;
|
||||
}
|
||||
body_received += count;
|
||||
}
|
||||
request[content_length] = '\0';
|
||||
|
||||
cJSON* response = handle_rpc(state, request);
|
||||
char* response_text = cJSON_PrintUnformatted(response);
|
||||
if (response_text != NULL && strlen(response_text) <= MCP_RESPONSE_LIMIT) {
|
||||
send_http_response(client, 200, "application/json", response_text);
|
||||
} else {
|
||||
send_http_response(client, 400, "text/plain", "Response serialization failed");
|
||||
if (!state->running || body_received < content_length) {
|
||||
free(body);
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON_free(response_text);
|
||||
cJSON_Delete(response);
|
||||
free(request);
|
||||
if (is_raw) {
|
||||
int x = query_integer(path, "x", 0);
|
||||
int y = query_integer(path, "y", 0);
|
||||
int width = query_integer(path, "w", state->draw_width);
|
||||
int height = query_integer(path, "h", state->draw_height);
|
||||
bool valid_size = width > 0 && height > 0 &&
|
||||
(size_t)width * height * 2 == (size_t)content_length;
|
||||
bool drawn = valid_size && mcp_ui_draw_rgb565_be(
|
||||
state,
|
||||
body,
|
||||
content_length,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height
|
||||
);
|
||||
send_http_response(
|
||||
client,
|
||||
drawn ? 200 : 400,
|
||||
"text/plain",
|
||||
drawn ? "OK" : "Invalid RGB565 payload"
|
||||
);
|
||||
} else {
|
||||
body[content_length] = '\0';
|
||||
cJSON* response = handle_rpc(state, (const char*)body);
|
||||
char* response_text = cJSON_PrintUnformatted(response);
|
||||
if (response_text != NULL && strlen(response_text) <= MCP_RESPONSE_LIMIT) {
|
||||
send_http_response(client, 200, "application/json", response_text);
|
||||
} else {
|
||||
send_http_response(client, 400, "text/plain", "Response serialization failed");
|
||||
}
|
||||
cJSON_free(response_text);
|
||||
cJSON_Delete(response);
|
||||
}
|
||||
|
||||
free(body);
|
||||
}
|
||||
|
||||
static void http_task(void* argument) {
|
||||
@@ -455,11 +733,28 @@ suspend_for_owner:
|
||||
|
||||
bool mcp_services_start(McpScreenState* state) {
|
||||
if (state == NULL || state->running) {
|
||||
ESP_LOGE(TAG, "Service start rejected state=%p running=%d", state, state != NULL && state->running);
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* onHide only requests shutdown because it runs in Tactility's GUI
|
||||
* lifecycle. Reap the worker after it has reached its safe suspended
|
||||
* state before starting a new foreground server.
|
||||
*/
|
||||
if (state->http_task != NULL) {
|
||||
if (!state->http_exited) {
|
||||
ESP_LOGE(TAG, "Previous HTTP worker is still stopping");
|
||||
return false;
|
||||
}
|
||||
vTaskDelete(state->http_task);
|
||||
state->http_task = NULL;
|
||||
}
|
||||
|
||||
state->running = true;
|
||||
state->http_exited = false;
|
||||
state->http_socket = -1;
|
||||
state->client_socket = -1;
|
||||
BaseType_t http_result = xTaskCreate(
|
||||
http_task,
|
||||
"mcp_http",
|
||||
@@ -485,7 +780,7 @@ bool mcp_services_start(McpScreenState* state) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void mcp_services_stop(McpScreenState* state) {
|
||||
void mcp_services_hide(McpScreenState* state) {
|
||||
if (state == NULL) {
|
||||
return;
|
||||
}
|
||||
@@ -493,12 +788,17 @@ void mcp_services_stop(McpScreenState* state) {
|
||||
state->running = false;
|
||||
state->http_ready = false;
|
||||
state->discovery_ready = false;
|
||||
close_socket(&state->http_socket);
|
||||
close_socket(&state->client_socket);
|
||||
close_socket(&state->discovery_socket);
|
||||
}
|
||||
|
||||
void mcp_services_stop(McpScreenState* state) {
|
||||
if (state == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
mcp_services_hide(state);
|
||||
|
||||
for (int attempt = 0; attempt < 80; ++attempt) {
|
||||
if (state->http_exited) {
|
||||
if (state->http_task == NULL || state->http_exited) {
|
||||
break;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
@@ -506,11 +806,15 @@ void mcp_services_stop(McpScreenState* state) {
|
||||
|
||||
if (state->http_task != NULL) {
|
||||
/*
|
||||
* Normal path: worker is suspended after closing its sockets.
|
||||
* Fallback path: force-delete after 800 ms so loader_dispatch can
|
||||
* never be held until the task watchdog fires.
|
||||
* Normal path: the non-blocking worker has closed its sockets and
|
||||
* suspended itself. The bounded fallback guarantees that no
|
||||
* external-ELF instruction survives onDestroy.
|
||||
*/
|
||||
vTaskDelete(state->http_task);
|
||||
state->http_task = NULL;
|
||||
}
|
||||
close_socket(&state->http_socket);
|
||||
close_socket(&state->client_socket);
|
||||
close_socket(&state->discovery_socket);
|
||||
state->http_exited = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user