From e9413fa89872fae949850373139fe03a8d4f866a Mon Sep 17 00:00:00 2001 From: electron-rare Date: Sun, 14 Jun 2026 13:54:02 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat(box3,plip):=20POST=20/game/cmd=20?= =?UTF-8?q?=E2=80=94=20trigger=20cmd=5Fexec=20over=20HTTP=20(WiFi-direct)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- box3_voice/main/scenario_server.c | 42 +++++++++++++++++++++++++++++-- plip_voice/main/net.c | 36 +++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/box3_voice/main/scenario_server.c b/box3_voice/main/scenario_server.c index 5bc2d2f..b3434e8 100644 --- a/box3_voice/main/scenario_server.c +++ b/box3_voice/main/scenario_server.c @@ -14,6 +14,7 @@ #include #include "cJSON.h" +#include "cmd_exec.h" #include "esp_err.h" #include "esp_http_server.h" #include "esp_log.h" @@ -219,6 +220,38 @@ static esp_err_t handle_scenario_post(httpd_req_t *req) { return send_json(req, "200 OK", buf); } +// ---------- POST /game/cmd ---------- +// +// WiFi-direct CMD endpoint: receives {op,a} JSON frames (≤512 bytes) and +// forwards them to cmd_exec_handle(). Replaces ESP-NOW CMD path for annexes. + +#define MAX_CMD_BYTES 512 + +static esp_err_t handle_cmd_post(httpd_req_t *req) { + if (req->content_len <= 0 || req->content_len > MAX_CMD_BYTES) { + ESP_LOGW(TAG, "POST /game/cmd: bad content_len=%d", (int)req->content_len); + return send_error(req, "400 Bad Request", "body must be 1..512 bytes"); + } + char body[MAX_CMD_BYTES + 1]; + int total = 0; + while (total < (int)req->content_len) { + int got = httpd_req_recv(req, body + total, req->content_len - total); + if (got <= 0) { + if (got == HTTPD_SOCK_ERR_TIMEOUT) continue; + return send_error(req, "400 Bad Request", "recv failed"); + } + total += got; + } + body[total] = '\0'; + + ESP_LOGI(TAG, "POST /game/cmd (%d B): %.*s", total, total < 80 ? total : 80, body); + esp_err_t err = cmd_exec_handle(body, (size_t)total); + if (err == ESP_ERR_INVALID_ARG) { + return send_error(req, "400 Bad Request", "malformed cmd json or missing op"); + } + return send_json(req, "200 OK", "{\"ok\":true}"); +} + // ---------- POST /game/file ---------- // // Write binary assets directly to the SD card over HTTP. @@ -326,7 +359,7 @@ esp_err_t scenario_server_start(void) { } httpd_config_t cfg = HTTPD_DEFAULT_CONFIG(); cfg.server_port = 80; - cfg.max_uri_handlers = 10; + cfg.max_uri_handlers = 12; cfg.stack_size = 8192; esp_err_t err = httpd_start(&s_server, &cfg); @@ -347,10 +380,15 @@ esp_err_t scenario_server_start(void) { .uri = "/game/file", .method = HTTP_POST, .handler = handle_file_post, .user_ctx = NULL, }; + static const httpd_uri_t uri_cmd = { + .uri = "/game/cmd", .method = HTTP_POST, + .handler = handle_cmd_post, .user_ctx = NULL, + }; httpd_register_uri_handler(s_server, &uri_healthz); httpd_register_uri_handler(s_server, &uri_scenario); httpd_register_uri_handler(s_server, &uri_file); + httpd_register_uri_handler(s_server, &uri_cmd); - ESP_LOGI(TAG, "scenario server up on :80 (GET /healthz, POST /game/scenario, POST /game/file)"); + ESP_LOGI(TAG, "scenario server up on :80 (GET /healthz, POST /game/scenario, POST /game/file, POST /game/cmd)"); return ESP_OK; } diff --git a/plip_voice/main/net.c b/plip_voice/main/net.c index 52ea76d..53952d9 100644 --- a/plip_voice/main/net.c +++ b/plip_voice/main/net.c @@ -21,6 +21,7 @@ #include #include +#include "cmd_exec.h" #include "esp_err.h" #include "esp_event.h" #include "esp_http_server.h" @@ -221,6 +222,34 @@ static esp_err_t handle_file_post(httpd_req_t *req) return send_json(req, "200 OK", resp); } +/* ── POST /game/cmd (WiFi-direct CMD endpoint) ───────────────────────────── */ + +#define MAX_CMD_BYTES 512 + +static esp_err_t handle_cmd_post(httpd_req_t *req) +{ + if (req->content_len <= 0 || req->content_len > MAX_CMD_BYTES) + return send_json(req, "400 Bad Request", "{\"error\":\"body must be 1..512 bytes\"}"); + + char body[MAX_CMD_BYTES + 1]; + int total = 0; + while (total < (int)req->content_len) { + int got = httpd_req_recv(req, body + total, req->content_len - total); + if (got <= 0) { + if (got == HTTPD_SOCK_ERR_TIMEOUT) continue; + return send_json(req, "400 Bad Request", "{\"error\":\"recv failed\"}"); + } + total += got; + } + body[total] = '\0'; + + ESP_LOGI(TAG, "POST /game/cmd (%d B): %.*s", total, total < 80 ? total : 80, body); + esp_err_t err = cmd_exec_handle(body, (size_t)total); + if (err == ESP_ERR_INVALID_ARG) + return send_json(req, "400 Bad Request", "{\"error\":\"malformed cmd json or missing op\"}"); + return send_json(req, "200 OK", "{\"ok\":true}"); +} + /* ── Public API ───────────────────────────────────────────────────────────── */ bool net_is_connected(void) @@ -314,10 +343,15 @@ esp_err_t net_init(void) .uri = "/game/file", .method = HTTP_POST, .handler = handle_file_post, .user_ctx = NULL, }; + static const httpd_uri_t uri_cmd = { + .uri = "/game/cmd", .method = HTTP_POST, + .handler = handle_cmd_post, .user_ctx = NULL, + }; httpd_register_uri_handler(s_httpd, &uri_status); httpd_register_uri_handler(s_httpd, &uri_scenario); httpd_register_uri_handler(s_httpd, &uri_file); + httpd_register_uri_handler(s_httpd, &uri_cmd); - ESP_LOGI(TAG, "httpd up on :80 (GET /status, POST /game/scenario, POST /game/file)"); + ESP_LOGI(TAG, "httpd up on :80 (GET /status, POST /game/scenario, POST /game/file, POST /game/cmd)"); return ESP_OK; } -- 2.54.0 From 875333b6f04a40d0f49517a8f152b68ec01d94d1 Mon Sep 17 00:00:00 2001 From: electron-rare Date: Sun, 14 Jun 2026 14:08:08 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix(plip):=20play=5Fwav=5Ffile=20=E2=80=94?= =?UTF-8?q?=20SPIFFS=20lazy-mount=20for=20/spiffs/=20paths,=20skip=20SD=20?= =?UTF-8?q?gate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plip_voice/main/audio.c | 46 +++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/plip_voice/main/audio.c b/plip_voice/main/audio.c index 774069e..44a4cb4 100644 --- a/plip_voice/main/audio.c +++ b/plip_voice/main/audio.c @@ -24,6 +24,7 @@ #include #include "esp_log.h" +#include "esp_spiffs.h" #include "esp_vfs_fat.h" #include "driver/i2s_std.h" #include "driver/sdspi_host.h" @@ -64,6 +65,27 @@ static QueueHandle_t s_queue; static i2s_chan_handle_t s_spk_handle = NULL; static volatile bool s_stop_req = false; static bool s_sd_mounted = false; +static bool s_spiffs_mounted = false; + +/* ── SPIFFS lazy mount ───────────────────────────────────────────────────── */ + +static void ensure_spiffs_audio(void) +{ + if (s_spiffs_mounted) return; + esp_vfs_spiffs_conf_t conf = { + .base_path = "/spiffs", + .partition_label = "storage", + .max_files = 8, + .format_if_mount_failed = false, + }; + esp_err_t ret = esp_vfs_spiffs_register(&conf); + if (ret == ESP_OK || ret == ESP_ERR_INVALID_STATE /* already mounted */) { + s_spiffs_mounted = true; + ESP_LOGI(TAG, "SPIFFS mounted at /spiffs"); + } else { + ESP_LOGW(TAG, "SPIFFS mount failed: %s", esp_err_to_name(ret)); + } +} /* ── SD card init (best-effort) ──────────────────────────────────────────── */ @@ -179,14 +201,26 @@ static void play_wav_buf(const uint8_t *buf, size_t len) stream_pcm(buf + wi.data_offset, wi.data_size); } -/* Play WAV from SD. Falls back to tone on error. */ +/* Play WAV from filesystem (SPIFFS or SD). Falls back to tone on error. + * Paths starting with /spiffs/ are read directly from SPIFFS; all other + * paths (relative or /sdcard/…) require the SD card to be mounted first. */ static void play_wav_file(const char *path) { - ensure_sd_mounted(); - if (!s_sd_mounted) { - ESP_LOGW(TAG, "SD not mounted — beep fallback"); - audio_play_tone(880.0f, 200); - return; + bool is_spiffs = (strncmp(path, "/spiffs/", 8) == 0 || strncmp(path, "/spiffs", 7) == 0); + if (is_spiffs) { + ensure_spiffs_audio(); + if (!s_spiffs_mounted) { + ESP_LOGW(TAG, "SPIFFS not mounted — beep fallback"); + audio_play_tone(880.0f, 200); + return; + } + } else { + ensure_sd_mounted(); + if (!s_sd_mounted) { + ESP_LOGW(TAG, "SD not mounted — beep fallback"); + audio_play_tone(880.0f, 200); + return; + } } char full[192]; -- 2.54.0