76 Commits

Author SHA1 Message Date
L'électron rare 076678fb45 feat(games): tilt-maze game with IMU
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.
2026-06-21 15:04:06 +02:00
L'électron rare 14fff819f2 feat(games): celebratory spinning star + sound 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).
2026-06-21 13:35:43 +02:00
L'électron rare 2fffcfe9a3 feat(box): celebratory spinning star on bravo
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 !").
2026-06-21 12:50:59 +02:00
L'électron rare 0ee754b391 chore: ignore local .cache and .claude dirs 2026-06-21 12:42:02 +02:00
L'électron rare 201cba1c5e feat(games): harder memory, additions, coloring book
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.
2026-06-21 12:42:02 +02:00
L'électron rare 31488982c7 build: restore wifi ram tuning defaults
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.
2026-06-21 12:41:51 +02:00
L'électron rare 3a23c0660f fix(box): routine bravo without full-screen layer
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.
2026-06-21 12:41:51 +02:00
L'électron rare 2d17283c5f docs: carousel wrap report 2026-06-21 11:45:54 +02:00
L'électron rare 6faca03549 feat(ui): infinite wrap carousels
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.
2026-06-21 11:45:54 +02:00
L'électron rare 7d2a5f9d1c feat(tower): seed routine ok and bravo sounds
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.
2026-06-21 11:29:47 +02:00
L'électron rare 1491af2769 fix(box): keep routine choice screen alive
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.
2026-06-21 11:23:01 +02:00
L'électron rare d809e8add4 feat(box): routine bravo screen + cleanup 2026-06-21 11:11:57 +02:00
L'électron rare 797d9caf98 feat(box): routine step screen + voice
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.
2026-06-21 11:05:20 +02:00
L'électron rare 0591c8acf6 feat(box): routine load + choice screen
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.
2026-06-21 10:59:45 +02:00
L'électron rare 21bfd2f7de feat(box): wire routine mode + tile
Add LISAEL_MODE_ROUTINE to the UI shell: enum entry, build_screen()
case, home carousel tile (8th, broom icon U+1F9F9, periwinkle pastel),
minimal navigable stub in modes/routine.c, CMakeLists srcs update,
Twemoji icon generated (17/17 ok). s_adots[8] now at capacity.
2026-06-21 10:53:17 +02:00
L'électron rare 3acde10831 feat(box): register routines files too 2026-06-21 10:46:23 +02:00
L'électron rare 6cda5d243d feat(box): routine manifest parser + host test 2026-06-21 10:41:50 +02:00
L'électron rare 5c8b846f05 fix(games): wrap long tile labels
Prevent "Calme ta colère" from clipping in carousel tiles by constraining
label width to 90%, enabling text wrap, and center-aligning.
2026-06-21 10:38:27 +02:00
L'électron rare 94533f84c9 build: shrink bootloader to fit iram
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.
2026-06-21 10:34:32 +02:00
L'électron rare 24d9e25381 feat(games): tile carousel like home menu
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.
2026-06-21 10:23:50 +02:00
L'électron rare a4d00827c7 build: restore sdkconfig defaults for routines
Add FATFS LFN (heap, 255), LVGL stdio FS ('S' drive),
mbedTLS external+dynamic alloc, SPIRAM task stacks.
WiFi dynamic TX blocked by SPIRAM_TRY_ALLOCATE_WIFI_LWIP
Kconfig dep; static TX (TYPE=0) is correct for this board.
2026-06-21 10:16:24 +02:00
L'électron rare 65d23cb25d fix(tower): key transfers journal by dir and name
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.
2026-06-21 10:01:18 +02:00
L'électron rare ad9ee4f057 fix(tower): explicit 400 on bad routines body
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.
2026-06-21 10:01:13 +02:00
L'électron rare 5a23f3e47b feat(ui): transfers tab with autorefresh
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.
2026-06-21 09:51:49 +02:00
L'électron rare 281f0ddbfe feat(ui): routines tab editor 2026-06-21 09:46:35 +02:00
L'électron rare 831fd3c45e feat(tower): transfers api and retry endpoint 2026-06-21 09:42:36 +02:00
L'électron rare 483b1ec853 feat(tower): routines api save build push 2026-06-21 09:39:18 +02:00
L'électron rare 89f1ccd3d1 feat(tower): instrument push and retry failed 2026-06-21 09:34:50 +02:00
L'électron rare dfc8d4762b fix(tower): lock transfers history writes
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.
2026-06-21 09:31:42 +02:00
L'électron rare 58da6b63e4 feat(tower): transfers journal begin mark end 2026-06-21 09:28:45 +02:00
L'électron rare 916d4d123f feat(tower): routines staging push and delta
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.
2026-06-21 09:24:45 +02:00
L'électron rare e493f85203 test(tower): cover routine cache regen paths
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.
2026-06-21 09:20:43 +02:00
L'électron rare 0a114e8638 feat(tower): build routine manifest with cache
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.
2026-06-21 09:17:45 +02:00
L'électron rare 39bee27a2d feat(tower): routines truth model load and save
TDD: test_routines_seed_and_roundtrip tests routines_load()
seeding default (matin/soir), round-trip save/load, and
verifies json structure with all required fields (id, title,
emoji, steps). All tests GREEN.
2026-06-21 09:13:19 +02:00
L'électron rare 38141bff69 test(tower): strengthen emoji_cp zwj assertion 2026-06-21 09:11:24 +02:00
L'électron rare f8532ed3f3 feat(tower): twemoji to RGB565A8 bin helper
Add emoji_cp() (pure: emoji → Twemoji codepoint, strips VS16) and
emoji_bin() (download Twemoji, resize 160×160 RGBA, LVGLImage.py →
RGB565A8 .bin in STAGING_ROUTINES). TDD test passes (test_routines.py).
2026-06-21 09:08:36 +02:00
L'électron rare f39bbdebf9 feat(tower): add gateway TTS helper
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.
2026-06-21 09:04:44 +02:00
L'électron rare 4bca57e514 docs(plans): firmware routine + merlin studio v2
Two implementation plans for the routine mode (firmware) and Merlin Studio v2 (Tower/web: routines tab + transfers viz).
2026-06-21 09:00:21 +02:00
L'électron rare baea6ccdd1 docs(merlin): transfers viz spec + routines note
Merlin Studio v2: in-progress + completed transfer journal (duration, throughput, retry failed) alongside the routines tab.
2026-06-21 08:48:59 +02:00
L'électron rare 3c6302bf0e docs(routines): design spec for routine mode
Matin/Soir illustrated step routine: SD manifest pushed by Merlin Studio/Tower (Twemoji .bin + fr TTS .mp3), firmware mode with voice announce + done button + final cheer.
2026-06-21 08:37:13 +02:00
Clément Saillant ea19aba53d fix(box): no reboot when switching radio stations
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.
2026-06-15 12:28:31 +02:00
Clément Saillant 3cb3bdb4eb feat(box): volume slider on radio screen 2026-06-15 12:05:54 +02:00
Clément Saillant a399a0f2f7 perf(box): cache Lire icons in PSRAM
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.
2026-06-15 10:21:51 +02:00
Clément Saillant 00596f39cd fix(box): radio sound + add SuperLoustic station
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).
2026-06-15 10:02:12 +02:00
Clément Saillant ce78cad14c feat(box): station carousel for radio menu
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.
2026-06-15 09:46:15 +02:00
Clément Saillant 7c8630abbe feat(box): activity carousel for home menu
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.
2026-06-15 09:13:33 +02:00
Clément Saillant 3f97e8c4bf feat(box): cover carousel for Histoires menu
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.
2026-06-15 09:03:56 +02:00
Clément Saillant ec2dd3f24f feat(tower): cap shows at latest N per feed
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.
2026-06-15 08:40:43 +02:00
Clément Saillant fdb81a8f86 docs: Merlin Studio design + plan 2026-06-15 08:15:29 +02:00
Clément Saillant 2ffec1cc57 feat(tower): Merlin Studio web UI 2026-06-15 08:06:12 +02:00