Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cfe429d885 | |||
| 82759ee536 | |||
| 8c076d81d6 | |||
| 99d79efcdd | |||
| 8dffe14972 | |||
| d71453d32e | |||
| ccc84da785 | |||
| 1561e613e1 | |||
| 4a6fc435a7 | |||
| ebcfb011d3 | |||
| 3c0eb75465 |
@@ -31,7 +31,7 @@ menu "PLIP Voice Configuration"
|
||||
|
||||
config PLIP_SPEAKER_VOLUME
|
||||
int "Default Speaker Volume (0-100)"
|
||||
default 80
|
||||
default 98
|
||||
range 0 100
|
||||
help
|
||||
Default speaker output volume at boot.
|
||||
|
||||
+99
-38
@@ -18,6 +18,7 @@
|
||||
#include "board_config.h"
|
||||
#include "es8388.h"
|
||||
#include "phone.h"
|
||||
#include "slic.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <math.h>
|
||||
@@ -376,8 +377,13 @@ static void audio_worker_task(void *arg)
|
||||
xTaskCreate(ring_task, "ring", 4096, NULL, 4, NULL);
|
||||
break;
|
||||
case CMD_PLAY: {
|
||||
/* Gate playback on hook state: no sound if handset is on-hook. */
|
||||
if (!phone_is_offhook()) {
|
||||
/* Gate playback on the REAL hook line (raw SLIC SHK), not phone.c's
|
||||
* debounced flag. The debounced flag lags or misses the pickup while
|
||||
* the bell is ringing, so an incoming-call greeting enqueued right
|
||||
* after pickup would be dropped here ("on-hook") even though the
|
||||
* handset is physically up. The raw SHK reads the pickup cleanly, and
|
||||
* is the single source of truth for hook state. */
|
||||
if (!slic_is_offhook()) {
|
||||
ESP_LOGI(TAG, "play ignored: on-hook (handset down)");
|
||||
break;
|
||||
}
|
||||
@@ -593,63 +599,118 @@ int audio_capture_wav(uint8_t *out, size_t out_max, int max_ms, int silence_ms)
|
||||
const int max_frames = (max_ms / 20);
|
||||
const int silence_frames = (silence_ms / 20);
|
||||
|
||||
/* VAD thresholds (raw S16: ~1% FS = ~328 for speech onset, ~0.3% for end) */
|
||||
const int32_t vad_onset_rms_sq = 328 * 328; /* ~1% FS² */
|
||||
const int32_t vad_silence_rms_sq = 100 * 100; /* ~0.3% FS² */
|
||||
/* VAD thresholds (raw S16). Tuned for the SLIC handset mic at +24 dB PGA:
|
||||
* speech peaks ~5 % FS / RMS ~1-2 %, noise floor ~0.6 %. Onset must sit
|
||||
* between (catch quiet speech), silence ABOVE the noise floor (so the turn
|
||||
* ends when the caller stops instead of running to max_ms). */
|
||||
/* Measured on the SLIC handset (DC-blocked): speech body sits ~1 % FS RMS
|
||||
* with peaks ~18 %, noise floor ~0.3 %. The previous silence threshold
|
||||
* (1.2 %) was ABOVE the speech body, so the recorder classified the
|
||||
* caller's own voice as silence and cut the turn after ~0.6 s → empty STT.
|
||||
* Onset must clear noise (3-frame sustained confirm also guards it); silence
|
||||
* must sit between the noise floor and the speech body so the turn ends only
|
||||
* on a real pause. */
|
||||
const int32_t vad_onset_rms_sq = 450 * 450; /* ~1.4% FS² — speech onset above noise */
|
||||
const int32_t vad_silence_rms_sq = 200 * 200; /* ~0.6% FS² — below speech body, above noise floor */
|
||||
|
||||
bool voice_started = false;
|
||||
int silent_frames = 0;
|
||||
int total_frames = 0;
|
||||
|
||||
/* One-pole DC blocker (high-pass ~80 Hz) applied to the captured mono.
|
||||
* The SLIC/handset path carries a huge DC/sub-audio offset (~80 % FS,
|
||||
* measured). Left in, it (a) swamps the faint voice and (b) keeps the VAD
|
||||
* RMS permanently above the silence threshold, so the capture never ends
|
||||
* and always runs the full max_ms — and the gateway then sees only a DC
|
||||
* transient, transcribing empty. Removing it lets the VAD track the real
|
||||
* speech envelope (onset/silence work) and hands the gateway a clean,
|
||||
* voice-dominated signal. Telephone band (300-3400 Hz) is untouched.
|
||||
* y[n] = x[n] - x[n-1] + R*y[n-1]; R=0.97 → cutoff ~80 Hz at 16 kHz. */
|
||||
float dc_x1 = 0.0f, dc_y1 = 0.0f;
|
||||
const float dc_R = 0.97f;
|
||||
|
||||
ESP_LOGI(TAG, "capture: RX enabled, max_ms=%d silence_ms=%d", max_ms, silence_ms);
|
||||
|
||||
for (int f = 0; f < max_frames && pcm_written + frame_out_bytes <= pcm_max; f++) {
|
||||
/* ── Phase A: wait for SUSTAINED voice before committing to a capture ─────
|
||||
* Only start recording when the caller actually speaks. We require the
|
||||
* DC-blocked RMS to stay above the onset threshold for ONSET_CONFIRM
|
||||
* consecutive frames (~60 ms) — a single loud frame is rejected as a
|
||||
* transient (e.g. the click when the PA mutes just before capture). Audio
|
||||
* is discarded during this phase. If no sustained voice arrives within
|
||||
* max_ms we return an empty WAV so the caller posts nothing (the NPC must
|
||||
* not "reply" to silence). The listen loop simply calls us again. */
|
||||
const int ONSET_CONFIRM = 3;
|
||||
int onset_run = 0;
|
||||
bool got_onset = false;
|
||||
for (int f = 0; f < max_frames; f++) {
|
||||
size_t bytes_read = 0;
|
||||
ret = i2s_channel_read(s_mic_handle, rx_buf, frame_in_bytes,
|
||||
&bytes_read, pdMS_TO_TICKS(100));
|
||||
if (ret != ESP_OK || bytes_read == 0) {
|
||||
ESP_LOGW(TAG, "capture: read error f=%d: %s", f, esp_err_to_name(ret));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Downmix stereo → mono, compute RMS². */
|
||||
int16_t *out_frame = (int16_t *)(pcm_out + pcm_written);
|
||||
int64_t rms_sq = 0;
|
||||
int n_stereo = (int)(bytes_read / (2 * sizeof(int16_t)));
|
||||
if (ret != ESP_OK || bytes_read == 0) continue;
|
||||
int64_t rms_sq = 0;
|
||||
int n_stereo = (int)(bytes_read / (2 * sizeof(int16_t)));
|
||||
for (int i = 0; i < n_stereo; i++) {
|
||||
int32_t l = rx_buf[i * 2];
|
||||
int32_t r = rx_buf[i * 2 + 1];
|
||||
int16_t mono = (int16_t)((l + r) / 2);
|
||||
out_frame[i] = mono;
|
||||
rms_sq += (int64_t)mono * mono;
|
||||
float xf = (float)((rx_buf[i * 2] + rx_buf[i * 2 + 1]) / 2);
|
||||
float yf = xf - dc_x1 + dc_R * dc_y1; /* DC-blocking high-pass */
|
||||
dc_x1 = xf; dc_y1 = yf;
|
||||
int32_t s = (int32_t)yf;
|
||||
rms_sq += (int64_t)s * s;
|
||||
}
|
||||
rms_sq /= (n_stereo > 0 ? n_stereo : 1);
|
||||
|
||||
/* VAD logic. */
|
||||
if (!voice_started) {
|
||||
if (rms_sq >= vad_onset_rms_sq) {
|
||||
voice_started = true;
|
||||
silent_frames = 0;
|
||||
ESP_LOGI(TAG, "capture: voice onset at frame %d (rms²=%"PRId64")", f, rms_sq);
|
||||
if (rms_sq >= vad_onset_rms_sq) {
|
||||
if (++onset_run >= ONSET_CONFIRM) {
|
||||
got_onset = true;
|
||||
ESP_LOGI(TAG, "capture: sustained voice onset at frame %d (rms²=%"PRId64")",
|
||||
f, rms_sq);
|
||||
break;
|
||||
}
|
||||
/* Before voice onset: still accumulate (to avoid clipping onset).
|
||||
* But don't count towards silence timeout yet. */
|
||||
} else {
|
||||
onset_run = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (got_onset) {
|
||||
/* ── Phase B: record the utterance until silence_ms of silence ──────── */
|
||||
for (int f = 0; f < max_frames && pcm_written + frame_out_bytes <= pcm_max; f++) {
|
||||
size_t bytes_read = 0;
|
||||
ret = i2s_channel_read(s_mic_handle, rx_buf, frame_in_bytes,
|
||||
&bytes_read, pdMS_TO_TICKS(100));
|
||||
if (ret != ESP_OK || bytes_read == 0) {
|
||||
ESP_LOGW(TAG, "capture: read error f=%d: %s", f, esp_err_to_name(ret));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Downmix stereo → mono (DC-blocked), compute RMS². */
|
||||
int16_t *out_frame = (int16_t *)(pcm_out + pcm_written);
|
||||
int64_t rms_sq = 0;
|
||||
int n_stereo = (int)(bytes_read / (2 * sizeof(int16_t)));
|
||||
for (int i = 0; i < n_stereo; i++) {
|
||||
float xf = (float)((rx_buf[i * 2] + rx_buf[i * 2 + 1]) / 2);
|
||||
float yf = xf - dc_x1 + dc_R * dc_y1;
|
||||
dc_x1 = xf; dc_y1 = yf;
|
||||
if (yf > 32767.0f) yf = 32767.0f;
|
||||
else if (yf < -32768.0f) yf = -32768.0f;
|
||||
int16_t mono = (int16_t)yf;
|
||||
out_frame[i] = mono;
|
||||
rms_sq += (int64_t)mono * mono;
|
||||
}
|
||||
rms_sq /= (n_stereo > 0 ? n_stereo : 1);
|
||||
|
||||
pcm_written += (size_t)n_stereo * sizeof(int16_t);
|
||||
total_frames = f + 1;
|
||||
|
||||
if (rms_sq < vad_silence_rms_sq) {
|
||||
silent_frames++;
|
||||
if (silent_frames >= silence_frames) {
|
||||
total_frames = f + 1;
|
||||
ESP_LOGI(TAG, "capture: VAD end (silence %d frames at f=%d)", silent_frames, f);
|
||||
pcm_written += (size_t)n_stereo * sizeof(int16_t);
|
||||
if (++silent_frames >= silence_frames) {
|
||||
ESP_LOGI(TAG, "capture: VAD end (silence %d frames at f=%d)",
|
||||
silent_frames, f);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
silent_frames = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pcm_written += (size_t)n_stereo * sizeof(int16_t);
|
||||
total_frames = f + 1;
|
||||
} else {
|
||||
ESP_LOGI(TAG, "capture: no sustained voice in %d ms — nothing to send", max_ms);
|
||||
/* pcm_written stays 0 → empty WAV → caller skips posting. */
|
||||
}
|
||||
|
||||
/* Leave RX enabled (full-duplex, as at boot). Disabling it here would make
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "tones.h"
|
||||
#include "audio.h"
|
||||
#include "phone.h"
|
||||
#include "slic.h"
|
||||
#include "turn_client.h"
|
||||
#if CONFIG_PLIP_DIAL_DTMF
|
||||
#include "dtmf.h"
|
||||
@@ -46,7 +47,7 @@
|
||||
#define CAPTURE_MAX_IRAM (128 * 1024)
|
||||
#define CAPTURE_MAX_MS_PSRAM 8000
|
||||
#define CAPTURE_MAX_MS_IRAM 4000
|
||||
#define CAPTURE_SILENCE_MS 800
|
||||
#define CAPTURE_SILENCE_MS 600 /* end-of-speech VAD: shorter = snappier reply (was 800) */
|
||||
#define REPLY_POLL_MS 200 /* interval for checking hook during playback */
|
||||
#define REPLY_PLAYBACK_EXTRA_MS 500 /* safety margin added to computed WAV duration */
|
||||
#define BETWEEN_TURNS_MS 300 /* short pause between capture rounds */
|
||||
@@ -82,6 +83,13 @@ static char s_sid[32] = {0};
|
||||
* would 404 at the gateway. Capture the clean routed number here once. */
|
||||
static char s_number[16] = {0};
|
||||
|
||||
/* Incoming-call mode: an NPC "calls" the phone. conversation_arm_incoming()
|
||||
* stores the caller's persona number and rings; when the handset is then picked
|
||||
* up from IDLE, we skip the outgoing dialtone/dial/ringback and go straight to
|
||||
* GREET with this number (the NPC speaks first). */
|
||||
static char s_incoming_number[16] = {0};
|
||||
static volatile bool s_incoming_armed = false;
|
||||
|
||||
/* Known numbers: ringback when dialed */
|
||||
static const char *KNOWN[] = {
|
||||
"12", "3615", "15", "17", "18", "0142738200", NULL
|
||||
@@ -108,6 +116,24 @@ static void go_idle(void)
|
||||
ESP_LOGI(TAG, "-> IDLE");
|
||||
}
|
||||
|
||||
/* Begin an answered INCOMING call: stop ringing, lock the persona number, and
|
||||
* jump straight to GREET (the NPC speaks first). Drives the conversation's own
|
||||
* s_offhook directly because phone.c's debounced GPIO detection is unreliable
|
||||
* during ringing — we trust the SLIC's raw off-hook reading instead. */
|
||||
static void enter_incoming_greet(void)
|
||||
{
|
||||
s_incoming_armed = false;
|
||||
phone_ring_stop();
|
||||
tones_stop();
|
||||
dialer_reset();
|
||||
s_offhook = true;
|
||||
snprintf(s_number, sizeof(s_number), "%s", s_incoming_number);
|
||||
snprintf(s_sid, sizeof(s_sid), "%lld", (long long)esp_timer_get_time());
|
||||
audio_pa_set(true);
|
||||
s_state = STATE_GREET;
|
||||
ESP_LOGI(TAG, "INCOMING pickup -> GREET num=%s sid=%s", s_number, s_sid);
|
||||
}
|
||||
|
||||
static void conv_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
@@ -127,15 +153,21 @@ static void conv_task(void *arg)
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
/* Off-hook: start dial tone if we were idle */
|
||||
/* Off-hook from idle */
|
||||
if (s_state == STATE_IDLE) {
|
||||
dialer_reset();
|
||||
tones_dialtone_start();
|
||||
if (s_incoming_armed) {
|
||||
/* INCOMING call answered (phone.c detected the edge). */
|
||||
enter_incoming_greet();
|
||||
} else {
|
||||
/* Outgoing call: dial tone, wait for digits. */
|
||||
dialer_reset();
|
||||
tones_dialtone_start();
|
||||
#if CONFIG_PLIP_DIAL_DTMF
|
||||
dtmf_start();
|
||||
dtmf_start();
|
||||
#endif
|
||||
s_state = STATE_DIALTONE;
|
||||
ESP_LOGI(TAG, "off-hook -> DIALTONE");
|
||||
s_state = STATE_DIALTONE;
|
||||
ESP_LOGI(TAG, "off-hook -> DIALTONE");
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -144,7 +176,13 @@ static void conv_task(void *arg)
|
||||
/* State machine polling */
|
||||
switch (s_state) {
|
||||
case STATE_IDLE:
|
||||
break; /* nothing to do */
|
||||
/* Incoming-call pickup: phone.c's debounced edge detection is
|
||||
* unreliable while the bell rings, so poll the SLIC's raw off-hook
|
||||
* line directly — it reads the pickup cleanly mid-ring. */
|
||||
if (s_incoming_armed && slic_is_offhook()) {
|
||||
enter_incoming_greet();
|
||||
}
|
||||
break;
|
||||
|
||||
case STATE_DIALTONE:
|
||||
if (!s_offhook) {
|
||||
@@ -268,14 +306,18 @@ static void conv_task(void *arg)
|
||||
* the mic. Never capture while anything is playing, or the
|
||||
* playback feeds back and the line saturates. Wait for the
|
||||
* greeting/filler/reply to finish, then let the line settle. */
|
||||
vTaskDelay(pdMS_TO_TICKS(250)); /* let a just-queued clip start */
|
||||
vTaskDelay(pdMS_TO_TICKS(120)); /* let a just-queued clip start (was 250) */
|
||||
while (s_offhook && audio_is_playing()) {
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
}
|
||||
if (!s_offhook) break;
|
||||
vTaskDelay(pdMS_TO_TICKS(200)); /* line settle after playback */
|
||||
vTaskDelay(pdMS_TO_TICKS(200)); /* let the I2S DMA tail drain */
|
||||
|
||||
/* Capture player utterance — nothing is playing now. */
|
||||
/* Capture the caller utterance with the PA LEFT ON. The loop
|
||||
* already waits for any playback to finish above, so nothing
|
||||
* is driving the earpiece during capture — there is no echo to
|
||||
* mute. Cutting the PA here was found to collapse the mic level
|
||||
* (captures came back near-silent / DC only), so keep it on. */
|
||||
int n = audio_capture_wav(cap_buf, cap_max,
|
||||
cap_ms, CAPTURE_SILENCE_MS);
|
||||
if (!s_offhook) break; /* hung up during capture */
|
||||
@@ -355,3 +397,13 @@ void conversation_on_hook_change(bool offhook)
|
||||
s_offhook = offhook;
|
||||
s_hook_changed = true;
|
||||
}
|
||||
|
||||
void conversation_arm_incoming(const char *number)
|
||||
{
|
||||
snprintf(s_incoming_number, sizeof(s_incoming_number), "%s",
|
||||
(number && number[0]) ? number : "17");
|
||||
s_incoming_armed = true;
|
||||
phone_ring_start();
|
||||
ESP_LOGI(TAG, "incoming call armed (num=%s) — ringing until pickup",
|
||||
s_incoming_number);
|
||||
}
|
||||
|
||||
@@ -3,3 +3,8 @@
|
||||
|
||||
void conversation_init(void);
|
||||
void conversation_on_hook_change(bool offhook);
|
||||
|
||||
/* Arm an INCOMING call from NPC persona `number` and start ringing. When the
|
||||
* handset is picked up from idle, the conversation jumps straight to the
|
||||
* greeting (persona speaks first) instead of the outgoing dial-tone path. */
|
||||
void conversation_arm_incoming(const char *number);
|
||||
|
||||
@@ -87,6 +87,11 @@ esp_err_t es8388_read_reg(uint8_t reg, uint8_t *value)
|
||||
pdMS_TO_TICKS(100));
|
||||
}
|
||||
|
||||
esp_err_t es8388_write_reg(uint8_t reg, uint8_t value)
|
||||
{
|
||||
return i2c_write_reg(reg, value);
|
||||
}
|
||||
|
||||
/* ── Public API ──────────────────────────────────────────────────────────── */
|
||||
|
||||
esp_err_t es8388_init(void)
|
||||
@@ -137,6 +142,18 @@ esp_err_t es8388_init(void)
|
||||
/* 3b. CONTROL2 (0x01): VROI=0, LPVrefBuf=0, normal operation. */
|
||||
if (i2c_write_reg(ES8388_CHIP_CTL2, 0x50) != ESP_OK) return ESP_FAIL;
|
||||
|
||||
/* 3c. Internal DLL stabilisation for LOW sample rates (16 kHz). UNDOCUMENTED
|
||||
* registers 0x35/0x37/0x39 — the proven A252 driver (hardware/projects/
|
||||
* slic-phone Es8388Driver.cpp) sets these to "disable internal DLL for low
|
||||
* sample-rate stability". WITHOUT them the ADC clock domain is unstable at
|
||||
* 16 kHz and the ADC outputs a frozen DC value (capture = flat, no AC) even
|
||||
* though analog signal is present on the LIN pin — exactly our symptom. The
|
||||
* DAC tolerates it, which is why playback worked but capture didn't. They
|
||||
* must be set here in the boot sequence (a live poke can't re-lock the DLL). */
|
||||
if (i2c_write_reg(0x35, 0xA0) != ESP_OK) return ESP_FAIL;
|
||||
if (i2c_write_reg(0x37, 0xD0) != ESP_OK) return ESP_FAIL;
|
||||
if (i2c_write_reg(0x39, 0xD0) != ESP_OK) return ESP_FAIL;
|
||||
|
||||
/* 4. Clock: MCLK divider = 256*Fs, DAC SRC = MCLK. */
|
||||
if (i2c_write_reg(0x08, 0x00) != ESP_OK) return ESP_FAIL; /* MASTERMODE = slave */
|
||||
|
||||
@@ -151,7 +168,7 @@ esp_err_t es8388_init(void)
|
||||
* - ADCCONTROL4 (0x0C) = 0x0C: I2S Philips 16-bit word length
|
||||
* - 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, 0x44) != ESP_OK) return ESP_FAIL; /* MIC PGA +12dB L+R (was +24dB: too hot for a close handset mic → clipping/feedback) */
|
||||
if (i2c_write_reg(ES8388_ADC_CTL1, 0x88) != ESP_OK) return ESP_FAIL; /* MIC PGA +24dB L+R — the SLIC handset mic is QUIET (~2-5% FS at +12dB); +24dB lifts speech above the capture VAD onset. The old "+24dB too hot" note was an artifact of the broken ADC (pre-DLL-fix); measured clean at ~5% peak now. */
|
||||
/* ADCCONTROL2 (0x0A): input select. The K50835F SLIC handset transmit audio is
|
||||
* wired to LIN2/RIN2 on this bench — PROVEN: speech captured (ACrms 196, crest 10.3)
|
||||
* on 0x50, vs DC-only floating offset on 0x00 (LIN1). */
|
||||
|
||||
@@ -40,6 +40,10 @@ esp_err_t es8388_mute(bool mute);
|
||||
/* Read a single ES8388 register for diagnostic purposes. */
|
||||
esp_err_t es8388_read_reg(uint8_t reg, uint8_t *value);
|
||||
|
||||
/* Write a single ES8388 register for diagnostic purposes (bench input/gain
|
||||
* sweeps via /debug/es8388?reg=&val= — find which input the SLIC feeds). */
|
||||
esp_err_t es8388_write_reg(uint8_t reg, uint8_t value);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+133
-27
@@ -24,8 +24,10 @@
|
||||
#include <errno.h>
|
||||
|
||||
#include "audio.h"
|
||||
#include "tones.h"
|
||||
#include "cmd_exec.h"
|
||||
#include "phone.h"
|
||||
#include "conversation.h"
|
||||
#include "es8388.h"
|
||||
#include "slic.h"
|
||||
#include "board_config.h"
|
||||
@@ -83,10 +85,13 @@ static void wifi_event_handler(void *arg, esp_event_base_t base,
|
||||
if (base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
|
||||
esp_wifi_connect();
|
||||
} else if (base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
if (!s_connected) return; /* still in initial connect loop */
|
||||
ESP_LOGW(TAG, "WiFi disconnected, reconnecting...");
|
||||
/* Retry on EVERY disconnect, including failures during the initial boot
|
||||
* attempt: on a mesh the first association frequently fails (wrong node
|
||||
* picked / channel roam) and must be retried, otherwise the boot connect
|
||||
* just times out. Previously the initial loop gave up on first failure. */
|
||||
s_connected = false;
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
ESP_LOGW(TAG, "WiFi disconnected/assoc failed, reconnecting...");
|
||||
vTaskDelay(pdMS_TO_TICKS(1500));
|
||||
esp_wifi_connect();
|
||||
} else if (base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
||||
ip_event_got_ip_t *ev = (ip_event_got_ip_t *)data;
|
||||
@@ -135,9 +140,17 @@ static esp_err_t handle_status(httpd_req_t *req)
|
||||
esp_netif_t *sta = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||
if (sta) esp_netif_get_ip_info(sta, &ip_info);
|
||||
|
||||
/* Report REAL state (this used to hardcode playing/off_hook to false, which
|
||||
* was badly misleading during bring-up). off_hook = firmware debounced view
|
||||
* (phone_is_offhook); shk = raw SLIC line, the single source of truth that
|
||||
* gates audio playback — sound routes to the handset only when shk is true. */
|
||||
snprintf(buf, sizeof(buf),
|
||||
"{\"board\":\"plip\",\"ip\":\"" IPSTR "\",\"playing\":false,\"off_hook\":false}",
|
||||
IP2STR(&ip_info.ip));
|
||||
"{\"board\":\"plip\",\"ip\":\"" IPSTR "\",\"playing\":%s,"
|
||||
"\"off_hook\":%s,\"shk\":%s}",
|
||||
IP2STR(&ip_info.ip),
|
||||
audio_is_playing() ? "true" : "false",
|
||||
phone_is_offhook() ? "true" : "false",
|
||||
slic_is_offhook() ? "true" : "false");
|
||||
return send_json(req, "200 OK", buf);
|
||||
}
|
||||
|
||||
@@ -404,6 +417,38 @@ static esp_err_t handle_debug_regs(httpd_req_t *req)
|
||||
return httpd_resp_send(req, buf, n);
|
||||
}
|
||||
|
||||
/* ── GET /debug/es8388?reg=0x0A&val=0x00 (poke a codec register) ──────────────
|
||||
* Bench tool to find which ADC input the SLIC handset mic feeds: sweep the
|
||||
* input-select (ADCCONTROL2 = reg 0x0A) and PGA gain (ADCCONTROL1 = reg 0x09)
|
||||
* live, then re-capture, without reflashing per experiment. reg/val accept
|
||||
* decimal or 0x-hex. Returns the written value read back. */
|
||||
static esp_err_t handle_debug_es8388(httpd_req_t *req)
|
||||
{
|
||||
char query[64] = {0};
|
||||
httpd_req_get_url_query_str(req, query, sizeof(query));
|
||||
char rs[12] = {0}, vs[12] = {0};
|
||||
if (httpd_query_key_value(query, "reg", rs, sizeof(rs)) != ESP_OK)
|
||||
return send_json(req, "400 Bad Request", "{\"error\":\"missing reg param\"}");
|
||||
uint8_t reg = (uint8_t)strtol(rs, NULL, 0);
|
||||
char resp[96];
|
||||
if (httpd_query_key_value(query, "val", vs, sizeof(vs)) == ESP_OK) {
|
||||
uint8_t val = (uint8_t)strtol(vs, NULL, 0);
|
||||
esp_err_t err = es8388_write_reg(reg, val);
|
||||
uint8_t rb = 0;
|
||||
es8388_read_reg(reg, &rb);
|
||||
ESP_LOGI(TAG, "debug/es8388: reg 0x%02X <- 0x%02X (readback 0x%02X, %s)",
|
||||
reg, val, rb, esp_err_to_name(err));
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"reg\":\"0x%02X\",\"wrote\":\"0x%02X\",\"readback\":\"0x%02X\",\"ok\":%s}",
|
||||
reg, val, rb, err == ESP_OK ? "true" : "false");
|
||||
} else {
|
||||
uint8_t rb = 0;
|
||||
es8388_read_reg(reg, &rb);
|
||||
snprintf(resp, sizeof(resp), "{\"reg\":\"0x%02X\",\"value\":\"0x%02X\"}", reg, rb);
|
||||
}
|
||||
return send_json(req, "200 OK", resp);
|
||||
}
|
||||
|
||||
/* ── POST /game/cmd (WiFi-direct CMD endpoint) ───────────────────────────── */
|
||||
|
||||
#define MAX_CMD_BYTES 512
|
||||
@@ -436,9 +481,17 @@ static esp_err_t handle_cmd_post(httpd_req_t *req)
|
||||
|
||||
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}");
|
||||
/* /debug/ring[?number=NN] — arm an INCOMING call from persona NN (default
|
||||
* 17) and ring. On pickup the conversation jumps straight to the greeting. */
|
||||
char query[48] = {0}, number[16] = {0};
|
||||
httpd_req_get_url_query_str(req, query, sizeof(query));
|
||||
httpd_query_key_value(query, "number", number, sizeof(number));
|
||||
conversation_arm_incoming(number); /* defaults to "17" if empty; starts ringing */
|
||||
char resp[64];
|
||||
snprintf(resp, sizeof(resp), "{\"ok\":true,\"ringing\":true,\"incoming\":\"%s\"}",
|
||||
number[0] ? number : "17");
|
||||
ESP_LOGI(TAG, "debug/ring: incoming armed (num=%s), ringing", number[0] ? number : "17");
|
||||
return send_json(req, "200 OK", resp);
|
||||
}
|
||||
|
||||
/* ── GET /debug/ringstop (stop ring tone over HTTP) ─────────────────────── */
|
||||
@@ -548,20 +601,59 @@ static esp_err_t handle_debug_dacvol(httpd_req_t *req)
|
||||
return send_json(req, "200 OK", resp);
|
||||
}
|
||||
|
||||
/* ── GET /debug/offhook?on=1 (force hook state, bypass flaky contact) ───────── */
|
||||
|
||||
static esp_err_t handle_debug_offhook(httpd_req_t *req)
|
||||
/* ── GET /debug/miccap?ms=4000 (TEMP DIAG: raw fixed-duration mic capture) ───
|
||||
* No VAD, DC-blocked, PA on, tones stopped. Lets us measure the true handset
|
||||
* mic level while the caller speaks continuously, independent of VAD timing. */
|
||||
static esp_err_t handle_debug_miccap(httpd_req_t *req)
|
||||
{
|
||||
char query[24] = {0};
|
||||
char query[64] = {0};
|
||||
httpd_req_get_url_query_str(req, query, sizeof(query));
|
||||
char on[4] = {0};
|
||||
int v = 1; /* default: force off-hook */
|
||||
if (httpd_query_key_value(query, "on", on, sizeof(on)) == ESP_OK) v = atoi(on);
|
||||
phone_force_offhook(v != 0);
|
||||
char resp[64];
|
||||
snprintf(resp, sizeof(resp), "{\"ok\":true,\"forced_offhook\":%s}", v ? "true" : "false");
|
||||
ESP_LOGI(TAG, "debug/offhook: forced %s", v ? "off-hook" : "on-hook");
|
||||
return send_json(req, "200 OK", resp);
|
||||
char val[16] = {0};
|
||||
int ms = 4000;
|
||||
if (httpd_query_key_value(query, "ms", val, sizeof(val)) == ESP_OK) {
|
||||
int v = atoi(val);
|
||||
if (v > 0 && v <= 8000) ms = v;
|
||||
}
|
||||
tones_stop();
|
||||
audio_pa_set(true);
|
||||
if (audio_capture_begin(ms, ms) != 0) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "capture init failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
httpd_resp_set_type(req, "audio/wav");
|
||||
const uint32_t cap_rate = 16000; const uint16_t cap_ch = 1, cap_bits = 16;
|
||||
const uint32_t byte_rate = cap_rate * cap_ch * (cap_bits / 8);
|
||||
const uint16_t block_align = (uint16_t)(cap_ch * (cap_bits / 8));
|
||||
const uint32_t big = 0xFFFFFFFFu; uint16_t fmt_pcm = 1; uint32_t fmt_size = 16;
|
||||
uint8_t hdr[44];
|
||||
memcpy(hdr, "RIFF", 4); memcpy(hdr + 4, &big, 4);
|
||||
memcpy(hdr + 8, "WAVE", 4); memcpy(hdr + 12, "fmt ", 4);
|
||||
memcpy(hdr + 16, &fmt_size, 4); memcpy(hdr + 20, &fmt_pcm, 2);
|
||||
memcpy(hdr + 22, &cap_ch, 2); memcpy(hdr + 24, &cap_rate, 4);
|
||||
memcpy(hdr + 28, &byte_rate, 4); memcpy(hdr + 32, &block_align, 2);
|
||||
memcpy(hdr + 34, &cap_bits, 2); memcpy(hdr + 36, "data", 4); memcpy(hdr + 40, &big, 4);
|
||||
httpd_resp_send_chunk(req, (const char *)hdr, sizeof(hdr));
|
||||
|
||||
const int max_frames = ms / 20;
|
||||
int16_t mono[320];
|
||||
float dc_x1 = 0.0f, dc_y1 = 0.0f; const float dc_R = 0.97f;
|
||||
for (int f = 0; f < max_frames; f++) {
|
||||
int64_t rms_sq = 0;
|
||||
int n = audio_capture_read_frame(mono, 320, &rms_sq);
|
||||
if (n < 0) break;
|
||||
if (n == 0) continue;
|
||||
for (int i = 0; i < n; i++) {
|
||||
float xf = (float)mono[i];
|
||||
float yf = xf - dc_x1 + dc_R * dc_y1;
|
||||
dc_x1 = xf; dc_y1 = yf;
|
||||
if (yf > 32767.0f) yf = 32767.0f; else if (yf < -32768.0f) yf = -32768.0f;
|
||||
mono[i] = (int16_t)yf;
|
||||
}
|
||||
if (httpd_resp_send_chunk(req, (const char *)mono, (ssize_t)(n * sizeof(int16_t))) != ESP_OK) break;
|
||||
}
|
||||
audio_capture_end();
|
||||
httpd_resp_send_chunk(req, NULL, 0);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ── GET /debug/getfile?path=/x.wav (read a SPIFFS file back for diagnosis) ── */
|
||||
@@ -684,7 +776,16 @@ esp_err_t net_init(void)
|
||||
.sta = {
|
||||
.threshold.authmode = (strlen(pwd) == 0)
|
||||
? WIFI_AUTH_OPEN : WIFI_AUTH_WPA2_PSK,
|
||||
.channel = CONFIG_PLIP_WIFI_CHANNEL,
|
||||
/* Mesh-friendly association. A fixed .channel breaks on mesh APs
|
||||
* (Freebox/Orange "Les cils") that roam channels via band-steering
|
||||
* and DFS: if the node isn't on that channel at boot, association
|
||||
* silently fails. So scan ALL channels (channel=0) and connect to
|
||||
* the STRONGEST node broadcasting our SSID. The PLIP talks HTTP to
|
||||
* the gateway, not ESP-NOW, so there is no co-channel constraint. */
|
||||
.channel = 0,
|
||||
.scan_method = WIFI_ALL_CHANNEL_SCAN,
|
||||
.sort_method = WIFI_CONNECT_AP_BY_SIGNAL,
|
||||
.failure_retry_cnt = 5,
|
||||
},
|
||||
};
|
||||
strncpy((char *)wcfg.sta.ssid, ssid, sizeof(wcfg.sta.ssid) - 1);
|
||||
@@ -711,7 +812,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 = 16;
|
||||
hcfg.max_uri_handlers = 17;
|
||||
hcfg.stack_size = 8192;
|
||||
|
||||
esp_err_t ret = httpd_start(&s_httpd, &hcfg);
|
||||
@@ -756,14 +857,14 @@ esp_err_t net_init(void)
|
||||
.uri = "/debug/getfile", .method = HTTP_GET,
|
||||
.handler = handle_debug_getfile, .user_ctx = NULL,
|
||||
};
|
||||
static const httpd_uri_t uri_debug_miccap = {
|
||||
.uri = "/debug/miccap", .method = HTTP_GET,
|
||||
.handler = handle_debug_miccap, .user_ctx = NULL,
|
||||
};
|
||||
static const httpd_uri_t uri_debug_dacvol = {
|
||||
.uri = "/debug/dacvol", .method = HTTP_GET,
|
||||
.handler = handle_debug_dacvol, .user_ctx = NULL,
|
||||
};
|
||||
static const httpd_uri_t uri_debug_offhook = {
|
||||
.uri = "/debug/offhook", .method = HTTP_GET,
|
||||
.handler = handle_debug_offhook, .user_ctx = NULL,
|
||||
};
|
||||
static const httpd_uri_t uri_debug_ring = {
|
||||
.uri = "/debug/ring", .method = HTTP_GET,
|
||||
.handler = handle_debug_ring, .user_ctx = NULL,
|
||||
@@ -780,6 +881,10 @@ esp_err_t net_init(void)
|
||||
.uri = "/debug/hookmon", .method = HTTP_GET,
|
||||
.handler = handle_debug_hookmon, .user_ctx = NULL,
|
||||
};
|
||||
static const httpd_uri_t uri_debug_es8388 = {
|
||||
.uri = "/debug/es8388", .method = HTTP_GET,
|
||||
.handler = handle_debug_es8388, .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);
|
||||
@@ -789,12 +894,13 @@ esp_err_t net_init(void)
|
||||
httpd_register_uri_handler(s_httpd, &uri_debug_dial);
|
||||
httpd_register_uri_handler(s_httpd, &uri_debug_vol);
|
||||
httpd_register_uri_handler(s_httpd, &uri_debug_getfile);
|
||||
httpd_register_uri_handler(s_httpd, &uri_debug_miccap);
|
||||
httpd_register_uri_handler(s_httpd, &uri_debug_dacvol);
|
||||
httpd_register_uri_handler(s_httpd, &uri_debug_offhook);
|
||||
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);
|
||||
httpd_register_uri_handler(s_httpd, &uri_debug_hookmon);
|
||||
httpd_register_uri_handler(s_httpd, &uri_debug_es8388);
|
||||
|
||||
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;
|
||||
|
||||
+20
-44
@@ -34,20 +34,23 @@
|
||||
#define TAG "phone"
|
||||
|
||||
#define DEBOUNCE_MS 30
|
||||
#define HANGUP_THRESHOLD_MS 1500 /* open > this → real hangup, not a pulse.
|
||||
* Raised from 500 ms: the A1S cradle contact
|
||||
* is marginal and produces ~500 ms spurious
|
||||
* opens that were resetting the call before
|
||||
* the NPC greeting. A real hangup holds the
|
||||
* line open indefinitely, so it still fires
|
||||
* (~1.5 s later). Rotary pulses (~60 ms) and
|
||||
* closed inter-digit gaps are unaffected. */
|
||||
#define HANGUP_THRESHOLD_MS 2500 /* open > this → real hangup, not a pulse/flicker.
|
||||
* HOOK DEBOUNCE: the A1S cradle contact is
|
||||
* marginal and flickers open for up to ~2 s
|
||||
* mid-call; treating those as a hangup dropped
|
||||
* the live conversation. Requiring the line to
|
||||
* stay open ≥ 2.5 s before hanging up rides
|
||||
* through the flickers. A real hangup holds the
|
||||
* line open indefinitely so it still fires
|
||||
* (~2.5 s later). Rotary pulses (~60 ms) and a
|
||||
* closed line are unaffected. */
|
||||
#define TASK_STACK 4096
|
||||
#define TASK_PRIO 5
|
||||
#define RESYNC_STABLE_MS 600 /* if the raw hook level stably disagrees with
|
||||
* s_offhook for this long (no pulse), the edge
|
||||
* detection missed/flapped a transition →
|
||||
* self-correct s_offhook to the physical level */
|
||||
#define RESYNC_PICKUP_MS 600 /* off-hook (pickup) self-correct: fast, so an
|
||||
* incoming call answers promptly. */
|
||||
#define RESYNC_HANGUP_MS HANGUP_THRESHOLD_MS /* on-hook (hangup) self-correct:
|
||||
* same long debounce as the prolonged-open path
|
||||
* so a flickering contact never drops the call. */
|
||||
|
||||
/* Rotary pulse hardening (CONFIG_PLIP_DIAL_PULSE) */
|
||||
#define PULSE_MIN_WIDTH_MS 20 /* ignore opens shorter than this (glitch filter) */
|
||||
@@ -60,9 +63,6 @@
|
||||
static volatile bool s_edge_pending = false;
|
||||
static volatile bool s_ringing = false;
|
||||
static volatile bool s_offhook = false; /* true = handset picked up */
|
||||
static volatile bool s_hook_override = false; /* when true, ignore the physical hook
|
||||
* and hold s_offhook at the forced value
|
||||
* (debug: decouple from a flaky contact) */
|
||||
|
||||
/* IRAM_ATTR: ISR must live in IRAM on original ESP32. */
|
||||
static void IRAM_ATTR on_hook_isr(void *arg)
|
||||
@@ -163,15 +163,6 @@ static void phone_task(void *arg)
|
||||
#endif
|
||||
|
||||
for (;;) {
|
||||
/* Debug override: hold s_offhook at the forced value, ignore the
|
||||
* physical hook entirely (decouples the voice loop from a flaky
|
||||
* cradle contact). Set via phone_force_offhook() / GET /debug/offhook. */
|
||||
if (s_hook_override) {
|
||||
s_edge_pending = false;
|
||||
resync_ms = 0;
|
||||
vTaskDelay(pdMS_TO_TICKS(20));
|
||||
continue;
|
||||
}
|
||||
if (s_edge_pending) {
|
||||
s_edge_pending = false;
|
||||
|
||||
@@ -328,7 +319,11 @@ poll_sleep:
|
||||
bool phys_off = (gpio_get_level(hook_gpio) == HOOK_OFFHOOK_LEVEL);
|
||||
if (!pulse_active && phys_off != s_offhook) {
|
||||
resync_ms += 10;
|
||||
if (resync_ms >= RESYNC_STABLE_MS) {
|
||||
/* Hook debounce: a PICKUP (→off-hook) is confirmed fast so calls
|
||||
* answer promptly; a HANGUP (→on-hook) needs the long debounce so
|
||||
* a flickering cradle contact can't drop a live call. */
|
||||
int need = phys_off ? RESYNC_PICKUP_MS : RESYNC_HANGUP_MS;
|
||||
if (resync_ms >= need) {
|
||||
ESP_LOGW(TAG, "hook resync: physical=%s but s_offhook=%d — correcting",
|
||||
phys_off ? "off-hook" : "on-hook", (int)s_offhook);
|
||||
s_offhook = phys_off;
|
||||
@@ -358,22 +353,3 @@ bool phone_is_offhook(void)
|
||||
{
|
||||
return s_offhook;
|
||||
}
|
||||
|
||||
void phone_force_offhook(bool off)
|
||||
{
|
||||
/* Debug: override the physical hook. off=true → simulate pickup (DIALTONE),
|
||||
* off=false → simulate hangup (IDLE). Stays in effect until reboot. */
|
||||
s_hook_override = true;
|
||||
if (off == s_offhook) {
|
||||
ESP_LOGI(TAG, "force_offhook: already %s (override on)", off ? "off-hook" : "on-hook");
|
||||
return;
|
||||
}
|
||||
s_offhook = off;
|
||||
ESP_LOGI(TAG, "force_offhook: %s (override)", off ? "off-hook" : "on-hook");
|
||||
audio_pa_set(off);
|
||||
if (!off) {
|
||||
audio_stop();
|
||||
phone_ring_stop();
|
||||
}
|
||||
report_offhook(off);
|
||||
}
|
||||
|
||||
@@ -31,11 +31,6 @@ void phone_ring_stop(void);
|
||||
* Safe to call from any task; backed by a volatile flag updated in phone_task. */
|
||||
bool phone_is_offhook(void);
|
||||
|
||||
/* Debug: force the hook state, overriding the physical contact until reboot.
|
||||
* off=true simulates pickup (→ DIALTONE), off=false simulates hangup (→ IDLE).
|
||||
* Lets the voice loop be validated independently of a flaky cradle contact. */
|
||||
void phone_force_offhook(bool off);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user