feat: integrate MCP and ES3C28P audio support with upstream compatibility updates

This commit is contained in:
Adolfo Reyna
2026-07-04 15:41:37 -04:00
parent b8bf59fedf
commit 300ddb3a7f
21 changed files with 1260 additions and 33 deletions
@@ -157,6 +157,116 @@ static const char* TOOLS_JSON =
"\"mp3_base64\":{\"type\":\"string\"},"
"\"volume\":{\"type\":\"integer\",\"default\":50}"
"},\"required\":[\"mp3_base64\"]}"
"},"
"{"
"\"name\":\"set_led\","
"\"description\":\"Control the onboard WS2812 NeoPixel RGB LED.\","
"\"inputSchema\":{"
"\"type\":\"object\","
"\"properties\":{"
"\"r\":{\"type\":\"integer\",\"minimum\":0,\"maximum\":255,\"description\":\"Red channel (0-255)\"},"
"\"g\":{\"type\":\"integer\",\"minimum\":0,\"maximum\":255,\"description\":\"Green channel (0-255)\"},"
"\"b\":{\"type\":\"integer\",\"minimum\":0,\"maximum\":255,\"description\":\"Blue channel (0-255)\"},"
"\"mode\":{\"type\":\"string\",\"enum\":[\"static\",\"breath\",\"rainbow\",\"off\"],\"description\":\"LED mode\"}"
"},"
"\"required\":[\"r\",\"g\",\"b\",\"mode\"]"
"}"
"},"
"{"
"\"name\":\"get_battery\","
"\"description\":\"Read the current battery voltage and estimated capacity percentage.\","
"\"inputSchema\":{\"type\":\"object\",\"properties\":{}}"
"},"
"{"
"\"name\":\"get_sensors\","
"\"description\":\"Read onboard sensors (IMU, temperature, humidity).\","
"\"inputSchema\":{\"type\":\"object\",\"properties\":{}}"
"},"
"{"
"\"name\":\"scan_ble\","
"\"description\":\"Scan for nearby Bluetooth Low Energy devices.\","
"\"inputSchema\":{"
"\"type\":\"object\","
"\"properties\":{"
"\"duration_ms\":{\"type\":\"integer\",\"default\":3000,\"description\":\"Scan duration in milliseconds\"}"
"}"
"}"
"},"
"{"
"\"name\":\"write_file\","
"\"description\":\"Write a file to the microSD card.\","
"\"inputSchema\":{"
"\"type\":\"object\","
"\"properties\":{"
"\"path\":{\"type\":\"string\",\"description\":\"Destination path relative to /sdcard/\"},"
"\"content\":{\"type\":\"string\",\"description\":\"Text content of the file\"}"
"},"
"\"required\":[\"path\",\"content\"]"
"}"
"},"
"{"
"\"name\":\"read_file\","
"\"description\":\"Read a text file from the microSD card.\","
"\"inputSchema\":{"
"\"type\":\"object\","
"\"properties\":{"
"\"path\":{\"type\":\"string\",\"description\":\"File path relative to /sdcard/\"}"
"},"
"\"required\":[\"path\"]"
"}"
"},"
"{"
"\"name\":\"download_file\","
"\"description\":\"Download a file from a URL directly to the microSD card.\","
"\"inputSchema\":{"
"\"type\":\"object\","
"\"properties\":{"
"\"url\":{\"type\":\"string\",\"description\":\"The URL of the file to download\"},"
"\"filename\":{\"type\":\"string\",\"description\":\"The destination filename relative to /sdcard/\"}"
"},"
"\"required\":[\"url\",\"filename\"]"
"}"
"},"
"{"
"\"name\":\"get_video_streaming_instructions\","
"\"description\":\"Get details for monochrome TCP streaming and RGB565 color TCP streaming.\","
"\"inputSchema\":{"
"\"type\":\"object\","
"\"properties\":{"
"\"protocol\":{\"type\":\"string\",\"enum\":[\"tcp\",\"color\",\"both\",\"all\"],\"default\":\"all\"}"
"}"
"}"
"},"
"{"
"\"name\":\"get_stream_stats\","
"\"description\":\"Get performance statistics for the active video streams.\","
"\"inputSchema\":{\"type\":\"object\",\"properties\":{}}"
"},"
"{"
"\"name\":\"list_apps\","
"\"description\":\"List all applications installed on the system, including metadata and locations.\","
"\"inputSchema\":{\"type\":\"object\",\"properties\":{}}"
"},"
"{"
"\"name\":\"run_app\","
"\"description\":\"Launch an application on the board by its App ID.\","
"\"inputSchema\":{"
"\"type\":\"object\","
"\"properties\":{"
"\"app_id\":{\"type\":\"string\",\"description\":\"The unique ID of the application (e.g., 'one.tactility.helloworld')\"}"
"},"
"\"required\":[\"app_id\"]"
"}"
"},"
"{"
"\"name\":\"list_files\","
"\"description\":\"List files and folders in a directory on the SD card, with details like sizes.\","
"\"inputSchema\":{"
"\"type\":\"object\","
"\"properties\":{"
"\"path\":{\"type\":\"string\",\"description\":\"Path relative to /sdcard/ to list (default is '/')\"}"
"}"
"}"
"}"
"]";
@@ -173,7 +283,7 @@ static uint8_t* base64_decode(const char* input, size_t* output_size) {
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);
uint8_t* output = (uint8_t*)malloc(approx_output_len + 4);
if (output == NULL) {
return NULL;
}
@@ -564,6 +674,162 @@ static cJSON* handle_tool_call(cJSON* id, cJSON* params) {
? make_tool_result(id, "Successfully played base64 MP3 audio.")
: make_error(id, -32020, error.c_str());
}
if (strcmp(name, "set_led") == 0) {
cJSON* r_item = cJSON_GetObjectItemCaseSensitive(arguments, "r");
cJSON* g_item = cJSON_GetObjectItemCaseSensitive(arguments, "g");
cJSON* b_item = cJSON_GetObjectItemCaseSensitive(arguments, "b");
cJSON* mode_item = cJSON_GetObjectItemCaseSensitive(arguments, "mode");
if (!cJSON_IsNumber(r_item) || !cJSON_IsNumber(g_item) ||
!cJSON_IsNumber(b_item) || !cJSON_IsString(mode_item)) {
return make_error(id, -32602, "set_led requires r, g, b, and mode");
}
std::string error;
if (!mcp::setLedColor(r_item->valueint, g_item->valueint, b_item->valueint, mode_item->valuestring, error)) {
return make_error(id, -32020, error.c_str());
}
char msg[128];
snprintf(msg, sizeof(msg), "LED set to mode '%s' with base color (%d, %d, %d).",
mode_item->valuestring, r_item->valueint, g_item->valueint, b_item->valueint);
return make_tool_result(id, msg);
}
if (strcmp(name, "get_battery") == 0) {
double voltage_v = 0.0;
int percentage_pct = 0;
std::string error;
if (!mcp::getBatteryStatus(voltage_v, percentage_pct, error)) {
return make_error(id, -32020, error.c_str());
}
char result_buf[128];
snprintf(result_buf, sizeof(result_buf), "{\"voltage_v\":%.3f,\"percentage_pct\":%d}", voltage_v, percentage_pct);
return make_tool_result(id, result_buf);
}
if (strcmp(name, "get_sensors") == 0) {
double temp_c = 0.0;
double hum_pct = 0.0;
std::string imu_json;
std::string error;
if (!mcp::getSensors(temp_c, hum_pct, imu_json, error)) {
return make_error(id, -32020, error.c_str());
}
char result_buf[512];
snprintf(result_buf, sizeof(result_buf), "{\"temperature_c\":%.2f,\"humidity_pct\":%.2f,\"imu\":%s}",
temp_c, hum_pct, imu_json.c_str());
return make_tool_result(id, result_buf);
}
if (strcmp(name, "scan_ble") == 0) {
cJSON* duration_item = cJSON_GetObjectItemCaseSensitive(arguments, "duration_ms");
int duration_ms = cJSON_IsNumber(duration_item) ? duration_item->valueint : 3000;
if (duration_ms < 500 || duration_ms > 15000) {
return make_error(id, -32602, "duration_ms must be between 500 and 15000");
}
std::string devices_json;
std::string error;
if (!mcp::scanBleDevices(duration_ms, devices_json, error)) {
return make_error(id, -32020, error.c_str());
}
return make_tool_result(id, devices_json.c_str());
}
if (strcmp(name, "write_file") == 0) {
cJSON* path_item = cJSON_GetObjectItemCaseSensitive(arguments, "path");
cJSON* content_item = cJSON_GetObjectItemCaseSensitive(arguments, "content");
if (!cJSON_IsString(path_item) || !cJSON_IsString(content_item)) {
return make_error(id, -32602, "write_file requires path and content");
}
std::string error;
if (!mcp::writeSdFile(path_item->valuestring, content_item->valuestring, error)) {
return make_error(id, -32020, error.c_str());
}
return make_tool_result(id, "Successfully wrote file to SD card.");
}
if (strcmp(name, "read_file") == 0) {
cJSON* path_item = cJSON_GetObjectItemCaseSensitive(arguments, "path");
if (!cJSON_IsString(path_item)) {
return make_error(id, -32602, "read_file requires path");
}
std::string content;
std::string error;
if (!mcp::readSdFile(path_item->valuestring, content, error)) {
return make_error(id, -32020, error.c_str());
}
return make_tool_result(id, content.c_str());
}
if (strcmp(name, "download_file") == 0) {
cJSON* url_item = cJSON_GetObjectItemCaseSensitive(arguments, "url");
cJSON* filename_item = cJSON_GetObjectItemCaseSensitive(arguments, "filename");
if (!cJSON_IsString(url_item) || !cJSON_IsString(filename_item)) {
return make_error(id, -32602, "download_file requires url and filename");
}
std::string error;
if (!mcp::downloadSdFile(url_item->valuestring, filename_item->valuestring, error)) {
return make_error(id, -32020, error.c_str());
}
return make_tool_result(id, "Successfully downloaded file to SD card.");
}
if (strcmp(name, "get_video_streaming_instructions") == 0) {
cJSON* protocol_item = cJSON_GetObjectItemCaseSensitive(arguments, "protocol");
const char* protocol = cJSON_IsString(protocol_item) ? protocol_item->valuestring : "all";
char buf[512];
snprintf(buf, sizeof(buf),
"Tactility OS TCP Video Streaming Instructions:\n"
"1. Mono TCP Stream (compatibility mode): Stream raw 15,000-byte PBM/RLCD frames to TCP port 8081.\n"
"2. Color TCP Stream (native): Stream packets to TCP port 8083.\n"
" Packet format:\n"
" - Header (16 bytes): Magic 'RAW\\x01' (4 bytes), X-coord (2 bytes BE), Y-coord (2 bytes BE), Width (2 bytes BE), Height (2 bytes BE), Payload length (4 bytes BE).\n"
" - Payload (variable): Raw big-endian RGB565 pixel data of length 'Payload length'.\n"
"Active protocol selection: %s", protocol);
return make_tool_result(id, buf);
}
if (strcmp(name, "get_stream_stats") == 0) {
std::string stats_json;
if (!mcp::getVideoStreamStats(stats_json)) {
return make_error(id, -32020, "Failed to get stream stats");
}
return make_tool_result(id, stats_json.c_str());
}
if (strcmp(name, "list_apps") == 0) {
std::string apps_json;
std::string error;
if (!mcp::listApps(apps_json, error)) {
return make_error(id, -32020, error.c_str());
}
return make_tool_result(id, apps_json.c_str());
}
if (strcmp(name, "run_app") == 0) {
cJSON* app_id_item = cJSON_GetObjectItemCaseSensitive(arguments, "app_id");
if (!cJSON_IsString(app_id_item)) {
return make_error(id, -32602, "run_app requires app_id");
}
std::string error;
if (!mcp::runApp(app_id_item->valuestring, error)) {
return make_error(id, -32020, error.c_str());
}
char msg[128];
snprintf(msg, sizeof(msg), "Successfully started application '%s'.", app_id_item->valuestring);
return make_tool_result(id, msg);
}
if (strcmp(name, "list_files") == 0) {
cJSON* path_item = cJSON_GetObjectItemCaseSensitive(arguments, "path");
const char* path = cJSON_IsString(path_item) ? path_item->valuestring : "/";
std::string files_json;
std::string error;
if (!mcp::listSdFiles(path, files_json, error)) {
return make_error(id, -32020, error.c_str());
}
return make_tool_result(id, files_json.c_str());
}
return make_error(id, -32602, "Unknown tool");
}