Files
MeshCore-Posadmesh/examples/simple_repeater/UITask.cpp
T
shade 04bb5714c8 feat: Save & exit in radio editors, wiki docs with screen images
- экраны Frequency и Bandwidth: единица измерения в конце строки значения
  (868.731 MHz, 062.50 kHz), внизу отдельный пункт Save & exit
- после правки цифр пользователь переходит на Save & exit и удерживает кнопку:
  значения применяются, экран возвращается в предыдущее меню
- документация: страница posadmesh-docs/device-setup.md дополнена схемой управления
  и изображениями всех экранов (12 картинок в posadmesh-docs/_assets/wizard/)
- вики Gitea: страницы Home и Device-Setup с теми же иллюстрациями
- README: ссылка на вики, схема управления и запись в журнале изменений
2026-10-11 18:44:30 +03:00

927 lines
28 KiB
C++

#include "UITask.h"
#include "target.h"
#include <Arduino.h>
#if defined(ESP32)
#include <WiFi.h>
#endif
#include <helpers/CommonCLI.h>
#include <helpers/ui/MomentaryButton.h>
#ifndef USER_BTN_PRESSED
#define USER_BTN_PRESSED LOW
#endif
#define AUTO_OFF_MILLIS 20000 // 20 seconds
#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)
// позиции курсора на экране частоты: МГц, три разряда кГц и пункт сохранения
#define FREQ_MHZ_POS 0
#define FREQ_SAVE_POS 4
#define FREQ_POSITIONS 5
// Пункты мастера. Подписи латиницей: шрифты драйверов не содержат кириллицы.
enum {
MENU_COORDS = 0,
MENU_FREQ,
MENU_BW,
MENU_CR,
MENU_SF,
MENU_TX,
MENU_REPEAT,
MENU_PATHHASH,
MENU_ADVERT,
MENU_FLOODADV,
MENU_FLOODMAX,
MENU_EXIT,
MENU_COUNT
};
static const char* const MENU_NAMES[MENU_COUNT] = {
"Coordinates",
"Frequency",
"Bandwidth",
"CR",
"SF",
"TX power",
"Repeat",
"Path hash",
"Advert int",
"Flood advert",
"Flood max",
"Save & exit"
};
static const char* const PATH_HASH_NAMES[] = { "0 bytes", "1 byte", "2 bytes" };
// позиции цифр в строке вида "xx.xxxx"
static const uint8_t COORD_POS[6] = { 0, 1, 3, 4, 5, 6 };
// 'meshcore', 128x13px
static const uint8_t meshcore_logo [] PROGMEM = {
0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe,
0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe,
0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc,
0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00,
0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00,
0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0,
0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00,
0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00,
0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8,
};
static void stepClamped(int& v, int dir, int lo, int hi, int step) {
v += dir * step;
if (v < lo) v = lo;
if (v > hi) v = hi;
}
// строка-пункт: при выборе выделяется инверсией
static void renderRow(DisplayDriver* d, int y, const char* label, bool active) {
d->setTextSize(1);
if (active) {
d->setColor(UIColor::primary_txt);
d->fillRect(0, y - 1, d->width(), 10);
d->setColor(UIColor::window_bkg);
} else {
d->setColor(UIColor::primary_txt);
}
d->setCursor(1, y);
d->print(label);
d->setColor(UIColor::primary_txt);
}
void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version, CommonCLI* cli) {
_prevBtnState = HIGH;
_auto_off = millis() + AUTO_OFF_MILLIS;
_started_at = millis();
_node_prefs = node_prefs;
_cli = cli;
_toast[0] = 0;
_display->turnOn();
#if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS)
user_btn.begin();
#endif
// strip off dash and commit hash by changing dash to null terminator
// e.g: v1.2.3-abcdef -> v1.2.3
char *version = strdup(firmware_version);
char *dash = strchr(version, '-');
if(dash){
*dash = 0;
}
// v1.2.3 (1 Jan 2025)
snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date);
free(version);
// первый запуск: узел ещё не назван — сразу открываем мастер настройки
if (_node_prefs != nullptr && _node_prefs->node_name[0] == 0) {
openMenu();
}
}
int UITask::rowHeight() const {
return _display->height() >= 48 ? 10 : 8;
}
bool UITask::runCommand(const char* command) {
_toast[0] = 0;
if (_cli == nullptr) {
setToast("no CLI");
return false;
}
char buf[96];
strncpy(buf, command, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = 0;
char reply[128];
reply[0] = 0;
_cli->handleCommand(0, buf, reply);
if (reply[0] == 0) {
setToast("OK");
return true;
}
bool ok = !(reply[0] == 'E' || reply[0] == 'e'); // "Error..." / "OK..."
setToast(reply);
return ok;
}
void UITask::setToast(const char* msg) {
strncpy(_toast, msg, sizeof(_toast) - 1);
_toast[sizeof(_toast) - 1] = 0;
_toast_until = millis() + TOAST_MILLIS;
}
void UITask::openMenu() {
_screen = SCREEN_MENU;
_menu_index = 0;
_menu_top = 0;
_auto_off = millis() + WIZARD_AUTO_OFF_MILLIS;
}
// --- радиопараметры ---------------------------------------------------------
void UITask::loadRadio() {
if (_radio_loaded || _node_prefs == nullptr) return;
_radio_freq = _node_prefs->freq;
_radio_bw = _node_prefs->bw;
_radio_sf = _node_prefs->sf;
_radio_cr = _node_prefs->cr;
_radio_loaded = true;
}
void UITask::applyRadio() {
char cmd[96];
snprintf(cmd, sizeof(cmd), "set radio %.3f,%.2f,%d,%d", _radio_freq, _radio_bw, _radio_sf, _radio_cr);
runCommand(cmd);
}
void UITask::beginFreqEdit() {
loadRadio();
for (int i = 0; i < 4; i++) _freq_applied[i] = false;
_freq_index = 0;
_freq_editing = false;
double v = _radio_freq < 0 ? -_radio_freq : _radio_freq;
int mhz = (int)v;
int khz = (int)((v - mhz) * 1000.0 + 0.5);
if (khz > 999) { khz = 0; mhz++; }
_freq_mhz = mhz;
_freq_khz[0] = (char)('0' + (khz / 100) % 10);
_freq_khz[1] = (char)('0' + (khz / 10) % 10);
_freq_khz[2] = (char)('0' + khz % 10);
_screen = SCREEN_FREQ;
}
double UITask::freqValue() const {
int khz = (_freq_khz[0] - '0') * 100 + (_freq_khz[1] - '0') * 10 + (_freq_khz[2] - '0');
return _freq_mhz + khz / 1000.0;
}
void UITask::beginBwEdit() {
loadRadio();
_digit_int = 3;
_digit_count = 5; // xxx.xx, например 062.50
for (int i = 0; i < _digit_count; i++) _digit_applied[i] = false;
_digit_index = 0;
_digit_editing = false;
double v = _radio_bw < 0 ? -_radio_bw : _radio_bw;
int ip = (int)v;
int fp = (int)((v - ip) * 100.0 + 0.5);
if (fp > 99) { fp = 0; ip++; }
char buf[8];
snprintf(buf, sizeof(buf), "%03d%02d", ip % 1000, fp);
memcpy(_digit_digits, buf, _digit_count);
_screen = SCREEN_DIGITS;
}
double UITask::digitValue() const {
int ip = 0, fp = 0;
for (int i = 0; i < _digit_int; i++) ip = ip * 10 + (_digit_digits[i] - '0');
for (int i = _digit_int; i < _digit_count; i++) fp = fp * 10 + (_digit_digits[i] - '0');
double scale = 1;
for (int i = _digit_int; i < _digit_count; i++) scale *= 10;
return ip + fp / scale;
}
// --- меню -------------------------------------------------------------------
void UITask::openField(uint8_t index) {
_menu_index = index;
_auto_off = millis() + WIZARD_AUTO_OFF_MILLIS;
switch (index) {
case MENU_COORDS:
coordFromPrefs();
_screen = SCREEN_COORDS;
return;
case MENU_FREQ:
beginFreqEdit();
return;
case MENU_BW:
beginBwEdit();
return;
case MENU_CR:
loadRadio();
_value = _radio_cr;
break;
case MENU_SF:
loadRadio();
_value = _radio_sf;
break;
case MENU_TX:
_value = _node_prefs ? _node_prefs->tx_power_dbm : 0;
break;
case MENU_REPEAT:
_value = (_node_prefs && _node_prefs->disable_fwd) ? 0 : 1;
break;
case MENU_PATHHASH:
_value = _node_prefs ? _node_prefs->path_hash_mode : 0;
break;
case MENU_ADVERT:
_value = _node_prefs ? _node_prefs->advert_interval * 2 : 0;
break;
case MENU_FLOODADV:
_value = _node_prefs ? _node_prefs->flood_advert_interval : 0;
break;
case MENU_FLOODMAX:
_value = _node_prefs ? _node_prefs->flood_max : 0;
break;
default:
_screen = SCREEN_STATUS;
return;
}
_screen = SCREEN_VALUE;
}
void UITask::applyField() {
char cmd[96];
switch (_menu_index) {
case MENU_CR:
_radio_cr = _value;
applyRadio();
return;
case MENU_SF:
_radio_sf = _value;
applyRadio();
return;
case MENU_TX:
snprintf(cmd, sizeof(cmd), "set tx %d", _value);
break;
case MENU_REPEAT:
snprintf(cmd, sizeof(cmd), "set repeat %s", _value ? "on" : "off");
break;
case MENU_PATHHASH:
snprintf(cmd, sizeof(cmd), "set path.hash.mode %d", _value);
break;
case MENU_ADVERT:
snprintf(cmd, sizeof(cmd), "set advert.interval %d", _value);
break;
case MENU_FLOODADV:
snprintf(cmd, sizeof(cmd), "set flood.advert.interval %d", _value);
break;
case MENU_FLOODMAX:
snprintf(cmd, sizeof(cmd), "set flood.max %d", _value);
break;
default:
return;
}
runCommand(cmd);
}
// --- координаты -------------------------------------------------------------
void UITask::coordFromPrefs() {
for (int i = 0; i < 12; i++) _coord_applied[i] = false;
_coord_digit = 0;
_coord_editing = false;
double lat = _node_prefs ? _node_prefs->node_lat : 0;
double lon = _node_prefs ? _node_prefs->node_lon : 0;
if (lat < 0) lat = -lat;
if (lon < 0) lon = -lon;
int ldeg = (int)lat;
int lfrac = (int)((lat - ldeg) * 10000.0 + 0.5);
int odeg = (int)lon;
int ofrac = (int)((lon - odeg) * 10000.0 + 0.5);
if (lfrac > 9999) { lfrac = 0; ldeg++; }
if (ofrac > 9999) { ofrac = 0; odeg++; }
char buf[16];
snprintf(buf, sizeof(buf), "%02d%04d%02d%04d", ldeg % 100, lfrac, odeg % 100, ofrac);
memcpy(_coord_digits, buf, 12);
}
double UITask::coordAxisValue(uint8_t axis) const {
const char* d = &_coord_digits[axis * 6];
int deg = (d[0] - '0') * 10 + (d[1] - '0');
int frac = (d[2] - '0') * 1000 + (d[3] - '0') * 100 + (d[4] - '0') * 10 + (d[5] - '0');
return deg + frac / 10000.0;
}
void UITask::coordSave() {
char cmd[64];
snprintf(cmd, sizeof(cmd), "set lat %.4f", coordAxisValue(0));
bool okLat = runCommand(cmd);
char latMsg[sizeof(_toast)];
strncpy(latMsg, _toast, sizeof(latMsg) - 1);
latMsg[sizeof(latMsg) - 1] = 0;
snprintf(cmd, sizeof(cmd), "set lon %.4f", coordAxisValue(1));
bool okLon = runCommand(cmd);
if (okLat && okLon) {
setToast("coords saved");
} else {
setToast(latMsg[0] ? latMsg : _toast);
}
}
// --- кнопка -----------------------------------------------------------------
void UITask::handleButton(int ev) {
switch (_screen) {
case SCREEN_STATUS:
if (ev == BUTTON_EVENT_CLICK) {
if (!_display->isOn()) _display->turnOn();
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
openMenu();
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
_display->turnOn();
Serial.println("Powering Off");
_powering_off_at = millis() + POWEROFF_DELAY;
}
break;
case SCREEN_MENU:
if (ev == BUTTON_EVENT_CLICK) {
_menu_index = (uint8_t)((_menu_index + 1) % MENU_COUNT);
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
_menu_index = (uint8_t)((_menu_index + MENU_COUNT - 1) % MENU_COUNT);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
if (_menu_index == MENU_EXIT) {
applyRadio(); // на случай правок частоты/полосы/SF/CR
_screen = SCREEN_STATUS;
} else {
openField(_menu_index);
}
}
break;
case SCREEN_VALUE:
if (ev == BUTTON_EVENT_CLICK || ev == BUTTON_EVENT_DOUBLE_CLICK) {
int dir = (ev == BUTTON_EVENT_CLICK) ? 1 : -1;
switch (_menu_index) {
case MENU_CR:
stepClamped(_value, dir, 5, 8, 1);
break;
case MENU_SF:
stepClamped(_value, dir, 5, 12, 1);
break;
case MENU_REPEAT:
_value = _value ? 0 : 1;
break;
case MENU_PATHHASH:
_value = (_value + 3 + dir) % 3;
break;
case MENU_TX:
stepClamped(_value, dir, 1, 30, 1);
break;
case MENU_ADVERT:
if (_value == 0 && dir > 0) _value = 60;
else if (_value <= 60 && dir < 0) _value = 0;
else stepClamped(_value, dir, 60, 240, 10);
break;
case MENU_FLOODADV:
stepClamped(_value, dir, 3, 168, 1);
break;
case MENU_FLOODMAX:
stepClamped(_value, dir, 0, 64, 1);
break;
}
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
applyField();
_screen = SCREEN_MENU;
}
break;
case SCREEN_DIGITS: { // полоса: цифры + пункт Save & exit последней позицией
uint8_t positions = (uint8_t)(_digit_count + 1);
if (_digit_editing) {
if (ev == BUTTON_EVENT_CLICK) {
int d = _digit_digits[_digit_index] - '0';
_digit_digits[_digit_index] = (char)('0' + (d + 1) % 10);
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
int d = _digit_digits[_digit_index] - '0';
_digit_digits[_digit_index] = (char)('0' + (d + 9) % 10);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
_digit_editing = false;
_digit_applied[_digit_index] = true;
}
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
if (_digit_index >= _digit_count) { // Save & exit
_radio_bw = digitValue();
applyRadio();
_screen = SCREEN_MENU;
} else {
_digit_editing = true;
}
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
_digit_index = (uint8_t)((_digit_index + positions - 1) % positions);
} else if (ev == BUTTON_EVENT_CLICK) {
_digit_index = (uint8_t)((_digit_index + 1) % positions);
}
break;
}
case SCREEN_FREQ: { // частота: МГц шагом 1, кГц по разрядам, затем Save & exit
if (_freq_editing) {
if (_freq_index == 0) {
if (ev == BUTTON_EVENT_CLICK) {
if (_freq_mhz < 2500) _freq_mhz++;
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
if (_freq_mhz > 150) _freq_mhz--;
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
_freq_editing = false;
_freq_applied[0] = true;
}
} else {
uint8_t k = (uint8_t)(_freq_index - 1);
if (ev == BUTTON_EVENT_CLICK) {
int d = _freq_khz[k] - '0';
_freq_khz[k] = (char)('0' + (d + 1) % 10);
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
int d = _freq_khz[k] - '0';
_freq_khz[k] = (char)('0' + (d + 9) % 10);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
_freq_editing = false;
_freq_applied[_freq_index] = true;
}
}
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
if (_freq_index == FREQ_SAVE_POS) { // Save & exit
_radio_freq = freqValue();
applyRadio();
_screen = SCREEN_MENU;
} else {
_freq_editing = true;
}
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
_freq_index = (uint8_t)((_freq_index + FREQ_POSITIONS - 1) % FREQ_POSITIONS);
} else if (ev == BUTTON_EVENT_CLICK) {
_freq_index = (uint8_t)((_freq_index + 1) % FREQ_POSITIONS);
}
break;
}
case SCREEN_COORDS:
if (_coord_editing) {
// правка текущей цифры: короткое +1, двойное -1, длинное — применить
if (ev == BUTTON_EVENT_CLICK) {
int d = _coord_digits[_coord_digit] - '0';
_coord_digits[_coord_digit] = (char)('0' + (d + 1) % 10);
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
int d = _coord_digits[_coord_digit] - '0';
_coord_digits[_coord_digit] = (char)('0' + (d + 9) % 10);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
_coord_editing = false;
_coord_applied[_coord_digit] = true;
}
} else {
// цифра применена: длинное — войти в правку, короткое — следующая цифра
if (ev == BUTTON_EVENT_LONG_PRESS) {
_coord_editing = true;
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
_coord_digit = (uint8_t)((_coord_digit + 11) % 12);
} else if (ev == BUTTON_EVENT_CLICK) {
bool allApplied = true;
for (int i = 0; i < 12; i++) {
if (!_coord_applied[i]) { allApplied = false; break; }
}
if (_coord_digit == 11 && allApplied) {
_screen = SCREEN_COORDS_SAVE;
} else {
_coord_digit = (uint8_t)((_coord_digit + 1) % 12);
}
}
}
break;
case SCREEN_COORDS_SAVE:
if (ev == BUTTON_EVENT_LONG_PRESS) {
coordSave();
_screen = SCREEN_MENU;
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
_screen = SCREEN_COORDS;
}
break;
}
}
void UITask::loop() {
#if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS)
int ev = user_btn.check();
if (ev != BUTTON_EVENT_NONE) {
handleButton(ev);
_auto_off = millis() + (wizardActive() ? WIZARD_AUTO_OFF_MILLIS : AUTO_OFF_MILLIS);
}
#endif
if (_display->isOn()) {
if (millis() >= _next_refresh) {
_display->startFrame();
renderCurrScreen();
_display->endFrame();
_next_refresh = millis() + 1000; // refresh every second
}
if (millis() > _auto_off) {
_display->turnOff();
}
}
if (_powering_off_at > 0) { // power off timer armed
#ifdef LED_PIN
digitalWrite(LED_PIN, LED_STATE_ON); // switch on the led until poweroff
#endif
if (millis() > _powering_off_at) {
_board->powerOff(); // should not return
}
}
}
// --- отрисовка --------------------------------------------------------------
void UITask::renderTitleBar(const char* title) {
_display->setColor(UIColor::primary_txt);
_display->fillRect(0, 0, _display->width(), 11);
_display->setColor(UIColor::window_bkg);
_display->setTextSize(1);
_display->setCursor(1, 2);
_display->print(title);
_display->setColor(UIColor::primary_txt);
}
void UITask::renderHint(const char* hint) {
int y = _display->height() - 9;
if (y < 11) return;
_display->setColor(UIColor::primary_txt);
_display->setTextSize(1);
_display->setCursor(0, y);
_display->print(hint);
}
void UITask::renderToast() {
if (_toast[0] == 0 || millis() > _toast_until) return;
int w = _display->width();
int h = _display->height();
_display->setColor(UIColor::primary_txt);
_display->drawRect(0, 12, w, h - 12);
_display->setTextSize(1);
_display->setCursor(2, 15);
_display->printWordWrap(_toast, w - 4);
}
void UITask::renderMenu() {
renderTitleBar("Setup");
int w = _display->width();
int h = _display->height();
int rowH = rowHeight();
int rows = (h - 11 - 10) / rowH;
if (rows < 1) rows = 1;
if (_menu_index < _menu_top) _menu_top = _menu_index;
if (_menu_index >= _menu_top + rows) _menu_top = (uint8_t)(_menu_index - rows + 1);
_display->setTextSize(1);
for (int i = 0; i < rows; i++) {
int idx = _menu_top + i;
if (idx >= MENU_COUNT) break;
int y = 13 + i * rowH;
if (idx == _menu_index) {
_display->setColor(UIColor::primary_txt);
_display->fillRect(0, y - 1, w, rowH);
_display->setColor(UIColor::window_bkg);
} else {
_display->setColor(UIColor::primary_txt);
}
_display->setCursor(1, y);
_display->print(MENU_NAMES[idx]);
}
_display->setColor(UIColor::primary_txt);
renderHint("click=next hold=sel");
renderToast();
}
void UITask::renderValue() {
char value[40];
const char* title = MENU_NAMES[_menu_index];
switch (_menu_index) {
case MENU_CR:
snprintf(value, sizeof(value), "%d", _value);
break;
case MENU_SF:
snprintf(value, sizeof(value), "%d", _value);
break;
case MENU_TX:
snprintf(value, sizeof(value), "%d dBm", _value);
break;
case MENU_REPEAT:
snprintf(value, sizeof(value), "%s", _value ? "ON" : "OFF");
break;
case MENU_PATHHASH:
snprintf(value, sizeof(value), "%s", PATH_HASH_NAMES[_value % 3]);
break;
case MENU_ADVERT:
if (_value == 0) snprintf(value, sizeof(value), "off");
else snprintf(value, sizeof(value), "%d min", _value);
break;
case MENU_FLOODADV:
snprintf(value, sizeof(value), "%d h", _value);
break;
case MENU_FLOODMAX:
snprintf(value, sizeof(value), "%d", _value);
break;
default:
value[0] = 0;
break;
}
renderTitleBar(title);
_display->setTextSize(1);
_display->setColor(UIColor::primary_txt);
_display->setCursor(2, 20);
_display->print(value);
renderHint("clk+ dbl- hold=OK");
renderToast();
}
// поразрядный ввод значения с точкой (полоса): "062.50" + пункт Save & exit
void UITask::renderDigits() {
renderTitleBar("Bandwidth");
_display->setTextSize(1);
int y = 18;
int cx = 2;
for (int i = 0; i < _digit_count; i++) {
if (i == _digit_int) {
_display->setColor(UIColor::primary_txt);
_display->setCursor(cx, y);
_display->print(".");
cx += CHAR_W;
}
char c[2] = { _digit_digits[i], 0 };
if (i == _digit_index) {
_display->setColor(UIColor::primary_txt);
_display->fillRect(cx, y - 1, CHAR_W, 9);
_display->setColor(UIColor::window_bkg);
} else {
_display->setColor(UIColor::primary_txt);
}
_display->setCursor(cx, y);
_display->print(c);
_display->setColor(UIColor::primary_txt);
cx += CHAR_W;
}
_display->setColor(UIColor::primary_txt);
_display->setCursor(cx + CHAR_W, y);
_display->print("kHz");
renderRow(_display, 34, "Save & exit", _digit_index >= _digit_count);
if (_digit_editing) {
renderHint("clk+ dbl- hold=set");
} else if (_digit_index >= _digit_count) {
renderHint("hold=save & exit");
} else {
renderHint("hold=edit clk=next");
}
renderToast();
}
// частота: целые МГц выделяются целиком (шаг 1), кГц — по разрядам, затем Save & exit
void UITask::renderFreq() {
renderTitleBar("Frequency");
_display->setTextSize(1);
int y = 18;
int cx = 2;
char mhz[8];
snprintf(mhz, sizeof(mhz), "%d", _freq_mhz);
int mhzLen = (int)strlen(mhz);
if (_freq_index == FREQ_MHZ_POS) {
_display->setColor(UIColor::primary_txt);
_display->fillRect(cx, y - 1, mhzLen * CHAR_W, 9);
_display->setColor(UIColor::window_bkg);
} else {
_display->setColor(UIColor::primary_txt);
}
_display->setCursor(cx, y);
_display->print(mhz);
_display->setColor(UIColor::primary_txt);
cx += mhzLen * CHAR_W;
_display->setCursor(cx, y);
_display->print(".");
cx += CHAR_W;
for (int i = 0; i < 3; i++) {
char c[2] = { _freq_khz[i], 0 };
if (_freq_index == (uint8_t)(i + 1)) {
_display->setColor(UIColor::primary_txt);
_display->fillRect(cx, y - 1, CHAR_W, 9);
_display->setColor(UIColor::window_bkg);
} else {
_display->setColor(UIColor::primary_txt);
}
_display->setCursor(cx, y);
_display->print(c);
_display->setColor(UIColor::primary_txt);
cx += CHAR_W;
}
// единица измерения в конце строки значения
_display->setColor(UIColor::primary_txt);
_display->setCursor(cx + CHAR_W, y);
_display->print("MHz");
renderRow(_display, 34, "Save & exit", _freq_index == FREQ_SAVE_POS);
if (_freq_editing) {
renderHint(_freq_index == FREQ_MHZ_POS ? "clk +1MHz dbl -1" : "clk+ dbl- hold=set");
} else if (_freq_index == FREQ_SAVE_POS) {
renderHint("hold=save & exit");
} else {
renderHint("hold=edit clk=next");
}
renderToast();
}
void UITask::renderCoords() {
char text[10];
renderTitleBar("Coordinates");
for (int axis = 0; axis < 2; axis++) {
const char* d = &_coord_digits[axis * 6];
snprintf(text, sizeof(text), "%c%c.%c%c%c%c", d[0], d[1], d[2], d[3], d[4], d[5]);
int y = 18 + axis * 12;
_display->setTextSize(1);
for (int i = 0; i < 7; i++) {
int digitIndex = -1;
for (int k = 0; k < 6; k++) {
if (COORD_POS[k] == i) { digitIndex = k; break; }
}
int x = i * CHAR_W;
bool active = (digitIndex >= 0) && ((axis * 6 + digitIndex) == _coord_digit);
char c[2] = { text[i], 0 };
if (active) {
_display->setColor(UIColor::primary_txt);
_display->fillRect(x, y - 1, CHAR_W, 9);
_display->setColor(UIColor::window_bkg);
_display->setCursor(x, y);
_display->print(c);
_display->setColor(UIColor::primary_txt);
} else {
_display->setColor(UIColor::primary_txt);
_display->setCursor(x, y);
_display->print(c);
}
}
}
if (_coord_editing) {
renderHint("clk+ dbl- hold=set");
} else {
renderHint("hold=edit clk=next");
}
renderToast();
}
void UITask::renderCoordsSave() {
renderTitleBar("Coordinates");
_display->setTextSize(1);
_display->setColor(UIColor::primary_txt);
_display->setCursor(2, 20);
_display->print("Save & exit?");
renderHint("hold=save dbl=cancel");
renderToast();
}
void UITask::renderStatus() {
char tmp[80];
_display->setCursor(0, 0);
_display->setTextSize(1);
_display->setColor(UIColor::primary_txt);
_display->print(_node_prefs->node_name);
// freq / sf
_display->setCursor(0, 20);
sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf);
_display->print(tmp);
// bw / cr
_display->setCursor(0, 30);
sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
_display->print(tmp);
// WiFi IP (только там, где Wi-Fi вообще есть)
_display->setCursor(0, 40);
#if defined(ESP32)
if (WiFi.status() == WL_CONNECTED) {
IPAddress ip = WiFi.localIP();
snprintf(tmp, sizeof(tmp), "IP: %u.%u.%u.%u", ip[0], ip[1], ip[2], ip[3]);
} else {
snprintf(tmp, sizeof(tmp), "IP: -");
}
#else
snprintf(tmp, sizeof(tmp), "IP: -");
#endif
_display->print(tmp);
if (_display->height() >= 56) {
renderHint("dbl-click: setup");
}
renderToast();
}
void UITask::renderCurrScreen() {
if (millis() < _started_at + BOOT_SCREEN_MILLIS) { // boot screen
// meshcore logo
_display->setColor(UIColor::corp_blue);
int logoWidth = 128;
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// meshcore website
const char* website = "https://meshcore.io";
_display->setColor(UIColor::primary_txt);
_display->setTextSize(1);
_display->drawTextCentered(_display->width() / 2, 22, website);
// version info
_display->setTextSize(1);
_display->drawTextCentered(_display->width() / 2, 35, _version_info);
// node type
const char* node_type = "< Repeater >";
_display->drawTextCentered(_display->width() / 2, 48, node_type);
return;
}
if (_powering_off_at > 0) {
_display->setColor(UIColor::corp_blue);
int logoWidth = 128;
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
const char* website = "https://meshcore.io";
_display->setColor(UIColor::primary_txt);
_display->setTextSize(1);
_display->drawTextCentered(_display->width() / 2, 22, website);
const char* poweroff_string = "Turning OFF";
_display->drawTextCentered(_display->width() / 2, 48, poweroff_string);
return;
}
switch (_screen) {
case SCREEN_MENU: renderMenu(); break;
case SCREEN_VALUE: renderValue(); break;
case SCREEN_DIGITS: renderDigits(); break;
case SCREEN_FREQ: renderFreq(); break;
case SCREEN_COORDS: renderCoords(); break;
case SCREEN_COORDS_SAVE: renderCoordsSave(); break;
default: renderStatus(); break;
}
}