feat(plip): conversation state machine — dialtone/dialing/routing/ringback/busy (stage 1)

This commit is contained in:
2026-06-14 22:01:33 +02:00
parent 4621f451be
commit b92f138f41
3 changed files with 167 additions and 0 deletions
+155
View File
@@ -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 <string.h>
#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;
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
#include <stdbool.h>
void conversation_init(void);
void conversation_on_hook_change(bool offhook);
+7
View File
@@ -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) {