ESP32: зафиксировать промежуточный NAV v6 UI прототип

This commit is contained in:
AidarKC 2026-06-09 15:22:45 +04:00
parent 32606fe1c2
commit e385bb6bf9
6 changed files with 719 additions and 35 deletions

View File

@ -1,17 +1,44 @@
# ESP32 nav minimal test # ESP32 nav minimal test
- Краткое описание: новый минимальный навигационный UI-прототип для сабсервера на базе рабочего `LVGL + subserver touch`. - Краткое описание: минимальный UI-прототип для сабсервера на базе `LVGL + subserver touch`, с Wi-Fi flow, серверными адресами и общим экраном редактирования текста.
- Что проверять: - Что проверять:
- стартует экран `HOME`; - стартует экран `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`; - кнопка `SETTINGS` открывает `SETTINGS_MENU`;
- свайп влево на `HOME` открывает `SETTINGS_MENU`; - свайп влево на `HOME` открывает `SETTINGS_MENU`;
- в `SETTINGS_MENU` сначала видны только `Wi-Fi` и `Server`; - в `SETTINGS_MENU` сначала видны только `Wi-Fi` и `Server`;
- обе видимые карточки меню одного цвета;
- свайп вверх показывает `Server` и `Account`; - свайп вверх показывает `Server` и `Account`;
- свайп вниз возвращает `Wi-Fi` и `Server`; - свайп вниз возвращает `Wi-Fi` и `Server`;
- свайп вправо из `SETTINGS_MENU` возвращает на `HOME`; - свайп вправо из `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`; - свайп вправо из внутренних экранов возвращает в `SETTINGS_MENU`;
- если во время реального свайпа палец проходит по кнопке, это не должно открывать кнопку как обычный `click`. - если во время реального свайпа палец проходит по кнопке, это не должно открывать кнопку как обычный `click`.
- Ожидаемый результат: новый скетч даёт чистый навигационный каркас без старой перегруженной логики. - Ожидаемый результат: новый скетч даёт чистый навигационный каркас и уже умеет настраивать Wi-Fi и серверные адреса на самой ESP32.
- Статус: pending - Статус: pending

View File

@ -4,12 +4,11 @@
## Цель ## Цель
Этот прототип проверяет только базовую механику экранов, крупных кнопок и свайпов. Этот прототип проверяет базовую механику экранов, крупных кнопок, свайпов, первичную настройку Wi-Fi и настройку серверных адресов через общий экран редактирования текста.
На этом этапе отсутствуют: На этом этапе отсутствуют:
- реальный Wi-Fi; - логика серверной проверки доступности;
- реальные серверные проверки; - логин/пароль учётной записи SHiNE;
- логин/пароль;
- PIN; - PIN;
- кошелёк; - кошелёк;
- QR; - QR;
@ -20,12 +19,13 @@
## Экраны ## Экраны
Прототип содержит 5 экранов: Прототип содержит 6 экранов:
- `HOME` - `HOME`
- `SETTINGS_MENU` - `SETTINGS_MENU`
- `WIFI_SCREEN` - `WIFI_SCREEN`
- `SERVER_SCREEN` - `SERVER_SCREEN`
- `ACCOUNT_SCREEN` - `ACCOUNT_SCREEN`
- `TEXT_EDIT_SCREEN`
## HOME ## HOME
@ -34,8 +34,15 @@
- ниже `subserver1`; - ниже `subserver1`;
- сверху справа простые индикаторы `W BAT`; - сверху справа простые индикаторы `W BAT`;
- по центру крупный текст `STATUS`; - по центру крупный текст `STATUS`;
- статус Wi-Fi;
- сохранённый SSID или пометку, что он не сохранён;
- снизу большую кнопку `SETTINGS`. - снизу большую кнопку `SETTINGS`.
Статусы Wi-Fi на `HOME`:
- `Wi-Fi not configured`
- `Wi-Fi disconnected`
- `Wi-Fi connected`
Переходы: Переходы:
- кнопка `SETTINGS` -> `SETTINGS_MENU`; - кнопка `SETTINGS` -> `SETTINGS_MENU`;
- свайп влево -> `SETTINGS_MENU`. - свайп влево -> `SETTINGS_MENU`.
@ -61,6 +68,8 @@
- `Wi-Fi` - `Wi-Fi`
- `Server` - `Server`
Обе видимые карточки меню имеют одинаковый цвет.
Переходы: Переходы:
- нажатие `Wi-Fi` -> `WIFI_SCREEN`; - нажатие `Wi-Fi` -> `WIFI_SCREEN`;
- нажатие `Server` -> `SERVER_SCREEN`; - нажатие `Server` -> `SERVER_SCREEN`;
@ -69,23 +78,52 @@
## WIFI_SCREEN ## WIFI_SCREEN
Экран содержит 2 внутренних режима.
### 1. Overview
Показывает: Показывает:
- `Wi-Fi` - текущий Wi-Fi статус;
- `Wi-Fi screen` - сохранённый 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` - кнопка `BACK` -> `SETTINGS_MENU`
## SERVER_SCREEN ## 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` - свайп вправо -> `SETTINGS_MENU`
- кнопка `BACK` -> `SETTINGS_MENU`
## ACCOUNT_SCREEN ## ACCOUNT_SCREEN
@ -97,6 +135,63 @@
- свайп вправо -> `SETTINGS_MENU` - свайп вправо -> `SETTINGS_MENU`
- кнопка `BACK` -> `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` - `WIFI_SCREEN`: свайп вправо -> `SETTINGS_MENU`
- `SERVER_SCREEN`: свайп вправо -> `SETTINGS_MENU` - `SERVER_SCREEN`: свайп вправо -> `SETTINGS_MENU`
- `ACCOUNT_SCREEN`: свайп вправо -> `SETTINGS_MENU` - `ACCOUNT_SCREEN`: свайп вправо -> `SETTINGS_MENU`
- `TEXT_EDIT_SCREEN`: свайп влево/вправо -> переключение страниц клавиатуры
- переключение страниц клавиатуры срабатывает только если свайп начался в зоне самой клавиатуры, а не по всему экрану редактора
## Особенность обработки жестов ## Особенность обработки жестов

View File

@ -20,7 +20,7 @@
- `lvgl-official-based-test` — наш минимальный экран, но на максимально близкой к официальному `LVGL_Widgets` инициализации - `lvgl-official-based-test` — наш минимальный экран, но на максимально близкой к официальному `LVGL_Widgets` инициализации
- `lvgl-subserver-touch-test` — гибрид: `LVGL`-интерфейс, но display/touch init и raw touch-read взяты из `shine_subserver_ui`; подтверждено на устройстве, touch работает, зелёных линий по краям нет - `lvgl-subserver-touch-test` — гибрид: `LVGL`-интерфейс, но display/touch init и raw touch-read взяты из `shine_subserver_ui`; подтверждено на устройстве, touch работает, зелёных линий по краям нет
- `lvgl-russian-font-test` — тест кастомного `LVGL`-шрифта с кириллицей: русские кнопки, длинные подписи и статусы - `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
Запуск: Запуск:

View File

@ -14,7 +14,7 @@
- `lvgl_official_based_test/` - минимальный наш экран поверх максимально близкой к официальному `05_LVGL_Widgets` инициализации - `lvgl_official_based_test/` - минимальный наш экран поверх максимально близкой к официальному `05_LVGL_Widgets` инициализации
- `lvgl_subserver_touch_test/` - гибридный тест: `LVGL`-экран с инициализацией дисплея и чтением touch из `shine_subserver_ui`; подтверждён на реальном устройстве - `lvgl_subserver_touch_test/` - гибридный тест: `LVGL`-экран с инициализацией дисплея и чтением touch из `shine_subserver_ui`; подтверждён на реальном устройстве
- `lvgl_russian_font_test/` - тест кастомного кириллического `LVGL`-шрифта с русскими кнопками, длинными строками и рабочим touch - `lvgl_russian_font_test/` - тест кастомного кириллического `LVGL`-шрифта с русскими кнопками, длинными строками и рабочим touch
- `lvgl_nav_minimal_test/` - новый минимальный навигационный каркас сабсервера на рабочем `LVGL + subserver touch` - `lvgl_nav_minimal_test/` - новый минимальный навигационный каркас сабсервера на рабочем `LVGL + subserver touch`, расширенный настройкой Wi-Fi и сохранением в NVS
## Запуск ## Запуск

View File

@ -1,5 +1,7 @@
#include <Arduino.h> #include <Arduino.h>
#include <Wire.h> #include <Wire.h>
#include <WiFi.h>
#include <Preferences.h>
#include <lvgl.h> #include <lvgl.h>
#include <Arduino_GFX_Library.h> #include <Arduino_GFX_Library.h>
#include <TouchDrvCSTXXX.hpp> #include <TouchDrvCSTXXX.hpp>
@ -21,7 +23,13 @@
#define LVGL_TICK_MS 2 #define LVGL_TICK_MS 2
#define SWIPE_THRESHOLD 48 #define SWIPE_THRESHOLD 48
#define TAP_CANCEL_THRESHOLD 18 #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 { enum Screen {
SCREEN_HOME, SCREEN_HOME,
@ -29,6 +37,7 @@ enum Screen {
SCREEN_WIFI, SCREEN_WIFI,
SCREEN_SERVER, SCREEN_SERVER,
SCREEN_ACCOUNT, SCREEN_ACCOUNT,
SCREEN_TEXT_EDIT,
}; };
enum SwipeDirection { enum SwipeDirection {
@ -47,6 +56,29 @@ enum ActionId {
ACTION_OPEN_ACCOUNT, ACTION_OPEN_ACCOUNT,
ACTION_BACK_HOME, ACTION_BACK_HOME,
ACTION_BACK_SETTINGS, 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"}; 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 *gBuf1 = nullptr;
static lv_color_t *gBuf2 = nullptr; static lv_color_t *gBuf2 = nullptr;
static lv_obj_t *gRoot = nullptr; static lv_obj_t *gRoot = nullptr;
static lv_obj_t *gInputTextArea = nullptr;
Arduino_DataBus *gBus = new Arduino_ESP32QSPI( Arduino_DataBus *gBus = new Arduino_ESP32QSPI(
PIN_LCD_CS, PIN_LCD_SCLK, PIN_LCD_D0, PIN_LCD_D1, PIN_LCD_D2, PIN_LCD_D3); PIN_LCD_CS, PIN_LCD_SCLK, PIN_LCD_D0, PIN_LCD_D1, PIN_LCD_D2, PIN_LCD_D3);
Arduino_CO5300 *gfx = new Arduino_CO5300( Arduino_CO5300 *gfx = new Arduino_CO5300(
gBus, PIN_LCD_RST, 0, DISP_W, DISP_H, 0, 0, 0, 0); gBus, PIN_LCD_RST, 0, DISP_W, DISP_H, 0, 0, 0, 0);
TouchDrvCST92xx gTouch; TouchDrvCST92xx gTouch;
Preferences gPrefs;
static Screen gCurrentScreen = SCREEN_HOME; static Screen gCurrentScreen = SCREEN_HOME;
static int gSettingsScrollIndex = 0; static int gSettingsScrollIndex = 0;
@ -73,9 +107,48 @@ static int16_t gTouchLastY = 0;
static SwipeDirection gPendingSwipe = SWIPE_NONE; static SwipeDirection gPendingSwipe = SWIPE_NONE;
static bool gBlockClick = false; 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 rebuildScreen();
static void showScreen(Screen screen); static void showScreen(Screen screen);
static void handleSwipe(SwipeDirection swipe); 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) { 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; 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; return label;
} }
static void actionButtonCb(lv_event_t *event) { static void showMessageAt(const String &text, lv_coord_t y) {
if (lv_event_get_code(event) != LV_EVENT_CLICKED) { 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; 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<int>(reinterpret_cast<uintptr_t>(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<const char *>(lv_event_get_user_data(event));
if (!token) {
return;
}
if (strcmp(token, "<DEL>") == 0) {
lv_textarea_del_char(gInputTextArea);
return;
}
if (strcmp(token, "<CLR>") == 0) {
lv_textarea_set_text(gInputTextArea, "");
return;
}
if (strcmp(token, "<SAVE>") == 0) {
applyEditorValue();
return;
}
if (strcmp(token, "<CANCEL>") == 0) {
cancelEditor();
return;
}
if (strcmp(token, "<MODE>") == 0) {
gKeyboardMode = (gKeyboardMode == KEYBOARD_MODE_ALPHA) ? KEYBOARD_MODE_SYMBOLS : KEYBOARD_MODE_ALPHA;
gKeyboardPage = 0;
rebuildScreen();
return;
}
if (strcmp(token, "<SHIFT>") == 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; return;
} }
@ -192,6 +495,7 @@ static void actionButtonCb(lv_event_t *event) {
showScreen(SCREEN_SETTINGS_MENU); showScreen(SCREEN_SETTINGS_MENU);
break; break;
case ACTION_OPEN_WIFI: case ACTION_OPEN_WIFI:
gWifiViewMode = WIFI_VIEW_OVERVIEW;
showScreen(SCREEN_WIFI); showScreen(SCREEN_WIFI);
break; break;
case ACTION_OPEN_SERVER: case ACTION_OPEN_SERVER:
@ -204,8 +508,40 @@ static void actionButtonCb(lv_event_t *event) {
showScreen(SCREEN_HOME); showScreen(SCREEN_HOME);
break; break;
case ACTION_BACK_SETTINGS: case ACTION_BACK_SETTINGS:
gWifiViewMode = WIFI_VIEW_OVERVIEW;
showScreen(SCREEN_SETTINGS_MENU); showScreen(SCREEN_SETTINGS_MENU);
break; 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: case ACTION_NONE:
default: default:
break; break;
@ -239,6 +575,33 @@ static lv_obj_t *makeButton(const char *text,
return btn; 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<char *>(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() { static void makeTopIcons() {
lv_obj_t *icons = lv_label_create(gRoot); lv_obj_t *icons = lv_label_create(gRoot);
lv_label_set_text(icons, "W BAT"); 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); lv_obj_align_to(subserver, login, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 6);
makeTopIcons(); makeTopIcons();
makeTitle("STATUS", 165, &lv_font_montserrat_28); makeTitle("STATUS", 150, &lv_font_montserrat_28);
makeBody("Swipe left or tap the button below.", 214, 360); 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); makeButton("SETTINGS", 22, 360, 436, 78, 0x2A6F97, ACTION_OPEN_SETTINGS, &lv_font_montserrat_24);
makeVersionTag(); makeVersionTag();
} }
@ -295,7 +660,7 @@ static void drawSettingsMenu() {
if (itemIndex == 2) action = ACTION_OPEN_ACCOUNT; if (itemIndex == 2) action = ACTION_OPEN_ACCOUNT;
makeButton(kMenuItems[itemIndex], 22, 132 + visibleIndex * 126, 436, 104, 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); lv_obj_t *hint = lv_label_create(gRoot);
@ -312,25 +677,189 @@ static void drawSettingsMenu() {
makeVersionTag(); 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<void *>(static_cast<uintptr_t>(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(); 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); makeBody("Swipe right to return to Settings.", 204, 400);
makeButton("BACK", 140, 344, 200, 72, 0x5A6570, ACTION_BACK_SETTINGS, &lv_font_montserrat_22); makeButton("BACK", 140, 344, 200, 72, 0x5A6570, ACTION_BACK_SETTINGS, &lv_font_montserrat_22);
makeVersionTag(); makeVersionTag();
} }
static void drawWifiScreen() { static void drawKeyRow(const char *const *tokens,
drawSimpleScreen("Wi-Fi", "Wi-Fi screen"); 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() { static void drawTextEditorKeyboard(lv_coord_t originY) {
drawSimpleScreen("Server", "Server screen"); 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);
}
} }
static void drawAccountScreen() { makeKeyboardButton(gKeyboardMode == KEYBOARD_MODE_ALPHA ? "123" : "ABC", "<MODE>", 20, row3Y, 88, 54, 0x5C6F82, &lv_font_montserrat_18);
drawSimpleScreen("Account", "Account screen"); makeKeyboardButton(gKeyboardShift ? "SHIFT*" : "SHIFT", "<SHIFT>", 116, row3Y, 88, 54, 0x7A5C9B, &lv_font_montserrat_16);
makeKeyboardButton("DEL", "<DEL>", 212, row3Y, 76, 54, 0x7D3A3A, &lv_font_montserrat_18);
makeKeyboardButton("SAVE", "<SAVE>", 296, row3Y, 76, 54, 0x2A9D8F, &lv_font_montserrat_18);
makeKeyboardButton("CANCEL", "<CANCEL>", 380, row3Y, 80, 54, 0x5A6570, &lv_font_montserrat_16);
}
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() { static void rebuildScreen() {
@ -356,6 +885,9 @@ static void rebuildScreen() {
case SCREEN_ACCOUNT: case SCREEN_ACCOUNT:
drawAccountScreen(); drawAccountScreen();
break; break;
case SCREEN_TEXT_EDIT:
drawTextEditScreen();
break;
} }
} }
@ -396,6 +928,7 @@ static void handleSettingsSwipe(SwipeDirection swipe) {
static void handleWifiSwipe(SwipeDirection swipe) { static void handleWifiSwipe(SwipeDirection swipe) {
if (swipe == SWIPE_RIGHT) { if (swipe == SWIPE_RIGHT) {
gWifiViewMode = WIFI_VIEW_OVERVIEW;
showScreen(SCREEN_SETTINGS_MENU); 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) { static void handleSwipe(SwipeDirection swipe) {
if (swipe == SWIPE_NONE) { if (swipe == SWIPE_NONE) {
return; return;
@ -433,6 +986,9 @@ static void handleSwipe(SwipeDirection swipe) {
case SCREEN_ACCOUNT: case SCREEN_ACCOUNT:
handleAccountSwipe(swipe); handleAccountSwipe(swipe);
break; break;
case SCREEN_TEXT_EDIT:
handleTextEditSwipe(swipe);
break;
} }
} }
@ -451,6 +1007,10 @@ void setup() {
gTouch.setSwapXY(true); gTouch.setSwapXY(true);
gTouch.setMirrorXY(true, false); gTouch.setMirrorXY(true, false);
gPrefs.begin("shine-nav-ui", false);
loadPrefs();
beginSavedWifi();
lv_init(); lv_init();
uint32_t screenWidth = gfx->width(); uint32_t screenWidth = gfx->width();

View File

@ -1,2 +1,2 @@
client.version=1.2.142 client.version=1.2.143
server.version=1.2.134 server.version=1.2.135