plip: optional auto story-ring (off by default)
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:
@@ -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"
|
||||||
|
|||||||
@@ -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 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)
|
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 15–30 min when idle. */
|
||||||
|
xTaskCreate(story_ring_task, "storyring", 3072, NULL, 3, NULL);
|
||||||
#endif
|
#endif
|
||||||
ESP_LOGI(TAG, "conversation init");
|
ESP_LOGI(TAG, "conversation init");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user