From b92f138f414b94fbea05b8c99bd5510ddeaf3374 Mon Sep 17 00:00:00 2001 From: electron-rare Date: Sun, 14 Jun 2026 22:01:33 +0200 Subject: [PATCH] =?UTF-8?q?feat(plip):=20conversation=20state=20machine=20?= =?UTF-8?q?=E2=80=94=20dialtone/dialing/routing/ringback/busy=20(stage=201?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plip_voice/main/conversation.c | 155 +++++++++++++++++++++++++++++++++ plip_voice/main/conversation.h | 5 ++ plip_voice/main/main.c | 7 ++ 3 files changed, 167 insertions(+) create mode 100644 plip_voice/main/conversation.c create mode 100644 plip_voice/main/conversation.h diff --git a/plip_voice/main/conversation.c b/plip_voice/main/conversation.c new file mode 100644 index 0000000..081428a --- /dev/null +++ b/plip_voice/main/conversation.c @@ -0,0 +1,155 @@ +/* + * conversation.c — PLIP telephone conversation state machine. + * + * States: IDLE → DIALTONE → DIALING → RINGBACK | BUSY + * + * Transitions: + * IDLE + off-hook → DIALTONE (tones_dialtone_start) + * DIALTONE + first digit → DIALING (tones_stop) + * DIALING + ms_since_last > 3000 → RINGBACK or BUSY (route decision) + * any state + on-hook → IDLE (tones_stop, audio_stop, PA off) + * + * Routing table (known numbers → ringback; unknown → busy after 3 s silence): + * "12", "3615", "15", "17", "18", "0142738200" + */ + +#include "conversation.h" +#include "dialer.h" +#include "tones.h" +#include "audio.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" + +#include + +#define TAG "conversation" + +typedef enum { + STATE_IDLE, + STATE_DIALTONE, + STATE_DIALING, + STATE_RINGBACK, + STATE_BUSY, +} conv_state_t; + +static volatile conv_state_t s_state = STATE_IDLE; +static volatile bool s_offhook = false; +static volatile bool s_hook_changed = false; + +/* Known numbers: ringback when dialed */ +static const char *KNOWN[] = { + "12", "3615", "15", "17", "18", "0142738200", NULL +}; + +static bool is_known(const char *num) +{ + for (int i = 0; KNOWN[i] != NULL; i++) { + if (strcmp(num, KNOWN[i]) == 0) return true; + } + return false; +} + +static void go_idle(void) +{ + tones_stop(); + audio_stop(); + audio_pa_set(false); + dialer_reset(); + s_state = STATE_IDLE; + ESP_LOGI(TAG, "-> IDLE"); +} + +static void conv_task(void *arg) +{ + (void)arg; + ESP_LOGI(TAG, "conversation task ready"); + + for (;;) { + vTaskDelay(pdMS_TO_TICKS(50)); + + /* Handle hook change events */ + if (s_hook_changed) { + s_hook_changed = false; + if (!s_offhook) { + /* On-hook: always go idle regardless of current state */ + if (s_state != STATE_IDLE) { + ESP_LOGI(TAG, "on-hook -> IDLE"); + go_idle(); + } + continue; + } else { + /* Off-hook: start dial tone if we were idle */ + if (s_state == STATE_IDLE) { + dialer_reset(); + tones_dialtone_start(); + s_state = STATE_DIALTONE; + ESP_LOGI(TAG, "off-hook -> DIALTONE"); + } + continue; + } + } + + /* State machine polling */ + switch (s_state) { + case STATE_IDLE: + break; /* nothing to do */ + + case STATE_DIALTONE: + if (!s_offhook) { + go_idle(); + break; + } + /* First digit received → stop dialtone, enter dialing */ + if (!dialer_idle()) { + tones_stop(); + s_state = STATE_DIALING; + ESP_LOGI(TAG, "first digit -> DIALING"); + } + break; + + case STATE_DIALING: + if (!s_offhook) { + go_idle(); + break; + } + /* Wait for 3 s of silence after last digit, then route */ + if (dialer_ms_since_last() > 3000) { + const char *num = dialer_current(); + if (is_known(num)) { + ESP_LOGI(TAG, "route %s -> known (ringback)", num); + tones_ringback_start(); + s_state = STATE_RINGBACK; + } else { + ESP_LOGI(TAG, "route %s -> unknown (busy)", num); + tones_busy_start(); + s_state = STATE_BUSY; + } + } + break; + + case STATE_RINGBACK: + case STATE_BUSY: + if (!s_offhook) { + go_idle(); + } + break; + } + } +} + +void conversation_init(void) +{ + s_state = STATE_IDLE; + s_offhook = false; + s_hook_changed = false; + xTaskCreate(conv_task, "conv", 3072, NULL, 4, NULL); + ESP_LOGI(TAG, "conversation init"); +} + +void conversation_on_hook_change(bool offhook) +{ + s_offhook = offhook; + s_hook_changed = true; +} diff --git a/plip_voice/main/conversation.h b/plip_voice/main/conversation.h new file mode 100644 index 0000000..b95c71a --- /dev/null +++ b/plip_voice/main/conversation.h @@ -0,0 +1,5 @@ +#pragma once +#include + +void conversation_init(void); +void conversation_on_hook_change(bool offhook); diff --git a/plip_voice/main/main.c b/plip_voice/main/main.c index 5648291..9b38563 100644 --- a/plip_voice/main/main.c +++ b/plip_voice/main/main.c @@ -30,6 +30,8 @@ #include "hook_client.h" #include "phone.h" #include "scenario_mesh.h" +#include "dialer.h" +#include "conversation.h" static const char *TAG = "plip-main"; @@ -101,6 +103,11 @@ static void boot_task(void *arg) ESP_LOGW(TAG, "hook_client_init: %s", esp_err_to_name(ret)); } + /* ── Stage 1: dialer + conversation state machine ── */ + dialer_init(); + conversation_init(); + ESP_LOGI(TAG, "Stage 1: dialer + conversation state machine up"); + /* ── 7. Phone task (Phase D) ── */ ret = phone_init(); if (ret != ESP_OK) {