309005360b
CI / platformio (pull_request) Failing after 4m52s
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.
144 lines
5.4 KiB
C
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);
|
|
}
|