From 1561e613e13ff6eaed96714e19d441415bf7731c Mon Sep 17 00:00:00 2001 From: clement Date: Tue, 16 Jun 2026 10:32:01 +0200 Subject: [PATCH] feat(plip): incoming call + fast half-duplex - /debug/ring?number=NN arms incoming call, starts ring - conversation_arm_incoming() + enter_incoming_greet() - offhook with armed call skips dial/ringback, goes to GREET - slic_is_offhook() used during ring (GPIO debounce unreliable) - CAPTURE_SILENCE_MS 800->600 ms, settles 250->120, 200->100 ms --- plip_voice/main/conversation.c | 68 +++++++++++++++++++++++++++++----- plip_voice/main/conversation.h | 5 +++ plip_voice/main/net.c | 15 ++++++-- 3 files changed, 75 insertions(+), 13 deletions(-) diff --git a/plip_voice/main/conversation.c b/plip_voice/main/conversation.c index f6fbdec..23337f5 100644 --- a/plip_voice/main/conversation.c +++ b/plip_voice/main/conversation.c @@ -21,6 +21,7 @@ #include "tones.h" #include "audio.h" #include "phone.h" +#include "slic.h" #include "turn_client.h" #if CONFIG_PLIP_DIAL_DTMF #include "dtmf.h" @@ -46,7 +47,7 @@ #define CAPTURE_MAX_IRAM (128 * 1024) #define CAPTURE_MAX_MS_PSRAM 8000 #define CAPTURE_MAX_MS_IRAM 4000 -#define CAPTURE_SILENCE_MS 800 +#define CAPTURE_SILENCE_MS 600 /* end-of-speech VAD: shorter = snappier reply (was 800) */ #define REPLY_POLL_MS 200 /* interval for checking hook during playback */ #define REPLY_PLAYBACK_EXTRA_MS 500 /* safety margin added to computed WAV duration */ #define BETWEEN_TURNS_MS 300 /* short pause between capture rounds */ @@ -82,6 +83,13 @@ static char s_sid[32] = {0}; * would 404 at the gateway. Capture the clean routed number here once. */ static char s_number[16] = {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). */ +static char s_incoming_number[16] = {0}; +static volatile bool s_incoming_armed = false; + /* Known numbers: ringback when dialed */ static const char *KNOWN[] = { "12", "3615", "15", "17", "18", "0142738200", NULL @@ -108,6 +116,24 @@ static void go_idle(void) ESP_LOGI(TAG, "-> IDLE"); } +/* Begin an answered INCOMING call: stop ringing, lock the persona number, and + * jump straight to GREET (the NPC speaks first). Drives the conversation's own + * s_offhook directly because phone.c's debounced GPIO detection is unreliable + * during ringing — we trust the SLIC's raw off-hook reading instead. */ +static void enter_incoming_greet(void) +{ + s_incoming_armed = false; + phone_ring_stop(); + tones_stop(); + dialer_reset(); + s_offhook = true; + snprintf(s_number, sizeof(s_number), "%s", s_incoming_number); + snprintf(s_sid, sizeof(s_sid), "%lld", (long long)esp_timer_get_time()); + audio_pa_set(true); + s_state = STATE_GREET; + ESP_LOGI(TAG, "INCOMING pickup -> GREET num=%s sid=%s", s_number, s_sid); +} + static void conv_task(void *arg) { (void)arg; @@ -127,15 +153,21 @@ static void conv_task(void *arg) } continue; } else { - /* Off-hook: start dial tone if we were idle */ + /* Off-hook from idle */ if (s_state == STATE_IDLE) { - dialer_reset(); - tones_dialtone_start(); + if (s_incoming_armed) { + /* INCOMING call answered (phone.c detected the edge). */ + enter_incoming_greet(); + } else { + /* Outgoing call: dial tone, wait for digits. */ + dialer_reset(); + tones_dialtone_start(); #if CONFIG_PLIP_DIAL_DTMF - dtmf_start(); + dtmf_start(); #endif - s_state = STATE_DIALTONE; - ESP_LOGI(TAG, "off-hook -> DIALTONE"); + s_state = STATE_DIALTONE; + ESP_LOGI(TAG, "off-hook -> DIALTONE"); + } } continue; } @@ -144,7 +176,13 @@ static void conv_task(void *arg) /* State machine polling */ switch (s_state) { case STATE_IDLE: - break; /* nothing to do */ + /* Incoming-call pickup: phone.c's debounced edge detection is + * unreliable while the bell rings, so poll the SLIC's raw off-hook + * line directly — it reads the pickup cleanly mid-ring. */ + if (s_incoming_armed && slic_is_offhook()) { + enter_incoming_greet(); + } + break; case STATE_DIALTONE: if (!s_offhook) { @@ -268,12 +306,12 @@ static void conv_task(void *arg) * the mic. Never capture while anything is playing, or the * playback feeds back and the line saturates. Wait for the * greeting/filler/reply to finish, then let the line settle. */ - vTaskDelay(pdMS_TO_TICKS(250)); /* let a just-queued clip start */ + vTaskDelay(pdMS_TO_TICKS(120)); /* let a just-queued clip start (was 250) */ while (s_offhook && audio_is_playing()) { vTaskDelay(pdMS_TO_TICKS(50)); } if (!s_offhook) break; - vTaskDelay(pdMS_TO_TICKS(200)); /* line settle after playback */ + vTaskDelay(pdMS_TO_TICKS(100)); /* line settle after playback (was 200) */ /* Capture player utterance — nothing is playing now. */ int n = audio_capture_wav(cap_buf, cap_max, @@ -355,3 +393,13 @@ void conversation_on_hook_change(bool offhook) s_offhook = offhook; s_hook_changed = true; } + +void conversation_arm_incoming(const char *number) +{ + snprintf(s_incoming_number, sizeof(s_incoming_number), "%s", + (number && number[0]) ? number : "17"); + s_incoming_armed = true; + phone_ring_start(); + ESP_LOGI(TAG, "incoming call armed (num=%s) — ringing until pickup", + s_incoming_number); +} diff --git a/plip_voice/main/conversation.h b/plip_voice/main/conversation.h index b95c71a..a48f278 100644 --- a/plip_voice/main/conversation.h +++ b/plip_voice/main/conversation.h @@ -3,3 +3,8 @@ void conversation_init(void); 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); diff --git a/plip_voice/main/net.c b/plip_voice/main/net.c index 347565a..6c787fe 100644 --- a/plip_voice/main/net.c +++ b/plip_voice/main/net.c @@ -26,6 +26,7 @@ #include "audio.h" #include "cmd_exec.h" #include "phone.h" +#include "conversation.h" #include "es8388.h" #include "slic.h" #include "board_config.h" @@ -471,9 +472,17 @@ static esp_err_t handle_cmd_post(httpd_req_t *req) static esp_err_t handle_debug_ring(httpd_req_t *req) { - phone_ring_start(); - ESP_LOGI(TAG, "debug/ring: ring started"); - return send_json(req, "200 OK", "{\"ok\":true,\"ringing\":true}"); + /* /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}; + 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 */ + char resp[64]; + snprintf(resp, sizeof(resp), "{\"ok\":true,\"ringing\":true,\"incoming\":\"%s\"}", + number[0] ? number : "17"); + ESP_LOGI(TAG, "debug/ring: incoming armed (num=%s), ringing", number[0] ? number : "17"); + return send_json(req, "200 OK", resp); } /* ── GET /debug/ringstop (stop ring tone over HTTP) ─────────────────────── */