/*
 * ESP32-S3 + ST77916 QSPI 圆屏 + CST816S 触摸 + LVGL 9 主程序
 */

#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "driver/i2c_master.h"
#include "driver/ledc.h"
#include "driver/spi_master.h"
#include "esp_check.h"
#include "esp_heap_caps.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_touch.h"
#include "esp_lcd_touch_cst816s.h"
#include "esp_log.h"
#include "esp_lvgl_port.h"
#include "lvgl.h"

#include "config.h"
#include "esp_lcd_st77916.h"
#include "esp_lcd_st77916_1_8in.h"
#include "esp_lcd_st77916_2_1in.h"
#include "imu_qmi8658.h"
#include "imu_visual_assets.h"
#include "pmu_axp2101.h"
#include "sd_jpg_browser.h"

/* 屏型号选择：0 = 1.8寸 ST77916 BOE，1 = 2.1寸 GC9B72 BOE。两屏 QSPI/触摸引脚一致，仅初始化表不同。 */
#define BOARD_PANEL_2_1IN       0

#if BOARD_PANEL_2_1IN
#define LCD_PANEL_DESC          "2.1in GC9B72 BOE"
#define LCD_PANEL_INIT_CMDS     st77916_2_1in_boe_init_cmds
#define LCD_PANEL_INIT_CMDS_SZ  ST77916_2_1IN_BOE_INIT_CMDS_SIZE
#else
#define LCD_PANEL_DESC          "1.8in ST77916 BOE"
#define LCD_PANEL_INIT_CMDS     st77916_1_8in_boe_init_cmds
#define LCD_PANEL_INIT_CMDS_SZ  ST77916_1_8IN_BOE_INIT_CMDS_SIZE
#endif

#define LCD_H_RES               360
#define LCD_V_RES               360
#define LCD_PIN_DATA0           GPIO_NUM_7
#define LCD_PIN_DATA1           GPIO_NUM_8
#define LCD_PIN_DATA2           GPIO_NUM_9
#define LCD_PIN_DATA3           GPIO_NUM_10
#define LCD_PIN_CS              GPIO_NUM_11
#define LCD_PIN_PCLK            GPIO_NUM_12
#define LCD_PIN_RST             GPIO_NUM_14
#define LCD_PIN_BK_LIGHT        GPIO_NUM_18
#define LCD_SPI_HOST            SPI2_HOST

#define TOUCH_PIN_SDA           GPIO_NUM_16
#define TOUCH_PIN_SCL           GPIO_NUM_15
#define TOUCH_PIN_INT           GPIO_NUM_17
/* 原理图中 IO6 是 SD_D2，触摸无独立复位脚，不能占用 GPIO6。 */
#define TOUCH_PIN_RST           GPIO_NUM_NC
#define TOUCH_USE_INT           0

#define BOOT_PIN                GPIO_NUM_0
#define LVGL_BUF_HEIGHT         40
#define LVGL_TASK_STACK_SIZE    (10 * 1024)
#define LVGL_TASK_PRIORITY      2
#define IMU_PHYSICS_TIMER_MS    20
#define PMU_TIMER_MS            1000
#define IMU_BALL_SIZE           40
#define IMU_COLLISION_FLASH_TICKS 5
#define IMU_ACCEL_FILTER_ALPHA  0.24f
#define IMU_ACCEL_TO_PIXEL      1850.0f
#define IMU_BALL_DAMPING        0.965f
#define IMU_BALL_BOUNCE         0.42f
#define IMU_BALL_MAX_SPEED      1150.0f
#define IMU_BALL_EDGE_MARGIN    2.0f
#define DASHBOARD_SWIPE_MIN_PX  45
#define DASHBOARD_SWIPE_AXIS_GAIN 1
#define DASHBOARD_PAGE_COUNT    3
#define BOOT_POLL_MS            20
#define BOOT_DEBOUNCE_MS        60

LV_FONT_DECLARE(jsyjst_18);
#define UI_FONT                 (&jsyjst_18)

static const char *TAG = "main";

typedef enum {
    DASHBOARD_PAGE_IMU = 0,
    DASHBOARD_PAGE_PMU,
    DASHBOARD_PAGE_SD,
} dashboard_page_t;

typedef struct {
    lv_obj_t *imu_page;
    lv_obj_t *imu_bg;
    lv_obj_t *imu_ball;
    lv_obj_t *pmu_page;
    lv_obj_t *pmu_status;
    lv_obj_t *pmu_battery;
    lv_obj_t *pmu_voltage;
    lv_obj_t *pmu_temp;
    lv_obj_t *pmu_bar;
    lv_obj_t *pmu_grid;
    lv_obj_t *pmu_chip[6];
    lv_obj_t *sd_page;
    lv_obj_t *sd_status;
    lv_obj_t *sd_full_layer;
    lv_obj_t *sd_full_image;
} dashboard_ui_t;

typedef struct {
    esp_lcd_touch_handle_t handle;
    lv_point_t last_point;
} touch_poll_ctx_t;

static bool s_imu_ready = false;
static bool s_pmu_ready = false;
static dashboard_page_t s_active_page = DASHBOARD_PAGE_IMU;
static uint8_t s_collision_flash_ticks = 0;
static uint8_t s_last_collision_mask = 0;
static bool s_ball_collision_color = false;
static float s_ball_x = (LCD_H_RES - IMU_BALL_SIZE) * 0.5f;
static float s_ball_y = (LCD_V_RES - IMU_BALL_SIZE) * 0.5f;
static float s_ball_vx = 0.0f;
static float s_ball_vy = 0.0f;
static float s_filtered_accel_x = 0.0f;
static float s_filtered_accel_y = 0.0f;
static dashboard_ui_t s_ui = {0};
static lv_point_t s_swipe_start_point = {0};
static bool s_swipe_tracking = false;
static size_t s_sd_selected = 0;
static bool s_sd_fullscreen = false;
static sd_jpg_image_t s_sd_full_image = {0};
static lv_image_dsc_t s_sd_full_dsc = {0};

static touch_poll_ctx_t s_touch_poll_ctx = {
    .handle = NULL,
    .last_point = {
        .x = LCD_H_RES / 2,
        .y = LCD_V_RES / 2,
    },
};

static void backlight_set(uint8_t brightness)
{
    if (brightness > 100) {
        brightness = 100;
    }

    uint32_t duty = (1023 * brightness) / 100;
    ESP_ERROR_CHECK(ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, duty));
    ESP_ERROR_CHECK(ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0));
    ESP_LOGI(TAG, "💡 背光亮度设置为 %u%%", brightness);
}

static void backlight_init(void)
{
    const ledc_timer_config_t timer_cfg = {
        .speed_mode = LEDC_LOW_SPEED_MODE,
        .duty_resolution = LEDC_TIMER_10_BIT,
        .timer_num = LEDC_TIMER_0,
        .freq_hz = 5000,
        .clk_cfg = LEDC_AUTO_CLK,
    };
    ESP_ERROR_CHECK(ledc_timer_config(&timer_cfg));

    const ledc_channel_config_t channel_cfg = {
        .gpio_num = LCD_PIN_BK_LIGHT,
        .speed_mode = LEDC_LOW_SPEED_MODE,
        .channel = LEDC_CHANNEL_0,
        .intr_type = LEDC_INTR_DISABLE,
        .timer_sel = LEDC_TIMER_0,
        .duty = 0,
        .hpoint = 0,
    };
    ESP_ERROR_CHECK(ledc_channel_config(&channel_cfg));
    ESP_LOGI(TAG, "💡 背光 PWM 初始化完成");
}

static void lcd_fill_black(esp_lcd_panel_handle_t panel)
{
    uint16_t *line_buf = heap_caps_calloc(LCD_H_RES, sizeof(uint16_t), MALLOC_CAP_DMA);
    if (line_buf == NULL) {
        ESP_LOGW(TAG, "⚠️ 清屏缓冲分配失败，跳过黑屏填充");
        return;
    }

    for (int y = 0; y < LCD_V_RES; y++) {
        ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(panel, 0, y, LCD_H_RES, y + 1, line_buf));
        if ((y % 40) == 39) {
            vTaskDelay(pdMS_TO_TICKS(1));
        }
    }

    free(line_buf);
}

static esp_err_t lcd_init(esp_lcd_panel_handle_t *ret_panel, esp_lcd_panel_io_handle_t *ret_io)
{
    ESP_LOGI(TAG, "🖥️ 初始化 ST77916 QSPI 显示屏...");
    backlight_init();

    ESP_LOGI(TAG, "⚙️ 配置 QSPI 总线...");
    const spi_bus_config_t bus_cfg = ST77916_PANEL_BUS_QSPI_CONFIG(
        LCD_PIN_PCLK,
        LCD_PIN_DATA0,
        LCD_PIN_DATA1,
        LCD_PIN_DATA2,
        LCD_PIN_DATA3,
        LCD_H_RES * LCD_V_RES * sizeof(uint16_t)
    );
    ESP_RETURN_ON_ERROR(spi_bus_initialize(LCD_SPI_HOST, &bus_cfg, SPI_DMA_CH_AUTO),
                        TAG, "❌ SPI 总线初始化失败");

    ESP_LOGI(TAG, "⚙️ 配置 LCD Panel IO...");
    const esp_lcd_panel_io_spi_config_t io_cfg = ST77916_PANEL_IO_QSPI_CONFIG(LCD_PIN_CS, NULL, NULL);

    esp_lcd_panel_io_handle_t io = NULL;
    ESP_RETURN_ON_ERROR(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_SPI_HOST, &io_cfg, &io),
                        TAG, "❌ LCD Panel IO 创建失败");

    ESP_LOGI(TAG, "🔧 创建 ST77916 面板（%s）...", LCD_PANEL_DESC);
    const st77916_vendor_config_t vendor_cfg = {
        .init_cmds = LCD_PANEL_INIT_CMDS,
        .init_cmds_size = LCD_PANEL_INIT_CMDS_SZ,
    };
    const esp_lcd_panel_dev_config_t panel_cfg = {
        .reset_gpio_num = LCD_PIN_RST,
        .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
        .bits_per_pixel = 16,
        .vendor_config = (void *)&vendor_cfg,
    };

    esp_lcd_panel_handle_t panel = NULL;
    ESP_RETURN_ON_ERROR(esp_lcd_new_panel_st77916(io, &panel_cfg, &panel),
                        TAG, "❌ ST77916 面板创建失败");
    ESP_RETURN_ON_ERROR(esp_lcd_panel_reset(panel), TAG, "❌ 面板复位失败");
    ESP_RETURN_ON_ERROR(esp_lcd_panel_init(panel), TAG, "❌ 面板初始化失败");
    ESP_RETURN_ON_ERROR(esp_lcd_panel_disp_on_off(panel, true), TAG, "❌ 面板开启失败");

    lcd_fill_black(panel);

    *ret_panel = panel;
    *ret_io = io;
    ESP_LOGI(TAG, "✅ ST77916 显示屏初始化完成 (%dx%d)", LCD_H_RES, LCD_V_RES);
    return ESP_OK;
}

static esp_err_t board_i2c_init(i2c_master_bus_handle_t *ret_i2c)
{
    if (ret_i2c == NULL) {
        return ESP_ERR_INVALID_ARG;
    }

    ESP_LOGI(TAG, "⚙️ 初始化 I2C0 总线...");
    i2c_master_bus_handle_t i2c = NULL;
    const i2c_master_bus_config_t i2c_cfg = {
        .i2c_port = BOARD_I2C_NUM,
        .sda_io_num = TOUCH_PIN_SDA,
        .scl_io_num = TOUCH_PIN_SCL,
        .clk_source = I2C_CLK_SRC_DEFAULT,
        .glitch_ignore_cnt = 7,
        .flags.enable_internal_pullup = true,
    };
    ESP_RETURN_ON_ERROR(i2c_new_master_bus(&i2c_cfg, &i2c), TAG, "❌ I2C0 总线创建失败");

    *ret_i2c = i2c;
    ESP_LOGI(TAG, "✅ I2C0 总线初始化完成，SDA=%d SCL=%d", TOUCH_PIN_SDA, TOUCH_PIN_SCL);
    return ESP_OK;
}

static esp_err_t touch_init(i2c_master_bus_handle_t i2c, esp_lcd_touch_handle_t *ret_touch)
{
    if (i2c == NULL || ret_touch == NULL) {
        return ESP_ERR_INVALID_ARG;
    }

    ESP_LOGI(TAG, "🖱️ 初始化 CST816S 触摸屏...");

    esp_lcd_panel_io_handle_t touch_io = NULL;
    const esp_lcd_panel_io_i2c_config_t touch_io_cfg = ESP_LCD_TOUCH_IO_I2C_CST816S_CONFIG();
    ESP_RETURN_ON_ERROR(esp_lcd_new_panel_io_i2c(i2c, &touch_io_cfg, &touch_io),
                        TAG, "❌ 触摸 IO 创建失败");

    const esp_lcd_touch_config_t touch_cfg = {
        .x_max = LCD_H_RES,
        .y_max = LCD_V_RES,
        .rst_gpio_num = TOUCH_PIN_RST,
        .int_gpio_num = TOUCH_USE_INT ? TOUCH_PIN_INT : GPIO_NUM_NC,
        .levels = {
            .reset = 0,
            .interrupt = 0,
        },
        .flags = {
            .swap_xy = 0,
            .mirror_x = 0,
            .mirror_y = 0,
        },
    };

    esp_lcd_touch_handle_t touch = NULL;
    ESP_RETURN_ON_ERROR(esp_lcd_touch_new_i2c_cst816s(touch_io, &touch_cfg, &touch),
                        TAG, "❌ CST816S 触摸驱动创建失败");

    *ret_touch = touch;
#if TOUCH_USE_INT
    ESP_LOGI(TAG, "✅ CST816S 触摸屏初始化完成（INT 模式）");
#else
    esp_log_level_set("CST816S", ESP_LOG_NONE);
    ESP_LOGI(TAG, "✅ CST816S 触摸屏初始化完成（轮询模式）");
#endif
    return ESP_OK;
}

static void lvgl_touch_poll_read(lv_indev_t *indev, lv_indev_data_t *data)
{
    touch_poll_ctx_t *ctx = (touch_poll_ctx_t *)lv_indev_get_driver_data(indev);
    if (ctx == NULL || ctx->handle == NULL) {
        data->state = LV_INDEV_STATE_RELEASED;
        return;
    }

    esp_lcd_touch_point_data_t touch_data[1] = {0};
    uint8_t touch_cnt = 0;
    esp_err_t ret = esp_lcd_touch_read_data(ctx->handle);
    if (ret == ESP_OK) {
        ret = esp_lcd_touch_get_data(ctx->handle, touch_data, &touch_cnt, 1);
    }

    if (ret == ESP_OK && touch_cnt > 0) {
        ctx->last_point.x = touch_data[0].x;
        ctx->last_point.y = touch_data[0].y;
        data->point = ctx->last_point;
        data->state = LV_INDEV_STATE_PRESSED;
        return;
    }

    data->point = ctx->last_point;
    data->state = LV_INDEV_STATE_RELEASED;
}

static lv_indev_t *lvgl_add_poll_touch(lv_display_t *disp, esp_lcd_touch_handle_t touch)
{
    if (!lvgl_port_lock(1000)) {
        return NULL;
    }

    s_touch_poll_ctx.handle = touch;
    lv_indev_t *indev = lv_indev_create();
    if (indev != NULL) {
        lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
        lv_indev_set_mode(indev, LV_INDEV_MODE_TIMER);
        lv_indev_set_read_cb(indev, lvgl_touch_poll_read);
        lv_indev_set_disp(indev, disp);
        lv_indev_set_driver_data(indev, &s_touch_poll_ctx);
    }

    lvgl_port_unlock();
    return indev;
}

static esp_err_t lvgl_init(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t io, esp_lcd_touch_handle_t touch)
{
    ESP_LOGI(TAG, "🖼️ 初始化 LVGL...");

    const lvgl_port_cfg_t lvgl_cfg = {
        .task_priority = LVGL_TASK_PRIORITY,
        .task_stack = LVGL_TASK_STACK_SIZE,
        .task_affinity = -1,
        .task_max_sleep_ms = 500,
        .timer_period_ms = 5,
    };
    ESP_RETURN_ON_ERROR(lvgl_port_init(&lvgl_cfg), TAG, "❌ LVGL 端口初始化失败");

    const lvgl_port_display_cfg_t disp_cfg = {
        .io_handle = io,
        .panel_handle = panel,
        .buffer_size = LCD_H_RES * LVGL_BUF_HEIGHT,
        .double_buffer = true,
        .hres = LCD_H_RES,
        .vres = LCD_V_RES,
        .monochrome = false,
        .color_format = LV_COLOR_FORMAT_RGB565,
        .rotation = {
            .swap_xy = false,
            .mirror_x = false,
            .mirror_y = false,
        },
        .flags = {
            .buff_dma = true,
            .buff_spiram = false,
            .swap_bytes = true,
        },
    };

    lv_display_t *disp = lvgl_port_add_disp(&disp_cfg);
    if (disp == NULL) {
        ESP_LOGE(TAG, "❌ LVGL 显示设备添加失败");
        return ESP_FAIL;
    }

    if (touch != NULL) {
        lv_indev_t *indev = lvgl_add_poll_touch(disp, touch);
        if (indev == NULL) {
            ESP_LOGW(TAG, "⚠️ LVGL 触摸输入设备添加失败，将继续显示");
        } else {
            ESP_LOGI(TAG, "✅ LVGL 触摸输入设备已添加（轮询模式）");
        }
    }

    ESP_LOGI(TAG, "✅ LVGL 初始化完成");
    return ESP_OK;
}

static void dashboard_enable_gesture(lv_obj_t *obj);
static void dashboard_sd_refresh_view(void);
static void dashboard_sd_select_delta(int delta);
static void dashboard_sd_close_fullscreen(void);

static lv_obj_t *dashboard_create_label(lv_obj_t *parent, const char *text)
{
    lv_obj_t *label = lv_label_create(parent);
    lv_obj_set_width(label, LV_PCT(100));
    lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP);
    lv_label_set_text(label, text);
    lv_obj_set_style_text_font(label, UI_FONT, 0);
    lv_obj_set_style_text_color(label, lv_color_hex(0xE8EEF4), 0);
    lv_obj_set_style_text_opa(label, LV_OPA_COVER, 0);
    dashboard_enable_gesture(label);
    return label;
}

static void dashboard_switch_page(dashboard_page_t page)
{
    if (s_ui.imu_page == NULL || s_ui.pmu_page == NULL || s_ui.sd_page == NULL || page == s_active_page) {
        return;
    }

    if (s_sd_fullscreen && page != DASHBOARD_PAGE_SD) {
        dashboard_sd_close_fullscreen();
    }

    s_active_page = page;
    lv_obj_add_flag(s_ui.imu_page, LV_OBJ_FLAG_HIDDEN);
    lv_obj_add_flag(s_ui.pmu_page, LV_OBJ_FLAG_HIDDEN);
    lv_obj_add_flag(s_ui.sd_page, LV_OBJ_FLAG_HIDDEN);

    switch (page) {
    case DASHBOARD_PAGE_IMU:
        lv_obj_clear_flag(s_ui.imu_page, LV_OBJ_FLAG_HIDDEN);
        break;
    case DASHBOARD_PAGE_PMU:
        lv_obj_clear_flag(s_ui.pmu_page, LV_OBJ_FLAG_HIDDEN);
        break;
    case DASHBOARD_PAGE_SD:
        lv_obj_clear_flag(s_ui.sd_page, LV_OBJ_FLAG_HIDDEN);
        sd_jpg_browser_rescan();
        dashboard_sd_refresh_view();
        break;
    default:
        break;
    }
}

static void dashboard_switch_relative(int delta)
{
    int page = (int)s_active_page + delta;
    while (page < 0) {
        page += DASHBOARD_PAGE_COUNT;
    }
    page %= DASHBOARD_PAGE_COUNT;
    dashboard_switch_page((dashboard_page_t)page);
}

static void dashboard_gesture_cb(lv_event_t *event)
{
    const lv_event_code_t code = lv_event_get_code(event);

    lv_indev_t *indev = lv_event_get_indev(event);
    if (indev == NULL) {
        return;
    }

    if (code == LV_EVENT_PRESSED) {
        lv_indev_get_point(indev, &s_swipe_start_point);
        s_swipe_tracking = true;
        return;
    }

    if (code == LV_EVENT_RELEASED || code == LV_EVENT_PRESS_LOST) {
        if (!s_swipe_tracking) {
            return;
        }

        lv_point_t end_point = {0};
        lv_indev_get_point(indev, &end_point);
        s_swipe_tracking = false;

        const int dx = end_point.x - s_swipe_start_point.x;
        const int dy = end_point.y - s_swipe_start_point.y;
        const int abs_dx = abs(dx);
        const int abs_dy = abs(dy);
        if (s_active_page == DASHBOARD_PAGE_SD &&
                abs_dy >= DASHBOARD_SWIPE_MIN_PX &&
                abs_dy > abs_dx * DASHBOARD_SWIPE_AXIS_GAIN) {
            dashboard_sd_select_delta(dy < 0 ? 1 : -1);
            return;
        }

        if (abs_dx < DASHBOARD_SWIPE_MIN_PX || abs_dx < abs_dy * DASHBOARD_SWIPE_AXIS_GAIN) {
            return;
        }

        if (dx < 0) {
            dashboard_switch_relative(1);
        } else {
            dashboard_switch_relative(-1);
        }
        return;
    }

    if (code == LV_EVENT_GESTURE) {
        const lv_dir_t dir = lv_indev_get_gesture_dir(indev);
        if (dir == LV_DIR_LEFT) {
            dashboard_switch_relative(1);
        } else if (dir == LV_DIR_RIGHT) {
            dashboard_switch_relative(-1);
        } else if (s_active_page == DASHBOARD_PAGE_SD && dir == LV_DIR_TOP) {
            dashboard_sd_select_delta(1);
        } else if (s_active_page == DASHBOARD_PAGE_SD && dir == LV_DIR_BOTTOM) {
            dashboard_sd_select_delta(-1);
        }
    }
}

static void dashboard_enable_gesture(lv_obj_t *obj)
{
    lv_obj_add_flag(obj, LV_OBJ_FLAG_CLICKABLE);
    lv_obj_add_flag(obj, LV_OBJ_FLAG_GESTURE_BUBBLE);
    lv_obj_add_event_cb(obj, dashboard_gesture_cb, LV_EVENT_PRESSED, NULL);
    lv_obj_add_event_cb(obj, dashboard_gesture_cb, LV_EVENT_RELEASED, NULL);
    lv_obj_add_event_cb(obj, dashboard_gesture_cb, LV_EVENT_PRESS_LOST, NULL);
    lv_obj_add_event_cb(obj, dashboard_gesture_cb, LV_EVENT_GESTURE, NULL);
}

static void dashboard_style_full_page(lv_obj_t *page, lv_color_t color)
{
    lv_obj_set_size(page, LV_PCT(100), LV_PCT(100));
    lv_obj_clear_flag(page, LV_OBJ_FLAG_SCROLLABLE);
    lv_obj_set_style_bg_color(page, color, 0);
    lv_obj_set_style_bg_opa(page, LV_OPA_COVER, 0);
    lv_obj_set_style_border_width(page, 0, 0);
    lv_obj_set_style_radius(page, 0, 0);
    lv_obj_set_style_pad_all(page, 0, 0);
    dashboard_enable_gesture(page);
}

static lv_obj_t *dashboard_create_value_chip(lv_obj_t *parent, int x, int y, int w, int h, const char *text)
{
    lv_obj_t *chip = lv_obj_create(parent);
    lv_obj_set_size(chip, w, h);
    lv_obj_set_pos(chip, x, y);
    lv_obj_clear_flag(chip, LV_OBJ_FLAG_SCROLLABLE);
    lv_obj_set_style_bg_color(chip, lv_color_hex(0x111B24), 0);
    lv_obj_set_style_bg_opa(chip, LV_OPA_80, 0);
    lv_obj_set_style_border_color(chip, lv_color_hex(0x1FCEB8), 0);
    lv_obj_set_style_border_opa(chip, LV_OPA_50, 0);
    lv_obj_set_style_border_width(chip, 1, 0);
    lv_obj_set_style_radius(chip, 8, 0);
    lv_obj_set_style_pad_all(chip, 2, 0);
    dashboard_enable_gesture(chip);

    lv_obj_t *label = lv_label_create(chip);
    lv_obj_set_width(label, LV_PCT(100));
    lv_label_set_long_mode(label, LV_LABEL_LONG_DOT);
    lv_label_set_text(label, text);
    lv_obj_set_style_text_font(label, UI_FONT, 0);
    lv_obj_set_style_text_color(label, lv_color_hex(0xE8EEF4), 0);
    lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0);
    lv_obj_set_style_text_line_space(label, 0, 0);
    lv_obj_center(label);
    dashboard_enable_gesture(label);
    return label;
}

static void dashboard_temp_to_text(float value, char *buf, size_t len)
{
    if (value < -100.0f) {
        snprintf(buf, len, "无效");
        return;
    }

    int scaled = (int)(value * 10.0f + (value >= 0.0f ? 0.5f : -0.5f));
    const bool negative = scaled < 0;
    if (negative) {
        scaled = -scaled;
    }
    snprintf(buf, len, "%s%d.%d℃", negative ? "-" : "", scaled / 10, scaled % 10);
}

static float dashboard_clamp_float(float value, float min_value, float max_value)
{
    if (value < min_value) {
        return min_value;
    }
    if (value > max_value) {
        return max_value;
    }
    return value;
}

static void dashboard_set_ball_collision_color(bool collision)
{
    if (s_ui.imu_ball == NULL) {
        return;
    }

    if (s_ball_collision_color == collision) {
        return;
    }

    s_ball_collision_color = collision;
    lv_obj_set_style_image_recolor(s_ui.imu_ball, lv_color_hex(0xEF4444), 0);
    lv_obj_set_style_image_recolor_opa(s_ui.imu_ball, collision ? LV_OPA_50 : LV_OPA_TRANSP, 0);
}

static void dashboard_sync_ball_position(void)
{
    if (s_ui.imu_ball == NULL) {
        return;
    }

    lv_obj_set_pos(s_ui.imu_ball, (int)(s_ball_x + 0.5f), (int)(s_ball_y + 0.5f));
}

static void dashboard_imu_physics_cb(lv_timer_t *timer)
{
    LV_UNUSED(timer);

    if (s_imu_ready) {
        imu_qmi8658_data_t data = {0};
        const esp_err_t ret = imu_qmi8658_read(&data);
        if (ret == ESP_OK) {
            s_filtered_accel_x += (data.accelX - s_filtered_accel_x) * IMU_ACCEL_FILTER_ALPHA;
            s_filtered_accel_y += (data.accelY - s_filtered_accel_y) * IMU_ACCEL_FILTER_ALPHA;
        }
    } else {
        s_filtered_accel_x *= 0.85f;
        s_filtered_accel_y *= 0.85f;
    }

    const float dt = (float)IMU_PHYSICS_TIMER_MS / 1000.0f;
    const float accel_x = dashboard_clamp_float(-s_filtered_accel_x / 9.807f, -1.0f, 1.0f) * IMU_ACCEL_TO_PIXEL;
    const float accel_y = dashboard_clamp_float(s_filtered_accel_y / 9.807f, -1.0f, 1.0f) * IMU_ACCEL_TO_PIXEL;

    s_ball_vx = dashboard_clamp_float((s_ball_vx + accel_x * dt) * IMU_BALL_DAMPING,
                                      -IMU_BALL_MAX_SPEED, IMU_BALL_MAX_SPEED);
    s_ball_vy = dashboard_clamp_float((s_ball_vy + accel_y * dt) * IMU_BALL_DAMPING,
                                      -IMU_BALL_MAX_SPEED, IMU_BALL_MAX_SPEED);
    s_ball_x += s_ball_vx * dt;
    s_ball_y += s_ball_vy * dt;

    const float ball_radius = (float)IMU_BALL_SIZE * 0.5f;
    const float screen_cx = (float)LCD_H_RES * 0.5f;
    const float screen_cy = (float)LCD_V_RES * 0.5f;
    const float screen_radius = ((LCD_H_RES < LCD_V_RES) ? LCD_H_RES : LCD_V_RES) * 0.5f;
    const float limit_radius = screen_radius - ball_radius - IMU_BALL_EDGE_MARGIN;
    float ball_cx = s_ball_x + ball_radius;
    float ball_cy = s_ball_y + ball_radius;
    const float dx = ball_cx - screen_cx;
    const float dy = ball_cy - screen_cy;
    const float dist_sq = dx * dx + dy * dy;
    uint8_t collision_mask = 0;

    if (dist_sq > (limit_radius * limit_radius)) {
        const float dist = __builtin_sqrtf(dist_sq);
        const float normal_x = (dist > 0.001f) ? (dx / dist) : 1.0f;
        const float normal_y = (dist > 0.001f) ? (dy / dist) : 0.0f;
        const float velocity_out = s_ball_vx * normal_x + s_ball_vy * normal_y;

        ball_cx = screen_cx + normal_x * limit_radius;
        ball_cy = screen_cy + normal_y * limit_radius;
        s_ball_x = ball_cx - ball_radius;
        s_ball_y = ball_cy - ball_radius;

        if (velocity_out > 0.0f) {
            s_ball_vx -= (1.0f + IMU_BALL_BOUNCE) * velocity_out * normal_x;
            s_ball_vy -= (1.0f + IMU_BALL_BOUNCE) * velocity_out * normal_y;
        }
        collision_mask = 0x01;
    }

    if (collision_mask != 0 && collision_mask != s_last_collision_mask) {
        s_collision_flash_ticks = IMU_COLLISION_FLASH_TICKS;
    }
    s_last_collision_mask = collision_mask;

    if (s_collision_flash_ticks > 0) {
        --s_collision_flash_ticks;
        dashboard_set_ball_collision_color(true);
    } else {
        dashboard_set_ball_collision_color(false);
    }

    dashboard_sync_ball_position();
}

static void dashboard_pmu_update_cb(lv_timer_t *timer)
{
    LV_UNUSED(timer);

    if (s_ui.pmu_status != NULL) {
        if (s_pmu_ready) {
            const int percent = pmu_get_battery_percent();
            const uint16_t batt_mv = pmu_get_batt_voltage();
            const uint16_t vbus_mv = pmu_get_vbus_voltage();
            const uint16_t sys_mv = pmu_get_system_voltage();
            const uint16_t charge_ma = pmu_get_charger_current_limit_ma();
            const uint16_t target_mv = pmu_get_charge_target_voltage_mv();
            const uint16_t vbus_limit_ma = pmu_get_vbus_current_limit_ma();
            const bool charging = pmu_is_charging();
            char batt_temp[16] = {0};
            char die_temp[16] = {0};

            dashboard_temp_to_text(pmu_get_batt_temperature(), batt_temp, sizeof(batt_temp));
            dashboard_temp_to_text(pmu_get_die_temperature(), die_temp, sizeof(die_temp));

            lv_label_set_text_fmt(s_ui.pmu_status, "%s", charging ? "充电中" : "电池供电");
            if (percent >= 0) {
                lv_label_set_text_fmt(s_ui.pmu_battery, "%d%%", percent);
                lv_bar_set_value(s_ui.pmu_bar, percent, LV_ANIM_OFF);
            } else {
                lv_label_set_text(s_ui.pmu_battery, "--%");
                lv_bar_set_value(s_ui.pmu_bar, 0, LV_ANIM_OFF);
            }
            lv_label_set_text_fmt(s_ui.pmu_voltage, "AXP2101 电源");
            lv_label_set_text_fmt(s_ui.pmu_temp, "芯片 %s", die_temp);
            lv_label_set_text_fmt(s_ui.pmu_chip[0], "电池 %u.%02uV",
                                  (unsigned int)(batt_mv / 1000), (unsigned int)((batt_mv % 1000) / 10));
            lv_label_set_text_fmt(s_ui.pmu_chip[1], "VBUS %u.%02uV",
                                  (unsigned int)(vbus_mv / 1000), (unsigned int)((vbus_mv % 1000) / 10));
            lv_label_set_text_fmt(s_ui.pmu_chip[2], "VSYS %u.%02uV",
                                  (unsigned int)(sys_mv / 1000), (unsigned int)((sys_mv % 1000) / 10));
            lv_label_set_text_fmt(s_ui.pmu_chip[3], "恒流 %umA", (unsigned int)charge_ma);
            lv_label_set_text_fmt(s_ui.pmu_chip[4], "目标 %u.%02uV",
                                  (unsigned int)(target_mv / 1000), (unsigned int)((target_mv % 1000) / 10));
            lv_label_set_text_fmt(s_ui.pmu_chip[5], "温度 %s", batt_temp);
            if (vbus_limit_ma > 0) {
                lv_label_set_text_fmt(s_ui.pmu_temp, "芯片 %s 输入 %umA", die_temp, (unsigned int)vbus_limit_ma);
            }
        } else {
            lv_label_set_text(s_ui.pmu_status, "AXP2101 未就绪");
            lv_label_set_text(s_ui.pmu_battery, "--%");
            lv_label_set_text(s_ui.pmu_voltage, "AXP2101 电源");
            lv_label_set_text(s_ui.pmu_temp, "等待 PMU 初始化");
            lv_bar_set_value(s_ui.pmu_bar, 0, LV_ANIM_OFF);
            for (int i = 0; i < 6; ++i) {
                lv_label_set_text(s_ui.pmu_chip[i], "--");
            }
        }
    }
}

static void dashboard_sd_release_image(void)
{
    if (s_ui.sd_full_image != NULL) {
        lv_image_set_src(s_ui.sd_full_image, NULL);
        lv_obj_add_flag(s_ui.sd_full_image, LV_OBJ_FLAG_HIDDEN);
    }
    sd_jpg_browser_free_image(&s_sd_full_image);
    memset(&s_sd_full_dsc, 0, sizeof(s_sd_full_dsc));
}

static void dashboard_sd_show_message(const char *message)
{
    dashboard_sd_release_image();
    s_sd_fullscreen = false;

    if (s_ui.sd_full_layer != NULL) {
        lv_obj_clear_flag(s_ui.sd_full_layer, LV_OBJ_FLAG_HIDDEN);
    }
    if (s_ui.sd_status != NULL) {
        lv_label_set_text(s_ui.sd_status, message);
        lv_obj_clear_flag(s_ui.sd_status, LV_OBJ_FLAG_HIDDEN);
        lv_obj_center(s_ui.sd_status);
    }
}

static void dashboard_sd_open_selected(void)
{
    const size_t count = sd_jpg_browser_count();
    if (count == 0 || s_ui.sd_full_layer == NULL || s_ui.sd_full_image == NULL) {
        return;
    }
    if (s_sd_selected >= count) {
        s_sd_selected = 0;
    }

    dashboard_sd_release_image();
    lv_obj_clear_flag(s_ui.sd_full_layer, LV_OBJ_FLAG_HIDDEN);
    if (s_ui.sd_status != NULL) {
        lv_obj_add_flag(s_ui.sd_status, LV_OBJ_FLAG_HIDDEN);
    }
    s_sd_fullscreen = true;

    esp_err_t ret = sd_jpg_browser_decode_fit(s_sd_selected, LCD_H_RES, LCD_V_RES, &s_sd_full_image);
    if (ret != ESP_OK) {
        dashboard_sd_show_message("图片打开失败\n请检查 JPG 文件");
        return;
    }

    s_sd_full_dsc.header.magic = LV_IMAGE_HEADER_MAGIC;
    s_sd_full_dsc.header.cf = LV_COLOR_FORMAT_RGB565;
    s_sd_full_dsc.header.flags = 0;
    s_sd_full_dsc.header.w = s_sd_full_image.width;
    s_sd_full_dsc.header.h = s_sd_full_image.height;
    s_sd_full_dsc.header.stride = s_sd_full_image.width * 2;
    s_sd_full_dsc.data_size = (uint32_t)s_sd_full_image.data_size;
    s_sd_full_dsc.data = s_sd_full_image.data;

    lv_image_set_src(s_ui.sd_full_image, &s_sd_full_dsc);
    lv_obj_set_size(s_ui.sd_full_image, s_sd_full_image.width, s_sd_full_image.height);
    lv_obj_center(s_ui.sd_full_image);
    lv_obj_clear_flag(s_ui.sd_full_image, LV_OBJ_FLAG_HIDDEN);
}

static void dashboard_sd_refresh_view(void)
{
    if (s_ui.sd_page == NULL) {
        return;
    }

    const size_t count = sd_jpg_browser_count();
    const size_t unsupported_images = sd_jpg_browser_unsupported_image_count();

    if (count == 0) {
        s_sd_selected = 0;
        if (!sd_jpg_browser_is_mounted()) {
            dashboard_sd_show_message("未检测到 SD 卡\n请插入 SD 卡");
        } else if (unsupported_images > 0) {
            char msg[64] = {0};
            snprintf(msg, sizeof(msg), "发现 %u 张非 JPG 图片\n请转成 JPG", (unsigned int)unsupported_images);
            dashboard_sd_show_message(msg);
        } else {
            dashboard_sd_show_message("未找到 JPG 图片");
        }
        return;
    }

    dashboard_sd_open_selected();
}

static void dashboard_sd_close_fullscreen(void)
{
    if (!s_sd_fullscreen) {
        return;
    }

    s_sd_fullscreen = false;
    dashboard_sd_release_image();
}

static void dashboard_sd_select_delta(int delta)
{
    const size_t count = sd_jpg_browser_count();
    if (count == 0) {
        sd_jpg_browser_rescan();
        dashboard_sd_refresh_view();
        return;
    }

    int next = (int)s_sd_selected + delta;
    while (next < 0) {
        next += (int)count;
    }
    next %= (int)count;
    s_sd_selected = (size_t)next;

    dashboard_sd_open_selected();
}

static esp_err_t dashboard_create(void)
{
    ESP_LOGI(TAG, "🖼️ 创建 QMI8658、AXP2101 与 SD 图片功能页面...");

    if (!lvgl_port_lock(1000)) {
        ESP_LOGE(TAG, "❌ LVGL 锁获取超时，页面创建失败");
        return ESP_ERR_TIMEOUT;
    }

    lv_obj_t *screen = lv_screen_active();
    lv_obj_set_style_bg_color(screen, lv_color_hex(0x101418), 0);
    lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0);
    lv_obj_clear_flag(screen, LV_OBJ_FLAG_SCROLLABLE);
    dashboard_enable_gesture(screen);

    s_ui.imu_page = lv_obj_create(screen);
    dashboard_style_full_page(s_ui.imu_page, lv_color_hex(0x05080F));

    s_ui.imu_bg = lv_image_create(s_ui.imu_page);
    lv_image_set_src(s_ui.imu_bg, &imu_bg_360);
    lv_obj_set_size(s_ui.imu_bg, LCD_H_RES, LCD_V_RES);
    lv_obj_set_pos(s_ui.imu_bg, 0, 0);
    lv_obj_clear_flag(s_ui.imu_bg, LV_OBJ_FLAG_SCROLLABLE);
    dashboard_enable_gesture(s_ui.imu_bg);

    s_ui.imu_ball = lv_image_create(s_ui.imu_page);
    lv_image_set_src(s_ui.imu_ball, &imu_ball_40);
    lv_image_set_antialias(s_ui.imu_ball, true);
    lv_obj_set_size(s_ui.imu_ball, IMU_BALL_SIZE, IMU_BALL_SIZE);
    lv_obj_clear_flag(s_ui.imu_ball, LV_OBJ_FLAG_SCROLLABLE);
    dashboard_enable_gesture(s_ui.imu_ball);
    lv_obj_set_style_bg_opa(s_ui.imu_ball, LV_OPA_TRANSP, 0);
    lv_obj_set_style_border_width(s_ui.imu_ball, 0, 0);
    lv_obj_set_style_image_recolor_opa(s_ui.imu_ball, LV_OPA_TRANSP, 0);
    dashboard_sync_ball_position();

    s_ui.pmu_page = lv_obj_create(screen);
    dashboard_style_full_page(s_ui.pmu_page, lv_color_hex(0x071014));

    s_ui.pmu_voltage = dashboard_create_label(s_ui.pmu_page, "AXP2101 电源");
    lv_obj_set_style_text_color(s_ui.pmu_voltage, lv_color_hex(0x8AF7C7), 0);
    lv_obj_set_style_text_align(s_ui.pmu_voltage, LV_TEXT_ALIGN_CENTER, 0);
    lv_obj_set_width(s_ui.pmu_voltage, 220);
    lv_obj_align(s_ui.pmu_voltage, LV_ALIGN_TOP_MID, 0, 16);

    lv_obj_t *battery_shell = lv_obj_create(s_ui.pmu_page);
    lv_obj_set_size(battery_shell, 178, 46);
    lv_obj_align(battery_shell, LV_ALIGN_TOP_MID, -5, 46);
    lv_obj_clear_flag(battery_shell, LV_OBJ_FLAG_SCROLLABLE);
    lv_obj_set_style_bg_color(battery_shell, lv_color_hex(0x0C2320), 0);
    lv_obj_set_style_bg_opa(battery_shell, LV_OPA_70, 0);
    lv_obj_set_style_border_color(battery_shell, lv_color_hex(0x80FF62), 0);
    lv_obj_set_style_border_width(battery_shell, 3, 0);
    lv_obj_set_style_radius(battery_shell, 10, 0);
    dashboard_enable_gesture(battery_shell);

    lv_obj_t *battery_head = lv_obj_create(s_ui.pmu_page);
    lv_obj_set_size(battery_head, 12, 24);
    lv_obj_align_to(battery_head, battery_shell, LV_ALIGN_OUT_RIGHT_MID, -1, 0);
    lv_obj_clear_flag(battery_head, LV_OBJ_FLAG_SCROLLABLE);
    lv_obj_set_style_bg_color(battery_head, lv_color_hex(0x80FF62), 0);
    lv_obj_set_style_bg_opa(battery_head, LV_OPA_90, 0);
    lv_obj_set_style_border_width(battery_head, 0, 0);
    lv_obj_set_style_radius(battery_head, 4, 0);
    dashboard_enable_gesture(battery_head);

    s_ui.pmu_bar = lv_bar_create(battery_shell);
    lv_obj_set_size(s_ui.pmu_bar, 152, 26);
    lv_obj_center(s_ui.pmu_bar);
    dashboard_enable_gesture(s_ui.pmu_bar);
    lv_bar_set_range(s_ui.pmu_bar, 0, 100);
    lv_bar_set_value(s_ui.pmu_bar, 0, LV_ANIM_OFF);
    lv_obj_set_style_bg_color(s_ui.pmu_bar, lv_color_hex(0x12252D), LV_PART_MAIN);
    lv_obj_set_style_bg_opa(s_ui.pmu_bar, LV_OPA_COVER, LV_PART_MAIN);
    lv_obj_set_style_bg_color(s_ui.pmu_bar, lv_color_hex(0x55FF75), LV_PART_INDICATOR);
    lv_obj_set_style_radius(s_ui.pmu_bar, 7, LV_PART_MAIN);
    lv_obj_set_style_radius(s_ui.pmu_bar, 7, LV_PART_INDICATOR);

    s_ui.pmu_battery = dashboard_create_label(s_ui.pmu_page, "--%");
    lv_obj_set_style_text_color(s_ui.pmu_battery, lv_color_hex(0xFFFFFF), 0);
    lv_obj_set_style_text_align(s_ui.pmu_battery, LV_TEXT_ALIGN_CENTER, 0);
    lv_obj_set_width(s_ui.pmu_battery, 120);
    lv_obj_align(s_ui.pmu_battery, LV_ALIGN_TOP_MID, 0, 98);

    s_ui.pmu_status = dashboard_create_label(s_ui.pmu_page, "初始化中");
    lv_obj_set_style_text_color(s_ui.pmu_status, lv_color_hex(0x9DFF7A), 0);
    lv_obj_set_style_text_align(s_ui.pmu_status, LV_TEXT_ALIGN_CENTER, 0);
    lv_obj_set_width(s_ui.pmu_status, 140);
    lv_obj_align(s_ui.pmu_status, LV_ALIGN_TOP_MID, 0, 124);

    s_ui.pmu_chip[0] = dashboard_create_value_chip(s_ui.pmu_page, 28, 160, 146, 38, "电池 --");
    s_ui.pmu_chip[1] = dashboard_create_value_chip(s_ui.pmu_page, 186, 160, 146, 38, "VBUS --");
    s_ui.pmu_chip[2] = dashboard_create_value_chip(s_ui.pmu_page, 28, 206, 146, 38, "VSYS --");
    s_ui.pmu_chip[3] = dashboard_create_value_chip(s_ui.pmu_page, 186, 206, 146, 38, "恒流 --");
    s_ui.pmu_chip[4] = dashboard_create_value_chip(s_ui.pmu_page, 28, 252, 146, 38, "目标 --");
    s_ui.pmu_chip[5] = dashboard_create_value_chip(s_ui.pmu_page, 186, 252, 146, 38, "温度 --");

    s_ui.pmu_temp = dashboard_create_label(s_ui.pmu_page, "等待 PMU 初始化");
    lv_obj_set_style_text_color(s_ui.pmu_temp, lv_color_hex(0x8EA8B4), 0);
    lv_obj_set_style_text_align(s_ui.pmu_temp, LV_TEXT_ALIGN_CENTER, 0);
    lv_obj_set_width(s_ui.pmu_temp, LCD_H_RES - 56);
    lv_obj_align(s_ui.pmu_temp, LV_ALIGN_BOTTOM_MID, 0, -22);

    s_ui.sd_page = lv_obj_create(screen);
    dashboard_style_full_page(s_ui.sd_page, lv_color_hex(0x05090D));

    s_ui.sd_full_layer = lv_obj_create(s_ui.sd_page);
    lv_obj_set_size(s_ui.sd_full_layer, LV_PCT(100), LV_PCT(100));
    lv_obj_set_pos(s_ui.sd_full_layer, 0, 0);
    lv_obj_clear_flag(s_ui.sd_full_layer, LV_OBJ_FLAG_SCROLLABLE);
    lv_obj_set_style_bg_color(s_ui.sd_full_layer, lv_color_hex(0x000000), 0);
    lv_obj_set_style_bg_opa(s_ui.sd_full_layer, LV_OPA_COVER, 0);
    lv_obj_set_style_border_width(s_ui.sd_full_layer, 0, 0);
    lv_obj_set_style_radius(s_ui.sd_full_layer, 0, 0);
    dashboard_enable_gesture(s_ui.sd_full_layer);

    s_ui.sd_full_image = lv_image_create(s_ui.sd_full_layer);
    lv_obj_center(s_ui.sd_full_image);
    dashboard_enable_gesture(s_ui.sd_full_image);
    lv_obj_add_flag(s_ui.sd_full_image, LV_OBJ_FLAG_HIDDEN);

    s_ui.sd_status = dashboard_create_label(s_ui.sd_full_layer, "正在扫描 SD 卡");
    lv_obj_set_width(s_ui.sd_status, LCD_H_RES - 72);
    lv_obj_set_style_text_color(s_ui.sd_status, lv_color_hex(0xE8EEF4), 0);
    lv_obj_set_style_text_align(s_ui.sd_status, LV_TEXT_ALIGN_CENTER, 0);
    lv_obj_center(s_ui.sd_status);

    dashboard_sd_refresh_view();
    lv_obj_add_flag(s_ui.pmu_page, LV_OBJ_FLAG_HIDDEN);
    lv_obj_add_flag(s_ui.sd_page, LV_OBJ_FLAG_HIDDEN);
    s_active_page = DASHBOARD_PAGE_IMU;

    lv_timer_create(dashboard_imu_physics_cb, IMU_PHYSICS_TIMER_MS, NULL);
    lv_timer_create(dashboard_pmu_update_cb, PMU_TIMER_MS, NULL);
    dashboard_imu_physics_cb(NULL);
    dashboard_pmu_update_cb(NULL);
    lv_obj_invalidate(screen);
    lv_refr_now(NULL);
    lvgl_port_unlock();

    ESP_LOGI(TAG, "✅ 功能页面创建完成");
    return ESP_OK;
}

static void dashboard_boot_key_action(void)
{
    if (!lvgl_port_lock(100)) {
        ESP_LOGW(TAG, "⚠️ BOOT 按键处理时 LVGL 锁获取超时");
        return;
    }

    if (s_ui.imu_page == NULL) {
        lvgl_port_unlock();
        return;
    }

    if (s_active_page == DASHBOARD_PAGE_SD) {
        dashboard_sd_select_delta(1);
    } else {
        dashboard_switch_relative(1);
    }
    lvgl_port_unlock();
}

static void boot_button_task(void *arg)
{
    LV_UNUSED(arg);

    int last_raw = gpio_get_level(BOOT_PIN);
    int stable = last_raw;
    TickType_t last_change = xTaskGetTickCount();

    while (true) {
        const int raw = gpio_get_level(BOOT_PIN);
        const TickType_t now = xTaskGetTickCount();
        if (raw != last_raw) {
            last_raw = raw;
            last_change = now;
        }

        if ((now - last_change) >= pdMS_TO_TICKS(BOOT_DEBOUNCE_MS) && raw != stable) {
            stable = raw;
            if (stable == 0) {
                ESP_LOGI(TAG, "🖱️ BOOT 按键触发切换");
                dashboard_boot_key_action();
            }
        }

        vTaskDelay(pdMS_TO_TICKS(BOOT_POLL_MS));
    }
}

static esp_err_t boot_button_init(void)
{
    const gpio_config_t cfg = {
        .pin_bit_mask = 1ULL << BOOT_PIN,
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE,
    };
    ESP_RETURN_ON_ERROR(gpio_config(&cfg), TAG, "❌ BOOT 按键 GPIO 初始化失败");

    BaseType_t ok = xTaskCreate(boot_button_task, "boot_btn", 3072, NULL, 3, NULL);
    if (ok != pdPASS) {
        ESP_LOGE(TAG, "❌ BOOT 按键任务创建失败");
        return ESP_ERR_NO_MEM;
    }

    ESP_LOGI(TAG, "✅ BOOT 按键初始化完成，GPIO=%d", BOOT_PIN);
    return ESP_OK;
}

static void dashboard_task(void *arg)
{
    LV_UNUSED(arg);

    ESP_ERROR_CHECK(dashboard_create());

    ESP_LOGI(TAG, "⏳ 等待功能页面首帧刷新...");
    vTaskDelay(pdMS_TO_TICKS(300));
    backlight_set(100);

    ESP_LOGI(TAG, "✅ QMI8658、AXP2101 与 SD 图片功能页面已启动");
    vTaskDelete(NULL);
}

static void sensors_init(i2c_master_bus_handle_t i2c)
{
    ESP_LOGI(TAG, "📊 初始化 QMI8658 陀螺仪...");
    s_imu_ready = (imu_qmi8658_init(i2c) == ESP_OK);
    if (!s_imu_ready) {
        ESP_LOGW(TAG, "⚠️ QMI8658 初始化失败，陀螺仪页面将显示未就绪");
    }

    ESP_LOGI(TAG, "⚡ 初始化 AXP2101 电源管理...");
    s_pmu_ready = (axp2101_init(i2c) == ESP_OK);
    if (!s_pmu_ready) {
        ESP_LOGW(TAG, "⚠️ AXP2101 初始化失败，电源页面将显示未就绪");
    }
}

void app_main(void)
{
    ESP_LOGI(TAG, "🚀 ESP32-S3 ST77916 圆屏项目启动（%s）", LCD_PANEL_DESC);
    ESP_LOGI(TAG, "📊 屏幕分辨率: %dx%d | 接口: QSPI", LCD_H_RES, LCD_V_RES);

    esp_lcd_panel_handle_t panel = NULL;
    esp_lcd_panel_io_handle_t io = NULL;
    ESP_ERROR_CHECK(lcd_init(&panel, &io));

    i2c_master_bus_handle_t i2c = NULL;
    ESP_ERROR_CHECK(board_i2c_init(&i2c));

    esp_lcd_touch_handle_t touch = NULL;
    esp_err_t touch_ret = touch_init(i2c, &touch);
    if (touch_ret != ESP_OK) {
        ESP_LOGW(TAG, "⚠️ 触摸屏初始化失败，将以无触摸模式运行");
        touch = NULL;
    }

    sensors_init(i2c);

    esp_err_t sd_ret = sd_jpg_browser_init();
    if (sd_ret != ESP_OK) {
        ESP_LOGW(TAG, "⚠️ SD 图片浏览初始化失败，第三页将显示未挂载状态: %s", esp_err_to_name(sd_ret));
    }

    ESP_ERROR_CHECK(lvgl_init(panel, io, touch));
    ESP_ERROR_CHECK(boot_button_init());

    BaseType_t task_ret = xTaskCreate(dashboard_task, "dashboard", 8192, NULL, 3, NULL);
    if (task_ret != pdPASS) {
        ESP_LOGE(TAG, "❌ 功能页面任务创建失败");
        ESP_ERROR_CHECK(ESP_ERR_NO_MEM);
    }

    ESP_LOGI(TAG, "✅ 系统初始化完成，LVGL 任务运行中");
}
