/* * esp32s3box3_ui.ino — LVGL v9 + ESP32_Display_Panel * * PenPot design → LVGL UI for the ESP32-S3-Box-3 * Display: 320 × 240, black background * 6 screens (0–5), left/right chevron navigation * * ── Required libraries (Arduino Library Manager) ───────────────────── * 1. ESP32_Display_Panel by Espressif (search "ESP32_Display_Panel") * 2. lvgl by LVGL v9.x * * ── lv_conf.h ──────────────────────────────────────────────────────── * Copy lvgl/lv_conf_template.h → libraries/lv_conf.h (one level UP) * Edit it: * #if 0 → #if 1 * #define LV_FONT_MONTSERRAT_48 1 * #define LV_FONT_MONTSERRAT_32 1 * * ── Board settings ─────────────────────────────────────────────────── * Board : ESP32S3 Dev Module * Flash Size : 16MB * PSRAM : OPI PSRAM ← essential, buffers live here * USB CDC : Enabled * ───────────────────────────────────────────────────────────────────── */ #include #include #include /* ── Display size ────────────────────────────────────────────────────*/ #define SCREEN_W 320 #define SCREEN_H 240 /* ── Draw buffers in PSRAM (fixes dram0_0_seg overflow) ─────────────*/ // MALLOC_CAP_SPIRAM puts them in the 8 MB OPI PSRAM on the S3-Box-3. // Two buffers of half the screen lets LVGL double-buffer nicely. #define BUF_SIZE (SCREEN_W * SCREEN_H / 2) static lv_color_t *buf1 = nullptr; static lv_color_t *buf2 = nullptr; /* ── Panel ───────────────────────────────────────────────────────────*/ static ESP_Panel *panel = nullptr; /* ── Mutex ───────────────────────────────────────────────────────────*/ static SemaphoreHandle_t lvgl_mux; /* ── Navigation ──────────────────────────────────────────────────────*/ #define NUM_SCREENS 6 static lv_obj_t *screens[NUM_SCREENS]; static int current_screen = 1; static const int NAV_PREV[NUM_SCREENS] = { -1, 0, 1, 2, 3, 4 }; static const int NAV_NEXT[NUM_SCREENS] = { 1, 2, 3, 4, 5, -1 }; /* ── Chevron point arrays ────────────────────────────────────────────*/ static const lv_point_precise_t CHEV_L[] = { {59,0}, {0,34}, {59,68} }; static const lv_point_precise_t CHEV_R[] = { {0,0}, {68,26}, {0,53} }; /* ═══════════════════════════════════════════════════════════════════ */ /* LVGL v9 callbacks */ /* ═══════════════════════════════════════════════════════════════════ */ static void lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) { panel->getLcd()->drawBitmap( area->x1, area->y1, area->x2 - area->x1 + 1, area->y2 - area->y1 + 1, px_map); lv_display_flush_ready(disp); } static void lvgl_touch_cb(lv_indev_t *indev, lv_indev_data_t *data) { ESP_PanelTouch *touch = panel->getTouch(); if (!touch) { data->state = LV_INDEV_STATE_RELEASED; return; } ESP_PanelTouchPoint points[1]; int num = touch->readPoints(points, 1, -1); if (num > 0) { data->point.x = points[0].x; data->point.y = points[0].y; data->state = LV_INDEV_STATE_PRESSED; } else { data->state = LV_INDEV_STATE_RELEASED; } } static void lvgl_tick_task(void *arg) { for (;;) { lv_tick_inc(5); vTaskDelay(pdMS_TO_TICKS(5)); } } /* ═══════════════════════════════════════════════════════════════════ */ /* UI helpers */ /* ═══════════════════════════════════════════════════════════════════ */ static void nav_to(int idx) { if (idx < 0 || idx >= NUM_SCREENS) return; lv_scr_load_anim(screens[idx], LV_SCR_LOAD_ANIM_FADE_ON, 150, 0, false); current_screen = idx; } static void btn_prev_cb(lv_event_t *e) { nav_to((int)(intptr_t)lv_event_get_user_data(e)); } static void btn_next_cb(lv_event_t *e) { nav_to((int)(intptr_t)lv_event_get_user_data(e)); } static void add_chevron(lv_obj_t *parent, int x, int y, int w, int h, const lv_point_precise_t *pts, int pt_cnt, lv_event_cb_t cb, int dest) { lv_obj_t *btn = lv_button_create(parent); lv_obj_set_pos(btn, x, y); lv_obj_set_size(btn, w, h); lv_obj_set_style_bg_opa (btn, LV_OPA_TRANSP, LV_PART_MAIN); lv_obj_set_style_bg_opa (btn, LV_OPA_TRANSP, LV_STATE_PRESSED); lv_obj_set_style_shadow_width(btn, 0, LV_PART_MAIN); lv_obj_set_style_border_width(btn, 0, LV_PART_MAIN); lv_obj_set_style_outline_width(btn, 0, LV_PART_MAIN); lv_obj_set_style_pad_all (btn, 0, LV_PART_MAIN); lv_obj_add_event_cb(btn, cb, LV_EVENT_CLICKED, (void *)(intptr_t)dest); lv_obj_t *line = lv_line_create(btn); lv_line_set_points(line, pts, pt_cnt); lv_obj_set_style_line_color (line, lv_color_white(), LV_PART_MAIN); lv_obj_set_style_line_width (line, 2, LV_PART_MAIN); lv_obj_set_style_line_rounded(line, true, LV_PART_MAIN); lv_obj_set_size(line, w, h); lv_obj_align(line, LV_ALIGN_CENTER, 0, 0); } static lv_obj_t *make_screen(int idx) { lv_obj_t *scr = lv_obj_create(NULL); lv_obj_set_size(scr, SCREEN_W, SCREEN_H); lv_obj_set_style_bg_color (scr, lv_color_black(), LV_PART_MAIN); lv_obj_set_style_bg_opa (scr, LV_OPA_COVER, LV_PART_MAIN); lv_obj_set_style_border_width(scr, 0, LV_PART_MAIN); lv_obj_set_style_pad_all (scr, 0, LV_PART_MAIN); /* Screen-number label */ lv_obj_t *lbl = lv_label_create(scr); char buf[4]; snprintf(buf, sizeof(buf), "%d", idx); lv_label_set_text(lbl, buf); lv_obj_set_style_text_color(lbl, lv_color_white(), LV_PART_MAIN); #if LV_FONT_MONTSERRAT_48 lv_obj_set_style_text_font(lbl, &lv_font_montserrat_48, LV_PART_MAIN); #else lv_obj_set_style_text_font(lbl, &lv_font_montserrat_32, LV_PART_MAIN); #endif lv_obj_set_pos (lbl, 144, 34); lv_obj_set_size(lbl, 72, 78); lv_label_set_long_mode(lbl, LV_LABEL_LONG_CLIP); /* Centre icon placeholder */ lv_obj_t *icon = lv_obj_create(scr); lv_obj_set_pos (icon, 124, 136); lv_obj_set_size(icon, 86, 73); lv_obj_set_style_bg_opa (icon, LV_OPA_TRANSP, LV_PART_MAIN); lv_obj_set_style_border_color(icon, lv_color_white(), LV_PART_MAIN); lv_obj_set_style_border_width(icon, 2, LV_PART_MAIN); lv_obj_set_style_border_opa (icon, LV_OPA_50, LV_PART_MAIN); lv_obj_set_style_radius (icon, 4, LV_PART_MAIN); if (NAV_PREV[idx] >= 0) add_chevron(scr, 27, 144, 59, 68, CHEV_L, 3, btn_prev_cb, NAV_PREV[idx]); if (NAV_NEXT[idx] >= 0) add_chevron(scr, 226, 146, 68, 53, CHEV_R, 3, btn_next_cb, NAV_NEXT[idx]); return scr; } /* ═══════════════════════════════════════════════════════════════════ */ /* setup() */ /* ═══════════════════════════════════════════════════════════════════ */ void setup() { Serial.begin(115200); Serial.println("ESP32-S3-Box-3 starting..."); /* 1. Allocate draw buffers from PSRAM */ buf1 = (lv_color_t *)heap_caps_malloc(BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_SPIRAM); buf2 = (lv_color_t *)heap_caps_malloc(BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_SPIRAM); if (!buf1 || !buf2) { Serial.println("ERROR: PSRAM alloc failed — check PSRAM is OPI in board settings"); while (1) delay(1000); } /* 2. Hardware panel */ panel = new ESP_Panel(); panel->init(); panel->begin(); /* 3. LVGL */ lv_init(); lv_display_t *disp = lv_display_create(SCREEN_W, SCREEN_H); lv_display_set_flush_cb(disp, lvgl_flush_cb); lv_display_set_buffers(disp, buf1, buf2, BUF_SIZE * sizeof(lv_color_t), LV_DISPLAY_RENDER_MODE_PARTIAL); if (panel->getTouch()) { lv_indev_t *indev = lv_indev_create(); lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER); lv_indev_set_read_cb(indev, lvgl_touch_cb); } /* 4. Tick task */ lvgl_mux = xSemaphoreCreateMutex(); xTaskCreatePinnedToCore(lvgl_tick_task, "lv_tick", 2048, NULL, 5, NULL, 0); /* 5. Build UI */ for (int i = 0; i < NUM_SCREENS; i++) screens[i] = make_screen(i); nav_to(1); Serial.println("UI ready."); } /* ═══════════════════════════════════════════════════════════════════ */ /* loop() */ /* ═══════════════════════════════════════════════════════════════════ */ void loop() { if (xSemaphoreTake(lvgl_mux, portMAX_DELAY)) { lv_timer_handler(); xSemaphoreGive(lvgl_mux); } delay(5); }