diff --git a/main/gateway/ws_server.c b/main/gateway/ws_server.c new file mode 100644 index 0000000..042ea70 --- /dev/null +++ b/main/gateway/ws_server.c @@ -0,0 +1,217 @@ +#include "ws_server.h" +#include "mimi_config.h" +#include "bus/message_bus.h" + +#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; +} diff --git a/main/gateway/ws_server.h b/main/gateway/ws_server.h new file mode 100644 index 0000000..22d5c9f --- /dev/null +++ b/main/gateway/ws_server.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esp_err.h" + +/** + * Initialize and start the WebSocket server on MIMI_WS_PORT. + * Allows external clients to interact with the Agent via JSON messages. + * + * Protocol: + * Inbound: {"type":"message","content":"hello","chat_id":"ws_client1"} + * Outbound: {"type":"response","content":"Hi!","chat_id":"ws_client1"} + */ +esp_err_t ws_server_start(void); + +/** + * Send a text message to a specific WebSocket client by chat_id. + * @param chat_id Client identifier (assigned on connection) + * @param text Message text + */ +esp_err_t ws_server_send(const char *chat_id, const char *text); + +/** + * Stop the WebSocket server. + */ +esp_err_t ws_server_stop(void);