- ESP32Board: общий механизм adcMultiplierBase()/adcMultiplier(); setAdcMultiplier возвращает false только там, где батарея измеряется не через ADC - платы со своей формулой переведены на общий механизм: Heltec E213/E290/T190, heltec_tracker_v2, Heltec v2, ThinkNode M2/M5; Station G2/G3 и T-Beam сообщают, что подстройка не поддерживается - мастер настройки: поле ADC стартует от действующего значения платы (getAdcMultiplier) и работает с тремя знаками (x.xxx), например 5.420; 0.000 возвращает заводское значение платы - Meshadventurer: добавлена поддержка множителя (раньше делитель был зашит числом 2 и set adc.multiplier отвечал Error: unsupported) - экран загрузки: meshcore.io, версия с датой и posadmesh.ru; строка < Repeater > убрана Проверено сборкой: heltec_v4_repeater_mqtt, Meshadventurer_sx1262_repeater_mqtt
1090 lines
35 KiB
C++
1090 lines
35 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 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_BATTERY,
|
|
MENU_ADC,
|
|
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",
|
|
"Battery",
|
|
"ADC",
|
|
"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) {
|
|
showPopup("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) {
|
|
showPopup("OK");
|
|
return true;
|
|
}
|
|
bool ok = !(reply[0] == 'E' || reply[0] == 'e'); // "Error..." / "OK..."
|
|
showPopup(reply);
|
|
return ok;
|
|
}
|
|
|
|
// Всплывающее окно с ответом устройства: показывается поверх экрана мастера
|
|
// и закрывается коротким нажатием кнопки (см. handleButton).
|
|
void UITask::showPopup(const char* msg) {
|
|
strncpy(_toast, msg, sizeof(_toast) - 1);
|
|
_toast[sizeof(_toast) - 1] = 0;
|
|
_popup_active = true;
|
|
_next_refresh = 0; // показать окно сразу, не ждать тика отрисовки
|
|
_auto_off = millis() + WIZARD_AUTO_OFF_MILLIS;
|
|
}
|
|
|
|
void UITask::openMenu() {
|
|
_screen = SCREEN_MENU;
|
|
_menu_index = 0;
|
|
_menu_top = 0;
|
|
_radio_loaded = false; // при каждом входе перечитываем радио из настроек узла
|
|
_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;
|
|
}
|
|
|
|
// Отправка радиопараметров. Вызывается только когда их действительно правили:
|
|
// иначе на выходе из мастера уходила команда с нулевыми значениями рабочей копии
|
|
// и устройство отвечало "Error, invalid radio params".
|
|
void UITask::applyRadio() {
|
|
if (!_radio_dirty) return;
|
|
loadRadio(); // на случай, если правка шла без открытия пункта
|
|
char cmd[96];
|
|
snprintf(cmd, sizeof(cmd), "set radio %.3f,%.2f,%d,%d", _radio_freq, _radio_bw, _radio_sf, _radio_cr);
|
|
if (runCommand(cmd)) {
|
|
_radio_dirty = false;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
void UITask::beginAdcEdit() {
|
|
// Множитель ADC: x.xxx. Стартуем от значения платы — базового или уже
|
|
// сохранённого, а не от нуля; 0.000 в поле означает «вернуть заводское значение».
|
|
_digit_int = 1;
|
|
_digit_count = 4;
|
|
for (int i = 0; i < _digit_count; i++) _digit_applied[i] = false;
|
|
_digit_index = 0;
|
|
_digit_editing = false;
|
|
|
|
float v = _board ? _board->getAdcMultiplier() : 0.0f;
|
|
if (v <= 0.0f && _node_prefs) v = _node_prefs->adc_multiplier;
|
|
if (v < 0) v = 0;
|
|
if (v > 9.999f) v = 9.999f;
|
|
int ip = (int)v;
|
|
int fp = (int)((v - ip) * 1000.0f + 0.5f);
|
|
if (fp > 999) { fp = 0; ip++; }
|
|
char buf[8];
|
|
snprintf(buf, sizeof(buf), "%d%03d", ip % 10, 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;
|
|
}
|
|
|
|
// Применение поразрядного редактора: полоса (set radio) или множитель ADC
|
|
void UITask::applyDigits() {
|
|
if (_menu_index == MENU_ADC) {
|
|
char cmd[48];
|
|
snprintf(cmd, sizeof(cmd), "set adc.multiplier %d.%03d",
|
|
_digit_digits[0] - '0',
|
|
(_digit_digits[1] - '0') * 100 + (_digit_digits[2] - '0') * 10 + (_digit_digits[3] - '0'));
|
|
runCommand(cmd);
|
|
return;
|
|
}
|
|
_radio_bw = digitValue();
|
|
_radio_dirty = true;
|
|
applyRadio();
|
|
}
|
|
|
|
// Процент заряда: значение платы, а если его нет — линейная оценка по напряжению
|
|
int UITask::batteryPercent() const {
|
|
if (_board == nullptr) return -1;
|
|
int pct = _board->getBatteryPercent();
|
|
if (pct >= 0) return pct > 100 ? 100 : pct;
|
|
|
|
const uint16_t mv = _board->getBattMilliVolts();
|
|
const uint16_t lo = _board->getBatteryMinMilliVolts();
|
|
const uint16_t hi = _board->getBatteryMaxMilliVolts();
|
|
if (hi <= lo) return -1;
|
|
if (mv <= lo) return 0;
|
|
if (mv >= hi) return 100;
|
|
return (int)(((uint32_t)(mv - lo) * 100U) / (uint32_t)(hi - lo));
|
|
}
|
|
|
|
// --- меню -------------------------------------------------------------------
|
|
|
|
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_BATTERY:
|
|
_screen = SCREEN_BATTERY;
|
|
return;
|
|
case MENU_ADC:
|
|
beginAdcEdit();
|
|
return;
|
|
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;
|
|
_radio_dirty = true;
|
|
applyRadio();
|
|
return;
|
|
case MENU_SF:
|
|
_radio_sf = _value;
|
|
_radio_dirty = true;
|
|
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) {
|
|
showPopup("coords saved");
|
|
} else {
|
|
showPopup(latMsg[0] ? latMsg : _toast);
|
|
}
|
|
}
|
|
|
|
// --- кнопка -----------------------------------------------------------------
|
|
|
|
void UITask::handleButton(int ev) {
|
|
// Пока открыто всплывающее окно, оно модальное: любое нажатие только закрывает его
|
|
// (короткое — как просили), экран под ним не меняется.
|
|
if (_popup_active) {
|
|
_popup_active = false;
|
|
_next_refresh = 0; // сразу перерисовать экран под окном
|
|
_auto_off = millis() + WIZARD_AUTO_OFF_MILLIS;
|
|
return;
|
|
}
|
|
|
|
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(); // применит радио, только если его правили в этой сессии
|
|
_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
|
|
applyDigits();
|
|
_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_BATTERY: { // заряд аккумулятора: только просмотр
|
|
if (ev == BUTTON_EVENT_CLICK) {
|
|
char msg[32];
|
|
snprintf(msg, sizeof(msg), "raw %u mV", (unsigned)(_board ? _board->getBattMilliVolts() : 0));
|
|
showPopup(msg);
|
|
} else if (ev == BUTTON_EVENT_LONG_PRESS || ev == BUTTON_EVENT_DOUBLE_CLICK) {
|
|
_screen = SCREEN_MENU;
|
|
}
|
|
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();
|
|
_radio_dirty = true;
|
|
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();
|
|
if (_popup_active) renderPopup(); // окно рисуется поверх экрана
|
|
_display->endFrame();
|
|
|
|
_next_refresh = millis() + 1000; // refresh every second
|
|
}
|
|
if (millis() > _auto_off) {
|
|
_display->turnOff();
|
|
}
|
|
}
|
|
|
|
if (_powering_off_at > 0) { // power off timer armed
|
|
#if defined(LED_PIN) && defined(LED_STATE_ON)
|
|
digitalWrite(LED_PIN, LED_STATE_ON); // switch on the led until poweroff
|
|
delay(1000);
|
|
digitalWrite(LED_PIN, !LED_STATE_ON); // off LED
|
|
#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);
|
|
}
|
|
|
|
// Всплывающее окно: рамка, свой фон (чтобы текст не сливался со строками экрана)
|
|
// и кнопка OK внизу. Рисуется поверх текущего экрана.
|
|
void UITask::renderPopup() {
|
|
const int w = _display->width();
|
|
const int h = _display->height();
|
|
int box_h = 46;
|
|
if (box_h > h - 8) box_h = h - 8;
|
|
int top = (h - box_h) / 2;
|
|
if (top < 12) top = 12;
|
|
|
|
// рамка: белая заливка и чёрная внутренняя область
|
|
_display->setColor(UIColor::primary_txt);
|
|
_display->fillRect(4, top, w - 8, box_h);
|
|
_display->setColor(UIColor::window_bkg);
|
|
_display->fillRect(6, top + 2, w - 12, box_h - 4);
|
|
|
|
// текст сообщения
|
|
_display->setColor(UIColor::primary_txt);
|
|
_display->setTextSize(1);
|
|
_display->setCursor(8, top + 5);
|
|
_display->printWordWrap(_toast, w - 20);
|
|
|
|
// кнопка OK
|
|
const char* ok = "OK";
|
|
int ok_w = (int)strlen(ok) * CHAR_W + 10;
|
|
int ok_x = (w - ok_w) / 2;
|
|
int ok_y = top + box_h - 14;
|
|
_display->setColor(UIColor::primary_txt);
|
|
_display->drawRect(ok_x, ok_y, ok_w, 12);
|
|
_display->setCursor(ok_x + 5, ok_y + 2);
|
|
_display->print(ok);
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
// поразрядный ввод значения с точкой: полоса ("062.50") или множитель ADC ("0.00")
|
|
void UITask::renderDigits() {
|
|
const bool adc = (_menu_index == MENU_ADC);
|
|
renderTitleBar(adc ? "ADC" : "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(adc ? "x" : "kHz");
|
|
|
|
// На экране ADC показываем измеренное напряжение: по нему видно результат калибровки
|
|
if (adc) {
|
|
const unsigned mv = _board ? _board->getBattMilliVolts() : 0;
|
|
char volts[24];
|
|
snprintf(volts, sizeof(volts), "%u.%02u V", mv / 1000U, (mv % 1000U) / 10U);
|
|
_display->setColor(UIColor::primary_txt);
|
|
_display->setCursor(2, 45);
|
|
_display->print(volts);
|
|
}
|
|
|
|
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");
|
|
}
|
|
}
|
|
|
|
// заряд аккумулятора: напряжение, процент, полоска и текущий множитель ADC
|
|
void UITask::renderBattery() {
|
|
renderTitleBar("Battery");
|
|
_display->setTextSize(1);
|
|
_display->setColor(UIColor::primary_txt);
|
|
|
|
const unsigned mv = _board ? _board->getBattMilliVolts() : 0;
|
|
char line[32];
|
|
snprintf(line, sizeof(line), "%u.%02u V", mv / 1000U, (mv % 1000U) / 10U);
|
|
_display->setCursor(2, 15);
|
|
_display->print(line);
|
|
|
|
const int pct = batteryPercent();
|
|
if (pct >= 0) {
|
|
snprintf(line, sizeof(line), "%d %%", pct);
|
|
int x = _display->width() - (int)strlen(line) * CHAR_W - 2;
|
|
if (x < 2) x = 2;
|
|
_display->setCursor(x, 15);
|
|
_display->print(line);
|
|
}
|
|
|
|
// полоска заряда
|
|
const int bar_w = _display->width() - 4;
|
|
_display->setColor(UIColor::primary_txt);
|
|
_display->drawRect(2, 27, bar_w, 9);
|
|
if (pct > 0) {
|
|
int fill = (bar_w - 4) * (pct > 100 ? 100 : pct) / 100;
|
|
if (fill > 0) _display->fillRect(4, 29, fill, 5);
|
|
}
|
|
|
|
// Текущий множитель ADC: показываем действующее значение платы и помечаем,
|
|
// что оно заводское (в настройках 0 = «использовать базовое значение платы»)
|
|
const float adc_eff = _board ? _board->getAdcMultiplier() : 0.0f;
|
|
const bool adc_is_base = (_node_prefs == nullptr) || (_node_prefs->adc_multiplier <= 0.0f);
|
|
if (adc_eff > 0.0f) {
|
|
snprintf(line, sizeof(line), "ADC %.3f%s", (double)adc_eff, adc_is_base ? " base" : "");
|
|
} else {
|
|
snprintf(line, sizeof(line), "ADC n/a");
|
|
}
|
|
_display->setColor(UIColor::primary_txt);
|
|
_display->setCursor(2, 40);
|
|
_display->print(line);
|
|
|
|
renderHint("hold=menu clk=raw");
|
|
}
|
|
|
|
// частота: целые МГц выделяются целиком (шаг 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");
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
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");
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
_display->setColor(UIColor::primary_txt);
|
|
_display->setTextSize(1);
|
|
_display->drawTextCentered(_display->width() / 2, 20, "meshcore.io");
|
|
_display->drawTextCentered(_display->width() / 2, 32, _version_info);
|
|
_display->drawTextCentered(_display->width() / 2, 44, "posadmesh.ru");
|
|
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_BATTERY: renderBattery(); break;
|
|
case SCREEN_COORDS: renderCoords(); break;
|
|
case SCREEN_COORDS_SAVE: renderCoordsSave(); break;
|
|
default: renderStatus(); break;
|
|
}
|
|
}
|