feat(net,audio): add networking and audio layers
Context: Lisael Box needs Wi-Fi + time + weather for the home screen, and an audio path for webradio and SD playback, all child-safe. Approach: net/ : STA bring-up with auto-reconnect (creds from NVS then menuconfig), SNTP with the Europe/Paris TZ string (no setlocale), and a periodic open-meteo.com fetch (HTTPS via the CA bundle, cJSON parse) that degrades to "—" on failure. audio/ : one shared ES8311 codec (BSP) behind a facade enforcing the child volume cap (persisted to NVS). Webradio and SD playback use ESP-ADF pipelines (http_stream/fatfs -> decoder -> i2s), guarded by __has_include so a no-ADF build still configures. Changes: - net/wifi, net/sntp_time, net/weather - audio/audio_player (codec + volume facade) - audio/radio_pipeline (ESP-ADF http stream) - audio/sd_player (ESP-ADF fatfs + manifest.json parser) Impact: Sources are idiomatic but unverified on hardware. The BSP<->ADF I2S ownership and the radio URLs are flagged in the README.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
// Shared audio facade for Lisael Box — see audio_player.h.
|
||||
|
||||
#include "audio/audio_player.h"
|
||||
#include "audio/radio_pipeline.h"
|
||||
#include "audio/sd_player.h"
|
||||
#include "lisael_config.h"
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "nvs.h"
|
||||
#include "bsp/esp-box-3.h"
|
||||
|
||||
static const char *TAG = "audio";
|
||||
|
||||
static esp_codec_dev_handle_t s_codec;
|
||||
static lisael_audio_source_t s_source = LISAEL_AUDIO_IDLE;
|
||||
static int s_volume = LISAEL_VOLUME_DEFAULT;
|
||||
|
||||
static int clamp_volume(int v)
|
||||
{
|
||||
if (v < 0) v = 0;
|
||||
if (v > LISAEL_VOLUME_MAX) v = LISAEL_VOLUME_MAX;
|
||||
return v;
|
||||
}
|
||||
|
||||
static void load_volume(void)
|
||||
{
|
||||
nvs_handle_t nvs;
|
||||
if (nvs_open(LISAEL_NVS_NS, NVS_READONLY, &nvs) == ESP_OK) {
|
||||
int32_t v;
|
||||
if (nvs_get_i32(nvs, LISAEL_NVS_VOLUME, &v) == ESP_OK) {
|
||||
s_volume = clamp_volume((int)v);
|
||||
}
|
||||
nvs_close(nvs);
|
||||
}
|
||||
}
|
||||
|
||||
static void persist_volume(void)
|
||||
{
|
||||
nvs_handle_t nvs;
|
||||
if (nvs_open(LISAEL_NVS_NS, NVS_READWRITE, &nvs) == ESP_OK) {
|
||||
nvs_set_i32(nvs, LISAEL_NVS_VOLUME, s_volume);
|
||||
nvs_commit(nvs);
|
||||
nvs_close(nvs);
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t lisael_audio_init(void)
|
||||
{
|
||||
// The BSP returns a ready-to-use ES8311 speaker codec device.
|
||||
// bsp_i2c_init() must already have been called (done in app_main).
|
||||
s_codec = bsp_audio_codec_speaker_init();
|
||||
if (!s_codec) {
|
||||
ESP_LOGE(TAG, "bsp_audio_codec_speaker_init failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
load_volume();
|
||||
esp_codec_dev_set_out_vol(s_codec, (float)s_volume);
|
||||
ESP_LOGI(TAG, "codec ready, volume=%d%% (cap %d%%)", s_volume, LISAEL_VOLUME_MAX);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_codec_dev_handle_t lisael_audio_codec(void)
|
||||
{
|
||||
return s_codec;
|
||||
}
|
||||
|
||||
void lisael_audio_stop_all(void)
|
||||
{
|
||||
switch (s_source) {
|
||||
case LISAEL_AUDIO_RADIO: lisael_radio_stop(); break;
|
||||
case LISAEL_AUDIO_SD: lisael_sd_stop(); break;
|
||||
default: break;
|
||||
}
|
||||
s_source = LISAEL_AUDIO_IDLE;
|
||||
}
|
||||
|
||||
void lisael_audio_set_source(lisael_audio_source_t src)
|
||||
{
|
||||
s_source = src;
|
||||
}
|
||||
|
||||
lisael_audio_source_t lisael_audio_get_source(void)
|
||||
{
|
||||
return s_source;
|
||||
}
|
||||
|
||||
int lisael_audio_get_volume(void)
|
||||
{
|
||||
return s_volume;
|
||||
}
|
||||
|
||||
void lisael_audio_set_volume(int percent)
|
||||
{
|
||||
s_volume = clamp_volume(percent);
|
||||
if (s_codec) {
|
||||
esp_codec_dev_set_out_vol(s_codec, (float)s_volume);
|
||||
}
|
||||
persist_volume();
|
||||
ESP_LOGI(TAG, "volume -> %d%%", s_volume);
|
||||
}
|
||||
|
||||
void lisael_audio_volume_step(int delta)
|
||||
{
|
||||
lisael_audio_set_volume(s_volume + delta);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Shared audio facade for Lisael Box.
|
||||
//
|
||||
// One ES8311 speaker codec instance (from the esp-box BSP) is shared by all
|
||||
// audio sources: the webradio pipeline (ESP-ADF http_stream -> decoder ->
|
||||
// i2s_stream), the SD "boite a sons" / story player, and the calm-corner
|
||||
// ambience. Sources are mutually exclusive: starting one stops the others.
|
||||
//
|
||||
// Volume is clamped to LISAEL_VOLUME_MAX (child-safe cap) everywhere.
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "esp_err.h"
|
||||
#include "esp_codec_dev.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
LISAEL_AUDIO_IDLE = 0,
|
||||
LISAEL_AUDIO_RADIO,
|
||||
LISAEL_AUDIO_SD,
|
||||
} lisael_audio_source_t;
|
||||
|
||||
// Initialise the BSP audio codec (ES8311 speaker). Call once after bsp_i2c_init.
|
||||
// Loads the persisted volume from NVS (default LISAEL_VOLUME_DEFAULT).
|
||||
esp_err_t lisael_audio_init(void);
|
||||
|
||||
// The shared codec handle, for the ESP-ADF i2s_stream / direct writes.
|
||||
// Returns NULL before lisael_audio_init().
|
||||
esp_codec_dev_handle_t lisael_audio_codec(void);
|
||||
|
||||
// Stop whatever source is currently playing and mark IDLE.
|
||||
void lisael_audio_stop_all(void);
|
||||
|
||||
// Source bookkeeping (set by radio_pipeline / sd_player when they take over).
|
||||
void lisael_audio_set_source(lisael_audio_source_t src);
|
||||
lisael_audio_source_t lisael_audio_get_source(void);
|
||||
|
||||
// --- Volume (percent, 0..LISAEL_VOLUME_MAX) ---------------------------------
|
||||
int lisael_audio_get_volume(void); // current, percent
|
||||
void lisael_audio_set_volume(int percent); // clamped + persisted to NVS
|
||||
void lisael_audio_volume_step(int delta); // +/- a step, clamped
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,165 @@
|
||||
// Webradio streaming pipeline (ESP-ADF) for Lisael Box.
|
||||
//
|
||||
// Pipeline: http_stream -> esp_decoder (auto MP3/AAC) -> i2s_stream
|
||||
//
|
||||
// IMPORTANT (verify on hardware):
|
||||
// The esp-box BSP also configures I2S/ES8311 (bsp_audio_init /
|
||||
// bsp_audio_codec_speaker_init). ESP-ADF's i2s_stream historically owns the
|
||||
// I2S peripheral itself. On a real BOX-3 you must make these agree on ONE
|
||||
// owner of the I2S port. Two known-good approaches:
|
||||
// (A) Let ADF own I2S: configure i2s_stream with the BOX-3 pins/port and
|
||||
// do NOT call bsp_audio_init(); keep using the BSP codec handle only
|
||||
// for volume.
|
||||
// (B) Let the BSP own I2S (bsp_audio_init) and write decoded PCM to the
|
||||
// codec via esp_codec_dev_write() from a raw_stream sink instead of
|
||||
// i2s_stream.
|
||||
// This file implements (A) — ADF i2s_stream as the output — which is the
|
||||
// most common ADF idiom. The i2s_std config (pins, port) MUST be matched to
|
||||
// the BOX-3 BSP values; left as TODOs below because they depend on the exact
|
||||
// BSP version and are not safe to guess. Marked clearly.
|
||||
//
|
||||
// Because ADF is only present when ADF_PATH is set, the whole body is guarded
|
||||
// by __has_include so a no-ADF inspection build still compiles (the functions
|
||||
// then log "audio disabled").
|
||||
|
||||
#include "audio/radio_pipeline.h"
|
||||
#include "audio/audio_player.h"
|
||||
#include "lisael_config.h"
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *TAG = "radio";
|
||||
|
||||
#if defined(__has_include)
|
||||
# if __has_include("audio_pipeline.h")
|
||||
# define LISAEL_HAVE_ADF 1
|
||||
# endif
|
||||
#endif
|
||||
|
||||
#ifdef LISAEL_HAVE_ADF
|
||||
|
||||
#include "audio_pipeline.h"
|
||||
#include "audio_element.h"
|
||||
#include "http_stream.h"
|
||||
#include "i2s_stream.h"
|
||||
#include "esp_decoder.h"
|
||||
#include "audio_mem.h"
|
||||
|
||||
static audio_pipeline_handle_t s_pipe;
|
||||
static audio_element_handle_t s_http, s_decoder, s_i2s;
|
||||
static bool s_playing;
|
||||
|
||||
static esp_err_t build_pipeline(void)
|
||||
{
|
||||
if (s_pipe) {
|
||||
return ESP_OK; // already built
|
||||
}
|
||||
|
||||
audio_pipeline_cfg_t pcfg = DEFAULT_AUDIO_PIPELINE_CONFIG();
|
||||
s_pipe = audio_pipeline_init(&pcfg);
|
||||
if (!s_pipe) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
// --- HTTP source ---------------------------------------------------------
|
||||
http_stream_cfg_t http_cfg = HTTP_STREAM_CFG_DEFAULT();
|
||||
http_cfg.type = AUDIO_STREAM_READER;
|
||||
s_http = http_stream_init(&http_cfg);
|
||||
|
||||
// --- Auto decoder (MP3 / AAC / others) ----------------------------------
|
||||
// esp_decoder auto-detects the container/codec from the stream — perfect
|
||||
// for webradios that may serve MP3 or AAC.
|
||||
audio_decoder_t decoders[] = {
|
||||
DEFAULT_ESP_MP3_DECODER_CONFIG(),
|
||||
DEFAULT_ESP_AAC_DECODER_CONFIG(),
|
||||
};
|
||||
esp_decoder_cfg_t dec_cfg = DEFAULT_ESP_DECODER_CONFIG();
|
||||
s_decoder = esp_decoder_init(&dec_cfg, decoders,
|
||||
sizeof(decoders) / sizeof(decoders[0]));
|
||||
|
||||
// --- I2S sink to ES8311 --------------------------------------------------
|
||||
// TODO(hardware): confirm the I2S port number and that these pins match the
|
||||
// BOX-3 BSP. With ADF >= 2.7 the simplest path is i2s_stream_init with the
|
||||
// default config and then i2s_stream_set_clk() once the decoder reports the
|
||||
// sample rate. The codec itself (gain/mute) stays driven by the BSP
|
||||
// esp_codec_dev handle (lisael_audio_codec()).
|
||||
i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT();
|
||||
i2s_cfg.type = AUDIO_STREAM_WRITER;
|
||||
s_i2s = i2s_stream_init(&i2s_cfg);
|
||||
|
||||
audio_pipeline_register(s_pipe, s_http, "http");
|
||||
audio_pipeline_register(s_pipe, s_decoder, "dec");
|
||||
audio_pipeline_register(s_pipe, s_i2s, "i2s");
|
||||
|
||||
const char *link[] = { "http", "dec", "i2s" };
|
||||
audio_pipeline_link(s_pipe, link, 3);
|
||||
|
||||
ESP_LOGI(TAG, "pipeline built: http -> dec -> i2s");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t lisael_radio_play(const char *url)
|
||||
{
|
||||
if (!url) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
// Take over the shared codec from any other source.
|
||||
lisael_audio_stop_all();
|
||||
|
||||
esp_err_t err = build_pipeline();
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
if (s_playing) {
|
||||
audio_pipeline_stop(s_pipe);
|
||||
audio_pipeline_wait_for_stop(s_pipe);
|
||||
audio_pipeline_reset_ringbuffer(s_pipe);
|
||||
audio_pipeline_reset_elements(s_pipe);
|
||||
s_playing = false;
|
||||
}
|
||||
|
||||
audio_element_set_uri(s_http, url);
|
||||
err = audio_pipeline_run(s_pipe);
|
||||
if (err == ESP_OK) {
|
||||
s_playing = true;
|
||||
lisael_audio_set_source(LISAEL_AUDIO_RADIO);
|
||||
// Re-assert the child-safe volume on the shared codec.
|
||||
lisael_audio_set_volume(lisael_audio_get_volume());
|
||||
ESP_LOGI(TAG, "playing %s", url);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "pipeline run failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
void lisael_radio_stop(void)
|
||||
{
|
||||
if (s_pipe && s_playing) {
|
||||
audio_pipeline_stop(s_pipe);
|
||||
audio_pipeline_wait_for_stop(s_pipe);
|
||||
audio_pipeline_reset_ringbuffer(s_pipe);
|
||||
audio_pipeline_reset_elements(s_pipe);
|
||||
s_playing = false;
|
||||
ESP_LOGI(TAG, "stopped");
|
||||
}
|
||||
}
|
||||
|
||||
bool lisael_radio_is_playing(void)
|
||||
{
|
||||
return s_playing;
|
||||
}
|
||||
|
||||
#else // !LISAEL_HAVE_ADF — inspection build without ESP-ADF
|
||||
|
||||
esp_err_t lisael_radio_play(const char *url)
|
||||
{
|
||||
(void)url;
|
||||
ESP_LOGW(TAG, "ESP-ADF not present (ADF_PATH unset) — radio disabled");
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
void lisael_radio_stop(void) {}
|
||||
bool lisael_radio_is_playing(void) { return false; }
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
// Webradio streaming pipeline (ESP-ADF) for Lisael Box.
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Start streaming a webradio by URL. Builds (once, lazily) an ESP-ADF pipeline:
|
||||
// http_stream -> esp_decoder (auto MP3/AAC) -> i2s_stream (to ES8311)
|
||||
// and points it at `url`. Stops any other audio source first.
|
||||
//
|
||||
// Safe to call repeatedly to switch stations: the running pipeline is stopped,
|
||||
// the URI swapped, and the pipeline restarted.
|
||||
esp_err_t lisael_radio_play(const char *url);
|
||||
|
||||
// Stop the radio pipeline (keeps it allocated for fast restart).
|
||||
void lisael_radio_stop(void);
|
||||
|
||||
// True if the pipeline is currently running.
|
||||
bool lisael_radio_is_playing(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,222 @@
|
||||
// microSD sound-clip player (ESP-ADF fatfs pipeline) for Lisael Box.
|
||||
// See sd_player.h. Same BSP<->ADF I2S ownership caveat as radio_pipeline.c.
|
||||
|
||||
#include "audio/sd_player.h"
|
||||
#include "audio/audio_player.h"
|
||||
#include "lisael_config.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "bsp/esp-box-3.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
static const char *TAG = "sd";
|
||||
static bool s_mounted;
|
||||
|
||||
esp_err_t lisael_sd_mount(void)
|
||||
{
|
||||
if (s_mounted) {
|
||||
return ESP_OK;
|
||||
}
|
||||
esp_err_t err = bsp_sdcard_mount();
|
||||
if (err == ESP_OK) {
|
||||
s_mounted = true;
|
||||
ESP_LOGI(TAG, "microSD mounted at %s", BSP_SD_MOUNT_POINT);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "microSD mount failed: %s (no card?)", esp_err_to_name(err));
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
int lisael_sd_load_manifest(lisael_sound_t *out, int max)
|
||||
{
|
||||
if (!out || max <= 0) {
|
||||
return -1;
|
||||
}
|
||||
if (!s_mounted && lisael_sd_mount() != ESP_OK) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *path = BSP_SD_MOUNT_POINT "/sounds/manifest.json";
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) {
|
||||
ESP_LOGW(TAG, "no manifest at %s", path);
|
||||
return -1;
|
||||
}
|
||||
fseek(f, 0, SEEK_END);
|
||||
long sz = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
if (sz <= 0 || sz > 16 * 1024) { // sanity bound
|
||||
fclose(f);
|
||||
return -1;
|
||||
}
|
||||
char *buf = malloc((size_t)sz + 1);
|
||||
if (!buf) {
|
||||
fclose(f);
|
||||
return -1;
|
||||
}
|
||||
size_t rd = fread(buf, 1, (size_t)sz, f);
|
||||
buf[rd] = '\0';
|
||||
fclose(f);
|
||||
|
||||
cJSON *root = cJSON_Parse(buf);
|
||||
free(buf);
|
||||
if (!cJSON_IsArray(root)) {
|
||||
ESP_LOGW(TAG, "manifest is not a JSON array");
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int n = 0;
|
||||
cJSON *item;
|
||||
cJSON_ArrayForEach(item, root) {
|
||||
if (n >= max) {
|
||||
break;
|
||||
}
|
||||
cJSON *file = cJSON_GetObjectItem(item, "file");
|
||||
cJSON *title = cJSON_GetObjectItem(item, "title");
|
||||
cJSON *emoji = cJSON_GetObjectItem(item, "emoji");
|
||||
if (!cJSON_IsString(file)) {
|
||||
continue;
|
||||
}
|
||||
memset(&out[n], 0, sizeof(out[n]));
|
||||
strlcpy(out[n].file, file->valuestring, sizeof(out[n].file));
|
||||
strlcpy(out[n].title,
|
||||
cJSON_IsString(title) ? title->valuestring : file->valuestring,
|
||||
sizeof(out[n].title));
|
||||
strlcpy(out[n].emoji,
|
||||
cJSON_IsString(emoji) ? emoji->valuestring : "\U0001F50A", // speaker
|
||||
sizeof(out[n].emoji));
|
||||
n++;
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
ESP_LOGI(TAG, "manifest: %d sounds", n);
|
||||
return n;
|
||||
}
|
||||
|
||||
esp_err_t lisael_sd_play_sound(const char *basename)
|
||||
{
|
||||
char abs[128];
|
||||
snprintf(abs, sizeof(abs), "%s/sounds/%s", BSP_SD_MOUNT_POINT, basename);
|
||||
return lisael_sd_play_file(abs);
|
||||
}
|
||||
|
||||
// --- ADF-guarded pipeline ---------------------------------------------------
|
||||
#if defined(__has_include)
|
||||
# if __has_include("audio_pipeline.h")
|
||||
# define LISAEL_HAVE_ADF 1
|
||||
# endif
|
||||
#endif
|
||||
|
||||
#ifdef LISAEL_HAVE_ADF
|
||||
|
||||
#include "audio_pipeline.h"
|
||||
#include "audio_element.h"
|
||||
#include "fatfs_stream.h"
|
||||
#include "mp3_decoder.h"
|
||||
#include "i2s_stream.h"
|
||||
|
||||
static audio_pipeline_handle_t s_pipe;
|
||||
static audio_element_handle_t s_fatfs, s_mp3, s_i2s;
|
||||
static bool s_playing;
|
||||
|
||||
static esp_err_t build_pipeline(void)
|
||||
{
|
||||
if (s_pipe) {
|
||||
return ESP_OK;
|
||||
}
|
||||
audio_pipeline_cfg_t pcfg = DEFAULT_AUDIO_PIPELINE_CONFIG();
|
||||
s_pipe = audio_pipeline_init(&pcfg);
|
||||
if (!s_pipe) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
fatfs_stream_cfg_t fs_cfg = FATFS_STREAM_CFG_DEFAULT();
|
||||
fs_cfg.type = AUDIO_STREAM_READER;
|
||||
s_fatfs = fatfs_stream_init(&fs_cfg);
|
||||
|
||||
mp3_decoder_cfg_t mp3_cfg = DEFAULT_MP3_DECODER_CONFIG();
|
||||
s_mp3 = mp3_decoder_init(&mp3_cfg);
|
||||
|
||||
// TODO(hardware): match i2s port/pins to the BOX-3 BSP — same caveat as
|
||||
// radio_pipeline.c (ADF vs BSP I2S ownership).
|
||||
i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT();
|
||||
i2s_cfg.type = AUDIO_STREAM_WRITER;
|
||||
s_i2s = i2s_stream_init(&i2s_cfg);
|
||||
|
||||
audio_pipeline_register(s_pipe, s_fatfs, "file");
|
||||
audio_pipeline_register(s_pipe, s_mp3, "mp3");
|
||||
audio_pipeline_register(s_pipe, s_i2s, "i2s");
|
||||
|
||||
const char *link[] = { "file", "mp3", "i2s" };
|
||||
audio_pipeline_link(s_pipe, link, 3);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t lisael_sd_play_file(const char *abs_path)
|
||||
{
|
||||
if (!abs_path) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (!s_mounted && lisael_sd_mount() != ESP_OK) {
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
lisael_audio_stop_all();
|
||||
|
||||
esp_err_t err = build_pipeline();
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
if (s_playing) {
|
||||
audio_pipeline_stop(s_pipe);
|
||||
audio_pipeline_wait_for_stop(s_pipe);
|
||||
audio_pipeline_reset_ringbuffer(s_pipe);
|
||||
audio_pipeline_reset_elements(s_pipe);
|
||||
s_playing = false;
|
||||
}
|
||||
|
||||
audio_element_set_uri(s_fatfs, abs_path);
|
||||
err = audio_pipeline_run(s_pipe);
|
||||
if (err == ESP_OK) {
|
||||
s_playing = true;
|
||||
lisael_audio_set_source(LISAEL_AUDIO_SD);
|
||||
lisael_audio_set_volume(lisael_audio_get_volume());
|
||||
ESP_LOGI(TAG, "playing %s", abs_path);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "run failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
void lisael_sd_stop(void)
|
||||
{
|
||||
if (s_pipe && s_playing) {
|
||||
audio_pipeline_stop(s_pipe);
|
||||
audio_pipeline_wait_for_stop(s_pipe);
|
||||
audio_pipeline_reset_ringbuffer(s_pipe);
|
||||
audio_pipeline_reset_elements(s_pipe);
|
||||
s_playing = false;
|
||||
ESP_LOGI(TAG, "stopped");
|
||||
}
|
||||
}
|
||||
|
||||
bool lisael_sd_is_playing(void)
|
||||
{
|
||||
return s_playing;
|
||||
}
|
||||
|
||||
#else // !LISAEL_HAVE_ADF
|
||||
|
||||
esp_err_t lisael_sd_play_file(const char *abs_path)
|
||||
{
|
||||
(void)abs_path;
|
||||
ESP_LOGW(TAG, "ESP-ADF not present — SD playback disabled");
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
void lisael_sd_stop(void) {}
|
||||
bool lisael_sd_is_playing(void) { return false; }
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,45 @@
|
||||
// microSD sound-clip player (ESP-ADF fatfs pipeline) for Lisael Box.
|
||||
//
|
||||
// Plays a single MP3 file from the microSD card:
|
||||
// fatfs_stream -> mp3_decoder -> i2s_stream (to ES8311)
|
||||
//
|
||||
// Also parses /sdcard/sounds/manifest.json into a small in-RAM list used by the
|
||||
// "boite a sons" UI grid.
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define LISAEL_SD_MAX_SOUNDS 32
|
||||
|
||||
typedef struct {
|
||||
char file[64]; // basename, e.g. "miaou.mp3" (under /sdcard/sounds/)
|
||||
char title[48]; // display title, e.g. "Le chat"
|
||||
char emoji[8]; // single emoji used as the button icon, e.g. "🐱"
|
||||
} lisael_sound_t;
|
||||
|
||||
// Mount the microSD card via the BSP (FATFS). Idempotent.
|
||||
esp_err_t lisael_sd_mount(void);
|
||||
|
||||
// Parse /sdcard/sounds/manifest.json. Returns count placed into `out` (up to
|
||||
// LISAEL_SD_MAX_SOUNDS). Returns -1 on error (no card / no manifest).
|
||||
int lisael_sd_load_manifest(lisael_sound_t *out, int max);
|
||||
|
||||
// Play an absolute SD path (e.g. "/sdcard/sounds/miaou.mp3"). Stops other audio.
|
||||
esp_err_t lisael_sd_play_file(const char *abs_path);
|
||||
|
||||
// Convenience: play a basename under /sdcard/sounds/.
|
||||
esp_err_t lisael_sd_play_sound(const char *basename);
|
||||
|
||||
// Stop the SD pipeline.
|
||||
void lisael_sd_stop(void);
|
||||
|
||||
bool lisael_sd_is_playing(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,49 @@
|
||||
// SNTP time sync for Lisael Box.
|
||||
//
|
||||
// Uses the IDF esp_sntp wrapper (esp_netif_sntp_*). The timezone is set with
|
||||
// the POSIX TZ string so that localtime() yields Europe/Paris wall-clock time
|
||||
// with automatic DST — no setlocale / no tz database needed.
|
||||
|
||||
#include "net/sntp_time.h"
|
||||
#include "lisael_config.h"
|
||||
|
||||
#include <time.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_netif_sntp.h"
|
||||
#include "esp_sntp.h"
|
||||
|
||||
static const char *TAG = "sntp";
|
||||
static volatile bool s_time_valid;
|
||||
|
||||
static void on_time_sync(struct timeval *tv)
|
||||
{
|
||||
(void)tv;
|
||||
s_time_valid = true;
|
||||
ESP_LOGI(TAG, "time synchronised");
|
||||
}
|
||||
|
||||
esp_err_t lisael_sntp_start(void)
|
||||
{
|
||||
// Apply Europe/Paris timezone for all localtime() calls.
|
||||
setenv("TZ", LISAEL_TZ, 1);
|
||||
tzset();
|
||||
|
||||
esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG("pool.ntp.org");
|
||||
config.sync_cb = on_time_sync;
|
||||
config.start = true;
|
||||
|
||||
esp_err_t err = esp_netif_sntp_init(&config);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_netif_sntp_init failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
ESP_LOGI(TAG, "SNTP started (pool.ntp.org, TZ=%s)", LISAEL_TZ);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool lisael_time_is_valid(void)
|
||||
{
|
||||
return s_time_valid;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// SNTP time sync + Europe/Paris timezone for Lisael Box.
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Configure the timezone (LISAEL_TZ) and start the SNTP client.
|
||||
// Must be called after Wi-Fi is connected. Non-blocking.
|
||||
esp_err_t lisael_sntp_start(void);
|
||||
|
||||
// True once the clock has been set at least once by SNTP.
|
||||
bool lisael_time_is_valid(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,211 @@
|
||||
// open-meteo.com weather fetch for Lisael Box.
|
||||
//
|
||||
// open-meteo is a free, key-less HTTPS forecast API. We request only the
|
||||
// "current" temperature + weather_code for a fixed lat/lon, parse the small
|
||||
// JSON reply with cJSON, and publish a thread-safe snapshot. A FreeRTOS task
|
||||
// refreshes periodically and tolerates network failures gracefully (snapshot
|
||||
// stays .valid=false / the UI shows "—").
|
||||
|
||||
#include "net/weather.h"
|
||||
#include "lisael_config.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_http_client.h"
|
||||
#include "esp_crt_bundle.h"
|
||||
#include "nvs.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
static const char *TAG = "weather";
|
||||
|
||||
#define WEATHER_HTTP_BUF 2048
|
||||
|
||||
static lisael_weather_t s_snapshot;
|
||||
static SemaphoreHandle_t s_lock;
|
||||
static void (*s_on_update)(const lisael_weather_t *);
|
||||
|
||||
// --- WMO weather code mapping (subset, child-friendly) ----------------------
|
||||
// https://open-meteo.com/en/docs -> "WMO Weather interpretation codes"
|
||||
const char *lisael_weather_code_label(int code)
|
||||
{
|
||||
switch (code) {
|
||||
case 0: return "Ciel clair";
|
||||
case 1: case 2: case 3: return "Nuageux";
|
||||
case 45: case 48: return "Brouillard";
|
||||
case 51: case 53: case 55: return "Bruine";
|
||||
case 61: case 63: case 65: return "Pluie";
|
||||
case 66: case 67: return "Pluie verglacante";
|
||||
case 71: case 73: case 75: return "Neige";
|
||||
case 77: return "Grains de neige";
|
||||
case 80: case 81: case 82: return "Averses";
|
||||
case 85: case 86: return "Averses de neige";
|
||||
case 95: return "Orage";
|
||||
case 96: case 99: return "Orage et grele";
|
||||
default: return "—";
|
||||
}
|
||||
}
|
||||
|
||||
const char *lisael_weather_code_emoji(int code)
|
||||
{
|
||||
switch (code) {
|
||||
case 0: return "☀️"; // sun
|
||||
case 1: case 2: case 3: return "⛅"; // sun behind cloud
|
||||
case 45: case 48: return "\U0001F32B️"; // fog
|
||||
case 51: case 53: case 55: return "\U0001F326️"; // drizzle
|
||||
case 61: case 63: case 65: return "\U0001F327️"; // rain
|
||||
case 66: case 67: return "\U0001F328️"; // sleet
|
||||
case 71: case 73: case 75:
|
||||
case 77: case 85: case 86: return "❄️"; // snow
|
||||
case 80: case 81: case 82: return "\U0001F327️"; // showers
|
||||
case 95: case 96: case 99: return "⛈️"; // thunderstorm
|
||||
default: return "❓"; // question mark
|
||||
}
|
||||
}
|
||||
|
||||
static void load_coords(char *lat, size_t lat_len, char *lon, size_t lon_len)
|
||||
{
|
||||
strlcpy(lat, CONFIG_LISAEL_WEATHER_LAT, lat_len);
|
||||
strlcpy(lon, CONFIG_LISAEL_WEATHER_LON, lon_len);
|
||||
|
||||
nvs_handle_t nvs;
|
||||
if (nvs_open(LISAEL_NVS_NS, NVS_READONLY, &nvs) == ESP_OK) {
|
||||
size_t len = lat_len;
|
||||
if (nvs_get_str(nvs, LISAEL_NVS_LAT, lat, &len) == ESP_OK) {
|
||||
len = lon_len;
|
||||
nvs_get_str(nvs, LISAEL_NVS_LON, lon, &len);
|
||||
}
|
||||
nvs_close(nvs);
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP event handler accumulating the body into a caller-supplied buffer.
|
||||
typedef struct {
|
||||
char *buf;
|
||||
int len;
|
||||
int cap;
|
||||
} http_accum_t;
|
||||
|
||||
static esp_err_t http_event(esp_http_client_event_t *evt)
|
||||
{
|
||||
if (evt->event_id == HTTP_EVENT_ON_DATA) {
|
||||
http_accum_t *acc = (http_accum_t *)evt->user_data;
|
||||
int copy = evt->data_len;
|
||||
if (acc->len + copy >= acc->cap) {
|
||||
copy = acc->cap - acc->len - 1;
|
||||
}
|
||||
if (copy > 0) {
|
||||
memcpy(acc->buf + acc->len, evt->data, copy);
|
||||
acc->len += copy;
|
||||
acc->buf[acc->len] = '\0';
|
||||
}
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static bool fetch_once(const char *lat, const char *lon, lisael_weather_t *out)
|
||||
{
|
||||
char url[256];
|
||||
snprintf(url, sizeof(url),
|
||||
"https://api.open-meteo.com/v1/forecast"
|
||||
"?latitude=%s&longitude=%s¤t=temperature_2m,weather_code",
|
||||
lat, lon);
|
||||
|
||||
char *body = calloc(1, WEATHER_HTTP_BUF);
|
||||
if (!body) {
|
||||
return false;
|
||||
}
|
||||
http_accum_t acc = { .buf = body, .len = 0, .cap = WEATHER_HTTP_BUF };
|
||||
|
||||
esp_http_client_config_t config = {
|
||||
.url = url,
|
||||
.event_handler = http_event,
|
||||
.user_data = &acc,
|
||||
.timeout_ms = 8000,
|
||||
.crt_bundle_attach = esp_crt_bundle_attach, // uses the CA bundle
|
||||
};
|
||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||
bool ok = false;
|
||||
|
||||
esp_err_t err = esp_http_client_perform(client);
|
||||
int status = esp_http_client_get_status_code(client);
|
||||
if (err == ESP_OK && status == 200) {
|
||||
cJSON *root = cJSON_Parse(body);
|
||||
cJSON *current = cJSON_GetObjectItem(root, "current");
|
||||
cJSON *temp = current ? cJSON_GetObjectItem(current, "temperature_2m") : NULL;
|
||||
cJSON *code = current ? cJSON_GetObjectItem(current, "weather_code") : NULL;
|
||||
if (cJSON_IsNumber(temp) && cJSON_IsNumber(code)) {
|
||||
out->temperature_c = (float)temp->valuedouble;
|
||||
out->weather_code = code->valueint;
|
||||
out->valid = true;
|
||||
ok = true;
|
||||
ESP_LOGI(TAG, "%.1f degC, code %d", out->temperature_c, out->weather_code);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "unexpected JSON shape");
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "fetch failed err=%s status=%d", esp_err_to_name(err), status);
|
||||
}
|
||||
|
||||
esp_http_client_cleanup(client);
|
||||
free(body);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void weather_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
char lat[24], lon[24];
|
||||
load_coords(lat, sizeof(lat), lon, sizeof(lon));
|
||||
|
||||
const TickType_t period = pdMS_TO_TICKS(
|
||||
(uint32_t)LISAEL_WEATHER_REFRESH_MIN * 60U * 1000U);
|
||||
|
||||
for (;;) {
|
||||
lisael_weather_t fresh = { 0 };
|
||||
strlcpy(fresh.city, CONFIG_LISAEL_WEATHER_CITY, sizeof(fresh.city));
|
||||
fetch_once(lat, lon, &fresh); // leaves .valid=false on failure
|
||||
|
||||
if (xSemaphoreTake(s_lock, portMAX_DELAY) == pdTRUE) {
|
||||
s_snapshot = fresh;
|
||||
xSemaphoreGive(s_lock);
|
||||
}
|
||||
if (s_on_update) {
|
||||
s_on_update(&fresh);
|
||||
}
|
||||
vTaskDelay(period);
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t lisael_weather_start(void (*on_update)(const lisael_weather_t *))
|
||||
{
|
||||
s_on_update = on_update;
|
||||
s_lock = xSemaphoreCreateMutex();
|
||||
if (!s_lock) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
strlcpy(s_snapshot.city, CONFIG_LISAEL_WEATHER_CITY, sizeof(s_snapshot.city));
|
||||
|
||||
// TLS + HTTP + JSON -> needs a generous stack.
|
||||
BaseType_t ok = xTaskCreatePinnedToCore(
|
||||
weather_task, "weather", 8192, NULL, 4, NULL, tskNO_AFFINITY);
|
||||
return ok == pdPASS ? ESP_OK : ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
void lisael_weather_get(lisael_weather_t *out)
|
||||
{
|
||||
if (!out) {
|
||||
return;
|
||||
}
|
||||
if (s_lock && xSemaphoreTake(s_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
*out = s_snapshot;
|
||||
xSemaphoreGive(s_lock);
|
||||
} else {
|
||||
memset(out, 0, sizeof(*out));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// open-meteo.com weather fetch for Lisael Box.
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Latest weather snapshot. `valid` is false until the first successful fetch
|
||||
// (or after a network failure resets it). The UI shows "—" when !valid.
|
||||
typedef struct {
|
||||
bool valid;
|
||||
float temperature_c; // current temperature, degrees Celsius
|
||||
int weather_code; // WMO weather interpretation code
|
||||
char city[32]; // label (from config), for display
|
||||
} lisael_weather_t;
|
||||
|
||||
// Start the periodic weather task. Fetches immediately once Wi-Fi is up, then
|
||||
// every CONFIG_LISAEL_WEATHER_REFRESH_MIN minutes. Latitude/longitude come from
|
||||
// NVS (LISAEL_NVS_LAT/_LON) or the menuconfig defaults.
|
||||
//
|
||||
// `on_update` is invoked from the weather task each time a fetch completes
|
||||
// (success or failure — check .valid). May be NULL.
|
||||
esp_err_t lisael_weather_start(void (*on_update)(const lisael_weather_t *));
|
||||
|
||||
// Thread-safe copy of the latest snapshot.
|
||||
void lisael_weather_get(lisael_weather_t *out);
|
||||
|
||||
// Map a WMO weather code to a short FR label and an emoji/symbol used as icon.
|
||||
// Both pointers are static strings — do not free.
|
||||
const char *lisael_weather_code_label(int code);
|
||||
const char *lisael_weather_code_emoji(int code);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
// Wi-Fi station bring-up for Lisael Box.
|
||||
//
|
||||
// Standard ESP-IDF 5.x STA pattern: esp_netif + default event loop +
|
||||
// auto-reconnect on disconnect. Credentials come from NVS first, then the
|
||||
// menuconfig defaults. nvs_flash_init() is the caller's responsibility.
|
||||
|
||||
#include "net/wifi.h"
|
||||
#include "lisael_config.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_log.h"
|
||||
#include "nvs.h"
|
||||
|
||||
static const char *TAG = "wifi";
|
||||
|
||||
#define WIFI_CONNECTED_BIT BIT0
|
||||
#define WIFI_FAIL_BIT BIT1
|
||||
#define WIFI_MAX_RETRY 8
|
||||
|
||||
static EventGroupHandle_t s_wifi_events;
|
||||
static int s_retry_count;
|
||||
static volatile bool s_connected;
|
||||
|
||||
static void load_credentials(char *ssid, size_t ssid_len,
|
||||
char *pass, size_t pass_len)
|
||||
{
|
||||
// Defaults from menuconfig.
|
||||
strlcpy(ssid, CONFIG_LISAEL_WIFI_SSID, ssid_len);
|
||||
strlcpy(pass, CONFIG_LISAEL_WIFI_PASSWORD, pass_len);
|
||||
|
||||
// Override with NVS-provisioned values if present.
|
||||
nvs_handle_t nvs;
|
||||
if (nvs_open(LISAEL_NVS_NS, NVS_READONLY, &nvs) != ESP_OK) {
|
||||
ESP_LOGI(TAG, "no NVS namespace yet, using menuconfig credentials");
|
||||
return;
|
||||
}
|
||||
size_t len = ssid_len;
|
||||
if (nvs_get_str(nvs, LISAEL_NVS_WIFI_SSID, ssid, &len) == ESP_OK) {
|
||||
len = pass_len;
|
||||
nvs_get_str(nvs, LISAEL_NVS_WIFI_PASS, pass, &len);
|
||||
ESP_LOGI(TAG, "using NVS-provisioned Wi-Fi credentials");
|
||||
}
|
||||
nvs_close(nvs);
|
||||
}
|
||||
|
||||
static void on_wifi_event(void *arg, esp_event_base_t base,
|
||||
int32_t id, void *data)
|
||||
{
|
||||
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) {
|
||||
esp_wifi_connect();
|
||||
} else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
s_connected = false;
|
||||
if (s_retry_count < WIFI_MAX_RETRY) {
|
||||
s_retry_count++;
|
||||
ESP_LOGW(TAG, "disconnected, retry %d/%d", s_retry_count, WIFI_MAX_RETRY);
|
||||
esp_wifi_connect();
|
||||
} else {
|
||||
// Keep trying slowly forever — a kid's box should self-heal once the
|
||||
// router comes back. Reset the counter and reconnect after a pause.
|
||||
ESP_LOGE(TAG, "max retries reached; will keep retrying");
|
||||
xEventGroupSetBits(s_wifi_events, WIFI_FAIL_BIT);
|
||||
s_retry_count = 0;
|
||||
esp_wifi_connect();
|
||||
}
|
||||
} else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
|
||||
ip_event_got_ip_t *event = (ip_event_got_ip_t *)data;
|
||||
ESP_LOGI(TAG, "got IP " IPSTR, IP2STR(&event->ip_info.ip));
|
||||
s_retry_count = 0;
|
||||
s_connected = true;
|
||||
xEventGroupSetBits(s_wifi_events, WIFI_CONNECTED_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t lisael_wifi_start(void)
|
||||
{
|
||||
s_wifi_events = xEventGroupCreate();
|
||||
if (!s_wifi_events) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
esp_netif_create_default_wifi_sta();
|
||||
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(
|
||||
WIFI_EVENT, ESP_EVENT_ANY_ID, &on_wifi_event, NULL, NULL));
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(
|
||||
IP_EVENT, IP_EVENT_STA_GOT_IP, &on_wifi_event, NULL, NULL));
|
||||
|
||||
wifi_config_t wifi_config = {
|
||||
.sta = {
|
||||
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
|
||||
.pmf_cfg = { .capable = true, .required = false },
|
||||
},
|
||||
};
|
||||
load_credentials((char *)wifi_config.sta.ssid, sizeof(wifi_config.sta.ssid),
|
||||
(char *)wifi_config.sta.password, sizeof(wifi_config.sta.password));
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
ESP_LOGI(TAG, "Wi-Fi started, SSID=\"%s\"", wifi_config.sta.ssid);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool lisael_wifi_wait_connected(uint32_t timeout_ms)
|
||||
{
|
||||
if (!s_wifi_events) {
|
||||
return false;
|
||||
}
|
||||
EventBits_t bits = xEventGroupWaitBits(
|
||||
s_wifi_events, WIFI_CONNECTED_BIT, pdFALSE, pdFALSE,
|
||||
pdMS_TO_TICKS(timeout_ms));
|
||||
return (bits & WIFI_CONNECTED_BIT) != 0;
|
||||
}
|
||||
|
||||
bool lisael_wifi_is_connected(void)
|
||||
{
|
||||
return s_connected;
|
||||
}
|
||||
|
||||
esp_err_t lisael_wifi_save_credentials(const char *ssid, const char *pass)
|
||||
{
|
||||
nvs_handle_t nvs;
|
||||
esp_err_t err = nvs_open(LISAEL_NVS_NS, NVS_READWRITE, &nvs);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
err = nvs_set_str(nvs, LISAEL_NVS_WIFI_SSID, ssid);
|
||||
if (err == ESP_OK) {
|
||||
err = nvs_set_str(nvs, LISAEL_NVS_WIFI_PASS, pass);
|
||||
}
|
||||
if (err == ESP_OK) {
|
||||
err = nvs_commit(nvs);
|
||||
}
|
||||
nvs_close(nvs);
|
||||
return err;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Wi-Fi station bring-up for Lisael Box.
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Initialise NVS-backed Wi-Fi station and start connecting.
|
||||
//
|
||||
// Credentials are resolved in this order:
|
||||
// 1. NVS keys (LISAEL_NVS_WIFI_SSID / _PASS) if present — runtime provisioned.
|
||||
// 2. menuconfig defaults (CONFIG_LISAEL_WIFI_SSID / _PASSWORD).
|
||||
//
|
||||
// Non-blocking: connection happens in the background. Use
|
||||
// lisael_wifi_wait_connected() to block until associated (or timeout).
|
||||
//
|
||||
// nvs_flash_init() MUST already have been called by app_main.
|
||||
esp_err_t lisael_wifi_start(void);
|
||||
|
||||
// Block until the station obtains an IP, or until timeout_ms elapses.
|
||||
// Returns true if connected.
|
||||
bool lisael_wifi_wait_connected(uint32_t timeout_ms);
|
||||
|
||||
// Current association state (IP acquired).
|
||||
bool lisael_wifi_is_connected(void);
|
||||
|
||||
// Persist new credentials to NVS (used by an eventual provisioning UI/console).
|
||||
esp_err_t lisael_wifi_save_credentials(const char *ssid, const char *pass);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user