fix(ui): всплывающее окно для ответов устройства в мастере настройки

- ответ CLI показывался строкой поверх экрана и сливался с содержимым, особенно
  в верхней части (например OK - reboot to apply на экране частоты)
- теперь это модальное окно: рамка, свой фон, перенос текста и кнопка OK;
  закрывается коротким нажатием, пока открыто — экран под ним не меняется
- окно показывается сразу (сброс таймера отрисовки), при закрытии экран
  перерисовывается немедленно
- setToast/renderToast заменены на showPopup/renderPopup
- документация: posadmesh-docs/device-setup.md (раздел «Всплывающие сообщения»
  и макет экрана), README — журнал изменений

Проверено сборкой: heltec_v4_repeater_mqtt, Meshadventurer_sx1262_repeater_mqtt,
Generic_E22_sx1262_repeater_mqtt (без дисплея)
This commit is contained in:
shade committed 2026-10-16 11:16:16 +03:00
1 parent 8f27aa9202
commit 82ec2f4c83
5 files changed
+64 -28

No files matched your search

+51 -24
View File
@@ -15,7 +15,6 @@
#define WIZARD_AUTO_OFF_MILLIS 60000 // в мастере настройки экран живёт дольше
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
#define POWEROFF_DELAY 3000
#define TOAST_MILLIS 3000
#define CHAR_W 6 // ширина символа при setTextSize(1)
// позиции курсора на экране частоты: МГц, три разряда кГц и пункт сохранения
@@ -140,7 +139,7 @@ int UITask::rowHeight() const {
bool UITask::runCommand(const char* command) {
_toast[0] = 0;
if (_cli == nullptr) {
setToast("no CLI");
showPopup("no CLI");
return false;
}
char buf[96];
@@ -150,18 +149,22 @@ bool UITask::runCommand(const char* command) {
reply[0] = 0;
_cli->handleCommand(0, buf, reply);
if (reply[0] == 0) {
setToast("OK");
showPopup("OK");
return true;
}
bool ok = !(reply[0] == 'E' || reply[0] == 'e'); // "Error..." / "OK..."
setToast(reply);
showPopup(reply);
return ok;
}
void UITask::setToast(const char* msg) {
// Всплывающее окно с ответом устройства: показывается поверх экрана мастера
// и закрывается коротким нажатием кнопки (см. handleButton).
void UITask::showPopup(const char* msg) {
strncpy(_toast, msg, sizeof(_toast) - 1);
_toast[sizeof(_toast) - 1] = 0;
_toast_until = millis() + TOAST_MILLIS;
_popup_active = true;
_next_refresh = 0; // показать окно сразу, не ждать тика отрисовки
_auto_off = millis() + WIZARD_AUTO_OFF_MILLIS;
}
void UITask::openMenu() {
@@ -432,15 +435,24 @@ void UITask::coordSave() {
bool okLon = runCommand(cmd);
if (okLat && okLon) {
setToast("coords saved");
showPopup("coords saved");
} else {
setToast(latMsg[0] ? latMsg : _toast);
showPopup(latMsg[0] ? latMsg : _toast);
}
}
// --- кнопка -----------------------------------------------------------------
void UITask::handleButton(int ev) {
// Пока открыто всплывающее окно, оно модальное: любое нажатие только закрывает его
// (короткое — как просили), экран под ним не меняется.
if (_popup_active) {
_popup_active = false;
_next_refresh = 0; // сразу перерисовать экран под окном
_auto_off = millis() + WIZARD_AUTO_OFF_MILLIS;
return;
}
switch (_screen) {
case SCREEN_STATUS:
if (ev == BUTTON_EVENT_CLICK) {
@@ -538,7 +550,7 @@ void UITask::handleButton(int ev) {
if (ev == BUTTON_EVENT_CLICK) {
char msg[32];
snprintf(msg, sizeof(msg), "raw %u mV", (unsigned)(_board ? _board->getBattMilliVolts() : 0));
setToast(msg);
showPopup(msg);
} else if (ev == BUTTON_EVENT_LONG_PRESS || ev == BUTTON_EVENT_DOUBLE_CLICK) {
_screen = SCREEN_MENU;
}
@@ -643,6 +655,7 @@ void UITask::loop() {
if (millis() >= _next_refresh) {
_display->startFrame();
renderCurrScreen();
if (_popup_active) renderPopup(); // окно рисуется поверх экрана
_display->endFrame();
_next_refresh = millis() + 1000; // refresh every second
@@ -685,15 +698,37 @@ void UITask::renderHint(const char* hint) {
_display->print(hint);
}
void UITask::renderToast() {
if (_toast[0] == 0 || millis() > _toast_until) return;
int w = _display->width();
int h = _display->height();
// Всплывающее окно: рамка, свой фон (чтобы текст не сливался со строками экрана)
// и кнопка OK внизу. Рисуется поверх текущего экрана.
void UITask::renderPopup() {
const int w = _display->width();
const int h = _display->height();
int box_h = 46;
if (box_h > h - 8) box_h = h - 8;
int top = (h - box_h) / 2;
if (top < 12) top = 12;
// рамка: белая заливка и чёрная внутренняя область
_display->setColor(UIColor::primary_txt);
_display->fillRect(4, top, w - 8, box_h);
_display->setColor(UIColor::window_bkg);
_display->fillRect(6, top + 2, w - 12, box_h - 4);
// текст сообщения
_display->setColor(UIColor::primary_txt);
_display->drawRect(0, 12, w, h - 12);
_display->setTextSize(1);
_display->setCursor(2, 15);
_display->printWordWrap(_toast, w - 4);
_display->setCursor(8, top + 5);
_display->printWordWrap(_toast, w - 20);
// кнопка OK
const char* ok = "OK";
int ok_w = (int)strlen(ok) * CHAR_W + 10;
int ok_x = (w - ok_w) / 2;
int ok_y = top + box_h - 14;
_display->setColor(UIColor::primary_txt);
_display->drawRect(ok_x, ok_y, ok_w, 12);
_display->setCursor(ok_x + 5, ok_y + 2);
_display->print(ok);
}
void UITask::renderMenu() {
@@ -724,7 +759,6 @@ void UITask::renderMenu() {
}
_display->setColor(UIColor::primary_txt);
renderHint("click=next hold=sel");
renderToast();
}
void UITask::renderValue() {
@@ -768,7 +802,6 @@ void UITask::renderValue() {
_display->setCursor(2, 20);
_display->print(value);
renderHint("clk+ dbl- hold=OK");
renderToast();
}
// поразрядный ввод значения с точкой: полоса ("062.50") или множитель ADC ("0.00")
@@ -821,7 +854,6 @@ void UITask::renderDigits() {
} else {
renderHint("hold=edit clk=next");
}
renderToast();
}
// заряд аккумулятора: напряжение, процент, полоска и текущий множитель ADC
@@ -865,7 +897,6 @@ void UITask::renderBattery() {
_display->print(line);
renderHint("hold=menu clk=raw");
renderToast();
}
// частота: целые МГц выделяются целиком (шаг 1), кГц — по разрядам, затем Save & exit
@@ -924,7 +955,6 @@ void UITask::renderFreq() {
} else {
renderHint("hold=edit clk=next");
}
renderToast();
}
void UITask::renderCoords() {
@@ -964,7 +994,6 @@ void UITask::renderCoords() {
} else {
renderHint("hold=edit clk=next");
}
renderToast();
}
void UITask::renderCoordsSave() {
@@ -974,7 +1003,6 @@ void UITask::renderCoordsSave() {
_display->setCursor(2, 20);
_display->print("Save & exit?");
renderHint("hold=save dbl=cancel");
renderToast();
}
void UITask::renderStatus() {
@@ -1011,7 +1039,6 @@ void UITask::renderStatus() {
if (_display->height() >= 56) {
renderHint("dbl-click: setup");
}
renderToast();
}
void UITask::renderCurrScreen() {