diff --git a/idf_zacus/components/npc_engine/CMakeLists.txt b/idf_zacus/components/npc_engine/CMakeLists.txt index a0f5ccb..dc845ed 100644 --- a/idf_zacus/components/npc_engine/CMakeLists.txt +++ b/idf_zacus/components/npc_engine/CMakeLists.txt @@ -22,6 +22,7 @@ idf_component_register( REQUIRES media_manager hints_client + esp_http_client esp_timer esp_system nvs_flash diff --git a/idf_zacus/components/npc_engine/include/npc_engine.h b/idf_zacus/components/npc_engine/include/npc_engine.h index e1b2c77..d320be3 100644 --- a/idf_zacus/components/npc_engine/include/npc_engine.h +++ b/idf_zacus/components/npc_engine/include/npc_engine.h @@ -184,6 +184,12 @@ const npc_state_t *npc_engine_state(void); // Thin wrapper kept here to give callers a single npc_engine_* surface. esp_err_t npc_engine_set_group_profile(const char *profile); +// Point the engine at the voice gateway (tools/zacus-gateway). On each scene +// change the engine POSTs the active SCENE_* to {base_url}/game/step?scene=… so +// the phone NPCs disguise that scene's hint. Best-effort; pass NULL/"" to +// disable. `token` is sent as a Bearer header (gateway require_token). +void npc_engine_set_gateway(const char *base_url, const char *token); + // Write the active puzzle id (e.g. "SCENE_LA_DETECTOR") into `out`, // truncated to `cap` bytes (NUL-terminated). Falls back to "SCENE_NPC" // when no scene is active or the engine is not initialised so callers diff --git a/idf_zacus/components/npc_engine/npc_engine.c b/idf_zacus/components/npc_engine/npc_engine.c index 9829689..2a4df1e 100644 --- a/idf_zacus/components/npc_engine/npc_engine.c +++ b/idf_zacus/components/npc_engine/npc_engine.c @@ -20,6 +20,7 @@ #include #include "esp_err.h" +#include "esp_http_client.h" #include "esp_log.h" #include "media_manager.h" @@ -27,6 +28,46 @@ static const char *TAG = "npc_engine"; +// Voice gateway (tools/zacus-gateway) — we push the active SCENE_* to it at each +// scene change so the phone NPCs disguise THIS scene's hint. Best-effort, set by +// npc_engine_set_gateway(); empty = disabled (no-op, e.g. CI / dry runs). +static char s_gw_url[128] = {0}; +static char s_gw_token[80] = {0}; + +void npc_engine_set_gateway(const char *base_url, const char *token) { + if (base_url) snprintf(s_gw_url, sizeof(s_gw_url), "%s", base_url); + if (token) snprintf(s_gw_token, sizeof(s_gw_token), "%s", token); +} + +// Fire a one-shot POST {gateway}/game/step?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; + char url[256]; + snprintf(url, sizeof(url), "%s/game/step?scene=%s", s_gw_url, scene); + esp_http_client_config_t cfg = { + .url = url, + .method = HTTP_METHOD_POST, + .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); + } + 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); +} + // ─── Core: scene/trigger/mood lookup tables (verbatim Arduino) ────────────── static const char *const kSceneIds[] = { @@ -372,6 +413,8 @@ esp_err_t npc_engine_set_step(uint8_t step_id, uint32_t expected_duration_ms) { (unsigned) prev_scene, (unsigned) step_id); } (void) hints_client_puzzle_start(puzzle_id); + // Keep the voice gateway in sync so the phone NPCs hint on THIS scene. + notify_gateway_scene(puzzle_id); } return ESP_OK; } diff --git a/idf_zacus/main/main.c b/idf_zacus/main/main.c index 6f71c72..8967cfe 100644 --- a/idf_zacus/main/main.c +++ b/idf_zacus/main/main.c @@ -63,6 +63,12 @@ // this to NVS so the field operator can repoint the firmware without a flash. #define ZACUS_HINTS_BASE_URL "http://192.168.0.150:8302" +// Voice gateway (tools/zacus-gateway) — the engine POSTs each scene change to +// {URL}/game/step?scene=SCENE_* so the phone NPCs disguise that scene's hint. +// Hardcoded for now like the URLs above — moves to NVS in the same follow-up. +#define ZACUS_GATEWAY_BASE_URL "http://192.168.0.175:8401" +#define ZACUS_GATEWAY_TOKEN "testtoken" + // Slice 7: voice-bridge WebSocket on the MacStudio (Tailscale address). // Hardcoded here for the same reason as ZACUS_HINTS_BASE_URL — moves to // NVS in a follow-up slice. The bridge endpoint is documented in @@ -530,6 +536,9 @@ void app_main(void) { ESP_LOGE(TAG, "npc_engine_init failed: %s", esp_err_to_name(npc_err)); } + // Push each scene change to the voice gateway so the phone NPCs + // disguise the CURRENT scene's hint (best-effort, see main.c URL). + npc_engine_set_gateway(ZACUS_GATEWAY_BASE_URL, ZACUS_GATEWAY_TOKEN); // Slice 5: bring up the hints HTTP client (so npc_engine can // route hint requests through the real backend) and the voice