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.
671 lines
23 KiB
C++
671 lines
23 KiB
C++
#include "UITask.h"
|
|
#include <helpers/MorseLoopPolicy.h>
|
|
#include "target.h"
|
|
#include <Arduino.h>
|
|
#include <helpers/UsbLogging.h>
|
|
#include <helpers/CommonCLI.h>
|
|
#include <helpers/ui/WiFiSetupQrDisplay.h>
|
|
#include <helpers/ui/RadioProfileDisplayPage.h>
|
|
#include <helpers/ui/RadioProfileSystemStatus.h>
|
|
#include <RadioProfiles.h>
|
|
#include "MyMesh.h"
|
|
|
|
extern MyMesh the_mesh;
|
|
|
|
namespace {
|
|
|
|
const mesh::RadioProfiles* configuredRadioProfiles() {
|
|
const auto* radio = the_mesh.getProfileRadio();
|
|
return radio ? radio->profiles() : NULL;
|
|
}
|
|
|
|
mesh::ui::RadioProfileSystemStatus radioProfileSystemStatus(
|
|
const NodePrefs& prefs) {
|
|
mesh::ui::RadioProfileSystemStatus status;
|
|
const auto* profiles = configuredRadioProfiles();
|
|
status.public_key = the_mesh.getSelfId().pub_key;
|
|
status.powersaving_enabled = prefs.powersaving_enabled != 0;
|
|
#if ENV_INCLUDE_GPS == 1
|
|
status.gps_enabled = prefs.gps_enabled != 0;
|
|
#endif
|
|
status.fem_enabled = board.canControlLoRaFemLna()
|
|
&& board.isLoRaFemLnaEnabled();
|
|
status.rx_boosted_gain = prefs.rx_boosted_gain != 0;
|
|
status.rx_powersaving_enabled = prefs.rx_powersaving_enabled != 0;
|
|
status.cad_enabled = prefs.cad_enabled != 0;
|
|
status.dual_radio_enabled = profiles != NULL && profiles->enabled();
|
|
if (status.dual_radio_enabled) {
|
|
status.secondary_temporary = profiles->secondary_temporary;
|
|
status.secondary_mode = profiles->secondary.mode;
|
|
status.cross = profiles->cross;
|
|
}
|
|
status.noise_floor_1 = radio_driver.getNoiseFloorDbm(0);
|
|
status.noise_floor_1_seconds =
|
|
radio_driver.getNoiseFloorCalibrationSecondsRemaining(0);
|
|
if (status.dual_radio_enabled) {
|
|
status.noise_floor_2 = radio_driver.getNoiseFloorDbm(1);
|
|
status.noise_floor_2_seconds =
|
|
radio_driver.getNoiseFloorCalibrationSecondsRemaining(1);
|
|
}
|
|
return status;
|
|
}
|
|
|
|
uint8_t radioProfileStatusPageCount(DisplayDriver& display,
|
|
bool dual_radio_enabled) {
|
|
display.setTextSize(1);
|
|
return mesh::ui::radioProfileSystemStatusPageCount(display,
|
|
dual_radio_enabled);
|
|
}
|
|
|
|
} // 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
|
|
|
|
#ifdef DISPLAY_TOUCH_TOGGLE
|
|
#define POWEROFF_DELAY 3000
|
|
#endif
|
|
|
|
// '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
|
|
#if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS) \
|
|
&& (defined(DISPLAY_TOUCH_TOGGLE) \
|
|
|| (defined(MOMENTARY_BUTTON_WAKE_FROM_SLEEP) \
|
|
&& MOMENTARY_BUTTON_WAKE_FROM_SLEEP))
|
|
user_btn.begin();
|
|
#endif
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
_frame_valid = false;
|
|
#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_page_started_at = millis();
|
|
_dual_radio_enabled_seen = the_mesh.isDualRadioActive();
|
|
}
|
|
|
|
bool UITask::showingSecondaryRadioProfilePage() {
|
|
const bool dual_radio_enabled = the_mesh.isDualRadioActive();
|
|
const uint32_t now = millis();
|
|
if (dual_radio_enabled != _dual_radio_enabled_seen) {
|
|
_dual_radio_enabled_seen = dual_radio_enabled;
|
|
_radio_profile_page_started_at = now;
|
|
}
|
|
const uint8_t status_pages = radioProfileStatusPageCount(*_display,
|
|
dual_radio_enabled);
|
|
return mesh::ui::showSecondaryRadioProfilePage(dual_radio_enabled,
|
|
status_pages,
|
|
now - _radio_profile_page_started_at);
|
|
}
|
|
|
|
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);
|
|
uint16_t websiteWidth = _display->getTextWidth(website);
|
|
_display->setCursor((_display->width() - websiteWidth) / 2, 22);
|
|
_display->print(website);
|
|
|
|
// version info
|
|
_display->setTextSize(1);
|
|
uint16_t versionWidth = _display->getTextWidth(_version_info);
|
|
_display->setCursor((_display->width() - versionWidth) / 2, 35);
|
|
_display->print(_version_info);
|
|
|
|
// node type
|
|
const char* node_type = "< Room Server >";
|
|
uint16_t typeWidth = _display->getTextWidth(node_type);
|
|
_display->setCursor((_display->width() - typeWidth) / 2, 48);
|
|
_display->print(node_type);
|
|
#ifdef DISPLAY_TOUCH_TOGGLE
|
|
} else if (_powering_off_at > 0) {
|
|
_display->setColor(UIColor::corp_blue);
|
|
_display->drawXbm(0, 3, meshcore_logo, 128, 13);
|
|
_display->setTextSize(1);
|
|
_display->setColor(UIColor::primary_txt);
|
|
_display->drawTextCentered(_display->width() / 2, 48, "Turning OFF");
|
|
#endif
|
|
} 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
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
renderDashboard();
|
|
return;
|
|
#endif
|
|
const bool dual_radio_enabled = the_mesh.isDualRadioActive();
|
|
const uint8_t status_pages = radioProfileStatusPageCount(
|
|
*_display, dual_radio_enabled);
|
|
uint8_t status_page_index = 0;
|
|
if (mesh::ui::showRadioProfileSystemStatusPage(dual_radio_enabled,
|
|
status_pages, millis() - _radio_profile_page_started_at,
|
|
&status_page_index)) {
|
|
mesh::ui::drawRadioProfileSystemStatusPage(*_display,
|
|
radioProfileSystemStatus(*_node_prefs), status_page_index);
|
|
return;
|
|
}
|
|
// node name
|
|
_display->setCursor(0, 0);
|
|
_display->setTextSize(1);
|
|
_display->setColor(UIColor::primary_txt);
|
|
_display->print(_node_prefs->node_name);
|
|
|
|
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);
|
|
}
|
|
|
|
// Compact R1/R2 rows preserve every RF field on 128px displays.
|
|
_display->setCursor(0, 20);
|
|
if (dual_radio) {
|
|
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);
|
|
}
|
|
_display->print(tmp);
|
|
|
|
_display->setCursor(0, 30);
|
|
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);
|
|
}
|
|
_display->print(tmp);
|
|
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
// Display IP address for MQTT bridge devices
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
IPAddress ip = WiFi.localIP();
|
|
_display->setCursor(0, 40);
|
|
_display->setColor(UIColor::primary_txt);
|
|
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
|
_display->print(tmp);
|
|
}
|
|
#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);
|
|
}
|
|
|
|
#ifdef DISPLAY_TOUCH_TOGGLE
|
|
if (_powering_off_at > 0) {
|
|
return DisplayFrameSignature::append(signature, "powering-off");
|
|
}
|
|
#endif
|
|
|
|
#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);
|
|
const mesh::RadioProfiles* profiles = configuredRadioProfiles();
|
|
const bool dual_radio = profiles != NULL && profiles->enabled();
|
|
const uint8_t status_pages = radioProfileStatusPageCount(*_display,
|
|
dual_radio);
|
|
uint8_t status_page_index = 0;
|
|
if (mesh::ui::showRadioProfileSystemStatusPage(dual_radio, status_pages,
|
|
millis() - _radio_profile_page_started_at, &status_page_index)) {
|
|
snprintf(tmp, sizeof(tmp), "radio-status:%u:%lu", status_page_index,
|
|
(unsigned long)(millis() / 1000UL));
|
|
return DisplayFrameSignature::append(signature, tmp);
|
|
}
|
|
const bool secondary_page = showingSecondaryRadioProfilePage();
|
|
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
|
|
|
|
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 = "ROOM SERVER";
|
|
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())) {
|
|
#ifdef MESHCORE_HAS_COLOR_DISPLAY
|
|
if (screen_was_on != _display->isOn())
|
|
#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_TOUCH_TOGGLE)
|
|
int ev = user_btn.check();
|
|
if (ev == BUTTON_EVENT_CLICK) {
|
|
toggleDisplay("button");
|
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
|
_display->wake(mesh::ui::DisplayWake::Button);
|
|
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;
|
|
}
|
|
#elif defined(PIN_USER_BTN) \
|
|
&& defined(DISPLAY_CLASS) \
|
|
&& defined(MOMENTARY_BUTTON_WAKE_FROM_SLEEP) \
|
|
&& MOMENTARY_BUTTON_WAKE_FROM_SLEEP
|
|
// Sleep-capable targets must use the shared debounced button state
|
|
// machine. Raw 200 ms polling can go back to sleep after the GPIO edge and
|
|
// miss both a short press and its release.
|
|
int ev = user_btn.check();
|
|
if (ev != BUTTON_EVENT_NONE) {
|
|
if (!_display->isOn()) {
|
|
_display->wake(mesh::ui::DisplayWake::Button);
|
|
resetRadioProfileDisplayPage();
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
_frame_valid = false;
|
|
#endif
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
_rows_valid = false;
|
|
#endif
|
|
_next_refresh = 0;
|
|
}
|
|
_display->wake(mesh::ui::DisplayWake::Button);
|
|
}
|
|
#elif defined(PIN_USER_BTN)
|
|
if (millisReached(millis(), _next_read)) {
|
|
int btnState = digitalRead(PIN_USER_BTN);
|
|
if (btnState != _prevBtnState) {
|
|
if (btnState == USER_BTN_PRESSED) { // pressed?
|
|
#ifdef DISPLAY_TOUCH_TOGGLE
|
|
toggleDisplay("button"); // same action as tapping the panel
|
|
#else
|
|
if (_display->isOn()) {
|
|
// TODO: any action ?
|
|
} else {
|
|
_display->wake(mesh::ui::DisplayWake::Button);
|
|
resetRadioProfileDisplayPage();
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
_frame_valid = false;
|
|
#endif
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
_rows_valid = false;
|
|
#endif
|
|
}
|
|
_display->wake(mesh::ui::DisplayWake::Button);
|
|
#endif
|
|
}
|
|
_prevBtnState = btnState;
|
|
}
|
|
_next_read = millis() + 200; // 5 reads per second
|
|
}
|
|
#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
|
|
|
|
_next_refresh = millis() + 1000; // check for visible changes every second
|
|
}
|
|
}
|
|
|
|
#ifdef DISPLAY_TOUCH_TOGGLE
|
|
if (_powering_off_at > 0 && millisReached(millis(), _powering_off_at)) {
|
|
_board->powerOff(); // current board path performs the full radio/GPS shutdown
|
|
}
|
|
#endif
|
|
}
|