Files
lisael-box/main/modes/home.c
T
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

328 lines
11 KiB
C

// 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/assets/lisael_icons.h"
#include "ui/fonts/lisael_fonts.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 lv_image_dsc_t *icon;
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);
}
// --- activity carousel (Lunii/Merlin style launcher) -------------------------
// One big activity tile per full-width slide; swipe (or arrows) to move one at a
// time, tap the tile to launch. Dots show the position.
#define HOME_SLIDE_W 320
static lv_obj_t *s_track;
static lv_obj_t *s_adots[8];
static int s_acur;
static int s_n_acts;
static void adots_update(int idx)
{
for (int i = 0; i < s_n_acts; i++) {
if (s_adots[i]) {
lv_obj_set_style_bg_opa(s_adots[i], i == idx ? LV_OPA_COVER : LV_OPA_30, 0);
}
}
}
static void home_scroll_cb(lv_event_t *e)
{
lv_obj_t *t = lv_event_get_target(e);
int idx = (lv_obj_get_scroll_x(t) + HOME_SLIDE_W / 2) / HOME_SLIDE_W;
if (idx < 0) idx = 0;
if (idx >= s_n_acts) idx = s_n_acts - 1;
s_acur = idx;
adots_update(idx);
}
static void home_arrow_cb(lv_event_t *e)
{
int idx = s_acur + (int)(intptr_t)lv_event_get_user_data(e);
if (idx < 0) idx = 0;
if (idx >= s_n_acts) idx = s_n_acts - 1;
s_acur = idx;
lv_obj_scroll_to_x(s_track, idx * HOME_SLIDE_W, LV_ANIM_ON);
adots_update(idx);
}
static void home_arrow(lv_obj_t *parent, const char *sym, lv_align_t al, int dir)
{
lv_obj_t *b = lv_button_create(parent);
lv_obj_remove_style_all(b);
lv_obj_set_size(b, 38, 60);
lv_obj_set_style_radius(b, 19, 0);
lv_obj_set_style_bg_opa(b, LV_OPA_40, 0);
lv_obj_set_style_bg_color(b, lv_color_white(), 0);
lv_obj_align(b, al, (al == LV_ALIGN_LEFT_MID) ? 2 : -2, 28);
lv_obj_add_event_cb(b, home_arrow_cb, LV_EVENT_CLICKED, (void *)(intptr_t)dir);
lv_obj_t *l = lv_label_create(b);
lv_label_set_text(l, sym);
lv_obj_set_style_text_color(l, lv_color_hex(0x495057), 0);
lv_obj_center(l);
}
static void add_act_slide(lv_obj_t *track, const tile_def_t *def)
{
lv_obj_t *slide = lv_obj_create(track);
lv_obj_remove_style_all(slide);
lv_obj_set_size(slide, HOME_SLIDE_W, LV_PCT(100));
lv_obj_clear_flag(slide, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_flex_flow(slide, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(slide, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t *tile = lv_button_create(slide);
lv_obj_add_style(tile, lisael_ui_style_tile(), 0);
lv_obj_set_style_bg_color(tile, lv_color_lighten(def->color, 90), 0);
lv_obj_set_style_bg_grad_color(tile, def->color, 0);
lv_obj_set_style_translate_y(tile, 4, LV_STATE_PRESSED);
lv_obj_set_style_shadow_ofs_y(tile, 3, LV_STATE_PRESSED);
lv_obj_set_size(tile, 176, 122);
lv_obj_set_flex_flow(tile, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(tile, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_row(tile, 6, 0);
lv_obj_clear_flag(tile, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_event_cb(tile, tile_cb, LV_EVENT_CLICKED, (void *)(intptr_t)def->mode);
lv_obj_t *icon = lv_image_create(tile);
lv_image_set_src(icon, def->icon);
lv_obj_set_size(icon, 64, 64); // scale-to-fit by size (reliable, unlike set_scale)
lv_image_set_inner_align(icon, LV_IMAGE_ALIGN_CONTAIN);
lv_obj_t *txt = lv_label_create(tile);
lv_label_set_text(txt, def->label);
lv_obj_set_style_text_font(txt, &lisael_font_24, 0);
}
// --- 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);
// Soft vertical sky gradient (light blue at the top fading to white).
lv_obj_set_style_bg_color(s_root, lv_color_hex(0xCDE8FF), 0);
lv_obj_set_style_bg_grad_color(s_root, lv_color_hex(0xF6FBFF), 0);
lv_obj_set_style_bg_grad_dir(s_root, LV_GRAD_DIR_VER, 0);
// --- 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), 74);
lv_obj_align(header, LV_ALIGN_TOP_MID, 0, 2);
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, &lisael_font_30, 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_image_create(header);
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);
// --- activities: horizontal carousel (one big tile per slide) -----------
static const tile_def_t tiles[] = {
{ &lisael_ic_histoires, "Histoires", LISAEL_MODE_HISTOIRES, {0} },
{ &lisael_ic_book, "Lire", LISAEL_MODE_LIRE, {0} },
{ &lisael_ic_radio, "Radio", LISAEL_MODE_RADIO, {0} },
{ &lisael_ic_calm, "Calme", LISAEL_MODE_CALM, {0} },
{ &lisael_ic_clock, "Horloge", LISAEL_MODE_CLOCK, {0} },
{ &lisael_ic_games, "Jeux", LISAEL_MODE_GAMES, {0} },
{ &lisael_ic_reglages, "Réglages", LISAEL_MODE_WIFI, {0} },
};
static const uint32_t colors[] = {
0xFFE08A, 0xFFD8A8, 0xB2F2BB, 0xA5D8FF, 0xD0BFFF, 0xFFC9C9, 0xCED4DA,
};
s_n_acts = (int)(sizeof(tiles) / sizeof(tiles[0]));
s_track = lv_obj_create(s_root);
lv_obj_remove_style_all(s_track);
lv_obj_set_size(s_track, HOME_SLIDE_W, 134);
lv_obj_align(s_track, LV_ALIGN_TOP_MID, 0, 80);
lv_obj_set_flex_flow(s_track, LV_FLEX_FLOW_ROW);
lv_obj_set_style_pad_all(s_track, 0, 0);
lv_obj_set_style_pad_column(s_track, 0, 0);
lv_obj_set_scroll_dir(s_track, LV_DIR_HOR);
lv_obj_set_scroll_snap_x(s_track, LV_SCROLL_SNAP_CENTER);
lv_obj_set_scrollbar_mode(s_track, LV_SCROLLBAR_MODE_OFF);
lv_obj_add_event_cb(s_track, home_scroll_cb, LV_EVENT_SCROLL_END, NULL);
for (int i = 0; i < s_n_acts; i++) {
tile_def_t t = tiles[i];
t.color = lv_color_hex(colors[i]);
add_act_slide(s_track, &t);
}
lv_obj_t *dotrow = lv_obj_create(s_root);
lv_obj_remove_style_all(dotrow);
lv_obj_set_size(dotrow, LV_PCT(90), 16);
lv_obj_align(dotrow, LV_ALIGN_BOTTOM_MID, 0, -6);
lv_obj_set_flex_flow(dotrow, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(dotrow, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_clear_flag(dotrow, LV_OBJ_FLAG_SCROLLABLE);
for (int i = 0; i < s_n_acts; i++) {
lv_obj_t *d = lv_obj_create(dotrow);
lv_obj_remove_style_all(d);
lv_obj_set_size(d, 9, 9);
lv_obj_set_style_radius(d, 5, 0);
lv_obj_set_style_margin_left(d, 3, 0);
lv_obj_set_style_margin_right(d, 3, 0);
lv_obj_set_style_bg_color(d, lv_color_hex(0x1971C2), 0);
lv_obj_set_style_bg_opa(d, i == 0 ? LV_OPA_COVER : LV_OPA_30, 0);
s_adots[i] = d;
}
home_arrow(s_root, LV_SYMBOL_LEFT, LV_ALIGN_LEFT_MID, -1);
home_arrow(s_root, LV_SYMBOL_RIGHT, LV_ALIGN_RIGHT_MID, +1);
s_acur = 0;
adots_update(0);
// 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);
if (w.valid) {
lv_image_set_src(s_weather_icon,
lisael_weather_code_icon(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();
}
}