ui-new: страница POWER — батарея (Volt/Perc/Off), коррекция АЦП, reboot/shutdown
This commit is contained in:
1 parent
e59a5f1759
commit
c58782aeeb
4 files changed
+241
-31
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user