diff --git a/plip_voice/main/CMakeLists.txt b/plip_voice/main/CMakeLists.txt index 9c79a3e..20ed31c 100644 --- a/plip_voice/main/CMakeLists.txt +++ b/plip_voice/main/CMakeLists.txt @@ -7,6 +7,9 @@ idf_component_register( "cmd_exec.c" "hook_client.c" "phone.c" + "tones.c" + "dialer.c" + "conversation.c" INCLUDE_DIRS "." PRIV_REQUIRES driver @@ -14,6 +17,7 @@ idf_component_register( esp_http_client esp_http_server esp_netif + esp_timer esp_wifi json nvs_flash diff --git a/plip_voice/main/Kconfig.projbuild b/plip_voice/main/Kconfig.projbuild index e89061c..5b3b9ed 100644 --- a/plip_voice/main/Kconfig.projbuild +++ b/plip_voice/main/Kconfig.projbuild @@ -43,4 +43,23 @@ menu "PLIP Voice Configuration" GPIO that signals handset off-hook (active LOW via INPUT_PULLUP). Dev kit uses BOOT button (GPIO4 / KEY1). PCB uses Si3210 INT. + config PLIP_DIAL_PULSE + bool "Enable rotary dial pulse decoding on SHK GPIO" + default y + help + When enabled, brief open/close pulses on the hook GPIO (from a + rotary dial) are decoded into digits and pushed to the dialer. + Each pulse train: ~60-100 ms open + ~40 ms closed; 10 pulses = digit 0. + A gap > PLIP_DIAL_PULSE_MAX_GAP_MS terminates the digit. + + config PLIP_DIAL_PULSE_MAX_GAP_MS + int "Rotary pulse inter-digit gap (ms)" + default 200 + depends on PLIP_DIAL_PULSE + range 100 500 + help + If the hook GPIO stays closed for more than this duration after + a pulse train, the train is considered complete and the digit is + emitted. 200 ms is standard for French rotary dials. + endmenu 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/dialer.c b/plip_voice/main/dialer.c new file mode 100644 index 0000000..2a73125 --- /dev/null +++ b/plip_voice/main/dialer.c @@ -0,0 +1,73 @@ +/* + * dialer.c — Digit accumulator for the PLIP telephone state machine. + * + * Digits are pushed from two sources: + * 1. HTTP /debug/dial?number=NNNN (net.c handler) + * 2. Rotary pulse detection (phone.c, CONFIG_PLIP_DIAL_PULSE) + * + * Thread-safety: all state is protected by atomic operations on s_last_us + * and simple writes to s_len / s_num (called from a single phone task or + * HTTP task at a time — no concurrent push expected). + */ + +#include "dialer.h" + +#include "esp_log.h" +#include "esp_timer.h" + +#include +#include + +#define TAG "dialer" + +#define MAX_DIGITS 12 + +static char s_num[MAX_DIGITS + 1]; +static int s_len; +static int64_t s_last_us; /* esp_timer_get_time() at last push */ + +void dialer_init(void) +{ + memset(s_num, 0, sizeof(s_num)); + s_len = 0; + s_last_us = 0; + ESP_LOGI(TAG, "dialer init"); +} + +void dialer_push_digit(int d) +{ + if (d < 0 || d > 9) return; + if (s_len >= MAX_DIGITS) return; /* silently discard overflow */ + + s_num[s_len++] = (char)('0' + d); + s_num[s_len] = '\0'; + s_last_us = esp_timer_get_time(); + + ESP_LOGI(TAG, "digit %d -> number so far: \"%s\"", d, s_num); +} + +void dialer_reset(void) +{ + memset(s_num, 0, sizeof(s_num)); + s_len = 0; + s_last_us = 0; + ESP_LOGI(TAG, "dialer reset"); +} + +const char *dialer_current(void) +{ + return s_num; +} + +bool dialer_idle(void) +{ + return s_len == 0; +} + +int dialer_ms_since_last(void) +{ + if (s_last_us == 0) return 0; + int64_t elapsed_us = esp_timer_get_time() - s_last_us; + int ms = (int)(elapsed_us / 1000); + return (ms < 0) ? 0 : ms; +} diff --git a/plip_voice/main/dialer.h b/plip_voice/main/dialer.h new file mode 100644 index 0000000..6483095 --- /dev/null +++ b/plip_voice/main/dialer.h @@ -0,0 +1,9 @@ +#pragma once +#include + +void dialer_init(void); +void dialer_push_digit(int d); +void dialer_reset(void); +const char *dialer_current(void); +bool dialer_idle(void); +int dialer_ms_since_last(void); 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) { diff --git a/plip_voice/main/net.c b/plip_voice/main/net.c index f91242c..1f1a20c 100644 --- a/plip_voice/main/net.c +++ b/plip_voice/main/net.c @@ -15,6 +15,7 @@ */ #include "net.h" +#include "dialer.h" #include #include @@ -428,6 +429,32 @@ static esp_err_t handle_cmd_post(httpd_req_t *req) return send_json(req, "200 OK", "{\"ok\":true}"); } +/* ── GET /debug/dial?number=NNNN (push digits into the dialer) ───────────── */ + +static esp_err_t handle_debug_dial(httpd_req_t *req) +{ + char query[64] = {0}; + httpd_req_get_url_query_str(req, query, sizeof(query)); + char number[16] = {0}; + if (httpd_query_key_value(query, "number", number, sizeof(number)) != ESP_OK) { + return send_json(req, "400 Bad Request", "{\"error\":\"missing number param\"}"); + } + + int pushed = 0; + for (int i = 0; number[i] != '\0' && i < 12; i++) { + if (number[i] >= '0' && number[i] <= '9') { + dialer_push_digit(number[i] - '0'); + pushed++; + } + } + + char resp[64]; + snprintf(resp, sizeof(resp), "{\"ok\":true,\"pushed\":%d,\"number\":\"%s\"}", + pushed, dialer_current()); + ESP_LOGI(TAG, "debug/dial: pushed %d digits -> \"%s\"", pushed, dialer_current()); + return send_json(req, "200 OK", resp); +} + /* ── Public API ───────────────────────────────────────────────────────────── */ bool net_is_connected(void) @@ -500,7 +527,7 @@ esp_err_t net_init(void) /* Start HTTP server. */ httpd_config_t hcfg = HTTPD_DEFAULT_CONFIG(); hcfg.server_port = 80; - hcfg.max_uri_handlers = 12; + hcfg.max_uri_handlers = 14; hcfg.stack_size = 8192; esp_err_t ret = httpd_start(&s_httpd, &hcfg); @@ -533,13 +560,18 @@ esp_err_t net_init(void) .uri = "/debug/regs", .method = HTTP_GET, .handler = handle_debug_regs, .user_ctx = NULL, }; + static const httpd_uri_t uri_debug_dial = { + .uri = "/debug/dial", .method = HTTP_GET, + .handler = handle_debug_dial, .user_ctx = NULL, + }; httpd_register_uri_handler(s_httpd, &uri_status); httpd_register_uri_handler(s_httpd, &uri_scenario); httpd_register_uri_handler(s_httpd, &uri_file); httpd_register_uri_handler(s_httpd, &uri_cmd); httpd_register_uri_handler(s_httpd, &uri_capture); httpd_register_uri_handler(s_httpd, &uri_debug_regs); + httpd_register_uri_handler(s_httpd, &uri_debug_dial); - ESP_LOGI(TAG, "httpd up on :80 (GET /status, GET /debug/regs, POST /game/scenario, POST /game/file, POST /game/cmd, POST /voice/capture)"); + ESP_LOGI(TAG, "httpd up on :80 (GET /status, GET /debug/regs, GET /debug/dial, POST /game/scenario, POST /game/file, POST /game/cmd, POST /voice/capture)"); return ESP_OK; } diff --git a/plip_voice/main/phone.c b/plip_voice/main/phone.c index 0d4eff9..984e59c 100644 --- a/plip_voice/main/phone.c +++ b/plip_voice/main/phone.c @@ -1,25 +1,41 @@ /* - * phone.c — Off-hook GPIO debounce + ring control (Phase D). + * phone.c — Off-hook GPIO debounce + rotary pulse decoding + ring control. * - * Mirrors PLIP_FIRMWARE/src/phone_task.cpp logic, ported to IDF C. - * ISR sets a flag; the task reads the GPIO level after a 30 ms debounce - * window and reports the transition via hook_client_report(). + * ISR sets a flag; the task reads the GPIO level after a short debounce + * and reports the transition via hook_client_report() and + * conversation_on_hook_change(). + * + * Rotary pulse decoding (CONFIG_PLIP_DIAL_PULSE): + * While off-hook, a rotary dial produces brief open/close pulses on the + * SHK line (~60-100 ms open per pulse). A real hangup holds the line + * open for >500 ms. The task polls the GPIO at 5 ms intervals to catch + * pulses that would be missed by the 30 ms debounce. + * + * Pulse train end: if the line stays closed for > PLIP_DIAL_PULSE_MAX_GAP_MS + * after at least one pulse, the count is emitted as a digit (10 pulses = 0). */ #include "phone.h" #include "audio.h" #include "hook_client.h" +#include "conversation.h" + +#if CONFIG_PLIP_DIAL_PULSE +#include "dialer.h" +#endif #include "driver/gpio.h" #include "esp_log.h" +#include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #define TAG "phone" -#define DEBOUNCE_MS 30 -#define TASK_STACK 4096 -#define TASK_PRIO 5 +#define DEBOUNCE_MS 30 +#define HANGUP_THRESHOLD_MS 500 /* open > this → real hangup, not a pulse */ +#define TASK_STACK 4096 +#define TASK_PRIO 5 static volatile bool s_edge_pending = false; static volatile bool s_ringing = false; @@ -48,6 +64,12 @@ void phone_ring_stop(void) audio_stop(); } +static void report_offhook(bool offhook) +{ + conversation_on_hook_change(offhook); + hook_client_report(offhook ? "off" : "on", offhook ? "pickup" : "hangup"); +} + static void phone_task(void *arg) { (void)arg; @@ -66,48 +88,148 @@ static void phone_task(void *arg) /* Read and report initial level so master state machine is in sync. */ int last_level = gpio_get_level(hook_gpio); - const char *init_state = (last_level == 0) ? "off" : "on"; s_offhook = (last_level == 0); ESP_LOGI(TAG, "phone task ready, hook GPIO=%d level=%d (%s)", - hook_gpio, last_level, init_state); + hook_gpio, last_level, s_offhook ? "off-hook" : "on-hook"); - /* Set PA state to match initial hook position. - * On-hook at boot → mute PA so no audio escapes while handset is down. */ if (!s_offhook) { - ESP_LOGI(TAG, "boot: on-hook → PA off (mute)"); + ESP_LOGI(TAG, "boot: on-hook -> PA off (mute)"); audio_pa_set(false); } else { - ESP_LOGI(TAG, "boot: off-hook → PA on (unmute)"); + ESP_LOGI(TAG, "boot: off-hook -> PA on (unmute)"); audio_pa_set(true); } - hook_client_report(init_state, "boot"); + hook_client_report(s_offhook ? "off" : "on", "boot"); + conversation_on_hook_change(s_offhook); + +#if CONFIG_PLIP_DIAL_PULSE + /* Rotary pulse state */ + int pulse_count = 0; + bool in_pulse = false; /* GPIO is currently high (open) */ + int64_t pulse_open_us = 0; /* timestamp when GPIO went high */ + int64_t last_close_us = 0; /* timestamp when GPIO last went low (closed) */ +#endif for (;;) { if (s_edge_pending) { s_edge_pending = false; + +#if CONFIG_PLIP_DIAL_PULSE + /* While off-hook, use fast polling instead of 30ms debounce + * so rotary pulses (~60-100ms) are not missed. */ + if (s_offhook) { + /* The ISR fired — re-read immediately to catch rising edge. */ + vTaskDelay(pdMS_TO_TICKS(5)); /* minimal settle */ + int level = gpio_get_level(hook_gpio); + + if (level == 1 && last_level == 0) { + /* Rising edge: GPIO went open */ + last_level = 1; + in_pulse = true; + pulse_open_us = esp_timer_get_time(); + ESP_LOGD(TAG, "pulse: open"); + } else if (level == 0 && last_level == 1) { + /* Falling edge: GPIO closed again */ + last_level = 0; + last_close_us = esp_timer_get_time(); + int64_t open_dur_ms = (last_close_us - pulse_open_us) / 1000; + + if (in_pulse && open_dur_ms < HANGUP_THRESHOLD_MS) { + /* Short open: count as a rotary pulse */ + pulse_count++; + in_pulse = false; + ESP_LOGD(TAG, "pulse %d (open %"PRId64"ms)", pulse_count, open_dur_ms); + } else if (open_dur_ms >= HANGUP_THRESHOLD_MS) { + /* Was open too long: treat as hangup */ + ESP_LOGI(TAG, "on-hook (hangup) after %"PRId64"ms open", open_dur_ms); + pulse_count = 0; + in_pulse = false; + s_offhook = false; + audio_stop(); + audio_pa_set(false); + report_offhook(false); + } + } + /* Don't fall through to the standard debounce below */ + goto poll_sleep; + } +#endif + /* Standard 30 ms debounce for on-hook state transitions */ vTaskDelay(pdMS_TO_TICKS(DEBOUNCE_MS)); int level = gpio_get_level(hook_gpio); if (level != last_level) { last_level = level; if (level == 0) { - /* Off-hook: handset picked up. Enable PA, stop ringing. */ + /* Off-hook: handset picked up. */ s_offhook = true; +#if CONFIG_PLIP_DIAL_PULSE + pulse_count = 0; + in_pulse = false; + last_close_us = esp_timer_get_time(); +#endif ESP_LOGI(TAG, "off-hook (pickup) detected — PA on"); audio_pa_set(true); phone_ring_stop(); - hook_client_report("off", "pickup"); + report_offhook(true); } else { - /* On-hook: handset hung up. Stop playback, kill PA. */ + /* On-hook: handset hung up. */ s_offhook = false; ESP_LOGI(TAG, "on-hook (hangup) detected — PA off, stopping audio"); audio_stop(); audio_pa_set(false); - hook_client_report("on", "hangup"); + report_offhook(false); } } } - vTaskDelay(pdMS_TO_TICKS(20)); + +#if CONFIG_PLIP_DIAL_PULSE + /* Check for inter-digit gap: if we accumulated pulses and the line + * has been closed (quiet) for > MAX_GAP_MS, emit the digit. */ + if (s_offhook && pulse_count > 0 && !in_pulse && last_close_us > 0) { + int64_t gap_ms = (esp_timer_get_time() - last_close_us) / 1000; + if (gap_ms > CONFIG_PLIP_DIAL_PULSE_MAX_GAP_MS) { + if (pulse_count > 10) { + ESP_LOGW(TAG, "bad pulse count %d, ignored", pulse_count); + pulse_count = 0; + } else { + int digit = (pulse_count == 10) ? 0 : pulse_count; + ESP_LOGI(TAG, "rotary digit: %d pulses -> %d", pulse_count, digit); + dialer_push_digit(digit); + pulse_count = 0; + } + } + } + + /* Also detect prolonged open (hangup) even if no more edges arrive */ + if (s_offhook && in_pulse && pulse_open_us > 0) { + int64_t open_ms = (esp_timer_get_time() - pulse_open_us) / 1000; + if (open_ms >= HANGUP_THRESHOLD_MS) { + int level_now = gpio_get_level(hook_gpio); + if (level_now == 1) { + ESP_LOGI(TAG, "on-hook (prolonged open %"PRId64"ms) detected", open_ms); + last_level = 1; + pulse_count = 0; + in_pulse = false; + s_offhook = false; + audio_stop(); + audio_pa_set(false); + report_offhook(false); + } else { + /* GPIO closed already but ISR missed it */ + last_level = 0; + last_close_us = esp_timer_get_time(); + if ((esp_timer_get_time() - pulse_open_us) / 1000 < HANGUP_THRESHOLD_MS) { + pulse_count++; + } + in_pulse = false; + } + } + } + +poll_sleep: +#endif + vTaskDelay(pdMS_TO_TICKS(10)); } } diff --git a/plip_voice/main/tones.c b/plip_voice/main/tones.c new file mode 100644 index 0000000..19819d3 --- /dev/null +++ b/plip_voice/main/tones.c @@ -0,0 +1,139 @@ +/* + * tones.c — France Télécom telephone tones (dial / busy / ringback). + * + * Generates 440 Hz sinusoid with on/off cadence written directly to the + * I2S speaker handle (stereo — ES8388 requires L+R pairs). + * + * Cadences (all at 440 Hz): + * DIAL : continuous + * BUSY : 0.5 s on / 0.5 s off (1 s cycle) + * RINGBACK : 1.5 s on / 3.5 s off (5 s cycle) + */ + +#include "tones.h" +#include "audio.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "driver/i2s_std.h" +#include "esp_log.h" + +#include +#include +#include + +#define TAG "tones" + +typedef enum { TONE_NONE, TONE_DIAL, TONE_BUSY, TONE_RINGBACK } tone_mode_t; + +static volatile tone_mode_t s_mode = TONE_NONE; +static volatile bool s_tone_idle = true; /* true when tone_task is NOT in i2s_channel_write */ +static TaskHandle_t s_task = NULL; + +#define SR 16000 /* sample rate (must match audio_init) */ +#define FRAME 320 /* 20 ms @ 16 kHz */ + +/* + * Fill `n` stereo frames (L=R = 440 Hz sine) into buf. + * buf must hold n * 2 int16_t (stereo interleaved). + * ph: running phase accumulator (mono sample index). + */ +static void fill_440_stereo(int16_t *buf, int n, int *ph) +{ + for (int i = 0; i < n; i++) { + int16_t v = (int16_t)(8000.0f * sinf(2.0f * (float)M_PI * 440.0f * (*ph) / SR)); + buf[i * 2] = v; /* L */ + buf[i * 2 + 1] = v; /* R */ + (*ph)++; + } +} + +static void tone_task(void *a) +{ + (void)a; + /* Stereo buffer: FRAME mono samples × 2 channels × 2 bytes = FRAME*4 bytes */ + int16_t buf[FRAME * 2]; + int ph = 0; + int64_t t = 0; /* ms elapsed in current tone state */ + + for (;;) { + tone_mode_t m = s_mode; + if (m == TONE_NONE) { + s_tone_idle = true; + vTaskDelay(pdMS_TO_TICKS(20)); + t = 0; + ph = 0; + continue; + } + + bool on = true; + if (m == TONE_BUSY) { + on = (t % 1000) < 500; /* 500ms on / 500ms off */ + } else if (m == TONE_RINGBACK) { + on = (t % 5000) < 1500; /* 1.5s on / 3.5s off */ + } + /* TONE_DIAL: always on */ + + if (on) { + fill_440_stereo(buf, FRAME, &ph); + } else { + memset(buf, 0, sizeof(buf)); + /* Keep phase accumulator advancing so pitch is smooth on resumption */ + ph += FRAME; + } + + size_t written = 0; + s_tone_idle = false; + i2s_channel_write(audio_spk_handle(), buf, sizeof(buf), &written, pdMS_TO_TICKS(50)); + t += 20; /* each frame = 20 ms */ + } +} + +static void ensure_task(void) +{ + if (!s_task) { + xTaskCreate(tone_task, "tones", 3072, NULL, 5, &s_task); + ESP_LOGI(TAG, "tone_task created"); + } +} + +void tones_dialtone_start(void) +{ + ESP_LOGI(TAG, "dial tone start"); + audio_pa_set(true); + ensure_task(); + s_mode = TONE_DIAL; +} + +void tones_busy_start(void) +{ + ESP_LOGI(TAG, "busy tone start"); + audio_pa_set(true); + ensure_task(); + s_mode = TONE_BUSY; +} + +void tones_ringback_start(void) +{ + ESP_LOGI(TAG, "ringback tone start"); + audio_pa_set(true); + ensure_task(); + s_mode = TONE_RINGBACK; +} + +void tones_stop(void) +{ + ESP_LOGI(TAG, "tones stop"); + s_mode = TONE_NONE; + /* Wait until tone_task is no longer inside i2s_channel_write (I2S arbitration). + * Guard: max 200 ms so we never block indefinitely. */ + const int max_wait_ms = 200; + int waited_ms = 0; + while (!s_tone_idle && waited_ms < max_wait_ms) { + vTaskDelay(pdMS_TO_TICKS(5)); + waited_ms += 5; + } + if (waited_ms >= max_wait_ms) { + ESP_LOGW(TAG, "tones_stop: timeout waiting for tone_task idle"); + } +} diff --git a/plip_voice/main/tones.h b/plip_voice/main/tones.h new file mode 100644 index 0000000..5444a97 --- /dev/null +++ b/plip_voice/main/tones.h @@ -0,0 +1,7 @@ +#pragma once +#include "esp_err.h" + +void tones_dialtone_start(void); /* 440 Hz continuous */ +void tones_busy_start(void); /* 440 Hz 0.5s on/off */ +void tones_ringback_start(void); /* 440 Hz 1.5s/3.5s */ +void tones_stop(void);