Merge pull request 'feat(plip): téléphone France Télécom — stage 1 (tonalités + dialer + machine à états)' (#20) from feat/plip-phone-stage1 into main
CI / platformio (push) Failing after 4m23s
CI / platformio (push) Failing after 4m23s
This commit was merged in pull request #20.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
#include <stdbool.h>
|
||||
|
||||
void conversation_init(void);
|
||||
void conversation_on_hook_change(bool offhook);
|
||||
@@ -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 <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
#include <stdbool.h>
|
||||
|
||||
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);
|
||||
@@ -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) {
|
||||
|
||||
+34
-2
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
#include "net.h"
|
||||
#include "dialer.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdlib.h>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+141
-19
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <math.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user