Files
ESP32_ZACUS/plip_voice/main/main.c
T
clemsail 309005360b
CI / platformio (pull_request) Failing after 4m52s
feat(plip_voice): ESP-IDF port of PLIP retro telephone annex
New IDF project targeting AI-Thinker ESP32-A1S Audio Kit (ESP32, ES8388
codec). Replaces the Arduino PLIP_FIRMWARE with a proper IDF component
structure aligned on box3_voice / scenario_mesh infrastructure.

Phases implemented and hardware-verified:
- A: ES8388 I2C init + I2S TX; 440 Hz test tone exits the speaker
- B: WiFi STA + httpd :80 (GET /status, POST /game/scenario, /game/file)
- C: ESP-NOW CMD receiver via shared scenario_mesh lib; op=play dispatched
     to audio; screen/evt/led logged (no display on PLIP)
- D: Off-hook GPIO monitor (GPIO4=BOOT button stand-in); ring tone cadence;
     POST /voice/hook to master on pickup/hangup (200 OK confirmed)

MAC (first observed): a0:b7:65:e7:f6:44
IP (DHCP): 192.168.0.138

Files:
  plip_voice/CMakeLists.txt          - project root; EXTRA_COMPONENT_DIRS=../lib/scenario_mesh
  plip_voice/partitions.csv          - 4MB OTA dual-bank + SPIFFS (matches PLIP_FIRMWARE)
  plip_voice/sdkconfig.defaults      - ESP32 target, 4MB flash DIO, ch=11 hint (no creds)
  plip_voice/main/Kconfig.projbuild  - PLIP_ menu (SSID/pwd/channel/master_url/hook_gpio)
  plip_voice/main/board_config.h     - A1S pin map (I2C 32/33, I2S 0/27/25/26/35, PA=21)
  plip_voice/main/es8388.[ch]        - ES8388 I2C register init sequence (legacy i2c driver)
  plip_voice/main/audio.[ch]         - I2S TX + async play queue + ring task
  plip_voice/main/net.[ch]           - WiFi STA + httpd (NVS creds with Kconfig fallback)
  plip_voice/main/cmd_exec.[ch]      - D5 CMD executor (play→audio; screen/evt/led→log)
  plip_voice/main/hook_client.[ch]   - POST /voice/hook via esp_http_client
  plip_voice/main/phone.[ch]         - Off-hook GPIO ISR + debounce + ring control
  plip_voice/main/main.c             - app_main boot sequence (all four phases)

Build: IDF 5.4.4, target esp32, clean.
Flash: /dev/cu.usbserial-0001, 4MB flash verified.
2026-06-14 13:29:33 +02:00

144 lines
5.4 KiB
C

/*
* main.c — PLIP voice annex application entry point.
*
* Board: AI-Thinker ESP32-A1S Audio Kit V2.2 (ESP32, ES8388, SD, mic, HP)
* Target: ESP-IDF 5.4, esp32 target (NOT esp32s3)
*
* Boot sequence:
* 1. NVS init
* 2. audio_init() — ES8388 + I2S (Phase A)
* 3. audio_play_tone(440Hz, 1s) — Phase A proof (sound before WiFi)
* 4. net_init() — WiFi STA + HTTP server (Phase B)
* 5. scenario_mesh_init() — ESP-NOW CMD receiver (Phase C)
* 6. hook_client_init() — hook event forwarder (Phase D)
* 7. phone_init() — off-hook GPIO monitor (Phase D)
*/
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "esp_err.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "audio.h"
#include "net.h"
#include "cmd_exec.h"
#include "hook_client.h"
#include "phone.h"
#include "scenario_mesh.h"
static const char *TAG = "plip-main";
/* ── ESP-NOW CMD/EVT text callback ─────────────────────────────────────────── */
static void on_mesh_text(uint8_t kind, const uint8_t src_mac[6], const char *text)
{
if (kind == SCENARIO_MESH_TEXT_CMD) {
ESP_LOGI(TAG, "ESP-NOW CMD from %02x:%02x:%02x:%02x:%02x:%02x: %.80s",
src_mac[0], src_mac[1], src_mac[2],
src_mac[3], src_mac[4], src_mac[5], text);
esp_err_t err = cmd_exec_handle(text, strlen(text));
if (err != ESP_OK && err != ESP_ERR_NOT_SUPPORTED) {
ESP_LOGW(TAG, "cmd_exec_handle: %s", esp_err_to_name(err));
}
} else {
ESP_LOGI(TAG, "ESP-NOW EVT from %02x:%02x:%02x:%02x:%02x:%02x: %.80s",
src_mac[0], src_mac[1], src_mac[2],
src_mac[3], src_mac[4], src_mac[5], text);
}
}
/* ── Boot task — runs all phases with sufficient stack ────────────────────── */
static void boot_task(void *arg)
{
(void)arg;
/* ── 2. Audio init (Phase A) ── */
esp_err_t 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)");
/* Continue anyway — WiFi + hook still work. */
} else {
/* ── 3. Phase A proof: 440 Hz test tone ── */
ESP_LOGI(TAG, "Phase A: playing 440 Hz test tone (1s) — LISTEN FOR SOUND");
audio_play_tone(440.0f, 1000);
vTaskDelay(pdMS_TO_TICKS(200));
ESP_LOGI(TAG, "Phase A: tone done. If you heard it, Phase A is VERIFIED.");
}
/* ── 4. Network + HTTP server (Phase B) ── */
ret = net_init();
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Phase B: WiFi + httpd up. Verify: curl http://<PLIP_IP>/status");
} else if (ret == ESP_ERR_TIMEOUT) {
ESP_LOGW(TAG, "Phase B: WiFi timeout — running offline (ESP-NOW still active)");
} else {
ESP_LOGW(TAG, "Phase B: net_init error %s", esp_err_to_name(ret));
}
/* ── 5. ESP-NOW CMD receiver + scenario mesh (Phase C) ── */
/* scenario_mesh_init() requires WiFi to be started (done in net_init). */
ret = scenario_mesh_init(NULL); /* receive CMD only — no scenario apply */
if (ret != ESP_OK) {
ESP_LOGW(TAG, "scenario_mesh_init: %s — ESP-NOW unavailable",
esp_err_to_name(ret));
} else {
ret = scenario_mesh_set_text_cb(on_mesh_text);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Phase C: ESP-NOW CMD receiver up");
}
}
/* ── 6. Hook client (Phase D) ── */
ret = hook_client_init(NULL); /* uses CONFIG_PLIP_MASTER_URL */
if (ret != ESP_OK) {
ESP_LOGW(TAG, "hook_client_init: %s", esp_err_to_name(ret));
}
/* ── 7. Phone task (Phase D) ── */
ret = phone_init();
if (ret != ESP_OK) {
ESP_LOGW(TAG, "phone_init: %s", esp_err_to_name(ret));
} else {
ESP_LOGI(TAG, "Phase D: off-hook GPIO monitor up (GPIO=%d)",
CONFIG_PLIP_HOOK_GPIO);
}
ESP_LOGI(TAG, "Boot complete. Free heap: %lu bytes", esp_get_free_heap_size());
ESP_LOGI(TAG, "Press BOOT button (GPIO%d) to simulate hook pickup.",
CONFIG_PLIP_HOOK_GPIO);
vTaskDelete(NULL);
}
/* ── Application entry point ─────────────────────────────────────────────── */
void app_main(void)
{
ESP_LOGI(TAG, "============================================");
ESP_LOGI(TAG, " Zacus PLIP Voice Annex");
ESP_LOGI(TAG, " Board: AI-Thinker ESP32-A1S (ES8388)");
ESP_LOGI(TAG, " IDF: %s", esp_get_idf_version());
ESP_LOGI(TAG, " Free heap: %lu bytes", esp_get_free_heap_size());
ESP_LOGI(TAG, "============================================");
/* ── 1. NVS ── */
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
/* Delegate all subsequent phases to a task with adequate stack.
* app_main's default stack (8192) is too tight for I2S + WiFi init
* when called from the main task (IDF internal stack usage adds up). */
xTaskCreatePinnedToCore(boot_task, "boot", 12288, NULL, 5, NULL, 0);
}