* Allow key verification to work for unknown nodes. * trunk * More reliable admin key decryption * Add admin key fallback tests * Actually check haveRemoteKey * Logging cleanup * Address review feedback - Persist the committed key + manually-verified flag in commitVerifiedRemoteNode via saveToDisk(SEGMENT_NODEDATABASE), replacing the "todo: initiate save" - Guard the CryptoEngine pending-key slot with a dedicated internal lock; the Router reads it while already holding the non-recursive cryptLock, so the accessors cannot reuse that lock - Draw the security number from the hardware RNG (CryptRNG fallback) under cryptLock instead of random(); on nRF52 the entropy fill toggles the same CC310 the BLE task's packet crypto uses - Return true after fully handling the hash2 response (restores develop behavior; consistent with the hash1 branch) - Take uint32_t in the number-picker callbacks so 8-digit hex nodenums can't truncate through int - Trim over-long comments flagged by review * feat(tests): add deterministic tests for admin session-key behavior --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2247 lines
84 KiB
C++
2247 lines
84 KiB
C++
/*
|
|
BaseUI
|
|
|
|
Developed and Maintained By:
|
|
- Ronald Garcia (HarukiToreda) - Lead development and implementation.
|
|
- JasonP (Xaositek) - Screen layout and icon design, UI improvements and testing.
|
|
- TonyG (Tropho) - Project management, structural planning, and testing
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
#include "Screen.h"
|
|
#include "NodeDB.h"
|
|
#include "PowerMon.h"
|
|
#include "Throttle.h"
|
|
#include "configuration.h"
|
|
#include "meshUtils.h"
|
|
#if HAS_SCREEN
|
|
#include "EInkParallelDisplay.h"
|
|
#include <OLEDDisplay.h>
|
|
|
|
#include "DisplayFormatters.h"
|
|
#include "TimeFormatters.h"
|
|
#include "draw/ClockRenderer.h"
|
|
#include "draw/DebugRenderer.h"
|
|
#include "draw/MenuHandler.h"
|
|
#include "draw/MessageRenderer.h"
|
|
#include "draw/NodeListRenderer.h"
|
|
#include "draw/NotificationRenderer.h"
|
|
#include "draw/UIRenderer.h"
|
|
#include "graphics/TFTColorRegions.h"
|
|
#include "modules/CannedMessageModule.h"
|
|
#include "security/LockdownDisplay.h"
|
|
|
|
#if !MESHTASTIC_EXCLUDE_GPS
|
|
#include "GPS.h"
|
|
#include "buzz.h"
|
|
#endif
|
|
#include "FSCommon.h"
|
|
#include "MeshService.h"
|
|
#include "MessageStore.h"
|
|
#include "RadioLibInterface.h"
|
|
#include "SPILock.h"
|
|
#include "error.h"
|
|
#include "gps/GeoCoord.h"
|
|
#include "gps/RTC.h"
|
|
#include "graphics/ScreenFonts.h"
|
|
#include "graphics/SharedUIDisplay.h"
|
|
#include "graphics/TFTPalette.h"
|
|
#include "graphics/emotes.h"
|
|
#include "graphics/images.h"
|
|
#include "input/TouchScreenImpl1.h"
|
|
#include "main.h"
|
|
#include "mesh-pb-constants.h"
|
|
#include "mesh/Channels.h"
|
|
#include "mesh/Default.h"
|
|
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
|
#include "modules/ExternalNotificationModule.h"
|
|
#include "modules/WaypointModule.h"
|
|
#include "sleep.h"
|
|
#include "target_specific.h"
|
|
extern MessageStore messageStore;
|
|
|
|
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
|
#include "mesh/wifi/WiFiAPClient.h"
|
|
#endif
|
|
|
|
#ifdef ARCH_ESP32
|
|
#endif
|
|
|
|
#if ARCH_PORTDUINO
|
|
#include "modules/StoreForwardModule.h"
|
|
#include "platform/portduino/PortduinoGlue.h"
|
|
#endif
|
|
|
|
#if defined(T_LORA_PAGER)
|
|
// KB backlight control
|
|
#include "input/cardKbI2cImpl.h"
|
|
#endif
|
|
|
|
using namespace meshtastic; /** @todo remove */
|
|
|
|
namespace graphics
|
|
{
|
|
|
|
// This means the *visible* area (sh1106 can address 132, but shows 128 for example)
|
|
#define IDLE_FRAMERATE 1 // in fps
|
|
#define COMPASS_ACTIVE_FRAMERATE 20
|
|
|
|
// DEBUG
|
|
#define NUM_EXTRA_FRAMES 3 // text message and debug frame
|
|
// if defined a pixel will blink to show redraws
|
|
// #define SHOW_REDRAWS
|
|
#define ASCII_BELL '\x07'
|
|
// A text message frame + debug frame + all the node infos
|
|
FrameCallback *normalFrames;
|
|
static uint32_t targetFramerate = IDLE_FRAMERATE;
|
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
|
static inline void prepareFrameColorRegions()
|
|
{
|
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
|
clearTFTColorRegions();
|
|
// Full-frame FrameMono inversion for themes that need it (e.g. light themes).
|
|
if (isThemeFullFrameInvert()) {
|
|
setAndRegisterTFTColorRole(TFTColorRole::FrameMono, getThemeBodyFg(), getThemeBodyBg(), 0, 0, screen->getWidth(),
|
|
screen->getHeight());
|
|
}
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
#ifdef MESHTASTIC_LOCKDOWN
|
|
// Static lock screen drawn in place of normal frames when
|
|
// meshtastic_security::shouldRedactDisplay() returns true. Renders centered
|
|
// "LOCKED" plus battery so the operator can see the device is alive and
|
|
// charged without leaking any node/channel/message/position content.
|
|
// Draw the LOCKED frame into the host-side framebuffer. Does NOT commit
|
|
// to the panel - the caller is responsible for calling display->display()
|
|
// once it has composited any overlays on top. Committing here would cause
|
|
// visible flicker between "just LOCKED" and "LOCKED + banner overlay" when
|
|
// the pairing-PIN special-case in updateUiFrame paints the overlay after
|
|
// this returns.
|
|
static void drawLockdownLockScreenIntoBuffer(OLEDDisplay *display)
|
|
{
|
|
display->clear();
|
|
|
|
const int w = display->getWidth();
|
|
const int h = display->getHeight();
|
|
|
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
|
display->setFont(FONT_LARGE);
|
|
display->drawString(w / 2, h / 2 - FONT_HEIGHT_LARGE, "LOCKED");
|
|
|
|
display->setFont(FONT_SMALL);
|
|
char status[32] = "Connect to unlock";
|
|
if (powerStatus && powerStatus->getHasBattery()) {
|
|
int pct = powerStatus->getBatteryChargePercent();
|
|
snprintf(status, sizeof(status), "Battery %d%%", pct);
|
|
}
|
|
display->drawString(w / 2, h / 2 + 2, status);
|
|
}
|
|
|
|
// Convenience wrapper for callers that want the LOCKED frame committed
|
|
// to the panel immediately and have no overlay to compose on top.
|
|
static void drawLockdownLockScreen(OLEDDisplay *display)
|
|
{
|
|
drawLockdownLockScreenIntoBuffer(display);
|
|
display->display();
|
|
}
|
|
#endif
|
|
|
|
static inline void updateUiFrame(OLEDDisplayUi *ui)
|
|
{
|
|
#ifdef MESHTASTIC_LOCKDOWN
|
|
if (meshtastic_security::shouldRedactDisplay() && screen != nullptr) {
|
|
OLEDDisplay *display = screen->getDisplayDevice();
|
|
// Paint LOCKED into the framebuffer WITHOUT committing. We commit
|
|
// exactly once at the bottom - after any overlay has been composed
|
|
// on top - so the panel never visibly transitions from "just LOCKED"
|
|
// to "LOCKED + overlay" mid-frame. Committing twice per cycle was
|
|
// the source of the H13 flicker.
|
|
drawLockdownLockScreenIntoBuffer(display);
|
|
// Special-case the BLE pairing PIN banner. The PIN is needed to
|
|
// complete first-pair against a locked device, but the lockdown
|
|
// short-circuit would otherwise hide the PIN entirely. The PIN is
|
|
// a per-attempt ephemeral pair-handshake artifact, not operator
|
|
// content, so compositing it over the LOCKED frame is safe.
|
|
//
|
|
// Calling ui->update() here would be wrong: it redraws the current
|
|
// carousel frame (the dashboard) into the framebuffer before the
|
|
// overlay paints, leaving operator content visible underneath the
|
|
// banner. Instead we invoke the banner overlay callback directly,
|
|
// which paints only the banner box on top of the LOCKED pixels we
|
|
// already have in the framebuffer.
|
|
if (NotificationRenderer::current_notification_type == notificationTypeEnum::pairing_pin) {
|
|
NotificationRenderer::drawBannercallback(display, ui->getUiState());
|
|
}
|
|
display->display();
|
|
return;
|
|
}
|
|
#endif
|
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
|
prepareFrameColorRegions();
|
|
#endif
|
|
ui->update();
|
|
}
|
|
// Global variables for alert banner - explicitly define with extern "C" linkage to prevent optimization
|
|
|
|
uint32_t logo_timeout = 5000; // 4 seconds for EACH logo
|
|
|
|
// Threshold values for the GPS lock accuracy bar display
|
|
uint32_t dopThresholds[5] = {2000, 1000, 500, 200, 100};
|
|
|
|
// At some point, we're going to ask all of the modules if they would like to display a screen frame
|
|
// we'll need to hold onto pointers for the modules that can draw a frame.
|
|
std::vector<MeshModule *> moduleFrames;
|
|
|
|
#if HAS_GPS
|
|
// GeoCoord object for the screen
|
|
GeoCoord geoCoord;
|
|
#endif
|
|
|
|
#ifdef SHOW_REDRAWS
|
|
static bool heartbeat = false;
|
|
#endif
|
|
|
|
#include "graphics/ScreenFonts.h"
|
|
#include <Throttle.h>
|
|
|
|
// Usage: int stringWidth = formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display);
|
|
// End Functions to write date/time to the screen
|
|
|
|
extern bool hasUnreadMessage;
|
|
|
|
static inline float wrapHeading360(float heading)
|
|
{
|
|
if (heading < 0.0f) {
|
|
heading += 360.0f;
|
|
} else if (heading >= 360.0f) {
|
|
heading -= 360.0f;
|
|
}
|
|
return heading;
|
|
}
|
|
|
|
static inline float wrapDelta180(float delta)
|
|
{
|
|
if (delta > 180.0f) {
|
|
delta -= 360.0f;
|
|
} else if (delta < -180.0f) {
|
|
delta += 360.0f;
|
|
}
|
|
return delta;
|
|
}
|
|
|
|
void Screen::setHeading(float heading)
|
|
{
|
|
const float wrappedHeading = wrapHeading360(heading);
|
|
|
|
if (!hasCompass) {
|
|
hasCompass = true;
|
|
compassHeading = wrappedHeading;
|
|
return;
|
|
}
|
|
|
|
// Interpolate using shortest-path angular delta to avoid jumps around 0/360.
|
|
float delta = wrapDelta180(wrappedHeading - compassHeading);
|
|
|
|
// Adaptive filtering:
|
|
// - Strong damping for tiny deltas (jitter)
|
|
// - Faster response for larger turns
|
|
const float absDelta = (delta >= 0.0f) ? delta : -delta;
|
|
if (absDelta >= 1.0f) {
|
|
float alpha = 0.35f;
|
|
if (absDelta > 25.0f) {
|
|
alpha = 0.85f;
|
|
} else if (absDelta > 10.0f) {
|
|
alpha = 0.65f;
|
|
}
|
|
|
|
float step = delta * alpha;
|
|
const float maxStep = 12.0f;
|
|
if (step > maxStep) {
|
|
step = maxStep;
|
|
} else if (step < -maxStep) {
|
|
step = -maxStep;
|
|
}
|
|
|
|
compassHeading = wrapHeading360(compassHeading + step);
|
|
}
|
|
}
|
|
|
|
// ==============================
|
|
// Overlay Alert Banner Renderer
|
|
// ==============================
|
|
// Displays a temporary centered banner message (e.g., warning, status, etc.)
|
|
// The banner appears in the center of the screen and disappears after the specified duration
|
|
|
|
void Screen::showSimpleBanner(const char *message, uint32_t durationMs)
|
|
{
|
|
BannerOverlayOptions options;
|
|
options.message = message;
|
|
options.durationMs = durationMs;
|
|
options.notificationType = notificationTypeEnum::text_banner;
|
|
showOverlayBanner(options);
|
|
}
|
|
|
|
// Called to trigger a banner with custom message and duration
|
|
void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options)
|
|
{
|
|
#ifdef USE_EINK
|
|
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
|
|
#endif
|
|
// Store the message and set the expiration timestamp
|
|
strncpy(NotificationRenderer::alertBannerMessage, banner_overlay_options.message, 255);
|
|
NotificationRenderer::parseBannerMessageWithFonts(NotificationRenderer::alertBannerMessage);
|
|
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
|
|
NotificationRenderer::alertBannerUntil =
|
|
(banner_overlay_options.durationMs == 0) ? 0 : millis() + banner_overlay_options.durationMs;
|
|
NotificationRenderer::optionsArrayPtr = banner_overlay_options.optionsArrayPtr;
|
|
NotificationRenderer::optionsEnumPtr = banner_overlay_options.optionsEnumPtr;
|
|
NotificationRenderer::alertBannerOptions = banner_overlay_options.optionsCount;
|
|
NotificationRenderer::alertBannerCallback = banner_overlay_options.bannerCallback;
|
|
NotificationRenderer::curSelected = banner_overlay_options.InitialSelected;
|
|
NotificationRenderer::pauseBanner = false;
|
|
NotificationRenderer::current_notification_type = banner_overlay_options.notificationType;
|
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
|
ui->setOverlays(overlays, 2);
|
|
ui->setTargetFPS(60);
|
|
updateUiFrame(ui);
|
|
}
|
|
|
|
// Called to trigger a banner with custom message and duration
|
|
void Screen::showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback)
|
|
{
|
|
#ifdef USE_EINK
|
|
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
|
|
#endif
|
|
nodeDB->pause_sort(true);
|
|
// Store the message and set the expiration timestamp
|
|
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
|
|
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
|
|
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
|
NotificationRenderer::alertBannerCallback = bannerCallback;
|
|
NotificationRenderer::pauseBanner = false;
|
|
NotificationRenderer::curSelected = 0;
|
|
NotificationRenderer::current_notification_type = notificationTypeEnum::node_picker;
|
|
|
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
|
ui->setOverlays(overlays, 2);
|
|
ui->setTargetFPS(60);
|
|
updateUiFrame(ui);
|
|
}
|
|
|
|
// Called to trigger a banner with custom message and duration
|
|
void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, bool useBase16,
|
|
std::function<void(uint32_t)> bannerCallback)
|
|
{
|
|
#ifdef USE_EINK
|
|
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
|
|
#endif
|
|
// Store the message and set the expiration timestamp
|
|
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
|
|
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
|
|
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
|
NotificationRenderer::alertBannerCallback = bannerCallback;
|
|
NotificationRenderer::pauseBanner = false;
|
|
NotificationRenderer::curSelected = 0;
|
|
if (useBase16)
|
|
NotificationRenderer::current_notification_type = notificationTypeEnum::hex_picker;
|
|
else
|
|
NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker;
|
|
NotificationRenderer::numDigits = digits;
|
|
NotificationRenderer::currentNumber = 0;
|
|
|
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
|
ui->setOverlays(overlays, 2);
|
|
ui->setTargetFPS(60);
|
|
updateUiFrame(ui);
|
|
}
|
|
|
|
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
|
std::function<void(const std::string &)> textCallback)
|
|
{
|
|
LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs);
|
|
|
|
// Start OnScreenKeyboardModule session (non-touch variant)
|
|
OnScreenKeyboardModule::instance().start(header, initialText, durationMs, textCallback);
|
|
NotificationRenderer::textInputCallback = textCallback;
|
|
|
|
// Store the message and set the expiration timestamp (use same pattern as other notifications)
|
|
strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255);
|
|
NotificationRenderer::alertBannerMessage[255] = '\0';
|
|
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
|
NotificationRenderer::pauseBanner = false;
|
|
NotificationRenderer::current_notification_type = notificationTypeEnum::text_input;
|
|
|
|
// Set the overlay using the same pattern as other notification types
|
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
|
ui->setOverlays(overlays, 2);
|
|
ui->setTargetFPS(60);
|
|
updateUiFrame(ui);
|
|
}
|
|
|
|
static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
|
{
|
|
uint8_t module_frame;
|
|
// there's a little but in the UI transition code
|
|
// where it invokes the function at the correct offset
|
|
// in the array of "drawScreen" functions; however,
|
|
// the passed-state doesn't quite reflect the "current"
|
|
// screen, so we have to detect it.
|
|
if (state->frameState == IN_TRANSITION && state->transitionFrameRelationship == TransitionRelationship_INCOMING) {
|
|
// if we're transitioning from the end of the frame list back around to the first
|
|
// frame, then we want this to be `0`
|
|
module_frame = state->transitionFrameTarget;
|
|
} else {
|
|
// otherwise, just display the module frame that's aligned with the current frame
|
|
module_frame = state->currentFrame;
|
|
}
|
|
MeshModule &pi = *moduleFrames.at(module_frame);
|
|
pi.drawFrame(display, state, x, y);
|
|
}
|
|
|
|
/**
|
|
* Given a recent lat/lon return a guess of the heading the user is walking on.
|
|
*
|
|
* We keep a series of "after you've gone 10 meters, what is your heading since
|
|
* the last reference point?"
|
|
*/
|
|
float Screen::estimatedHeading(double lat, double lon)
|
|
{
|
|
static double oldLat, oldLon;
|
|
static float b = -1.0f;
|
|
static uint32_t lastHeadingAtMs = 0;
|
|
const uint32_t now = millis();
|
|
const uint32_t gpsUpdateIntervalSecs =
|
|
Default::getConfiguredOrDefault(config.position.gps_update_interval, default_gps_update_interval);
|
|
uint32_t effectiveUpdateIntervalSecs = gpsUpdateIntervalSecs;
|
|
if (config.position.position_broadcast_smart_enabled) {
|
|
const uint32_t smartMinIntervalSecs = Default::getConfiguredOrDefault(
|
|
config.position.broadcast_smart_minimum_interval_secs, default_broadcast_smart_minimum_interval_secs);
|
|
if (smartMinIntervalSecs > effectiveUpdateIntervalSecs) {
|
|
effectiveUpdateIntervalSecs = smartMinIntervalSecs;
|
|
}
|
|
}
|
|
// Two expected update windows; keep arithmetic 32-bit to avoid pulling in larger 64-bit helpers.
|
|
const uint32_t headingStaleMs =
|
|
(effectiveUpdateIntervalSecs > (UINT32_MAX / 2000U)) ? UINT32_MAX : (effectiveUpdateIntervalSecs * 2000U);
|
|
|
|
if (oldLat == 0) {
|
|
// Need at least two position points before we can infer heading.
|
|
oldLat = lat;
|
|
oldLon = lon;
|
|
|
|
return b;
|
|
}
|
|
|
|
float d = GeoCoord::latLongToMeter(oldLat, oldLon, lat, lon);
|
|
if (d < 10) { // haven't moved enough, keep previous heading (invalid until first real movement)
|
|
if (lastHeadingAtMs != 0 && (now - lastHeadingAtMs) >= headingStaleMs) {
|
|
// Heading is stale after prolonged no-movement; force reacquire.
|
|
b = -1.0f;
|
|
oldLat = lat;
|
|
oldLon = lon;
|
|
}
|
|
return b;
|
|
}
|
|
|
|
b = GeoCoord::bearing(oldLat, oldLon, lat, lon) * RAD_TO_DEG;
|
|
oldLat = lat;
|
|
oldLon = lon;
|
|
lastHeadingAtMs = now;
|
|
|
|
return b;
|
|
}
|
|
|
|
/// We will skip one node - the one for us, so we just blindly loop over all
|
|
/// nodes
|
|
static int8_t prevFrame = -1;
|
|
|
|
// Combined dynamic node list frame cycling through LastHeard, HopSignal, and Distance modes
|
|
// Uses a single frame and changes data every few seconds (E-Ink variant is separate)
|
|
|
|
#if defined(ESP_PLATFORM) && (defined(USE_ST7789) || defined(USE_ST7796))
|
|
SPIClass SPI1(HSPI);
|
|
#endif
|
|
|
|
Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_OledType screenType, OLEDDISPLAY_GEOMETRY geometry)
|
|
: concurrency::OSThread("Screen"), address_found(address), model(screenType), geometry(geometry), cmdQueue(32)
|
|
{
|
|
graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES];
|
|
|
|
#if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64)
|
|
dispdev = new SH1106Wire(address.address, -1, -1, geometry,
|
|
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
|
isI2cScreen = true;
|
|
#elif defined(USE_ST7789)
|
|
#ifdef ESP_PLATFORM
|
|
dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT, ST7789_SDA,
|
|
ST7789_MISO, ST7789_SCK);
|
|
#else
|
|
dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT);
|
|
#endif
|
|
#elif defined(USE_ST7796)
|
|
#ifdef ESP_PLATFORM
|
|
dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT, ST7796_SDA,
|
|
ST7796_MISO, ST7796_SCK, TFT_SPI_FREQUENCY);
|
|
#else
|
|
dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT);
|
|
#endif
|
|
#elif defined(USE_SSD1306)
|
|
dispdev = new SSD1306Wire(address.address, -1, -1, geometry,
|
|
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
|
isI2cScreen = true;
|
|
#if defined(OLED_Y_OFFSET_PAGES)
|
|
// Panels whose active window does not start at GDDRAM row 0 (e.g. 72x40
|
|
// modules on pages 3..7) need a fixed vertical page shift on every write.
|
|
static_cast<SSD1306Wire *>(dispdev)->setYOffset(OLED_Y_OFFSET_PAGES);
|
|
#endif
|
|
#elif defined(USE_SPISSD1306)
|
|
dispdev = new SSD1306Spi(SSD1306_RESET, SSD1306_RS, SSD1306_NSS, GEOMETRY_64_48);
|
|
if (!dispdev->init()) {
|
|
LOG_DEBUG("Error: SSD1306 not detected!");
|
|
} else {
|
|
static_cast<SSD1306Spi *>(dispdev)->setHorizontalOffset(32);
|
|
LOG_INFO("SSD1306 init success");
|
|
}
|
|
#elif ARCH_PORTDUINO
|
|
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
|
if (portduino_config.displayPanel != no_screen) {
|
|
LOG_DEBUG("Make TFTDisplay!");
|
|
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
|
|
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
|
} else {
|
|
dispdev = new AutoOLEDWire(address.address, -1, -1, geometry,
|
|
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
|
isAUTOOled = true;
|
|
isI2cScreen = true;
|
|
}
|
|
}
|
|
#elif USE_TFTDISPLAY
|
|
LOG_DEBUG("Make TFTDisplay!");
|
|
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
|
|
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
|
#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) && !defined(USE_EINK_PARALLELDISPLAY)
|
|
dispdev = new EInkDisplay(address.address, -1, -1, geometry,
|
|
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
|
#elif defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY)
|
|
dispdev = new EInkDynamicDisplay(address.address, -1, -1, geometry,
|
|
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
|
#elif defined(USE_EINK_PARALLELDISPLAY)
|
|
dispdev = new EInkParallelDisplay(EPD_WIDTH, EPD_HEIGHT, EInkParallelDisplay::EPD_ROT_PORTRAIT);
|
|
#elif defined(USE_ST7567)
|
|
dispdev = new ST7567Wire(address.address, -1, -1, geometry,
|
|
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
|
isI2cScreen = true;
|
|
#else
|
|
dispdev = new AutoOLEDWire(address.address, -1, -1, geometry,
|
|
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
|
isAUTOOled = true;
|
|
isI2cScreen = true;
|
|
#endif
|
|
|
|
#if defined(USE_ST7789)
|
|
// Keep firmware and ST7789 driver region structs layout-compatible:
|
|
// we pass `graphics::colorRegions` through a type cast below.
|
|
static_assert(sizeof(graphics::TFTColorRegion) == sizeof(::TFTColorRegion),
|
|
"graphics::TFTColorRegion layout must match ST7789 TFTColorRegion");
|
|
static_cast<ST7789Spi *>(dispdev)->setRGB(TFTPalette::White, (::TFTColorRegion *)colorRegions);
|
|
#elif defined(USE_ST7796)
|
|
static_cast<ST7796Spi *>(dispdev)->setRGB(TFTPalette::White);
|
|
#endif
|
|
|
|
ui = new OLEDDisplayUi(dispdev);
|
|
cmdQueue.setReader(this);
|
|
}
|
|
|
|
Screen::~Screen()
|
|
{
|
|
delete[] graphics::normalFrames;
|
|
}
|
|
|
|
/**
|
|
* Prepare the display for the unit going to the lowest power mode possible. Most screens will just
|
|
* poweroff, but eink screens will show a "I'm sleeping" graphic, possibly with a QR code
|
|
*/
|
|
void Screen::doDeepSleep()
|
|
{
|
|
#ifdef USE_EINK
|
|
setOn(false, graphics::UIRenderer::drawDeepSleepFrame);
|
|
#else
|
|
// Without E-Ink display:
|
|
setOn(false);
|
|
#endif
|
|
}
|
|
|
|
void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
|
{
|
|
if (!useDisplay)
|
|
return;
|
|
|
|
if (on != screenOn) {
|
|
if (on) {
|
|
LOG_INFO("Turn on screen");
|
|
powerMon->setState(meshtastic_PowerMon_State_Screen_On);
|
|
#ifdef T_WATCH_S3
|
|
PMU->enablePowerOutput(XPOWERS_ALDO2);
|
|
#endif
|
|
|
|
// some screens seem to need a kick in the pants to turn back on
|
|
#if defined(MUZI_BASE) || defined(M5STACK_CARDPUTER_ADV)
|
|
dispdev->init();
|
|
dispdev->setBrightness(brightness);
|
|
dispdev->flipScreenVertically();
|
|
dispdev->resetDisplay();
|
|
#ifdef SCREEN_12V_ENABLE
|
|
digitalWrite(SCREEN_12V_ENABLE, HIGH);
|
|
#endif
|
|
delay(100);
|
|
#endif
|
|
#if !ARCH_PORTDUINO
|
|
#if defined(USE_ST7789) && defined(VTFT_CTRL)
|
|
// Ensure panel power rail is enabled before sending wake commands.
|
|
pinMode(VTFT_CTRL, OUTPUT);
|
|
digitalWrite(VTFT_CTRL, LOW);
|
|
#endif
|
|
dispdev->displayOn();
|
|
#endif
|
|
|
|
#ifdef PIN_EINK_EN
|
|
if (uiconfig.screen_brightness == 1)
|
|
digitalWrite(PIN_EINK_EN, HIGH);
|
|
#elif defined(PCA_PIN_EINK_EN)
|
|
if (uiconfig.screen_brightness > 0)
|
|
io.digitalWrite(PCA_PIN_EINK_EN, HIGH);
|
|
#endif
|
|
|
|
#if defined(ST7789_CS) && \
|
|
!defined(M5STACK) // set display brightness when turning on screens. Just moved function from TFTDisplay to here.
|
|
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
|
|
#endif
|
|
|
|
dispdev->displayOn();
|
|
#if defined(HELTEC_TRACKER_V1_X) || defined(HELTEC_WIRELESS_TRACKER_V2)
|
|
ui->init();
|
|
#endif
|
|
#if defined(USE_ST7789) && defined(VTFT_LEDA)
|
|
ui->init();
|
|
#ifdef ESP_PLATFORM
|
|
analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT);
|
|
#else
|
|
pinMode(VTFT_LEDA, OUTPUT);
|
|
digitalWrite(VTFT_LEDA, TFT_BACKLIGHT_ON);
|
|
#endif
|
|
#endif
|
|
#ifdef USE_ST7796
|
|
ui->init();
|
|
#ifdef ESP_PLATFORM
|
|
analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT);
|
|
#else
|
|
pinMode(VTFT_LEDA, OUTPUT);
|
|
digitalWrite(VTFT_LEDA, TFT_BACKLIGHT_ON);
|
|
#endif
|
|
#endif
|
|
enabled = true;
|
|
setInterval(0); // Draw ASAP
|
|
runASAP = true;
|
|
} else {
|
|
powerMon->clearState(meshtastic_PowerMon_State_Screen_On);
|
|
#ifdef USE_EINK
|
|
// eInkScreensaver parameter is usually NULL (default argument), default frame used instead
|
|
setScreensaverFrames(einkScreensaver);
|
|
#endif
|
|
|
|
#ifdef MESHTASTIC_LOCKDOWN
|
|
// M19: before turning the panel off, paint a safe frame into the
|
|
// OLED's GDDRAM. The panel retains whatever was last written even
|
|
// while powered down, so when displayOn() is called later the
|
|
// screen would otherwise flash the previous frame's content for
|
|
// 16-50 ms before the next ui->update() lands. Painting the
|
|
// LOCKED frame now ensures the only thing the operator (or
|
|
// someone over their shoulder) can see on wake is the redacted
|
|
// view. Gated on lockdown - non-lockdown builds keep the
|
|
// previous frame as a UX cue that the display is just dimmed.
|
|
// dispdev is dereferenced unguarded throughout this file (incl.
|
|
// displayOff() just below), so no null check here.
|
|
drawLockdownLockScreen(dispdev);
|
|
#endif
|
|
|
|
#ifdef PIN_EINK_EN
|
|
digitalWrite(PIN_EINK_EN, LOW);
|
|
#elif defined(PCA_PIN_EINK_EN)
|
|
io.digitalWrite(PCA_PIN_EINK_EN, LOW);
|
|
#endif
|
|
|
|
dispdev->displayOff();
|
|
|
|
#ifdef SCREEN_12V_ENABLE
|
|
digitalWrite(SCREEN_12V_ENABLE, LOW);
|
|
#endif
|
|
#ifdef USE_ST7789
|
|
SPI1.end();
|
|
// Keep TFT control pins in deterministic states while timed-off.
|
|
// Floating/default pin states can corrupt panel edge rows on wake.
|
|
#ifdef VTFT_LEDA
|
|
pinMode(VTFT_LEDA, OUTPUT);
|
|
digitalWrite(VTFT_LEDA, !TFT_BACKLIGHT_ON);
|
|
#endif
|
|
#ifdef VTFT_CTRL
|
|
pinMode(VTFT_CTRL, OUTPUT);
|
|
digitalWrite(VTFT_CTRL, HIGH);
|
|
#endif
|
|
pinMode(ST7789_RESET, OUTPUT);
|
|
digitalWrite(ST7789_RESET, HIGH);
|
|
pinMode(ST7789_RS, OUTPUT);
|
|
digitalWrite(ST7789_RS, HIGH);
|
|
pinMode(ST7789_NSS, OUTPUT);
|
|
digitalWrite(ST7789_NSS, HIGH);
|
|
#endif
|
|
#ifdef USE_ST7796
|
|
SPI1.end();
|
|
#if defined(ARCH_ESP32)
|
|
pinMode(VTFT_LEDA, OUTPUT);
|
|
digitalWrite(VTFT_LEDA, LOW);
|
|
pinMode(ST7796_RESET, ANALOG);
|
|
pinMode(ST7796_RS, ANALOG);
|
|
pinMode(ST7796_NSS, ANALOG);
|
|
#else
|
|
nrf_gpio_cfg_default(VTFT_LEDA);
|
|
nrf_gpio_cfg_default(ST7796_RESET);
|
|
nrf_gpio_cfg_default(ST7796_RS);
|
|
nrf_gpio_cfg_default(ST7796_NSS);
|
|
#endif
|
|
#endif
|
|
|
|
#ifdef T_WATCH_S3
|
|
PMU->disablePowerOutput(XPOWERS_ALDO2);
|
|
#endif
|
|
enabled = false;
|
|
}
|
|
screenOn = on;
|
|
}
|
|
}
|
|
|
|
void Screen::setup()
|
|
{
|
|
|
|
// Enable display rendering
|
|
useDisplay = true;
|
|
|
|
// Load saved brightness from UI config
|
|
// For OLED displays (SSD1306), default brightness is 255 if not set
|
|
if (uiconfig.screen_brightness == 0) {
|
|
#if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
|
|
brightness = 255; // Default for OLED
|
|
#else
|
|
brightness = BRIGHTNESS_DEFAULT;
|
|
#endif
|
|
} else {
|
|
brightness = uiconfig.screen_brightness;
|
|
}
|
|
|
|
// Restore which frames the user has hidden (persisted across reboots).
|
|
// Must happen before the first setFrames().
|
|
loadFrameVisibility();
|
|
|
|
// Detect OLED subtype (if supported by board variant)
|
|
#ifdef AutoOLEDWire_h
|
|
if (isAUTOOled)
|
|
static_cast<AutoOLEDWire *>(dispdev)->setDetected(model);
|
|
#endif
|
|
|
|
#if defined(USE_SH1107_128_64) || defined(USE_SH1107)
|
|
static_cast<SH1106Wire *>(dispdev)->setSubtype(7);
|
|
#endif
|
|
|
|
#if defined(USE_ST7789)
|
|
static_assert(sizeof(graphics::TFTColorRegion) == sizeof(::TFTColorRegion),
|
|
"graphics::TFTColorRegion layout must match ST7789 TFTColorRegion");
|
|
static_cast<ST7789Spi *>(dispdev)->setRGB(TFTPalette::White, (::TFTColorRegion *)colorRegions);
|
|
#endif
|
|
#if defined(MUZI_BASE)
|
|
dispdev->delayPoweron = true;
|
|
#endif
|
|
#if defined(USE_ST7796)
|
|
static_cast<ST7796Spi *>(dispdev)->setRGB(TFTPalette::White);
|
|
#endif
|
|
|
|
// Initialize display and UI system
|
|
ui->init();
|
|
displayWidth = dispdev->width();
|
|
displayHeight = dispdev->height();
|
|
|
|
ui->setTimePerTransition(0); // Disable animation delays
|
|
ui->setIndicatorPosition(BOTTOM); // Not used (indicators disabled below)
|
|
ui->setIndicatorDirection(LEFT_RIGHT); // Not used (indicators disabled below)
|
|
ui->setFrameAnimation(SLIDE_LEFT); // Used only when indicators are active
|
|
ui->disableAllIndicators(); // Disable page indicator dots
|
|
ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance
|
|
|
|
// Apply loaded brightness
|
|
#if defined(ST7789_CS)
|
|
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
|
|
#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306)
|
|
dispdev->setBrightness(brightness);
|
|
#endif
|
|
LOG_INFO("Applied screen brightness: %d", brightness);
|
|
|
|
#if defined(MESHTASTIC_LOCKDOWN) && defined(USE_EINK)
|
|
// M20: e-ink panels physically retain the last-rendered image without
|
|
// power, so a power-cycled lockdown handheld would keep showing
|
|
// operator-identifying content (position, messages, node info) until
|
|
// the firmware's first natural refresh - which on e-ink can be seconds
|
|
// into boot. Force a full refresh to the LOCKED frame here, immediately
|
|
// after the display is initialised and before any other rendering, so
|
|
// the persistent pixels are wiped to the redacted view before an
|
|
// observer can see them.
|
|
if (meshtastic_security::shouldRedactDisplay()) {
|
|
drawLockdownLockScreen(dispdev);
|
|
#if defined(USE_EINK_PARALLELDISPLAY)
|
|
// Parallel-display variants drive refresh through a different path;
|
|
// a bare drawLockdownLockScreen above lands the frame into the
|
|
// panel buffer and the next ui->update() commits it as normal.
|
|
#else
|
|
static_cast<EInkDisplay *>(dispdev)->forceDisplay();
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
// Set custom overlay callbacks
|
|
static OverlayCallback overlays[] = {
|
|
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
|
|
};
|
|
ui->setOverlays(overlays, 1);
|
|
|
|
// Enable UTF-8 to display mapping
|
|
dispdev->setFontTableLookupFunction(customFontTableLookup);
|
|
|
|
#ifdef USERPREFS_OEM_TEXT
|
|
logo_timeout *= 2; // Give more time for branded boot logos
|
|
#endif
|
|
|
|
// Configure alert frames (e.g., "Resuming..." or region name)
|
|
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip slow refresh
|
|
alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) {
|
|
#ifdef ARCH_ESP32
|
|
if (wakeCause == ESP_SLEEP_WAKEUP_TIMER || wakeCause == ESP_SLEEP_WAKEUP_EXT1)
|
|
graphics::UIRenderer::drawFrameText(display, state, x, y, "Resuming...");
|
|
else
|
|
#endif
|
|
{
|
|
const char *region = myRegion ? myRegion->name : nullptr;
|
|
graphics::UIRenderer::drawBootIconScreen(region, display, state, x, y);
|
|
}
|
|
};
|
|
ui->setFrames(alertFrames, 1);
|
|
ui->disableAutoTransition(); // Require manual navigation between frames
|
|
|
|
// Log buffer for on-screen logs (3 lines max)
|
|
dispdev->setLogBuffer(3, 32);
|
|
|
|
// Optional screen mirroring or flipping (e.g. for T-Beam orientation)
|
|
#ifdef SCREEN_MIRROR
|
|
dispdev->mirrorScreen();
|
|
#else
|
|
if (!config.display.flip_screen) {
|
|
#if USE_TFTDISPLAY && !ARCH_PORTDUINO
|
|
static_cast<TFTDisplay *>(dispdev)->flipScreenVertically();
|
|
#elif defined(USE_ST7789)
|
|
static_cast<ST7789Spi *>(dispdev)->flipScreenVertically();
|
|
#elif defined(USE_ST7796)
|
|
static_cast<ST7796Spi *>(dispdev)->mirrorScreen();
|
|
#elif !defined(M5STACK_UNITC6L)
|
|
dispdev->flipScreenVertically();
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
// Generate device ID from MAC address
|
|
uint8_t dmac[6];
|
|
getMacAddr(dmac);
|
|
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
|
|
|
|
#if ARCH_PORTDUINO
|
|
handleSetOn(false); // Ensure proper init for Arduino targets
|
|
#endif
|
|
|
|
// Turn on display and trigger first draw
|
|
handleSetOn(true);
|
|
graphics::currentResolution = graphics::determineScreenResolution(dispdev->height(), dispdev->width());
|
|
updateUiFrame(ui);
|
|
#ifndef USE_EINK
|
|
updateUiFrame(ui); // Some SSD1306 clones drop the first draw, so run twice
|
|
#endif
|
|
serialSinceMsec = millis();
|
|
|
|
#if ARCH_PORTDUINO
|
|
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
|
if (portduino_config.touchscreenModule) {
|
|
touchScreenImpl1 =
|
|
new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast<TFTDisplay *>(dispdev)->getTouch);
|
|
touchScreenImpl1->init();
|
|
}
|
|
}
|
|
#elif HAS_TOUCHSCREEN && !defined(USE_EINK) && !VARIANT_TOUCHSCREEN
|
|
touchScreenImpl1 =
|
|
new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast<TFTDisplay *>(dispdev)->getTouch);
|
|
touchScreenImpl1->init();
|
|
#endif
|
|
|
|
// Subscribe to device status updates
|
|
powerStatusObserver.observe(&powerStatus->onNewStatus);
|
|
gpsStatusObserver.observe(&gpsStatus->onNewStatus);
|
|
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
|
|
|
|
#if !MESHTASTIC_EXCLUDE_ADMIN
|
|
adminMessageObserver.observe(adminModule);
|
|
#endif
|
|
if (inputBroker)
|
|
inputObserver.observe(inputBroker);
|
|
|
|
// Load persisted messages into RAM
|
|
messageStore.loadFromFlash();
|
|
LOG_INFO("MessageStore loaded from flash");
|
|
|
|
// Notify modules that support UI events
|
|
MeshModule::observeUIEvents(&uiFrameEventObserver);
|
|
}
|
|
|
|
void Screen::setOn(bool on, FrameCallback einkScreensaver)
|
|
{
|
|
#if defined(T_LORA_PAGER)
|
|
if (cardKbI2cImpl)
|
|
cardKbI2cImpl->toggleBacklight(on);
|
|
#endif
|
|
if (!on) {
|
|
#ifdef MESHTASTIC_LOCKDOWN
|
|
// Screen powering off (idle timeout, shutdown, deep sleep) latches
|
|
// the screen-lock. Next time the display wakes it shows the LOCKED
|
|
// frame until a client authenticates with the passphrase.
|
|
meshtastic_security::lockScreen();
|
|
#endif
|
|
// We handle off commands immediately, because they might be called because the CPU is shutting down
|
|
handleSetOn(false, einkScreensaver);
|
|
} else
|
|
enqueueCmd(ScreenCmd{.cmd = Cmd::SET_ON});
|
|
}
|
|
|
|
void Screen::forceDisplay(bool forceUiUpdate)
|
|
{
|
|
// Nasty hack to force epaper updates for 'key' frames. FIXME, cleanup.
|
|
#ifdef USE_EINK
|
|
// If requested, make sure queued commands are run, and UI has rendered a new frame
|
|
if (forceUiUpdate) {
|
|
// Force a display refresh, in addition to the UI update
|
|
// Changing the GPS status bar icon apparently doesn't register as a change in image
|
|
// (False negative of the image hashing algorithm used to skip identical frames)
|
|
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST);
|
|
|
|
// No delay between UI frame rendering
|
|
setFastFramerate();
|
|
|
|
// Make sure all CMDs have run first
|
|
while (!cmdQueue.isEmpty())
|
|
runOnce();
|
|
|
|
// Ensure at least one frame has drawn
|
|
uint64_t startUpdate;
|
|
do {
|
|
startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow..
|
|
delay(10);
|
|
updateUiFrame(ui);
|
|
} while (ui->getUiState()->lastUpdate < startUpdate);
|
|
|
|
// Return to normal frame rate
|
|
targetFramerate = IDLE_FRAMERATE;
|
|
ui->setTargetFPS(targetFramerate);
|
|
}
|
|
|
|
// Tell EInk class to update the display
|
|
#if defined(USE_EINK_PARALLELDISPLAY)
|
|
static_cast<EInkParallelDisplay *>(dispdev)->forceDisplay();
|
|
#elif defined(USE_EINK)
|
|
static_cast<EInkDisplay *>(dispdev)->forceDisplay();
|
|
#endif
|
|
#else
|
|
// No delay between UI frame rendering
|
|
if (forceUiUpdate) {
|
|
setFastFramerate();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static uint32_t lastScreenTransition;
|
|
|
|
int32_t Screen::runOnce()
|
|
{
|
|
// If we don't have a screen, don't ever spend any CPU for us.
|
|
if (!useDisplay) {
|
|
enabled = false;
|
|
return RUN_SAME;
|
|
}
|
|
|
|
if (displayHeight == 0) {
|
|
displayHeight = dispdev->getHeight();
|
|
}
|
|
|
|
// Detect frame transitions and clear message cache when leaving text message screen
|
|
{
|
|
static int8_t lastFrameIndex = -1;
|
|
int8_t currentFrameIndex = ui->getUiState()->currentFrame;
|
|
int8_t textMsgIndex = framesetInfo.positions.textMessage;
|
|
|
|
if (lastFrameIndex != -1 && currentFrameIndex != lastFrameIndex) {
|
|
|
|
if (lastFrameIndex == textMsgIndex && currentFrameIndex != textMsgIndex) {
|
|
graphics::MessageRenderer::clearMessageCache();
|
|
}
|
|
}
|
|
|
|
lastFrameIndex = currentFrameIndex;
|
|
}
|
|
|
|
menuHandler::handleMenuSwitch(dispdev);
|
|
|
|
// Show boot screen for first logo_timeout seconds, then switch to normal operation.
|
|
// serialSinceMsec adjusts for additional serial wait time during nRF52 bootup
|
|
static bool showingBootScreen = true;
|
|
if (showingBootScreen && (millis() > (logo_timeout + serialSinceMsec))) {
|
|
LOG_INFO("Done with boot screen");
|
|
stopBootScreen();
|
|
showingBootScreen = false;
|
|
}
|
|
|
|
#ifdef USERPREFS_OEM_TEXT
|
|
static bool showingOEMBootScreen = true;
|
|
if (showingOEMBootScreen && (millis() > ((logo_timeout / 2) + serialSinceMsec))) {
|
|
LOG_INFO("Switch to OEM screen...");
|
|
// Change frames.
|
|
static FrameCallback bootOEMFrames[] = {graphics::UIRenderer::drawOEMBootScreen};
|
|
static const int bootOEMFrameCount = sizeof(bootOEMFrames) / sizeof(bootOEMFrames[0]);
|
|
ui->setFrames(bootOEMFrames, bootOEMFrameCount);
|
|
updateUiFrame(ui);
|
|
#ifndef USE_EINK
|
|
updateUiFrame(ui);
|
|
#endif
|
|
showingOEMBootScreen = false;
|
|
}
|
|
#endif
|
|
|
|
#ifndef DISABLE_WELCOME_UNSET
|
|
bool suppressRegionOnboard = false;
|
|
#ifdef MESHTASTIC_LOCKDOWN
|
|
// While lockdown is active and storage is still locked, config.lora.region
|
|
// is a deliberate UNSET placeholder - the real region lives in encrypted
|
|
// storage and is restored on unlock (see NodeDB's locked-boot path). Don't
|
|
// pop the region picker over the lock screen: it would trap input, and the
|
|
// operator can't set a region until they unlock anyway.
|
|
suppressRegionOnboard = meshtastic_security::shouldRedactDisplay();
|
|
#endif
|
|
if (!suppressRegionOnboard && !NotificationRenderer::isOverlayBannerShowing() &&
|
|
config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
|
#if defined(OLED_TINY)
|
|
menuHandler::LoraRegionPicker();
|
|
#else
|
|
menuHandler::OnboardMessage();
|
|
#endif
|
|
}
|
|
#endif
|
|
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0 && !suppressRebootBanner) {
|
|
showSimpleBanner("Rebooting...", 0);
|
|
}
|
|
|
|
// Process incoming commands.
|
|
for (;;) {
|
|
ScreenCmd cmd;
|
|
if (!cmdQueue.dequeue(&cmd, 0)) {
|
|
break;
|
|
}
|
|
switch (cmd.cmd) {
|
|
case Cmd::SET_ON:
|
|
handleSetOn(true);
|
|
break;
|
|
case Cmd::SET_OFF:
|
|
handleSetOn(false);
|
|
break;
|
|
case Cmd::ON_PRESS:
|
|
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
|
showFrame(FrameDirection::NEXT);
|
|
}
|
|
break;
|
|
case Cmd::SHOW_PREV_FRAME:
|
|
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
|
showFrame(FrameDirection::PREVIOUS);
|
|
}
|
|
break;
|
|
case Cmd::SHOW_NEXT_FRAME:
|
|
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
|
showFrame(FrameDirection::NEXT);
|
|
}
|
|
break;
|
|
case Cmd::START_ALERT_FRAME: {
|
|
showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away
|
|
showingNormalScreen = false;
|
|
NotificationRenderer::pauseBanner = true;
|
|
alertFrames[0] = alertFrame;
|
|
#ifdef USE_EINK
|
|
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please
|
|
EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update
|
|
handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?)
|
|
#endif
|
|
setFrameImmediateDraw(alertFrames);
|
|
break;
|
|
}
|
|
case Cmd::START_FIRMWARE_UPDATE_SCREEN:
|
|
handleStartFirmwareUpdateScreen();
|
|
break;
|
|
case Cmd::STOP_ALERT_FRAME:
|
|
NotificationRenderer::pauseBanner = false;
|
|
// Return from one-off alert mode back to regular frames.
|
|
if (!showingNormalScreen && NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
|
setFrames();
|
|
}
|
|
break;
|
|
case Cmd::STOP_BOOT_SCREEN:
|
|
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
|
|
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
|
setFrames();
|
|
}
|
|
break;
|
|
case Cmd::NOOP:
|
|
break;
|
|
default:
|
|
LOG_ERROR("Invalid screen cmd");
|
|
}
|
|
}
|
|
|
|
if (!screenOn) { // If we didn't just wake and the screen is still off, then
|
|
// stop updating until it is on again
|
|
enabled = false;
|
|
return 0;
|
|
}
|
|
|
|
// this must be before the frameState == FIXED check, because we always
|
|
// want to draw at least one FIXED frame before doing forceDisplay
|
|
updateUiFrame(ui);
|
|
|
|
// Switch to a low framerate (to save CPU) when we are not in transition
|
|
// but we should only call setTargetFPS when framestate changes, because
|
|
// otherwise that breaks animations.
|
|
|
|
uint32_t desiredFramerate = IDLE_FRAMERATE;
|
|
#if HAS_GPS && !defined(USE_EINK)
|
|
if (showingNormalScreen && hasCompass) {
|
|
const uint8_t currentFrame = ui->getUiState()->currentFrame;
|
|
if ((framesetInfo.positions.gps != 255 && currentFrame == framesetInfo.positions.gps) ||
|
|
(framesetInfo.positions.waypoint != 255 && currentFrame == framesetInfo.positions.waypoint) ||
|
|
(framesetInfo.positions.firstFavorite != 255 && currentFrame >= framesetInfo.positions.firstFavorite &&
|
|
currentFrame <= framesetInfo.positions.lastFavorite)) {
|
|
desiredFramerate = COMPASS_ACTIVE_FRAMERATE;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (targetFramerate != desiredFramerate && ui->getUiState()->frameState == FIXED) {
|
|
// oldFrameState = ui->getUiState()->frameState;
|
|
targetFramerate = desiredFramerate;
|
|
|
|
ui->setTargetFPS(targetFramerate);
|
|
forceDisplay();
|
|
}
|
|
|
|
// While showing the bootscreen or Bluetooth pair screen all of our
|
|
// standard screen switching is stopped.
|
|
if (showingNormalScreen) {
|
|
// standard screen loop handling here
|
|
if (config.display.auto_screen_carousel_secs > 0 &&
|
|
NotificationRenderer::current_notification_type != notificationTypeEnum::text_input &&
|
|
!Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) {
|
|
|
|
// If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead
|
|
// Carousel is potentially a major source of E-Ink display wear
|
|
#if !defined(EINK_BACKGROUND_USES_FAST)
|
|
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC);
|
|
#endif
|
|
|
|
LOG_DEBUG("LastScreenTransition exceeded %ums transition to next frame", (millis() - lastScreenTransition));
|
|
handleOnPress();
|
|
}
|
|
}
|
|
|
|
// LOG_DEBUG("want fps %d, fixed=%d", targetFramerate,
|
|
// ui->getUiState()->frameState); If we are scrolling we need to be called
|
|
// soon, otherwise just 1 fps (to save CPU) We also ask to be called twice
|
|
// as fast as we really need so that any rounding errors still result with
|
|
// the correct framerate
|
|
return (1000 / targetFramerate);
|
|
}
|
|
|
|
/* show a message that the SSL cert is being built
|
|
* it is expected that this will be used during the boot phase */
|
|
void Screen::setSSLFrames()
|
|
{
|
|
if (address_found.address) {
|
|
// LOG_DEBUG("Show SSL frames");
|
|
static FrameCallback sslFrames[] = {NotificationRenderer::drawSSLScreen};
|
|
ui->setFrames(sslFrames, 1);
|
|
updateUiFrame(ui);
|
|
}
|
|
}
|
|
|
|
#ifdef USE_EINK
|
|
/// Determine which screensaver frame to use, then set the FrameCallback
|
|
void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
|
|
{
|
|
// Retain specified frame / overlay callback beyond scope of this method
|
|
static FrameCallback screensaverFrame;
|
|
static OverlayCallback screensaverOverlay;
|
|
|
|
#if defined(HAS_EINK_ASYNCFULL) && defined(USE_EINK_DYNAMICDISPLAY)
|
|
// Join (await) a currently running async refresh, then run the post-update code.
|
|
// Avoid skipping of screensaver frame. Would otherwise be handled by NotifiedWorkerThread.
|
|
EINK_JOIN_ASYNCREFRESH(dispdev);
|
|
#endif
|
|
|
|
// If: one-off screensaver frame passed as argument. Handles doDeepSleep()
|
|
if (einkScreensaver != NULL) {
|
|
screensaverFrame = einkScreensaver;
|
|
ui->setFrames(&screensaverFrame, 1);
|
|
}
|
|
|
|
// Else, display the usual "overlay" screensaver
|
|
else {
|
|
screensaverOverlay = graphics::UIRenderer::drawScreensaverOverlay;
|
|
ui->setOverlays(&screensaverOverlay, 1);
|
|
}
|
|
|
|
// Request new frame, ASAP
|
|
setFastFramerate();
|
|
uint64_t startUpdate;
|
|
do {
|
|
startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow..
|
|
delay(1);
|
|
updateUiFrame(ui);
|
|
} while (ui->getUiState()->lastUpdate < startUpdate);
|
|
|
|
#if defined(USE_EINK_PARALLELDISPLAY)
|
|
static_cast<EInkParallelDisplay *>(dispdev)->forceDisplay(0);
|
|
#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY)
|
|
// Old EInkDisplay class
|
|
static_cast<EInkDisplay *>(dispdev)->forceDisplay(0); // Screen::forceDisplay(), but override rate-limit
|
|
#endif
|
|
|
|
// Prepare now for next frame, shown when display wakes
|
|
ui->setOverlays(NULL, 0); // Clear overlay
|
|
setFrames(FOCUS_PRESERVE); // Return to normal display updates, showing same frame as before screensaver, ideally
|
|
|
|
// Pick a refresh method, for when display wakes
|
|
#ifdef EINK_HASQUIRK_GHOSTING
|
|
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // Really ugly to see ghosting from "screen paused"
|
|
#else
|
|
EINK_ADD_FRAMEFLAG(dispdev, RESPONSIVE); // Really nice to wake screen with a fast-refresh
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
// Regenerate the normal set of frames, focusing a specific frame if requested
|
|
// Called when a frame should be added / removed, or custom frames should be cleared
|
|
void Screen::setFrames(FrameFocus focus)
|
|
{
|
|
// Block setFrames calls when virtual keyboard is active to prevent overlay interference
|
|
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
|
return;
|
|
}
|
|
|
|
uint8_t originalPosition = ui->getUiState()->currentFrame;
|
|
uint8_t previousFrameCount = framesetInfo.frameCount;
|
|
FramesetInfo fsi; // Location of specific frames, for applying focus parameter
|
|
|
|
graphics::UIRenderer::rebuildFavoritedNodes();
|
|
|
|
LOG_DEBUG("Show standard frames");
|
|
showingNormalScreen = true;
|
|
|
|
indicatorIcons.clear();
|
|
|
|
size_t numframes = 0;
|
|
|
|
// If we have a critical fault, show it first
|
|
fsi.positions.fault = numframes;
|
|
if (error_code) {
|
|
normalFrames[numframes++] = NotificationRenderer::drawCriticalFaultFrame;
|
|
indicatorIcons.push_back(icon_error);
|
|
focus = FOCUS_FAULT; // Change our "focus" parameter, to ensure we show the fault frame
|
|
}
|
|
|
|
#if defined(DISPLAY_CLOCK_FRAME)
|
|
if (!hiddenFrames.clock) {
|
|
fsi.positions.clock = numframes;
|
|
#if defined(OLED_TINY)
|
|
normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame;
|
|
#else
|
|
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
|
|
: graphics::ClockRenderer::drawDigitalClockFrame;
|
|
#endif
|
|
indicatorIcons.push_back(digital_icon_clock);
|
|
}
|
|
#endif
|
|
|
|
if (!hiddenFrames.home) {
|
|
fsi.positions.home = numframes;
|
|
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
|
|
indicatorIcons.push_back(icon_home);
|
|
}
|
|
|
|
fsi.positions.textMessage = numframes;
|
|
normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame;
|
|
indicatorIcons.push_back(icon_mail);
|
|
|
|
#ifndef USE_EINK
|
|
if (!hiddenFrames.nodelist_nodes) {
|
|
fsi.positions.nodelist_nodes = numframes;
|
|
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Nodes;
|
|
indicatorIcons.push_back(icon_nodes);
|
|
}
|
|
if (!hiddenFrames.nodelist_location) {
|
|
fsi.positions.nodelist_location = numframes;
|
|
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Location;
|
|
indicatorIcons.push_back(icon_list);
|
|
}
|
|
#endif
|
|
|
|
// Show detailed node views only on E-Ink builds
|
|
#ifdef USE_EINK
|
|
if (!hiddenFrames.nodelist_lastheard) {
|
|
fsi.positions.nodelist_lastheard = numframes;
|
|
normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen;
|
|
indicatorIcons.push_back(icon_nodes);
|
|
}
|
|
if (!hiddenFrames.nodelist_hopsignal) {
|
|
fsi.positions.nodelist_hopsignal = numframes;
|
|
normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen;
|
|
indicatorIcons.push_back(icon_signal);
|
|
}
|
|
if (!hiddenFrames.nodelist_distance) {
|
|
fsi.positions.nodelist_distance = numframes;
|
|
normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen;
|
|
indicatorIcons.push_back(icon_distance);
|
|
}
|
|
#endif
|
|
#if HAS_GPS
|
|
#ifdef USE_EINK
|
|
if (!hiddenFrames.nodelist_bearings) {
|
|
fsi.positions.nodelist_bearings = numframes;
|
|
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
|
|
indicatorIcons.push_back(icon_list);
|
|
}
|
|
#endif
|
|
if (!hiddenFrames.gps) {
|
|
fsi.positions.gps = numframes;
|
|
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
|
|
indicatorIcons.push_back(icon_compass);
|
|
}
|
|
#endif
|
|
if (RadioLibInterface::instance && !hiddenFrames.lora) {
|
|
fsi.positions.lora = numframes;
|
|
normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused;
|
|
indicatorIcons.push_back(icon_radio);
|
|
}
|
|
if (!hiddenFrames.system) {
|
|
fsi.positions.system = numframes;
|
|
normalFrames[numframes++] = graphics::DebugRenderer::drawSystemScreen;
|
|
indicatorIcons.push_back(icon_system);
|
|
}
|
|
#if !defined(DISPLAY_CLOCK_FRAME)
|
|
if (!hiddenFrames.clock) {
|
|
fsi.positions.clock = numframes;
|
|
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
|
|
: graphics::ClockRenderer::drawDigitalClockFrame;
|
|
indicatorIcons.push_back(digital_icon_clock);
|
|
}
|
|
#endif
|
|
if (!hiddenFrames.chirpy) {
|
|
fsi.positions.chirpy = numframes;
|
|
normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy;
|
|
indicatorIcons.push_back(chirpy_small);
|
|
}
|
|
|
|
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
|
if (!hiddenFrames.wifi && isWifiAvailable()) {
|
|
fsi.positions.wifi = numframes;
|
|
normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline;
|
|
indicatorIcons.push_back(icon_wifi);
|
|
}
|
|
#endif
|
|
|
|
// Beware of what changes you make in this code!
|
|
// We pass numframes into GetMeshModulesWithUIFrames() which is highly important!
|
|
// Inside of that callback, goes over to MeshModule.cpp and we run
|
|
// modulesWithUIFrames.resize(startIndex, nullptr), to insert nullptr
|
|
// entries until we're ready to start building the matching entries.
|
|
// We are doing our best to keep the normalFrames vector
|
|
// and the moduleFrames vector in lock step.
|
|
moduleFrames = MeshModule::GetMeshModulesWithUIFrames(numframes);
|
|
LOG_DEBUG("Show %d module frames", moduleFrames.size());
|
|
|
|
for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) {
|
|
// Draw the module frame, using the hack described above
|
|
if (*i != nullptr) {
|
|
normalFrames[numframes] = drawModuleFrame;
|
|
|
|
// Check if the module being drawn has requested focus
|
|
// We will honor this request later, if setFrames was triggered by a UIFrameEvent
|
|
MeshModule *m = *i;
|
|
if (m && m->isRequestingFocus())
|
|
fsi.positions.focusedModule = numframes;
|
|
if (m && m == waypointModule)
|
|
fsi.positions.waypoint = numframes;
|
|
|
|
indicatorIcons.push_back(icon_module);
|
|
numframes++;
|
|
}
|
|
}
|
|
|
|
LOG_DEBUG("Added modules. numframes: %d", numframes);
|
|
|
|
// We don't show the node info of our node (if we have it yet - we should)
|
|
size_t numMeshNodes = nodeDB->getNumMeshNodes();
|
|
if (numMeshNodes > 0)
|
|
numMeshNodes--;
|
|
|
|
if (!hiddenFrames.show_favorites) {
|
|
// Temporary array to hold favorite node frames
|
|
std::vector<FrameCallback> favoriteFrames;
|
|
|
|
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
|
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
|
if (n && n->num != nodeDB->getNodeNum() && nodeInfoLiteIsFavorite(n)) {
|
|
favoriteFrames.push_back(graphics::UIRenderer::drawFavoriteNode);
|
|
}
|
|
}
|
|
|
|
// Insert favorite frames *after* collecting them all
|
|
if (!favoriteFrames.empty()) {
|
|
fsi.positions.firstFavorite = numframes;
|
|
for (const auto &f : favoriteFrames) {
|
|
normalFrames[numframes++] = f;
|
|
indicatorIcons.push_back(icon_node);
|
|
}
|
|
fsi.positions.lastFavorite = numframes - 1;
|
|
} else {
|
|
fsi.positions.firstFavorite = 255;
|
|
fsi.positions.lastFavorite = 255;
|
|
}
|
|
}
|
|
|
|
fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE
|
|
this->frameCount = numframes; // Save frame count for use in custom overlay
|
|
LOG_DEBUG("Finished build frames. numframes: %d", numframes);
|
|
|
|
ui->setFrames(normalFrames, numframes);
|
|
ui->disableAllIndicators();
|
|
|
|
// Add overlays: frame icons and alert banner)
|
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
|
ui->setOverlays(overlays, 2);
|
|
|
|
prevFrame = -1; // Force drawFavoriteNode to pick a new node (because our list just changed)
|
|
|
|
// Focus on a specific frame, in the frame set we just created
|
|
switch (focus) {
|
|
case FOCUS_DEFAULT:
|
|
ui->switchToFrame(fsi.positions.deviceFocused);
|
|
break;
|
|
case FOCUS_FAULT:
|
|
ui->switchToFrame(fsi.positions.fault);
|
|
break;
|
|
case FOCUS_MODULE:
|
|
// Whichever frame was marked by MeshModule::requestFocus(), if any
|
|
// If no module requested focus, will show the first frame instead
|
|
ui->switchToFrame(fsi.positions.focusedModule);
|
|
break;
|
|
case FOCUS_CLOCK:
|
|
// Whichever frame was marked by MeshModule::requestFocus(), if any
|
|
// If no module requested focus, will show the first frame instead
|
|
ui->switchToFrame(fsi.positions.clock);
|
|
break;
|
|
case FOCUS_SYSTEM:
|
|
ui->switchToFrame(fsi.positions.system);
|
|
break;
|
|
|
|
case FOCUS_PRESERVE:
|
|
// No more adjustment - force stay on same index
|
|
if (previousFrameCount > fsi.frameCount) {
|
|
ui->switchToFrame(originalPosition - 1);
|
|
} else if (previousFrameCount < fsi.frameCount) {
|
|
ui->switchToFrame(originalPosition + 1);
|
|
} else {
|
|
ui->switchToFrame(originalPosition);
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Store the info about this frameset, for future setFrames calls
|
|
this->framesetInfo = fsi;
|
|
|
|
#ifdef USERPREFS_UI_TEST_LOG
|
|
logFrameChange("rebuild", ui->getUiState()->currentFrame);
|
|
#endif
|
|
|
|
setFastFramerate(); // Draw ASAP
|
|
}
|
|
|
|
void Screen::setFrameImmediateDraw(FrameCallback *drawFrames)
|
|
{
|
|
ui->disableAllIndicators();
|
|
ui->setFrames(drawFrames, 1);
|
|
setFastFramerate();
|
|
}
|
|
|
|
void Screen::toggleFrameVisibility(const std::string &frameName)
|
|
{
|
|
#ifndef USE_EINK
|
|
if (frameName == "nodelist_nodes") {
|
|
hiddenFrames.nodelist_nodes = !hiddenFrames.nodelist_nodes;
|
|
}
|
|
if (frameName == "nodelist_location") {
|
|
hiddenFrames.nodelist_location = !hiddenFrames.nodelist_location;
|
|
}
|
|
#endif
|
|
#ifdef USE_EINK
|
|
if (frameName == "nodelist_lastheard") {
|
|
hiddenFrames.nodelist_lastheard = !hiddenFrames.nodelist_lastheard;
|
|
}
|
|
if (frameName == "nodelist_hopsignal") {
|
|
hiddenFrames.nodelist_hopsignal = !hiddenFrames.nodelist_hopsignal;
|
|
}
|
|
if (frameName == "nodelist_distance") {
|
|
hiddenFrames.nodelist_distance = !hiddenFrames.nodelist_distance;
|
|
}
|
|
#endif
|
|
#if HAS_GPS
|
|
#ifdef USE_EINK
|
|
if (frameName == "nodelist_bearings") {
|
|
hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings;
|
|
}
|
|
#endif
|
|
if (frameName == "gps") {
|
|
hiddenFrames.gps = !hiddenFrames.gps;
|
|
}
|
|
#endif
|
|
if (frameName == "lora") {
|
|
hiddenFrames.lora = !hiddenFrames.lora;
|
|
}
|
|
if (frameName == "clock") {
|
|
hiddenFrames.clock = !hiddenFrames.clock;
|
|
}
|
|
if (frameName == "show_favorites") {
|
|
hiddenFrames.show_favorites = !hiddenFrames.show_favorites;
|
|
}
|
|
if (frameName == "chirpy") {
|
|
hiddenFrames.chirpy = !hiddenFrames.chirpy;
|
|
}
|
|
|
|
// Save the new visibility state so it survives a reboot.
|
|
saveFrameVisibility();
|
|
}
|
|
|
|
bool Screen::isFrameHidden(const std::string &frameName) const
|
|
{
|
|
#ifndef USE_EINK
|
|
if (frameName == "nodelist_nodes")
|
|
return hiddenFrames.nodelist_nodes;
|
|
if (frameName == "nodelist_location")
|
|
return hiddenFrames.nodelist_location;
|
|
#endif
|
|
#ifdef USE_EINK
|
|
if (frameName == "nodelist_lastheard")
|
|
return hiddenFrames.nodelist_lastheard;
|
|
if (frameName == "nodelist_hopsignal")
|
|
return hiddenFrames.nodelist_hopsignal;
|
|
if (frameName == "nodelist_distance")
|
|
return hiddenFrames.nodelist_distance;
|
|
#endif
|
|
#if HAS_GPS
|
|
#ifdef USE_EINK
|
|
if (frameName == "nodelist_bearings")
|
|
return hiddenFrames.nodelist_bearings;
|
|
#endif
|
|
if (frameName == "gps")
|
|
return hiddenFrames.gps;
|
|
#endif
|
|
if (frameName == "lora")
|
|
return hiddenFrames.lora;
|
|
if (frameName == "clock")
|
|
return hiddenFrames.clock;
|
|
if (frameName == "show_favorites")
|
|
return hiddenFrames.show_favorites;
|
|
if (frameName == "chirpy")
|
|
return hiddenFrames.chirpy;
|
|
|
|
return false;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Frame visibility persistence
|
|
//
|
|
// The set of hideable frames varies by build (USE_EINK, HAS_GPS, ...), so we
|
|
// serialize to a fixed bitmask where each frame name owns a permanent bit
|
|
// position. Bits for frames that don't exist in the current build are simply
|
|
// left untouched, which keeps the saved file portable across firmware variants.
|
|
// ---------------------------------------------------------------------------
|
|
namespace
|
|
{
|
|
static const char *frameVisibilityFileName = "/prefs/framevis";
|
|
constexpr uint32_t FRAMEVIS_MAGIC = 0x53495646; // "FVIS" little-endian
|
|
constexpr uint8_t FRAMEVIS_VERSION = 1;
|
|
|
|
// Permanent bit assignments. Never renumber these; only append new ones.
|
|
enum FrameVisBit : uint8_t {
|
|
FVBIT_TEXT_MESSAGE = 0,
|
|
FVBIT_WAYPOINT = 1,
|
|
FVBIT_WIFI = 2,
|
|
FVBIT_SYSTEM = 3,
|
|
FVBIT_HOME = 4,
|
|
FVBIT_CLOCK = 5,
|
|
FVBIT_NODELIST_NODES = 6,
|
|
FVBIT_NODELIST_LOCATION = 7,
|
|
FVBIT_NODELIST_LASTHEARD = 8,
|
|
FVBIT_NODELIST_HOPSIGNAL = 9,
|
|
FVBIT_NODELIST_DISTANCE = 10,
|
|
FVBIT_NODELIST_BEARINGS = 11,
|
|
FVBIT_GPS = 12,
|
|
FVBIT_LORA = 13,
|
|
FVBIT_SHOW_FAVORITES = 14,
|
|
FVBIT_CHIRPY = 15,
|
|
};
|
|
|
|
struct __attribute__((packed)) FrameVisFile {
|
|
uint32_t magic;
|
|
uint8_t version;
|
|
uint32_t mask;
|
|
};
|
|
|
|
inline void setBit(uint32_t &mask, uint8_t bit, bool value)
|
|
{
|
|
if (value)
|
|
mask |= (1UL << bit);
|
|
else
|
|
mask &= ~(1UL << bit);
|
|
}
|
|
|
|
inline bool getBit(uint32_t mask, uint8_t bit)
|
|
{
|
|
return (mask & (1UL << bit)) != 0;
|
|
}
|
|
} // namespace
|
|
|
|
uint32_t Screen::packHiddenFrames() const
|
|
{
|
|
uint32_t mask = 0;
|
|
setBit(mask, FVBIT_TEXT_MESSAGE, hiddenFrames.textMessage);
|
|
setBit(mask, FVBIT_WAYPOINT, hiddenFrames.waypoint);
|
|
setBit(mask, FVBIT_WIFI, hiddenFrames.wifi);
|
|
setBit(mask, FVBIT_SYSTEM, hiddenFrames.system);
|
|
setBit(mask, FVBIT_HOME, hiddenFrames.home);
|
|
setBit(mask, FVBIT_CLOCK, hiddenFrames.clock);
|
|
#ifndef USE_EINK
|
|
setBit(mask, FVBIT_NODELIST_NODES, hiddenFrames.nodelist_nodes);
|
|
setBit(mask, FVBIT_NODELIST_LOCATION, hiddenFrames.nodelist_location);
|
|
#endif
|
|
#ifdef USE_EINK
|
|
setBit(mask, FVBIT_NODELIST_LASTHEARD, hiddenFrames.nodelist_lastheard);
|
|
setBit(mask, FVBIT_NODELIST_HOPSIGNAL, hiddenFrames.nodelist_hopsignal);
|
|
setBit(mask, FVBIT_NODELIST_DISTANCE, hiddenFrames.nodelist_distance);
|
|
#endif
|
|
#if HAS_GPS
|
|
#ifdef USE_EINK
|
|
setBit(mask, FVBIT_NODELIST_BEARINGS, hiddenFrames.nodelist_bearings);
|
|
#endif
|
|
setBit(mask, FVBIT_GPS, hiddenFrames.gps);
|
|
#endif
|
|
setBit(mask, FVBIT_LORA, hiddenFrames.lora);
|
|
setBit(mask, FVBIT_SHOW_FAVORITES, hiddenFrames.show_favorites);
|
|
setBit(mask, FVBIT_CHIRPY, hiddenFrames.chirpy);
|
|
return mask;
|
|
}
|
|
|
|
void Screen::applyHiddenFramesMask(uint32_t mask)
|
|
{
|
|
hiddenFrames.textMessage = getBit(mask, FVBIT_TEXT_MESSAGE);
|
|
hiddenFrames.waypoint = getBit(mask, FVBIT_WAYPOINT);
|
|
hiddenFrames.wifi = getBit(mask, FVBIT_WIFI);
|
|
hiddenFrames.system = getBit(mask, FVBIT_SYSTEM);
|
|
hiddenFrames.home = getBit(mask, FVBIT_HOME);
|
|
hiddenFrames.clock = getBit(mask, FVBIT_CLOCK);
|
|
#ifndef USE_EINK
|
|
hiddenFrames.nodelist_nodes = getBit(mask, FVBIT_NODELIST_NODES);
|
|
hiddenFrames.nodelist_location = getBit(mask, FVBIT_NODELIST_LOCATION);
|
|
#endif
|
|
#ifdef USE_EINK
|
|
hiddenFrames.nodelist_lastheard = getBit(mask, FVBIT_NODELIST_LASTHEARD);
|
|
hiddenFrames.nodelist_hopsignal = getBit(mask, FVBIT_NODELIST_HOPSIGNAL);
|
|
hiddenFrames.nodelist_distance = getBit(mask, FVBIT_NODELIST_DISTANCE);
|
|
#endif
|
|
#if HAS_GPS
|
|
#ifdef USE_EINK
|
|
hiddenFrames.nodelist_bearings = getBit(mask, FVBIT_NODELIST_BEARINGS);
|
|
#endif
|
|
hiddenFrames.gps = getBit(mask, FVBIT_GPS);
|
|
#endif
|
|
hiddenFrames.lora = getBit(mask, FVBIT_LORA);
|
|
hiddenFrames.show_favorites = getBit(mask, FVBIT_SHOW_FAVORITES);
|
|
hiddenFrames.chirpy = getBit(mask, FVBIT_CHIRPY);
|
|
}
|
|
|
|
void Screen::loadFrameVisibility()
|
|
{
|
|
#ifdef FSCom
|
|
spiLock->lock();
|
|
auto file = FSCom.open(frameVisibilityFileName, FILE_O_READ);
|
|
if (file) {
|
|
FrameVisFile data{};
|
|
bool ok = file.read((uint8_t *)&data, sizeof(data)) == sizeof(data) && data.magic == FRAMEVIS_MAGIC &&
|
|
data.version == FRAMEVIS_VERSION;
|
|
file.close();
|
|
spiLock->unlock();
|
|
if (ok) {
|
|
applyHiddenFramesMask(data.mask);
|
|
LOG_INFO("Loaded frame visibility (mask 0x%08x)", data.mask);
|
|
} else {
|
|
LOG_WARN("Frame visibility file invalid, keeping defaults");
|
|
}
|
|
return;
|
|
}
|
|
spiLock->unlock();
|
|
LOG_DEBUG("No saved frame visibility, using defaults");
|
|
#endif
|
|
}
|
|
|
|
void Screen::saveFrameVisibility()
|
|
{
|
|
#ifdef FSCom
|
|
spiLock->lock();
|
|
FSCom.mkdir("/prefs");
|
|
if (FSCom.exists(frameVisibilityFileName))
|
|
FSCom.remove(frameVisibilityFileName);
|
|
|
|
auto file = FSCom.open(frameVisibilityFileName, FILE_O_WRITE);
|
|
if (file) {
|
|
FrameVisFile data{};
|
|
data.magic = FRAMEVIS_MAGIC;
|
|
data.version = FRAMEVIS_VERSION;
|
|
data.mask = packHiddenFrames();
|
|
file.write((uint8_t *)&data, sizeof(data));
|
|
file.flush();
|
|
file.close();
|
|
LOG_INFO("Saved frame visibility (mask 0x%08x)", data.mask);
|
|
} else {
|
|
LOG_WARN("Failed to open %s for writing", frameVisibilityFileName);
|
|
}
|
|
spiLock->unlock();
|
|
#endif
|
|
}
|
|
|
|
void Screen::handleStartFirmwareUpdateScreen()
|
|
{
|
|
LOG_DEBUG("Show firmware screen");
|
|
showingNormalScreen = false;
|
|
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame
|
|
|
|
static FrameCallback frames[] = {graphics::NotificationRenderer::drawFrameFirmware};
|
|
setFrameImmediateDraw(frames);
|
|
}
|
|
|
|
void Screen::blink()
|
|
{
|
|
#ifdef MESHTASTIC_LOCKDOWN
|
|
// L4: defensive guard. blink() paints arbitrary geometry, not node
|
|
// data, so it doesn't actually leak today. But it bypasses the normal
|
|
// ui->update() path that the lockdown short-circuit gates, so any
|
|
// future change that puts content into blink would silently leak past
|
|
// redaction. Refuse to draw when the redaction latch is set.
|
|
if (meshtastic_security::shouldRedactDisplay())
|
|
return;
|
|
#endif
|
|
setFastFramerate();
|
|
uint8_t count = 10;
|
|
dispdev->setBrightness(254);
|
|
while (count > 0) {
|
|
dispdev->fillRect(0, 0, dispdev->getWidth(), dispdev->getHeight());
|
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
|
prepareFrameColorRegions();
|
|
#endif
|
|
dispdev->display();
|
|
delay(50);
|
|
dispdev->clear();
|
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
|
prepareFrameColorRegions();
|
|
#endif
|
|
dispdev->display();
|
|
delay(50);
|
|
count = count - 1;
|
|
}
|
|
// The dispdev->setBrightness does not work for t-deck display, it seems to run the setBrightness function in
|
|
// OLEDDisplay.
|
|
dispdev->setBrightness(brightness);
|
|
}
|
|
|
|
void Screen::increaseBrightness()
|
|
{
|
|
brightness = ((brightness + 62) > 254) ? brightness : (brightness + 62);
|
|
|
|
#if defined(ST7789_CS)
|
|
// run the setDisplayBrightness function. This works on t-decks
|
|
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
|
|
#endif
|
|
|
|
/* TO DO: add little popup in center of screen saying what brightness level it is set to*/
|
|
}
|
|
|
|
void Screen::decreaseBrightness()
|
|
{
|
|
brightness = (brightness < 70) ? brightness : (brightness - 62);
|
|
|
|
#if defined(ST7789_CS)
|
|
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
|
|
#endif
|
|
|
|
/* TO DO: add little popup in center of screen saying what brightness level it is set to*/
|
|
}
|
|
|
|
void Screen::handleOnPress()
|
|
{
|
|
// If screen was off, just wake it, otherwise advance to next frame
|
|
// If we are in a transition, the press must have bounced, drop it.
|
|
if (ui->getUiState()->frameState == FIXED) {
|
|
ui->nextFrame();
|
|
lastScreenTransition = millis();
|
|
setFastFramerate();
|
|
}
|
|
}
|
|
|
|
#ifdef USERPREFS_UI_TEST_LOG
|
|
void Screen::logFrameChange(const char *reason, uint8_t targetIdx)
|
|
{
|
|
// Reverse-map an index to a stable name string keyed off FramePositions
|
|
// field names - so the pytest harness can assert `name=nodelist_nodes`
|
|
// without caring about how the positions were ordered this boot.
|
|
const auto &p = framesetInfo.positions;
|
|
const char *name = "unknown";
|
|
if (targetIdx == p.home)
|
|
name = "home";
|
|
else if (targetIdx == p.deviceFocused)
|
|
name = "deviceFocused";
|
|
else if (targetIdx == p.textMessage)
|
|
name = "textMessage";
|
|
else if (targetIdx == p.nodelist_nodes)
|
|
name = "nodelist_nodes";
|
|
else if (targetIdx == p.nodelist_location)
|
|
name = "nodelist_location";
|
|
else if (targetIdx == p.nodelist_lastheard)
|
|
name = "nodelist_lastheard";
|
|
else if (targetIdx == p.nodelist_hopsignal)
|
|
name = "nodelist_hopsignal";
|
|
else if (targetIdx == p.nodelist_distance)
|
|
name = "nodelist_distance";
|
|
else if (targetIdx == p.nodelist_bearings)
|
|
name = "nodelist_bearings";
|
|
else if (targetIdx == p.system)
|
|
name = "system";
|
|
else if (targetIdx == p.gps)
|
|
name = "gps";
|
|
else if (targetIdx == p.lora)
|
|
name = "lora";
|
|
else if (targetIdx == p.clock)
|
|
name = "clock";
|
|
else if (targetIdx == p.chirpy)
|
|
name = "chirpy";
|
|
else if (targetIdx == p.fault)
|
|
name = "fault";
|
|
else if (targetIdx == p.waypoint)
|
|
name = "waypoint";
|
|
else if (targetIdx == p.focusedModule)
|
|
name = "focusedModule";
|
|
else if (targetIdx == p.log)
|
|
name = "log";
|
|
else if (targetIdx == p.settings)
|
|
name = "settings";
|
|
else if (targetIdx == p.wifi)
|
|
name = "wifi";
|
|
else if (p.firstFavorite != 255 && p.lastFavorite != 255 && targetIdx >= p.firstFavorite && targetIdx <= p.lastFavorite)
|
|
name = "favorite";
|
|
LOG_INFO("Screen: frame %u/%u name=%s reason=%s", (unsigned)targetIdx, (unsigned)framesetInfo.frameCount, name, reason);
|
|
}
|
|
#endif
|
|
|
|
void Screen::showFrame(FrameDirection direction)
|
|
{
|
|
// Only advance frames when UI is stable
|
|
if (ui->getUiState()->frameState == FIXED) {
|
|
|
|
#ifdef USERPREFS_UI_TEST_LOG
|
|
// Log the *intended* target before the (async) transition fires, so
|
|
// tests see a deterministic record of what was requested.
|
|
if (framesetInfo.frameCount > 0) {
|
|
uint8_t curr = ui->getUiState()->currentFrame;
|
|
uint8_t target = (direction == FrameDirection::NEXT)
|
|
? (uint8_t)((curr + 1) % framesetInfo.frameCount)
|
|
: (uint8_t)((curr + framesetInfo.frameCount - 1) % framesetInfo.frameCount);
|
|
logFrameChange(direction == FrameDirection::NEXT ? "next" : "prev", target);
|
|
}
|
|
#endif
|
|
|
|
if (direction == FrameDirection::NEXT) {
|
|
ui->nextFrame();
|
|
} else {
|
|
ui->previousFrame();
|
|
}
|
|
|
|
lastScreenTransition = millis();
|
|
setFastFramerate();
|
|
}
|
|
}
|
|
|
|
#ifndef SCREEN_TRANSITION_FRAMERATE
|
|
#define SCREEN_TRANSITION_FRAMERATE 30 // fps
|
|
#endif
|
|
|
|
void Screen::setFastFramerate()
|
|
{
|
|
#if defined(OLED_TINY)
|
|
dispdev->clear();
|
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
|
prepareFrameColorRegions();
|
|
#endif
|
|
dispdev->display();
|
|
#endif
|
|
// We are about to start a transition so speed up fps
|
|
targetFramerate = SCREEN_TRANSITION_FRAMERATE;
|
|
|
|
ui->setTargetFPS(targetFramerate);
|
|
setInterval(0); // redraw ASAP
|
|
runASAP = true;
|
|
}
|
|
|
|
int Screen::handleStatusUpdate(const meshtastic::Status *arg)
|
|
{
|
|
switch (arg->getStatusType()) {
|
|
case STATUS_TYPE_NODE:
|
|
if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) {
|
|
setFrames(FOCUS_PRESERVE); // Regen the list of screen frames (returning to same frame, if possible)
|
|
}
|
|
nodeDB->updateGUI = false;
|
|
break;
|
|
case STATUS_TYPE_POWER: {
|
|
bool currentUSB = powerStatus->getHasUSB();
|
|
if (currentUSB != lastPowerUSBState) {
|
|
lastPowerUSBState = currentUSB;
|
|
forceDisplay(true);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Triggered by MeshModules
|
|
int Screen::handleUIFrameEvent(const UIFrameEvent *event)
|
|
{
|
|
// Block UI frame events when virtual keyboard is active
|
|
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
|
return 0;
|
|
}
|
|
|
|
if (showingNormalScreen) {
|
|
// Regenerate the frameset, potentially honoring a module's internal requestFocus() call
|
|
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) {
|
|
setFrames(FOCUS_MODULE);
|
|
}
|
|
|
|
// Regenerate the frameset, while attempting to maintain focus on the current frame
|
|
else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) {
|
|
setFrames(FOCUS_PRESERVE);
|
|
}
|
|
|
|
// Don't regenerate the frameset, just re-draw whatever is on screen ASAP
|
|
else if (event->action == UIFrameEvent::Action::REDRAW_ONLY) {
|
|
setFastFramerate();
|
|
}
|
|
|
|
// Jump directly to the Text Message screen
|
|
else if (event->action == UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE) {
|
|
setFrames(FOCUS_PRESERVE); // preserve current frame ordering
|
|
ui->switchToFrame(framesetInfo.positions.textMessage);
|
|
setFastFramerate(); // force redraw ASAP
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int Screen::handleInputEvent(const InputEvent *event)
|
|
{
|
|
LOG_INPUT("Screen Input event %u! kb %u", event->inputEvent, event->kbchar);
|
|
if (!screenOn)
|
|
return 0;
|
|
|
|
// Handle text input notifications specially - pass input to virtual keyboard
|
|
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
|
NotificationRenderer::inEvent = *event;
|
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
|
ui->setOverlays(overlays, 2);
|
|
setFastFramerate(); // Draw ASAP
|
|
updateUiFrame(ui);
|
|
return 0;
|
|
}
|
|
|
|
#ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw.
|
|
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please
|
|
EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update
|
|
handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?)
|
|
setFastFramerate(); // Draw ASAP
|
|
#endif
|
|
if (NotificationRenderer::isOverlayBannerShowing()) {
|
|
NotificationRenderer::inEvent = *event;
|
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
|
ui->setOverlays(overlays, 2);
|
|
setFastFramerate(); // Draw ASAP
|
|
updateUiFrame(ui);
|
|
|
|
menuHandler::handleMenuSwitch(dispdev);
|
|
return 0;
|
|
}
|
|
// UP/DOWN in message screen scrolls through message threads
|
|
if (ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
|
|
|
|
if (event->inputEvent == INPUT_BROKER_UP) {
|
|
if (messageStore.getMessages().empty()) {
|
|
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
|
} else {
|
|
graphics::MessageRenderer::scrollUp();
|
|
setFastFramerate(); // match existing behavior
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
if (event->inputEvent == INPUT_BROKER_DOWN) {
|
|
if (messageStore.getMessages().empty()) {
|
|
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
|
} else {
|
|
graphics::MessageRenderer::scrollDown();
|
|
setFastFramerate();
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
// UP/DOWN in node list screens scrolls through node pages
|
|
if (ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes ||
|
|
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location ||
|
|
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard ||
|
|
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
|
|
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance ||
|
|
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) {
|
|
if (event->inputEvent == INPUT_BROKER_UP) {
|
|
graphics::NodeListRenderer::scrollUp();
|
|
setFastFramerate();
|
|
return 0;
|
|
}
|
|
|
|
if (event->inputEvent == INPUT_BROKER_DOWN) {
|
|
graphics::NodeListRenderer::scrollDown();
|
|
setFastFramerate();
|
|
return 0;
|
|
}
|
|
}
|
|
// Use left or right input from a keyboard to move between frames,
|
|
// so long as a mesh module isn't using these events for some other purpose
|
|
if (showingNormalScreen) {
|
|
|
|
// Ask any MeshModules if they're handling keyboard input right now
|
|
bool inputIntercepted = false;
|
|
for (MeshModule *module : moduleFrames) {
|
|
if (module && module->interceptingKeyboardInput())
|
|
inputIntercepted = true;
|
|
}
|
|
|
|
// If no modules are using the input, move between frames
|
|
if (!inputIntercepted) {
|
|
#if defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2
|
|
bool handledEncoderScroll = false;
|
|
const bool isTextMessageFrame = (framesetInfo.positions.textMessage != 255 &&
|
|
this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage &&
|
|
!messageStore.getMessages().empty());
|
|
if (isTextMessageFrame) {
|
|
if (event->inputEvent == INPUT_BROKER_UP_LONG) {
|
|
graphics::MessageRenderer::nudgeScroll(-1);
|
|
handledEncoderScroll = true;
|
|
} else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) {
|
|
graphics::MessageRenderer::nudgeScroll(1);
|
|
handledEncoderScroll = true;
|
|
}
|
|
}
|
|
|
|
if (handledEncoderScroll) {
|
|
setFastFramerate();
|
|
return 0;
|
|
}
|
|
#endif
|
|
if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) {
|
|
showFrame(FrameDirection::PREVIOUS);
|
|
} else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) {
|
|
showFrame(FrameDirection::NEXT);
|
|
} else if (event->inputEvent == INPUT_BROKER_FN_F1) {
|
|
this->ui->switchToFrame(0);
|
|
#ifdef USERPREFS_UI_TEST_LOG
|
|
logFrameChange("fn_f1", 0);
|
|
#endif
|
|
lastScreenTransition = millis();
|
|
setFastFramerate();
|
|
} else if (event->inputEvent == INPUT_BROKER_FN_F2) {
|
|
this->ui->switchToFrame(1);
|
|
#ifdef USERPREFS_UI_TEST_LOG
|
|
logFrameChange("fn_f2", 1);
|
|
#endif
|
|
lastScreenTransition = millis();
|
|
setFastFramerate();
|
|
} else if (event->inputEvent == INPUT_BROKER_FN_F3) {
|
|
this->ui->switchToFrame(2);
|
|
#ifdef USERPREFS_UI_TEST_LOG
|
|
logFrameChange("fn_f3", 2);
|
|
#endif
|
|
lastScreenTransition = millis();
|
|
setFastFramerate();
|
|
} else if (event->inputEvent == INPUT_BROKER_FN_F4) {
|
|
this->ui->switchToFrame(3);
|
|
#ifdef USERPREFS_UI_TEST_LOG
|
|
logFrameChange("fn_f4", 3);
|
|
#endif
|
|
lastScreenTransition = millis();
|
|
setFastFramerate();
|
|
} else if (event->inputEvent == INPUT_BROKER_FN_F5) {
|
|
this->ui->switchToFrame(4);
|
|
#ifdef USERPREFS_UI_TEST_LOG
|
|
logFrameChange("fn_f5", 4);
|
|
#endif
|
|
lastScreenTransition = millis();
|
|
setFastFramerate();
|
|
} else if (event->inputEvent == INPUT_BROKER_UP_LONG) {
|
|
// Long press up button for fast frame switching
|
|
showPrevFrame();
|
|
} else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) {
|
|
// Long press down button for fast frame switching
|
|
showNextFrame();
|
|
} else if ((event->inputEvent == INPUT_BROKER_UP || event->inputEvent == INPUT_BROKER_DOWN) &&
|
|
this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
|
|
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
|
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
|
|
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
|
|
menuHandler::homeBaseMenu();
|
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) {
|
|
menuHandler::systemBaseMenu();
|
|
#if HAS_GPS
|
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) {
|
|
menuHandler::positionBaseMenu();
|
|
#endif
|
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) {
|
|
menuHandler::clockMenu();
|
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
|
|
menuHandler::loraMenu();
|
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
|
|
if (!messageStore.getMessages().empty()) {
|
|
menuHandler::messageResponseMenu();
|
|
} else {
|
|
if (currentResolution == ScreenResolution::UltraLow) {
|
|
menuHandler::textMessageMenu();
|
|
} else {
|
|
menuHandler::textMessageBaseMenu();
|
|
}
|
|
}
|
|
} else if (framesetInfo.positions.firstFavorite != 255 &&
|
|
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
|
|
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
|
|
menuHandler::favoriteBaseMenu();
|
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes ||
|
|
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location ||
|
|
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard ||
|
|
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
|
|
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance ||
|
|
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
|
|
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) {
|
|
menuHandler::nodeListMenu();
|
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.wifi) {
|
|
menuHandler::wifiBaseMenu();
|
|
}
|
|
} else if (event->inputEvent == INPUT_BROKER_BACK) {
|
|
showFrame(FrameDirection::PREVIOUS);
|
|
} else if (event->inputEvent == INPUT_BROKER_CANCEL) {
|
|
setOn(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int Screen::handleAdminMessage(AdminModule_ObserverData *arg)
|
|
{
|
|
switch (arg->request->which_payload_variant) {
|
|
// Node removed manually (i.e. via app)
|
|
case meshtastic_AdminMessage_remove_by_nodenum_tag:
|
|
setFrames(FOCUS_PRESERVE);
|
|
*arg->result = AdminMessageHandleResult::HANDLED;
|
|
break;
|
|
|
|
// Default no-op, in case the admin message observable gets used by other classes in future
|
|
default:
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
bool Screen::isOverlayBannerShowing()
|
|
{
|
|
return NotificationRenderer::isOverlayBannerShowing();
|
|
}
|
|
|
|
} // namespace graphics
|
|
|
|
#else
|
|
graphics::Screen::Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY) {}
|
|
#endif // HAS_SCREEN
|
|
|
|
bool shouldWakeOnReceivedMessage()
|
|
{
|
|
/*
|
|
The goal here is to determine when we do NOT wake up the screen on message received:
|
|
- Any ext. notifications are turned on
|
|
- If role is not CLIENT / CLIENT_MUTE / CLIENT_HIDDEN / CLIENT_BASE
|
|
- If the battery level is very low
|
|
*/
|
|
if (moduleConfig.external_notification.enabled) {
|
|
return false;
|
|
}
|
|
if (!IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT,
|
|
meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN,
|
|
meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) {
|
|
return false;
|
|
}
|
|
if (powerStatus && powerStatus->getBatteryChargePercent() < 10) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|