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).
This commit is contained in:
+108
@@ -0,0 +1,108 @@
|
||||
// Lisael Box — application entry point.
|
||||
//
|
||||
// Boot sequence:
|
||||
// 1. NVS (required before Wi-Fi)
|
||||
// 2. BSP I2C + display + touch + LVGL (esp-box BSP)
|
||||
// 3. Audio codec (ES8311 via BSP)
|
||||
// 4. microSD mount (best-effort)
|
||||
// 5. Build the LVGL UI (home screen + mode router)
|
||||
// 6. Wi-Fi station start, then SNTP + weather once connected (background)
|
||||
// 7. A light "clock tick" task refreshes the home/clock once per minute.
|
||||
//
|
||||
// app_main returns; the LVGL task (created by the BSP) and our FreeRTOS tasks
|
||||
// keep the system alive.
|
||||
|
||||
#include "lisael_config.h"
|
||||
#include "ui/ui.h"
|
||||
#include "net/wifi.h"
|
||||
#include "net/sntp_time.h"
|
||||
#include "net/weather.h"
|
||||
#include "audio/audio_player.h"
|
||||
#include "audio/sd_player.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "bsp/esp-box-3.h"
|
||||
|
||||
static const char *TAG = "app";
|
||||
|
||||
// Called by the weather task whenever a fetch completes — push to the home UI.
|
||||
static void on_weather_update(const lisael_weather_t *w)
|
||||
{
|
||||
(void)w;
|
||||
lisael_home_update_weather();
|
||||
}
|
||||
|
||||
// Light periodic task: refresh the clock label every minute, plus the internal
|
||||
// sensor readout. Cheap; keeps the home screen current without a busy loop.
|
||||
static void tick_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
for (;;) {
|
||||
lisael_home_update_clock();
|
||||
lisael_home_update_sensors();
|
||||
vTaskDelay(pdMS_TO_TICKS(60 * 1000));
|
||||
}
|
||||
}
|
||||
|
||||
// Networking bring-up runs off the main path so the UI is responsive even with
|
||||
// no Wi-Fi. Blocks (in its own task) on association, then starts SNTP+weather.
|
||||
static void net_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
ESP_ERROR_CHECK(lisael_wifi_start());
|
||||
|
||||
if (lisael_wifi_wait_connected(30000)) {
|
||||
ESP_LOGI(TAG, "Wi-Fi up — starting SNTP + weather");
|
||||
lisael_sntp_start();
|
||||
lisael_weather_start(on_weather_update);
|
||||
// Nudge the clock once time is likely valid.
|
||||
vTaskDelay(pdMS_TO_TICKS(3000));
|
||||
lisael_home_update_clock();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Wi-Fi not connected after 30s — offline mode");
|
||||
// UI still works (radio/weather show "—"); Wi-Fi keeps retrying.
|
||||
}
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Lisael Box starting");
|
||||
|
||||
// --- 1. NVS --------------------------------------------------------------
|
||||
esp_err_t ret = nvs_flash_init();
|
||||
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
ret = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(ret);
|
||||
|
||||
// --- 2. BSP: I2C, display + touch + LVGL --------------------------------
|
||||
ESP_ERROR_CHECK(bsp_i2c_init());
|
||||
bsp_display_start(); // brings up LCD, touch and the LVGL task
|
||||
bsp_display_backlight_on();
|
||||
bsp_display_brightness_set(80); // comfortable default brightness
|
||||
|
||||
// --- 3. Audio codec ------------------------------------------------------
|
||||
if (lisael_audio_init() != ESP_OK) {
|
||||
ESP_LOGE(TAG, "audio init failed — continuing without sound");
|
||||
}
|
||||
|
||||
// --- 4. microSD (best-effort) -------------------------------------------
|
||||
lisael_sd_mount(); // logs a warning if no card; modes handle absence
|
||||
|
||||
// --- 5. Build the UI (must hold the display lock) -----------------------
|
||||
if (bsp_display_lock(0)) {
|
||||
lisael_ui_init();
|
||||
bsp_display_unlock();
|
||||
}
|
||||
|
||||
// --- 6 & 7. Networking + periodic refresh tasks -------------------------
|
||||
xTaskCreatePinnedToCore(net_task, "net", 8192, NULL, 4, NULL, tskNO_AFFINITY);
|
||||
xTaskCreatePinnedToCore(tick_task, "tick", 4096, NULL, 3, NULL, tskNO_AFFINITY);
|
||||
|
||||
ESP_LOGI(TAG, "Lisael Box ready");
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// "Coin calme" / breathing mode for Lisael Box.
|
||||
//
|
||||
// A circle (the "bubble") inflates over ~4 s ("Inspire…") then deflates over
|
||||
// ~6 s ("Souffle…"), looping. Driven by two chained LVGL animations on the
|
||||
// bubble's width/height. An optional soft sound can be played from the SD card
|
||||
// if present (best-effort, ignored on failure).
|
||||
|
||||
#include "ui/ui.h"
|
||||
#include "ui/icons.h"
|
||||
#include "audio/sd_player.h"
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *TAG = "ui.calm";
|
||||
|
||||
#define BUBBLE_MIN 70
|
||||
#define BUBBLE_MAX 200
|
||||
#define INHALE_MS 4000
|
||||
#define EXHALE_MS 6000
|
||||
|
||||
static lv_obj_t *s_root;
|
||||
static lv_obj_t *s_bubble;
|
||||
static lv_obj_t *s_phase_lbl;
|
||||
|
||||
static void anim_size_cb(void *obj, int32_t v)
|
||||
{
|
||||
lv_obj_set_size((lv_obj_t *)obj, v, v);
|
||||
}
|
||||
|
||||
// Forward decls for the inhale<->exhale chain.
|
||||
static void start_inhale(void);
|
||||
static void start_exhale(void);
|
||||
|
||||
static void on_inhale_done(lv_anim_t *a)
|
||||
{
|
||||
(void)a;
|
||||
start_exhale();
|
||||
}
|
||||
|
||||
static void on_exhale_done(lv_anim_t *a)
|
||||
{
|
||||
(void)a;
|
||||
start_inhale();
|
||||
}
|
||||
|
||||
static void start_inhale(void)
|
||||
{
|
||||
if (s_phase_lbl) {
|
||||
lv_label_set_text(s_phase_lbl, "Inspire…");
|
||||
}
|
||||
lv_anim_t a;
|
||||
lv_anim_init(&a);
|
||||
lv_anim_set_var(&a, s_bubble);
|
||||
lv_anim_set_exec_cb(&a, anim_size_cb);
|
||||
lv_anim_set_values(&a, BUBBLE_MIN, BUBBLE_MAX);
|
||||
lv_anim_set_duration(&a, INHALE_MS);
|
||||
lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out);
|
||||
lv_anim_set_completed_cb(&a, on_inhale_done);
|
||||
lv_anim_start(&a);
|
||||
}
|
||||
|
||||
static void start_exhale(void)
|
||||
{
|
||||
if (s_phase_lbl) {
|
||||
lv_label_set_text(s_phase_lbl, "Souffle…");
|
||||
}
|
||||
lv_anim_t a;
|
||||
lv_anim_init(&a);
|
||||
lv_anim_set_var(&a, s_bubble);
|
||||
lv_anim_set_exec_cb(&a, anim_size_cb);
|
||||
lv_anim_set_values(&a, BUBBLE_MAX, BUBBLE_MIN);
|
||||
lv_anim_set_duration(&a, EXHALE_MS);
|
||||
lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out);
|
||||
lv_anim_set_completed_cb(&a, on_exhale_done);
|
||||
lv_anim_start(&a);
|
||||
}
|
||||
|
||||
// Stop the animation when we leave the screen so it doesn't run forever.
|
||||
static void on_screen_event(lv_event_t *e)
|
||||
{
|
||||
lv_event_code_t code = lv_event_get_code(e);
|
||||
if (code == LV_EVENT_SCREEN_LOADED) {
|
||||
ESP_LOGI(TAG, "breathing started");
|
||||
start_inhale();
|
||||
// Best-effort soft ambience (optional file; ignored if absent).
|
||||
lisael_sd_play_sound("calme.mp3");
|
||||
} else if (code == LV_EVENT_SCREEN_UNLOADED) {
|
||||
lv_anim_delete(s_bubble, anim_size_cb);
|
||||
}
|
||||
}
|
||||
|
||||
lv_obj_t *lisael_screen_calm(void)
|
||||
{
|
||||
if (s_root) {
|
||||
return s_root;
|
||||
}
|
||||
s_root = lv_obj_create(NULL);
|
||||
lv_obj_set_style_bg_color(s_root, lv_color_hex(0xE7F5FF), 0); // calm blue
|
||||
lv_obj_add_event_cb(s_root, on_screen_event, LV_EVENT_SCREEN_LOADED, NULL);
|
||||
lv_obj_add_event_cb(s_root, on_screen_event, LV_EVENT_SCREEN_UNLOADED, NULL);
|
||||
|
||||
lisael_ui_add_back_button(s_root);
|
||||
|
||||
s_bubble = lv_obj_create(s_root);
|
||||
lv_obj_set_size(s_bubble, BUBBLE_MIN, BUBBLE_MIN);
|
||||
lv_obj_center(s_bubble);
|
||||
lv_obj_set_style_radius(s_bubble, LV_RADIUS_CIRCLE, 0);
|
||||
lv_obj_set_style_bg_color(s_bubble, lv_color_hex(0x74C0FC), 0);
|
||||
lv_obj_set_style_bg_opa(s_bubble, LV_OPA_70, 0);
|
||||
lv_obj_set_style_border_width(s_bubble, 0, 0);
|
||||
lv_obj_clear_flag(s_bubble, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
s_phase_lbl = lv_label_create(s_root);
|
||||
lv_label_set_text(s_phase_lbl, "Inspire…");
|
||||
lv_obj_set_style_text_font(s_phase_lbl, &lv_font_montserrat_28, 0);
|
||||
lv_obj_align(s_phase_lbl, LV_ALIGN_BOTTOM_MID, 0, -16);
|
||||
|
||||
return s_root;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// Horloge / veilleuse mode for Lisael Box.
|
||||
//
|
||||
// A large centred time + French date on a soft, dimmed background (gentle for a
|
||||
// dark bedroom). A "Histoire du soir" button plays a bedtime story from the SD
|
||||
// card. A periodic LVGL timer refreshes the time once a minute.
|
||||
|
||||
#include "ui/ui.h"
|
||||
#include "ui/icons.h"
|
||||
#include "ui/fr_date.h"
|
||||
#include "audio/sd_player.h"
|
||||
#include "net/sntp_time.h"
|
||||
|
||||
#include <time.h>
|
||||
#include "esp_log.h"
|
||||
#include "bsp/esp-box-3.h"
|
||||
|
||||
static const char *TAG = "ui.clock";
|
||||
|
||||
static lv_obj_t *s_root;
|
||||
static lv_obj_t *s_time_lbl;
|
||||
static lv_obj_t *s_date_lbl;
|
||||
static lv_timer_t *s_timer;
|
||||
|
||||
static void refresh(void)
|
||||
{
|
||||
time_t now = time(NULL);
|
||||
struct tm tm;
|
||||
localtime_r(&now, &tm);
|
||||
|
||||
char time_buf[8], date_buf[48];
|
||||
lisael_fr_time(&tm, time_buf, sizeof(time_buf));
|
||||
lisael_fr_date(&tm, date_buf, sizeof(date_buf));
|
||||
|
||||
lv_label_set_text(s_time_lbl, lisael_time_is_valid() ? time_buf : "--:--");
|
||||
lv_label_set_text(s_date_lbl, date_buf);
|
||||
}
|
||||
|
||||
static void timer_cb(lv_timer_t *t)
|
||||
{
|
||||
(void)t;
|
||||
refresh(); // already on the LVGL task -> no extra lock needed
|
||||
}
|
||||
|
||||
static void story_cb(lv_event_t *e)
|
||||
{
|
||||
(void)e;
|
||||
ESP_LOGI(TAG, "histoire du soir");
|
||||
// Plays /sdcard/sounds/histoire.mp3 if present (best-effort).
|
||||
lisael_sd_play_sound("histoire.mp3");
|
||||
}
|
||||
|
||||
static void on_screen_event(lv_event_t *e)
|
||||
{
|
||||
lv_event_code_t code = lv_event_get_code(e);
|
||||
if (code == LV_EVENT_SCREEN_LOADED) {
|
||||
// Dim the backlight for a soft night-light feel.
|
||||
bsp_display_brightness_set(30);
|
||||
if (!s_timer) {
|
||||
s_timer = lv_timer_create(timer_cb, 30000, NULL); // every 30 s
|
||||
}
|
||||
lv_timer_resume(s_timer);
|
||||
refresh();
|
||||
} else if (code == LV_EVENT_SCREEN_UNLOADED) {
|
||||
if (s_timer) {
|
||||
lv_timer_pause(s_timer);
|
||||
}
|
||||
bsp_display_brightness_set(80); // restore normal brightness
|
||||
}
|
||||
}
|
||||
|
||||
lv_obj_t *lisael_screen_clock(void)
|
||||
{
|
||||
if (s_root) {
|
||||
return s_root;
|
||||
}
|
||||
s_root = lv_obj_create(NULL);
|
||||
lv_obj_set_style_bg_color(s_root, lv_color_hex(0x1E2233), 0); // soft night
|
||||
lv_obj_add_event_cb(s_root, on_screen_event, LV_EVENT_SCREEN_LOADED, NULL);
|
||||
lv_obj_add_event_cb(s_root, on_screen_event, LV_EVENT_SCREEN_UNLOADED, NULL);
|
||||
|
||||
lisael_ui_add_back_button(s_root);
|
||||
|
||||
s_time_lbl = lv_label_create(s_root);
|
||||
lv_obj_set_style_text_font(s_time_lbl, &lv_font_montserrat_48, 0);
|
||||
lv_obj_set_style_text_color(s_time_lbl, lv_color_hex(0xFFE8B0), 0); // warm
|
||||
lv_label_set_text(s_time_lbl, "--:--");
|
||||
lv_obj_align(s_time_lbl, LV_ALIGN_CENTER, 0, -20);
|
||||
|
||||
s_date_lbl = lv_label_create(s_root);
|
||||
lv_obj_set_style_text_color(s_date_lbl, lv_color_hex(0xC0C8E0), 0);
|
||||
lv_label_set_text(s_date_lbl, "...");
|
||||
lv_obj_align(s_date_lbl, LV_ALIGN_CENTER, 0, 24);
|
||||
|
||||
lv_obj_t *story = lv_button_create(s_root);
|
||||
lv_obj_add_style(story, lisael_ui_style_button(), 0);
|
||||
lv_obj_set_style_bg_color(story, lv_color_hex(0x5C7CFA), 0);
|
||||
lv_obj_align(story, LV_ALIGN_BOTTOM_MID, 0, -12);
|
||||
lv_obj_add_event_cb(story, story_cb, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_t *lbl = lv_label_create(story);
|
||||
lv_label_set_text(lbl, LISAEL_EMOJI_CALM " Histoire du soir");
|
||||
lv_obj_center(lbl);
|
||||
|
||||
return s_root;
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
// Jeux mode for Lisael Box: a small menu -> Memory, Compter, (Coloriage stub).
|
||||
//
|
||||
// Design rule: no losing, no scores that shame — only positive feedback (a star
|
||||
// / "Bravo !"). All games are LVGL-only and fit the 320x240 screen.
|
||||
//
|
||||
// * Memory : 3x2 grid of 6 cards (3 emoji pairs). Flip two; if they match
|
||||
// they stay revealed; otherwise they flip back. Win = all matched.
|
||||
// * Compter : show N emoji objects, offer 3 number buttons, tap the right
|
||||
// count -> "Bravo !". New round on success.
|
||||
// * Coloriage: documented STUB (free-draw on a canvas is heavier on 320x240
|
||||
// and needs an lv_canvas + touch drawing loop; left as a labelled
|
||||
// placeholder screen). See note in build_coloring_stub().
|
||||
|
||||
#include "ui/ui.h"
|
||||
#include "ui/icons.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_random.h"
|
||||
|
||||
static const char *TAG = "ui.games";
|
||||
|
||||
static lv_obj_t *s_menu_root;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared: a positive-feedback toast ("Bravo ! ⭐").
|
||||
// ---------------------------------------------------------------------------
|
||||
static void show_bravo(lv_obj_t *parent)
|
||||
{
|
||||
lv_obj_t *toast = lv_obj_create(parent);
|
||||
lv_obj_set_size(toast, 200, 70);
|
||||
lv_obj_center(toast);
|
||||
lv_obj_set_style_bg_color(toast, lv_color_hex(0xFFF3BF), 0);
|
||||
lv_obj_set_style_radius(toast, 16, 0);
|
||||
lv_obj_t *lbl = lv_label_create(toast);
|
||||
lv_label_set_text(lbl, "Bravo ! " LISAEL_EMOJI_STAR);
|
||||
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_28, 0);
|
||||
lv_obj_center(lbl);
|
||||
// Auto-dismiss after 1.5 s.
|
||||
lv_obj_delete_delayed(toast, 1500);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// MEMORY
|
||||
// ===========================================================================
|
||||
#define MEM_PAIRS 3
|
||||
#define MEM_CARDS (MEM_PAIRS * 2)
|
||||
|
||||
static const char *const MEM_FACES[MEM_PAIRS] = {
|
||||
"\U0001F431", // 🐱
|
||||
"\U0001F436", // 🐶
|
||||
"\U0001F438", // 🐸
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
lv_obj_t *cards[MEM_CARDS];
|
||||
int face[MEM_CARDS]; // index into MEM_FACES
|
||||
bool matched[MEM_CARDS];
|
||||
int first; // currently revealed card, or -1
|
||||
int matched_count;
|
||||
lv_obj_t *root;
|
||||
} memory_state_t;
|
||||
|
||||
static void mem_set_revealed(memory_state_t *st, int i, bool revealed)
|
||||
{
|
||||
lv_obj_t *lbl = lv_obj_get_child(st->cards[i], 0);
|
||||
lv_label_set_text(lbl, revealed ? MEM_FACES[st->face[i]] : "?");
|
||||
lv_obj_set_style_bg_color(st->cards[i],
|
||||
revealed ? lv_color_hex(0xB2F2BB) : lv_color_hex(0x99C8FF), 0);
|
||||
}
|
||||
|
||||
static void mem_flip_back_cb(lv_timer_t *t)
|
||||
{
|
||||
memory_state_t *st = lv_timer_get_user_data(t);
|
||||
for (int i = 0; i < MEM_CARDS; i++) {
|
||||
if (!st->matched[i]) {
|
||||
mem_set_revealed(st, i, false);
|
||||
}
|
||||
}
|
||||
st->first = -1;
|
||||
lv_timer_delete(t);
|
||||
}
|
||||
|
||||
static void mem_card_cb(lv_event_t *e)
|
||||
{
|
||||
memory_state_t *st = lv_event_get_user_data(e);
|
||||
lv_obj_t *card = lv_event_get_target_obj(e);
|
||||
|
||||
int idx = -1;
|
||||
for (int i = 0; i < MEM_CARDS; i++) {
|
||||
if (st->cards[i] == card) { idx = i; break; }
|
||||
}
|
||||
if (idx < 0 || st->matched[idx] || idx == st->first) {
|
||||
return; // ignore taps on matched / already-open card
|
||||
}
|
||||
|
||||
mem_set_revealed(st, idx, true);
|
||||
|
||||
if (st->first < 0) {
|
||||
st->first = idx; // first of the pair
|
||||
return;
|
||||
}
|
||||
|
||||
// Second card: check match.
|
||||
if (st->face[idx] == st->face[st->first]) {
|
||||
st->matched[idx] = st->matched[st->first] = true;
|
||||
st->matched_count += 2;
|
||||
st->first = -1;
|
||||
if (st->matched_count == MEM_CARDS) {
|
||||
show_bravo(st->root);
|
||||
}
|
||||
} else {
|
||||
// Flip both back after a short delay.
|
||||
lv_timer_t *t = lv_timer_create(mem_flip_back_cb, 800, st);
|
||||
lv_timer_set_repeat_count(t, 1);
|
||||
}
|
||||
}
|
||||
|
||||
static void mem_build(lv_obj_t *parent)
|
||||
{
|
||||
memory_state_t *st = lv_malloc(sizeof(memory_state_t));
|
||||
memset(st, 0, sizeof(*st));
|
||||
st->first = -1;
|
||||
st->root = parent;
|
||||
|
||||
// Build and shuffle the deck.
|
||||
for (int i = 0; i < MEM_CARDS; i++) {
|
||||
st->face[i] = i % MEM_PAIRS;
|
||||
}
|
||||
for (int i = MEM_CARDS - 1; i > 0; i--) {
|
||||
int j = (int)(esp_random() % (uint32_t)(i + 1));
|
||||
int tmp = st->face[i]; st->face[i] = st->face[j]; st->face[j] = tmp;
|
||||
}
|
||||
|
||||
lv_obj_t *grid = lv_obj_create(parent);
|
||||
lv_obj_remove_style_all(grid);
|
||||
lv_obj_set_size(grid, LV_PCT(100), 180);
|
||||
lv_obj_align(grid, LV_ALIGN_BOTTOM_MID, 0, -4);
|
||||
lv_obj_set_flex_flow(grid, LV_FLEX_FLOW_ROW_WRAP);
|
||||
lv_obj_set_flex_align(grid, LV_FLEX_ALIGN_SPACE_EVENLY,
|
||||
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
|
||||
for (int i = 0; i < MEM_CARDS; i++) {
|
||||
lv_obj_t *card = lv_button_create(grid);
|
||||
lv_obj_set_size(card, 90, 80);
|
||||
lv_obj_set_style_radius(card, 12, 0);
|
||||
lv_obj_add_event_cb(card, mem_card_cb, LV_EVENT_CLICKED, st);
|
||||
lv_obj_t *lbl = lv_label_create(card);
|
||||
lv_label_set_text(lbl, "?");
|
||||
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_28, 0);
|
||||
lv_obj_center(lbl);
|
||||
st->cards[i] = card;
|
||||
mem_set_revealed(st, i, false);
|
||||
}
|
||||
// NOTE: `st` intentionally lives for the screen's lifetime (freed when the
|
||||
// screen object is deleted). For a long-lived single-screen app this is
|
||||
// fine; if screens are recreated, attach a delete handler to free `st`.
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// COMPTER ("combien d'objets ?")
|
||||
// ===========================================================================
|
||||
typedef struct {
|
||||
lv_obj_t *objects_box;
|
||||
lv_obj_t *buttons[3];
|
||||
int answer;
|
||||
lv_obj_t *root;
|
||||
} count_state_t;
|
||||
|
||||
static void count_new_round(count_state_t *st);
|
||||
|
||||
static void count_btn_cb(lv_event_t *e)
|
||||
{
|
||||
count_state_t *st = lv_event_get_user_data(e);
|
||||
lv_obj_t *btn = lv_event_get_target_obj(e);
|
||||
lv_obj_t *lbl = lv_obj_get_child(btn, 0);
|
||||
int chosen = atoi(lv_label_get_text(lbl));
|
||||
if (chosen == st->answer) {
|
||||
show_bravo(st->root);
|
||||
count_new_round(st);
|
||||
}
|
||||
// Wrong answer: no penalty, no message — child just tries again.
|
||||
}
|
||||
|
||||
static void count_new_round(count_state_t *st)
|
||||
{
|
||||
// 1..5 objects.
|
||||
st->answer = 1 + (int)(esp_random() % 5);
|
||||
|
||||
// Render the objects.
|
||||
lv_obj_clean(st->objects_box);
|
||||
for (int i = 0; i < st->answer; i++) {
|
||||
lv_obj_t *o = lv_label_create(st->objects_box);
|
||||
lv_label_set_text(o, "\U0001F34E"); // 🍎
|
||||
lv_obj_set_style_text_font(o, &lv_font_montserrat_28, 0);
|
||||
}
|
||||
|
||||
// Offer the right answer + two distractors in a shuffled set.
|
||||
int opts[3];
|
||||
opts[0] = st->answer;
|
||||
for (int i = 1; i < 3; i++) {
|
||||
int v;
|
||||
do { v = 1 + (int)(esp_random() % 5); }
|
||||
while (v == opts[0] || (i == 2 && v == opts[1]));
|
||||
opts[i] = v;
|
||||
}
|
||||
for (int i = 2; i > 0; i--) {
|
||||
int j = (int)(esp_random() % (uint32_t)(i + 1));
|
||||
int tmp = opts[i]; opts[i] = opts[j]; opts[j] = tmp;
|
||||
}
|
||||
for (int i = 0; i < 3; i++) {
|
||||
lv_obj_t *lbl = lv_obj_get_child(st->buttons[i], 0);
|
||||
lv_label_set_text_fmt(lbl, "%d", opts[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static void count_build(lv_obj_t *parent)
|
||||
{
|
||||
count_state_t *st = lv_malloc(sizeof(count_state_t));
|
||||
memset(st, 0, sizeof(*st));
|
||||
st->root = parent;
|
||||
|
||||
lv_obj_t *q = lv_label_create(parent);
|
||||
lv_label_set_text(q, "Combien ?");
|
||||
lv_obj_align(q, LV_ALIGN_TOP_MID, 0, 50);
|
||||
|
||||
st->objects_box = lv_obj_create(parent);
|
||||
lv_obj_remove_style_all(st->objects_box);
|
||||
lv_obj_set_size(st->objects_box, LV_PCT(90), 80);
|
||||
lv_obj_align(st->objects_box, LV_ALIGN_CENTER, 0, -10);
|
||||
lv_obj_set_flex_flow(st->objects_box, LV_FLEX_FLOW_ROW_WRAP);
|
||||
lv_obj_set_flex_align(st->objects_box, LV_FLEX_ALIGN_CENTER,
|
||||
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
|
||||
lv_obj_t *bar = lv_obj_create(parent);
|
||||
lv_obj_remove_style_all(bar);
|
||||
lv_obj_set_size(bar, LV_PCT(100), 64);
|
||||
lv_obj_align(bar, LV_ALIGN_BOTTOM_MID, 0, -6);
|
||||
lv_obj_set_flex_flow(bar, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(bar, LV_FLEX_ALIGN_SPACE_EVENLY,
|
||||
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
lv_obj_t *b = lv_button_create(bar);
|
||||
lv_obj_add_style(b, lisael_ui_style_button(), 0);
|
||||
lv_obj_set_size(b, 72, 56);
|
||||
lv_obj_add_event_cb(b, count_btn_cb, LV_EVENT_CLICKED, st);
|
||||
lv_obj_t *l = lv_label_create(b);
|
||||
lv_obj_set_style_text_font(l, &lv_font_montserrat_28, 0);
|
||||
lv_label_set_text(l, "0");
|
||||
lv_obj_center(l);
|
||||
st->buttons[i] = b;
|
||||
}
|
||||
|
||||
count_new_round(st);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// COLORIAGE (documented stub)
|
||||
// ===========================================================================
|
||||
static void build_coloring_stub(lv_obj_t *parent)
|
||||
{
|
||||
// STUB: a real coloring game would use an lv_canvas (e.g. 280x180 in PSRAM)
|
||||
// plus an LV_EVENT_PRESSING handler reading the touch point and painting a
|
||||
// brush of the selected color via lv_canvas_set_px / lv_draw on the canvas
|
||||
// buffer. That is doable but heavier on a 320x240 screen and needs a tuned
|
||||
// brush/undo UX for small children, so it is intentionally deferred.
|
||||
lv_obj_t *lbl = lv_label_create(parent);
|
||||
lv_label_set_text(lbl,
|
||||
"Coloriage\n(bientôt)\n\n🎨");
|
||||
lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_center(lbl);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Game sub-screen wrapper + games menu
|
||||
// ===========================================================================
|
||||
typedef void (*game_builder_t)(lv_obj_t *parent);
|
||||
|
||||
static void open_game(game_builder_t builder, const char *title)
|
||||
{
|
||||
lv_obj_t *scr = lv_obj_create(NULL);
|
||||
lv_obj_set_style_bg_color(scr, lv_color_hex(0xF3F0FF), 0);
|
||||
lisael_ui_add_back_button(scr); // returns HOME
|
||||
|
||||
lv_obj_t *t = lv_label_create(scr);
|
||||
lv_label_set_text(t, title);
|
||||
lv_obj_align(t, LV_ALIGN_TOP_MID, 0, 16);
|
||||
|
||||
builder(scr);
|
||||
lv_screen_load_anim(scr, LV_SCR_LOAD_ANIM_MOVE_LEFT, 250, 0, false);
|
||||
ESP_LOGI(TAG, "game open: %s", title);
|
||||
}
|
||||
|
||||
static void memory_cb(lv_event_t *e) { (void)e; open_game(mem_build, LISAEL_EMOJI_GAMES " Memory"); }
|
||||
static void count_cb(lv_event_t *e) { (void)e; open_game(count_build, "🔢 Compter"); }
|
||||
static void color_cb(lv_event_t *e) { (void)e; open_game(build_coloring_stub, "🎨 Coloriage"); }
|
||||
|
||||
lv_obj_t *lisael_screen_games(void)
|
||||
{
|
||||
if (s_menu_root) {
|
||||
return s_menu_root;
|
||||
}
|
||||
s_menu_root = lv_obj_create(NULL);
|
||||
lv_obj_set_style_bg_color(s_menu_root, lv_color_hex(0xF3F0FF), 0);
|
||||
|
||||
lisael_ui_add_back_button(s_menu_root);
|
||||
|
||||
lv_obj_t *title = lv_label_create(s_menu_root);
|
||||
lv_label_set_text(title, LISAEL_EMOJI_GAMES " Jeux");
|
||||
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 16);
|
||||
|
||||
lv_obj_t *list = lv_obj_create(s_menu_root);
|
||||
lv_obj_remove_style_all(list);
|
||||
lv_obj_set_size(list, LV_PCT(100), 150);
|
||||
lv_obj_align(list, LV_ALIGN_CENTER, 0, 20);
|
||||
lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(list, LV_FLEX_ALIGN_CENTER,
|
||||
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_row(list, 10, 0);
|
||||
|
||||
struct { const char *txt; lv_event_cb_t cb; } games[] = {
|
||||
{ LISAEL_EMOJI_GAMES " Memory", memory_cb },
|
||||
{ "🔢 Compter", count_cb },
|
||||
{ "🎨 Coloriage (bientôt)", color_cb },
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(games) / sizeof(games[0]); i++) {
|
||||
lv_obj_t *b = lv_button_create(list);
|
||||
lv_obj_add_style(b, lisael_ui_style_button(), 0);
|
||||
lv_obj_set_width(b, LV_PCT(80));
|
||||
lv_obj_add_event_cb(b, games[i].cb, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_t *l = lv_label_create(b);
|
||||
lv_label_set_text(l, games[i].txt);
|
||||
lv_obj_center(l);
|
||||
}
|
||||
return s_menu_root;
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
// Home screen for Lisael Box: French date + time, weather, sensors, mode tiles.
|
||||
//
|
||||
// Live-update hooks (lisael_home_update_*) are called from background tasks and
|
||||
// take the display lock themselves. The build function runs on the LVGL task
|
||||
// (lock already held by the caller in app_main / lisael_ui_goto).
|
||||
|
||||
#include "ui/ui.h"
|
||||
#include "ui/icons.h"
|
||||
#include "ui/fr_date.h"
|
||||
#include "net/weather.h"
|
||||
#include "net/sntp_time.h"
|
||||
#include "lisael_config.h"
|
||||
|
||||
#include <time.h>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "bsp/esp-box-3.h"
|
||||
|
||||
// Optional internal temperature sensor (ESP32-S3 has a built-in one). The
|
||||
// BOX-3 has no external temp/humidity sensor on the base board (the BSP exposes
|
||||
// an aht30 driver for the optional sensor add-on; not assumed present here).
|
||||
#include "driver/temperature_sensor.h"
|
||||
|
||||
static const char *TAG = "home";
|
||||
|
||||
static lv_obj_t *s_date_lbl;
|
||||
static lv_obj_t *s_time_lbl;
|
||||
static lv_obj_t *s_weather_lbl;
|
||||
static lv_obj_t *s_weather_icon;
|
||||
static lv_obj_t *s_sensor_lbl;
|
||||
static lv_obj_t *s_root;
|
||||
|
||||
static temperature_sensor_handle_t s_tsens; // NULL if unavailable
|
||||
|
||||
// --- one mode tile ----------------------------------------------------------
|
||||
typedef struct {
|
||||
const char *emoji;
|
||||
const char *label;
|
||||
lisael_mode_t mode;
|
||||
lv_color_t color;
|
||||
} tile_def_t;
|
||||
|
||||
static void tile_cb(lv_event_t *e)
|
||||
{
|
||||
lisael_mode_t mode = (lisael_mode_t)(intptr_t)lv_event_get_user_data(e);
|
||||
lisael_ui_goto(mode);
|
||||
}
|
||||
|
||||
static lv_obj_t *make_tile(lv_obj_t *parent, const tile_def_t *def)
|
||||
{
|
||||
lv_obj_t *tile = lv_button_create(parent);
|
||||
lv_obj_add_style(tile, lisael_ui_style_tile(), 0);
|
||||
lv_obj_set_style_bg_color(tile, def->color, 0);
|
||||
lv_obj_set_size(tile, 92, 78);
|
||||
lv_obj_add_event_cb(tile, tile_cb, LV_EVENT_CLICKED,
|
||||
(void *)(intptr_t)def->mode);
|
||||
|
||||
lv_obj_t *col = lv_obj_create(tile);
|
||||
lv_obj_remove_style_all(col);
|
||||
lv_obj_set_size(col, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_flex_flow(col, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(col, LV_FLEX_ALIGN_CENTER,
|
||||
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_clear_flag(col, LV_OBJ_FLAG_CLICKABLE);
|
||||
|
||||
lv_obj_t *emoji = lv_label_create(col);
|
||||
lv_label_set_text(emoji, def->emoji);
|
||||
|
||||
lv_obj_t *txt = lv_label_create(col);
|
||||
lv_label_set_text(txt, def->label);
|
||||
return tile;
|
||||
}
|
||||
|
||||
// --- internal temperature sensor (graceful fallback) ------------------------
|
||||
static void sensors_init(void)
|
||||
{
|
||||
temperature_sensor_config_t cfg =
|
||||
TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80);
|
||||
if (temperature_sensor_install(&cfg, &s_tsens) == ESP_OK &&
|
||||
temperature_sensor_enable(s_tsens) == ESP_OK) {
|
||||
ESP_LOGI(TAG, "internal temperature sensor ready");
|
||||
} else {
|
||||
s_tsens = NULL;
|
||||
ESP_LOGW(TAG, "internal temperature sensor unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
lv_obj_t *lisael_screen_home(void)
|
||||
{
|
||||
if (s_root) {
|
||||
return s_root;
|
||||
}
|
||||
sensors_init();
|
||||
|
||||
s_root = lv_obj_create(NULL);
|
||||
lv_obj_set_style_bg_color(s_root, lv_color_hex(0xEAF4FF), 0); // soft sky
|
||||
|
||||
// --- header: date + time + weather --------------------------------------
|
||||
lv_obj_t *header = lv_obj_create(s_root);
|
||||
lv_obj_remove_style_all(header);
|
||||
lv_obj_set_size(header, LV_PCT(100), 86);
|
||||
lv_obj_align(header, LV_ALIGN_TOP_MID, 0, 4);
|
||||
lv_obj_clear_flag(header, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
s_time_lbl = lv_label_create(header);
|
||||
lv_obj_set_style_text_font(s_time_lbl, &lv_font_montserrat_28, 0);
|
||||
lv_label_set_text(s_time_lbl, "--:--");
|
||||
lv_obj_align(s_time_lbl, LV_ALIGN_TOP_LEFT, 12, 2);
|
||||
|
||||
s_date_lbl = lv_label_create(header);
|
||||
lv_label_set_text(s_date_lbl, "...");
|
||||
lv_obj_align(s_date_lbl, LV_ALIGN_TOP_LEFT, 12, 38);
|
||||
|
||||
s_weather_icon = lv_label_create(header);
|
||||
lv_label_set_text(s_weather_icon, "");
|
||||
lv_obj_align(s_weather_icon, LV_ALIGN_TOP_RIGHT, -12, 2);
|
||||
|
||||
s_weather_lbl = lv_label_create(header);
|
||||
lv_label_set_text(s_weather_lbl, "—");
|
||||
lv_obj_align(s_weather_lbl, LV_ALIGN_TOP_RIGHT, -12, 34);
|
||||
|
||||
s_sensor_lbl = lv_label_create(header);
|
||||
lv_label_set_text(s_sensor_lbl, "");
|
||||
lv_obj_align(s_sensor_lbl, LV_ALIGN_TOP_RIGHT, -12, 58);
|
||||
|
||||
// --- mode tiles ----------------------------------------------------------
|
||||
lv_obj_t *grid = lv_obj_create(s_root);
|
||||
lv_obj_remove_style_all(grid);
|
||||
lv_obj_set_size(grid, LV_PCT(100), 150);
|
||||
lv_obj_align(grid, LV_ALIGN_BOTTOM_MID, 0, -4);
|
||||
lv_obj_set_flex_flow(grid, LV_FLEX_FLOW_ROW_WRAP);
|
||||
lv_obj_set_flex_align(grid, LV_FLEX_ALIGN_SPACE_EVENLY,
|
||||
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
|
||||
static const tile_def_t tiles[] = {
|
||||
{ LISAEL_EMOJI_RADIO, "Radio", LISAEL_MODE_RADIO, {0} },
|
||||
{ LISAEL_EMOJI_SOUND, "Sons", LISAEL_MODE_SOUNDBOX, {0} },
|
||||
{ LISAEL_EMOJI_CALM, "Calme", LISAEL_MODE_CALM, {0} },
|
||||
{ LISAEL_EMOJI_CLOCK, "Horloge", LISAEL_MODE_CLOCK, {0} },
|
||||
{ LISAEL_EMOJI_GAMES, "Jeux", LISAEL_MODE_GAMES, {0} },
|
||||
};
|
||||
static const uint32_t colors[] = {
|
||||
0xFFD8A8, 0xB2F2BB, 0xA5D8FF, 0xD0BFFF, 0xFFC9C9,
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(tiles) / sizeof(tiles[0]); i++) {
|
||||
tile_def_t t = tiles[i];
|
||||
t.color = lv_color_hex(colors[i]);
|
||||
make_tile(grid, &t);
|
||||
}
|
||||
|
||||
// NB: the live values are filled in shortly by the tick/net tasks via
|
||||
// lisael_home_update_*(). We do NOT call them here: this builder runs while
|
||||
// the caller already holds the display lock, and those helpers take the
|
||||
// lock themselves (the esp_lvgl_port mutex is recursive so it would be
|
||||
// safe, but keeping init lock-clean is clearer). The labels above already
|
||||
// show sensible placeholders ("--:--", "...", "—").
|
||||
return s_root;
|
||||
}
|
||||
|
||||
// --- live updates (called from background tasks; take the lock here) --------
|
||||
void lisael_home_update_clock(void)
|
||||
{
|
||||
if (!s_date_lbl || !s_time_lbl) {
|
||||
return;
|
||||
}
|
||||
time_t now = time(NULL);
|
||||
struct tm tm;
|
||||
localtime_r(&now, &tm);
|
||||
|
||||
char date_buf[48];
|
||||
char time_buf[8];
|
||||
lisael_fr_date(&tm, date_buf, sizeof(date_buf));
|
||||
lisael_fr_time(&tm, time_buf, sizeof(time_buf));
|
||||
|
||||
if (bsp_display_lock(50)) {
|
||||
lv_label_set_text(s_time_lbl, lisael_time_is_valid() ? time_buf : "--:--");
|
||||
lv_label_set_text(s_date_lbl, date_buf);
|
||||
bsp_display_unlock();
|
||||
}
|
||||
}
|
||||
|
||||
void lisael_home_update_weather(void)
|
||||
{
|
||||
if (!s_weather_lbl) {
|
||||
return;
|
||||
}
|
||||
lisael_weather_t w;
|
||||
lisael_weather_get(&w);
|
||||
|
||||
char buf[32];
|
||||
if (w.valid) {
|
||||
snprintf(buf, sizeof(buf), "%.0f°C", w.temperature_c);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "—");
|
||||
}
|
||||
|
||||
if (bsp_display_lock(50)) {
|
||||
lv_label_set_text(s_weather_lbl, buf);
|
||||
lv_label_set_text(s_weather_icon,
|
||||
w.valid ? lisael_weather_code_emoji(w.weather_code) : "");
|
||||
bsp_display_unlock();
|
||||
}
|
||||
}
|
||||
|
||||
void lisael_home_update_sensors(void)
|
||||
{
|
||||
if (!s_sensor_lbl) {
|
||||
return;
|
||||
}
|
||||
char buf[32] = "";
|
||||
if (s_tsens) {
|
||||
float c = 0;
|
||||
if (temperature_sensor_get_celsius(s_tsens, &c) == ESP_OK) {
|
||||
// Internal die temperature — informational only, not room temp.
|
||||
snprintf(buf, sizeof(buf), "puce %.0f°C", c);
|
||||
}
|
||||
}
|
||||
if (bsp_display_lock(50)) {
|
||||
lv_label_set_text(s_sensor_lbl, buf);
|
||||
bsp_display_unlock();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// Webradio mode screen for Lisael Box.
|
||||
//
|
||||
// A scrollable column of big station buttons + a bottom bar with volume down /
|
||||
// stop / volume up. Tapping a station starts the ESP-ADF http_stream pipeline.
|
||||
|
||||
#include "ui/ui.h"
|
||||
#include "ui/icons.h"
|
||||
#include "modes/radios.h"
|
||||
#include "audio/radio_pipeline.h"
|
||||
#include "audio/audio_player.h"
|
||||
#include "lisael_config.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *TAG = "ui.radio";
|
||||
|
||||
static lv_obj_t *s_root;
|
||||
static lv_obj_t *s_status_lbl;
|
||||
static lv_obj_t *s_vol_lbl;
|
||||
|
||||
static void refresh_volume_label(void)
|
||||
{
|
||||
if (s_vol_lbl) {
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "%d%%", lisael_audio_get_volume());
|
||||
lv_label_set_text(s_vol_lbl, buf);
|
||||
}
|
||||
}
|
||||
|
||||
static void station_cb(lv_event_t *e)
|
||||
{
|
||||
const lisael_radio_station_t *st = lv_event_get_user_data(e);
|
||||
ESP_LOGI(TAG, "station: %s", st->name);
|
||||
esp_err_t err = lisael_radio_play(st->url);
|
||||
if (s_status_lbl) {
|
||||
lv_label_set_text_fmt(s_status_lbl, "%s %s",
|
||||
err == ESP_OK ? "Écoute :" : "Erreur :", st->name);
|
||||
}
|
||||
}
|
||||
|
||||
static void stop_cb(lv_event_t *e)
|
||||
{
|
||||
(void)e;
|
||||
lisael_audio_stop_all();
|
||||
if (s_status_lbl) {
|
||||
lv_label_set_text(s_status_lbl, "Arrêté");
|
||||
}
|
||||
}
|
||||
|
||||
static void vol_up_cb(lv_event_t *e) { (void)e; lisael_audio_volume_step(+5); refresh_volume_label(); }
|
||||
static void vol_down_cb(lv_event_t *e) { (void)e; lisael_audio_volume_step(-5); refresh_volume_label(); }
|
||||
|
||||
lv_obj_t *lisael_screen_radio(void)
|
||||
{
|
||||
if (s_root) {
|
||||
refresh_volume_label();
|
||||
return s_root;
|
||||
}
|
||||
s_root = lv_obj_create(NULL);
|
||||
lv_obj_set_style_bg_color(s_root, lv_color_hex(0xFFF4E6), 0);
|
||||
|
||||
lisael_ui_add_back_button(s_root);
|
||||
|
||||
lv_obj_t *title = lv_label_create(s_root);
|
||||
lv_label_set_text(title, LISAEL_EMOJI_RADIO " Radios");
|
||||
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 16);
|
||||
|
||||
// Station list (scrollable column).
|
||||
lv_obj_t *list = lv_obj_create(s_root);
|
||||
lv_obj_remove_style_all(list);
|
||||
lv_obj_set_size(list, LV_PCT(100), 130);
|
||||
lv_obj_align(list, LV_ALIGN_TOP_MID, 0, 66);
|
||||
lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(list, LV_FLEX_ALIGN_START,
|
||||
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_row(list, 8, 0);
|
||||
|
||||
for (size_t i = 0; i < LISAEL_RADIOS_COUNT; i++) {
|
||||
lv_obj_t *btn = lv_button_create(list);
|
||||
lv_obj_add_style(btn, lisael_ui_style_button(), 0);
|
||||
lv_obj_set_width(btn, LV_PCT(92));
|
||||
lv_obj_add_event_cb(btn, station_cb, LV_EVENT_CLICKED,
|
||||
(void *)&LISAEL_RADIOS[i]);
|
||||
lv_obj_t *lbl = lv_label_create(btn);
|
||||
lv_label_set_text_fmt(lbl, "%s %s",
|
||||
LISAEL_RADIOS[i].emoji, LISAEL_RADIOS[i].name);
|
||||
lv_obj_center(lbl);
|
||||
}
|
||||
|
||||
// Bottom control bar: vol-, stop, vol+, and a volume readout.
|
||||
lv_obj_t *bar = lv_obj_create(s_root);
|
||||
lv_obj_remove_style_all(bar);
|
||||
lv_obj_set_size(bar, LV_PCT(100), 60);
|
||||
lv_obj_align(bar, LV_ALIGN_BOTTOM_MID, 0, -2);
|
||||
lv_obj_set_flex_flow(bar, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(bar, LV_FLEX_ALIGN_SPACE_EVENLY,
|
||||
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
|
||||
struct { const char *txt; lv_event_cb_t cb; } ctrls[] = {
|
||||
{ LV_SYMBOL_MINUS, vol_down_cb },
|
||||
{ LV_SYMBOL_STOP, stop_cb },
|
||||
{ LV_SYMBOL_PLUS, vol_up_cb },
|
||||
};
|
||||
for (size_t i = 0; i < 3; i++) {
|
||||
lv_obj_t *b = lv_button_create(bar);
|
||||
lv_obj_add_style(b, lisael_ui_style_button(), 0);
|
||||
lv_obj_set_size(b, 64, 50);
|
||||
lv_obj_add_event_cb(b, ctrls[i].cb, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_t *l = lv_label_create(b);
|
||||
lv_label_set_text(l, ctrls[i].txt);
|
||||
lv_obj_center(l);
|
||||
}
|
||||
s_vol_lbl = lv_label_create(bar);
|
||||
refresh_volume_label();
|
||||
|
||||
s_status_lbl = lv_label_create(s_root);
|
||||
lv_label_set_text(s_status_lbl, "");
|
||||
lv_obj_align(s_status_lbl, LV_ALIGN_BOTTOM_MID, 0, -66);
|
||||
|
||||
return s_root;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Webradio station list for Lisael Box.
|
||||
//
|
||||
// ⚠️ À VÉRIFIER / COMPLÉTER: the stream URLs below are PLAUSIBLE placeholders.
|
||||
// French kids' webradio stream endpoints change and are not all publicly
|
||||
// documented. Before shipping, confirm each URL actually returns an MP3/AAC
|
||||
// stream (e.g. `curl -I <url>` should show audio/mpeg or audio/aac, or use the
|
||||
// station's official "écouter en direct" / TuneIn stream link). Prefer plain
|
||||
// HTTP MP3 streams for the lightest decode path on the ESP32-S3.
|
||||
#pragma once
|
||||
|
||||
typedef struct {
|
||||
const char *name; // display name
|
||||
const char *emoji; // small icon
|
||||
const char *url; // stream URL (MP3 or AAC)
|
||||
} lisael_radio_station_t;
|
||||
|
||||
// NOTE: keep this list short — each station is one big touch button.
|
||||
static const lisael_radio_station_t LISAEL_RADIOS[] = {
|
||||
// Radio Pomme d'Api (Bayard) — kids' radio.
|
||||
{ "Pomme d'Api", "\U0001F34E", // 🍎
|
||||
"https://radio.pommedapi.example/stream.mp3" }, // TODO verify
|
||||
|
||||
// Radio Doudou — lullabies / very young children.
|
||||
{ "Radio Doudou", "\U0001F9F8", // 🧸
|
||||
"http://stream.radiodoudou.example/doudou.mp3" }, // TODO verify
|
||||
|
||||
// Mmuz Kids / Mouv'Kids style FR kids music.
|
||||
{ "Muz Kids", "\U0001F3B5", // 🎵
|
||||
"http://stream.muzkids.example/kids.mp3" }, // TODO verify
|
||||
|
||||
// Une histoire et Oli (France Inter) — stories podcast stream proxy.
|
||||
{ "Histoires", "\U0001F4D6", // 📖
|
||||
"http://stream.histoires.example/oli.mp3" }, // TODO verify
|
||||
|
||||
// Comptines FR.
|
||||
{ "Comptines", "\U0001F3B6", // 🎶
|
||||
"http://stream.comptines.example/fr.mp3" }, // TODO verify
|
||||
};
|
||||
|
||||
#define LISAEL_RADIOS_COUNT \
|
||||
(sizeof(LISAEL_RADIOS) / sizeof(LISAEL_RADIOS[0]))
|
||||
@@ -0,0 +1,91 @@
|
||||
// "Boîte à sons" mode: grid of buttons reading /sdcard/sounds/*.mp3.
|
||||
//
|
||||
// Buttons come from /sdcard/sounds/manifest.json (file/title/emoji). If the
|
||||
// card is missing or has no manifest, a friendly placeholder is shown instead
|
||||
// of an error.
|
||||
|
||||
#include "ui/ui.h"
|
||||
#include "ui/icons.h"
|
||||
#include "audio/sd_player.h"
|
||||
#include "audio/audio_player.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *TAG = "ui.soundbox";
|
||||
|
||||
static lv_obj_t *s_root;
|
||||
static lisael_sound_t s_sounds[LISAEL_SD_MAX_SOUNDS];
|
||||
static int s_count;
|
||||
|
||||
static void sound_cb(lv_event_t *e)
|
||||
{
|
||||
int idx = (int)(intptr_t)lv_event_get_user_data(e);
|
||||
if (idx >= 0 && idx < s_count) {
|
||||
ESP_LOGI(TAG, "play %s", s_sounds[idx].file);
|
||||
lisael_sd_play_sound(s_sounds[idx].file);
|
||||
}
|
||||
}
|
||||
|
||||
static void build_grid(lv_obj_t *parent)
|
||||
{
|
||||
s_count = lisael_sd_load_manifest(s_sounds, LISAEL_SD_MAX_SOUNDS);
|
||||
|
||||
if (s_count <= 0) {
|
||||
lv_obj_t *info = lv_label_create(parent);
|
||||
lv_label_set_text(info,
|
||||
"Insère la carte SD\navec des sons\n(/sounds/manifest.json)");
|
||||
lv_obj_set_style_text_align(info, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_center(info);
|
||||
return;
|
||||
}
|
||||
|
||||
lv_obj_t *grid = lv_obj_create(parent);
|
||||
lv_obj_remove_style_all(grid);
|
||||
lv_obj_set_size(grid, LV_PCT(100), 170);
|
||||
lv_obj_align(grid, LV_ALIGN_BOTTOM_MID, 0, -4);
|
||||
lv_obj_set_flex_flow(grid, LV_FLEX_FLOW_ROW_WRAP);
|
||||
lv_obj_set_flex_align(grid, LV_FLEX_ALIGN_SPACE_EVENLY,
|
||||
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_row(grid, 6, 0);
|
||||
|
||||
for (int i = 0; i < s_count; i++) {
|
||||
lv_obj_t *btn = lv_button_create(grid);
|
||||
lv_obj_add_style(btn, lisael_ui_style_tile(), 0);
|
||||
lv_obj_set_style_bg_color(btn, lv_color_hex(0xD3F9D8), 0);
|
||||
lv_obj_set_size(btn, 92, 76);
|
||||
lv_obj_add_event_cb(btn, sound_cb, LV_EVENT_CLICKED,
|
||||
(void *)(intptr_t)i);
|
||||
|
||||
lv_obj_t *col = lv_obj_create(btn);
|
||||
lv_obj_remove_style_all(col);
|
||||
lv_obj_set_size(col, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_flex_flow(col, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(col, LV_FLEX_ALIGN_CENTER,
|
||||
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_clear_flag(col, LV_OBJ_FLAG_CLICKABLE);
|
||||
|
||||
lv_obj_t *emoji = lv_label_create(col);
|
||||
lv_label_set_text(emoji, s_sounds[i].emoji);
|
||||
lv_obj_t *title = lv_label_create(col);
|
||||
lv_label_set_text(title, s_sounds[i].title);
|
||||
}
|
||||
}
|
||||
|
||||
lv_obj_t *lisael_screen_soundbox(void)
|
||||
{
|
||||
if (s_root) {
|
||||
return s_root;
|
||||
}
|
||||
s_root = lv_obj_create(NULL);
|
||||
lv_obj_set_style_bg_color(s_root, lv_color_hex(0xEBFBEE), 0);
|
||||
|
||||
lisael_ui_add_back_button(s_root);
|
||||
|
||||
lv_obj_t *title = lv_label_create(s_root);
|
||||
lv_label_set_text(title, LISAEL_EMOJI_SOUND " Boîte à sons");
|
||||
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 16);
|
||||
|
||||
build_grid(s_root);
|
||||
return s_root;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// French date/time formatting for Lisael Box.
|
||||
//
|
||||
// We deliberately do NOT rely on setlocale(LC_TIME, "fr_FR") because the
|
||||
// newlib build shipped with ESP-IDF has no locale data — strftime("%A") would
|
||||
// return English. Instead we keep small FR tables and format by hand.
|
||||
//
|
||||
// Strings use UTF-8 with proper diacritics ("décembre", "février"). The LVGL
|
||||
// font in use must include the accented glyphs (the BSP default Montserrat
|
||||
// covers Latin-1; verify on hardware — see README "À vérifier").
|
||||
|
||||
#include "ui/fr_date.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
// tm_wday: 0 = Sunday .. 6 = Saturday
|
||||
static const char *const k_days[7] = {
|
||||
"dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi",
|
||||
};
|
||||
|
||||
// tm_mon: 0 = January .. 11 = December
|
||||
static const char *const k_months[12] = {
|
||||
"janvier", "février", "mars", "avril", "mai", "juin",
|
||||
"juillet", "août", "septembre", "octobre", "novembre", "décembre",
|
||||
};
|
||||
|
||||
const char *lisael_fr_weekday(int tm_wday)
|
||||
{
|
||||
if (tm_wday < 0 || tm_wday > 6) {
|
||||
return "";
|
||||
}
|
||||
return k_days[tm_wday];
|
||||
}
|
||||
|
||||
const char *lisael_fr_month(int tm_mon)
|
||||
{
|
||||
if (tm_mon < 0 || tm_mon > 11) {
|
||||
return "";
|
||||
}
|
||||
return k_months[tm_mon];
|
||||
}
|
||||
|
||||
char *lisael_fr_date(const struct tm *tm, char *out, size_t out_size)
|
||||
{
|
||||
if (!out || out_size == 0) {
|
||||
return out;
|
||||
}
|
||||
snprintf(out, out_size, "%s %d %s",
|
||||
lisael_fr_weekday(tm->tm_wday),
|
||||
tm->tm_mday,
|
||||
lisael_fr_month(tm->tm_mon));
|
||||
return out;
|
||||
}
|
||||
|
||||
char *lisael_fr_date_full(const struct tm *tm, char *out, size_t out_size)
|
||||
{
|
||||
if (!out || out_size == 0) {
|
||||
return out;
|
||||
}
|
||||
snprintf(out, out_size, "%s %d %s %d",
|
||||
lisael_fr_weekday(tm->tm_wday),
|
||||
tm->tm_mday,
|
||||
lisael_fr_month(tm->tm_mon),
|
||||
tm->tm_year + 1900);
|
||||
return out;
|
||||
}
|
||||
|
||||
char *lisael_fr_time(const struct tm *tm, char *out, size_t out_size)
|
||||
{
|
||||
if (!out || out_size == 0) {
|
||||
return out;
|
||||
}
|
||||
snprintf(out, out_size, "%02d:%02d", tm->tm_hour, tm->tm_min);
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// French date/time formatting for Lisael Box (no setlocale dependency).
|
||||
#pragma once
|
||||
|
||||
#include <time.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Format a struct tm as a French date string, e.g. "samedi 14 juin".
|
||||
// Writes into `out` (NUL-terminated, truncated to `out_size`). The year is
|
||||
// omitted by default (kid-friendly). Returns `out`.
|
||||
char *lisael_fr_date(const struct tm *tm, char *out, size_t out_size);
|
||||
|
||||
// Format "samedi 14 juin 2026" (with year).
|
||||
char *lisael_fr_date_full(const struct tm *tm, char *out, size_t out_size);
|
||||
|
||||
// Format the time as "HH:MM", e.g. "07:05". Returns `out`.
|
||||
char *lisael_fr_time(const struct tm *tm, char *out, size_t out_size);
|
||||
|
||||
// Day-of-week name in French, 0=dimanche .. 6=samedi (matches tm_wday).
|
||||
const char *lisael_fr_weekday(int tm_wday);
|
||||
|
||||
// Month name in French, 0=janvier .. 11=decembre (matches tm_mon).
|
||||
const char *lisael_fr_month(int tm_mon);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,5 @@
|
||||
// Icon helpers translation unit — currently header-only (macros in icons.h).
|
||||
// Kept as a compiled unit so future icon image descriptors / converted emoji
|
||||
// fonts have a home and the build file list stays stable.
|
||||
|
||||
#include "ui/icons.h"
|
||||
@@ -0,0 +1,32 @@
|
||||
// Icon / emoji helpers for the Lisael Box UI.
|
||||
//
|
||||
// The home tiles and many buttons are icon-first (no reading required). We use
|
||||
// emoji glyphs as the "icons". IMPORTANT (verify on hardware): the default
|
||||
// LVGL Montserrat fonts do NOT contain emoji glyphs. To actually render these
|
||||
// you must add an emoji-capable font (e.g. a subset of Noto Color Emoji built
|
||||
// with lv_font_conv, or LVGL's built-in symbol set LV_SYMBOL_*). Until then
|
||||
// the emoji may show as tofu boxes. The text labels below always render.
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Mode tile glyphs (UTF-8 emoji). Swap for a bundled emoji font or replace with
|
||||
// LV_SYMBOL_* if you prefer the built-in monochrome icon set.
|
||||
#define LISAEL_EMOJI_RADIO "\U0001F4FB" // 📻
|
||||
#define LISAEL_EMOJI_SOUND "\U0001F50A" // 🔊
|
||||
#define LISAEL_EMOJI_CALM "\U0001FAE7" // 🫧 bubbles
|
||||
#define LISAEL_EMOJI_CLOCK "\U0001F550" // 🕐
|
||||
#define LISAEL_EMOJI_GAMES "\U0001F3AE" // 🎮
|
||||
#define LISAEL_EMOJI_HOME "\U0001F3E0" // 🏠
|
||||
#define LISAEL_EMOJI_PLAY "▶" // ▶
|
||||
#define LISAEL_EMOJI_STOP "⏹" // ⏹
|
||||
#define LISAEL_EMOJI_VOL_UP "\U0001F509" // 🔉 (up)
|
||||
#define LISAEL_EMOJI_VOL_DOWN "\U0001F508" // 🔈 (down)
|
||||
#define LISAEL_EMOJI_STAR "⭐" // ⭐ (positive feedback)
|
||||
#define LISAEL_EMOJI_HEART "❤️" // ❤️
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
// LVGL UI shell + mode router for Lisael Box (LVGL 9 API).
|
||||
//
|
||||
// Screens are built lazily and cached so switching back is instant. Mode
|
||||
// transitions use lv_screen_load_anim() for a gentle slide. All LVGL access
|
||||
// from background tasks goes through bsp_display_lock()/unlock(); the functions
|
||||
// here are called either from app_main during init (lock held by caller) or
|
||||
// from LVGL event callbacks (already on the LVGL task).
|
||||
|
||||
#include "ui/ui.h"
|
||||
#include "ui/icons.h"
|
||||
#include "lisael_config.h"
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "bsp/esp-box-3.h"
|
||||
|
||||
static const char *TAG = "ui";
|
||||
|
||||
static lv_obj_t *s_screens[LISAEL_MODE_COUNT];
|
||||
static lisael_mode_t s_current = LISAEL_MODE_HOME;
|
||||
|
||||
static lv_style_t s_style_tile;
|
||||
static lv_style_t s_style_button;
|
||||
static bool s_styles_ready;
|
||||
|
||||
static void init_styles(void)
|
||||
{
|
||||
if (s_styles_ready) {
|
||||
return;
|
||||
}
|
||||
// Big rounded pastel tiles for the home menu.
|
||||
lv_style_init(&s_style_tile);
|
||||
lv_style_set_radius(&s_style_tile, 18);
|
||||
lv_style_set_bg_opa(&s_style_tile, LV_OPA_COVER);
|
||||
lv_style_set_border_width(&s_style_tile, 0);
|
||||
lv_style_set_pad_all(&s_style_tile, 6);
|
||||
lv_style_set_shadow_width(&s_style_tile, 8);
|
||||
lv_style_set_shadow_ofs_y(&s_style_tile, 3);
|
||||
lv_style_set_shadow_opa(&s_style_tile, LV_OPA_20);
|
||||
|
||||
// Generic large touch button.
|
||||
lv_style_init(&s_style_button);
|
||||
lv_style_set_radius(&s_style_button, 14);
|
||||
lv_style_set_pad_all(&s_style_button, 10);
|
||||
lv_style_set_min_height(&s_style_button, 56); // comfortable for small hands
|
||||
|
||||
s_styles_ready = true;
|
||||
}
|
||||
|
||||
const lv_style_t *lisael_ui_style_tile(void)
|
||||
{
|
||||
init_styles();
|
||||
return &s_style_tile;
|
||||
}
|
||||
|
||||
const lv_style_t *lisael_ui_style_button(void)
|
||||
{
|
||||
init_styles();
|
||||
return &s_style_button;
|
||||
}
|
||||
|
||||
static lv_obj_t *build_screen(lisael_mode_t mode)
|
||||
{
|
||||
switch (mode) {
|
||||
case LISAEL_MODE_HOME: return lisael_screen_home();
|
||||
case LISAEL_MODE_RADIO: return lisael_screen_radio();
|
||||
case LISAEL_MODE_SOUNDBOX: return lisael_screen_soundbox();
|
||||
case LISAEL_MODE_CALM: return lisael_screen_calm();
|
||||
case LISAEL_MODE_CLOCK: return lisael_screen_clock();
|
||||
case LISAEL_MODE_GAMES: return lisael_screen_games();
|
||||
default: return lisael_screen_home();
|
||||
}
|
||||
}
|
||||
|
||||
void lisael_ui_goto(lisael_mode_t mode)
|
||||
{
|
||||
if (mode < 0 || mode >= LISAEL_MODE_COUNT) {
|
||||
return;
|
||||
}
|
||||
if (!s_screens[mode]) {
|
||||
s_screens[mode] = build_screen(mode);
|
||||
}
|
||||
// Slide direction: going "deeper" (home->mode) slides left, back slides right.
|
||||
lv_screen_load_anim_t anim = (mode == LISAEL_MODE_HOME)
|
||||
? LV_SCR_LOAD_ANIM_MOVE_RIGHT
|
||||
: LV_SCR_LOAD_ANIM_MOVE_LEFT;
|
||||
lv_screen_load_anim(s_screens[mode], anim, 250, 0, false);
|
||||
s_current = mode;
|
||||
ESP_LOGI(TAG, "mode -> %d", mode);
|
||||
}
|
||||
|
||||
static void back_cb(lv_event_t *e)
|
||||
{
|
||||
(void)e;
|
||||
lisael_ui_goto(LISAEL_MODE_HOME);
|
||||
}
|
||||
|
||||
lv_obj_t *lisael_ui_add_back_button(lv_obj_t *parent)
|
||||
{
|
||||
lv_obj_t *btn = lv_button_create(parent);
|
||||
lv_obj_add_style(btn, lisael_ui_style_button(), 0);
|
||||
lv_obj_set_size(btn, 64, 56);
|
||||
lv_obj_align(btn, LV_ALIGN_TOP_LEFT, 6, 6);
|
||||
lv_obj_add_event_cb(btn, back_cb, LV_EVENT_CLICKED, NULL);
|
||||
|
||||
lv_obj_t *lbl = lv_label_create(btn);
|
||||
lv_label_set_text(lbl, LV_SYMBOL_LEFT); // monochrome arrow — always renders
|
||||
lv_obj_center(lbl);
|
||||
return btn;
|
||||
}
|
||||
|
||||
void lisael_ui_init(void)
|
||||
{
|
||||
init_styles();
|
||||
s_screens[LISAEL_MODE_HOME] = lisael_screen_home();
|
||||
lv_screen_load(s_screens[LISAEL_MODE_HOME]);
|
||||
s_current = LISAEL_MODE_HOME;
|
||||
ESP_LOGI(TAG, "UI initialised");
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// LVGL UI shell + mode router for Lisael Box.
|
||||
//
|
||||
// The UI runs entirely on the LVGL task created by the esp-box BSP
|
||||
// (bsp_display_start / esp_lvgl_port). ALL LVGL calls from other tasks (e.g.
|
||||
// the weather task pushing an update) MUST be wrapped in
|
||||
// bsp_display_lock()/bsp_display_unlock(). Helpers here assume they are called
|
||||
// while holding that lock unless noted otherwise.
|
||||
#pragma once
|
||||
|
||||
#include "lvgl.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// The available screens / modes.
|
||||
typedef enum {
|
||||
LISAEL_MODE_HOME = 0, // accueil: date FR + meteo + capteurs + tuiles
|
||||
LISAEL_MODE_RADIO, // webradios enfants
|
||||
LISAEL_MODE_SOUNDBOX, // boite a sons (SD)
|
||||
LISAEL_MODE_CALM, // coin calme / respiration
|
||||
LISAEL_MODE_CLOCK, // horloge / veilleuse
|
||||
LISAEL_MODE_GAMES, // jeux (memory, compter)
|
||||
LISAEL_MODE_COUNT,
|
||||
} lisael_mode_t;
|
||||
|
||||
// Build the UI (call once, while holding the display lock). Loads HOME.
|
||||
void lisael_ui_init(void);
|
||||
|
||||
// Switch to a mode. Each mode lazily (re-)builds its screen and is loaded with
|
||||
// a gentle slide animation. Safe to call from LVGL callbacks.
|
||||
void lisael_ui_goto(lisael_mode_t mode);
|
||||
|
||||
// Convenience used by every mode screen to add a top-left "← Accueil" button
|
||||
// (big, icon-only) that returns HOME. Returns the button object.
|
||||
lv_obj_t *lisael_ui_add_back_button(lv_obj_t *parent);
|
||||
|
||||
// Shared style accessors (lazily initialised). Big rounded buttons, soft
|
||||
// pastel palette suited to a young child.
|
||||
const lv_style_t *lisael_ui_style_tile(void); // home menu tiles
|
||||
const lv_style_t *lisael_ui_style_button(void); // generic big buttons
|
||||
|
||||
// --- Each mode exposes a builder that returns its root screen object. --------
|
||||
// (Implemented in modes/*.c.)
|
||||
lv_obj_t *lisael_screen_home(void);
|
||||
lv_obj_t *lisael_screen_radio(void);
|
||||
lv_obj_t *lisael_screen_soundbox(void);
|
||||
lv_obj_t *lisael_screen_calm(void);
|
||||
lv_obj_t *lisael_screen_clock(void);
|
||||
lv_obj_t *lisael_screen_games(void);
|
||||
|
||||
// Home screen live-update hooks (called from background tasks; they take the
|
||||
// display lock internally — do NOT hold it when calling these).
|
||||
void lisael_home_update_clock(void); // refresh date/time labels
|
||||
void lisael_home_update_weather(void); // pull latest weather snapshot
|
||||
void lisael_home_update_sensors(void); // pull latest sensor readings
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user