7c8630abbe
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.
328 lines
11 KiB
C
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();
|
|
}
|
|
}
|