Removes a staged file with the same sd/ vs SPIFFS path routing as the
POST handler (mounts SD on demand). Lets the host clean stale clips
when regenerating the SD voice pack. max_uri_handlers 17->18.
Validated: probe WAVs removed from /sdcard/voice (200, then 404).
/debug/ring now takes an optional scene; conversation_arm_incoming
locks it and, after the greeting, queues that scene's local clip
/sdcard/voice/hint_<scene>_l1_0.wav (played back-to-back). For a
scripted offline call CONNECTED skips the gateway/model listen loop
and just waits for hang-up. Scene is cleared in go_idle so a later
dial-out call never inherits it. Validated: greeting 5.05s + WARNING
hint 6.20s from SD on pickup, no gateway/model.
On incoming pickup the GREET state now plays the local clip
/sdcard/voice/greet_<num>.wav when present — the phone greets the
caller with no gateway and no model in RAM. Falls back to the live
gateway greeting (turn_client) only when the pack lacks a clip for
that number.
/game/file only wrote SPIFFS (960 KB, too small for a voice pack). A
path prefixed sd/ (or /sdcard/) now routes to the microSD: the card is
mounted on demand (audio_ensure_sd, independent of hook state) and the
parent dir is created. MAX_BODY 256->512 KB so full say() hints fit
(streamed in 2 KB chunks, RAM-safe). Enable FATFS LFN_HEAP so >8.3
filenames work (greet_0142738200.wav etc). Validated: 40-sample FR
voice pack staged to /sdcard/voice on the bench PLIP (960 MB card).
notify_gateway_scene() + hints puzzle_start lived only in
npc_engine_set_step(), which had NO callers — the runtime->gateway
sync was dead code, so scripted incoming calls would never fire in a
real game. POST /game/step goes through game_endpoint_apply_step(),
which now resolves the step's scene_id (new field on scene_binding,
parsed straight from the IR) and calls the new
npc_engine_set_scene_by_id(). Hardware-validated: STEP_WARNING ->
scene 3 -> gateway notify -> auto-ring Professeur Zacus on the PLIP.
The scene->gateway POST ran synchronously (2 s timeout) on the scene
transition, so an offline gateway lagged EVERY scene change by 2 s.
Fire it on a detached one-shot task instead, so the game's scene
transitions never wait on the network. Frees the heap scene copy and
self-deletes. Builds clean.
On each scene change (where it already signals /hints/puzzle_start), the
npc_engine now also POSTs the active SCENE_* to the voice gateway
/game/step?scene=… (best-effort, esp_http_client, Bearer token). The
phone NPCs then disguise THIS scene's hint automatically during a game.
Gateway URL/token hardcoded in main.c like the hints URL (-> NVS later).
Recalibrate the capture VAD to the quiet handset voice: onset 1.4%->0.7%
FS (was never triggering -> 'no sustained voice'), silence 0.6%->0.34%
FS and end-of-speech window 600->900 ms so a whole sentence is held
instead of cut mid-phrase. Also force slic_ring_stop() on incoming
pickup so the physical bell always stops when answered (phone.c/slic.c
s_ringing could desync, leaving the bell ringing through the call).
The marginal A1S cradle contact flickers open mid-call; treating a
brief open as a hangup dropped the live conversation. Raise the
prolonged-open hangup threshold to 2.5 s and make the resync
asymmetric: a PICKUP is confirmed fast (600 ms, calls answer promptly)
while a HANGUP needs the line to stay open 2.5 s. Brief flickers no
longer end the call; a real hangup still fires ~2.5 s later.
Listen-loop capture now only commits when the caller actually speaks:
phase A waits for a sustained voice onset (3 frames above threshold,
rejecting the PA-mute click), phase B records until silence. Empty
captures are no longer posted, so the NPC never replies to silence.
Also: DC-blocking high-pass on the captured mono, VAD thresholds tuned
to the quiet SLIC handset mic (onset ~1.4%, silence ~0.6%), keep the
PA on during capture (muting it collapsed the mic), and add a
/debug/miccap diagnostic endpoint (raw fixed-duration mic capture).
Incoming-call greeting was silently dropped: audio CMD_PLAY gated on
phone.c's debounced phone_is_offhook(), which lags/misses the pickup
while the bell rings, but the incoming flow established off-hook only
via the raw SLIC poll in conversation.c. The two flags desynced and
playback was skipped as 'on-hook' though the handset was up.
Gate playback on the real SHK line (slic_is_offhook) instead — the
single source of truth. Remove all hook forcing (s_hook_override,
phone_force_offhook, /debug/offhook): the SHK contact is reliable in
hardware, so the firmware must trust it, never override it. Builds
clean (ESP-IDF v5.4.4).
Add es8388_write_reg() public API and GET /debug/es8388?reg=&val=
HTTP handler to read/write codec registers at runtime without
reflashing. Bumps max_uri_handlers 16→17.
STA config: channel=0 (all-channel scan), WIFI_ALL_CHANNEL_SCAN,
WIFI_CONNECT_AP_BY_SIGNAL, failure_retry_cnt=5.
Disconnect handler: retry on every disconnect event, including during
initial association — previously the first failure at boot caused an
immediate abort and a ~30s timeout before IP was acquired.
Validated on hardware: connects reliably on ch1, RSSI -32, IP in ~2.5s.
IDF5 i2s_channel_init_std_mode() constitutes full-duplex ONLY when TX and
RX std_cfg are byte-for-byte identical (memcmp). When din/dout differ
between the two calls, the driver silently moves RX to I2S_NUM_1 which has
no BCLK/WS routing, producing permanent zeros on i2s_channel_read().
Fix: use the same i2s_std_config_t for both TX and RX init calls, with
dout=GPIO26 and din=GPIO35 both set. IDF handles GPIO direction internally.
Also clean up ES8388 register sequence:
- ADCCONTROL2 = 0x00 (LIN1/RIN1 differential, LINE IN header)
- ADCCONTROL6 = 0x00 (clear ADCSMUTE, reset default was 0x30)
- ADCPOWER = 0x00 (full ADC power-up, was 0x09)
- DACCONTROL21 = 0x80 (DAC+ADC normal mode, not line bypass 0xC0)
Verified: peak=2593, rms=2482 over 48000 samples (3s @ 16kHz).
ADCCONTROL6 (reg 0x0E) reset default = 0x30 which has ADCSMUTE=1 (bit5) — ADC output
muted by default. Writing 0x00 unmutes. Without this, ADCINSEL=0x50 (LIN2) is selected
but the signal is suppressed at the ADC output stage → peak=0.
ADCPOWER (reg 0x03) changed from 0x09 (intermediate Espressif open state) to 0x00 (full
power-up: all power-down bits cleared). Value 0x09 = bits 0+3 set (ADCPD_L + MICBIAS_PD)
— MICBIAS_PD in particular means the internal microphone bias is powered off, which can
starve the SLIC line-in path. 0x00 is the correct end state for recording mode per
Espressif esp8388_start(ES_MODULE_ADC) reference.
sdkconfig.defaults: add CONFIG_PLIP_HOOK_GPIO=23 / CONFIG_PLIP_HOOK_ACTIVE_HIGH=y
as explicit defaults so clean builds use the SLIC SHK pin without menuconfig.
ADCCONTROL2 (reg 0x0A): was 0x00 (LINSEL=00 LINPUT1/RINPUT1 — onboard PCB mic),
now 0x50 (LINSEL=01 LINPUT2 / RINSEL=01 RINPUT2 — telephone handset mic via SLIC).
The SLIC K50835F routes the handset mic signal to the ES8388 LINPUT2/RINPUT2 pins.
Writing 0x00 meant the ADC was listening to the empty onboard mic, producing 0/0 RMS.
Writing 0x50 connects the SLIC audio path, enabling voice capture from the handset.
PGA stays at +24 dB (ADCCONTROL1 = 0xBB). ADC power-up sequence unchanged.
- 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.