From b203f0e4de38cd980554e02d3d943f5b9c8f16a5 Mon Sep 17 00:00:00 2001 From: clement Date: Sat, 20 Jun 2026 21:08:56 +0200 Subject: [PATCH] plip: optional auto story-ring (off by default) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Escape-room mechanic: the retro phone can call the players on its own and tell them a story when they pick up. Approach: a story_ring_task rings at a random 15-30 min interval (only when the line is idle and a story pack is on the SD). Picking up routes through enter_story_from_ring(), which silences the bell and launches a random story via plip_gamebook_begin_random() straight into the gamebook state — no menu. If nobody answers within 1 min the bell stops and the next ring is rescheduled; hanging up mid-ring also silences it. The whole feature is gated behind CONFIG_PLIP_AUTO_RING (default n), so the phone stays silent unless explicitly enabled in menuconfig. The hook pickup paths (raw SLIC poll + debounced edge) mirror the existing NPC incoming-call handling, which is unreliable while the bell rings. --- plip_voice/main/Kconfig.projbuild | 10 ++++ plip_voice/main/conversation.c | 84 ++++++++++++++++++++++++++++++- plip_voice/main/plip_gamebook.c | 19 +++++++ plip_voice/main/plip_gamebook.h | 4 ++ 4 files changed, 116 insertions(+), 1 deletion(-) diff --git a/plip_voice/main/Kconfig.projbuild b/plip_voice/main/Kconfig.projbuild index cffc459..b050b0b 100644 --- a/plip_voice/main/Kconfig.projbuild +++ b/plip_voice/main/Kconfig.projbuild @@ -84,6 +84,16 @@ menu "PLIP Voice Configuration" Can be combined with PLIP_DIAL_PULSE: whichever source detects a digit first wins. Default off — enable for touch-tone handsets. + config PLIP_AUTO_RING + bool "Auto story-ring (rings by itself every 15-30 min)" + default n + help + When enabled, the phone rings on its own at a random 15-30 minute + interval; picking up launches a random audio story straight away + (no menu). If nobody answers within 1 minute the bell stops and the + next ring is rescheduled. Default off — enable for the escape-room + ambience where the phone calls the players. + config PLIP_GATEWAY_URL string "NPC Gateway Base URL" default "http://192.168.0.50:8401" diff --git a/plip_voice/main/conversation.c b/plip_voice/main/conversation.c index 36937a5..6264c51 100644 --- a/plip_voice/main/conversation.c +++ b/plip_voice/main/conversation.c @@ -33,6 +33,7 @@ #include "esp_log.h" #include "esp_timer.h" #include "esp_heap_caps.h" +#include "esp_random.h" #include #include @@ -98,6 +99,15 @@ static char s_incoming_number[16] = {0}; static char s_incoming_scene[40] = {0}; static volatile bool s_incoming_armed = false; +/* Auto story-ring: the phone rings on its own at a random interval; picking up + * launches a random audio story straight away. If nobody answers, the bell + * stops after STORY_RING_TIMEOUT_MS and the next ring is rescheduled. */ +#define STORY_RING_MIN_MS (15 * 60 * 1000) /* 15 min */ +#define STORY_RING_SPAN_MS (15 * 60 * 1000) /* + up to 15 min → 30 max */ +#define STORY_RING_TIMEOUT_MS (60 * 1000) /* give up after 1 min */ +static volatile bool s_story_ring_armed = false; +static int64_t s_story_ring_start_us = 0; + /* Known numbers: ringback when dialed */ static const char *KNOWN[] = { "12", "3615", "15", "17", "18", "0142738200", NULL @@ -127,6 +137,11 @@ static void scene_slug(const char *scene, char *out, size_t cap) static void go_idle(void) { + if (s_story_ring_armed) { /* hung up mid auto-ring → silence the bell */ + phone_ring_stop(); + slic_ring_stop(); + s_story_ring_armed = false; + } tones_stop(); audio_stop(); audio_pa_set(false); @@ -164,6 +179,53 @@ static void enter_incoming_greet(void) ESP_LOGI(TAG, "INCOMING pickup -> GREET num=%s sid=%s", s_number, s_sid); } +/* Answered an auto story-ring: stop the bell and launch a random story right + * into STATE_GAMEBOOK (no menu). Drives s_offhook directly because the + * debounced GPIO hook detection is unreliable while the bell rings. */ +static void enter_story_from_ring(void) +{ + s_story_ring_armed = false; + phone_ring_stop(); + slic_ring_stop(); + tones_stop(); + dialer_reset(); + s_offhook = true; +#if CONFIG_PLIP_DIAL_DTMF + dtmf_start(); +#endif + audio_pa_set(true); + plip_gamebook_begin_random(); + s_state = STATE_GAMEBOOK; + ESP_LOGI(TAG, "story-ring pickup -> GAMEBOOK (random story)"); +} + +#if CONFIG_PLIP_AUTO_RING +/* Rings the phone at a random 15–30 min interval. Only rings when the line is + * truly idle (on-hook, no other call/ring armed) and a story pack is present. + * The 1-minute no-answer timeout is handled in conv_task. */ +static void story_ring_task(void *arg) +{ + (void)arg; + for (;;) { + uint32_t wait_ms = STORY_RING_MIN_MS + (esp_random() % STORY_RING_SPAN_MS); + ESP_LOGI(TAG, "auto story-ring: next attempt in %u min", + (unsigned)(wait_ms / 60000)); + vTaskDelay(pdMS_TO_TICKS(wait_ms)); + + if (s_state == STATE_IDLE && !s_offhook && !s_incoming_armed + && !s_story_ring_armed && plip_gamebook_available()) { + s_story_ring_armed = true; + s_story_ring_start_us = esp_timer_get_time(); + phone_ring_start(); + ESP_LOGI(TAG, "auto story-ring: ringing (up to %d s)", + STORY_RING_TIMEOUT_MS / 1000); + } else { + ESP_LOGI(TAG, "auto story-ring: line busy/unavailable — skipped"); + } + } +} +#endif /* CONFIG_PLIP_AUTO_RING */ + static void conv_task(void *arg) { (void)arg; @@ -185,7 +247,10 @@ static void conv_task(void *arg) } else { /* Off-hook from idle */ if (s_state == STATE_IDLE) { - if (s_incoming_armed) { + if (s_story_ring_armed) { + /* Answered an auto story-ring → straight into a story. */ + enter_story_from_ring(); + } else if (s_incoming_armed) { /* INCOMING call answered (phone.c detected the edge). */ enter_incoming_greet(); } else if (plip_gamebook_available()) { @@ -222,6 +287,19 @@ static void conv_task(void *arg) * line directly — it reads the pickup cleanly mid-ring. */ if (s_incoming_armed && slic_is_offhook()) { enter_incoming_greet(); + } else if (s_story_ring_armed) { + /* Same raw-SLIC poll for the auto story-ring, plus the + * no-answer timeout: stop the bell after 1 min unanswered. */ + if (slic_is_offhook()) { + enter_story_from_ring(); + } else if ((esp_timer_get_time() - s_story_ring_start_us) / 1000 + >= STORY_RING_TIMEOUT_MS) { + phone_ring_stop(); + slic_ring_stop(); + s_story_ring_armed = false; + ESP_LOGI(TAG, "auto story-ring: no answer after %d s — stop", + STORY_RING_TIMEOUT_MS / 1000); + } } break; @@ -483,6 +561,10 @@ void conversation_init(void) xTaskCreate(conv_task, "conv", 8192, NULL, 4, NULL); #else xTaskCreate(conv_task, "conv", 6144, NULL, 4, NULL); +#endif +#if CONFIG_PLIP_AUTO_RING + /* Auto story-ring scheduler: rings every 15–30 min when idle. */ + xTaskCreate(story_ring_task, "storyring", 3072, NULL, 3, NULL); #endif ESP_LOGI(TAG, "conversation init"); } diff --git a/plip_voice/main/plip_gamebook.c b/plip_voice/main/plip_gamebook.c index 954af96..281e629 100644 --- a/plip_voice/main/plip_gamebook.c +++ b/plip_voice/main/plip_gamebook.c @@ -7,6 +7,7 @@ #include "cJSON.h" #include "esp_log.h" +#include "esp_random.h" #include "audio.h" @@ -134,6 +135,24 @@ void plip_gamebook_begin(void) enter_menu(); } +void plip_gamebook_begin_random(void) +{ + plip_gamebook_end(); // clean any previous run + + s_lib_root = load_json(GB_LIBRARY); + if (!s_lib_root) return; + s_lib = cJSON_GetObjectItem(s_lib_root, "library"); + if (!cJSON_IsArray(s_lib) || cJSON_GetArraySize(s_lib) == 0) { + cJSON_Delete(s_lib_root); s_lib_root = NULL; s_lib = NULL; + return; + } + s_lib_n = cJSON_GetArraySize(s_lib); + audio_pa_set(true); // earpiece amplifier on + int idx = (int)(esp_random() % (uint32_t)s_lib_n); + ESP_LOGI(TAG, "auto-start random story #%d/%d", idx, s_lib_n); + load_book(idx); // straight into the story, no menu +} + void plip_gamebook_feed_digit(int d) { if (s_mode == GB_MENU) { diff --git a/plip_voice/main/plip_gamebook.h b/plip_voice/main/plip_gamebook.h index 20971eb..3fb71d9 100644 --- a/plip_voice/main/plip_gamebook.h +++ b/plip_voice/main/plip_gamebook.h @@ -28,6 +28,10 @@ bool plip_gamebook_available(void); // Démarre le mode livre-jeu : charge la bibliothèque et joue le menu audio. void plip_gamebook_begin(void); +// Démarre directement UNE histoire tirée au hasard (sans passer par le menu) — +// utilisé quand le téléphone sonne tout seul et qu'on décroche. +void plip_gamebook_begin_random(void); + // Traite un chiffre composé (0..9). Menu → choisit l'histoire ; en histoire → // choisit la réponse ; sur une fin → 0 revient au menu. Interrompt la // narration en cours pour enchaîner. -- 2.52.0