The reading game referenced icons by SD file path, so LVGL re-read and decoded
each .bin from the slow microSD on every draw band (16-line buffer) and every
frame, with no image cache — hence the laggy display. Read each .bin once into
PSRAM and hand LVGL an in-RAM image descriptor: no per-band SD reads, and
repeated words are instant.
Webradio was silent: the streams are stereo 44.1 kHz MP3, and opening the
ES8311 codec in stereo failed ("i2s_std_set_slot: dma descriptor") because the
stereo I2S DMA no longer fits the tight internal RAM — the podcast path works
only because its files are mono. Downmix the decoded stereo PCM to mono and
open the codec at one channel. Also switch the stream URLs to plain HTTP: the
HTTPS handshake intermittently failed to allocate AES memory, and these mounts
serve identical audio over http with no redirect. Add SuperLoustic (3 stations).
Match Histoires and home: replace the column of station buttons with the
Lunii-style horizontal carousel (one big station card per slide, radio icon +
name, swipe/arrows, a dot per station, tap = play). Keep the Stop button and
status line at the bottom.
The 7 mode tiles overflowed the grid and scrolled. Replace the wrapping tile
grid with the same Lunii-style horizontal carousel as Histoires: one big
activity tile per full-width slide, snap-scroll swipe, tap arrows, a dot per
activity. Header (date/time/weather) unchanged. Drop the now-unused make_tile.
Replace the paged 2x2 tile grid (cramped covers, multi-page scroll) with a
Lunii/Merlin-style horizontal carousel: one large 140 px cover per full-width
slide, snap-scroll swipe, tap arrows, a dot per show, big show name. Levels 2
(episode list) and 3 (player) unchanged.
The additive refresh accumulated episodes without bound (Oli reached 13).
Tag each manifest episode with a source (auto/pick/upload); the refresh now
prunes "auto" episodes that aged out of the feed's latest N while keeping
hand-picked and uploaded ones. Feeds default to N=5.
Episodes are 18-24 MB and the box's link tops out near 100 KB/s, so
full-size pushes time out. Transcode to mono 48 kbps MP3 (a few MB,
plenty for spoken-word kids podcasts); the _a48 tag gives a fresh name
so the box re-pulls the light version. Ack /register immediately and
push the delta on a background thread (one batch per box) so the box's
short register timeout never trips.
The PUT handler looped forever on HTTPD_SOCK_ERR_TIMEOUT, wedging the
single httpd worker when a push was cut off mid-transfer. Bound it to a
few idle waits and drop truncated files so they never block a re-push.
Receive into a 16 KB heap buffer with a matching stdio buffer. Register
with Tower last (after the first weather TLS fetch) so the content push
it triggers does not contend for memory at boot.
- lire.c: the image rounds (image->word, word->image) now only draw words
that actually have an SD picture; abstract words without an emoji stay in
the listen->word round (text choices). No more blank answer tiles.
- Fonts: regenerated Fredoka 18/24/30 with U+0152-0153 added so "œ" renders
(cœur/sœur/œuf/œil); all other French accents were already covered by the
Latin-1 range. Montserrat-14 fallback re-applied (LV_SYMBOL_* glyphs).
Three follow-ups after the reading game landed.
- WiFi: a "Réglages" home tile (gear) opens LISAEL_MODE_WIFI which forces
the SoftAP + captive portal up on demand (lisael_ui_goto starts them;
the screen's "Fermer" button tears them down), so the provisioning
portal can be tested/used without waiting for the station to fail.
- Lire: the content generator now also takes words that have no emoji
(optional img in the regex) -> 366 words instead of 286 (the extra
~80 are abstract words: text + spoken audio, no picture).
- Cleanup: removed 21 baked word icons that became dead weight once the
reading game started loading its pictures from the SD (~315 KB flash),
plus their declarations and gen_icons.py specs.
Context: the box needs a learn-to-read game for Lisael (6). Baking
hundreds of word pictures into the firmware does not fit in flash, so
content must live on the SD card and grow without reflashing.
Approach: words, Twemoji .bin icons and pre-generated French TTS audio
all live on the SD under /sdcard/lire/; the box loads them at runtime
(LVGL stdio FS for icons, MP3 player for audio). A small LAN file
server lets a computer push that content over Wi-Fi (no USB-MSC).
Changes:
- modes/lire.c: 3 round types (listen->word, image->word, word->image),
reads /sdcard/lire/words.json into a PSRAM array, loads per-word
icons from SD, plays the spoken word; score + tap-to-replay.
- net/file_server.c/.h: POST /put?name=... on :8080 writes the body to
/sdcard/<name>; started by net_task once the station connects.
- home tile "Lire" + LISAEL_MODE_LIRE routing; 20 extra Twemoji icons.
- tools/gen_lire_content.py + push_to_box.sh: content pipeline
(lecture.data.js -> icons + TTS + words.json -> Wi-Fi push).
Impact: 286 words (vs 12), expandable over Wi-Fi; reusable file-drop
for future content (podcasts, more words) without the MSC dance.
Fetches Radio France RSS feeds to build show cover images (160x160
RGB565 LVGL .bin) and writes a v2 manifest.json grouped by show,
preserving episode titles from the existing flat v1 manifest.
Runs with the IDF Python venv (pillow + pypng already installed).
Replace the flat "Sons" tile and podcasts.c RSS stub with a
3-level LVGL screen that browses shows stored on the SD card.
- Add modes/histoires.c: paged 2x2 cover grid (level 1),
episode list per show (level 2), full-screen player with
prev/toggle/next transport (level 3).
- Wire lisael_screen_histoires() into ui.c build_screen().
- Remove LISAEL_MODE_SOUNDBOX from the enum and drop the
"Sons" tile from home.c (5 tiles remain).
- Delete modes/soundbox.c (git rm) and modes/podcasts.c (rm).
- Update CMakeLists.txt: remove soundbox.c / podcasts.c,
add histoires.c.
Playback dispatches by extension: .m4a -> aac_player,
otherwise -> sd_player (MP3). Covers are RGB565 .bin files
loaded via the LVGL stdio FS driver; a coloured initial
fallback is shown when no cover is available.
Register audio/podcast_manifest.c in the SRCS list so the new JSON
manifest parser is compiled. Enable CONFIG_LV_USE_FS_STDIO (letter 'S')
in sdkconfig so LVGL can open cover images via fopen("/sdcard/...").
Pure C parser (cJSON) for /sdcard/podcasts/manifest.json v2
format (shows grouped with cover + episodes list). Skips shows
with zero episodes. SD loader guarded by #ifndef LISAEL_HOST_TEST.
Host test compiles and passes on Mac without IDF toolchain.
Context:
The README is the primary usable deliverable: how to install
ESP-IDF + ESP-ADF, build, flash, and prepare the microSD card, plus an
honest list of what is unverified.
Changes:
- README.md: prerequisites (ADF_PATH), build/flash, provisioning,
module table, child-safety notes, and a thorough "A verifier sur le
materiel" section (build, BSP<->ADF I2S, radio URLs, emoji fonts...)
- sdcard/README.md + sdcard/sounds/manifest.json example
Impact:
A reader can stand the project up and knows exactly what still needs
validation on real hardware.
Context:
The box is a touch device for a young child: big icon buttons, no
reading required, gentle visuals, no game with a losing state.
Approach:
An LVGL 9 shell (ui/) with a mode router that lazily builds and caches
each screen and slides between them. Shared pastel styles, an
icon-first home with French date in letters (custom fr_date, no
setlocale), live time/weather/chip-temp hooks that take the display
lock. Six modes: home, radio, soundbox, calm (breathing bubble),
clock/night-light, games (Memory + Compter implemented, Coloriage a
documented stub). app_main sequences NVS -> BSP -> codec -> SD -> UI
and spawns the net and tick tasks.
Changes:
- ui/ui, ui/fr_date, ui/icons
- modes/{home,radio,soundbox,calm,clock,games}, modes/radios.h
- app_main.c boot sequence + tasks
Impact:
Full feature surface present; emoji glyphs need an embedded font and
several BSP/LVGL assumptions need a real board (see README).
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.
Context:
New firmware "Lisael Box" targeting the ESP32-S3-BOX-3 (8MB PSRAM,
16MB flash, LCD+touch, ES8311 codec, microSD). Establish the ESP-IDF
5.4/5.5 build skeleton before adding modules.
Approach:
Standard IDF project layout with a single big factory app (4MB) since
the binary links the esp-box BSP, LVGL 9 and ESP-ADF. ESP-ADF is not a
managed component, so the top-level CMake injects $ADF_PATH/components
via EXTRA_COMPONENT_DIRS and warns clearly when ADF_PATH is unset.
Changes:
- top-level CMakeLists with ADF_PATH wiring
- partitions.csv (16MB: factory 4MB + FAT storage)
- sdkconfig.defaults (+ .esp32s3: octal PSRAM, 16MB, 240MHz, LVGL/PSRAM)
- main/CMakeLists, idf_component.yml (esp-box-3 ^3.2, built-in json)
- Kconfig.projbuild (Wi-Fi, weather lat/lon, child volume cap)
- lisael_config.h shared constants, .gitignore
Impact:
Buildable skeleton; modules added in following commits. Not yet
compiled (no IDF/ADF/board available) - see README.