Files
xiaozhi-esp32/main/boards/otto-robot/websocket_control_server.cc
小鹏 67bf599149 fix(otto): WebSocket direct clients not receiving MCP responses (#1992)
* Enhance Otto Robot camera support by adding configuration for OV3660. Updated config.h to define camera types and GPIO settings, modified config.json to include new camera options, and refactored otto_robot.cc for improved camera detection and initialization logic.

* fix: 移除 OttoEmojiDisplay 构造函数中的 SetTheme 调用以修复 LoadProhibited 崩溃

Made-with: Cursor

* refactor: improve audio service error handling and codec timeout management

- Updated AudioService to prevent input task termination on read timeout, introducing a delay instead.
- Enhanced NoAudioCodec to implement a read timeout for I2S channel reads.
- Adjusted WebSocketControlServer to set a control port for improved socket management.
- Added manufacturer information to the config.json for waveshare ESP32-Touch-LCD-3.5.

* fix(otto): WebSocket direct clients not receiving MCP responses

When a browser connects directly to the WebSocket control server (port
8080) and sends a JSON-RPC request, the MCP response was routed through
Application::SendMcpMessage -> protocol_->SendMcpMessage, which sends it
to the cloud protocol channel. As a result, the direct WebSocket client
never received the response, while the WeChat mini-program could because
it communicates via the cloud.

Fix:
- Add BroadcastMessage() to WebSocketControlServer, using
  httpd_queue_work + httpd_ws_send_frame_async to asynchronously
  send responses back to all connected clients on port 8080
- Add RegisterMcpBroadcastCallback() to Application, allowing an
  additional MCP send callback to be registered; SendMcpMessage()
  now invokes it alongside the cloud protocol
- Register the broadcast callback in OttoRobot after the WebSocket
  server starts successfully

Also add WebSocket direct-connect API documentation to README.md
with complete JSON-RPC 2.0 command examples.
2026-05-14 14:35:49 +08:00

251 lines
7.2 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "websocket_control_server.h"
#include "mcp_server.h"
#include <esp_log.h>
#include <esp_http_server.h>
#include <sys/param.h>
#include <cstring>
#include <cstdlib>
#include <map>
static const char* TAG = "WSControl";
WebSocketControlServer* WebSocketControlServer::instance_ = nullptr;
WebSocketControlServer::WebSocketControlServer() : server_handle_(nullptr) {
instance_ = this;
}
WebSocketControlServer::~WebSocketControlServer() {
Stop();
instance_ = nullptr;
}
esp_err_t WebSocketControlServer::ws_handler(httpd_req_t *req) {
if (instance_ == nullptr) {
return ESP_FAIL;
}
if (req->method == HTTP_GET) {
ESP_LOGI(TAG, "Handshake done, the new connection was opened");
instance_->AddClient(req);
return ESP_OK;
}
httpd_ws_frame_t ws_pkt;
uint8_t *buf = NULL;
memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
ws_pkt.type = HTTPD_WS_TYPE_TEXT;
/* Set max_len = 0 to get the frame len */
esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "httpd_ws_recv_frame failed to get frame len with %d", ret);
return ret;
}
ESP_LOGI(TAG, "frame len is %d", ws_pkt.len);
if (ws_pkt.len) {
/* ws_pkt.len + 1 is for NULL termination as we are expecting a string */
buf = (uint8_t*)calloc(1, ws_pkt.len + 1);
if (buf == NULL) {
ESP_LOGE(TAG, "Failed to calloc memory for buf");
return ESP_ERR_NO_MEM;
}
ws_pkt.payload = buf;
/* Set max_len = ws_pkt.len to get the frame payload */
ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "httpd_ws_recv_frame failed with %d", ret);
free(buf);
return ret;
}
ESP_LOGI(TAG, "Got packet with message: %s", ws_pkt.payload);
}
ESP_LOGI(TAG, "Packet type: %d", ws_pkt.type);
if (ws_pkt.type == HTTPD_WS_TYPE_CLOSE) {
ESP_LOGI(TAG, "WebSocket close frame received");
instance_->RemoveClient(req);
free(buf);
return ESP_OK;
}
if (ws_pkt.type == HTTPD_WS_TYPE_TEXT) {
if (ws_pkt.len > 0 && buf != nullptr) {
buf[ws_pkt.len] = '\0';
instance_->HandleMessage(req, (const char*)buf, ws_pkt.len);
}
} else {
ESP_LOGW(TAG, "Unsupported frame type: %d", ws_pkt.type);
}
free(buf);
return ESP_OK;
}
bool WebSocketControlServer::Start(int port) {
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = port;
config.max_open_sockets = 7;
config.ctrl_port = 32769;
httpd_uri_t ws_uri = {
.uri = "/ws",
.method = HTTP_GET,
.handler = ws_handler,
.user_ctx = nullptr,
.is_websocket = true
};
if (httpd_start(&server_handle_, &config) == ESP_OK) {
httpd_register_uri_handler(server_handle_, &ws_uri);
ESP_LOGI(TAG, "WebSocket server started on port %d", port);
return true;
}
ESP_LOGE(TAG, "Failed to start WebSocket server");
return false;
}
void WebSocketControlServer::Stop() {
if (server_handle_) {
httpd_stop(server_handle_);
server_handle_ = nullptr;
clients_.clear();
ESP_LOGI(TAG, "WebSocket server stopped");
}
}
void WebSocketControlServer::HandleMessage(httpd_req_t *req, const char* data, size_t len) {
if (data == nullptr || len == 0) {
ESP_LOGE(TAG, "Invalid message: data is null or len is 0");
return;
}
if (len > 4096) {
ESP_LOGE(TAG, "Message too long: %zu bytes", len);
return;
}
char* temp_buf = (char*)malloc(len + 1);
if (temp_buf == nullptr) {
ESP_LOGE(TAG, "Failed to allocate memory");
return;
}
memcpy(temp_buf, data, len);
temp_buf[len] = '\0';
cJSON* root = cJSON_Parse(temp_buf);
free(temp_buf);
if (root == nullptr) {
ESP_LOGE(TAG, "Failed to parse JSON");
return;
}
// 支持两种格式:
// 1. 完整格式:{"type":"mcp","payload":{...}}
// 2. 简化格式直接是MCP payload对象
cJSON* payload = nullptr;
cJSON* type = cJSON_GetObjectItem(root, "type");
if (type && cJSON_IsString(type) && strcmp(type->valuestring, "mcp") == 0) {
payload = cJSON_GetObjectItem(root, "payload");
if (payload != nullptr) {
cJSON_DetachItemViaPointer(root, payload);
McpServer::GetInstance().ParseMessage(payload);
cJSON_Delete(payload);
}
} else {
payload = cJSON_Duplicate(root, 1);
if (payload != nullptr) {
McpServer::GetInstance().ParseMessage(payload);
cJSON_Delete(payload);
}
}
if (payload == nullptr) {
ESP_LOGE(TAG, "Invalid message format or failed to parse");
}
cJSON_Delete(root);
}
void WebSocketControlServer::AddClient(httpd_req_t *req) {
int sock_fd = httpd_req_to_sockfd(req);
if (clients_.find(sock_fd) == clients_.end()) {
clients_[sock_fd] = req;
ESP_LOGI(TAG, "Client connected: %d (total: %zu)", sock_fd, clients_.size());
}
}
void WebSocketControlServer::RemoveClient(httpd_req_t *req) {
int sock_fd = httpd_req_to_sockfd(req);
clients_.erase(sock_fd);
ESP_LOGI(TAG, "Client disconnected: %d (total: %zu)", sock_fd, clients_.size());
}
size_t WebSocketControlServer::GetClientCount() const {
return clients_.size();
}
struct WsBroadcastJob {
httpd_handle_t server;
int fd;
char* payload;
size_t len;
};
static void ws_broadcast_send_job(void* arg) {
WsBroadcastJob* job = static_cast<WsBroadcastJob*>(arg);
httpd_ws_frame_t ws_pkt = {};
ws_pkt.type = HTTPD_WS_TYPE_TEXT;
ws_pkt.payload = reinterpret_cast<uint8_t*>(job->payload);
ws_pkt.len = job->len;
ws_pkt.final = true;
esp_err_t ret = httpd_ws_send_frame_async(job->server, job->fd, &ws_pkt);
if (ret != ESP_OK) {
ESP_LOGE("WSControl", "BroadcastMessage: send failed fd=%d err=%d", job->fd, ret);
}
free(job->payload);
free(job);
}
void WebSocketControlServer::BroadcastMessage(const std::string& message) {
if (!server_handle_ || clients_.empty()) {
return;
}
for (auto& [fd, req] : clients_) {
WsBroadcastJob* job = static_cast<WsBroadcastJob*>(malloc(sizeof(WsBroadcastJob)));
if (!job) {
ESP_LOGE(TAG, "BroadcastMessage: failed to allocate job");
continue;
}
job->server = server_handle_;
job->fd = fd;
job->len = message.length();
job->payload = static_cast<char*>(malloc(message.length() + 1));
if (!job->payload) {
ESP_LOGE(TAG, "BroadcastMessage: failed to allocate payload");
free(job);
continue;
}
memcpy(job->payload, message.c_str(), message.length());
job->payload[message.length()] = '\0';
esp_err_t ret = httpd_queue_work(server_handle_, ws_broadcast_send_job, job);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "BroadcastMessage: httpd_queue_work failed fd=%d err=%d", fd, ret);
free(job->payload);
free(job);
}
}
}