feat(box3,plip): POST /game/cmd — trigger cmd_exec over HTTP #14

Merged
electron merged 2 commits from feat/annex-game-cmd into main 2026-06-14 12:13:34 +00:00
3 changed files with 115 additions and 9 deletions
+40 -2
View File
@@ -14,6 +14,7 @@
#include <unistd.h>
#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;
}
+40 -6
View File
@@ -24,6 +24,7 @@
#include <string.h>
#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];
+35 -1
View File
@@ -21,6 +21,7 @@
#include <sys/stat.h>
#include <errno.h>
#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;
}