#include "ws_server.h" #include "mimi_config.h" #include "bus/message_bus.h" #include #include #include #include #include #include "esp_log.h" #include "esp_http_server.h" #include "cJSON.h" static const char *TAG = "ws"; static httpd_handle_t s_server = NULL; /* Simple client tracking */ typedef struct { int fd; char chat_id[32]; bool active; } ws_client_t; static ws_client_t s_clients[MIMI_WS_MAX_CLIENTS]; static ws_client_t *find_client_by_fd(int fd) { for (int i = 0; i < MIMI_WS_MAX_CLIENTS; i++) { if (s_clients[i].active && s_clients[i].fd == fd) { return &s_clients[i]; } } return NULL; } static ws_client_t *find_client_by_chat_id(const char *chat_id) { for (int i = 0; i < MIMI_WS_MAX_CLIENTS; i++) { if (s_clients[i].active && strcmp(s_clients[i].chat_id, chat_id) == 0) { return &s_clients[i]; } } return NULL; } static ws_client_t *add_client(int fd) { for (int i = 0; i < MIMI_WS_MAX_CLIENTS; i++) { if (!s_clients[i].active) { s_clients[i].fd = fd; snprintf(s_clients[i].chat_id, sizeof(s_clients[i].chat_id), "ws_%d", fd); s_clients[i].active = true; ESP_LOGI(TAG, "Client connected: %s (fd=%d)", s_clients[i].chat_id, fd); return &s_clients[i]; } } ESP_LOGW(TAG, "Max clients reached, rejecting fd=%d", fd); return NULL; } static void remove_client(int fd) { for (int i = 0; i < MIMI_WS_MAX_CLIENTS; i++) { if (s_clients[i].active && s_clients[i].fd == fd) { ESP_LOGI(TAG, "Client disconnected: %s", s_clients[i].chat_id); s_clients[i].active = false; return; } } } static esp_err_t ws_handler(httpd_req_t *req) { if (req->method == HTTP_GET) { /* WebSocket handshake — register client */ int fd = httpd_req_to_sockfd(req); add_client(fd); return ESP_OK; } /* Receive WebSocket frame */ httpd_ws_frame_t ws_pkt = {0}; ws_pkt.type = HTTPD_WS_TYPE_TEXT; /* Get frame length */ esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0); if (ret != ESP_OK) return ret; if (ws_pkt.len == 0) return ESP_OK; ws_pkt.payload = calloc(1, ws_pkt.len + 1); if (!ws_pkt.payload) return ESP_ERR_NO_MEM; ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len); if (ret != ESP_OK) { free(ws_pkt.payload); return ret; } int fd = httpd_req_to_sockfd(req); ws_client_t *client = find_client_by_fd(fd); /* Parse JSON message */ cJSON *root = cJSON_Parse((char *)ws_pkt.payload); free(ws_pkt.payload); if (!root) { ESP_LOGW(TAG, "Invalid JSON from fd=%d", fd); return ESP_OK; } cJSON *type = cJSON_GetObjectItem(root, "type"); cJSON *content = cJSON_GetObjectItem(root, "content"); if (type && cJSON_IsString(type) && strcmp(type->valuestring, "message") == 0 && content && cJSON_IsString(content)) { /* Determine chat_id */ const char *chat_id = client ? client->chat_id : "ws_unknown"; cJSON *cid = cJSON_GetObjectItem(root, "chat_id"); if (cid && cJSON_IsString(cid)) { chat_id = cid->valuestring; /* Update client's chat_id if provided */ if (client) { strncpy(client->chat_id, chat_id, sizeof(client->chat_id) - 1); } } ESP_LOGI(TAG, "WS message from %s: %.40s...", chat_id, content->valuestring); /* Push to inbound bus */ mimi_msg_t msg = {0}; strncpy(msg.channel, MIMI_CHAN_WEBSOCKET, sizeof(msg.channel) - 1); strncpy(msg.chat_id, chat_id, sizeof(msg.chat_id) - 1); msg.content = strdup(content->valuestring); if (msg.content) { message_bus_push_inbound(&msg); } } cJSON_Delete(root); return ESP_OK; } esp_err_t ws_server_start(void) { memset(s_clients, 0, sizeof(s_clients)); httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = MIMI_WS_PORT; config.ctrl_port = MIMI_WS_PORT + 1; config.max_open_sockets = MIMI_WS_MAX_CLIENTS; esp_err_t ret = httpd_start(&s_server, &config); if (ret != ESP_OK) { ESP_LOGE(TAG, "Failed to start WebSocket server: %s", esp_err_to_name(ret)); return ret; } /* Register WebSocket URI */ httpd_uri_t ws_uri = { .uri = "/", .method = HTTP_GET, .handler = ws_handler, .is_websocket = true, }; httpd_register_uri_handler(s_server, &ws_uri); ESP_LOGI(TAG, "WebSocket server started on port %d", MIMI_WS_PORT); return ESP_OK; } esp_err_t ws_server_send(const char *chat_id, const char *text) { if (!s_server) return ESP_ERR_INVALID_STATE; ws_client_t *client = find_client_by_chat_id(chat_id); if (!client) { ESP_LOGW(TAG, "No WS client with chat_id=%s", chat_id); return ESP_ERR_NOT_FOUND; } /* Build response JSON */ cJSON *resp = cJSON_CreateObject(); cJSON_AddStringToObject(resp, "type", "response"); cJSON_AddStringToObject(resp, "content", text); cJSON_AddStringToObject(resp, "chat_id", chat_id); char *json_str = cJSON_PrintUnformatted(resp); cJSON_Delete(resp); if (!json_str) return ESP_ERR_NO_MEM; httpd_ws_frame_t ws_pkt = { .type = HTTPD_WS_TYPE_TEXT, .payload = (uint8_t *)json_str, .len = strlen(json_str), }; esp_err_t ret = httpd_ws_send_frame_async(s_server, client->fd, &ws_pkt); free(json_str); if (ret != ESP_OK) { ESP_LOGW(TAG, "Failed to send to %s: %s", chat_id, esp_err_to_name(ret)); remove_client(client->fd); } return ret; } esp_err_t ws_server_stop(void) { if (s_server) { httpd_stop(s_server); s_server = NULL; ESP_LOGI(TAG, "WebSocket server stopped"); } return ESP_OK; }