Commit Graph

39 Commits

Author SHA1 Message Date
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
Clément Saillant 0eb3d1eb05 feat(tower): Merlin admin API + basic auth 2026-06-15 08:02:50 +02:00
Clément Saillant d6176f295f refactor(tower): additive refresh on shared module 2026-06-15 08:01:49 +02:00
Clément Saillant 0ca9df5496 feat(tower): shared content module 2026-06-15 07:59:14 +02:00
Clément Saillant c46a2e268b docs: Spec A autopush design + plan
Design and implementation plan for Tower auto-push: registration-
triggered, weekly, podcasts only. Ignore __pycache__ and *.real
editor backups.
2026-06-15 07:45:15 +02:00
Clément Saillant 1aab35bc3d feat(tower): transcode episodes + async push
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.
2026-06-15 07:45:10 +02:00
Clément Saillant d4b7700270 fix(box): harden file-server + register ordering
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.
2026-06-15 07:45:04 +02:00
Clément Saillant aa4707502e fix(tower): unique per-episode filenames for delta 2026-06-15 00:57:35 +02:00
Clément Saillant 47df147e7b chore(tower): systemd service + weekly timer 2026-06-15 00:52:59 +02:00
Clément Saillant 69e968a238 feat(box): register with Tower on WiFi connect 2026-06-15 00:52:12 +02:00
Clément Saillant 647bc57818 feat(tower): register server + delta push to box 2026-06-15 00:50:11 +02:00
Clément Saillant 48e2b6c830 feat(tower): weekly podcast refresh to staging 2026-06-15 00:48:11 +02:00
Clément Saillant e35058f53d chore(box): commit firmware base + feature docs
Backfills the working firmware that had stayed uncommitted across the
build sessions, so the branch is self-contained.

- audio: audio_player, radio_pipeline, sd_player, calm_tone, aac_player
- net: podcast (RSS), udp_log
- modes: balloons, plus calm/games/radio tweaks
- ui: fonts header, icons; build files (CMakeLists, idf_component.yml,
  sdkconfig.defaults)
- docs/superpowers: Histoires design + plan

Note: sdkconfig stays gitignored (Wi-Fi creds + tuned LFN/FS/mbedtls
settings); a fresh checkout must reconfigure those.
2026-06-15 00:20:47 +02:00
Clément Saillant e2f0010688 fix(lire): icon-only image rounds + font accents
- 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).
2026-06-15 00:20:26 +02:00
Clément Saillant d8cbf35572 feat: WiFi settings tile, 366 words, icon cleanup
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.
2026-06-14 23:54:58 +02:00
Clément Saillant f6bbb7bb80 feat(lire): SD-backed reading game (286 words)
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.
2026-06-14 23:39:07 +02:00
Clément Saillant 3b697ad09b fix(histoires): covers, MP3, volume, font symbols 2026-06-14 21:40:12 +02:00
Clément Saillant de0b01ad3c feat(wifi): SoftAP captive provisioning portal 2026-06-14 19:38:43 +02:00
Clément Saillant 7e69182d7a tools: add gen_covers.py for v2 manifest + covers
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).
2026-06-14 17:37:28 +02:00
Clément Saillant 94917f633d feat(histoires): add vignette podcast browser
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.
2026-06-14 17:34:00 +02:00
Clément Saillant dcbc23c374 build(histoires): manifest parser + LVGL stdio FS
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/...").
2026-06-14 17:14:44 +02:00
Clément Saillant 0caffbbe8b feat(histoires): podcast manifest v2 parser
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.
2026-06-14 17:10:44 +02:00
Clément Saillant 72fee10538 docs: add README and SD card layout
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.
2026-06-14 00:40:46 +02:00
Clément Saillant bc2d1ecb1c feat(ui): add LVGL UI shell, modes and app_main
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).
2026-06-14 00:40:32 +02:00
Clément Saillant a53d7e273b 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.
2026-06-14 00:40:19 +02:00
Clément Saillant e44efe13e0 chore(build): scaffold ESP-IDF project for BOX-3
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.
2026-06-14 00:40:07 +02:00
electron 7dfea75472 Initial commit 2026-06-13 22:20:01 +00:00