New Feature

Iconed Screen navigation bar.
This commit is contained in:
HarukiToreda 2025-05-04 20:21:02 -04:00
parent 33093e28fa
commit 75c5080fd9
3 changed files with 294 additions and 56 deletions

View File

@ -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<size_t>(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<AutoOLEDWire *>(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<ST7789Spi *>(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<TFTDisplay *>(dispdev)->flipScreenVertically();
#elif defined(USE_ST7789)
#elif defined(USE_ST7789)
static_cast<ST7789Spi *>(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<TFTDisplay *>(dispdev)->getTouch);
touchScreenImpl1 = new TouchScreenImpl1(
dispdev->getWidth(), dispdev->getHeight(),
static_cast<TFTDisplay *>(dispdev)->getTouch
);
touchScreenImpl1->init();
}
#elif HAS_TOUCHSCREEN
touchScreenImpl1 =
new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast<TFTDisplay *>(dispdev)->getTouch);
touchScreenImpl1 = new TouchScreenImpl1(
dispdev->getWidth(), dispdev->getHeight(),
static_cast<TFTDisplay *>(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)

View File

@ -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<const uint8_t *> indicatorIcons; // Per-frame custom icon pointers
Screen(const Screen &) = delete;
Screen &operator=(const Screen &) = delete;

View File

@ -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"