f531bf3e55
- 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.
278 lines
10 KiB
C
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;
|
|
}
|