diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 4a70c1905..b11239773 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2369,6 +2369,26 @@ static void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState * } #endif +// Add these below (still inside #ifdef USE_EINK if you prefer): +#ifdef USE_EINK +static void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + const char *title = "Node List"; + drawNodeListScreen(display, state, x, y, title, drawEntryLastHeard); +} + +static void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + const char *title = (display->getWidth() > 128) ? "Hops|Signals" : "Hop|Sig"; + drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal); +} + +static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + const char *title = "Distances"; + drawNodeListScreen(display, state, x, y, title, drawNodeDistance); +} +#endif // Helper function: Draw a single node entry for Node List (Modified for Compass Screen) void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { @@ -3116,13 +3136,60 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) screenOn = on; } } +static int8_t lastFrameIndex = -1; +static uint32_t lastFrameChangeTime = 0; +constexpr uint32_t ICON_DISPLAY_DURATION_MS = 1000; + +void drawCustomFrameIcons(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + int currentFrame = state->currentFrame; + + // Detect frame change and record time + if (currentFrame != lastFrameIndex) { + lastFrameIndex = currentFrame; + lastFrameChangeTime = millis(); + } + + // Only show bar briefly after switching frames + if (millis() - lastFrameChangeTime > ICON_DISPLAY_DURATION_MS) return; + + const int iconSize = 8; + const int spacing = 2; + size_t totalIcons = screen->indicatorIcons.size(); + if (totalIcons == 0) return; + + int totalWidth = totalIcons * iconSize + (totalIcons - 1) * spacing; + int xStart = (SCREEN_WIDTH - totalWidth) / 2; + int y = SCREEN_HEIGHT - iconSize - 1; + + // Clear background under icon bar to avoid overlaps + display->setColor(BLACK); + display->fillRect(xStart - 1, y - 2, totalWidth + 2, iconSize + 4); + display->setColor(WHITE); + + for (size_t i = 0; i < totalIcons; ++i) { + const uint8_t* icon = screen->indicatorIcons[i]; + int x = xStart + i * (iconSize + spacing); + + if (i == static_cast(currentFrame)) { + // Draw white box and invert icon for visibility + display->setColor(WHITE); + display->fillRect(x - 1, y - 1, iconSize + 2, iconSize + 2); + display->setColor(BLACK); + display->drawXbm(x, y, iconSize, iconSize, icon); + display->setColor(WHITE); + } else { + display->drawXbm(x, y, iconSize, iconSize, icon); + } + } +} void Screen::setup() { - // We don't set useDisplay until setup() is called, because some boards have a declaration of this object but the device - // is never found when probing i2c and therefore we don't call setup and never want to do (invalid) accesses to this device. + // === Enable display rendering === useDisplay = true; + // === Detect OLED subtype (if supported by board variant) === #ifdef AutoOLEDWire_h if (isAUTOOled) static_cast(dispdev)->setDetected(model); @@ -3133,110 +3200,110 @@ void Screen::setup() #endif #if defined(USE_ST7789) && defined(TFT_MESH) - // Heltec T114 and T190: honor a custom text color, if defined in variant.h + // Apply custom RGB color (e.g. Heltec T114/T190) static_cast(dispdev)->setRGB(TFT_MESH); #endif - // Initialising the UI will init the display too. + // === Initialize display and UI system === ui->init(); - displayWidth = dispdev->width(); displayHeight = dispdev->height(); - ui->setTimePerTransition(0); + 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 - ui->setIndicatorPosition(BOTTOM); - // Defines where the first frame is located in the bar. - ui->setIndicatorDirection(LEFT_RIGHT); - ui->setFrameAnimation(SLIDE_LEFT); - // Don't show the page swipe dots while in boot screen. - ui->disableAllIndicators(); - // Store a pointer to Screen so we can get to it from static functions. - ui->getUiState()->userData = this; + // === Set custom overlay callbacks === + static OverlayCallback overlays[] = { + drawFunctionOverlay, // For mute/buzzer modifiers etc. + drawCustomFrameIcons // Custom indicator icons for each frame + }; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - // Set the utf8 conversion function + // === Enable UTF-8 to display mapping === dispdev->setFontTableLookupFunction(customFontTableLookup); #ifdef USERPREFS_OEM_TEXT - logo_timeout *= 2; // Double the time if we have a custom logo + logo_timeout *= 2; // Give more time for branded boot logos #endif - // Add frames. - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); - alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + // === 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) { + if (wakeCause == ESP_SLEEP_WAKEUP_TIMER || wakeCause == ESP_SLEEP_WAKEUP_EXT1) drawFrameText(display, state, x, y, "Resuming..."); - } else + else #endif { - // Draw region in upper left - const char *region = myRegion ? myRegion->name : NULL; + const char *region = myRegion ? myRegion->name : nullptr; drawIconScreen(region, display, state, x, y); } }; ui->setFrames(alertFrames, 1); - // No overlays. - ui->setOverlays(nullptr, 0); + ui->disableAutoTransition(); // Require manual navigation between frames - // Require presses to switch between frames. - ui->disableAutoTransition(); - - // Set up a log buffer with 3 lines, 32 chars each. + // === 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 - // Standard behaviour is to FLIP the screen (needed on T-Beam). If this config item is set, unflip it, and thereby logically - // flip it. If you have a headache now, you're welcome. if (!config.display.flip_screen) { -#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || \ - defined(ST7789_CS) || defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) + #if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ + defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) static_cast(dispdev)->flipScreenVertically(); -#elif defined(USE_ST7789) + #elif defined(USE_ST7789) static_cast(dispdev)->flipScreenVertically(); -#else + #else dispdev->flipScreenVertically(); -#endif + #endif } #endif - // Get our hardware ID + // === Generate device ID from MAC address === uint8_t dmac[6]; getMacAddr(dmac); snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); + #if ARCH_PORTDUINO - handleSetOn(false); // force clean init + handleSetOn(false); // Ensure proper init for Arduino targets #endif - // Turn on the display. + // === Turn on display and trigger first draw === handleSetOn(true); - - // On some ssd1306 clones, the first draw command is discarded, so draw it - // twice initially. Skip this for EINK Displays to save a few seconds during boot ui->update(); #ifndef USE_EINK - ui->update(); + ui->update(); // Some SSD1306 clones drop the first draw, so run twice #endif serialSinceMsec = millis(); + // === Optional touchscreen support === #if ARCH_PORTDUINO && !HAS_TFT if (settingsMap[touchscreenModule]) { - touchScreenImpl1 = - new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); + touchScreenImpl1 = new TouchScreenImpl1( + dispdev->getWidth(), dispdev->getHeight(), + static_cast(dispdev)->getTouch + ); touchScreenImpl1->init(); } #elif HAS_TOUCHSCREEN - touchScreenImpl1 = - new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); + touchScreenImpl1 = new TouchScreenImpl1( + dispdev->getWidth(), dispdev->getHeight(), + static_cast(dispdev)->getTouch + ); touchScreenImpl1->init(); #endif - // Subscribe to status updates + // === 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 @@ -3245,7 +3312,7 @@ void Screen::setup() if (inputBroker) inputObserver.observe(inputBroker); - // Modules can notify screen about refresh + // === Notify modules that support UI events === MeshModule::observeUIEvents(&uiFrameEventObserver); } @@ -3394,6 +3461,9 @@ int32_t Screen::runOnce() // 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. + // === Auto-hide indicator icons unless in transition === + OLEDDisplayUiState *state = ui->getUiState(); + if (targetFramerate != IDLE_FRAMERATE && ui->getUiState()->frameState == FIXED) { // oldFrameState = ui->getUiState()->frameState; targetFramerate = IDLE_FRAMERATE; @@ -3409,8 +3479,8 @@ int32_t Screen::runOnce() if (config.display.auto_screen_carousel_secs > 0 && !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 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 @@ -3532,6 +3602,7 @@ void Screen::setFrames(FrameFocus focus) LOG_DEBUG("Show standard frames"); showingNormalScreen = true; + indicatorIcons.clear(); #ifdef USE_EINK // If user has disabled the screensaver, warn them after boot static bool warnedScreensaverDisabled = false; @@ -3573,6 +3644,7 @@ void Screen::setFrames(FrameFocus focus) if (m == waypointModule) fsi.positions.waypoint = numframes; + indicatorIcons.push_back(icon_module); numframes++; } @@ -3582,6 +3654,7 @@ void Screen::setFrames(FrameFocus focus) fsi.positions.fault = numframes; if (error_code) { normalFrames[numframes++] = drawCriticalFaultFrame; + indicatorIcons.push_back(icon_error); focus = FOCUS_FAULT; // Change our "focus" parameter, to ensure we show the fault frame } @@ -3595,22 +3668,40 @@ void Screen::setFrames(FrameFocus focus) if (willInsertTextMessage) { fsi.positions.textMessage = numframes; normalFrames[numframes++] = drawTextMessageFrame; + indicatorIcons.push_back(icon_mail); } normalFrames[numframes++] = drawDeviceFocused; + indicatorIcons.push_back(icon_home); + +#ifndef USE_EINK normalFrames[numframes++] = drawDynamicNodeListScreen; + indicatorIcons.push_back(icon_nodes); +#endif // Show detailed node views only on E-Ink builds #ifdef USE_EINK normalFrames[numframes++] = drawLastHeardScreen; + indicatorIcons.push_back(icon_nodes); + normalFrames[numframes++] = drawHopSignalScreen; + indicatorIcons.push_back(icon_signal); + normalFrames[numframes++] = drawDistanceScreen; + indicatorIcons.push_back(icon_distance); #endif normalFrames[numframes++] = drawNodeListWithCompasses; + indicatorIcons.push_back(icon_list); + normalFrames[numframes++] = drawCompassAndLocationScreen; + indicatorIcons.push_back(icon_compass); + normalFrames[numframes++] = drawLoRaFocused; + indicatorIcons.push_back(icon_radio); + normalFrames[numframes++] = drawMemoryScreen; + indicatorIcons.push_back(icon_memory); // then all the nodes // We only show a few nodes in our scrolling list - because meshes with many nodes would have too many screens @@ -3632,20 +3723,24 @@ void Screen::setFrames(FrameFocus focus) fsi.positions.wifi = numframes; #if HAS_WIFI && !defined(ARCH_PORTDUINO) if (isWifiAvailable()) { - // call a method on debugInfoScreen object (for more details) normalFrames[numframes++] = &Screen::drawDebugInfoWiFiTrampoline; + indicatorIcons.push_back(icon_wifi); } #endif 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->enableAllIndicators(); + ui->disableAllIndicators(); // Add function overlay here. This can show when notifications muted, modifier key is active etc - static OverlayCallback functionOverlay[] = {drawFunctionOverlay}; - ui->setOverlays(functionOverlay, sizeof(functionOverlay) / sizeof(functionOverlay[0])); + static OverlayCallback overlays[] = { + drawFunctionOverlay, + drawCustomFrameIcons + }; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list // just changed) diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index a66593d9f..8d101108f 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -181,9 +181,10 @@ class Screen : public concurrency::OSThread public: explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); - + size_t frameCount = 0; // Total number of active frames ~Screen(); + std::vector indicatorIcons; // Per-frame custom icon pointers Screen(const Screen &) = delete; Screen &operator=(const Screen &) = delete; diff --git a/src/graphics/images.h b/src/graphics/images.h index 1387cc60c..af459faa6 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -256,6 +256,148 @@ static const unsigned char mail[] PROGMEM = { 0b11111111, 0b00 // Bottom line }; +// 📬 Mail / Message +const uint8_t icon_mail[] PROGMEM = { + 0b11111111, // ████████ top border + 0b10000001, // █ █ sides + 0b11000011, // ██ ██ diagonal + 0b10100101, // █ █ █ █ inner M + 0b10011001, // █ ██ █ inner M + 0b10000001, // █ █ sides + 0b11111111, // ████████ bottom + 0b00000000 // (padding) +}; + +// 📍 GPS Screen / Location Pin +const uint8_t icon_compass[] PROGMEM = { + 0b00011000, // ██ + 0b00111100, // ████ + 0b01100110, // ██ ██ + 0b01000010, // █ █ + 0b01000010, // █ █ + 0b00111100, // ████ + 0b00011000, // ██ + 0b00010000 // █ +}; + +const uint8_t icon_radio[] PROGMEM = { + 0b00111000, // ░███░ + 0b01000100, // █░░░█ + 0b10000010, // █░░░░█ + 0b00010000, // ░░█░ + 0b00010000, // ░░█░ + 0b00111000, // ░███░ + 0b01111100, // █████ + 0b00000000 // ░░░░░ +}; + +// 🪙 Memory Drum Icon (Barrel shape with cuts on the sides) +const uint8_t icon_memory[] PROGMEM = { + 0b00111100, // ░░████░░ + 0b01111110, // ░██████░ + 0b11100111, // ███░░███ + 0b11100111, // ███░░███ + 0b11100111, // ███░░███ + 0b11100111, // ███░░███ + 0b01111110, // ░██████░ + 0b00111100 // ░░████░░ +}; + +// 🌐 Wi-Fi +const uint8_t icon_wifi[] PROGMEM = { + 0b00000000, + 0b00011000, + 0b00111100, + 0b01111110, + 0b11011011, + 0b00011000, + 0b00011000, + 0b00000000 +}; + +// 📄 Paper/List Icon (for DynamicNodeListScreen) +const uint8_t icon_nodes[] PROGMEM = { + 0b11111111, // Top edge of paper + 0b10000001, // Left & right margin + 0b10101001, // ••• line + 0b10000001, // + 0b10101001, // ••• line + 0b10000001, // + 0b11111111, // Bottom edge + 0b00000000 // +}; + +// ➤ Chevron Triangle Arrow Icon (8x8) +const uint8_t icon_list[] PROGMEM = { + 0b00011000, // ░░██░░ + 0b00011100, // ░░███░ + 0b00011110, // ░░████ + 0b11111111, // ██████ + 0b00011110, // ░░████ + 0b00011100, // ░░███░ + 0b00011000, // ░░██░░ + 0b00000000 // ░░░░░░ +}; + +// 📶 Signal Bars Icon (left to right, small to large with spacing) +const uint8_t icon_signal[] PROGMEM = { + 0b00000000, // ░░░░░░░ + 0b10000000, // ░░░░░░░ + 0b10100000, // ░░░░█░█ + 0b10100000, // ░░░░█░█ + 0b10101000, // ░░█░█░█ + 0b10101000, // ░░█░█░█ + 0b10101010, // █░█░█░█ + 0b11111111 // ███████ +}; + +// ↔️ Distance / Measurement Icon (double-ended arrow) +const uint8_t icon_distance[] PROGMEM = { + 0b00000000, // ░░░░░░░░ + 0b10000001, // █░░░░░█ arrowheads + 0b01000010, // ░█░░░█░ + 0b00100100, // ░░█░█░░ + 0b00011000, // ░░░██░░ center + 0b00100100, // ░░█░█░░ + 0b01000010, // ░█░░░█░ + 0b10000001 // █░░░░░█ +}; + +// ⚠️ Error / Fault +const uint8_t icon_error[] PROGMEM = { + 0b00011000, // ░░░██░░░ + 0b00011000, // ░░░██░░░ + 0b00011000, // ░░░██░░░ + 0b00011000, // ░░░██░░░ + 0b00000000, // ░░░░░░░░ + 0b00011000, // ░░░██░░░ + 0b00000000, // ░░░░░░░░ + 0b00000000 // ░░░░░░░░ +}; + +// 🏠 Optimized Home Icon (8x8) +const uint8_t icon_home[] PROGMEM = { + 0b00011000, // ██ + 0b00111100, // ████ + 0b01111110, // ██████ + 0b11111111, // ███████ + 0b11000011, // ██ ██ + 0b11011011, // ██ ██ ██ + 0b11011011, // ██ ██ ██ + 0b11111111 // ███████ +}; + +// 🔧 Generic module (gear-like shape) +const uint8_t icon_module[] PROGMEM = { + 0b00011000, // ░░░██░░░ + 0b00111100, // ░░████░░ + 0b01111110, // ░██████░ + 0b11011011, // ██░██░██ + 0b11011011, // ██░██░██ + 0b01111110, // ░██████░ + 0b00111100, // ░░████░░ + 0b00011000 // ░░░██░░░ +}; #endif #include "img/icon.xbm"