Detect OLED or the original stock TFT touch assembly in ordinary V4.2/V4.3 images. Add saved live display.type auto/oled/tft/off selection and report the active selection in board output. Keep sensors on primary I2C, preserve shared power, and isolate TFT FSPI from radio HSPI through sleep and changes. Consolidate future picker/build choices only with application capability proof. Retain exact legacy OTA identities and separate R8 hardware images. Document controls and physical qualification limits. Validation: five serialized Full builds with capability/size/RAM checks; focused host tests, existing R8/T-Deck/OLED regressions, build-profile suite, and picker/provider tests. V4.3 OLED live changes and off/auto reboot persistence passed on Mercerwood. Physical TFT qualification remains pending.
1013 lines
38 KiB
C++
1013 lines
38 KiB
C++
#include "UITask.h"
|
|
#include "target.h"
|
|
#include <Arduino.h>
|
|
#include <helpers/UsbLogging.h>
|
|
#include <helpers/MorseLoopPolicy.h>
|
|
#include <helpers/CommonCLI.h>
|
|
#include <helpers/ui/WiFiSetupQrDisplay.h>
|
|
#include <helpers/ui/CompanionHomeLayout.h>
|
|
#include <helpers/ui/RadioProfileDisplayPage.h>
|
|
#include <RadioProfiles.h>
|
|
#include <Utils.h>
|
|
#include "MyMesh.h"
|
|
|
|
extern MyMesh the_mesh;
|
|
|
|
namespace {
|
|
|
|
const mesh::RadioProfiles* configuredRadioProfiles() {
|
|
const auto* radio = the_mesh.getProfileRadio();
|
|
return radio ? radio->profiles() : NULL;
|
|
}
|
|
|
|
const char* secondaryRadioProfileTag() {
|
|
const auto* profiles = configuredRadioProfiles();
|
|
return mesh::ui::radioProfileDisplayTag(true,
|
|
profiles != NULL && profiles->secondary_temporary);
|
|
}
|
|
|
|
// The complete system-status view needs four rows on a single-radio node and
|
|
// two more when radio2 is configured. Grouping related switches keeps a
|
|
// normal 128x64 OLED to one status page (rather than silently dropping the
|
|
// lower rows), while the measured page count below naturally splits this on
|
|
// a genuinely short display.
|
|
constexpr uint8_t RADIO_SYSTEM_STATUS_ROWS = 8;
|
|
constexpr uint8_t RADIO_SYSTEM_STATUS_ROWS_WITHOUT_RADIO2 = 5;
|
|
|
|
uint8_t radioSystemStatusRowsPerPage(DisplayDriver& display) {
|
|
const int line_height = display.textLineHeight();
|
|
if (line_height <= 0) return 1;
|
|
const int row_height = line_height + 2;
|
|
const int rows = (display.height() + 2) / row_height;
|
|
return rows > 0 ? rows : 1;
|
|
}
|
|
|
|
uint8_t radioSystemStatusPageCount(DisplayDriver& display,
|
|
bool dual_radio_enabled) {
|
|
const uint8_t row_count = dual_radio_enabled ? RADIO_SYSTEM_STATUS_ROWS
|
|
: RADIO_SYSTEM_STATUS_ROWS_WITHOUT_RADIO2;
|
|
const uint8_t rows_per_page = radioSystemStatusRowsPerPage(display);
|
|
return (row_count + rows_per_page - 1) / rows_per_page;
|
|
}
|
|
|
|
int drawRadioStatusState(DisplayDriver& display, int x, int y,
|
|
const char* label, bool enabled) {
|
|
const char* state = enabled ? "ON" : "OFF";
|
|
display.setColor(UIColor::primary_txt);
|
|
display.setCursor(x, y);
|
|
display.print(label);
|
|
x += display.getTextWidth(label);
|
|
display.setColor(enabled ? UIColor::primary_txt : UIColor::warning_txt);
|
|
display.setCursor(x, y);
|
|
display.print(state);
|
|
return x + display.getTextWidth(state);
|
|
}
|
|
|
|
void drawRadioStatusPair(DisplayDriver& display, int y,
|
|
const char* first_label, bool first_enabled,
|
|
const char* second_label, bool second_enabled) {
|
|
int x = drawRadioStatusState(display, 0, y, first_label, first_enabled);
|
|
drawRadioStatusState(display, x + display.getTextWidth(" "), y,
|
|
second_label, second_enabled);
|
|
display.setColor(UIColor::primary_txt);
|
|
}
|
|
|
|
void formatRadioNoiseFloor(char* noise_floor, size_t size, const char* label,
|
|
uint8_t profile = 0) {
|
|
const float dbm = radio_driver.getNoiseFloorDbm(profile);
|
|
if (dbm == 0.0f) {
|
|
const float seconds = radio_driver.getNoiseFloorCalibrationSecondsRemaining(profile);
|
|
if (seconds > 0.0f) {
|
|
snprintf(noise_floor, size, "%s:%.1fs", label, seconds);
|
|
} else {
|
|
snprintf(noise_floor, size, "%s:WAIT", label);
|
|
}
|
|
} else {
|
|
snprintf(noise_floor, size, "%s:%.1f", label, dbm);
|
|
}
|
|
}
|
|
|
|
void drawRadioNoiseFloor(DisplayDriver& display, int y, const char* label,
|
|
uint8_t profile = 0) {
|
|
char noise_floor[12];
|
|
formatRadioNoiseFloor(noise_floor, sizeof(noise_floor), label, profile);
|
|
display.setColor(UIColor::primary_txt);
|
|
display.setCursor(0, y);
|
|
display.print(noise_floor);
|
|
}
|
|
|
|
void drawRadioSystemStatusPage(DisplayDriver& display, const NodePrefs& prefs,
|
|
uint8_t status_page_index) {
|
|
display.setTextSize(1);
|
|
const auto* profiles = configuredRadioProfiles();
|
|
const bool dual_radio = profiles != NULL && profiles->enabled();
|
|
const uint8_t rows_per_page = radioSystemStatusRowsPerPage(display);
|
|
const uint8_t first_row = status_page_index * rows_per_page;
|
|
const uint8_t row_count = dual_radio ? RADIO_SYSTEM_STATUS_ROWS
|
|
: RADIO_SYSTEM_STATUS_ROWS_WITHOUT_RADIO2;
|
|
const int row_height = display.textLineHeight() + 2;
|
|
|
|
for (uint8_t row = first_row; row < row_count
|
|
&& row < first_row + rows_per_page; ++row) {
|
|
const int y = (row - first_row) * row_height;
|
|
switch (row) {
|
|
case 0: {
|
|
char identity_prefix[7];
|
|
mesh::Utils::toHex(identity_prefix, the_mesh.getSelfId().pub_key, 3);
|
|
display.setColor(UIColor::primary_txt);
|
|
display.setCursor(0, y);
|
|
display.print("ID:");
|
|
display.print(identity_prefix);
|
|
break;
|
|
}
|
|
case 1:
|
|
#if ENV_INCLUDE_GPS == 1
|
|
drawRadioStatusPair(display, y, "PS:", prefs.powersaving_enabled != 0,
|
|
"GPS:", prefs.gps_enabled != 0);
|
|
#else
|
|
drawRadioStatusPair(display, y, "PS:", prefs.powersaving_enabled != 0,
|
|
"GPS:", false);
|
|
#endif
|
|
break;
|
|
case 2:
|
|
drawRadioStatusPair(display, y, "FEM:",
|
|
board.canControlLoRaFemLna() && board.isLoRaFemLnaEnabled(),
|
|
"RXB:", prefs.rx_boosted_gain != 0);
|
|
break;
|
|
case 3:
|
|
drawRadioStatusPair(display, y, "RXPS:", prefs.rx_powersaving_enabled != 0,
|
|
"CAD:", prefs.cad_enabled != 0);
|
|
break;
|
|
case 4: {
|
|
char mode[10];
|
|
snprintf(mode, sizeof(mode), "%s:%s", secondaryRadioProfileTag(),
|
|
profiles->secondary.mode == mesh::RadioProfileMode::RxTx ? "RXTX" : "RX");
|
|
display.setColor(UIColor::primary_txt);
|
|
display.setCursor(0, y);
|
|
display.print(mode);
|
|
break;
|
|
}
|
|
case 5: {
|
|
const char* cross = profiles->cross == mesh::RadioCrossMode::On ? "X:ON"
|
|
: profiles->cross == mesh::RadioCrossMode::Off ? "X:OFF" : "X:AUTO";
|
|
display.setColor(UIColor::primary_txt);
|
|
display.setCursor(0, y);
|
|
display.print(cross);
|
|
break;
|
|
}
|
|
case 6:
|
|
drawRadioNoiseFloor(display, y, "N1", 0);
|
|
break;
|
|
case 7:
|
|
drawRadioNoiseFloor(display, y, "N2", 1);
|
|
break;
|
|
}
|
|
}
|
|
display.setColor(UIColor::primary_txt);
|
|
}
|
|
|
|
#if defined(HELTEC_T096)
|
|
|
|
// The native T096 canvas is 160x80. Compact F/B/S/C rows use the first 108
|
|
// pixels; this leaves a 50px status column plus a 2px right margin. The
|
|
// widest normal 6x8 label, "RXPS:OFF", is 48px, so it has a 2px buffer.
|
|
constexpr int T096_STATUS_PANEL_WIDTH = 50;
|
|
constexpr int T096_STATUS_RIGHT_MARGIN = 2;
|
|
constexpr int T096_STATUS_TOP = 10;
|
|
constexpr int T096_STATUS_LINE_HEIGHT = 10;
|
|
|
|
void drawT096State(DisplayDriver& display, int x, int y,
|
|
const char* label, bool enabled) {
|
|
const char* state = enabled ? "ON" : "OFF";
|
|
display.setColor(UIColor::primary_txt);
|
|
display.setCursor(x, y);
|
|
display.print(label);
|
|
display.setColor(enabled ? UIColor::primary_txt : UIColor::warning_txt);
|
|
display.setCursor(x + display.getTextWidth(label), y);
|
|
display.print(state);
|
|
}
|
|
|
|
void drawT096StatusRow(DisplayDriver& display, int y,
|
|
const char* name, bool enabled) {
|
|
char label[8]; // "RXPS:" plus its terminator
|
|
snprintf(label, sizeof(label), "%s:", name);
|
|
const char* state = enabled ? "ON" : "OFF";
|
|
const int x = display.width() - T096_STATUS_RIGHT_MARGIN
|
|
- display.getTextWidth(label) - display.getTextWidth(state);
|
|
drawT096State(display, x, y, label, enabled);
|
|
}
|
|
|
|
void drawT096NoiseFloorRow(DisplayDriver& display, int y, uint8_t profile) {
|
|
char noise_floor[12];
|
|
const bool dual_radio = the_mesh.isDualRadioActive();
|
|
const char* label = dual_radio ? (profile == 1 ? "N2" : "N1") : "N";
|
|
// "N1:-120.0" needs 54px in the normal 6px font. On this final line the
|
|
// left side contains only the short cross-mode marker, so it may safely use
|
|
// four pixels of the otherwise empty gutter while retaining decimal dBm.
|
|
formatRadioNoiseFloor(noise_floor, sizeof(noise_floor), label, profile);
|
|
display.setColor(UIColor::primary_txt);
|
|
display.setCursor(display.width() - T096_STATUS_RIGHT_MARGIN
|
|
- display.getTextWidth(noise_floor), y);
|
|
display.print(noise_floor);
|
|
}
|
|
|
|
void drawT096StatusPanel(DisplayDriver& display, const NodePrefs& prefs,
|
|
uint8_t profile) {
|
|
// This is the ST7735 driver's standard 6x8 font, rather than Squeezed6.
|
|
display.setTextSize(1);
|
|
int y = T096_STATUS_TOP;
|
|
#if ENV_INCLUDE_GPS == 1
|
|
drawT096StatusRow(display, y, "GPS", prefs.gps_enabled != 0);
|
|
#else
|
|
drawT096StatusRow(display, y, "GPS", false);
|
|
#endif
|
|
y += T096_STATUS_LINE_HEIGHT;
|
|
drawT096StatusRow(display, y, "FEM", board.canControlLoRaFemLna()
|
|
&& board.isLoRaFemLnaEnabled());
|
|
y += T096_STATUS_LINE_HEIGHT;
|
|
drawT096StatusRow(display, y, "RXB", prefs.rx_boosted_gain != 0);
|
|
y += T096_STATUS_LINE_HEIGHT;
|
|
drawT096StatusRow(display, y, "RXPS", prefs.rx_powersaving_enabled != 0);
|
|
|
|
// A configured secondary profile keeps both profiles live; show it only
|
|
// when it exists, so the panel does not imply a second radio on every T096.
|
|
if (the_mesh.isDualRadioActive()) {
|
|
y += T096_STATUS_LINE_HEIGHT;
|
|
drawT096StatusRow(display, y, secondaryRadioProfileTag(), true);
|
|
}
|
|
y += T096_STATUS_LINE_HEIGHT;
|
|
drawT096StatusRow(display, y, "CAD", prefs.cad_enabled != 0);
|
|
y += T096_STATUS_LINE_HEIGHT;
|
|
drawT096NoiseFloorRow(display, y, profile);
|
|
display.setColor(UIColor::primary_txt);
|
|
}
|
|
|
|
void drawT096Radio2Details(DisplayDriver& display, int mode_y, int cross_y) {
|
|
const auto* profiles = configuredRadioProfiles();
|
|
if (profiles == NULL || !profiles->enabled()) return;
|
|
|
|
char mode[10];
|
|
snprintf(mode, sizeof(mode), "%s:%s", secondaryRadioProfileTag(),
|
|
profiles->secondary.mode == mesh::RadioProfileMode::RxTx ? "RXTX" : "RX");
|
|
const char* cross = profiles->cross == mesh::RadioCrossMode::On ? "X:ON"
|
|
: profiles->cross == mesh::RadioCrossMode::Off ? "X:OFF" : "X:AUTO";
|
|
display.setColor(UIColor::primary_txt);
|
|
display.setCursor(0, mode_y);
|
|
display.print(mode);
|
|
display.setCursor(0, cross_y);
|
|
display.print(cross);
|
|
}
|
|
|
|
#endif // HELTEC_T096
|
|
|
|
} // namespace
|
|
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
#include <helpers/ui/DisplayFrameSignature.h>
|
|
#endif
|
|
|
|
#ifndef USER_BTN_PRESSED
|
|
#define USER_BTN_PRESSED LOW
|
|
#endif
|
|
|
|
#ifdef ESP_PLATFORM
|
|
#include <WiFi.h>
|
|
#include <helpers/esp32/WebConfigServer.h> // defines WITH_WEBCONFIG on ESP32
|
|
#endif
|
|
|
|
|
|
#ifdef DISPLAY_TOUCH_TOGGLE
|
|
#define TOUCH_POLL_MILLIS 50
|
|
#endif
|
|
|
|
// Wrap-safe deadline test. `millis() >= deadline` fires early for the whole
|
|
// interval before a rollover, because the deadline has already wrapped to a
|
|
// small value while millis() is still near UINT32_MAX; the signed difference
|
|
// stays correct across it.
|
|
static inline bool millisReached(unsigned long now, unsigned long deadline) {
|
|
return (int32_t)((uint32_t)now - (uint32_t)deadline) >= 0;
|
|
}
|
|
|
|
#if defined(HELTEC_V4_R8_TFT) && defined(HELTEC_TFT_RUNTIME_ROTATION) && HELTEC_TFT_RUNTIME_ROTATION == 1
|
|
#include <helpers/ui/DisplayRotationSettings.h>
|
|
|
|
void UITask::applyDisplayRotation() {
|
|
if (_display == nullptr || !_display->supportsRotation()) return;
|
|
const uint16_t degrees = mesh::ui::displayRotationDegrees();
|
|
if (degrees == _rotation_seen || !_display->setRotationDegrees(degrees)) return;
|
|
_rotation_seen = degrees;
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
_frame_valid = false;
|
|
#endif
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
_rows_valid = false;
|
|
#endif
|
|
_next_refresh = millis();
|
|
}
|
|
#endif
|
|
|
|
// Applies `display.flip` when it changes, forcing a complete repaint because
|
|
// the panel's existing contents are now the wrong way up.
|
|
void UITask::applyDisplayFlip() {
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
if (_observer_prefs == NULL || _observer_prefs->display_flip == _flip_seen) return;
|
|
_flip_seen = _observer_prefs->display_flip;
|
|
_display->setFlipped(_flip_seen != 0);
|
|
// Logged unconditionally: this is persisted config, so it survives a reflash
|
|
// and is otherwise invisible when someone is chasing a wrong orientation.
|
|
mesh::usbConsolePort().printf("Display: flip %s\n", _flip_seen ? "on (rotated 180)" : "off");
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
_frame_valid = false;
|
|
#endif
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
_rows_valid = false;
|
|
#endif
|
|
_next_refresh = 0;
|
|
#endif
|
|
}
|
|
|
|
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
|
|
|
|
#define POWEROFF_DELAY 3000
|
|
|
|
// '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,
|
|
};
|
|
|
|
void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) {
|
|
_prevBtnState = HIGH;
|
|
_started_at = millis();
|
|
_startup_complete = false;
|
|
_node_prefs = node_prefs;
|
|
resetRadioProfileDisplayPage();
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
ObserverDashboard::applyDarkPalette(); // retunes UIColor for this target only
|
|
#endif
|
|
#if defined(HELTEC_V4_R8_TFT) && defined(HELTEC_TFT_RUNTIME_ROTATION) && HELTEC_TFT_RUNTIME_ROTATION == 1
|
|
applyDisplayRotation();
|
|
#endif
|
|
_display->servicePower(board.isExternalPowered() || board.isUsbHostConnected());
|
|
applyDisplayFlip();
|
|
#ifdef DISPLAY_TOUCH_TOGGLE
|
|
_touch.begin();
|
|
#endif
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
_frame_valid = false;
|
|
#endif
|
|
|
|
#if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS)
|
|
user_btn.begin();
|
|
#endif
|
|
|
|
// strip off dash and commit hash without allocating a mutable version copy
|
|
// e.g: v1.2.3-abcdef -> v1.2.3
|
|
size_t version_len = 0;
|
|
while (version_len < sizeof(_version_info) - 1
|
|
&& firmware_version[version_len] && firmware_version[version_len] != '-') {
|
|
++version_len;
|
|
}
|
|
|
|
// v1.2.3 (1 Jan 2025)
|
|
snprintf(_version_info, sizeof(_version_info), "%.*s (%s)",
|
|
static_cast<int>(version_len), firmware_version, build_date);
|
|
}
|
|
|
|
void UITask::resetRadioProfileDisplayPage() {
|
|
_radio_profile_display_page = 0;
|
|
_dual_radio_enabled_seen = the_mesh.isDualRadioActive();
|
|
}
|
|
|
|
uint8_t UITask::currentRadioProfileDisplayPage() {
|
|
const bool dual_radio_enabled = the_mesh.isDualRadioActive();
|
|
if (dual_radio_enabled != _dual_radio_enabled_seen) {
|
|
_dual_radio_enabled_seen = dual_radio_enabled;
|
|
_radio_profile_display_page = 0;
|
|
}
|
|
return mesh::ui::radioProfileManualPageIndex(dual_radio_enabled,
|
|
radioProfileSystemStatusPageCount(), _radio_profile_display_page);
|
|
}
|
|
|
|
void UITask::advanceRadioProfileDisplayPage() {
|
|
const bool dual_radio_enabled = the_mesh.isDualRadioActive();
|
|
const uint8_t page_count = mesh::ui::radioProfileDisplayPageCount(
|
|
dual_radio_enabled, radioProfileSystemStatusPageCount());
|
|
_radio_profile_display_page = (currentRadioProfileDisplayPage() + 1) % page_count;
|
|
_next_refresh = 0;
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
_frame_valid = false;
|
|
#endif
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
_rows_valid = false;
|
|
#endif
|
|
}
|
|
|
|
bool UITask::showingSecondaryRadioProfilePage() {
|
|
return mesh::ui::showManualSecondaryRadioProfilePage(the_mesh.isDualRadioActive(),
|
|
currentRadioProfileDisplayPage());
|
|
}
|
|
|
|
uint8_t UITask::radioProfileSystemStatusPageCount() const {
|
|
#if defined(HELTEC_T096)
|
|
// The T096's dedicated 50px status column already shows every switch.
|
|
return 0;
|
|
#else
|
|
_display->setTextSize(1);
|
|
return radioSystemStatusPageCount(*_display, the_mesh.isDualRadioActive());
|
|
#endif
|
|
}
|
|
|
|
bool UITask::showingRadioProfileSystemStatusPage(uint8_t* status_page_index) {
|
|
return mesh::ui::showManualRadioProfileSystemStatusPage(the_mesh.isDualRadioActive(),
|
|
radioProfileSystemStatusPageCount(), currentRadioProfileDisplayPage(),
|
|
status_page_index);
|
|
}
|
|
|
|
void UITask::renderCurrScreen() {
|
|
char tmp[80];
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
_rows_valid = false;
|
|
#endif
|
|
if (!_startup_complete && (uint32_t)(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);
|
|
} else if (_powering_off_at > 0) {
|
|
// 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);
|
|
|
|
// Powering off
|
|
const char* poweroff_string = "Turning OFF";
|
|
uint16_t poffWidth = _display->getTextWidth(poweroff_string);
|
|
_display->setCursor((_display->width() - poffWidth) / 2, 48);
|
|
_display->drawTextCentered(_display->width()/2, 48, poweroff_string);
|
|
} else { // home screen
|
|
#ifdef WITH_WEBCONFIG
|
|
if (WebConfigServer::isRebootPending()) {
|
|
// save confirmed on-device: show ground truth even if the browser
|
|
// lost its connection before the confirmation reached it
|
|
_display->setTextSize(1);
|
|
_display->setColor(UIColor::corp_blue);
|
|
_display->setCursor(0, 14);
|
|
_display->print("Config saved!");
|
|
_display->setColor(UIColor::primary_txt);
|
|
_display->setCursor(0, 30);
|
|
_display->print("Rebooting...");
|
|
return;
|
|
}
|
|
char wc_ssid[33], wc_ip[16];
|
|
if (WebConfigServer::getSetupInfo(wc_ssid, sizeof(wc_ssid), wc_ip, sizeof(wc_ip))) {
|
|
// setup portal active: show join instructions instead of the home screen
|
|
if (mesh::ui::drawWiFiSetupQr(*_display, wc_ssid, wc_ip)) {
|
|
return;
|
|
}
|
|
_display->setTextSize(1);
|
|
_display->setColor(UIColor::corp_blue);
|
|
_display->setCursor(0, 0);
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
_display->print("Observer WiFi Setup");
|
|
#else
|
|
_display->print("WebUI WiFi Setup");
|
|
#endif
|
|
|
|
_display->setColor(UIColor::primary_txt);
|
|
_display->setCursor(0, 14);
|
|
_display->print("Join WiFi:");
|
|
_display->setColor(UIColor::warning_txt);
|
|
_display->setCursor(6, 24);
|
|
_display->print(wc_ssid);
|
|
|
|
_display->setColor(UIColor::primary_txt);
|
|
_display->setCursor(0, 40);
|
|
_display->print("Then browse to:");
|
|
_display->setColor(UIColor::warning_txt);
|
|
_display->setCursor(6, 50);
|
|
_display->print(wc_ip);
|
|
return;
|
|
}
|
|
#endif
|
|
uint8_t status_page_index = 0;
|
|
if (showingRadioProfileSystemStatusPage(&status_page_index)) {
|
|
drawRadioSystemStatusPage(*_display, *_node_prefs, status_page_index);
|
|
return;
|
|
}
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
renderDashboard();
|
|
return;
|
|
#endif
|
|
// Reserve a full measured font-height for each row on OLED/TFT/e-paper.
|
|
#if defined(HELTEC_T096)
|
|
// Never let normal full-size rows overwrite the dedicated normal-font
|
|
// column at the right of the native 160x80 T096 screen.
|
|
const int content_width = _display->width() - T096_STATUS_PANEL_WIDTH
|
|
- T096_STATUS_RIGHT_MARGIN;
|
|
#else
|
|
const int content_width = _display->width();
|
|
#endif
|
|
_display->setTextSize(1);
|
|
_display->setColor(UIColor::primary_txt);
|
|
#if defined(HELTEC_T096)
|
|
// The name gets the entire top row. The status strip begins below it, so
|
|
// no part of the name is covered or forced into a second line.
|
|
_display->drawTextEllipsized(0, 0, _display->width(), _node_prefs->node_name);
|
|
const int rows_top = T096_STATUS_TOP;
|
|
mesh::ui::BoundedTextRows rows(*_display,
|
|
{0, rows_top, content_width, _display->height() - rows_top});
|
|
#else
|
|
mesh::ui::BoundedTextRows rows(*_display,
|
|
{0, 0, content_width, _display->height()});
|
|
rows.draw(_node_prefs->node_name, false);
|
|
#endif
|
|
|
|
const mesh::RadioProfiles* profiles = configuredRadioProfiles();
|
|
const bool secondary_page = showingSecondaryRadioProfilePage();
|
|
const bool dual_radio = profiles != NULL && profiles->enabled();
|
|
float freq = _node_prefs->freq;
|
|
float bw = _node_prefs->bw;
|
|
uint8_t sf = _node_prefs->sf;
|
|
uint8_t cr = _node_prefs->cr;
|
|
const char* profile_tag = "";
|
|
if (profiles != NULL) {
|
|
const uint8_t profile = dual_radio && secondary_page ? 1 : 0;
|
|
const auto& params = profiles->params(profile);
|
|
freq = params.freq;
|
|
bw = params.bw;
|
|
sf = params.sf;
|
|
cr = params.cr;
|
|
if (dual_radio) profile_tag = mesh::ui::radioProfileDisplayTag(profile,
|
|
profile == 1 ? profiles->secondary_temporary : profiles->primary_temporary);
|
|
}
|
|
|
|
// T096's normal 6x8 right column needs compact radio labels. Other
|
|
// displays retain the longer labels for their roomier layouts.
|
|
#if defined(HELTEC_T096)
|
|
snprintf(tmp, sizeof(tmp), "%s%sF:%06.3f S:%d", profile_tag,
|
|
dual_radio ? " " : "", freq, sf);
|
|
#else
|
|
if (dual_radio) {
|
|
// R1/R2/T1/T2 needs three extra cells. Compact fields retain every RF
|
|
// value on a 128px OLED even at the widest legal frequency/SF pair.
|
|
snprintf(tmp, sizeof(tmp), "%s F:%06.3f S:%d", profile_tag, freq, sf);
|
|
} else {
|
|
snprintf(tmp, sizeof(tmp), "FREQ: %06.3f SF%d", freq, sf);
|
|
}
|
|
#endif
|
|
rows.draw(tmp, false);
|
|
|
|
// bandwidth / coding rate
|
|
#if defined(HELTEC_T096)
|
|
snprintf(tmp, sizeof(tmp), "%s%sB:%03.2f C:%d", profile_tag,
|
|
dual_radio ? " " : "", bw, cr);
|
|
#else
|
|
if (dual_radio) {
|
|
snprintf(tmp, sizeof(tmp), "%s B:%03.2f C:%d", profile_tag, bw, cr);
|
|
} else {
|
|
snprintf(tmp, sizeof(tmp), "BW: %03.2f CR: %d", bw, cr);
|
|
}
|
|
#endif
|
|
rows.draw(tmp, false);
|
|
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
// Display IP address for MQTT bridge devices
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
IPAddress ip = WiFi.localIP();
|
|
_display->setColor(UIColor::primary_txt);
|
|
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
|
rows.draw(tmp, false);
|
|
} else
|
|
#endif
|
|
{
|
|
snprintf(tmp, sizeof(tmp), "BAT: %.2fV", _board->getBattMilliVolts() / 1000.0f);
|
|
rows.draw(tmp, false);
|
|
}
|
|
|
|
// Keep power-saving state visible even when the MQTT IP replaces battery.
|
|
#if defined(HELTEC_T096)
|
|
// This is the fourth left-column row (after F, B, and battery/IP). Keep
|
|
// its label green, with just the saved ON/OFF value carrying the state
|
|
// color used by the status strip.
|
|
const int power_saving_y = rows.nextY();
|
|
if (rows.reserve()) {
|
|
drawT096State(*_display, 0, power_saving_y, "PS: ",
|
|
_node_prefs->powersaving_enabled != 0);
|
|
}
|
|
const int identity_y = rows.nextY();
|
|
if (rows.reserve()) {
|
|
char identity_prefix[7];
|
|
mesh::Utils::toHex(identity_prefix, the_mesh.getSelfId().pub_key, 3);
|
|
_display->setColor(UIColor::primary_txt);
|
|
_display->setCursor(0, identity_y);
|
|
_display->print("ID:");
|
|
_display->print(identity_prefix);
|
|
}
|
|
drawT096Radio2Details(*_display, rows.nextY(),
|
|
rows.nextY() + _display->textLineHeight() + 2);
|
|
#else
|
|
snprintf(tmp, sizeof(tmp), "PS: %s", _node_prefs->powersaving_enabled ? "ON" : "OFF");
|
|
rows.draw(tmp, false);
|
|
#endif
|
|
#if defined(HELTEC_T096)
|
|
drawT096StatusPanel(*_display, *_node_prefs, secondary_page ? 1 : 0);
|
|
#else
|
|
// The shared radio-profile layer reports this on every repeater board.
|
|
// BoundedTextRows simply omits this extra row on a screen too short to
|
|
// hold it, rather than overwriting an existing status line.
|
|
if (the_mesh.isDualRadioActive()) rows.draw("DualRadio: ON", false);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
uint32_t UITask::getFrameSignature() {
|
|
uint32_t signature = DisplayFrameSignature::INITIAL;
|
|
char tmp[80];
|
|
|
|
if (!_startup_complete && (uint32_t)(millis() - _started_at) < BOOT_SCREEN_MILLIS) {
|
|
signature = DisplayFrameSignature::append(signature, "boot");
|
|
return DisplayFrameSignature::append(signature, _version_info);
|
|
}
|
|
|
|
if (_powering_off_at > 0) {
|
|
return DisplayFrameSignature::append(signature, "powering-off");
|
|
}
|
|
|
|
#ifdef WITH_WEBCONFIG
|
|
if (WebConfigServer::isRebootPending()) {
|
|
return DisplayFrameSignature::append(signature, "rebooting");
|
|
}
|
|
|
|
char wc_ssid[33], wc_ip[16];
|
|
if (WebConfigServer::getSetupInfo(wc_ssid, sizeof(wc_ssid), wc_ip, sizeof(wc_ip))) {
|
|
signature = DisplayFrameSignature::append(signature, "setup");
|
|
signature = DisplayFrameSignature::append(signature, wc_ssid);
|
|
return DisplayFrameSignature::append(signature, wc_ip);
|
|
}
|
|
#endif
|
|
|
|
signature = DisplayFrameSignature::append(signature, "home");
|
|
signature = DisplayFrameSignature::append(signature, _node_prefs->node_name);
|
|
uint8_t status_page_index = 0;
|
|
if (showingRadioProfileSystemStatusPage(&status_page_index)) {
|
|
signature = DisplayFrameSignature::append(signature, "radio-system-status");
|
|
snprintf(tmp, sizeof(tmp), "status-page-%u", status_page_index);
|
|
signature = DisplayFrameSignature::append(signature, tmp);
|
|
signature = DisplayFrameSignature::append(
|
|
signature, _node_prefs->powersaving_enabled ? "powersaving-on" : "powersaving-off");
|
|
signature = DisplayFrameSignature::append(
|
|
signature, _node_prefs->gps_enabled ? "gps-on" : "gps-off");
|
|
signature = DisplayFrameSignature::append(
|
|
signature, _board->canControlLoRaFemLna() && _board->isLoRaFemLnaEnabled()
|
|
? "fem-on" : "fem-off");
|
|
signature = DisplayFrameSignature::append(
|
|
signature, _node_prefs->rx_boosted_gain ? "rxb-on" : "rxb-off");
|
|
signature = DisplayFrameSignature::append(
|
|
signature, _node_prefs->rx_powersaving_enabled ? "rxps-on" : "rxps-off");
|
|
signature = DisplayFrameSignature::append(
|
|
signature, _node_prefs->cad_enabled ? "cad-on" : "cad-off");
|
|
const mesh::RadioProfiles* status_profiles = configuredRadioProfiles();
|
|
if (status_profiles != NULL && status_profiles->enabled()) {
|
|
formatRadioNoiseFloor(tmp, sizeof(tmp), "N1", 0);
|
|
signature = DisplayFrameSignature::append(signature, tmp);
|
|
formatRadioNoiseFloor(tmp, sizeof(tmp), "N2", 1);
|
|
signature = DisplayFrameSignature::append(signature, tmp);
|
|
signature = DisplayFrameSignature::append(signature,
|
|
status_profiles->secondary.mode == mesh::RadioProfileMode::RxTx
|
|
? "radio2-rxtx" : "radio2-rx");
|
|
signature = DisplayFrameSignature::append(signature,
|
|
status_profiles->cross == mesh::RadioCrossMode::On ? "cross-on"
|
|
: status_profiles->cross == mesh::RadioCrossMode::Off
|
|
? "cross-off" : "cross-auto");
|
|
}
|
|
return signature;
|
|
}
|
|
const mesh::RadioProfiles* profiles = configuredRadioProfiles();
|
|
const bool secondary_page = showingSecondaryRadioProfilePage();
|
|
const bool dual_radio = profiles != NULL && profiles->enabled();
|
|
float freq = _node_prefs->freq;
|
|
float bw = _node_prefs->bw;
|
|
uint8_t sf = _node_prefs->sf;
|
|
uint8_t cr = _node_prefs->cr;
|
|
if (profiles != NULL) {
|
|
const uint8_t profile = dual_radio && secondary_page ? 1 : 0;
|
|
const auto& params = profiles->params(profile);
|
|
freq = params.freq;
|
|
bw = params.bw;
|
|
sf = params.sf;
|
|
cr = params.cr;
|
|
signature = DisplayFrameSignature::append(signature,
|
|
mesh::ui::radioProfileDisplayTag(profile,
|
|
profile == 1 ? profiles->secondary_temporary : profiles->primary_temporary));
|
|
}
|
|
snprintf(tmp, sizeof(tmp), "FREQ: %06.3f SF%d", freq, sf);
|
|
signature = DisplayFrameSignature::append(signature, tmp);
|
|
snprintf(tmp, sizeof(tmp), "BW: %03.2f CR: %d", bw, cr);
|
|
signature = DisplayFrameSignature::append(signature, tmp);
|
|
|
|
#if defined(WITH_MQTT_BRIDGE) && !defined(DISPLAY_ACTIVITY_DASHBOARD)
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
IPAddress ip = WiFi.localIP();
|
|
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
|
signature = DisplayFrameSignature::append(signature, tmp);
|
|
} else {
|
|
signature = DisplayFrameSignature::append(signature, "wifi-disconnected");
|
|
}
|
|
#endif
|
|
|
|
#ifndef DISPLAY_ACTIVITY_DASHBOARD
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
if (WiFi.status() != WL_CONNECTED) {
|
|
snprintf(tmp, sizeof(tmp), "BAT: %.2fV", _board->getBattMilliVolts() / 1000.0f);
|
|
signature = DisplayFrameSignature::append(signature, tmp);
|
|
}
|
|
#else
|
|
snprintf(tmp, sizeof(tmp), "BAT: %.2fV", _board->getBattMilliVolts() / 1000.0f);
|
|
signature = DisplayFrameSignature::append(signature, tmp);
|
|
#endif
|
|
signature = DisplayFrameSignature::append(
|
|
signature, _node_prefs->powersaving_enabled ? "powersaving-on" : "powersaving-off");
|
|
#if defined(HELTEC_T096)
|
|
signature = DisplayFrameSignature::append(
|
|
signature, _node_prefs->gps_enabled ? "gps-on" : "gps-off");
|
|
signature = DisplayFrameSignature::append(
|
|
signature, _board->canControlLoRaFemLna() && _board->isLoRaFemLnaEnabled()
|
|
? "fem-on" : "fem-off");
|
|
signature = DisplayFrameSignature::append(
|
|
signature, _node_prefs->rx_boosted_gain ? "rxb-on" : "rxb-off");
|
|
signature = DisplayFrameSignature::append(
|
|
signature, _node_prefs->rx_powersaving_enabled ? "rxps-on" : "rxps-off");
|
|
signature = DisplayFrameSignature::append(
|
|
signature, _node_prefs->cad_enabled ? "cad-on" : "cad-off");
|
|
formatRadioNoiseFloor(tmp, sizeof(tmp), dual_radio
|
|
? (secondary_page ? "N2" : "N1") : "N", secondary_page ? 1 : 0);
|
|
signature = DisplayFrameSignature::append(signature, tmp);
|
|
#endif
|
|
signature = DisplayFrameSignature::append(
|
|
signature, the_mesh.isDualRadioActive() ? "radio2-on" : "radio2-off");
|
|
#endif
|
|
|
|
return signature;
|
|
}
|
|
#endif
|
|
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
#define ACTIVITY_REFRESH_MILLIS 5000
|
|
|
|
bool UITask::buildDashboardContext(ObserverDashboard::Context* ctx) {
|
|
if (_node_prefs == NULL) return false;
|
|
ctx->node_name = _node_prefs->node_name;
|
|
ctx->role_label = "REPEATER";
|
|
ctx->freq = _node_prefs->freq;
|
|
ctx->sf = _node_prefs->sf;
|
|
ctx->bw = _node_prefs->bw;
|
|
ctx->radio_label = "";
|
|
const mesh::RadioProfiles* profiles = configuredRadioProfiles();
|
|
if (profiles != NULL) {
|
|
const uint8_t profile = profiles->enabled() && showingSecondaryRadioProfilePage() ? 1 : 0;
|
|
const auto& params = profiles->params(profile);
|
|
ctx->freq = params.freq;
|
|
ctx->sf = params.sf;
|
|
ctx->bw = params.bw;
|
|
if (profiles->enabled()) ctx->radio_label = mesh::ui::radioProfileDisplayTag(profile,
|
|
profile == 1 ? profiles->secondary_temporary : profiles->primary_temporary);
|
|
}
|
|
ctx->dual_radio = the_mesh.isDualRadioActive();
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
ctx->link_up = (WiFi.status() == WL_CONNECTED);
|
|
#else
|
|
ctx->link_up = false;
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
void UITask::renderDashboard() {
|
|
ObserverDashboard::Context ctx;
|
|
if (!buildDashboardContext(&ctx)) return;
|
|
|
|
RadioActivitySnapshot snap;
|
|
if (_activity) {
|
|
_activity->snapshot(millis(), &snap);
|
|
} else {
|
|
memset(&snap, 0, sizeof(snap));
|
|
}
|
|
|
|
const ObserverDashboard::Layout& layout = ObserverDashboard::activeLayout(*_display);
|
|
ObserverDashboard::drawFull(*_display, layout, ctx, snap);
|
|
ObserverDashboard::allRowSignatures(layout, ctx, snap, _row_signatures);
|
|
_rows_valid = true;
|
|
_next_activity = millis() + ACTIVITY_REFRESH_MILLIS;
|
|
}
|
|
|
|
// Repaints just the analytics rows whose contents moved. No startFrame(), so
|
|
// the header, the radio strip and the rest of the panel are never cleared.
|
|
void UITask::updateActivityRows() {
|
|
if (!_rows_valid || _activity == NULL) return; // not showing the dashboard
|
|
|
|
ObserverDashboard::Context ctx;
|
|
if (!buildDashboardContext(&ctx)) return;
|
|
|
|
RadioActivitySnapshot snap;
|
|
_activity->snapshot(millis(), &snap);
|
|
ObserverDashboard::drawChangedRows(*_display, ObserverDashboard::activeLayout(*_display), ctx, snap,
|
|
_row_signatures);
|
|
}
|
|
#endif
|
|
|
|
#ifdef DISPLAY_TOUCH_TOGGLE
|
|
void UITask::toggleDisplay(const char* source) {
|
|
_display->wake(mesh::ui::DisplayWake::Button);
|
|
resetRadioProfileDisplayPage();
|
|
#ifdef DISPLAY_TOUCH_DEBUG
|
|
mesh::usbConsolePort().printf("Display: %s -> %s\n", source, _display->isOn() ? "on" : "off");
|
|
#else
|
|
(void)source;
|
|
#endif
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
_frame_valid = false; // wake draws one complete current frame
|
|
#endif
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
_rows_valid = false;
|
|
#endif
|
|
_next_refresh = 0; // redraw at once rather than showing the stale frame
|
|
}
|
|
#endif
|
|
|
|
void UITask::loop() {
|
|
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
|
static uint32_t display_generation = 0;
|
|
if (display_generation != display.generation()) {
|
|
display_generation = display.generation();
|
|
_next_refresh = millis();
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
_frame_valid = false;
|
|
#endif
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
_rows_valid = false;
|
|
#endif
|
|
}
|
|
#endif
|
|
#if defined(HELTEC_V4_R8_TFT) && defined(HELTEC_TFT_RUNTIME_ROTATION) && HELTEC_TFT_RUNTIME_ROTATION == 1
|
|
applyDisplayRotation();
|
|
#endif
|
|
// Observe expiry outside display/Morse/input gates; startup is one-shot.
|
|
if (!_startup_complete && (uint32_t)(millis() - _started_at) >= BOOT_SCREEN_MILLIS) {
|
|
_startup_complete = true;
|
|
}
|
|
const bool morse_timing = mesh::morseTimingActive(the_mesh.getProfileRadio());
|
|
#ifdef MESHCORE_HAS_COLOR_DISPLAY
|
|
const bool screen_was_on = _display->isOn();
|
|
#endif
|
|
if (_display->servicePower(board.isExternalPowered() || board.isUsbHostConnected())) {
|
|
if (_display->isOn()
|
|
#ifdef MESHCORE_HAS_COLOR_DISPLAY
|
|
&& !screen_was_on
|
|
#endif
|
|
) resetRadioProfileDisplayPage();
|
|
_next_refresh = 0;
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
_frame_valid = false;
|
|
#endif
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
_rows_valid = false;
|
|
#endif
|
|
}
|
|
#if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS)
|
|
int ev = user_btn.check();
|
|
// Multiclick stays enabled on the R8 observer so triple-click can toggle
|
|
// WebConfig. Collapse a completed double-click into one display action
|
|
// instead of silently consuming both presses.
|
|
if (ev == BUTTON_EVENT_CLICK || ev == BUTTON_EVENT_DOUBLE_CLICK) {
|
|
// A page change is still user activity. Capture the old state first so
|
|
// the first press wakes to R1, while every subsequent press both extends
|
|
// the 15-second timer and advances to the next available detail page.
|
|
const bool display_was_on = _display->isOn();
|
|
_display->wake(mesh::ui::DisplayWake::Button);
|
|
if (display_was_on) {
|
|
advanceRadioProfileDisplayPage();
|
|
} else {
|
|
resetRadioProfileDisplayPage();
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
_frame_valid = false;
|
|
#endif
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
_rows_valid = false;
|
|
#endif
|
|
}
|
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
|
_display->wake(mesh::ui::DisplayWake::Button);
|
|
resetRadioProfileDisplayPage();
|
|
mesh::usbConsolePort().printf("Powering Off\r\n");
|
|
_powering_off_at = millis() + POWEROFF_DELAY;
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
_frame_valid = false;
|
|
#endif
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
_rows_valid = false;
|
|
#endif
|
|
_next_refresh = 0;
|
|
#ifdef WITH_WEBCONFIG
|
|
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
|
// Preserve the fork's persistent WebUI toggle. R8 observer repeaters keep
|
|
// multiclick enabled specifically for this action.
|
|
WebConfigServer::requestToggleFromButton();
|
|
_display->wake(mesh::ui::DisplayWake::Button);
|
|
resetRadioProfileDisplayPage();
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
#ifdef DISPLAY_TOUCH_TOGGLE
|
|
{
|
|
unsigned long now = millis();
|
|
if (millisReached(now, _next_touch)) {
|
|
_next_touch = now + TOUCH_POLL_MILLIS;
|
|
if (_powering_off_at == 0 && _touch.checkTap(now)) toggleDisplay("touch");
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (!morse_timing && _display->isOn()) {
|
|
// Apply a live orientation change before drawing, including the first frame
|
|
// after a preference was changed while the panel was blanked.
|
|
applyDisplayFlip();
|
|
if (millisReached(millis(), _next_refresh)) {
|
|
bool redraw = true;
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
uint32_t frame_signature = getFrameSignature();
|
|
redraw = !_frame_valid || frame_signature != _last_frame_signature;
|
|
#endif
|
|
if (redraw) {
|
|
_display->startFrame();
|
|
renderCurrScreen();
|
|
_display->endFrame();
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
_last_frame_signature = frame_signature;
|
|
_frame_valid = true;
|
|
#endif
|
|
}
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
else if (millisReached(millis(), _next_activity)) {
|
|
updateActivityRows();
|
|
_next_activity = millis() + ACTIVITY_REFRESH_MILLIS;
|
|
}
|
|
#endif
|
|
|
|
unsigned long refresh_interval = 1000; // check normal status changes every second
|
|
#if defined(HELTEC_T096)
|
|
// The T096's TFT can show each tenth while the short, 3.2-second noise
|
|
// floor collection window is active. Keep every other screen at the
|
|
// normal cadence, particularly displays with slow/expensive refreshes.
|
|
if (radio_driver.getNoiseFloorCalibrationSecondsRemaining() > 0.0f) {
|
|
refresh_interval = 100;
|
|
}
|
|
#endif
|
|
_next_refresh = millis() + refresh_interval;
|
|
}
|
|
}
|
|
|
|
if (_powering_off_at > 0) { // power off timer armed
|
|
#if defined(LED_PIN) && defined(LED_STATE_ON)
|
|
if (mesh::leds::enabled()) digitalWrite(LED_PIN, LED_STATE_ON);
|
|
delay(1000);
|
|
digitalWrite(LED_PIN, !LED_STATE_ON); // off LED
|
|
#endif
|
|
if (millisReached(millis(), _powering_off_at)) {
|
|
_board->powerOff(); // should not return
|
|
}
|
|
}
|
|
}
|