Files
ESP32_ZACUS/plip_voice/main/phone.c
T
clemsail f531bf3e55 feat(plip): SLIC control — power-up (PD OD-HIGH), hook on SHK GPIO23, ring RM/FR
- board_config.h: add PLIP_SLIC_RM=18, FR=5, SHK=23, PD=19 (A1S KEY3-6 repurposed)
- slic.c/slic.h: new ESP-IDF module porting Ks0835SlicController:
    * slic_init(): RM/FR output LOW, SHK input+pullup, PD open-drain HIGH (power-up)
    * slic_is_offhook(): reads SHK GPIO23, HIGH = off-hook (active-high, matches A252ConfigStore default)
    * slic_ring_start/stop(): RM HIGH + FreeRTOS task toggles FR at 25 Hz (20 ms period)
- CMakeLists.txt: add slic.c to SRCS, esp_driver_gpio to PRIV_REQUIRES
- Kconfig: PLIP_HOOK_GPIO default 4→23, add PLIP_HOOK_ACTIVE_HIGH (default y)
- phone.c: hook reads SHK GPIO23 via HOOK_OFFHOOK_LEVEL/HOOK_PULSE_OPEN macros (active-HIGH);
    phone_ring_start/stop() now drives slic_ring_start/stop() for physical bell + audio tone
- main.c: slic_init() called early in boot_task before audio_init

Root cause fixed: SLIC was never powered (PD never released from reset state).
Hook was read on wrong GPIO (4) with wrong polarity. Ring drove only audio, not bell.
2026-06-15 00:26:35 +02:00

278 lines
10 KiB
C

/*
* phone.c — Off-hook GPIO debounce + rotary pulse decoding + ring control.
*
* 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"
#include "slic.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 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;
static volatile bool s_offhook = false; /* true = handset picked up */
/* IRAM_ATTR: ISR must live in IRAM on original ESP32. */
static void IRAM_ATTR on_hook_isr(void *arg)
{
(void)arg;
s_edge_pending = true;
}
void phone_ring_start(void)
{
if (s_ringing) return;
s_ringing = true;
ESP_LOGI(TAG, "ring start: SLIC RM/FR + audio tone");
/* Physical bell via SLIC RM/FR (main ringer) */
slic_ring_start();
/* Optional in-earpiece audio tone (audible feedback when handset is up) */
audio_ring_start();
}
void phone_ring_stop(void)
{
if (!s_ringing) return;
s_ringing = false;
ESP_LOGI(TAG, "ring stop: SLIC RM/FR off + audio stop");
slic_ring_stop();
audio_stop();
}
static void report_offhook(bool offhook)
{
conversation_on_hook_change(offhook);
hook_client_report(offhook ? "off" : "on", offhook ? "pickup" : "hangup");
}
/*
* Hook polarity:
* CONFIG_PLIP_HOOK_ACTIVE_HIGH=y (default, SLIC SHK GPIO23): HIGH = off-hook
* CONFIG_PLIP_HOOK_ACTIVE_HIGH=n (legacy BOOT button GPIO4): LOW = off-hook
*
* Rotary pulse notes (SLIC SHK, active-HIGH):
* Off-hook base: SHK HIGH. A rotary pulse briefly opens the loop → SHK drops LOW
* for ~60-100 ms, then returns HIGH. Hangup: SHK stays LOW for >500 ms.
* So "open" (pulse event) = level drops LOW when active-HIGH polarity.
*/
#if CONFIG_PLIP_HOOK_ACTIVE_HIGH
#define HOOK_OFFHOOK_LEVEL 1 /* HIGH = off-hook */
#define HOOK_PULSE_OPEN 0 /* LOW = rotary pulse open (loop broken) */
#define HOOK_PULSE_CLOSED 1 /* HIGH = rotary pulse closed (loop restored) */
#else
#define HOOK_OFFHOOK_LEVEL 0 /* LOW = off-hook (legacy pull-up + BOOT button) */
#define HOOK_PULSE_OPEN 1 /* HIGH = rotary pulse open */
#define HOOK_PULSE_CLOSED 0 /* LOW = rotary pulse closed */
#endif
static void phone_task(void *arg)
{
(void)arg;
const int hook_gpio = CONFIG_PLIP_HOOK_GPIO;
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << hook_gpio),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_ANYEDGE,
};
ESP_ERROR_CHECK(gpio_config(&io_conf));
ESP_ERROR_CHECK(gpio_install_isr_service(0));
ESP_ERROR_CHECK(gpio_isr_handler_add(hook_gpio, on_hook_isr, NULL));
/* Read and report initial level so master state machine is in sync. */
int last_level = gpio_get_level(hook_gpio);
s_offhook = (last_level == HOOK_OFFHOOK_LEVEL);
ESP_LOGI(TAG, "phone task ready, hook GPIO=%d level=%d active_%s (%s)",
hook_gpio, last_level,
(HOOK_OFFHOOK_LEVEL == 1) ? "high" : "low",
s_offhook ? "off-hook" : "on-hook");
if (!s_offhook) {
ESP_LOGI(TAG, "boot: on-hook -> PA off (mute)");
audio_pa_set(false);
} else {
ESP_LOGI(TAG, "boot: off-hook -> PA on (unmute)");
audio_pa_set(true);
}
hook_client_report(s_offhook ? "off" : "on", "boot");
conversation_on_hook_change(s_offhook);
#if CONFIG_PLIP_DIAL_PULSE
/* Rotary pulse state.
* With SLIC SHK (active-HIGH): off-hook = HIGH, pulse = brief LOW dip. */
int pulse_count = 0;
bool in_pulse = false; /* true while SHK is in pulse-open state */
int64_t pulse_open_us = 0; /* timestamp when pulse open started */
int64_t last_close_us = 0; /* timestamp when pulse closed (SHK back to OFFHOOK) */
#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 the edge. */
vTaskDelay(pdMS_TO_TICKS(5)); /* minimal settle */
int level = gpio_get_level(hook_gpio);
if (level == HOOK_PULSE_OPEN && last_level == HOOK_PULSE_CLOSED) {
/* Pulse open: loop broken (SHK dropped from off-hook level) */
last_level = HOOK_PULSE_OPEN;
in_pulse = true;
pulse_open_us = esp_timer_get_time();
ESP_LOGD(TAG, "pulse: open");
} else if (level == HOOK_PULSE_CLOSED && last_level == HOOK_PULSE_OPEN) {
/* Pulse closed: loop restored */
last_level = HOOK_PULSE_CLOSED;
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/off-hook transitions */
vTaskDelay(pdMS_TO_TICKS(DEBOUNCE_MS));
int level = gpio_get_level(hook_gpio);
if (level != last_level) {
last_level = level;
if (level == HOOK_OFFHOOK_LEVEL) {
/* 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, SLIC offhook=%d",
slic_is_offhook());
audio_pa_set(true);
phone_ring_stop();
report_offhook(true);
} else {
/* 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);
report_offhook(false);
}
}
}
#if CONFIG_PLIP_DIAL_PULSE
/* Check for inter-digit gap: if we accumulated pulses and the line
* has been at closed (off-hook) state 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.
* Active-HIGH: hangup = level stays at HOOK_PULSE_OPEN (LOW) > 500 ms. */
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 == HOOK_PULSE_OPEN) {
ESP_LOGI(TAG, "on-hook (prolonged open %"PRId64"ms) detected", open_ms);
last_level = HOOK_PULSE_OPEN;
pulse_count = 0;
in_pulse = false;
s_offhook = false;
audio_stop();
audio_pa_set(false);
report_offhook(false);
} else {
/* GPIO returned to off-hook level but ISR missed it */
last_level = HOOK_PULSE_CLOSED;
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));
}
}
esp_err_t phone_init(void)
{
BaseType_t ok = xTaskCreatePinnedToCore(phone_task, "phone",
TASK_STACK, NULL,
TASK_PRIO, NULL, 1);
return (ok == pdPASS) ? ESP_OK : ESP_ERR_NO_MEM;
}
bool phone_is_offhook(void)
{
return s_offhook;
}