From c58782aeeb689ab6467c15d5a56eb9daf235bf1d Mon Sep 17 00:00:00 2001 From: Roman Terekhov Date: Mon, 12 Oct 2026 09:44:06 +0300 Subject: [PATCH] =?UTF-8?q?ui-new:=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=86=D0=B0=20POWER=20=E2=80=94=20=D0=B1=D0=B0=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D0=B5=D1=8F=20(Volt/Perc/Off),=20=D0=BA=D0=BE=D1=80?= =?UTF-8?q?=D1=80=D0=B5=D0=BA=D1=86=D0=B8=D1=8F=20=D0=90=D0=A6=D0=9F,=20re?= =?UTF-8?q?boot/shutdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 ++ examples/companion_radio/NodePrefs.h | 9 + examples/companion_radio/ui-new/UITask.cpp | 246 ++++++++++++++++++--- examples/companion_radio/ui-new/UITask.h | 4 + 4 files changed, 241 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index a74a96ad..6c3b6c55 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,19 @@ git merge upstream/main # правки для upstream отправляю тихие часы — в `NodePrefs::isQuietNow()`, «тихая» доставка — через `AbstractUITask::newMsgSilent()`. В тихие часы глушатся звук, вибрация и всплытия для каналов, личных сообщений и уведомлений о новых контактах, но сообщения не теряются. +- **Страница POWER** (последняя в списке страниц; длинное нажатие — вход в меню) — питание, + батарея и перезагрузка. Раньше этот пункт назывался `Hibernation` и сразу выключал устройство. + - `Battery` — что показывать рядом с иконкой батареи: `Volt` (напряжение, `4.05V`), + `Perc` (процент) или `Off` (индикатор скрыт). Длинное нажатие переключает значение; + применяется и сохраняется сразу. Иконка батареи стала компактнее (18×9). + - `ADC corr` — коррекция АЦП батареи. Видно текущее значение (или `auto`, если коррекция + не задана); длинное нажатие открывает правку, где короткое нажатие даёт `+0.01`, + двойное `-0.01`, а длинное возвращает в меню. Значение применяется к плате и сохраняется + сразу, а при следующем включении подхватывается из prefs. + - `Reboot/Shut` — подменю из трёх пунктов: `Reboot`, `Shutdown`, `Back`; длинное нажатие + выполняет выбранное. + - `Back` — выход из меню. Сохранять ничего не нужно: всё сохраняется в момент изменения, + через 5 минут простоя меню закрывается само. ### Сборка и CI diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index c64f71c7..480a1865 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -14,6 +14,11 @@ #define CH_MODE_SILENT 1 // в непрочитанные, но без звука и без всплытия #define CH_MODE_OFF 2 // на устройстве не уведомляем вообще +// Что показывать рядом с иконкой батареи в статус-баре +#define BATT_DISPLAY_VOLT 0 // напряжение, например 4.05V +#define BATT_DISPLAY_PERC 1 // процент заряда +#define BATT_DISPLAY_OFF 2 // индикатор батареи скрыт + // Сколько каналов покрываем настройками. Совпадает с MAX_GROUP_CHANNELS у companion-сборок, // но этот заголовок его не включает, поэтому задаём свой предел. #ifndef NOTIFY_MAX_CHANNELS @@ -62,6 +67,8 @@ public: uint8_t quiet_to = 7; // час окончания (локальный) int8_t tz_offset = 0; // смещение от UTC в часах, -12..+14 uint8_t ch_mode[CH_MODE_BYTES]; // режимы по каналам (CH_MODE_*), по 2 бита + uint8_t batt_display = BATT_DISPLAY_PERC; // что показывать рядом с батареей (BATT_DISPLAY_*) + float adc_multiplier = 0; // коррекция АЦП батареи (0 = как считает плата) private: class RadioPrefs : public ConfigSerializer { // COPIED from CommonCLI (for now) @@ -83,6 +90,7 @@ private: #endif def("tx", _parent->tx_power_dbm); def("af", _parent->airtime_factor); + def("adc_mult", _parent->adc_multiplier); // коррекция АЦП батареи (как в CommonCLI) def("rxdelay", _parent->rx_delay_base); //def("f_txdelay", _parent->tx_delay_factor); currently hard-coded //def("d_txdelay", _parent->direct_tx_delay_factor); currently hard-coded @@ -143,6 +151,7 @@ private: def("quiet_to", _parent->quiet_to); def("tz", _parent->tz_offset); def("ch_mode", (void *)_parent->ch_mode, sizeof(_parent->ch_mode)); // hex-блоб + def("batt_disp", _parent->batt_display); // что показывать у батареи def("tel_base", _parent->telemetry_mode_base); def("tel_loc", _parent->telemetry_mode_loc); def("tel_env", _parent->telemetry_mode_env); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 6a224962..47571433 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -148,7 +148,7 @@ class HomeScreen : public UIScreen { SENSORS, #endif THEME, // выбор темы дисплея (только если драйвер поддерживает темы) - SHUTDOWN, + POWER, // battery display, ADC correction, reboot/shutdown Count // keep as last }; @@ -158,6 +158,15 @@ class HomeScreen : public UIScreen { NodePrefs* _node_prefs; uint8_t _page; bool _shutdown_init; + bool _shutdown_restart; // что делать после отпускания кнопки: true — reboot, false — power off + + // меню Power (страница POWER) + enum PowerScreen { PWR_OFF, PWR_MAIN, PWR_ADC, PWR_RS }; + enum PowerRow { PWR_BATT, PWR_ADC_ROW, PWR_RST_ROW, PWR_BACK, PWR_ROW_COUNT }; + uint8_t _power_screen; + uint8_t _power_sel; + uint8_t _rs_sel; + unsigned long _power_last_activity; AdvertPath recent[UI_RECENT_LIST_SIZE]; // режим ручной настройки радио (страница RADIO) @@ -191,6 +200,16 @@ class HomeScreen : public UIScreen { const char* radioRowName(uint8_t row); void renderRadioEditor(DisplayDriver& display); + // меню Power: отображение батареи, коррекция АЦП, reboot/shutdown + void powerEnter(); + void powerLeave(); + void powerStep(int8_t dir); + void powerAction(); + void powerAdcStep(int8_t dir); + void renderPowerPage(DisplayDriver& display); + void renderPowerScreen(DisplayDriver& display); + void renderPowerAdc(DisplayDriver& display); + // выбор темы дисплея (страница THEME) bool _theme_edit; uint8_t _theme_sel; @@ -288,27 +307,44 @@ class HomeScreen : public UIScreen { if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100% // battery icon - int iconWidth = 24; - int iconHeight = 10; - int iconX = display.width() - iconWidth - 5; // Position the icon near the top-right corner - int iconY = 0; - display.setColor(UIColor::title_txt); + // что показывать рядом с батареей — напряжение, процент или ничего (страница POWER) + const uint8_t mode = _node_prefs->batt_display; + int textWidth = 0; - // battery outline - display.drawRect(iconX, iconY, iconWidth, iconHeight); + if (mode != BATT_DISPLAY_OFF) { + // компактная иконка в правом верхнем углу + const int iconWidth = 18; + const int iconHeight = 9; + const int iconX = display.width() - iconWidth - 3; + const int iconY = 1; + display.setColor(UIColor::title_txt); - // battery "cap" - display.fillRect(iconX + iconWidth, iconY + (iconHeight / 4), 3, iconHeight / 2); + // battery outline + display.drawRect(iconX, iconY, iconWidth, iconHeight); - // fill the battery based on the percentage - int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100; - display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4); + // battery "cap" + display.fillRect(iconX + iconWidth, iconY + 2, 2, iconHeight - 4); - // show muted icon if buzzer is muted + // fill the battery based on the percentage + int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100; + display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4); + + char txt[10]; + if (mode == BATT_DISPLAY_VOLT) { + snprintf(txt, sizeof(txt), "%.2fV", batteryMilliVolts / 1000.0f); + } else { + snprintf(txt, sizeof(txt), "%d%%", batteryPercentage); + } + textWidth = (int)display.getTextWidth(txt); + display.drawTextRightAlign(iconX - 3, iconY, txt); + } + + // show muted icon if buzzer is muted (левее значения батареи) #ifdef PIN_BUZZER if (_task->isBuzzerQuiet()) { + const int iconX = display.width() - 18 - 3; display.setColor(UIColor::warning_txt); - display.drawXbm(iconX - 9, iconY + 1, muted_icon, 8, 8); + display.drawXbm(iconX - 3 - textWidth - 10, 1, muted_icon, 8, 8); } #endif } @@ -343,7 +379,9 @@ class HomeScreen : public UIScreen { public: HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, NodePrefs* node_prefs) : _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0), - _shutdown_init(false), _radio_edit(false), _radio_val_edit(false), _radio_sel(0), _radio_fq_pos(0), + _shutdown_init(false), _shutdown_restart(false), + _power_screen(PWR_OFF), _power_sel(0), _rs_sel(0), _power_last_activity(0), + _radio_edit(false), _radio_val_edit(false), _radio_sel(0), _radio_fq_pos(0), _radio_last_activity(0), _theme_edit(false), _theme_sel(0), _theme_last_activity(0), _notify_screen(NOTIFY_OFF), _notify_sel(0), _notify_val_edit(false), _notify_last_activity(0), sensors_lpp(200) { } @@ -376,8 +414,16 @@ public: _task->keepDisplayOn(); } } - if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released - _task->shutdown(); + if (_power_screen != PWR_OFF) { + if (millis() - _power_last_activity >= UI_RADIO_MENU_TIMEOUT_MILLIS) { + powerLeave(); // 5 минут простоя — закрыть меню + _task->blankDisplay(); + } else { + _task->keepDisplayOn(); + } + } + if (_shutdown_init && !_task->isButtonPressed()) { // дождаться отпускания кнопки + _task->shutdown(_shutdown_restart); } } @@ -396,6 +442,10 @@ public: renderNotifyMenu(display); return 5000; } + if (_power_screen != PWR_OFF) { + renderPowerScreen(display); + return 5000; + } display.setColor(UIColor::title_bkg); display.fillRect(0, 0, display.width(), 12); char tmp[80]; @@ -631,17 +681,8 @@ public: #endif } else if (_page == HomePage::THEME) { renderThemePage(display); - } else if (_page == HomePage::SHUTDOWN) { - display.setColor(UIColor::corp_blue); - display.setTextSize(1); - if (_shutdown_init) { - display.setColor(UIColor::warning_txt); - display.drawTextCentered(display.width() / 2, 34, "hibernating..."); - } else { - display.setColor(UIColor::secondary_txt); - display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32); - display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL); - } + } else if (_page == HomePage::POWER) { + renderPowerPage(display); } return 5000; // next render after 5000 ms } @@ -717,6 +758,22 @@ public: } return true; } + if (_power_screen != PWR_OFF) { // меню Power: страницы не листаются + _power_last_activity = millis(); + if (c == KEY_NEXT || c == KEY_RIGHT) { + powerStep(+1); + return true; + } + if (c == KEY_PREV || c == KEY_LEFT) { + powerStep(-1); + return true; + } + if (c == KEY_ENTER) { + powerAction(); + return true; + } + return true; + } if (c == KEY_LEFT || c == KEY_PREV) { _page = pageStep(-1); return true; @@ -772,8 +829,8 @@ public: return true; } #endif - if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) { - _shutdown_init = true; // need to wait for button to be released + if (c == KEY_ENTER && _page == HomePage::POWER) { + powerEnter(); // внутри меню Power: батарея, коррекция АЦП, reboot/shutdown return true; } return false; @@ -860,6 +917,128 @@ const char* HomeScreen::radioRowName(uint8_t row) { } } +// ---------------------------------------------------------------- меню Power + +static const char* battDisplayName(uint8_t mode) { + if (mode == BATT_DISPLAY_VOLT) return "Volt"; + if (mode == BATT_DISPLAY_PERC) return "Perc"; + return "Off"; +} + +void HomeScreen::powerEnter() { + _task->clearAlert(); + _power_screen = PWR_MAIN; + _power_sel = PWR_BATT; + _rs_sel = 0; + _power_last_activity = millis(); +} + +void HomeScreen::powerLeave() { + _power_screen = PWR_OFF; + _task->showPendingMessages(); // сообщения, пришедшие пока было открыто меню +} + +void HomeScreen::powerStep(int8_t dir) { + if (_power_screen == PWR_ADC) { powerAdcStep(dir); return; } // ±0.01 + if (_power_screen == PWR_RS) { + _rs_sel = (uint8_t)((_rs_sel + 3 + dir) % 3); + return; + } + _power_sel = (uint8_t)((_power_sel + PWR_ROW_COUNT + dir) % PWR_ROW_COUNT); +} + +// длинное нажатие: у Battery переключает значение, у остальных — вход/выход +void HomeScreen::powerAction() { + if (_power_screen == PWR_ADC) { // из правки АЦП — назад в основное меню + _power_screen = PWR_MAIN; + return; + } + if (_power_screen == PWR_RS) { + if (_rs_sel == 0) { _shutdown_restart = true; _shutdown_init = true; } // reboot + else if (_rs_sel == 1) { _shutdown_restart = false; _shutdown_init = true; } // shutdown + else { _power_screen = PWR_MAIN; } // back + return; + } + switch (_power_sel) { + case PWR_BATT: + _node_prefs->batt_display = (uint8_t)((_node_prefs->batt_display + 1) % 3); + the_mesh.savePrefs(); // применяется сразу и сохраняется + _task->showAlert(battDisplayName(_node_prefs->batt_display), 800); + break; + case PWR_ADC_ROW: _power_screen = PWR_ADC; break; + case PWR_RST_ROW: _power_screen = PWR_RS; _rs_sel = 0; break; + default: powerLeave(); break; // Back + } +} + +void HomeScreen::powerAdcStep(int8_t dir) { + float v = _node_prefs->adc_multiplier + (float)dir * 0.01f; + if (v < 0.0f) v = 0.0f; + if (v > 10.0f) v = 10.0f; + v = roundf(v * 100.0f) / 100.0f; // без накопления погрешности float + _node_prefs->adc_multiplier = v; + if (!_task->setAdcMultiplier(v)) { + _task->showAlert("ADC not supported", 1200); + } + the_mesh.savePrefs(); +} + +void HomeScreen::renderPowerPage(DisplayDriver& display) { + char tmp[32]; + display.setTextSize(1); + display.setColor(UIColor::secondary_txt); + snprintf(tmp, sizeof(tmp), "Battery: %s", battDisplayName(_node_prefs->batt_display)); + display.drawTextCentered(display.width() / 2, 20, tmp); + if (_node_prefs->adc_multiplier > 0.0f) { + snprintf(tmp, sizeof(tmp), "ADC corr: %.2f", _node_prefs->adc_multiplier); + } else { + strcpy(tmp, "ADC corr: auto"); + } + display.drawTextCentered(display.width() / 2, 32, tmp); + display.setColor(UIColor::primary_txt); + display.drawTextCentered(display.width() / 2, 64 - 11, "power: " PRESS_LABEL); +} + +void HomeScreen::renderPowerScreen(DisplayDriver& display) { + if (_power_screen == PWR_ADC) { + renderPowerAdc(display); + return; + } + display.setTextSize(1); + if (_power_screen == PWR_RS) { + drawMenuRow(display, 2, _rs_sel == 0, "Reboot", ""); + drawMenuRow(display, 12, _rs_sel == 1, "Shutdown", ""); + drawMenuRow(display, 22, _rs_sel == 2, "Back", ""); + display.setColor(UIColor::secondary_txt); + display.drawTextCentered(display.width() / 2, 64 - 11, "hold=run clk=next"); + return; + } + char val[16]; + if (_node_prefs->adc_multiplier > 0.0f) snprintf(val, sizeof(val), "%.2f", _node_prefs->adc_multiplier); + else strcpy(val, "auto"); + drawMenuRow(display, 2, _power_sel == PWR_BATT, "Battery", battDisplayName(_node_prefs->batt_display)); + drawMenuRow(display, 12, _power_sel == PWR_ADC_ROW, "ADC corr", val); + drawMenuRow(display, 22, _power_sel == PWR_RST_ROW, "Reboot/Shut", ">"); + drawMenuRow(display, 32, _power_sel == PWR_BACK, "Back", ""); + display.setColor(UIColor::secondary_txt); + display.drawTextCentered(display.width() / 2, 64 - 11, "hold=change clk=next"); +} + +void HomeScreen::renderPowerAdc(DisplayDriver& display) { + char buf[16]; + display.setColor(UIColor::secondary_txt); + display.setTextSize(1); + display.drawTextLeftAlign(0, 2, "ADC +0.01/-0.01"); + if (_node_prefs->adc_multiplier > 0.0f) snprintf(buf, sizeof(buf), "%.2f", _node_prefs->adc_multiplier); + else strcpy(buf, "auto"); + display.setColor(UIColor::warning_txt); + display.setTextSize(2); + display.drawTextCentered(display.width() / 2, 20, buf); + display.setColor(UIColor::secondary_txt); + display.setTextSize(1); + display.drawTextCentered(display.width() / 2, 64 - 11, "hold=back"); +} + void HomeScreen::renderRadioEditor(DisplayDriver& display) { char buf[24]; char head[32]; @@ -1273,6 +1452,11 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no _node_prefs = node_prefs; + // применить сохранённую коррекцию АЦП батареи (0 = как считает плата) + if (_node_prefs->adc_multiplier > 0.0f) { + _board->setAdcMultiplier(_node_prefs->adc_multiplier); + } + if (_display != NULL) { // применить сохранённую тему дисплея (если драйвер их поддерживает) if (_display->getThemeCount() > 0) { diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 56e28dab..1a658a10 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -88,6 +88,10 @@ public: bool displayHasThemes() const { return _display != NULL && _display->getThemeCount() > 0; } bool isButtonPressed() const; + // Коррекция АЦП батареи: реализация в board конкретного варианта, + // если плата так не умеет — вернёт false + bool setAdcMultiplier(float multiplier) { return _board->setAdcMultiplier(multiplier); } + bool isBuzzerQuiet() { #ifdef PIN_BUZZER return buzzer.isQuiet();