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 00000000..bbb668f1 Binary files /dev/null and b/posadmesh-docs/_assets/wizard/15-popup.png differ diff --git a/posadmesh-docs/device-setup.md b/posadmesh-docs/device-setup.md index 2e8a685d..71d731e3 100644 --- a/posadmesh-docs/device-setup.md +++ b/posadmesh-docs/device-setup.md @@ -128,6 +128,14 @@ `0.00` означает «использовать заводское значение платы»: команда `set adc.multiplier 0` возвращает расчёт по умолчанию. Значение применяется командой `set adc.multiplier ` — той же, что и в последовательном CLI. +## Всплывающие сообщения + +Ответ устройства на любую команду показывается отдельным окном поверх экрана: рамка, свой фон, текст с переносом строк и кнопка `OK`. Окно модальное — пока оно открыто, экран под ним не меняется, а **короткое нажатие кнопки закрывает его**. Так видно и успешные ответы (например `OK - reboot to apply` после правки радио), и ошибки проверки значений. + +![Всплывающее окно с ответом устройства](_assets/wizard/15-popup.png) + +Признак успеха: после правки частоты, полосы, `CR` или `SF` окно показывает `OK - reboot to apply` — изменения радио вступают в силу после перезагрузки узла. Если в ответе ошибка (например значение вне диапазона), окно покажет её текст, а настройка не сохранится. + ## Ограничения - На экранах без кириллицы подписи меню выводятся латиницей.