From 82ec2f4c8309e735cbbbec481252b18fcade6461 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 16 Oct 2026 11:16:16 +0300 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFfix(ui):=20=D0=B2=D1=81=D0=BF=D0=BB?= =?UTF-8?q?=D1=8B=D0=B2=D0=B0=D1=8E=D1=89=D0=B5=D0=B5=20=D0=BE=D0=BA=D0=BD?= =?UTF-8?q?=D0=BE=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=20=D1=83=D1=81=D1=82=D1=80=D0=BE=D0=B9=D1=81=D1=82?= =?UTF-8?q?=D0=B2=D0=B0=20=D0=B2=20=D0=BC=D0=B0=D1=81=D1=82=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ответ 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 (без дисплея) --- README.md | 1 + examples/simple_repeater/UITask.cpp | 75 ++++++++++++++------- examples/simple_repeater/UITask.h | 8 +-- posadmesh-docs/_assets/wizard/15-popup.png | Bin 0 -> 5120 bytes posadmesh-docs/device-setup.md | 8 +++ 5 files changed, 64 insertions(+), 28 deletions(-) create mode 100644 posadmesh-docs/_assets/wizard/15-popup.png diff --git a/README.md b/README.md index a2ead30b..cac9343c 100644 --- a/README.md +++ b/README.md @@ -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` и терял бит собственного брокера при переключении любого другого; diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index 41dc566b..5d37cad1 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -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() { diff --git a/examples/simple_repeater/UITask.h b/examples/simple_repeater/UITask.h index eb93ed32..294074cb 100644 --- a/examples/simple_repeater/UITask.h +++ b/examples/simple_repeater/UITask.h @@ -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: diff --git a/posadmesh-docs/_assets/wizard/15-popup.png b/posadmesh-docs/_assets/wizard/15-popup.png new file mode 100644 index 0000000000000000000000000000000000000000..bbb668f12289c55f1ab78c9e66d4174c06099b0d GIT binary patch literal 5120 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 kHh}x;%74eqZ(k` — той же, что и в последовательном CLI. +## Всплывающие сообщения + +Ответ устройства на любую команду показывается отдельным окном поверх экрана: рамка, свой фон, текст с переносом строк и кнопка `OK`. Окно модальное — пока оно открыто, экран под ним не меняется, а **короткое нажатие кнопки закрывает его**. Так видно и успешные ответы (например `OK - reboot to apply` после правки радио), и ошибки проверки значений. + +![Всплывающее окно с ответом устройства](_assets/wizard/15-popup.png) + +Признак успеха: после правки частоты, полосы, `CR` или `SF` окно показывает `OK - reboot to apply` — изменения радио вступают в силу после перезагрузки узла. Если в ответе ошибка (например значение вне диапазона), окно покажет её текст, а настройка не сохранится. + ## Ограничения - На экранах без кириллицы подписи меню выводятся латиницей.