New "Labyrinthe" game: guide a ball to the star by tilting the
box, read from the on-board ICM42670 accelerometer (BSP I2C bus,
added to REQUIRES). A perfect maze is carved by iterative
backtracking on a 12x8 grid, so there is one path with branches
and dead ends; only a 6x4 window is shown and the view scrolls to
follow the ball. The whole world is drawn into a single RGB565
PSRAM canvas (not one object per wall) and scrolled — layer-free,
keeps the render task light. The ball bounces off walls and edges
with speed-proportional restitution. Tilt axes calibrated on the
real board. Wired as the 5th tile in the games carousel; reuses
the festive bravo on win.
Reuse the routine bravo celebration in the games: the win toast
now shows a spinning, bouncing star (layer-free image rotation +
translate_y) and plays a sound. Memory win gets the full festive
bravo.mp3 with a longer toast; Compter plays the short "ok" cue
on each correct answer (it fires often, so the long voice would
be too much).
Make the end-of-routine "Bravo !" lively: the star now spins
(continuous full turn via lv_image_set_rotation) and bounces
(translate_y ping-pong). Both use layer-free paths — image
rotation transforms against the flush band, translate is not a
layer-forcing style — so the render task stays light (no repeat
of the full-screen-layer task_wdt). Auto-return stretched to
3.6 s so the spin shows and the festive cue finishes.
Tower: seed a more festive bravo.mp3 line ("Youpi, tu as
réussi ! Tu es super !").
Memory: 18 cards (9 pairs) over a 3-page snap carousel of 6, all
in play at once, plus an elapsed-time chrono and a best-time high
score persisted to NVS (new "mem_best" key) shown as "Record !".
Compter: now an addition — two apple groups (1..10 each) with a
"+" sign; tap the total (up to 20).
Coloriage: replaces the stub with a tap-to-fill colouring book.
Line-art pictures (house, flower, fish, car, boat) are rasterised
into an RGB565 canvas in PSRAM; a tap scanline flood-fills the
region (iterative, PSRAM work stack, no layer). Palette of 6
colours + white eraser, prev/next picture arrows, Effacer.
Re-pin the Wi-Fi RX/TX buffer counts and AMPDU window that the
gitignored sdkconfig held, so a fresh checkout reconfigures with
the internal-RAM budget that avoids esp_wifi_init NO_MEM.
The end-of-routine "Bravo !" screen wedged the LVGL render task
(task_wdt loop) on this RAM-tight box. Root cause via decoded
backtrace: LV_SCR_LOAD_ANIM_FADE_IN applies opacity to the whole
screen and the star's transform_scale animation both force a
320x240 intermediate layer that the 16-line flush buffer cannot
allocate, hanging lv_draw_layer_alloc_buf.
Use a MOVE_LEFT slide like every other screen and a static star.
Arrow prev/next in home and games carousels now use modular
arithmetic ((cur + dir + n) % n) instead of clamping, so
pressing left on the first slide jumps to the last and vice
versa. Swipe still clamps at the edges (LVGL snap resists a
clean programmatic jump mid-gesture); arrow-wrap is seamless.
Add _seed_fixed_sound() helper and call it from build_routine_manifest
on the real TTS path (guarded by _real_audio = gen_audio is None).
Writes ok.mp3 (Et hop !) and bravo.mp3 (Bravo ! Tu as fini.) into
STAGING_ROUTINES if absent/empty; existence guard avoids re-generation.
Tests stay offline: injected gen_audio != None skips the block entirely.
On the first hop from the choice screen into a step screen,
build_step_screen() was calling lv_screen_load_anim with auto_del=true.
LVGL therefore deleted the outgoing choice screen (s_root) at the end of
the transition, leaving a dangling pointer in routine.c:s_root and
ui.c:s_screens[ROUTINE]. On the second entry into Routine mode,
lisael_ui_goto reused that freed pointer, causing a use-after-free.
Fix: use del_prev = (s_step_screen != NULL). When s_step_screen is NULL
the outgoing screen is the memoized s_root (auto_del=false, kept); when
non-NULL it is a transient step screen (auto_del=true, freed by LVGL as
before). show_bravo and the home-return paths are unchanged.
Build and show the per-step LVGL screen: big picto (150 px,
PSRAM cache), Fredoka-30 label, green progress dots (filled up
to current step), giant "C'est fait" button (green, 240×64),
Home back button. On entry: voice announce via lisael_aac_play_file.
On "C'est fait": play ok.mp3 then advance to next step or call
show_bravo (provisional stub → HOME, Task 4c). Each step creates
a fresh screen; previous is auto_del'd by lv_screen_load_anim.
Replace stub routine.c with manifest load into PSRAM, PSRAM picto cache
(mirrors lire.c: no S: driver, negative-cache misses, reject tiny),
and choice screen with per-routine tap cards or no-manifest message.
open_routine() placeholder wired with routine index; Task 4b fills it.
Log level INFO pulls libc printf into IRAM (~15 KB), overflowing
the fixed 12 KB iram_seg by 9787 bytes. Pin SIZE optimization
and drop to WARN level to avoid the vfprintf/stdio chain.
Replace vertical button list with a horizontal snap-scroll
carousel: one 176x122 tile per slide, 64px CONTAIN icon,
lisael_font_24 label, left/right arrows, dot page indicator.
Mirrors home.c pattern exactly (own s_gtrack/s_gdots statics).
All game callbacks and sub-screens unchanged.
transfers_mark gains an explicit dir kwarg; push_batch (sole caller)
passes dir=subdir so marking is precise. failed_names keys status_by
by (dir, name) instead of name alone, so a podcast and a routine
asset with the same basename are tracked as two distinct records.
Adds test_failed_names_disambiguation_by_dir covering this case.
Replace bare assert (stripped under python -O) with an explicit
isinstance check that returns HTTP 400 when routines is missing
or not a list. JSON-decode errors still caught by the outer try.
Add panel-transfers + tab-transfers DOM, loadTransfers() with
setInterval(2s) while active != null, stopTransfersTimer() on
leave-tab and on idle, retryTransfers() for POST /api/transfers/retry.
All record fields escaped; null duration/rate guarded.
Add _history_lock to serialize history load→extend→write in
transfers_end(), preventing lost-update races on transfers.json if a
second writer is added later. Fix file handle leak in
_transfers_history_load() with context manager.
Add compute_delta_named, routines_staging_files, compute_routines_delta
so routines.json goes last in the push delta. Generalize push_file with
subdir param (default podcasts, compat preserved). Fix test_delta.py:
compute_delta lives in lisael_content, not lisael_server.
Add two test cases to verify that the routine manifest cache correctly
regenerates assets when their dependencies change:
1. test_build_routine_manifest_regen_on_text_change: Mutate ONE step's
text while keeping emoji unchanged; verify that audio is regenerated
(audio count >= 1) AND bin is NOT regenerated (bin count == 0).
2. test_build_routine_manifest_regen_on_emoji_change: Mutate ONE step's
emoji while keeping text unchanged; verify that bin is regenerated
(bin count >= 1) AND audio is NOT regenerated (audio count == 0).
Both tests follow the existing cache test pattern: fake generators that
count calls and write files to STAGING_ROUTINES, warm cache via initial
build, mutate content, rebuild with fresh counters, assert expectations.
Cleanup removes ROUTINES_JSON to avoid test state leakage.
All 6 tests pass; cache regen branches are now fully covered.
Adds asset_key(), build_routine_manifest(), _gen_step_audio(),
_routine_cache_load() and ROUTINES_CACHE to lisael_content.py.
Cache (STAGING_ROUTINES/.cache.json) skips regeneration when
emoji/text unchanged and file exists on disk. SD manifest uses
icon/audio keys (firmware schema). Injectable gen_bin/gen_audio
for unit-testable TDD without network.
Add tts_speak() function to call the tailnet gateway TTS endpoint
(model tts-1, voice fr, response mp3). Configurable via LISAEL_TTS_URL
and LISAEL_TTS_VOICE env vars. Returns raw mp3 bytes, raises on failure.
Switching station without pressing Stop rebooted: a StreamBuffer allows a
single sender, but the previous station's HTTP task lingered (blocked in recv,
or in a full-buffer send) past stop()'s wait, so starting the next station made
two senders -> FreeRTOS assert -> reboot. Fixes: close the live HTTP socket
from stop() (mutex-guarded) so a blocked recv returns at once; short net/decode
timeouts; a per-play generation guard so an old task never touches the buffer;
and play() waits for the old session to be fully gone before starting.
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.