diff --git a/plip_voice/main/CMakeLists.txt b/plip_voice/main/CMakeLists.txt index b4ad37f..0ba7d89 100644 --- a/plip_voice/main/CMakeLists.txt +++ b/plip_voice/main/CMakeLists.txt @@ -11,9 +11,11 @@ idf_component_register( "dialer.c" "conversation.c" "turn_client.c" + "slic.c" INCLUDE_DIRS "." PRIV_REQUIRES driver + esp_driver_gpio esp_event esp_http_client esp_http_server diff --git a/plip_voice/main/Kconfig.projbuild b/plip_voice/main/Kconfig.projbuild index 618c037..81dff9d 100644 --- a/plip_voice/main/Kconfig.projbuild +++ b/plip_voice/main/Kconfig.projbuild @@ -38,10 +38,19 @@ menu "PLIP Voice Configuration" config PLIP_HOOK_GPIO int "Off-hook GPIO number" - default 4 + default 23 help - GPIO that signals handset off-hook (active LOW via INPUT_PULLUP). - Dev kit uses BOOT button (GPIO4 / KEY1). PCB uses Si3210 INT. + GPIO that signals handset off-hook. + SLIC SHK line is GPIO23 (A1S board KEY4, re-assigned to SLIC). + Legacy dev kit stub used GPIO4 (BOOT button, active-LOW). + + config PLIP_HOOK_ACTIVE_HIGH + bool "Hook GPIO active-HIGH means off-hook" + default y + help + When enabled, a HIGH level on PLIP_HOOK_GPIO means the handset is + off-hook (SLIC SHK polarity). When disabled, LOW means off-hook + (original dev-kit pull-up + BOOT button polarity). config PLIP_DIAL_PULSE bool "Enable rotary dial pulse decoding on SHK GPIO" diff --git a/plip_voice/main/audio.c b/plip_voice/main/audio.c index 3732452..573f934 100644 --- a/plip_voice/main/audio.c +++ b/plip_voice/main/audio.c @@ -428,7 +428,13 @@ esp_err_t audio_init(void) return ret; } - /* 3a. TX (speaker): Philips 16-bit stereo @ 16 kHz. */ + /* 3a+3b. Full-duplex I2S config: SAME gpio_cfg for TX and RX. + * IDF5 i2s_std constitutes full-duplex ONLY when TX and RX configs are + * identical (memcmp). If they differ, on ESP32 HW v1 it tries to move + * RX to I2S_NUM_1, breaking clock routing and yielding permanent zeros. + * Fix: set both dout=PLIP_I2S_DOUT and din=PLIP_I2S_DIN in the same + * config and apply it to both handles. The IDF GPIO driver handles + * direction internally (dout→output, din→input). */ i2s_std_config_t std_cfg = { .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE), .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG( @@ -438,7 +444,7 @@ esp_err_t audio_init(void) .bclk = PLIP_I2S_BCLK, .ws = PLIP_I2S_WS, .dout = PLIP_I2S_DOUT, - .din = I2S_GPIO_UNUSED, + .din = PLIP_I2S_DIN, .invert_flags = { .mclk_inv = false, .bclk_inv = false, @@ -451,29 +457,7 @@ esp_err_t audio_init(void) ESP_LOGE(TAG, "i2s_channel_init_std_mode (TX): %s", esp_err_to_name(ret)); return ret; } - - /* 3b. RX (mic): same GPIO config as TX except DIN=GPIO35, DOUT=UNUSED. - * In full-duplex std mode, IDF requires the same BCLK/WS GPIOs to be - * specified in both TX and RX configs — it deduplicates GPIO matrix - * routing internally. Using UNUSED for BCLK/WS causes zero-data RX. */ - i2s_std_config_t rx_cfg = { - .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE), - .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG( - I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO), - .gpio_cfg = { - .mclk = PLIP_I2S_MCLK, - .bclk = PLIP_I2S_BCLK, - .ws = PLIP_I2S_WS, - .dout = I2S_GPIO_UNUSED, - .din = PLIP_I2S_DIN, - .invert_flags = { - .mclk_inv = false, - .bclk_inv = false, - .ws_inv = false, - }, - }, - }; - ret = i2s_channel_init_std_mode(s_mic_handle, &rx_cfg); + ret = i2s_channel_init_std_mode(s_mic_handle, &std_cfg); if (ret != ESP_OK) { ESP_LOGE(TAG, "i2s_channel_init_std_mode (RX): %s", esp_err_to_name(ret)); return ret; diff --git a/plip_voice/main/board_config.h b/plip_voice/main/board_config.h index 06ffbfc..9090423 100644 --- a/plip_voice/main/board_config.h +++ b/plip_voice/main/board_config.h @@ -40,3 +40,10 @@ /* ---------- Off-hook GPIO (dev kit uses BOOT/KEY1 GPIO4 as stand-in) ---------- */ /* Actual value comes from CONFIG_PLIP_HOOK_GPIO (Kconfig) */ + +/* ---------- SLIC K50835F / AG1171-class front-end (A1S board wiring) ---------- */ +/* KEY3=GPIO19, KEY4=GPIO23, KEY5=GPIO18, KEY6=GPIO5 share these pins — reassigned to SLIC */ +#define PLIP_SLIC_RM 18 /* Ring Mode output — HIGH = ring burst active */ +#define PLIP_SLIC_FR 5 /* Forward/Reverse output — toggled at 25 Hz for bell */ +#define PLIP_SLIC_SHK 23 /* Switch Hook input — HIGH = off-hook (active-high) */ +#define PLIP_SLIC_PD 19 /* Power Down (open-drain) — HIGH = SLIC active */ diff --git a/plip_voice/main/es8388.c b/plip_voice/main/es8388.c index ce846c2..623d8c7 100644 --- a/plip_voice/main/es8388.c +++ b/plip_voice/main/es8388.c @@ -152,10 +152,14 @@ esp_err_t es8388_init(void) * - ADCCONTROL5 (0x0D) = 0x02: ADCFsMode SINGLE SPEED RATIO=256 (16kHz@MCLK 4.096MHz) * - ADCCONTROL8/9 (0x10/0x11) = 0x00: ADC digital volume 0dB */ if (i2c_write_reg(ES8388_ADC_CTL1, 0xBB) != ESP_OK) return ESP_FAIL; /* PGA +24dB L+R */ - if (i2c_write_reg(ES8388_ADC_CTL2, 0x00) != ESP_OK) return ESP_FAIL; /* LIN1/RIN1 differential (onboard mic) */ + /* ADCCONTROL2 (0x0A): LINSEL/RINSEL = 0x00 → LINPUT1/RINPUT1 (LINE IN header on A1S kit) + * 0x00 = LIN1/RIN1 differential (standard LINE IN jack where AG1171 SLIC audio is wired) + * 0x50 = LIN2/RIN2 single-ended (onboard mic path — no SLIC signal here) */ + if (i2c_write_reg(ES8388_ADC_CTL2, 0x00) != ESP_OK) return ESP_FAIL; /* LIN1/RIN1 differential (LINE IN / AG1171 SLIC) */ if (i2c_write_reg(ES8388_ADC_CTL3, 0x02) != ESP_OK) return ESP_FAIL; /* DS filter sel */ if (i2c_write_reg(ES8388_ADC_CTL4, 0x0C) != ESP_OK) return ESP_FAIL; /* I2S 16-bit */ if (i2c_write_reg(ES8388_ADC_CTL5, 0x02) != ESP_OK) return ESP_FAIL; /* RATIO=256 */ + if (i2c_write_reg(0x0E, 0x00) != ESP_OK) return ESP_FAIL; /* ADCCONTROL6: clear ADCSMUTE bit5 (reset default 0x30 = ADC output muted) */ if (i2c_write_reg(ES8388_ADC_CTL8, 0x00) != ESP_OK) return ESP_FAIL; /* ADC vol L 0dB */ if (i2c_write_reg(ES8388_ADC_CTL9, 0x00) != ESP_OK) return ESP_FAIL; /* ADC vol R 0dB */ @@ -173,11 +177,12 @@ esp_err_t es8388_init(void) if (i2c_write_reg(ES8388_DAC_CTL17, 0x90) != ESP_OK) return ESP_FAIL; if (i2c_write_reg(ES8388_DAC_CTL20, 0x90) != ESP_OK) return ESP_FAIL; - /* 11. DACCONTROL21 (0x2B): ADC+DAC LRCK sync = 0xC0 (both ADC and DAC share LRCK). - * Espressif reference: 0xC0 when both ADC and DAC active (record+playback mode). - * 0x80 when DAC only. - * This register is a clock routing register, NOT a volume register. */ - if (i2c_write_reg(ES8388_DAC_CTL21, 0xC0) != ESP_OK) return ESP_FAIL; + /* 11. DACCONTROL21 (0x2B): ADC+DAC LRCK sync. + * Espressif reference es8388_start(): + * 0xC0 = LINE mode (analog bypass, not ADC digital path) + * 0x80 = DAC+ADC digital record+playback mode (bit7 only) + * We want digital ADC recording + DAC playback → use 0x80. */ + if (i2c_write_reg(ES8388_DAC_CTL21, 0x80) != ESP_OK) return ESP_FAIL; /* 11b. Restart internal state machine (required after DACCONTROL21 change). * Without this pulse, the ADC clock domain may not synchronise properly. @@ -198,9 +203,12 @@ esp_err_t es8388_init(void) /* 13. DAC power: power up DAC L+R. */ if (i2c_write_reg(ES8388_DAC_POWER, 0x3C) != ESP_OK) return ESP_FAIL; - /* 14. ADC power: power up ADC (0x09 per Espressif reference). - * Must come AFTER DACCONTROL21 state machine restart and all ADC register writes. */ - if (i2c_write_reg(ES8388_ADC_POWER, 0x09) != ESP_OK) return ESP_FAIL; + /* 14. ADC power: full power-up (all Pdn bits cleared = 0x00). + * 0x09 is the intermediate state (Espressif es8388_open end-state), but + * the full ADC + analog input power-up requires 0x00 (Espressif es8388_start(ADC)). + * Must come AFTER DACCONTROL21 state machine restart and ADCCONTROL6 unmute. */ + if (i2c_write_reg(ES8388_ADC_POWER, 0x00) != ESP_OK) return ESP_FAIL; + vTaskDelay(pdMS_TO_TICKS(10)); /* let ADC analog settle after full power-up */ /* 15. Enable PA (power amplifier for speaker). */ gpio_set_direction(PLIP_PA_ENABLE, GPIO_MODE_OUTPUT); @@ -216,7 +224,7 @@ esp_err_t es8388_init(void) es8388_read_reg(ES8388_CHIP_POWER, &chippower); ESP_LOGI(TAG, "ES8388 regs: CTL1=0x%02X ADCPWR=0x%02X ADCINSEL=0x%02X ADCCTL3=0x%02X DACCTL21=0x%02X CHIPPOWER=0x%02X", ctl1, adcpwr, adcinsel, adcctl3, dacctl21, chippower); - ESP_LOGI(TAG, "ES8388 init OK — PA enabled, DAC @ 0dB, ADC PGA +24dB, input=LIN1, DACCTL21=0xC0"); + ESP_LOGI(TAG, "ES8388 init OK — PA enabled, DAC @ 0dB, ADC PGA +24dB, input=LIN1/RIN1 (LINE IN), DACCTL21=0x80"); return ESP_OK; } diff --git a/plip_voice/main/main.c b/plip_voice/main/main.c index 9b38563..740ae26 100644 --- a/plip_voice/main/main.c +++ b/plip_voice/main/main.c @@ -24,11 +24,13 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" +#include "board_config.h" #include "audio.h" #include "net.h" #include "cmd_exec.h" #include "hook_client.h" #include "phone.h" +#include "slic.h" #include "scenario_mesh.h" #include "dialer.h" #include "conversation.h" @@ -60,8 +62,18 @@ static void boot_task(void *arg) { (void)arg; + /* ── 1b. SLIC power-up (before audio — PD must be released early) ── */ + esp_err_t ret = slic_init(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "slic_init FAILED: %s — SLIC remains in power-down (no audio path, no hook sense)", + esp_err_to_name(ret)); + } else { + ESP_LOGI(TAG, "SLIC powered up: PD=GPIO%d OD-HIGH, SHK=GPIO%d active-HIGH, RM=GPIO%d, FR=GPIO%d", + PLIP_SLIC_PD, PLIP_SLIC_SHK, PLIP_SLIC_RM, PLIP_SLIC_FR); + } + /* ── 2. Audio init (Phase A) ── */ - esp_err_t ret = audio_init(); + ret = audio_init(); if (ret != ESP_OK) { ESP_LOGE(TAG, "audio_init FAILED: %s", esp_err_to_name(ret)); ESP_LOGE(TAG, "PHASE A BLOCKED — check ES8388 I2C wiring (SDA=33, SCL=32)"); diff --git a/plip_voice/main/net.c b/plip_voice/main/net.c index 1f1a20c..3cdc570 100644 --- a/plip_voice/main/net.c +++ b/plip_voice/main/net.c @@ -26,7 +26,11 @@ #include "audio.h" #include "cmd_exec.h" +#include "phone.h" #include "es8388.h" +#include "slic.h" +#include "board_config.h" +#include "driver/gpio.h" #include "esp_err.h" #include "esp_event.h" #include "esp_http_server.h" @@ -429,6 +433,40 @@ static esp_err_t handle_cmd_post(httpd_req_t *req) return send_json(req, "200 OK", "{\"ok\":true}"); } +/* ── GET /debug/ring (trigger ring tone over HTTP) ──────────────────────── */ + +static esp_err_t handle_debug_ring(httpd_req_t *req) +{ + phone_ring_start(); + ESP_LOGI(TAG, "debug/ring: ring started"); + return send_json(req, "200 OK", "{\"ok\":true,\"ringing\":true}"); +} + +/* ── GET /debug/ringstop (stop ring tone over HTTP) ─────────────────────── */ + +static esp_err_t handle_debug_ringstop(httpd_req_t *req) +{ + phone_ring_stop(); + ESP_LOGI(TAG, "debug/ringstop: ring stopped"); + return send_json(req, "200 OK", "{\"ok\":true,\"ringing\":false}"); +} + +/* ── GET /debug/slic (read GPIO levels of all 4 SLIC pins) ──────────────── */ + +static esp_err_t handle_debug_slic(httpd_req_t *req) +{ + char buf[192]; + /* gpio_get_level reads the actual pad level even for output pins. */ + snprintf(buf, sizeof(buf), + "{\"rm\":%d,\"fr\":%d,\"shk\":%d,\"pd\":%d,\"offhook\":%s,\"ringing\":%s}", + gpio_get_level(PLIP_SLIC_RM), gpio_get_level(PLIP_SLIC_FR), + gpio_get_level(PLIP_SLIC_SHK), gpio_get_level(PLIP_SLIC_PD), + slic_is_offhook() ? "true" : "false", + slic_is_ringing() ? "true" : "false"); + ESP_LOGI(TAG, "debug/slic: %s", buf); + return send_json(req, "200 OK", buf); +} + /* ── GET /debug/dial?number=NNNN (push digits into the dialer) ───────────── */ static esp_err_t handle_debug_dial(httpd_req_t *req) @@ -527,7 +565,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 = 14; + hcfg.max_uri_handlers = 17; hcfg.stack_size = 8192; esp_err_t ret = httpd_start(&s_httpd, &hcfg); @@ -564,6 +602,18 @@ esp_err_t net_init(void) .uri = "/debug/dial", .method = HTTP_GET, .handler = handle_debug_dial, .user_ctx = NULL, }; + static const httpd_uri_t uri_debug_ring = { + .uri = "/debug/ring", .method = HTTP_GET, + .handler = handle_debug_ring, .user_ctx = NULL, + }; + static const httpd_uri_t uri_debug_ringstop = { + .uri = "/debug/ringstop", .method = HTTP_GET, + .handler = handle_debug_ringstop, .user_ctx = NULL, + }; + static const httpd_uri_t uri_debug_slic = { + .uri = "/debug/slic", .method = HTTP_GET, + .handler = handle_debug_slic, .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); @@ -571,7 +621,10 @@ esp_err_t net_init(void) 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); + httpd_register_uri_handler(s_httpd, &uri_debug_ring); + httpd_register_uri_handler(s_httpd, &uri_debug_ringstop); + httpd_register_uri_handler(s_httpd, &uri_debug_slic); - 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)"); + ESP_LOGI(TAG, "httpd up on :80 (GET /status, GET /debug/regs, GET /debug/dial, GET /debug/ring, GET /debug/ringstop, GET /debug/slic, 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 984e59c..eee7653 100644 --- a/plip_voice/main/phone.c +++ b/plip_voice/main/phone.c @@ -19,6 +19,7 @@ #include "audio.h" #include "hook_client.h" #include "conversation.h" +#include "slic.h" #if CONFIG_PLIP_DIAL_PULSE #include "dialer.h" @@ -52,7 +53,10 @@ void phone_ring_start(void) { if (s_ringing) return; s_ringing = true; - ESP_LOGI(TAG, "ring start"); + 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(); } @@ -60,7 +64,8 @@ void phone_ring_stop(void) { if (!s_ringing) return; s_ringing = false; - ESP_LOGI(TAG, "ring stop"); + ESP_LOGI(TAG, "ring stop: SLIC RM/FR off + audio stop"); + slic_ring_stop(); audio_stop(); } @@ -70,6 +75,26 @@ static void report_offhook(bool 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; @@ -88,9 +113,11 @@ 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); - s_offhook = (last_level == 0); - ESP_LOGI(TAG, "phone task ready, hook GPIO=%d level=%d (%s)", - hook_gpio, last_level, s_offhook ? "off-hook" : "on-hook"); + 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)"); @@ -104,11 +131,12 @@ static void phone_task(void *arg) conversation_on_hook_change(s_offhook); #if CONFIG_PLIP_DIAL_PULSE - /* Rotary pulse state */ + /* Rotary pulse state. + * With SLIC SHK (active-HIGH): off-hook = HIGH, pulse = brief LOW dip. */ 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) */ + 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 (;;) { @@ -119,19 +147,19 @@ static void phone_task(void *arg) /* 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. */ + /* 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 == 1 && last_level == 0) { - /* Rising edge: GPIO went open */ - last_level = 1; + 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 == 0 && last_level == 1) { - /* Falling edge: GPIO closed again */ - last_level = 0; + } 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; @@ -155,12 +183,12 @@ static void phone_task(void *arg) goto poll_sleep; } #endif - /* Standard 30 ms debounce for on-hook state transitions */ + /* 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 == 0) { + if (level == HOOK_OFFHOOK_LEVEL) { /* Off-hook: handset picked up. */ s_offhook = true; #if CONFIG_PLIP_DIAL_PULSE @@ -168,7 +196,8 @@ static void phone_task(void *arg) in_pulse = false; last_close_us = esp_timer_get_time(); #endif - ESP_LOGI(TAG, "off-hook (pickup) detected — PA on"); + 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); @@ -185,7 +214,7 @@ static void phone_task(void *arg) #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. */ + * 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) { @@ -201,14 +230,15 @@ static void phone_task(void *arg) } } - /* Also detect prolonged open (hangup) even if no more edges arrive */ + /* 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 == 1) { + if (level_now == HOOK_PULSE_OPEN) { ESP_LOGI(TAG, "on-hook (prolonged open %"PRId64"ms) detected", open_ms); - last_level = 1; + last_level = HOOK_PULSE_OPEN; pulse_count = 0; in_pulse = false; s_offhook = false; @@ -216,8 +246,8 @@ static void phone_task(void *arg) audio_pa_set(false); report_offhook(false); } else { - /* GPIO closed already but ISR missed it */ - last_level = 0; + /* 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++; diff --git a/plip_voice/main/slic.c b/plip_voice/main/slic.c new file mode 100644 index 0000000..7d80dc6 --- /dev/null +++ b/plip_voice/main/slic.c @@ -0,0 +1,150 @@ +/* + * slic.c — K50835F / AG1171-class SLIC control (ESP-IDF port). + * + * Ported from hardware/projects/slic-phone/src/slic/Ks0835SlicController.cpp + * (Arduino) to bare ESP-IDF 5.x GPIO driver. + * + * Key differences from Arduino original: + * - Open-drain on PD achieved via GPIO_MODE_OUTPUT_OD + gpio_set_level(PD, 1) + * - Ring FR toggle runs as a FreeRTOS task instead of a cooperative tick() + * - hook_active_high is hard-coded TRUE (matches A252ConfigStore default) + */ + +#include "slic.h" +#include "board_config.h" + +#include "driver/gpio.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#define TAG "slic" + +/* Active-high hook polarity: HIGH on SHK = off-hook. + * Source: hardware/projects/slic-phone/src/config/A252ConfigStore.cpp:262 + * cfg.hook_active_high = true; */ +#define SLIC_SHK_OFFHOOK_LEVEL 1 + +static volatile bool s_ringing = false; +static TaskHandle_t s_ring_task = NULL; + +/* ── FR toggle task (ring drive at ~25 Hz) ────────────────────────────────── */ + +static void slic_ring_task(void *arg) +{ + (void)arg; + bool fr_state = false; + + for (;;) { + if (s_ringing) { + fr_state = !fr_state; + gpio_set_level(PLIP_SLIC_FR, fr_state ? 1 : 0); + ESP_LOGV(TAG, "FR=%d", fr_state ? 1 : 0); + } else { + /* Suspended — keep FR low while idle */ + gpio_set_level(PLIP_SLIC_FR, 0); + fr_state = false; + } + vTaskDelay(pdMS_TO_TICKS(20)); /* 20 ms → ~25 Hz */ + } +} + +/* ── Public API ──────────────────────────────────────────────────────────── */ + +esp_err_t slic_init(void) +{ + /* RM: Ring Mode output, init LOW */ + gpio_config_t rm_cfg = { + .pin_bit_mask = (1ULL << PLIP_SLIC_RM), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + esp_err_t ret = gpio_config(&rm_cfg); + if (ret != ESP_OK) return ret; + gpio_set_level(PLIP_SLIC_RM, 0); + + /* FR: Forward/Reverse output, init LOW */ + gpio_config_t fr_cfg = { + .pin_bit_mask = (1ULL << PLIP_SLIC_FR), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + ret = gpio_config(&fr_cfg); + if (ret != ESP_OK) return ret; + gpio_set_level(PLIP_SLIC_FR, 0); + + /* SHK: Switch Hook input with pull-up (physical line has no external pull) */ + gpio_config_t shk_cfg = { + .pin_bit_mask = (1ULL << PLIP_SLIC_SHK), + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_ENABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + ret = gpio_config(&shk_cfg); + if (ret != ESP_OK) return ret; + + /* PD: Power Down — push-pull output HIGH = SLIC active (definite power-up). + * Previous config (open-drain HIGH) left the line floating when no pull-up + * is present on the bench, keeping the SLIC in power-down. + * Push-pull HIGH drives the PD pin firmly high regardless of external resistors. */ + gpio_config_t pd_cfg = { + .pin_bit_mask = (1ULL << PLIP_SLIC_PD), + .mode = GPIO_MODE_OUTPUT, /* push-pull (pas OD) */ + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + ret = gpio_config(&pd_cfg); + if (ret != ESP_OK) return ret; + ret = gpio_set_level(PLIP_SLIC_PD, 1); /* HIGH franc = SLIC actif */ + if (ret != ESP_OK) return ret; + + ESP_LOGI(TAG, "slic: PD GPIO%d push-pull HIGH (SLIC power-up, franc)", PLIP_SLIC_PD); + ESP_LOGI(TAG, "slic: pins RM=%d FR=%d SHK=%d PD=%d hook_active_high=1", + PLIP_SLIC_RM, PLIP_SLIC_FR, PLIP_SLIC_SHK, PLIP_SLIC_PD); + + /* Spawn the FR-toggle task (runs indefinitely, toggles only when s_ringing=true) */ + BaseType_t ok = xTaskCreatePinnedToCore(slic_ring_task, "slic_ring", + 2048, NULL, 4, &s_ring_task, 1); + if (ok != pdPASS) { + ESP_LOGE(TAG, "Failed to create slic_ring task"); + return ESP_ERR_NO_MEM; + } + + int shk = gpio_get_level(PLIP_SLIC_SHK); + ESP_LOGI(TAG, "SLIC init OK — SHK level=%d (%s)", + shk, (shk == SLIC_SHK_OFFHOOK_LEVEL) ? "off-hook" : "on-hook"); + return ESP_OK; +} + +bool slic_is_offhook(void) +{ + return gpio_get_level(PLIP_SLIC_SHK) == SLIC_SHK_OFFHOOK_LEVEL; +} + +void slic_ring_start(void) +{ + if (s_ringing) return; + ESP_LOGI(TAG, "ring start: RM=HIGH, FR toggling at 25 Hz"); + gpio_set_level(PLIP_SLIC_RM, 1); + s_ringing = true; +} + +void slic_ring_stop(void) +{ + if (!s_ringing) return; + ESP_LOGI(TAG, "ring stop: RM=LOW, FR=LOW"); + s_ringing = false; + gpio_set_level(PLIP_SLIC_RM, 0); + gpio_set_level(PLIP_SLIC_FR, 0); +} + +bool slic_is_ringing(void) +{ + return s_ringing; +} diff --git a/plip_voice/main/slic.h b/plip_voice/main/slic.h new file mode 100644 index 0000000..8f39928 --- /dev/null +++ b/plip_voice/main/slic.h @@ -0,0 +1,59 @@ +/* + * slic.h — K50835F / AG1171-class SLIC control (ESP-IDF port of Ks0835SlicController). + * + * Pins (A1S board, from board_config.h): + * RM GPIO18 Ring Mode output — HIGH = ring burst active + * FR GPIO5 Forward/Reverse out — toggled at ~25 Hz during ring to drive bell + * SHK GPIO23 Switch Hook input — HIGH = off-hook (active-high) + * PD GPIO19 Power Down (OD) — HIGH = SLIC powered (released open-drain) + * + * Power-up: PD is open-drain, written HIGH → high-impedance → SLIC active. + * Power-down: PD driven LOW → SLIC off (no audio, no hook sense). + * + * Ring: slic_ring_start() sets RM=1 and spawns/enables a 20 ms FR-toggle task. + * slic_ring_stop() clears RM=0, FR=0, pauses the toggle task. + * + * Hook: slic_is_offhook() reads SHK; HIGH = off-hook (matches A252ConfigStore default). + */ + +#pragma once + +#include +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Initialise SLIC GPIO and power up the SLIC. + * Must be called early in boot, before audio init. + * Returns ESP_OK on success. + */ +esp_err_t slic_init(void); + +/** + * Returns true when the handset is off-hook (SHK GPIO23 HIGH). + */ +bool slic_is_offhook(void); + +/** + * Activate ringing: RM=HIGH + FR toggles at ~25 Hz (20 ms period). + * No-op if already ringing. + */ +void slic_ring_start(void); + +/** + * Stop ringing: RM=LOW, FR=LOW, FR toggle suspended. + * No-op if not ringing. + */ +void slic_ring_stop(void); + +/** + * Returns true if slic_ring_start() was called and not yet stopped. + */ +bool slic_is_ringing(void); + +#ifdef __cplusplus +} +#endif diff --git a/plip_voice/sdkconfig.defaults b/plip_voice/sdkconfig.defaults index 20540ad..b6f354d 100644 --- a/plip_voice/sdkconfig.defaults +++ b/plip_voice/sdkconfig.defaults @@ -35,6 +35,10 @@ CONFIG_ESP_TASK_WDT_TIMEOUT_S=15 # when I2S/WiFi interrupts spill into idle stack on original ESP32. CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=2048 +# SLIC hook GPIO — SHK on GPIO23 (A1S KEY4, active-HIGH: HIGH = off-hook) +CONFIG_PLIP_HOOK_GPIO=23 +CONFIG_PLIP_HOOK_ACTIVE_HIGH=y + # WiFi credentials — set via `idf.py menuconfig` (PLIP Voice Configuration) # or override locally in sdkconfig (NOT committed). # CONFIG_PLIP_WIFI_SSID="..."