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

+1
View File
@@ -397,6 +397,7 @@ MQTT-репитер использует:
**Исправлено**
- ответы устройства в мастере настройки показывались строкой поверх экрана и сливались с содержимым (например `OK - reboot to apply` в верхней части). Теперь это модальное всплывающее окно с рамкой, собственным фоном, переносом текста и кнопкой `OK`; закрывается коротким нажатием, пока открыто — экран под ним не меняется;
- мастер настройки: при выходе из меню отправлялась команда `set radio` с нулевыми значениями рабочей копии, если в этой сессии не открывали ни один радиопункт (например сразу `Save & exit`) — устройство отвечало `Error, invalid radio params`. Теперь радиопараметры отправляются только когда их действительно правили, значения перед отправкой перечитываются из настроек узла, а при каждом входе в мастер рабочая копия обновляется;
- `set mqtt.meshcoretel on|off` не срабатывал: обработчик сравнивался со строкой `set mqtt.mqtt.meshcoretel `. Алиас `set mqtt.meshcoretel.ru on|off` читает значение по правильному смещению;
- `setEndpointEnabled` маскировал сохранённую маску брокеров значением `0x07` и терял бит собственного брокера при переключении любого другого;
+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() {
+4 -4
View File
@@ -43,8 +43,8 @@ private:
bool _coord_editing = false; // true: цифра правится, false: применена
bool _coord_applied[12];
char _coord_digits[12];
char _toast[40];
unsigned long _toast_until = 0;
char _toast[64];
bool _popup_active = false; // всплывающее окно с ответом устройства открыто
// радиопараметры: рабочая копия, пока мастер открыт
bool _radio_loaded = false;
@@ -85,7 +85,7 @@ private:
void coordFromPrefs();
void coordSave();
double coordAxisValue(uint8_t axis) const; // axis 0 = lat, 1 = lon
void setToast(const char* msg);
void showPopup(const char* msg);
bool runCommand(const char* command);
void handleButton(int ev);
@@ -101,7 +101,7 @@ private:
void renderCoordsSave();
void renderTitleBar(const char* title);
void renderHint(const char* hint);
void renderToast();
void renderPopup();
int rowHeight() const;
public:
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

+8
View File
@@ -128,6 +128,14 @@
`0.00` означает «использовать заводское значение платы»: команда `set adc.multiplier 0` возвращает расчёт по умолчанию. Значение применяется командой `set adc.multiplier <x.xx>` — той же, что и в последовательном CLI.
## Всплывающие сообщения
Ответ устройства на любую команду показывается отдельным окном поверх экрана: рамка, свой фон, текст с переносом строк и кнопка `OK`. Окно модальное — пока оно открыто, экран под ним не меняется, а **короткое нажатие кнопки закрывает его**. Так видно и успешные ответы (например `OK - reboot to apply` после правки радио), и ошибки проверки значений.
![Всплывающее окно с ответом устройства](_assets/wizard/15-popup.png)
Признак успеха: после правки частоты, полосы, `CR` или `SF` окно показывает `OK - reboot to apply` — изменения радио вступают в силу после перезагрузки узла. Если в ответе ошибка (например значение вне диапазона), окно покажет её текст, а настройка не сохранится.
## Ограничения
- На экранах без кириллицы подписи меню выводятся латиницей.