- добавлен локальный мастер настройки репитера: examples/simple_repeater/UITask.* (меню на кнопке, редактор координат, применение значений через CommonCLI) - MyMesh: публичный доступ к CommonCLI для UI (getCLI), main.cpp передаёт его в UITask - мастер работает на любой цели с дисплеем и кнопкой и исключается препроцессором там, где их нет; WiFi-код теперь под #if defined(ESP32) - новая страница документации posadmesh-docs/device-setup.md, ссылки в index.md и mkdocs.yml - README: раздел «Настройка с дисплея и кнопки» и запись в журнале изменений - проверено сборкой: heltec_v4_repeater_mqtt, Meshadventurer_sx1262_repeater_mqtt, Generic_E22_sx1262_repeater_mqtt (без дисплея), RAK_3112_repeater_mqtt
650 lines
19 KiB
C++
650 lines
19 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
|
|
|
|
// Пункты мастера. Подписи латиницей: шрифты драйверов не содержат кириллицы.
|
|
enum {
|
|
MENU_COORDS = 0,
|
|
MENU_RADIO,
|
|
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",
|
|
"Radio preset",
|
|
"TX power",
|
|
"Repeat",
|
|
"Path hash",
|
|
"Advert int",
|
|
"Flood advert",
|
|
"Flood max",
|
|
"Save & exit"
|
|
};
|
|
|
|
// 0 = не менять; MeshCoreTel MOW: 868.731 МГц, 62.5 кГц, SF7, CR7
|
|
static const char* const RADIO_PRESET_CMDS[] = { nullptr, "set radio 868.731,62.5,7,7" };
|
|
static const char* const RADIO_PRESET_NAMES[] = { "keep current", "MeshCoreTel MOW" };
|
|
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;
|
|
}
|
|
|
|
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::openField(uint8_t index) {
|
|
_menu_index = index;
|
|
switch (index) {
|
|
case MENU_COORDS:
|
|
coordFromPrefs();
|
|
_screen = SCREEN_COORDS;
|
|
break;
|
|
case MENU_RADIO:
|
|
_value = 0; // keep current
|
|
_screen = SCREEN_VALUE;
|
|
break;
|
|
case MENU_TX:
|
|
_value = _node_prefs ? _node_prefs->tx_power_dbm : 0;
|
|
_screen = SCREEN_VALUE;
|
|
break;
|
|
case MENU_REPEAT:
|
|
_value = (_node_prefs && _node_prefs->disable_fwd) ? 0 : 1;
|
|
_screen = SCREEN_VALUE;
|
|
break;
|
|
case MENU_PATHHASH:
|
|
_value = _node_prefs ? _node_prefs->path_hash_mode : 0;
|
|
_screen = SCREEN_VALUE;
|
|
break;
|
|
case MENU_ADVERT:
|
|
_value = _node_prefs ? _node_prefs->advert_interval * 2 : 0;
|
|
_screen = SCREEN_VALUE;
|
|
break;
|
|
case MENU_FLOODADV:
|
|
_value = _node_prefs ? _node_prefs->flood_advert_interval : 0;
|
|
_screen = SCREEN_VALUE;
|
|
break;
|
|
case MENU_FLOODMAX:
|
|
_value = _node_prefs ? _node_prefs->flood_max : 0;
|
|
_screen = SCREEN_VALUE;
|
|
break;
|
|
default:
|
|
_screen = SCREEN_STATUS;
|
|
break;
|
|
}
|
|
_auto_off = millis() + WIZARD_AUTO_OFF_MILLIS;
|
|
}
|
|
|
|
void UITask::applyField() {
|
|
char cmd[96];
|
|
bool ok = false;
|
|
|
|
switch (_menu_index) {
|
|
case MENU_RADIO:
|
|
if (_value <= 0 || RADIO_PRESET_CMDS[_value] == nullptr) {
|
|
setToast("keep current");
|
|
return;
|
|
}
|
|
ok = runCommand(RADIO_PRESET_CMDS[_value]);
|
|
break;
|
|
case MENU_TX:
|
|
snprintf(cmd, sizeof(cmd), "set tx %d", _value);
|
|
ok = runCommand(cmd);
|
|
break;
|
|
case MENU_REPEAT:
|
|
snprintf(cmd, sizeof(cmd), "set repeat %s", _value ? "on" : "off");
|
|
ok = runCommand(cmd);
|
|
break;
|
|
case MENU_PATHHASH:
|
|
snprintf(cmd, sizeof(cmd), "set path.hash.mode %d", _value);
|
|
ok = runCommand(cmd);
|
|
break;
|
|
case MENU_ADVERT:
|
|
snprintf(cmd, sizeof(cmd), "set advert.interval %d", _value);
|
|
ok = runCommand(cmd);
|
|
break;
|
|
case MENU_FLOODADV:
|
|
snprintf(cmd, sizeof(cmd), "set flood.advert.interval %d", _value);
|
|
ok = runCommand(cmd);
|
|
break;
|
|
case MENU_FLOODMAX:
|
|
snprintf(cmd, sizeof(cmd), "set flood.max %d", _value);
|
|
ok = runCommand(cmd);
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
(void)ok;
|
|
}
|
|
|
|
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) {
|
|
_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_RADIO:
|
|
_value = (_value + 1) % 2;
|
|
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_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_RADIO:
|
|
snprintf(value, sizeof(value), "%s", RADIO_PRESET_NAMES[_value % 2]);
|
|
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();
|
|
}
|
|
|
|
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 * 6;
|
|
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, 6, 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_COORDS: renderCoords(); break;
|
|
case SCREEN_COORDS_SAVE: renderCoordsSave(); break;
|
|
default: renderStatus(); break;
|
|
}
|
|
}
|