Commit Graph

28 Commits

Author SHA1 Message Date
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