plip: optional auto story-ring (off by default)
CI / platformio (pull_request) Failing after 3m46s
CI / platformio (push) Failing after 4m1s

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.
This commit was merged in pull request #46.
This commit is contained in:
clement
2026-06-20 21:08:56 +02:00
parent 42b5d6375a
commit b203f0e4de
4 changed files with 116 additions and 1 deletions
+10
View File
@@ -84,6 +84,16 @@ menu "PLIP Voice Configuration"
Can be combined with PLIP_DIAL_PULSE: whichever source detects a Can be combined with PLIP_DIAL_PULSE: whichever source detects a
digit first wins. Default off — enable for touch-tone handsets. 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 config PLIP_GATEWAY_URL
string "NPC Gateway Base URL" string "NPC Gateway Base URL"
default "http://192.168.0.50:8401" default "http://192.168.0.50:8401"
+83 -1
View File
@@ -33,6 +33,7 @@
#include "esp_log.h" #include "esp_log.h"
#include "esp_timer.h" #include "esp_timer.h"
#include "esp_heap_caps.h" #include "esp_heap_caps.h"
#include "esp_random.h"
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
@@ -98,6 +99,15 @@ static char s_incoming_number[16] = {0};
static char s_incoming_scene[40] = {0}; static char s_incoming_scene[40] = {0};
static volatile bool s_incoming_armed = false; 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 */ /* Known numbers: ringback when dialed */
static const char *KNOWN[] = { static const char *KNOWN[] = {
"12", "3615", "15", "17", "18", "0142738200", NULL "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) 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(); tones_stop();
audio_stop(); audio_stop();
audio_pa_set(false); 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); 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 1530 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) static void conv_task(void *arg)
{ {
(void)arg; (void)arg;
@@ -185,7 +247,10 @@ static void conv_task(void *arg)
} else { } else {
/* Off-hook from idle */ /* Off-hook from idle */
if (s_state == STATE_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). */ /* INCOMING call answered (phone.c detected the edge). */
enter_incoming_greet(); enter_incoming_greet();
} else if (plip_gamebook_available()) { } else if (plip_gamebook_available()) {
@@ -222,6 +287,19 @@ static void conv_task(void *arg)
* line directly — it reads the pickup cleanly mid-ring. */ * line directly — it reads the pickup cleanly mid-ring. */
if (s_incoming_armed && slic_is_offhook()) { if (s_incoming_armed && slic_is_offhook()) {
enter_incoming_greet(); 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; break;
@@ -483,6 +561,10 @@ void conversation_init(void)
xTaskCreate(conv_task, "conv", 8192, NULL, 4, NULL); xTaskCreate(conv_task, "conv", 8192, NULL, 4, NULL);
#else #else
xTaskCreate(conv_task, "conv", 6144, NULL, 4, NULL); xTaskCreate(conv_task, "conv", 6144, NULL, 4, NULL);
#endif
#if CONFIG_PLIP_AUTO_RING
/* Auto story-ring scheduler: rings every 1530 min when idle. */
xTaskCreate(story_ring_task, "storyring", 3072, NULL, 3, NULL);
#endif #endif
ESP_LOGI(TAG, "conversation init"); ESP_LOGI(TAG, "conversation init");
} }
+19
View File
@@ -7,6 +7,7 @@
#include "cJSON.h" #include "cJSON.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_random.h"
#include "audio.h" #include "audio.h"
@@ -134,6 +135,24 @@ void plip_gamebook_begin(void)
enter_menu(); 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) void plip_gamebook_feed_digit(int d)
{ {
if (s_mode == GB_MENU) { if (s_mode == GB_MENU) {
+4
View File
@@ -28,6 +28,10 @@ bool plip_gamebook_available(void);
// Démarre le mode livre-jeu : charge la bibliothèque et joue le menu audio. // Démarre le mode livre-jeu : charge la bibliothèque et joue le menu audio.
void plip_gamebook_begin(void); 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 → // 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 // choisit la réponse ; sur une fin → 0 revient au menu. Interrompt la
// narration en cours pour enchaîner. // narration en cours pour enchaîner.