ui-new: страница POWER — батарея (Volt/Perc/Off), коррекция АЦП, reboot/shutdown

This commit is contained in:
shade committed 2026-10-12 09:44:06 +03:00
1 parent e59a5f1759
commit c58782aeeb
4 files changed
+241 -31

No files matched your search

+13
View File
@@ -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
+9
View File
@@ -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);
+215 -31
View File
@@ -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) {
+4
View File
@@ -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();