Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3d03c637a | |||
| 39e0498221 | |||
| da7b6ace9c | |||
| be369fa260 | |||
| ace740a629 | |||
| 4ac3981845 |
@@ -22,4 +22,5 @@ idf_component_register(
|
||||
p7_coffre
|
||||
p5_morse
|
||||
p6_nfc
|
||||
npc_engine
|
||||
)
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "nvs_flash.h"
|
||||
|
||||
#include "hints_client.h"
|
||||
#include "npc_engine.h"
|
||||
#include "scenario_mesh.h"
|
||||
#include "puzzle_binding.h"
|
||||
#include "local_puzzles.h"
|
||||
@@ -807,6 +808,16 @@ esp_err_t game_endpoint_apply_step(const char *step_id,
|
||||
strncpy(s_current_step_id, step_id, sizeof(s_current_step_id) - 1);
|
||||
s_current_step_id[sizeof(s_current_step_id) - 1] = '\0';
|
||||
|
||||
// Sync the NPC/hints engine (and, through it, the voice gateway) with the
|
||||
// scene this step enters. This is what makes the phone NPCs hint on — and
|
||||
// scripted calls auto-ring for — the current scene, regardless of whether
|
||||
// the step was driven by the local game loop or an external POST /game/step.
|
||||
// Best-effort: notify is async + server-side idempotent and never blocks
|
||||
// the step change. Steps with no scene_id (or an unknown one) are skipped.
|
||||
if (s_current_scene.scene_id[0] != '\0') {
|
||||
(void) npc_engine_set_scene_by_id(s_current_scene.scene_id, 0);
|
||||
}
|
||||
|
||||
// P7 coffre: fire the actuator when the final win step is reached.
|
||||
// p7_coffre_unlock() is a no-op stub when CONFIG_ZACUS_P7_COFFRE_ENABLE=n.
|
||||
if (strcmp(step_id, "STEP_FINAL_WIN") == 0) {
|
||||
|
||||
@@ -55,6 +55,7 @@ esp_err_t puzzle_binding_from_ir(const char *ir_json, const char *step_id,
|
||||
#define SB_MAX_TITLE 48
|
||||
#define SB_MAX_SUBTITLE 64
|
||||
#define SB_MAX_SYMBOL 16
|
||||
#define SB_MAX_SCENE_ID 40
|
||||
|
||||
typedef enum {
|
||||
SB_FX_PULSE = 0, // default (original SceneEffect::kPulse)
|
||||
@@ -65,6 +66,8 @@ typedef enum {
|
||||
|
||||
typedef struct {
|
||||
bool present; // step has a scene object
|
||||
char scene_id[SB_MAX_SCENE_ID]; // canonical SCENE_* id (sibling of step id;
|
||||
// set even when no display scene object)
|
||||
char title[SB_MAX_TITLE];
|
||||
char subtitle[SB_MAX_SUBTITLE];
|
||||
char symbol[SB_MAX_SYMBOL];
|
||||
|
||||
@@ -239,9 +239,15 @@ esp_err_t scene_binding_from_ir(const char *ir_json, const char *step_id,
|
||||
}
|
||||
if (!step) { cJSON_Delete(root); return ESP_ERR_NOT_FOUND; }
|
||||
|
||||
// Canonical scene id (SCENE_*), sibling of the step "id". Captured
|
||||
// unconditionally so npc_engine / the voice gateway can be synced even
|
||||
// on steps that carry no display "scene" object.
|
||||
copy_scene_str(out->scene_id, sizeof(out->scene_id),
|
||||
cJSON_GetObjectItemCaseSensitive(step, "scene_id"));
|
||||
|
||||
const cJSON *scene = cJSON_GetObjectItemCaseSensitive(step, "scene");
|
||||
if (!scene || !cJSON_IsObject(scene)) {
|
||||
// No scene — present stays false, ESP_OK.
|
||||
// No scene object — present stays false, ESP_OK (scene_id may be set).
|
||||
cJSON_Delete(root);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -169,6 +169,16 @@ esp_err_t npc_engine_trigger_cue(const char *cue_id);
|
||||
// and primes the stuck timer.
|
||||
esp_err_t npc_engine_set_step(uint8_t step_id, uint32_t expected_duration_ms);
|
||||
|
||||
// Same bridge as npc_engine_set_step(), but keyed by the canonical scene
|
||||
// string id (e.g. "SCENE_WARNING") rather than its numeric index. Resolves
|
||||
// the id against the engine's internal scene table and forwards to
|
||||
// npc_engine_set_step(). Lets the game endpoint sync the NPC/hints engine
|
||||
// (and the voice gateway) straight from the scenario IR's `scene_id`.
|
||||
// Returns ESP_ERR_NOT_FOUND when the id matches no known scene (no notify
|
||||
// is sent in that case), ESP_ERR_INVALID_ARG on a NULL/empty id.
|
||||
esp_err_t npc_engine_set_scene_by_id(const char *scene_id,
|
||||
uint32_t expected_duration_ms);
|
||||
|
||||
// Request a hint for `puzzle_id` at escalation `level` (0..3). The engine
|
||||
// invokes `cb` with the resulting text. The current implementation is a
|
||||
// LOCAL STUB: it returns a hardcoded French placeholder synchronously.
|
||||
|
||||
@@ -17,11 +17,14 @@
|
||||
#include "npc_engine.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "esp_http_client.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "media_manager.h"
|
||||
#include "hints_client.h"
|
||||
@@ -39,11 +42,12 @@ void npc_engine_set_gateway(const char *base_url, const char *token) {
|
||||
if (token) snprintf(s_gw_token, sizeof(s_gw_token), "%s", token);
|
||||
}
|
||||
|
||||
// Fire a one-shot POST {gateway}/game/step?scene=<scene>. Short timeout,
|
||||
// best-effort: the puzzle progresses fine without it, it only keeps the phone
|
||||
// NPCs' hints in sync with the current scene. Never propagates failure.
|
||||
static void notify_gateway_scene(const char *scene) {
|
||||
if (!s_gw_url[0] || !scene || !scene[0]) return;
|
||||
// One-shot worker: POST {gateway}/game/step?scene=<scene> then self-delete.
|
||||
// Runs on its own task so a slow/unreachable gateway NEVER stalls the scene
|
||||
// transition (the game must not lag 2 s on every scene change if the gateway is
|
||||
// offline). `arg` is a heap copy of the scene, freed here. Best-effort.
|
||||
static void gw_notify_task(void *arg) {
|
||||
char *scene = (char *) arg;
|
||||
char url[256];
|
||||
snprintf(url, sizeof(url), "%s/game/step?scene=%s", s_gw_url, scene);
|
||||
esp_http_client_config_t cfg = {
|
||||
@@ -52,20 +56,37 @@ static void notify_gateway_scene(const char *scene) {
|
||||
.timeout_ms = 2000,
|
||||
};
|
||||
esp_http_client_handle_t cli = esp_http_client_init(&cfg);
|
||||
if (!cli) return;
|
||||
if (s_gw_token[0]) {
|
||||
char auth[112];
|
||||
snprintf(auth, sizeof(auth), "Bearer %s", s_gw_token);
|
||||
esp_http_client_set_header(cli, "Authorization", auth);
|
||||
if (cli) {
|
||||
if (s_gw_token[0]) {
|
||||
char auth[112];
|
||||
snprintf(auth, sizeof(auth), "Bearer %s", s_gw_token);
|
||||
esp_http_client_set_header(cli, "Authorization", auth);
|
||||
}
|
||||
esp_err_t err = esp_http_client_perform(cli);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "gateway /game/step notify failed: %s", esp_err_to_name(err));
|
||||
} else {
|
||||
ESP_LOGI(TAG, "gateway scene -> %s (HTTP %d)", scene,
|
||||
esp_http_client_get_status_code(cli));
|
||||
}
|
||||
esp_http_client_cleanup(cli);
|
||||
}
|
||||
esp_err_t err = esp_http_client_perform(cli);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "gateway /game/step notify failed: %s", esp_err_to_name(err));
|
||||
} else {
|
||||
ESP_LOGI(TAG, "gateway scene -> %s (HTTP %d)", scene,
|
||||
esp_http_client_get_status_code(cli));
|
||||
free(scene);
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
// Fire-and-forget the gateway scene notification on a detached task so a scene
|
||||
// change is never blocked by the network. No-op if no gateway configured.
|
||||
static void notify_gateway_scene(const char *scene) {
|
||||
if (!s_gw_url[0] || !scene || !scene[0]) return;
|
||||
size_t n = strlen(scene) + 1;
|
||||
char *copy = malloc(n);
|
||||
if (!copy) return;
|
||||
memcpy(copy, scene, n);
|
||||
if (xTaskCreate(gw_notify_task, "gw_notify", 4096, copy, 3, NULL) != pdPASS) {
|
||||
ESP_LOGW(TAG, "gw_notify task spawn failed");
|
||||
free(copy);
|
||||
}
|
||||
esp_http_client_cleanup(cli);
|
||||
}
|
||||
|
||||
// ─── Core: scene/trigger/mood lookup tables (verbatim Arduino) ──────────────
|
||||
@@ -419,6 +440,19 @@ esp_err_t npc_engine_set_step(uint8_t step_id, uint32_t expected_duration_ms) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t npc_engine_set_scene_by_id(const char *scene_id,
|
||||
uint32_t expected_duration_ms) {
|
||||
if (scene_id == NULL || scene_id[0] == '\0') return ESP_ERR_INVALID_ARG;
|
||||
for (uint8_t i = 0; i < kSceneCount; i++) {
|
||||
if (strcmp(scene_id, kSceneIds[i]) == 0) {
|
||||
return npc_engine_set_step(i, expected_duration_ms);
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "set_scene_by_id: unknown scene \"%s\" (no kSceneIds match)",
|
||||
scene_id);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
esp_err_t npc_engine_set_group_profile(const char *profile) {
|
||||
// Thin pass-through. hints_client validates the value and logs the
|
||||
// outcome. Kept on npc_engine so the rest of the firmware doesn't
|
||||
|
||||
@@ -137,6 +137,12 @@ static void ensure_sd_mounted(void)
|
||||
}
|
||||
}
|
||||
|
||||
bool audio_ensure_sd(void)
|
||||
{
|
||||
ensure_sd_mounted();
|
||||
return s_sd_mounted;
|
||||
}
|
||||
|
||||
/* ── WAV parser ──────────────────────────────────────────────────────────── */
|
||||
|
||||
static esp_err_t parse_wav_header(const uint8_t *buf, size_t len, wav_info_t *out)
|
||||
|
||||
@@ -39,6 +39,12 @@ esp_err_t audio_play_async(const char *path);
|
||||
/* Stop current playback immediately. */
|
||||
esp_err_t audio_stop(void);
|
||||
|
||||
/* Mount the microSD card at /sdcard on demand (best-effort, idempotent) and
|
||||
* report whether it is mounted. Unlike playback, this does NOT depend on the
|
||||
* hook state — it lets the REST layer stage a voice pack onto the SD while the
|
||||
* handset is down. Returns true if /sdcard is usable. */
|
||||
bool audio_ensure_sd(void);
|
||||
|
||||
/* True while the audio worker is playing a clip (tone/WAV). The conversation
|
||||
* LISTEN loop polls this to stay half-duplex: never capture while playing
|
||||
* (avoids the earpiece→mic feedback that saturated the line). */
|
||||
|
||||
@@ -84,12 +84,16 @@ static char s_sid[32] = {0};
|
||||
* NOT re-read dialer_current() for the greeting/reply — that polluted number
|
||||
* would 404 at the gateway. Capture the clean routed number here once. */
|
||||
static char s_number[16] = {0};
|
||||
/* Scene this call hints on (e.g. "SCENE_WARNING"), locked at pickup like the
|
||||
* number. Empty for player-dialled calls. Drives the local SD hint clip. */
|
||||
static char s_scene[40] = {0};
|
||||
|
||||
/* Incoming-call mode: an NPC "calls" the phone. conversation_arm_incoming()
|
||||
* stores the caller's persona number and rings; when the handset is then picked
|
||||
* up from IDLE, we skip the outgoing dialtone/dial/ringback and go straight to
|
||||
* GREET with this number (the NPC speaks first). */
|
||||
* stores the caller's persona number (and optional scene) and rings; when the
|
||||
* handset is then picked up from IDLE, we skip the outgoing dialtone/dial/
|
||||
* ringback and go straight to GREET with this number (the NPC speaks first). */
|
||||
static char s_incoming_number[16] = {0};
|
||||
static char s_incoming_scene[40] = {0};
|
||||
static volatile bool s_incoming_armed = false;
|
||||
|
||||
/* Known numbers: ringback when dialed */
|
||||
@@ -105,6 +109,20 @@ static bool is_known(const char *num)
|
||||
return false;
|
||||
}
|
||||
|
||||
/* "SCENE_WARNING" → "warning": strip the SCENE_ prefix and lowercase, matching
|
||||
* the SD pack filenames from tools/tts/generate_plip_sd_pack.py. */
|
||||
static void scene_slug(const char *scene, char *out, size_t cap)
|
||||
{
|
||||
const char *p = scene;
|
||||
if (strncmp(p, "SCENE_", 6) == 0) p += 6;
|
||||
size_t i = 0;
|
||||
for (; p[i] && i + 1 < cap; i++) {
|
||||
char c = p[i];
|
||||
out[i] = (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : c;
|
||||
}
|
||||
out[i] = '\0';
|
||||
}
|
||||
|
||||
static void go_idle(void)
|
||||
{
|
||||
tones_stop();
|
||||
@@ -114,6 +132,8 @@ static void go_idle(void)
|
||||
#if CONFIG_PLIP_DIAL_DTMF
|
||||
dtmf_stop();
|
||||
#endif
|
||||
s_scene[0] = '\0'; /* clear scripted-call scene so a later dial-out call
|
||||
* doesn't inherit it (CONNECTED would skip listening). */
|
||||
s_state = STATE_IDLE;
|
||||
ESP_LOGI(TAG, "-> IDLE");
|
||||
}
|
||||
@@ -134,6 +154,7 @@ static void enter_incoming_greet(void)
|
||||
dialer_reset();
|
||||
s_offhook = true;
|
||||
snprintf(s_number, sizeof(s_number), "%s", s_incoming_number);
|
||||
snprintf(s_scene, sizeof(s_scene), "%s", s_incoming_scene);
|
||||
snprintf(s_sid, sizeof(s_sid), "%lld", (long long)esp_timer_get_time());
|
||||
audio_pa_set(true);
|
||||
s_state = STATE_GREET;
|
||||
@@ -257,18 +278,57 @@ static void conv_task(void *arg)
|
||||
go_idle();
|
||||
break;
|
||||
}
|
||||
/* Fetch greeting WAV from gateway and enqueue playback */
|
||||
if (turn_client_greeting(s_sid, s_number,
|
||||
"/spiffs/turn.wav")) {
|
||||
audio_play_async("/spiffs/turn.wav");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "turn_client_greeting failed — proceeding silent");
|
||||
/* Prefer the local SD voice pack (no gateway, no model in RAM):
|
||||
* play /sdcard/voice/greet_<number>.wav when present. Fall back to
|
||||
* the live gateway greeting only when the pack has no clip for this
|
||||
* NPC number (see tools/tts/generate_plip_sd_pack.py). */
|
||||
{
|
||||
char sd_greet[64];
|
||||
snprintf(sd_greet, sizeof(sd_greet),
|
||||
"/sdcard/voice/greet_%s.wav", s_number);
|
||||
FILE *tf = NULL;
|
||||
if (audio_ensure_sd() && (tf = fopen(sd_greet, "rb")) != NULL) {
|
||||
fclose(tf);
|
||||
audio_play_async(sd_greet);
|
||||
ESP_LOGI(TAG, "GREET: local SD pack %s", sd_greet);
|
||||
} else if (turn_client_greeting(s_sid, s_number,
|
||||
"/spiffs/turn.wav")) {
|
||||
audio_play_async("/spiffs/turn.wav");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "no SD clip + gateway greeting failed — silent");
|
||||
}
|
||||
}
|
||||
/* Scripted offline call: chain this scene's hint from the SD pack
|
||||
* right after the greeting (the audio queue plays them back-to-back).
|
||||
* Fully local — no gateway, no model. */
|
||||
if (s_scene[0] != '\0' && audio_ensure_sd()) {
|
||||
char slug[40], hint[80];
|
||||
scene_slug(s_scene, slug, sizeof(slug));
|
||||
snprintf(hint, sizeof(hint),
|
||||
"/sdcard/voice/hint_%s_l1_0.wav", slug);
|
||||
FILE *hf = fopen(hint, "rb");
|
||||
if (hf != NULL) {
|
||||
fclose(hf);
|
||||
audio_play_async(hint);
|
||||
ESP_LOGI(TAG, "GREET: chained SD hint %s", hint);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "GREET: no SD hint for scene %s (%s)",
|
||||
s_scene, hint);
|
||||
}
|
||||
}
|
||||
s_state = STATE_CONNECTED;
|
||||
ESP_LOGI(TAG, "-> CONNECTED");
|
||||
break;
|
||||
|
||||
case STATE_CONNECTED:
|
||||
/* Scripted offline call (greeting + SD hint already queued): there's
|
||||
* no live two-way conversation to run — just let the clips play out
|
||||
* and wait for the player to hang up. Skips the gateway/model listen
|
||||
* loop entirely. The global on-hook check returns us to IDLE. */
|
||||
if (s_scene[0] != '\0') {
|
||||
vTaskDelay(pdMS_TO_TICKS(REPLY_POLL_MS));
|
||||
break;
|
||||
}
|
||||
#if CONFIG_PLIP_VOICE_REPLY
|
||||
/*
|
||||
* Stage 3 — LISTEN loop.
|
||||
@@ -404,12 +464,14 @@ void conversation_on_hook_change(bool offhook)
|
||||
s_hook_changed = true;
|
||||
}
|
||||
|
||||
void conversation_arm_incoming(const char *number)
|
||||
void conversation_arm_incoming(const char *number, const char *scene)
|
||||
{
|
||||
snprintf(s_incoming_number, sizeof(s_incoming_number), "%s",
|
||||
(number && number[0]) ? number : "17");
|
||||
snprintf(s_incoming_scene, sizeof(s_incoming_scene), "%s",
|
||||
(scene && scene[0]) ? scene : "");
|
||||
s_incoming_armed = true;
|
||||
phone_ring_start();
|
||||
ESP_LOGI(TAG, "incoming call armed (num=%s) — ringing until pickup",
|
||||
s_incoming_number);
|
||||
ESP_LOGI(TAG, "incoming call armed (num=%s scene=%s) — ringing until pickup",
|
||||
s_incoming_number, s_incoming_scene[0] ? s_incoming_scene : "-");
|
||||
}
|
||||
|
||||
@@ -6,5 +6,8 @@ void conversation_on_hook_change(bool offhook);
|
||||
|
||||
/* Arm an INCOMING call from NPC persona `number` and start ringing. When the
|
||||
* handset is picked up from idle, the conversation jumps straight to the
|
||||
* greeting (persona speaks first) instead of the outgoing dial-tone path. */
|
||||
void conversation_arm_incoming(const char *number);
|
||||
* greeting (persona speaks first) instead of the outgoing dial-tone path.
|
||||
* `scene` (optional, e.g. "SCENE_WARNING") makes the phone play that scene's
|
||||
* hint from its local SD pack right after the greeting; pass NULL/"" for a
|
||||
* greeting-only call. */
|
||||
void conversation_arm_incoming(const char *number, const char *scene);
|
||||
|
||||
+83
-10
@@ -22,6 +22,7 @@
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "audio.h"
|
||||
#include "tones.h"
|
||||
@@ -53,7 +54,8 @@
|
||||
|
||||
#define SPIFFS_LABEL "storage"
|
||||
#define SPIFFS_BASE "/spiffs"
|
||||
#define MAX_BODY (256 * 1024)
|
||||
#define MAX_BODY (512 * 1024) /* streamed in 2 KB chunks → RAM-safe; raised
|
||||
so full-length say() hint WAVs fit on SD */
|
||||
|
||||
static volatile bool s_connected = false;
|
||||
static httpd_handle_t s_httpd = NULL;
|
||||
@@ -209,14 +211,39 @@ static esp_err_t handle_file_post(httpd_req_t *req)
|
||||
if (req->content_len <= 0 || req->content_len > MAX_BODY)
|
||||
return send_json(req, "413 Payload Too Large", "{\"error\":\"too large\"}");
|
||||
|
||||
ensure_spiffs();
|
||||
/* Route to the microSD when the path is sd-prefixed ("sd/…", "sdcard/…"
|
||||
* or "/sdcard/…"); otherwise stage in SPIFFS (default). The SD is mounted
|
||||
* on demand here so a voice pack can be staged without an off-hook play. */
|
||||
const char *base;
|
||||
const char *rel = fpath;
|
||||
bool to_sd = false;
|
||||
if (strncmp(fpath, "sd/", 3) == 0) { rel = fpath + 3; to_sd = true; }
|
||||
else if (strncmp(fpath, "sdcard/", 7) == 0) { rel = fpath + 7; to_sd = true; }
|
||||
else if (strncmp(fpath, "/sdcard/", 8) == 0) { rel = fpath + 8; to_sd = true; }
|
||||
|
||||
/* Build full SPIFFS path. */
|
||||
char full[160];
|
||||
if (fpath[0] == '/') {
|
||||
snprintf(full, sizeof(full), "%s%s", SPIFFS_BASE, fpath);
|
||||
if (to_sd) {
|
||||
if (!audio_ensure_sd())
|
||||
return send_json(req, "503 Service Unavailable", "{\"error\":\"no sd card\"}");
|
||||
base = PLIP_SD_MOUNT; /* "/sdcard" */
|
||||
} else {
|
||||
snprintf(full, sizeof(full), "%s/%s", SPIFFS_BASE, fpath);
|
||||
ensure_spiffs();
|
||||
base = SPIFFS_BASE;
|
||||
}
|
||||
while (*rel == '/') rel++; /* avoid a double slash when joining */
|
||||
|
||||
char full[192];
|
||||
snprintf(full, sizeof(full), "%s/%s", base, rel);
|
||||
|
||||
/* Create the immediate parent dir (one level) so a pack can live in a
|
||||
* subfolder like /sdcard/voice/<file>.wav — fopen won't create it. FAT
|
||||
* supports mkdir; SPIFFS is flat so we only do this for the SD path. */
|
||||
if (to_sd) {
|
||||
char *slash = strrchr(full, '/');
|
||||
if (slash && slash != full) {
|
||||
*slash = '\0';
|
||||
mkdir(full, 0775); /* best-effort; ignore EEXIST */
|
||||
*slash = '/';
|
||||
}
|
||||
}
|
||||
|
||||
FILE *f = fopen(full, "wb");
|
||||
@@ -254,6 +281,44 @@ static esp_err_t handle_file_post(httpd_req_t *req)
|
||||
return send_json(req, "200 OK", resp);
|
||||
}
|
||||
|
||||
/* ── DELETE /game/file?path=... (remove a staged file; SD or SPIFFS) ─────── */
|
||||
/* Same path routing as the POST handler (sd/ prefix → /sdcard, else /spiffs).
|
||||
* Lets the host clean stale clips when regenerating the SD voice pack. */
|
||||
static esp_err_t handle_file_delete(httpd_req_t *req)
|
||||
{
|
||||
char query[128] = {0}, fpath[128] = {0};
|
||||
httpd_req_get_url_query_str(req, query, sizeof(query));
|
||||
if (httpd_query_key_value(query, "path", fpath, sizeof(fpath)) != ESP_OK)
|
||||
return send_json(req, "400 Bad Request", "{\"error\":\"missing path param\"}");
|
||||
|
||||
const char *base, *rel = fpath;
|
||||
bool to_sd = false;
|
||||
if (strncmp(fpath, "sd/", 3) == 0) { rel = fpath + 3; to_sd = true; }
|
||||
else if (strncmp(fpath, "sdcard/", 7) == 0) { rel = fpath + 7; to_sd = true; }
|
||||
else if (strncmp(fpath, "/sdcard/", 8) == 0) { rel = fpath + 8; to_sd = true; }
|
||||
|
||||
if (to_sd) {
|
||||
if (!audio_ensure_sd())
|
||||
return send_json(req, "503 Service Unavailable", "{\"error\":\"no sd card\"}");
|
||||
base = PLIP_SD_MOUNT;
|
||||
} else {
|
||||
ensure_spiffs();
|
||||
base = SPIFFS_BASE;
|
||||
}
|
||||
while (*rel == '/') rel++;
|
||||
|
||||
char full[192];
|
||||
snprintf(full, sizeof(full), "%s/%s", base, rel);
|
||||
if (remove(full) != 0) {
|
||||
ESP_LOGW(TAG, "remove %s failed: errno=%d", full, errno);
|
||||
return send_json(req, "404 Not Found", "{\"error\":\"remove failed\"}");
|
||||
}
|
||||
ESP_LOGI(TAG, "file removed: %s", full);
|
||||
char resp[256];
|
||||
snprintf(resp, sizeof(resp), "{\"status\":\"ok\",\"removed\":\"%s\"}", full);
|
||||
return send_json(req, "200 OK", resp);
|
||||
}
|
||||
|
||||
/* ── POST /voice/capture ──────────────────────────────────────────────────── */
|
||||
|
||||
#define CAP_MAX_MS_DEFAULT 5000
|
||||
@@ -483,10 +548,13 @@ static esp_err_t handle_debug_ring(httpd_req_t *req)
|
||||
{
|
||||
/* /debug/ring[?number=NN] — arm an INCOMING call from persona NN (default
|
||||
* 17) and ring. On pickup the conversation jumps straight to the greeting. */
|
||||
char query[48] = {0}, number[16] = {0};
|
||||
char query[96] = {0}, number[16] = {0}, scene[40] = {0};
|
||||
httpd_req_get_url_query_str(req, query, sizeof(query));
|
||||
httpd_query_key_value(query, "number", number, sizeof(number));
|
||||
conversation_arm_incoming(number); /* defaults to "17" if empty; starts ringing */
|
||||
httpd_query_key_value(query, "scene", scene, sizeof(scene));
|
||||
/* scene (optional) lets the phone play that scene's hint from its SD pack
|
||||
* after the greeting — fully offline, no model. */
|
||||
conversation_arm_incoming(number, scene[0] ? scene : NULL);
|
||||
char resp[64];
|
||||
snprintf(resp, sizeof(resp), "{\"ok\":true,\"ringing\":true,\"incoming\":\"%s\"}",
|
||||
number[0] ? number : "17");
|
||||
@@ -812,7 +880,7 @@ esp_err_t net_init(void)
|
||||
/* Start HTTP server. */
|
||||
httpd_config_t hcfg = HTTPD_DEFAULT_CONFIG();
|
||||
hcfg.server_port = 80;
|
||||
hcfg.max_uri_handlers = 17;
|
||||
hcfg.max_uri_handlers = 18;
|
||||
hcfg.stack_size = 8192;
|
||||
|
||||
esp_err_t ret = httpd_start(&s_httpd, &hcfg);
|
||||
@@ -833,6 +901,10 @@ 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_file_delete = {
|
||||
.uri = "/game/file", .method = HTTP_DELETE,
|
||||
.handler = handle_file_delete, .user_ctx = NULL,
|
||||
};
|
||||
static const httpd_uri_t uri_cmd = {
|
||||
.uri = "/game/cmd", .method = HTTP_POST,
|
||||
.handler = handle_cmd_post, .user_ctx = NULL,
|
||||
@@ -888,6 +960,7 @@ esp_err_t net_init(void)
|
||||
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_file_delete);
|
||||
httpd_register_uri_handler(s_httpd, &uri_cmd);
|
||||
httpd_register_uri_handler(s_httpd, &uri_capture);
|
||||
httpd_register_uri_handler(s_httpd, &uri_debug_regs);
|
||||
|
||||
@@ -52,3 +52,7 @@ CONFIG_PLIP_HOOK_ACTIVE_HIGH=y
|
||||
# Example: CONFIG_PLIP_GATEWAY_URL="http://192.168.0.175:8401"
|
||||
# CONFIG_PLIP_GATEWAY_TOKEN="testtoken"
|
||||
# Default (Kconfig): http://192.168.0.50:8401 with empty token.
|
||||
|
||||
# Long file names on the SD (voice sample pack uses >8.3 names)
|
||||
CONFIG_FATFS_LFN_HEAP=y
|
||||
CONFIG_FATFS_MAX_LFN=255
|
||||
|
||||
Reference in New Issue
Block a user