diff --git a/Dev_Docs/Pending_Features/2026-06-08_1940_esp32_nav_minimal_test.md b/Dev_Docs/Pending_Features/2026-06-08_1940_esp32_nav_minimal_test.md index d1ebf24..7a74109 100644 --- a/Dev_Docs/Pending_Features/2026-06-08_1940_esp32_nav_minimal_test.md +++ b/Dev_Docs/Pending_Features/2026-06-08_1940_esp32_nav_minimal_test.md @@ -1,17 +1,44 @@ # ESP32 nav minimal test -- Краткое описание: новый минимальный навигационный UI-прототип для сабсервера на базе рабочего `LVGL + subserver touch`. +- Краткое описание: минимальный UI-прототип для сабсервера на базе `LVGL + subserver touch`, с Wi-Fi flow, серверными адресами и общим экраном редактирования текста. - Что проверять: - стартует экран `HOME`; - - на `HOME` видны `login`, `subserver1`, `W BAT`, `STATUS`, кнопка `SETTINGS`; + - на `HOME` видны `login`, `subserver1`, `W BAT`, `STATUS`, Wi-Fi статус и кнопка `SETTINGS`; + - Wi-Fi статус на `HOME` корректно показывает одно из состояний: + - `Wi-Fi not configured` + - `Wi-Fi disconnected` + - `Wi-Fi connected` - кнопка `SETTINGS` открывает `SETTINGS_MENU`; - свайп влево на `HOME` открывает `SETTINGS_MENU`; - в `SETTINGS_MENU` сначала видны только `Wi-Fi` и `Server`; + - обе видимые карточки меню одного цвета; - свайп вверх показывает `Server` и `Account`; - свайп вниз возвращает `Wi-Fi` и `Server`; - свайп вправо из `SETTINGS_MENU` возвращает на `HOME`; - - нажатия по `Wi-Fi`, `Server`, `Account` открывают соответствующие экраны; + - нажатие `Wi-Fi` открывает `WIFI_SCREEN`; + - `SELECT NETWORK` запускает скан; + - после скана показывается список доступных SSID; + - выбор SSID открывает общий экран редактирования текста для пароля; + - на этом экране видно старое значение, курсор стоит в конце; + - две верхние служебные строки над полем ввода отсутствуют; + - большая клавиатура реально видна на экране и занимает большую часть высоты; + - буквы разбиты на 2 страницы; + - режим символов тоже разбит на 2 страницы; + - на правой странице кнопки стоят в ровных вертикальных колонках; + - свайп влево/вправо на экране ввода переключает страницы клавиатуры; + - при этом свайп страниц клавиатуры срабатывает только из нижней клавиатурной зоны, а не из верхней части экрана; + - `ABC/123`, `SHIFT`, `DEL`, `SAVE`, `CANCEL` работают; + - при успехе SSID и пароль сохраняются, а `HOME` показывает `Wi-Fi connected`; + - при ошибке показывается `Connection failed`; + - `CLEAR SAVED WI-FI` очищает сохранённые настройки; + - нажатие `Server` открывает `SERVER_SCREEN`; + - в `SERVER_SCREEN` видны и редактируются два значения: + - `https://api.devnet.solana.com` + - `https://shineup.me` + - нажатие `SOLANA RPC` открывает общий экран редактирования; + - нажатие `SHINE SERVER` открывает общий экран редактирования; + - после `SAVE` новые адреса сохраняются в NVS; - свайп вправо из внутренних экранов возвращает в `SETTINGS_MENU`; - если во время реального свайпа палец проходит по кнопке, это не должно открывать кнопку как обычный `click`. -- Ожидаемый результат: новый скетч даёт чистый навигационный каркас без старой перегруженной логики. +- Ожидаемый результат: новый скетч даёт чистый навигационный каркас и уже умеет настраивать Wi-Fi и серверные адреса на самой ESP32. - Статус: pending diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_subserver_ui_nav_minimal_spec.md b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_subserver_ui_nav_minimal_spec.md index 8a8a325..ebe096d 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_subserver_ui_nav_minimal_spec.md +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_subserver_ui_nav_minimal_spec.md @@ -4,12 +4,11 @@ ## Цель -Этот прототип проверяет только базовую механику экранов, крупных кнопок и свайпов. +Этот прототип проверяет базовую механику экранов, крупных кнопок, свайпов, первичную настройку Wi-Fi и настройку серверных адресов через общий экран редактирования текста. На этом этапе отсутствуют: -- реальный Wi-Fi; -- реальные серверные проверки; -- логин/пароль; +- логика серверной проверки доступности; +- логин/пароль учётной записи SHiNE; - PIN; - кошелёк; - QR; @@ -20,12 +19,13 @@ ## Экраны -Прототип содержит 5 экранов: +Прототип содержит 6 экранов: - `HOME` - `SETTINGS_MENU` - `WIFI_SCREEN` - `SERVER_SCREEN` - `ACCOUNT_SCREEN` +- `TEXT_EDIT_SCREEN` ## HOME @@ -34,8 +34,15 @@ - ниже `subserver1`; - сверху справа простые индикаторы `W BAT`; - по центру крупный текст `STATUS`; +- статус Wi-Fi; +- сохранённый SSID или пометку, что он не сохранён; - снизу большую кнопку `SETTINGS`. +Статусы Wi-Fi на `HOME`: +- `Wi-Fi not configured` +- `Wi-Fi disconnected` +- `Wi-Fi connected` + Переходы: - кнопка `SETTINGS` -> `SETTINGS_MENU`; - свайп влево -> `SETTINGS_MENU`. @@ -61,6 +68,8 @@ - `Wi-Fi` - `Server` +Обе видимые карточки меню имеют одинаковый цвет. + Переходы: - нажатие `Wi-Fi` -> `WIFI_SCREEN`; - нажатие `Server` -> `SERVER_SCREEN`; @@ -69,23 +78,52 @@ ## WIFI_SCREEN +Экран содержит 2 внутренних режима. + +### 1. Overview + Показывает: -- `Wi-Fi` -- `Wi-Fi screen` +- текущий Wi-Fi статус; +- сохранённый SSID; +- статусное сообщение; +- кнопку `SELECT NETWORK`; +- кнопку `CLEAR SAVED WI-FI`; +- кнопку `BACK`. + +### 2. Scan Results + +После `SELECT NETWORK` выполняется скан доступных Wi-Fi сетей. + +Показывает: +- заголовок `SELECT NETWORK`; +- количество или результат сканирования; +- список найденных SSID как крупные кнопки; +- кнопку `SCAN AGAIN`; +- кнопку `BACK`. + +Нажатие на SSID открывает `TEXT_EDIT_SCREEN` для ввода пароля. Переходы: -- свайп вправо -> `SETTINGS_MENU` +- свайп вправо из любого режима `WIFI_SCREEN` -> `SETTINGS_MENU` - кнопка `BACK` -> `SETTINGS_MENU` ## SERVER_SCREEN Показывает: -- `Server` -- `Server screen` +- статусное сообщение; +- текущий `Solana RPC` адрес; +- кнопку `SOLANA RPC`; +- текущий `Shine server` адрес; +- кнопку `SHINE SERVER`. + +Значения по умолчанию: +- Solana RPC: `https://api.devnet.solana.com` +- Shine server: `https://shineup.me` + +Нажатие на любую из двух кнопок открывает `TEXT_EDIT_SCREEN`. Переходы: - свайп вправо -> `SETTINGS_MENU` -- кнопка `BACK` -> `SETTINGS_MENU` ## ACCOUNT_SCREEN @@ -97,6 +135,63 @@ - свайп вправо -> `SETTINGS_MENU` - кнопка `BACK` -> `SETTINGS_MENU` +## TEXT_EDIT_SCREEN + +Общий экран редактирования строковых значений. + +Используется для: +- пароля Wi-Fi; +- Solana RPC; +- Shine server. + +Показывает: +- заголовок; +- поле ввода, уже заполненное старым значением; +- курсор установлен в конец текста; +- кнопки `SAVE`, `CANCEL`, `DEL`, `CLR`; +- большую экранную клавиатуру. + +## Клавиатура + +Клавиатура единая для всех текстовых вводов. + +Особенности: +- занимает нижнюю часть экрана; +- разбита на 2 страницы; +- переключение страниц выполняется свайпом влево/вправо внутри `TEXT_EDIT_SCREEN`; +- страница 1 содержит в основном буквы и базовые URL-символы; +- страница 2 содержит цифры и дополнительные URL/символьные кнопки; +- есть специальные действия `DEL` и `CLR`. + +## Хранение Wi-Fi + +Используется `Preferences` (NVS памяти ESP32): +- `wifi_ssid` +- `wifi_pass` + +При старте устройства, если сохранён SSID, выполняется попытка подключения к сохранённой сети. + +## Хранение серверов + +Используется `Preferences` (NVS памяти ESP32): +- `solana_rpc` +- `shine_server` + +## Детали клавиатуры + +- клавиатура занимает примерно `2/3`-`3/4` высоты экрана; +- сверху остаются только заголовок, подсказка и поле ввода; +- буквы занимают 3 ряда; +- половина букв находится на левой странице, половина на правой; +- на правой странице кнопки тоже стоят в ровных колонках, без сдвига рядов вправо; +- отдельный режим `symbols` тоже разделён на 2 страницы; +- четвёртый ряд содержит: + - переключение `ABC/123` + - `SHIFT` + - `DEL` + - `SAVE` + - `CANCEL` + ## Жесты Поддерживаются направления: @@ -112,6 +207,8 @@ - `WIFI_SCREEN`: свайп вправо -> `SETTINGS_MENU` - `SERVER_SCREEN`: свайп вправо -> `SETTINGS_MENU` - `ACCOUNT_SCREEN`: свайп вправо -> `SETTINGS_MENU` +- `TEXT_EDIT_SCREEN`: свайп влево/вправо -> переключение страниц клавиатуры +- переключение страниц клавиатуры срабатывает только если свайп начался в зоне самой клавиатуры, а не по всему экрану редактора ## Особенность обработки жестов diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/README.md b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/README.md index f4284fb..f5126ca 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/README.md +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/README.md @@ -20,7 +20,7 @@ - `lvgl-official-based-test` — наш минимальный экран, но на максимально близкой к официальному `LVGL_Widgets` инициализации - `lvgl-subserver-touch-test` — гибрид: `LVGL`-интерфейс, но display/touch init и raw touch-read взяты из `shine_subserver_ui`; подтверждено на устройстве, touch работает, зелёных линий по краям нет - `lvgl-russian-font-test` — тест кастомного `LVGL`-шрифта с кириллицей: русские кнопки, длинные подписи и статусы -- `lvgl-nav-minimal-test` — новый минимальный UI-каркас сабсервера: `HOME`, `SETTINGS_MENU`, `Wi-Fi`, `Server`, `Account`, свайпы и крупные кнопки +- `lvgl-nav-minimal-test` — новый минимальный UI-каркас сабсервера: `HOME`, `SETTINGS_MENU`, `Wi-Fi`, `Server`, `Account`, свайпы, крупные кнопки и реальная настройка Wi-Fi с сохранением в NVS Запуск: diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/README.md b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/README.md index 4caff50..19c379e 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/README.md +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/README.md @@ -14,7 +14,7 @@ - `lvgl_official_based_test/` - минимальный наш экран поверх максимально близкой к официальному `05_LVGL_Widgets` инициализации - `lvgl_subserver_touch_test/` - гибридный тест: `LVGL`-экран с инициализацией дисплея и чтением touch из `shine_subserver_ui`; подтверждён на реальном устройстве - `lvgl_russian_font_test/` - тест кастомного кириллического `LVGL`-шрифта с русскими кнопками, длинными строками и рабочим touch -- `lvgl_nav_minimal_test/` - новый минимальный навигационный каркас сабсервера на рабочем `LVGL + subserver touch` +- `lvgl_nav_minimal_test/` - новый минимальный навигационный каркас сабсервера на рабочем `LVGL + subserver touch`, расширенный настройкой Wi-Fi и сохранением в NVS ## Запуск diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/lvgl_nav_minimal_test/lvgl_nav_minimal_test.ino b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/lvgl_nav_minimal_test/lvgl_nav_minimal_test.ino index 1264c2b..11b22f0 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/lvgl_nav_minimal_test/lvgl_nav_minimal_test.ino +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/lvgl_nav_minimal_test/lvgl_nav_minimal_test.ino @@ -1,5 +1,7 @@ #include #include +#include +#include #include #include #include @@ -21,7 +23,13 @@ #define LVGL_TICK_MS 2 #define SWIPE_THRESHOLD 48 #define TAP_CANCEL_THRESHOLD 18 -#define TEST_VERSION "NAV v2" +#define MAX_SCAN_RESULTS 8 +#define WIFI_CONNECT_TIMEOUT_MS 12000 +#define TEXT_EDIT_PANEL_X 10 +#define TEXT_EDIT_PANEL_Y 126 +#define TEXT_EDIT_PANEL_W 460 +#define TEXT_EDIT_PANEL_H 344 +#define TEST_VERSION "NAV v6" enum Screen { SCREEN_HOME, @@ -29,6 +37,7 @@ enum Screen { SCREEN_WIFI, SCREEN_SERVER, SCREEN_ACCOUNT, + SCREEN_TEXT_EDIT, }; enum SwipeDirection { @@ -47,6 +56,29 @@ enum ActionId { ACTION_OPEN_ACCOUNT, ACTION_BACK_HOME, ACTION_BACK_SETTINGS, + ACTION_WIFI_SELECT_NETWORK, + ACTION_WIFI_CLEAR, + ACTION_SERVER_EDIT_SOLANA, + ACTION_SERVER_EDIT_SHINE, + ACTION_EDITOR_SAVE, + ACTION_EDITOR_CANCEL, +}; + +enum WifiViewMode { + WIFI_VIEW_OVERVIEW, + WIFI_VIEW_SCAN_RESULTS, +}; + +enum EditContext { + EDIT_CONTEXT_NONE, + EDIT_CONTEXT_WIFI_PASSWORD, + EDIT_CONTEXT_SOLANA_RPC, + EDIT_CONTEXT_SHINE_SERVER, +}; + +enum KeyboardMode { + KEYBOARD_MODE_ALPHA, + KEYBOARD_MODE_SYMBOLS, }; static const char *kMenuItems[] = {"Wi-Fi", "Server", "Account"}; @@ -56,12 +88,14 @@ static lv_disp_draw_buf_t gDrawBuf; static lv_color_t *gBuf1 = nullptr; static lv_color_t *gBuf2 = nullptr; static lv_obj_t *gRoot = nullptr; +static lv_obj_t *gInputTextArea = nullptr; Arduino_DataBus *gBus = new Arduino_ESP32QSPI( PIN_LCD_CS, PIN_LCD_SCLK, PIN_LCD_D0, PIN_LCD_D1, PIN_LCD_D2, PIN_LCD_D3); Arduino_CO5300 *gfx = new Arduino_CO5300( gBus, PIN_LCD_RST, 0, DISP_W, DISP_H, 0, 0, 0, 0); TouchDrvCST92xx gTouch; +Preferences gPrefs; static Screen gCurrentScreen = SCREEN_HOME; static int gSettingsScrollIndex = 0; @@ -73,9 +107,48 @@ static int16_t gTouchLastY = 0; static SwipeDirection gPendingSwipe = SWIPE_NONE; static bool gBlockClick = false; +static String gWifiSavedSsid; +static String gWifiSavedPassword; +static String gWifiSelectedSsid; +static String gWifiStatusMessage = "No Wi-Fi configured"; +static String gScanResults[MAX_SCAN_RESULTS]; +static int gScanResultCount = 0; +static WifiViewMode gWifiViewMode = WIFI_VIEW_OVERVIEW; + +static String gSolanaRpcUrl = "https://api.devnet.solana.com"; +static String gShineServerUrl = "https://shineup.me"; +static String gServerStatusMessage = "Edit RPC or shine host"; + +static EditContext gEditContext = EDIT_CONTEXT_NONE; +static Screen gEditReturnScreen = SCREEN_HOME; +static String gEditTitle; +static String gEditHint; +static String gEditValue; +static int gKeyboardPage = 0; +static bool gEditIsPassword = false; +static KeyboardMode gKeyboardMode = KEYBOARD_MODE_ALPHA; +static bool gKeyboardShift = false; + static void rebuildScreen(); static void showScreen(Screen screen); static void handleSwipe(SwipeDirection swipe); +static void loadPrefs(); +static void saveWifiPrefs(); +static void clearWifiPrefs(); +static void saveServerPrefs(); +static void beginSavedWifi(); +static void scanWifiNetworks(); +static bool connectWifiNow(const String &ssid, const String &password); +static String wifiHomeStatus(); +static String wifiSavedLabel(); +static void openEditor(EditContext context, + Screen returnScreen, + const String &title, + const String &hint, + const String &value, + bool isPassword); +static void applyEditorValue(); +static bool isTextEditKeyboardSwipeArea(int16_t x, int16_t y); static void lvglFlushCb(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *colorP) { uint32_t w = area->x2 - area->x1 + 1; @@ -177,12 +250,242 @@ static lv_obj_t *makeBody(const char *text, lv_coord_t y, lv_coord_t width) { return label; } -static void actionButtonCb(lv_event_t *event) { - if (lv_event_get_code(event) != LV_EVENT_CLICKED) { +static void showMessageAt(const String &text, lv_coord_t y) { + lv_obj_t *label = lv_label_create(gRoot); + lv_label_set_text(label, text.c_str()); + lv_obj_set_width(label, 420); + lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_font(label, &lv_font_montserrat_16, 0); + lv_obj_set_style_text_color(label, lv_color_hex(0xB8C6D3), 0); + lv_obj_align(label, LV_ALIGN_TOP_MID, 0, y); +} + +static void loadPrefs() { + gWifiSavedSsid = gPrefs.getString("wifi_ssid", ""); + gWifiSavedPassword = gPrefs.getString("wifi_pass", ""); + gSolanaRpcUrl = gPrefs.getString("solana_rpc", "https://api.devnet.solana.com"); + gShineServerUrl = gPrefs.getString("shine_server", "https://shineup.me"); +} + +static void saveWifiPrefs() { + gPrefs.putString("wifi_ssid", gWifiSavedSsid); + gPrefs.putString("wifi_pass", gWifiSavedPassword); +} + +static void saveServerPrefs() { + gPrefs.putString("solana_rpc", gSolanaRpcUrl); + gPrefs.putString("shine_server", gShineServerUrl); +} + +static void clearWifiPrefs() { + gWifiSavedSsid = ""; + gWifiSavedPassword = ""; + gWifiSelectedSsid = ""; + saveWifiPrefs(); + WiFi.disconnect(true, false); + gWifiStatusMessage = "Saved Wi-Fi cleared"; +} + +static void beginSavedWifi() { + if (gWifiSavedSsid.isEmpty()) { return; } - if (gBlockClick) { + WiFi.mode(WIFI_STA); + WiFi.begin(gWifiSavedSsid.c_str(), gWifiSavedPassword.c_str()); +} + +static String wifiHomeStatus() { + if (gWifiSavedSsid.isEmpty()) { + return "Wi-Fi not configured"; + } + if (WiFi.status() == WL_CONNECTED) { + return "Wi-Fi connected"; + } + return "Wi-Fi disconnected"; +} + +static String wifiSavedLabel() { + if (gWifiSavedSsid.isEmpty()) { + return "Saved: none"; + } + return String("Saved: ") + gWifiSavedSsid; +} + +static void scanWifiNetworks() { + gScanResultCount = 0; + gWifiStatusMessage = "Scanning networks..."; + WiFi.mode(WIFI_STA); + WiFi.disconnect(false, false); + delay(100); + + int found = WiFi.scanNetworks(); + if (found <= 0) { + gWifiStatusMessage = "No networks found"; + gWifiViewMode = WIFI_VIEW_OVERVIEW; + return; + } + + gScanResultCount = min(found, MAX_SCAN_RESULTS); + for (int i = 0; i < gScanResultCount; ++i) { + gScanResults[i] = WiFi.SSID(i); + } + WiFi.scanDelete(); + gWifiStatusMessage = String("Found ") + found + " networks"; + gWifiViewMode = WIFI_VIEW_SCAN_RESULTS; +} + +static bool connectWifiNow(const String &ssid, const String &password) { + gWifiStatusMessage = String("Connecting to ") + ssid + "..."; + rebuildScreen(); + lv_timer_handler(); + + WiFi.mode(WIFI_STA); + WiFi.disconnect(false, false); + delay(150); + WiFi.begin(ssid.c_str(), password.c_str()); + + unsigned long started = millis(); + while (millis() - started < WIFI_CONNECT_TIMEOUT_MS) { + if (WiFi.status() == WL_CONNECTED) { + gWifiSavedSsid = ssid; + gWifiSavedPassword = password; + saveWifiPrefs(); + gWifiStatusMessage = String("Connected: ") + WiFi.localIP().toString(); + return true; + } + delay(200); + } + + WiFi.disconnect(false, false); + gWifiStatusMessage = "Connection failed"; + return false; +} + +static void openEditor(EditContext context, + Screen returnScreen, + const String &title, + const String &hint, + const String &value, + bool isPassword) { + gEditContext = context; + gEditReturnScreen = returnScreen; + gEditTitle = title; + gEditHint = hint; + gEditValue = value; + gEditIsPassword = isPassword; + gKeyboardPage = 0; + gKeyboardMode = KEYBOARD_MODE_ALPHA; + gKeyboardShift = false; + showScreen(SCREEN_TEXT_EDIT); +} + +static void applyEditorValue() { + String value = gInputTextArea ? String(lv_textarea_get_text(gInputTextArea)) : gEditValue; + + if (gEditContext == EDIT_CONTEXT_WIFI_PASSWORD) { + bool ok = connectWifiNow(gWifiSelectedSsid, value); + gWifiViewMode = WIFI_VIEW_OVERVIEW; + if (ok) { + showScreen(SCREEN_HOME); + } else { + showScreen(SCREEN_WIFI); + } + return; + } + + if (gEditContext == EDIT_CONTEXT_SOLANA_RPC) { + gSolanaRpcUrl = value; + saveServerPrefs(); + gServerStatusMessage = "Solana RPC saved"; + showScreen(SCREEN_SERVER); + return; + } + + if (gEditContext == EDIT_CONTEXT_SHINE_SERVER) { + gShineServerUrl = value; + saveServerPrefs(); + gServerStatusMessage = "Shine server saved"; + showScreen(SCREEN_SERVER); + return; + } +} + +static void cancelEditor() { + if (gEditContext == EDIT_CONTEXT_WIFI_PASSWORD) { + showScreen(SCREEN_WIFI); + } else { + showScreen(gEditReturnScreen); + } +} + +static void networkSelectCb(lv_event_t *event) { + if (lv_event_get_code(event) != LV_EVENT_CLICKED || gBlockClick) { + return; + } + + int index = static_cast(reinterpret_cast(lv_event_get_user_data(event))); + if (index < 0 || index >= gScanResultCount) { + return; + } + + gWifiSelectedSsid = gScanResults[index]; + openEditor(EDIT_CONTEXT_WIFI_PASSWORD, + SCREEN_WIFI, + "ENTER PASSWORD", + String("SSID: ") + gWifiSelectedSsid, + "", + true); +} + +static void editorKeyCb(lv_event_t *event) { + if (lv_event_get_code(event) != LV_EVENT_CLICKED || gBlockClick || !gInputTextArea) { + return; + } + + const char *token = static_cast(lv_event_get_user_data(event)); + if (!token) { + return; + } + + if (strcmp(token, "") == 0) { + lv_textarea_del_char(gInputTextArea); + return; + } + if (strcmp(token, "") == 0) { + lv_textarea_set_text(gInputTextArea, ""); + return; + } + if (strcmp(token, "") == 0) { + applyEditorValue(); + return; + } + if (strcmp(token, "") == 0) { + cancelEditor(); + return; + } + if (strcmp(token, "") == 0) { + gKeyboardMode = (gKeyboardMode == KEYBOARD_MODE_ALPHA) ? KEYBOARD_MODE_SYMBOLS : KEYBOARD_MODE_ALPHA; + gKeyboardPage = 0; + rebuildScreen(); + return; + } + if (strcmp(token, "") == 0) { + gKeyboardShift = !gKeyboardShift; + rebuildScreen(); + return; + } + + String out = token; + if (gKeyboardMode == KEYBOARD_MODE_ALPHA && gKeyboardShift) { + out.toUpperCase(); + } + + lv_textarea_add_text(gInputTextArea, out.c_str()); +} + +static void actionButtonCb(lv_event_t *event) { + if (lv_event_get_code(event) != LV_EVENT_CLICKED || gBlockClick) { return; } @@ -192,6 +495,7 @@ static void actionButtonCb(lv_event_t *event) { showScreen(SCREEN_SETTINGS_MENU); break; case ACTION_OPEN_WIFI: + gWifiViewMode = WIFI_VIEW_OVERVIEW; showScreen(SCREEN_WIFI); break; case ACTION_OPEN_SERVER: @@ -204,8 +508,40 @@ static void actionButtonCb(lv_event_t *event) { showScreen(SCREEN_HOME); break; case ACTION_BACK_SETTINGS: + gWifiViewMode = WIFI_VIEW_OVERVIEW; showScreen(SCREEN_SETTINGS_MENU); break; + case ACTION_WIFI_SELECT_NETWORK: + scanWifiNetworks(); + rebuildScreen(); + break; + case ACTION_WIFI_CLEAR: + clearWifiPrefs(); + gWifiViewMode = WIFI_VIEW_OVERVIEW; + rebuildScreen(); + break; + case ACTION_SERVER_EDIT_SOLANA: + openEditor(EDIT_CONTEXT_SOLANA_RPC, + SCREEN_SERVER, + "EDIT SOLANA RPC", + "Cursor starts at the end.", + gSolanaRpcUrl, + false); + break; + case ACTION_SERVER_EDIT_SHINE: + openEditor(EDIT_CONTEXT_SHINE_SERVER, + SCREEN_SERVER, + "EDIT SHINE HOST", + "Cursor starts at the end.", + gShineServerUrl, + false); + break; + case ACTION_EDITOR_SAVE: + applyEditorValue(); + break; + case ACTION_EDITOR_CANCEL: + cancelEditor(); + break; case ACTION_NONE: default: break; @@ -239,6 +575,33 @@ static lv_obj_t *makeButton(const char *text, return btn; } +static lv_obj_t *makeKeyboardButton(const char *label, + const char *token, + lv_coord_t x, + lv_coord_t y, + lv_coord_t w, + lv_coord_t h, + uint32_t bgColor, + const lv_font_t *font = &lv_font_montserrat_18) { + lv_obj_t *btn = lv_btn_create(gRoot); + lv_obj_set_size(btn, w, h); + lv_obj_set_pos(btn, x, y); + lv_obj_set_style_radius(btn, 12, 0); + lv_obj_set_style_bg_color(btn, lv_color_hex(bgColor), 0); + lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(btn, 1, 0); + lv_obj_set_style_border_color(btn, lv_color_hex(0x6E8AA3), 0); + lv_obj_set_style_shadow_width(btn, 0, 0); + lv_obj_add_event_cb(btn, editorKeyCb, LV_EVENT_CLICKED, const_cast(token)); + + lv_obj_t *textObj = lv_label_create(btn); + lv_label_set_text(textObj, label); + lv_obj_set_style_text_font(textObj, font, 0); + lv_obj_set_style_text_color(textObj, lv_color_hex(0xFFFFFF), 0); + lv_obj_center(textObj); + return btn; +} + static void makeTopIcons() { lv_obj_t *icons = lv_label_create(gRoot); lv_label_set_text(icons, "W BAT"); @@ -271,8 +634,10 @@ static void drawHome() { lv_obj_align_to(subserver, login, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 6); makeTopIcons(); - makeTitle("STATUS", 165, &lv_font_montserrat_28); - makeBody("Swipe left or tap the button below.", 214, 360); + makeTitle("STATUS", 150, &lv_font_montserrat_28); + showMessageAt(wifiHomeStatus(), 204); + showMessageAt(wifiSavedLabel(), 238); + makeBody("Swipe left or tap the button below.", 274, 360); makeButton("SETTINGS", 22, 360, 436, 78, 0x2A6F97, ACTION_OPEN_SETTINGS, &lv_font_montserrat_24); makeVersionTag(); } @@ -295,7 +660,7 @@ static void drawSettingsMenu() { if (itemIndex == 2) action = ACTION_OPEN_ACCOUNT; makeButton(kMenuItems[itemIndex], 22, 132 + visibleIndex * 126, 436, 104, - visibleIndex == 0 ? 0x355C7D : 0x2A9D8F, action, &lv_font_montserrat_24); + 0x355C7D, action, &lv_font_montserrat_24); } lv_obj_t *hint = lv_label_create(gRoot); @@ -312,25 +677,189 @@ static void drawSettingsMenu() { makeVersionTag(); } -static void drawSimpleScreen(const char *title, const char *body) { +static lv_obj_t *makeNetworkButton(const char *text, int index, lv_coord_t y) { + lv_obj_t *btn = lv_btn_create(gRoot); + lv_obj_set_size(btn, 436, 52); + lv_obj_set_pos(btn, 22, y); + lv_obj_set_style_radius(btn, 14, 0); + lv_obj_set_style_bg_color(btn, lv_color_hex(0x2A6F97), 0); + lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(btn, 2, 0); + lv_obj_set_style_border_color(btn, lv_color_hex(0x6E8AA3), 0); + lv_obj_add_event_cb(btn, networkSelectCb, LV_EVENT_CLICKED, reinterpret_cast(static_cast(index))); + + lv_obj_t *label = lv_label_create(btn); + lv_label_set_text(label, text); + lv_obj_set_width(label, 404); + lv_label_set_long_mode(label, LV_LABEL_LONG_DOT); + lv_obj_set_style_text_font(label, &lv_font_montserrat_18, 0); + lv_obj_set_style_text_color(label, lv_color_hex(0xFFFFFF), 0); + lv_obj_center(label); + return btn; +} + +static void drawWifiOverview() { + makeTitle("Wi-Fi", 18, &lv_font_montserrat_24); + showMessageAt(wifiHomeStatus(), 60); + showMessageAt(wifiSavedLabel(), 92); + showMessageAt(gWifiStatusMessage, 124); + makeButton("SELECT NETWORK", 22, 182, 436, 68, 0x2A6F97, ACTION_WIFI_SELECT_NETWORK, &lv_font_montserrat_22); + makeButton("CLEAR SAVED WI-FI", 22, 270, 436, 68, 0x7D3A3A, ACTION_WIFI_CLEAR, &lv_font_montserrat_20); + makeButton("BACK", 140, 360, 200, 72, 0x5A6570, ACTION_BACK_SETTINGS, &lv_font_montserrat_22); +} + +static void drawWifiScanResults() { + makeTitle("SELECT NETWORK", 18, &lv_font_montserrat_24); + showMessageAt(gWifiStatusMessage, 58); + if (gScanResultCount == 0) { + showMessageAt("No networks available", 110); + } + for (int i = 0; i < gScanResultCount; ++i) { + makeNetworkButton(gScanResults[i].c_str(), i, 100 + i * 58); + } + makeButton("SCAN AGAIN", 22, 370, 210, 64, 0x2A6F97, ACTION_WIFI_SELECT_NETWORK, &lv_font_montserrat_20); + makeButton("BACK", 248, 370, 210, 64, 0x5A6570, ACTION_BACK_SETTINGS, &lv_font_montserrat_20); +} + +static void drawWifiScreen() { setRootStyle(); - makeTitle(title, 56, &lv_font_montserrat_28); - makeBody(body, 132, 400); + + if (gWifiViewMode == WIFI_VIEW_OVERVIEW) { + drawWifiOverview(); + } else { + drawWifiScanResults(); + } + + makeVersionTag(); +} + +static void drawServerScreen() { + setRootStyle(); + makeTitle("SERVER", 18, &lv_font_montserrat_24); + showMessageAt(gServerStatusMessage, 56); + showMessageAt(String("Solana: ") + gSolanaRpcUrl, 96); + makeButton("SOLANA RPC", 22, 146, 436, 84, 0x355C7D, ACTION_SERVER_EDIT_SOLANA, &lv_font_montserrat_24); + showMessageAt(String("Shine: ") + gShineServerUrl, 248); + makeButton("SHINE SERVER", 22, 298, 436, 84, 0x355C7D, ACTION_SERVER_EDIT_SHINE, &lv_font_montserrat_24); + makeBody("Swipe right to return to Settings.", 396, 420); + makeVersionTag(); +} + +static void drawAccountScreen() { + setRootStyle(); + makeTitle("ACCOUNT", 56, &lv_font_montserrat_28); + makeBody("Account screen", 132, 400); makeBody("Swipe right to return to Settings.", 204, 400); makeButton("BACK", 140, 344, 200, 72, 0x5A6570, ACTION_BACK_SETTINGS, &lv_font_montserrat_22); makeVersionTag(); } -static void drawWifiScreen() { - drawSimpleScreen("Wi-Fi", "Wi-Fi screen"); +static void drawKeyRow(const char *const *tokens, + int count, + lv_coord_t x, + lv_coord_t y, + lv_coord_t w, + lv_coord_t h, + lv_coord_t gap, + uint32_t bgColor, + bool uppercase = false, + const lv_font_t *font = &lv_font_montserrat_18) { + for (int i = 0; i < count; ++i) { + String label = tokens[i]; + if (uppercase) { + label.toUpperCase(); + } + makeKeyboardButton(label.c_str(), tokens[i], x + i * (w + gap), y, w, h, bgColor, font); + } } -static void drawServerScreen() { - drawSimpleScreen("Server", "Server screen"); +static void drawTextEditorKeyboard(lv_coord_t originY) { + static const char *alphaLeftRow0[] = {"q", "w", "e", "r", "t"}; + static const char *alphaLeftRow1[] = {"a", "s", "d", "f", "g"}; + static const char *alphaLeftRow2[] = {"z", "x", "c", "v", "b"}; + static const char *alphaRightRow0[] = {"y", "u", "i", "o", "p"}; + static const char *alphaRightRow1[] = {"h", "j", "k", "l"}; + static const char *alphaRightRow2[] = {"n", "m", ".", "-"}; + + static const char *symLeftRow0[] = {"1", "2", "3", "4", "5"}; + static const char *symLeftRow1[] = {"://", "/", ":", "_", "@"}; + static const char *symLeftRow2[] = {".com", ".net", ".org", ".me", ".io"}; + static const char *symRightRow0[] = {"6", "7", "8", "9", "0"}; + static const char *symRightRow1[] = {"?", "&", "=", "+", "-"}; + static const char *symRightRow2[] = {"%", "#", "~", ":", "/"}; + + const lv_coord_t keyH = 50; + const lv_coord_t keyW5 = 84; + const lv_coord_t keyW4 = 104; + const lv_coord_t gap = 8; + const lv_coord_t row0Y = originY; + const lv_coord_t row1Y = originY + 58; + const lv_coord_t row2Y = originY + 116; + const lv_coord_t row3Y = originY + 180; + const bool uppercase = gKeyboardMode == KEYBOARD_MODE_ALPHA && gKeyboardShift; + + if (gKeyboardMode == KEYBOARD_MODE_ALPHA) { + if (gKeyboardPage == 0) { + drawKeyRow(alphaLeftRow0, 5, 20, row0Y, keyW5, keyH, gap, 0x2A6F97, uppercase); + drawKeyRow(alphaLeftRow1, 5, 20, row1Y, keyW5, keyH, gap, 0x2A6F97, uppercase); + drawKeyRow(alphaLeftRow2, 5, 20, row2Y, keyW5, keyH, gap, 0x2A6F97, uppercase); + } else { + drawKeyRow(alphaRightRow0, 5, 20, row0Y, keyW5, keyH, gap, 0x2A6F97, uppercase); + drawKeyRow(alphaRightRow1, 4, 20, row1Y, keyW5, keyH, gap, 0x2A6F97, uppercase); + drawKeyRow(alphaRightRow2, 4, 20, row2Y, keyW5, keyH, gap, 0x2A6F97, uppercase); + } + } else { + if (gKeyboardPage == 0) { + drawKeyRow(symLeftRow0, 5, 20, row0Y, keyW5, keyH, gap, 0x2A6F97); + drawKeyRow(symLeftRow1, 5, 20, row1Y, keyW5, keyH, gap, 0x2A6F97, false, &lv_font_montserrat_16); + drawKeyRow(symLeftRow2, 5, 20, row2Y, keyW5, keyH, gap, 0x2A6F97, false, &lv_font_montserrat_16); + } else { + drawKeyRow(symRightRow0, 5, 20, row0Y, keyW5, keyH, gap, 0x2A6F97); + drawKeyRow(symRightRow1, 5, 20, row1Y, keyW5, keyH, gap, 0x2A6F97, false, &lv_font_montserrat_16); + drawKeyRow(symRightRow2, 5, 20, row2Y, keyW5, keyH, gap, 0x2A6F97, false, &lv_font_montserrat_16); + } + } + + makeKeyboardButton(gKeyboardMode == KEYBOARD_MODE_ALPHA ? "123" : "ABC", "", 20, row3Y, 88, 54, 0x5C6F82, &lv_font_montserrat_18); + makeKeyboardButton(gKeyboardShift ? "SHIFT*" : "SHIFT", "", 116, row3Y, 88, 54, 0x7A5C9B, &lv_font_montserrat_16); + makeKeyboardButton("DEL", "", 212, row3Y, 76, 54, 0x7D3A3A, &lv_font_montserrat_18); + makeKeyboardButton("SAVE", "", 296, row3Y, 76, 54, 0x2A9D8F, &lv_font_montserrat_18); + makeKeyboardButton("CANCEL", "", 380, row3Y, 80, 54, 0x5A6570, &lv_font_montserrat_16); } -static void drawAccountScreen() { - drawSimpleScreen("Account", "Account screen"); +static void drawTextEditScreen() { + setRootStyle(); + gInputTextArea = nullptr; + + makeTitle(gEditTitle.c_str(), 8, &lv_font_montserrat_24); + + gInputTextArea = lv_textarea_create(gRoot); + lv_obj_set_size(gInputTextArea, 436, 58); + lv_obj_set_pos(gInputTextArea, 22, 48); + lv_textarea_set_one_line(gInputTextArea, true); + lv_textarea_set_text(gInputTextArea, gEditValue.c_str()); + lv_textarea_set_cursor_pos(gInputTextArea, LV_TEXTAREA_CURSOR_LAST); + lv_textarea_set_password_mode(gInputTextArea, gEditIsPassword); + lv_obj_set_style_text_font(gInputTextArea, &lv_font_montserrat_18, 0); + lv_obj_set_style_bg_color(gInputTextArea, lv_color_hex(0x132131), 0); + lv_obj_set_style_text_color(gInputTextArea, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_border_color(gInputTextArea, lv_color_hex(0x6E8AA3), 0); + lv_obj_set_style_border_width(gInputTextArea, 2, 0); + lv_obj_add_state(gInputTextArea, LV_STATE_FOCUSED); + + lv_obj_t *panel = lv_obj_create(gRoot); + lv_obj_set_size(panel, TEXT_EDIT_PANEL_W, TEXT_EDIT_PANEL_H); + lv_obj_set_pos(panel, TEXT_EDIT_PANEL_X, TEXT_EDIT_PANEL_Y); + lv_obj_set_style_radius(panel, 16, 0); + lv_obj_set_style_bg_color(panel, lv_color_hex(0x0F1A27), 0); + lv_obj_set_style_bg_opa(panel, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(panel, 1, 0); + lv_obj_set_style_border_color(panel, lv_color_hex(0x2E455A), 0); + lv_obj_clear_flag(panel, LV_OBJ_FLAG_SCROLLABLE); + + drawTextEditorKeyboard(TEXT_EDIT_PANEL_Y + 10); + + makeVersionTag(); } static void rebuildScreen() { @@ -356,6 +885,9 @@ static void rebuildScreen() { case SCREEN_ACCOUNT: drawAccountScreen(); break; + case SCREEN_TEXT_EDIT: + drawTextEditScreen(); + break; } } @@ -396,6 +928,7 @@ static void handleSettingsSwipe(SwipeDirection swipe) { static void handleWifiSwipe(SwipeDirection swipe) { if (swipe == SWIPE_RIGHT) { + gWifiViewMode = WIFI_VIEW_OVERVIEW; showScreen(SCREEN_SETTINGS_MENU); } } @@ -412,6 +945,26 @@ static void handleAccountSwipe(SwipeDirection swipe) { } } +static void handleTextEditSwipe(SwipeDirection swipe) { + if (!isTextEditKeyboardSwipeArea(gTouchStartX, gTouchStartY)) { + return; + } + if (swipe == SWIPE_LEFT) { + gKeyboardPage = min(gKeyboardPage + 1, 1); + rebuildScreen(); + return; + } + if (swipe == SWIPE_RIGHT) { + gKeyboardPage = max(gKeyboardPage - 1, 0); + rebuildScreen(); + } +} + +static bool isTextEditKeyboardSwipeArea(int16_t x, int16_t y) { + return x >= TEXT_EDIT_PANEL_X && x <= (TEXT_EDIT_PANEL_X + TEXT_EDIT_PANEL_W) && + y >= TEXT_EDIT_PANEL_Y && y <= (TEXT_EDIT_PANEL_Y + TEXT_EDIT_PANEL_H); +} + static void handleSwipe(SwipeDirection swipe) { if (swipe == SWIPE_NONE) { return; @@ -433,6 +986,9 @@ static void handleSwipe(SwipeDirection swipe) { case SCREEN_ACCOUNT: handleAccountSwipe(swipe); break; + case SCREEN_TEXT_EDIT: + handleTextEditSwipe(swipe); + break; } } @@ -451,6 +1007,10 @@ void setup() { gTouch.setSwapXY(true); gTouch.setMirrorXY(true, false); + gPrefs.begin("shine-nav-ui", false); + loadPrefs(); + beginSavedWifi(); + lv_init(); uint32_t screenWidth = gfx->width(); diff --git a/VERSION.properties b/VERSION.properties index d3fcf82..df68587 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.142 -server.version=1.2.134 +client.version=1.2.143 +server.version=1.2.135