diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp
index 0818619a6..c8c9d8b74 100644
--- a/src/graphics/Screen.cpp
+++ b/src/graphics/Screen.cpp
@@ -31,6 +31,7 @@ along with this program. If not, see .
#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"
@@ -135,13 +136,17 @@ extern bool hasUnreadMessage;
// The banner appears in the center of the screen and disappears after the specified duration
// Called to trigger a banner with custom message and duration
-void Screen::showOverlayBanner(const char *message, uint32_t durationMs, uint8_t options, std::function bannerCallback,
- int8_t InitialSelected)
+void Screen::showOverlayBanner(const char *message, uint32_t durationMs, const char **optionsArrayPtr, uint8_t options,
+ std::function bannerCallback, int8_t InitialSelected)
{
+#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::optionsArrayPtr = optionsArrayPtr;
NotificationRenderer::alertBannerOptions = options;
NotificationRenderer::alertBannerCallback = bannerCallback;
NotificationRenderer::curSelected = InitialSelected;
@@ -203,7 +208,7 @@ float Screen::estimatedHeading(double lat, double lon)
if (d < 10) // haven't moved enough, just keep current bearing
return b;
- b = GeoCoord::bearing(oldLat, oldLon, lat, lon);
+ b = GeoCoord::bearing(oldLat, oldLon, lat, lon) * RAD_TO_DEG;
oldLat = lat;
oldLon = lon;
@@ -413,8 +418,7 @@ void Screen::setup()
// === Set custom overlay callbacks ===
static OverlayCallback overlays[] = {
- graphics::UIRenderer::drawFunctionOverlay, // For mute/buzzer modifiers etc.
- graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
+ graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
@@ -471,6 +475,7 @@ void Screen::setup()
// === Turn on display and trigger first draw ===
handleSetOn(true);
+ determineResolution(dispdev->height(), dispdev->width());
ui->update();
#ifndef USE_EINK
ui->update(); // Some SSD1306 clones drop the first draw, so run twice
@@ -557,6 +562,7 @@ int32_t Screen::runOnce()
if (displayHeight == 0) {
displayHeight = dispdev->getHeight();
}
+ menuHandler::handleMenuSwitch();
// Show boot screen for first logo_timeout seconds, then switch to normal operation.
// serialSinceMsec adjusts for additional serial wait time during nRF52 bootup
@@ -585,7 +591,7 @@ int32_t Screen::runOnce()
#ifndef DISABLE_WELCOME_UNSET
if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
- LoraRegionPicker(0);
+ menuHandler::LoraRegionPicker(0);
}
#endif
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) {
@@ -768,32 +774,6 @@ void Screen::setFrames(FrameFocus focus)
indicatorIcons.clear();
size_t numframes = 0;
- moduleFrames = MeshModule::GetMeshModulesWithUIFrames();
- LOG_DEBUG("Show %d module frames", moduleFrames.size());
-
- // put all of the module frames first.
- // this is a little bit of a dirty hack; since we're going to call
- // the same drawModuleFrame handler here for all of these module frames
- // and then we'll just assume that the state->currentFrame value
- // is the same offset into the moduleFrames vector
- // so that we can invoke the module's callback
- for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) {
- // Draw the module frame, using the hack described above
- 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->isRequestingFocus())
- fsi.positions.focusedModule = numframes;
- if (m == waypointModule)
- fsi.positions.waypoint = numframes;
-
- indicatorIcons.push_back(icon_module);
- numframes++;
- }
-
- LOG_DEBUG("Added modules. numframes: %d", numframes);
// If we have a critical fault, show it first
fsi.positions.fault = numframes;
@@ -807,7 +787,7 @@ void Screen::setFrames(FrameFocus focus)
fsi.positions.clock = numframes;
normalFrames[numframes++] = graphics::ClockRenderer::digitalWatchFace ? graphics::ClockRenderer::drawDigitalClockFrame
: &graphics::ClockRenderer::drawAnalogClockFrame;
- indicatorIcons.push_back(icon_clock);
+ indicatorIcons.push_back(digital_icon_clock);
#endif
// Declare this early so it’s available in FOCUS_PRESERVE block
@@ -822,22 +802,27 @@ void Screen::setFrames(FrameFocus focus)
indicatorIcons.push_back(icon_mail);
#ifndef USE_EINK
+ fsi.positions.nodelist = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen;
indicatorIcons.push_back(icon_nodes);
#endif
// Show detailed node views only on E-Ink builds
#ifdef USE_EINK
+ fsi.positions.nodelist_lastheard = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen;
indicatorIcons.push_back(icon_nodes);
+ fsi.positions.nodelist_hopsignal = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen;
indicatorIcons.push_back(icon_signal);
+ fsi.positions.nodelist_distance = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen;
indicatorIcons.push_back(icon_distance);
#endif
#if HAS_GPS
+ fsi.positions.nodelist_bearings = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
indicatorIcons.push_back(icon_list);
@@ -857,8 +842,9 @@ void Screen::setFrames(FrameFocus focus)
}
#if !defined(DISPLAY_CLOCK_FRAME)
fsi.positions.clock = numframes;
- normalFrames[numframes++] = graphics::ClockRenderer::drawDigitalClockFrame;
- indicatorIcons.push_back(icon_clock);
+ normalFrames[numframes++] = graphics::ClockRenderer::digitalWatchFace ? graphics::ClockRenderer::drawDigitalClockFrame
+ : graphics::ClockRenderer::drawAnalogClockFrame;
+ indicatorIcons.push_back(digital_icon_clock);
#endif
// We don't show the node info of our node (if we have it yet - we should)
@@ -885,6 +871,36 @@ void Screen::setFrames(FrameFocus focus)
}
#endif
+ // Beware of what changes you make in this code!
+ // We pass numfames 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);
+
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);
@@ -916,6 +932,11 @@ void Screen::setFrames(FrameFocus focus)
// 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_PRESERVE:
// No more adjustment — force stay on same index
@@ -1204,6 +1225,8 @@ int Screen::handleInputEvent(const InputEvent *event)
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
setFastFramerate(); // Draw ASAP
ui->update();
+
+ menuHandler::handleMenuSwitch();
return 0;
}
/*
@@ -1229,7 +1252,7 @@ int Screen::handleInputEvent(const InputEvent *event)
// Ask any MeshModules if they're handling keyboard input right now
bool inputIntercepted = false;
for (MeshModule *module : moduleFrames) {
- if (module->interceptingKeyboardInput())
+ if (module && module->interceptingKeyboardInput())
inputIntercepted = true;
}
@@ -1241,129 +1264,36 @@ int Screen::handleInputEvent(const InputEvent *event)
showNextFrame();
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
- const char *banner_message;
- int options;
- if (kb_found) {
- banner_message = "Action?\nBack\nSleep Screen\nNew Preset Msg\nNew Freetext Msg";
- options = 4;
- } else {
- banner_message = "Action?\nBack\nSleep Screen\nNew Preset Msg";
- options = 3;
- }
- showOverlayBanner(banner_message, 30000, options, [](int selected) -> void {
- if (selected == 1) {
- screen->setOn(false);
- } else if (selected == 2) {
- cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
- } else if (selected == 3) {
- cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST);
- }
- });
+ menuHandler::homeBaseMenu();
#if HAS_TFT
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) {
- showOverlayBanner("Switch to MUI?\nYes\nNo", 30000, 2, [](int selected) -> void {
- if (selected == 0) {
- config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR;
- config.bluetooth.enabled = false;
- service->reloadConfig(SEGMENT_CONFIG);
- rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
- }
- });
+ menuHandler::switchToMUIMenu();
#else
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) {
- showOverlayBanner(
- "Beeps Mode\nAll Enabled\nDisabled\nNotifications\nSystem Only", 30000, 4,
- [](int selected) -> void {
- config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected;
- service->reloadConfig(SEGMENT_CONFIG);
- },
- config.device.buzzer_mode);
+ menuHandler::BuzzerModeMenu();
#endif
#if HAS_GPS
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) {
- showOverlayBanner(
- "Toggle GPS\nBack\nEnabled\nDisabled", 30000, 3,
- [](int selected) -> void {
- if (selected == 1) {
- config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED;
- playGPSEnableBeep();
- gps->enable();
- service->reloadConfig(SEGMENT_CONFIG);
- } else if (selected == 2) {
- config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED;
- playGPSDisableBeep();
- gps->disable();
- service->reloadConfig(SEGMENT_CONFIG);
- }
- },
- config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1
- : 2); // set inital selection
+ menuHandler::positionBaseMenu();
#endif
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) {
- TZPicker();
+ menuHandler::clockMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
- LoraRegionPicker();
+ menuHandler::LoraRegionPicker();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage &&
devicestate.rx_text_message.from) {
- const char *banner_message;
- int options;
- if (kb_found) {
- banner_message = "Message Action?\nBack\nDismiss\nReply via Preset\nReply via Freetext";
- options = 4;
- } else {
- banner_message = "Message Action?\nBack\nDismiss\nReply via Preset";
- options = 3;
- }
-#ifdef HAS_I2S
- banner_message = "Message Action?\nBack\nDismiss\nReply via Preset\nReply via Freetext\nRead Aloud";
- options = 5;
-#endif
- showOverlayBanner(banner_message, 30000, options, [](int selected) -> void {
- if (selected == 1) {
- screen->dismissCurrentFrame();
- } else if (selected == 2) {
- if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
- cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST,
- devicestate.rx_text_message.channel);
- } else {
- cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
- }
- } else if (selected == 3) {
- if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
- cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST,
- devicestate.rx_text_message.channel);
- } else {
- cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
- }
- }
-#ifdef HAS_I2S
- else if (selected == 4) {
- const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
- const char *msg = reinterpret_cast(mp.decoded.payload.bytes);
-
- audioThread->readAloud(msg);
- }
-#endif
- });
+ menuHandler::messageResponseMenu();
} else if (framesetInfo.positions.firstFavorite != 255 &&
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
- const char *banner_message;
- int options;
- if (kb_found) {
- banner_message = "Message Node?\nCancel\nNew Preset Msg\nNew Freetext Msg";
- options = 3;
- } else {
- banner_message = "Message Node?\nCancel\nConfirm";
- options = 2;
- }
- showOverlayBanner(banner_message, 30000, options, [](int selected) -> void {
- if (selected == 1) {
- cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
- } else if (selected == 2) {
- cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
- }
- });
+ menuHandler::favoriteBaseMenu();
+ } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist ||
+ 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 (event->inputEvent == INPUT_BROKER_BACK) {
showPrevFrame();
@@ -1397,96 +1327,6 @@ bool Screen::isOverlayBannerShowing()
return NotificationRenderer::isOverlayBannerShowing();
}
-void Screen::LoraRegionPicker(uint32_t duration)
-{
- showOverlayBanner(
- "Set the LoRa "
- "region\nBack\nUS\nEU_433\nEU_868\nCN\nJP\nANZ\nKR\nTW\nRU\nIN\nNZ_865\nTH\nLORA_24\nUA_433\nUA_868\nMY_433\nMY_"
- "919\nSG_"
- "923\nPH_433\nPH_868\nPH_915\nANZ_433",
- duration, 23,
- [](int selected) -> void {
- if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) {
- config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected);
- // This is needed as we wait til picking the LoRa region to generate keys for the first time.
- if (!owner.is_licensed) {
- bool keygenSuccess = false;
- if (config.security.private_key.size == 32) {
- // public key is derived from private, so this will always have the same result.
- if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
- keygenSuccess = true;
- }
- } else {
- LOG_INFO("Generate new PKI keys");
- crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
- keygenSuccess = true;
- }
- if (keygenSuccess) {
- config.security.public_key.size = 32;
- config.security.private_key.size = 32;
- owner.public_key.size = 32;
- memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
- }
- }
- config.lora.tx_enabled = true;
- initRegion();
- if (myRegion->dutyCycle < 100) {
- config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit
- }
- service->reloadConfig(SEGMENT_CONFIG);
- rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
- }
- },
- 0);
-}
-
-void Screen::TZPicker()
-{
- showOverlayBanner(
- "Pick "
- "Timezone\nBack\nUS/Hawaii\nUS/Alaska\nUS/Pacific\nUS/Mountain\nUS/Central\nUS/Eastern\nUTC\nEU/Western\nEU/"
- "Central\nEU/Eastern\nAsia/Kolkata\nAsia/Hong_Kong\nAU/AWST\nAU/ACST\nAU/AEST\nPacific/NZ",
- 30000, 17, [](int selected) -> void {
- if (selected == 1) { // Hawaii
- strncpy(config.device.tzdef, "HST10", sizeof(config.device.tzdef));
- } else if (selected == 2) { // Alaska
- strncpy(config.device.tzdef, "AKST9AKDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
- } else if (selected == 3) { // Pacific
- strncpy(config.device.tzdef, "PST8PDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
- } else if (selected == 4) { // Mountain
- strncpy(config.device.tzdef, "MST7MDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
- } else if (selected == 5) { // Central
- strncpy(config.device.tzdef, "CST6CDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
- } else if (selected == 6) { // Eastern
- strncpy(config.device.tzdef, "EST5EDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
- } else if (selected == 7) { // UTC
- strncpy(config.device.tzdef, "UTC", sizeof(config.device.tzdef));
- } else if (selected == 8) { // EU/Western
- strncpy(config.device.tzdef, "GMT0BST,M3.5.0/1,M10.5.0", sizeof(config.device.tzdef));
- } else if (selected == 9) { // EU/Central
- strncpy(config.device.tzdef, "CET-1CEST,M3.5.0,M10.5.0/3", sizeof(config.device.tzdef));
- } else if (selected == 10) { // EU/Eastern
- strncpy(config.device.tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4", sizeof(config.device.tzdef));
- } else if (selected == 11) { // Asia/Kolkata
- strncpy(config.device.tzdef, "IST-5:30", sizeof(config.device.tzdef));
- } else if (selected == 12) { // China
- strncpy(config.device.tzdef, "HKT-8", sizeof(config.device.tzdef));
- } else if (selected == 13) { // AU/AWST
- strncpy(config.device.tzdef, "AWST-8", sizeof(config.device.tzdef));
- } else if (selected == 14) { // AU/ACST
- strncpy(config.device.tzdef, "ACST-9:30ACDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef));
- } else if (selected == 15) { // AU/AEST
- strncpy(config.device.tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef));
- } else if (selected == 16) { // NZ
- strncpy(config.device.tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3", sizeof(config.device.tzdef));
- }
- if (selected != 0) {
- setenv("TZ", config.device.tzdef, 1);
- service->reloadConfig(SEGMENT_CONFIG);
- }
- });
-}
-
} // namespace graphics
#else
diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h
index 8a836edfc..ac7d9aa69 100644
--- a/src/graphics/Screen.h
+++ b/src/graphics/Screen.h
@@ -24,6 +24,7 @@ class Screen
FOCUS_FAULT,
FOCUS_TEXTMESSAGE,
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
+ FOCUS_CLOCK,
};
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
@@ -38,8 +39,8 @@ class Screen
void setFunctionSymbol(std::string) {}
void removeFunctionSymbol(std::string) {}
void startAlert(const char *) {}
- void showOverlayBanner(const char *message, uint32_t durationMs = 3000, uint8_t options = 0,
- std::function bannerCallback = NULL, int8_t InitialSelected = 0)
+ void showOverlayBanner(const char *message, uint32_t durationMs = 3000, const char **optionsArrayPtr = nullptr,
+ uint8_t options = 0, std::function bannerCallback = NULL, int8_t InitialSelected = 0)
{
}
void setFrames(FrameFocus focus) {}
@@ -209,6 +210,7 @@ class Screen : public concurrency::OSThread
FOCUS_FAULT,
FOCUS_TEXTMESSAGE,
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
+ FOCUS_CLOCK,
};
// Regenerate the normal set of frames, focusing a specific frame if requested
@@ -223,6 +225,8 @@ class Screen : public concurrency::OSThread
meshtastic_Config_DisplayConfig_OledType model;
OLEDDISPLAY_GEOMETRY geometry;
+ bool ignoreCompass = false;
+
bool isOverlayBannerShowing();
// Stores the last 4 of our hardware ID, to make finding the device for pairing easier
@@ -286,8 +290,8 @@ class Screen : public concurrency::OSThread
enqueueCmd(cmd);
}
- void showOverlayBanner(const char *message, uint32_t durationMs = 3000, uint8_t options = 0,
- std::function bannerCallback = NULL, int8_t InitialSelected = 0);
+ void showOverlayBanner(const char *message, uint32_t durationMs = 3000, const char **optionsArrayPtr = nullptr,
+ uint8_t options = 0, std::function bannerCallback = NULL, int8_t InitialSelected = 0);
void startFirmwareUpdateScreen()
{
@@ -301,7 +305,7 @@ class Screen : public concurrency::OSThread
void setHeading(long _heading)
{
hasCompass = true;
- compassHeading = _heading;
+ compassHeading = fmod(_heading, 360);
}
bool hasHeading() { return hasCompass; }
@@ -602,8 +606,6 @@ class Screen : public concurrency::OSThread
void handleShowNextFrame();
void handleShowPrevFrame();
void handleStartFirmwareUpdateScreen();
- void TZPicker();
- void LoraRegionPicker(uint32_t duration = 30000);
// Info collected by setFrames method.
// Index location of specific frames.
@@ -612,7 +614,6 @@ class Screen : public concurrency::OSThread
struct FramesetInfo {
struct FramePositions {
uint8_t fault = 255;
- uint8_t textMessage = 255;
uint8_t waypoint = 255;
uint8_t focusedModule = 255;
uint8_t log = 255;
@@ -622,6 +623,12 @@ class Screen : public concurrency::OSThread
uint8_t memory = 255;
uint8_t gps = 255;
uint8_t home = 255;
+ uint8_t textMessage = 255;
+ uint8_t nodelist = 255;
+ uint8_t nodelist_lastheard = 255;
+ uint8_t nodelist_hopsignal = 255;
+ uint8_t nodelist_distance = 255;
+ uint8_t nodelist_bearings = 255;
uint8_t clock = 255;
uint8_t firstFavorite = 255;
uint8_t lastFavorite = 255;
@@ -679,5 +686,6 @@ class Screen : public concurrency::OSThread
// Extern declarations for function symbols used in UIRenderer
extern std::vector functionSymbol;
extern std::string functionSymbolString;
+extern graphics::Screen *screen;
#endif
\ No newline at end of file
diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp
index af427cae4..07f2e5cde 100644
--- a/src/graphics/SharedUIDisplay.cpp
+++ b/src/graphics/SharedUIDisplay.cpp
@@ -10,9 +10,22 @@
namespace graphics
{
+void determineResolution(int16_t screenheight, int16_t screenwidth)
+{
+ if (screenwidth > 128) {
+ isHighResolution = true;
+ }
+
+ // Special case for Heltec Wireless Tracker v1.1
+ if (screenwidth == 160 && screenheight == 80) {
+ isHighResolution = false;
+ }
+}
+
// === Shared External State ===
bool hasUnreadMessage = false;
bool isMuted = false;
+bool isHighResolution = false;
// === Internal State ===
bool isBoltVisibleShared = true;
@@ -40,7 +53,7 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w,
// *************************
// * Common Header Drawing *
// *************************
-void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr)
+void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only)
{
constexpr int HEADER_OFFSET_Y = 1;
y += HEADER_OFFSET_Y;
@@ -56,34 +69,40 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
const int screenW = display->getWidth();
const int screenH = display->getHeight();
- const bool useBigIcons = (screenW > 128);
-
- // === Inverted Header Background ===
- if (isInverted) {
- drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
- display->setColor(BLACK);
- } else {
- display->setColor(BLACK);
- display->fillRect(0, 0, screenW, highlightHeight + 3);
- display->setColor(WHITE);
- if (screenW > 128) {
- display->drawLine(0, 20, screenW, 20);
+ if (!battery_only) {
+ // === Inverted Header Background ===
+ if (isInverted) {
+ display->setColor(BLACK);
+ display->fillRect(0, 0, screenW, highlightHeight + 2);
+ display->setColor(WHITE);
+ drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
+ display->setColor(BLACK);
} else {
- display->drawLine(0, 14, screenW, 14);
+ display->setColor(BLACK);
+ display->fillRect(0, 0, screenW, highlightHeight + 2);
+ display->setColor(WHITE);
+ if (isHighResolution) {
+ display->drawLine(0, 20, screenW, 20);
+ } else {
+ display->drawLine(0, 14, screenW, 14);
+ }
}
- }
- // === Screen Title ===
- display->setTextAlignment(TEXT_ALIGN_CENTER);
- display->drawString(SCREEN_WIDTH / 2, y, titleStr);
- if (config.display.heading_bold) {
- display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr);
+ // === Screen Title ===
+ display->setTextAlignment(TEXT_ALIGN_CENTER);
+ display->drawString(SCREEN_WIDTH / 2, y, titleStr);
+ if (config.display.heading_bold) {
+ display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr);
+ }
}
display->setTextAlignment(TEXT_ALIGN_LEFT);
// === Battery State ===
int chargePercent = powerStatus->getBatteryChargePercent();
bool isCharging = powerStatus->getIsCharging() == meshtastic::OptionalBool::OptTrue;
+ if (chargePercent == 100) {
+ isCharging = false;
+ }
uint32_t now = millis();
#ifndef USE_EINK
@@ -93,20 +112,22 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
}
#endif
- bool useHorizontalBattery = (screenW > 128 && screenW >= screenH);
+ bool useHorizontalBattery = (isHighResolution && screenW >= screenH);
const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
// === Battery Icons ===
if (useHorizontalBattery) {
int batteryX = 2;
- int batteryY = HEADER_OFFSET_Y + 2;
- display->drawXbm(batteryX, batteryY, 29, 15, batteryBitmap_h);
+ int batteryY = HEADER_OFFSET_Y + 3;
+ display->drawXbm(batteryX, batteryY, 9, 13, batteryBitmap_h_bottom);
+ display->drawXbm(batteryX + 9, batteryY, 9, 13, batteryBitmap_h_top);
if (isCharging && isBoltVisibleShared)
- display->drawXbm(batteryX + 9, batteryY + 1, 9, 13, lightning_bolt_h);
+ display->drawXbm(batteryX + 4, batteryY, 9, 13, lightning_bolt_h);
else {
- display->drawXbm(batteryX + 8, batteryY, 12, 15, batteryBitmap_sidegaps_h);
- int fillWidth = 24 * chargePercent / 100;
- display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 13);
+ display->drawLine(batteryX + 5, batteryY, batteryX + 10, batteryY);
+ display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12);
+ int fillWidth = 14 * chargePercent / 100;
+ display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11);
}
} else {
int batteryX = 1;
@@ -129,12 +150,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
char chargeStr[4];
snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent);
int chargeNumWidth = display->getStringWidth(chargeStr);
- const int batteryOffset = useHorizontalBattery ? 28 : 6;
-#ifdef USE_EINK
- const int percentX = x + xOffset + batteryOffset - 2;
-#else
- const int percentX = x + xOffset + batteryOffset;
-#endif
+ const int batteryOffset = useHorizontalBattery ? 19 : 9;
+ const int percentX = x + batteryOffset;
display->drawString(percentX, textY, chargeStr);
display->drawString(percentX + chargeNumWidth - 1, textY, "%");
if (isBold) {
@@ -148,7 +165,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
int timeStrWidth = display->getStringWidth("12:34"); // Default alignment
int timeX = screenW - xOffset - timeStrWidth + 4;
- if (rtc_sec > 0) {
+ if (rtc_sec > 0 && !battery_only) {
// === Build Time String ===
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
int hour = hms / SEC_PER_HOUR;
@@ -164,7 +181,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
}
timeStrWidth = display->getStringWidth(timeStr);
- timeX = screenW - xOffset - timeStrWidth + 4;
+ timeX = screenW - xOffset - timeStrWidth + 3;
// === Show Mail or Mute Icon to the Left of Time ===
int iconRightEdge = timeX - 1;
@@ -217,7 +234,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
}
} else if (isMuted) {
- if (useBigIcons) {
+ if (isHighResolution) {
int iconX = iconRightEdge - mute_symbol_big_width;
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
@@ -259,6 +276,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
bool showMail = false;
+#ifndef USE_EINK
if (hasUnreadMessage) {
if (now - lastMailBlink > 500) {
isMailIconVisible = !isMailIconVisible;
@@ -266,6 +284,11 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
}
showMail = isMailIconVisible;
}
+#else
+ if (hasUnreadMessage) {
+ showMail = true;
+ }
+#endif
if (showMail) {
if (useHorizontalBattery) {
@@ -281,7 +304,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
}
} else if (isMuted) {
- if (useBigIcons) {
+ if (isHighResolution) {
int iconX = iconRightEdge - mute_symbol_big_width;
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big);
@@ -300,7 +323,7 @@ const int *getTextPositions(OLEDDisplay *display)
{
static int textPositions[7]; // Static array that persists beyond function scope
- if (display->getHeight() > 64) {
+ if (isHighResolution) {
textPositions[0] = textZeroLine;
textPositions[1] = textFirstLine_medium;
textPositions[2] = textSecondLine_medium;
diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h
index 41411ba7f..2e97052a8 100644
--- a/src/graphics/SharedUIDisplay.h
+++ b/src/graphics/SharedUIDisplay.h
@@ -41,12 +41,14 @@ namespace graphics
// Shared state (declare inside namespace)
extern bool hasUnreadMessage;
extern bool isMuted;
+extern bool isHighResolution;
+void determineResolution(int16_t screenheight, int16_t screenwidth);
// Rounded highlight (used for inverted headers)
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r);
// Shared battery/time/mail header
-void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "");
+void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool battery_only = false);
const int *getTextPositions(OLEDDisplay *display);
diff --git a/src/graphics/draw/ClockRenderer.cpp b/src/graphics/draw/ClockRenderer.cpp
index 2e301b4e1..aa177078b 100644
--- a/src/graphics/draw/ClockRenderer.cpp
+++ b/src/graphics/draw/ClockRenderer.cpp
@@ -21,6 +21,7 @@ namespace graphics
namespace ClockRenderer
{
+bool digitalWatchFace = true;
void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
{
@@ -146,6 +147,7 @@ void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int heig
display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight);
}
+/*
void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale)
{
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
@@ -179,21 +181,22 @@ void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool
drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
}
}
-
+*/
// Draw a digital clock
void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->clear();
display->setTextAlignment(TEXT_ALIGN_LEFT);
int line = 1;
+ // === Set Title, Blank for Clock
+ const char *titleStr = "";
+ // === Header ===
+ graphics::drawCommonHeader(display, x, y, titleStr, true);
#ifdef T_WATCH_S3
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
- graphics::ClockRenderer::drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2);
+ graphics::ClockRenderer::drawBluetoothConnectedIcon(display, display->getWidth() - 18, display->getHeight() - 14);
}
-
- drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36,
- graphics::ClockRenderer::digitalWatchFace, 1);
#endif
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
@@ -230,7 +233,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
float scale = 1.5;
#else
float scale = 0.75;
- if (SCREEN_WIDTH > 128) {
+ if (isHighResolution) {
scale = 1.5;
}
#endif
@@ -276,17 +279,17 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
// draw seconds string
display->setFont(FONT_SMALL);
- int xOffset = (SCREEN_WIDTH > 128) ? 0 : -1;
+ int xOffset = (isHighResolution) ? 0 : -1;
if (hour >= 10) {
- xOffset += (SCREEN_WIDTH > 128) ? 32 : 18;
+ xOffset += (isHighResolution) ? 32 : 18;
}
- int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1;
+ int yOffset = (isHighResolution) ? 3 : 1;
if (config.display.use_12h_clock) {
display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2,
isPM ? "pm" : "am");
}
#ifndef USE_EINK
- xOffset = (SCREEN_WIDTH > 128) ? 18 : 10;
+ xOffset = (isHighResolution) ? 18 : 10;
display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset,
secondString);
#endif
@@ -301,31 +304,30 @@ void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y)
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->setTextAlignment(TEXT_ALIGN_LEFT);
+ // === Set Title, Blank for Clock
+ const char *titleStr = "";
+ // === Header ===
+ graphics::drawCommonHeader(display, x, y, titleStr, true);
- graphics::UIRenderer::drawBattery(display, x, y + 7, imgBattery, powerStatus);
-
- if (powerStatus->getHasBattery()) {
- char batteryPercent[8];
- snprintf(batteryPercent, sizeof(batteryPercent), "%d%%", powerStatus->getBatteryChargePercent());
-
- display->setFont(FONT_SMALL);
-
- display->drawString(x + 20, y + 2, batteryPercent);
- }
#ifdef T_WATCH_S3
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
- drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2);
+ drawBluetoothConnectedIcon(display, display->getWidth() - 18, display->getHeight() - 14);
}
#endif
- drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36,
- graphics::ClockRenderer::digitalWatchFace, 1);
-
// clock face center coordinates
int16_t centerX = display->getWidth() / 2;
int16_t centerY = display->getHeight() / 2;
// clock face radius
- int16_t radius = (display->getWidth() / 2) * 0.8;
+ int16_t radius = 0;
+ if (display->getHeight() < display->getWidth()) {
+ radius = (display->getHeight() / 2) * 0.9;
+ } else {
+ radius = (display->getWidth() / 2) * 0.9;
+ }
+#ifdef T_WATCH_S3
+ radius = (display->getWidth() / 2) * 0.8;
+#endif
// noon (0 deg) coordinates (outermost circle)
int16_t noonX = centerX;
@@ -338,10 +340,16 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
int16_t tickMarkOuterNoonY = secondHandNoonY;
// seconds tick mark inner y coordinate; (second nested circle)
- double secondsTickMarkInnerNoonY = (double)noonY + 8;
+ double secondsTickMarkInnerNoonY = (double)noonY + 4;
+ if (isHighResolution) {
+ secondsTickMarkInnerNoonY = (double)noonY + 8;
+ }
// hours tick mark inner y coordinate; (third nested circle)
- double hoursTickMarkInnerNoonY = (double)noonY + 16;
+ double hoursTickMarkInnerNoonY = (double)noonY + 6;
+ if (isHighResolution) {
+ hoursTickMarkInnerNoonY = (double)noonY + 16;
+ }
// minute hand y coordinate
int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4;
@@ -350,7 +358,10 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
int16_t hourStringNoonY = minuteHandNoonY + 18;
// hour hand radius and y coordinate
- int16_t hourHandRadius = radius * 0.55;
+ int16_t hourHandRadius = radius * 0.35;
+ if (isHighResolution) {
+ int16_t hourHandRadius = radius * 0.55;
+ }
int16_t hourHandNoonY = centerY - hourHandRadius;
display->setColor(OLEDDISPLAY_COLOR::WHITE);
@@ -366,7 +377,20 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
- hour = hour > 12 ? hour - 12 : hour;
+ bool isPM = hour >= 12;
+ if (config.display.use_12h_clock) {
+ bool isPM = hour >= 12;
+ display->setFont(FONT_SMALL);
+ int yOffset = isHighResolution ? 1 : 0;
+#ifdef USE_EINK
+ yOffset += 3;
+#endif
+ display->drawString(centerX - (display->getStringWidth(isPM ? "pm" : "am") / 2), centerY + yOffset,
+ isPM ? "pm" : "am");
+ }
+ hour %= 12;
+ if (hour == 0)
+ hour = 12;
int16_t degreesPerHour = 30;
int16_t degreesPerMinuteOrSecond = 6;
@@ -443,16 +467,32 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
double hourStringX = (sineAngleInRadians * (hourStringNoonY - centerY) + noonX) - hourStringXOffset;
double hourStringY = (cosineAngleInRadians * (hourStringNoonY - centerY) + centerY) - hourStringYOffset;
+#ifdef T_WATCH_S3
// draw hour number
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
+#else
+#ifdef USE_EINK
+ if (isHighResolution) {
+ // draw hour number
+ display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
+ }
+#else
+ if (isHighResolution && (hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) {
+ // draw hour number
+ display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
+ }
+#endif
+#endif
}
if (angle % degreesPerMinuteOrSecond == 0) {
double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX;
double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY;
- // draw minute tick mark
- display->drawLine(startX, startY, endX, endY);
+ if (isHighResolution) {
+ // draw minute tick mark
+ display->drawLine(startX, startY, endX, endY);
+ }
}
}
@@ -461,9 +501,10 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// draw minute hand
display->drawLine(centerX, centerY, minuteX, minuteY);
-
+#ifndef USE_EINK
// draw second hand
display->drawLine(centerX, centerY, secondX, secondY);
+#endif
}
}
diff --git a/src/graphics/draw/ClockRenderer.h b/src/graphics/draw/ClockRenderer.h
index 4660dcc35..9c3238b14 100644
--- a/src/graphics/draw/ClockRenderer.h
+++ b/src/graphics/draw/ClockRenderer.h
@@ -12,7 +12,7 @@ class Screen;
namespace ClockRenderer
{
// Whether we are showing the digital watch face or the analog one
-static bool digitalWatchFace = true;
+extern bool digitalWatchFace;
// Clock frame functions
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
@@ -25,7 +25,7 @@ void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int he
void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height);
// UI elements for clock displays
-void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode = true, float scale = 1);
+// void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode = true, float scale = 1);
void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y);
} // namespace ClockRenderer
diff --git a/src/graphics/draw/CompassRenderer.cpp b/src/graphics/draw/CompassRenderer.cpp
index fef993e2d..6d8051546 100644
--- a/src/graphics/draw/CompassRenderer.cpp
+++ b/src/graphics/draw/CompassRenderer.cpp
@@ -4,6 +4,7 @@
#include "configuration.h"
#include "gps/GeoCoord.h"
#include "graphics/ScreenFonts.h"
+#include "graphics/SharedUIDisplay.h"
#include
namespace graphics
@@ -45,17 +46,18 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY,
// This could draw a "N" indicator or north arrow
// For now, we'll draw a simple north indicator
// const float radius = 17.0f;
- if (display->width() > 128) {
+ if (isHighResolution) {
radius += 4;
}
Point north(0, -radius);
- north.rotate(-myHeading);
+ if (!config.display.compass_north_top)
+ north.rotate(-myHeading);
north.translate(compassX, compassY);
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_CENTER);
display->setColor(BLACK);
- if (display->width() > 128) {
+ if (isHighResolution) {
display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6);
} else {
display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6);
@@ -91,18 +93,22 @@ void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, f
float radians = bearing * DEG_TO_RAD;
Point tip(0, -size / 2);
- Point left(-size / 4, size / 4);
- Point right(size / 4, size / 4);
+ Point left(-size / 6, size / 4);
+ Point right(size / 6, size / 4);
+ Point tail(0, size / 4.5);
tip.rotate(radians);
left.rotate(radians);
right.rotate(radians);
+ tail.rotate(radians);
tip.translate(x, y);
left.translate(x, y);
right.translate(x, y);
+ tail.translate(x, y);
- display->drawTriangle(tip.x, tip.y, left.x, left.y, right.x, right.y);
+ display->fillTriangle(tip.x, tip.y, left.x, left.y, tail.x, tail.y);
+ display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y);
}
float estimatedHeading(double lat, double lon)
@@ -127,14 +133,5 @@ uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight)
return maxDiam;
}
-float calculateBearing(double lat1, double lon1, double lat2, double lon2)
-{
- double dLon = (lon2 - lon1) * DEG_TO_RAD;
- double y = sin(dLon) * cos(lat2 * DEG_TO_RAD);
- double x = cos(lat1 * DEG_TO_RAD) * sin(lat2 * DEG_TO_RAD) - sin(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * cos(dLon);
- double bearing = atan2(y, x) * RAD_TO_DEG;
- return fmod(bearing + 360.0, 360.0);
-}
-
} // namespace CompassRenderer
} // namespace graphics
diff --git a/src/graphics/draw/CompassRenderer.h b/src/graphics/draw/CompassRenderer.h
index 4b26e6463..ca7532b66 100644
--- a/src/graphics/draw/CompassRenderer.h
+++ b/src/graphics/draw/CompassRenderer.h
@@ -28,9 +28,6 @@ void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, f
float estimatedHeading(double lat, double lon);
uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight);
-// Utility functions for bearing calculations
-float calculateBearing(double lat1, double lon1, double lat2, double lon2);
-
} // namespace CompassRenderer
} // namespace graphics
diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp
index 2c3a3a3a8..92cf49610 100644
--- a/src/graphics/draw/DebugRenderer.cpp
+++ b/src/graphics/draw/DebugRenderer.cpp
@@ -67,21 +67,6 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
char channelStr[20];
snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex()));
-
- // Display power status
- if (powerStatus->getHasBattery()) {
- if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
- UIRenderer::drawBattery(display, x, y + 2, imgBattery, powerStatus);
- } else {
- UIRenderer::drawBattery(display, x + 1, y + 3, imgBattery, powerStatus);
- }
- } else if (powerStatus->knowsUSB()) {
- if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
- display->drawFastImage(x, y + 2, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower);
- } else {
- display->drawFastImage(x + 1, y + 3, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower);
- }
- }
// Display nodes status
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus);
@@ -393,7 +378,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
int line = 1;
// === Set Title
- const char *titleStr = (SCREEN_WIDTH > 128) ? "LoRa Info" : "LoRa";
+ const char *titleStr = (isHighResolution) ? "LoRa Info" : "LoRa";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr);
@@ -444,12 +429,12 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
char chUtilPercentage[10];
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
- int chUtil_x = (SCREEN_WIDTH > 128) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5;
+ int chUtil_x = (isHighResolution) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5;
int chUtil_y = getTextPositions(display)[line] + 3;
- int chutil_bar_width = (SCREEN_WIDTH > 128) ? 100 : 50;
- int chutil_bar_height = (SCREEN_WIDTH > 128) ? 12 : 7;
- int extraoffset = (SCREEN_WIDTH > 128) ? 6 : 3;
+ int chutil_bar_width = (isHighResolution) ? 100 : 50;
+ int chutil_bar_height = (isHighResolution) ? 12 : 7;
+ int extraoffset = (isHighResolution) ? 6 : 3;
int chutil_percent = airTime->channelUtilizationPercent();
int centerofscreen = SCREEN_WIDTH / 2;
@@ -516,7 +501,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
int line = 1;
const int barHeight = 6;
const int labelX = x;
- const int barsOffset = (SCREEN_WIDTH > 128) ? 24 : 0;
+ const int barsOffset = (isHighResolution) ? 24 : 0;
const int barX = x + 40 + barsOffset;
auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) {
@@ -526,7 +511,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
int percent = (used * 100) / total;
char combinedStr[24];
- if (SCREEN_WIDTH > 128) {
+ if (isHighResolution) {
snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024,
total / 1024);
} else {
diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp
new file mode 100644
index 000000000..1c327117e
--- /dev/null
+++ b/src/graphics/draw/MenuHandler.cpp
@@ -0,0 +1,479 @@
+#include "configuration.h"
+#if HAS_SCREEN
+#include "ClockRenderer.h"
+#include "GPS.h"
+#include "MenuHandler.h"
+#include "MeshRadio.h"
+#include "MeshService.h"
+#include "NodeDB.h"
+#include "buzz.h"
+#include "graphics/Screen.h"
+#include "graphics/draw/UIRenderer.h"
+#include "main.h"
+#include "modules/AdminModule.h"
+#include "modules/CannedMessageModule.h"
+
+namespace graphics
+{
+menuHandler::screenMenus menuHandler::menuQueue = menu_none;
+
+void menuHandler::LoraRegionPicker(uint32_t duration)
+{
+ static const char *optionsArray[] = {"Back",
+ "US",
+ "EU_433",
+ "EU_868",
+ "CN",
+ "JP",
+ "ANZ",
+ "KR",
+ "TW",
+ "RU",
+ "IN",
+ "NZ_865",
+ "TH",
+ "LORA_24",
+ "UA_433",
+ "UA_868",
+ "MY_433",
+ "MY_"
+ "919",
+ "SG_"
+ "923",
+ "PH_433",
+ "PH_868",
+ "PH_915",
+ "ANZ_433"};
+ screen->showOverlayBanner(
+ "Set the LoRa region", duration, optionsArray, 23,
+ [](int selected) -> void {
+ if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) {
+ config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected);
+ // This is needed as we wait til picking the LoRa region to generate keys for the first time.
+ if (!owner.is_licensed) {
+ bool keygenSuccess = false;
+ if (config.security.private_key.size == 32) {
+ // public key is derived from private, so this will always have the same result.
+ if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
+ keygenSuccess = true;
+ }
+ } else {
+ LOG_INFO("Generate new PKI keys");
+ crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
+ keygenSuccess = true;
+ }
+ if (keygenSuccess) {
+ config.security.public_key.size = 32;
+ config.security.private_key.size = 32;
+ owner.public_key.size = 32;
+ memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
+ }
+ }
+ config.lora.tx_enabled = true;
+ initRegion();
+ if (myRegion->dutyCycle < 100) {
+ config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit
+ }
+ service->reloadConfig(SEGMENT_CONFIG);
+ rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
+ }
+ },
+ 0);
+}
+
+void menuHandler::TwelveHourPicker()
+{
+ static const char *optionsArray[] = {"Back", "12-hour", "24-hour"};
+ screen->showOverlayBanner("Time Format", 30000, optionsArray, 3, [](int selected) -> void {
+ if (selected == 0) {
+ menuHandler::menuQueue = menuHandler::clock_menu;
+ } else if (selected == 1) {
+ config.display.use_12h_clock = true;
+ } else {
+ config.display.use_12h_clock = false;
+ }
+ service->reloadConfig(SEGMENT_CONFIG);
+ });
+}
+
+void menuHandler::ClockFacePicker()
+{
+ static const char *optionsArray[] = {"Back", "Digital", "Analog"};
+ screen->showOverlayBanner("Which Face?", 30000, optionsArray, 3, [](int selected) -> void {
+ if (selected == 0) {
+ menuHandler::menuQueue = menuHandler::clock_menu;
+ } else if (selected == 1) {
+ graphics::ClockRenderer::digitalWatchFace = true;
+ screen->setFrames(Screen::FOCUS_CLOCK);
+ } else {
+ graphics::ClockRenderer::digitalWatchFace = false;
+ screen->setFrames(Screen::FOCUS_CLOCK);
+ }
+ });
+}
+
+void menuHandler::TZPicker()
+{
+ static const char *optionsArray[] = {"Back",
+ "US/Hawaii",
+ "US/Alaska",
+ "US/Pacific",
+ "US/Arizona",
+ "US/Mountain",
+ "US/Central",
+ "US/Eastern",
+ "UTC",
+ "EU/Western",
+ "EU/"
+ "Central",
+ "EU/Eastern",
+ "Asia/Kolkata",
+ "Asia/Hong_Kong",
+ "AU/AWST",
+ "AU/ACST",
+ "AU/AEST",
+ "Pacific/NZ"};
+ screen->showOverlayBanner("Pick Timezone", 30000, optionsArray, 17, [](int selected) -> void {
+ if (selected == 0) {
+ menuHandler::menuQueue = menuHandler::clock_menu;
+ } else if (selected == 1) { // Hawaii
+ strncpy(config.device.tzdef, "HST10", sizeof(config.device.tzdef));
+ } else if (selected == 2) { // Alaska
+ strncpy(config.device.tzdef, "AKST9AKDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
+ } else if (selected == 3) { // Pacific
+ strncpy(config.device.tzdef, "PST8PDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
+ } else if (selected == 4) { // Arizona
+ strncpy(config.device.tzdef, "MST7", sizeof(config.device.tzdef));
+ } else if (selected == 5) { // Mountain
+ strncpy(config.device.tzdef, "MST7MDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
+ } else if (selected == 6) { // Central
+ strncpy(config.device.tzdef, "CST6CDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
+ } else if (selected == 7) { // Eastern
+ strncpy(config.device.tzdef, "EST5EDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
+ } else if (selected == 8) { // UTC
+ strncpy(config.device.tzdef, "UTC", sizeof(config.device.tzdef));
+ } else if (selected == 9) { // EU/Western
+ strncpy(config.device.tzdef, "GMT0BST,M3.5.0/1,M10.5.0", sizeof(config.device.tzdef));
+ } else if (selected == 10) { // EU/Central
+ strncpy(config.device.tzdef, "CET-1CEST,M3.5.0,M10.5.0/3", sizeof(config.device.tzdef));
+ } else if (selected == 11) { // EU/Eastern
+ strncpy(config.device.tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4", sizeof(config.device.tzdef));
+ } else if (selected == 12) { // Asia/Kolkata
+ strncpy(config.device.tzdef, "IST-5:30", sizeof(config.device.tzdef));
+ } else if (selected == 13) { // China
+ strncpy(config.device.tzdef, "HKT-8", sizeof(config.device.tzdef));
+ } else if (selected == 14) { // AU/AWST
+ strncpy(config.device.tzdef, "AWST-8", sizeof(config.device.tzdef));
+ } else if (selected == 15) { // AU/ACST
+ strncpy(config.device.tzdef, "ACST-9:30ACDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef));
+ } else if (selected == 16) { // AU/AEST
+ strncpy(config.device.tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef));
+ } else if (selected == 17) { // NZ
+ strncpy(config.device.tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3", sizeof(config.device.tzdef));
+ }
+ if (selected != 0) {
+ setenv("TZ", config.device.tzdef, 1);
+ service->reloadConfig(SEGMENT_CONFIG);
+ }
+ });
+}
+
+void menuHandler::clockMenu()
+{
+ static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"};
+ screen->showOverlayBanner("Clock Action", 30000, optionsArray, 4, [](int selected) -> void {
+ if (selected == 1) {
+ menuHandler::menuQueue = menuHandler::clock_face_picker;
+ screen->setInterval(0);
+ runASAP = true;
+ } else if (selected == 2) {
+ menuHandler::menuQueue = menuHandler::twelve_hour_picker;
+ screen->setInterval(0);
+ runASAP = true;
+ } else if (selected == 3) {
+ menuHandler::menuQueue = menuHandler::TZ_picker;
+ screen->setInterval(0);
+ runASAP = true;
+ }
+ });
+}
+
+void menuHandler::messageResponseMenu()
+{
+
+ static const char **optionsArrayPtr;
+ int options;
+ if (kb_found) {
+ static const char *optionsArray[] = {"Back", "Dismiss", "Reply via Preset", "Reply via Freetext"};
+ optionsArrayPtr = optionsArray;
+ options = 4;
+ } else {
+ static const char *optionsArray[] = {"Back", "Dismiss", "Reply via Preset"};
+ optionsArrayPtr = optionsArray;
+ options = 3;
+ }
+#ifdef HAS_I2S
+ static const char *optionsArray[] = {"Back", "Dismiss", "Reply via Preset", "Reply via Freetext", "Read Aloud"};
+ optionsArrayPtr = optionsArray;
+ options = 5;
+#endif
+ screen->showOverlayBanner("Message Action", 30000, optionsArrayPtr, options, [](int selected) -> void {
+ if (selected == 1) {
+ screen->dismissCurrentFrame();
+ } else if (selected == 2) {
+ if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
+ cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
+ } else {
+ cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
+ }
+ } else if (selected == 3) {
+ if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
+ cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
+ } else {
+ cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
+ }
+ }
+#ifdef HAS_I2S
+ else if (selected == 4) {
+ const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
+ const char *msg = reinterpret_cast(mp.decoded.payload.bytes);
+
+ audioThread->readAloud(msg);
+ }
+#endif
+ });
+}
+
+void menuHandler::homeBaseMenu()
+{
+ int options;
+ static const char **optionsArrayPtr;
+
+ if (kb_found) {
+#ifdef PIN_EINK_EN
+ static const char *optionsArray[] = {"Back", "Toggle Backlight", "Send Position", "New Preset Msg", "New Freetext Msg"};
+#else
+ static const char *optionsArray[] = {"Back", "Sleep Screen", "Send Position", "New Preset Msg", "New Freetext Msg"};
+#endif
+ optionsArrayPtr = optionsArray;
+ options = 5;
+ } else {
+#ifdef PIN_EINK_EN
+ static const char *optionsArray[] = {"Back", "Toggle Backlight", "Send Position", "New Preset Msg"};
+#else
+ static const char *optionsArray[] = {"Back", "Sleep Screen", "Send Position", "New Preset Msg"};
+#endif
+ optionsArrayPtr = optionsArray;
+ options = 4;
+ }
+ screen->showOverlayBanner("Home Action", 30000, optionsArrayPtr, options, [](int selected) -> void {
+ if (selected == 1) {
+#ifdef PIN_EINK_EN
+ if (digitalRead(PIN_EINK_EN) == HIGH) {
+ digitalWrite(PIN_EINK_EN, LOW);
+ } else {
+ digitalWrite(PIN_EINK_EN, HIGH);
+ }
+#else
+ screen->setOn(false);
+#endif
+ } else if (selected == 2) {
+ InputEvent event = {.inputEvent = (input_broker_event)175, .kbchar = 175, .touchX = 0, .touchY = 0};
+ inputBroker->injectInputEvent(&event);
+ } else if (selected == 3) {
+ cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
+ } else if (selected == 4) {
+ cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST);
+ }
+ });
+}
+
+void menuHandler::favoriteBaseMenu()
+{
+ int options;
+ static const char **optionsArrayPtr;
+
+ if (kb_found) {
+ static const char *optionsArray[] = {"Back", "New Preset Msg", "New Freetext Msg"};
+ optionsArrayPtr = optionsArray;
+ options = 3;
+ } else {
+ static const char *optionsArray[] = {"Back", "New Preset Msg"};
+ optionsArrayPtr = optionsArray;
+ options = 2;
+ }
+ screen->showOverlayBanner("Favorites Action", 30000, optionsArrayPtr, options, [](int selected) -> void {
+ if (selected == 1) {
+ cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
+ } else if (selected == 2) {
+ cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
+ }
+ });
+}
+
+void menuHandler::positionBaseMenu()
+{
+ int options;
+ static const char **optionsArrayPtr;
+ static const char *optionsArray[] = {"Back", "GPS Toggle", "Compass"};
+ static const char *optionsArrayCalibrate[] = {"Back", "GPS Toggle", "Compass", "Compass Calibrate"};
+
+ if (accelerometerThread) {
+ optionsArrayPtr = optionsArrayCalibrate;
+ options = 4;
+ } else {
+ optionsArrayPtr = optionsArray;
+ options = 3;
+ }
+ screen->showOverlayBanner("Position Action", 30000, optionsArrayPtr, options, [](int selected) -> void {
+ if (selected == 1) {
+ menuQueue = gps_toggle_menu;
+ } else if (selected == 2) {
+ menuQueue = compass_point_north_menu;
+ } else if (selected == 3) {
+ accelerometerThread->calibrate(30);
+ }
+ });
+}
+
+void menuHandler::nodeListMenu()
+{
+ static const char *optionsArray[] = {"Back", "Reset NodeDB"};
+ screen->showOverlayBanner("Node Action", 30000, optionsArray, 2, [](int selected) -> void {
+ if (selected == 1) {
+ menuQueue = reset_node_db_menu;
+ }
+ });
+}
+
+void menuHandler::resetNodeDBMenu()
+{
+ static const char *optionsArray[] = {"Back", "Confirm"};
+ screen->showOverlayBanner("Confirm Reset NodeDB", 30000, optionsArray, 2, [](int selected) -> void {
+ if (selected == 1) {
+ disableBluetooth();
+ LOG_INFO("Initiate node-db reset");
+ nodeDB->resetNodes();
+ rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
+ }
+ });
+}
+
+void menuHandler::compassNorthMenu()
+{
+ static const char *optionsArray[] = {"Back", "Dynamic", "Fixed Ring", "Freeze Heading"};
+ screen->showOverlayBanner("North Directions?", 30000, optionsArray, 4, [](int selected) -> void {
+ if (selected == 1) {
+ if (config.display.compass_north_top != false) {
+ config.display.compass_north_top = false;
+ service->reloadConfig(SEGMENT_CONFIG);
+ }
+ screen->ignoreCompass = false;
+ screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
+ } else if (selected == 2) {
+ if (config.display.compass_north_top != true) {
+ config.display.compass_north_top = true;
+ service->reloadConfig(SEGMENT_CONFIG);
+ }
+ screen->ignoreCompass = false;
+ screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
+ } else if (selected == 3) {
+ if (config.display.compass_north_top != true) {
+ config.display.compass_north_top = true;
+ service->reloadConfig(SEGMENT_CONFIG);
+ }
+ screen->ignoreCompass = true;
+ screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
+ } else if (selected == 0) {
+ menuQueue = position_base_menu;
+ }
+ });
+}
+
+void menuHandler::GPSToggleMenu()
+{
+ static const char *optionsArray[] = {"Back", "Enabled", "Disabled"};
+ screen->showOverlayBanner(
+ "Toggle GPS", 30000, optionsArray, 3,
+ [](int selected) -> void {
+ if (selected == 1) {
+ config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED;
+ playGPSEnableBeep();
+ gps->enable();
+ service->reloadConfig(SEGMENT_CONFIG);
+ } else if (selected == 2) {
+ config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED;
+ playGPSDisableBeep();
+ gps->disable();
+ service->reloadConfig(SEGMENT_CONFIG);
+ } else {
+ menuQueue = position_base_menu;
+ }
+ },
+ config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2); // set inital selection
+}
+
+void menuHandler::BuzzerModeMenu()
+{
+ static const char *optionsArray[] = {"All Enabled", "Disabled", "Notifications", "System Only"};
+ screen->showOverlayBanner(
+ "Beep Action", 30000, optionsArray, 4,
+ [](int selected) -> void {
+ config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected;
+ service->reloadConfig(SEGMENT_CONFIG);
+ },
+ config.device.buzzer_mode);
+}
+
+void menuHandler::switchToMUIMenu()
+{
+ static const char *optionsArray[] = {"Yes", "No"};
+ screen->showOverlayBanner("Switch to MUI?", 30000, optionsArray, 2, [](int selected) -> void {
+ if (selected == 0) {
+ config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR;
+ config.bluetooth.enabled = false;
+ service->reloadConfig(SEGMENT_CONFIG);
+ rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
+ }
+ });
+}
+
+void menuHandler::handleMenuSwitch()
+{
+ switch (menuQueue) {
+ case menu_none:
+ break;
+ case lora_picker:
+ LoraRegionPicker();
+ break;
+ case TZ_picker:
+ TZPicker();
+ break;
+ case twelve_hour_picker:
+ TwelveHourPicker();
+ break;
+ case clock_face_picker:
+ ClockFacePicker();
+ break;
+ case clock_menu:
+ clockMenu();
+ break;
+ case position_base_menu:
+ positionBaseMenu();
+ break;
+ case gps_toggle_menu:
+ GPSToggleMenu();
+ break;
+ case compass_point_north_menu:
+ compassNorthMenu();
+ break;
+ case reset_node_db_menu:
+ resetNodeDBMenu();
+ break;
+ }
+ menuQueue = menu_none;
+}
+
+} // namespace graphics
+
+#endif
\ No newline at end of file
diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h
new file mode 100644
index 000000000..a5bea5176
--- /dev/null
+++ b/src/graphics/draw/MenuHandler.h
@@ -0,0 +1,40 @@
+#include "configuration.h"
+namespace graphics
+{
+
+class menuHandler
+{
+ public:
+ enum screenMenus {
+ menu_none,
+ lora_picker,
+ TZ_picker,
+ twelve_hour_picker,
+ clock_face_picker,
+ clock_menu,
+ position_base_menu,
+ gps_toggle_menu,
+ compass_point_north_menu,
+ reset_node_db_menu
+ };
+ static screenMenus menuQueue;
+
+ static void LoraRegionPicker(uint32_t duration = 30000);
+ static void handleMenuSwitch();
+ static void clockMenu();
+ static void TZPicker();
+ static void TwelveHourPicker();
+ static void ClockFacePicker();
+ static void messageResponseMenu();
+ static void homeBaseMenu();
+ static void favoriteBaseMenu();
+ static void positionBaseMenu();
+ static void compassNorthMenu();
+ static void GPSToggleMenu();
+ static void BuzzerModeMenu();
+ static void switchToMUIMenu();
+ static void nodeListMenu();
+ static void resetNodeDBMenu();
+};
+
+} // namespace graphics
\ No newline at end of file
diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp
index 707517d82..3df8a003c 100644
--- a/src/graphics/draw/MessageRenderer.cpp
+++ b/src/graphics/draw/MessageRenderer.cpp
@@ -56,6 +56,11 @@ namespace graphics
namespace MessageRenderer
{
+// Simple cache based on text hash
+static size_t cachedKey = 0;
+static std::vector cachedLines;
+static std::vector cachedHeights;
+
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount)
{
int cursorX = x;
@@ -225,6 +230,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
sender);
}
+ uint32_t now = millis();
#ifndef EXCLUDE_EMOJI
// === Bounce animation setup ===
static uint32_t lastBounceTime = 0;
@@ -232,7 +238,6 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
const int bounceRange = 2; // Max pixels to bounce up/down
const int bounceInterval = 10; // How quickly to change bounce direction (ms)
- uint32_t now = millis();
if (now - lastBounceTime >= bounceInterval) {
lastBounceTime = now;
bounceY = (bounceY + 1) % (bounceRange * 2);
@@ -246,82 +251,51 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
display->drawString(x + 4, headerY, headerStr);
// Draw separator (same as scroll version)
- for (int separatorX = 0; separatorX <= (display->getStringWidth(headerStr) + 3); separatorX += 2) {
- display->setPixel(separatorX, headerY + ((SCREEN_WIDTH > 128) ? 19 : 13));
+ for (int separatorX = 1; separatorX <= (display->getStringWidth(headerStr) + 2); separatorX += 2) {
+ display->setPixel(separatorX, headerY + ((isHighResolution) ? 19 : 13));
}
// Center the emote below the header line + separator + nav
int remainingHeight = SCREEN_HEIGHT - (headerY + FONT_HEIGHT_SMALL) - navHeight;
- int emoteY = headerY + FONT_HEIGHT_SMALL + (remainingHeight - e.height) / 2 + bounceY - bounceRange;
+ int emoteY = headerY + 6 + FONT_HEIGHT_SMALL + (remainingHeight - e.height) / 2 + bounceY - bounceRange;
display->drawXbm((SCREEN_WIDTH - e.width) / 2, emoteY, e.width, e.height, e.bitmap);
+
+ // Draw header at the end to sort out overlapping elements
+ graphics::drawCommonHeader(display, x, y, titleStr);
return;
}
}
#endif
+ // === Generate the cache key ===
+ size_t currentKey = (size_t)mp.from;
+ currentKey ^= ((size_t)mp.to << 8);
+ currentKey ^= ((size_t)mp.rx_time << 16);
+ currentKey ^= ((size_t)mp.id << 24);
- // === Word-wrap and build line list ===
- std::vector lines;
- lines.push_back(std::string(headerStr)); // Header line is always first
+ if (cachedKey != currentKey) {
+ LOG_INFO("Message cache key is misssed cachedKey=0x%0x, currentKey=0x%x", cachedKey, currentKey);
- std::string line, word;
- for (int i = 0; messageBuf[i]; ++i) {
- char ch = messageBuf[i];
- if (ch == '\n') {
- if (!word.empty())
- line += word;
- if (!line.empty())
- lines.push_back(line);
- line.clear();
- word.clear();
- } else if (ch == ' ') {
- line += word + ' ';
- word.clear();
- } else {
- word += ch;
- std::string test = line + word;
- if (display->getStringWidth(test.c_str()) > textWidth) {
- if (!line.empty())
- lines.push_back(line);
- line = word;
- word.clear();
- }
- }
+ // Cache miss - regenerate lines and heights
+ cachedLines = generateLines(display, headerStr, messageBuf, textWidth);
+ cachedHeights = calculateLineHeights(cachedLines, emotes);
+ cachedKey = currentKey;
+ } else {
+ // Cache hit but update the header line with current time information
+ cachedLines[0] = std::string(headerStr);
+ // The header always has a fixed height since it doesn't contain emotes
+ // As per calculateLineHeights logic for lines without emotes:
+ cachedHeights[0] = FONT_HEIGHT_SMALL - 2;
+ if (cachedHeights[0] < 8)
+ cachedHeights[0] = 8; // minimum safety
}
- if (!word.empty())
- line += word;
- if (!line.empty())
- lines.push_back(line);
// === Scrolling logic ===
- std::vector rowHeights;
-
- for (const auto &_line : lines) {
- int lineHeight = FONT_HEIGHT_SMALL;
- bool hasEmote = false;
-
- for (int i = 0; i < numEmotes; ++i) {
- const Emote &e = emotes[i];
- if (_line.find(e.label) != std::string::npos) {
- lineHeight = std::max(lineHeight, e.height);
- hasEmote = true;
- }
- }
-
- // Apply tighter spacing if no emotes on this line
- if (!hasEmote) {
- lineHeight -= 2; // reduce by 2px for tighter spacing
- if (lineHeight < 8)
- lineHeight = 8; // minimum safety
- }
-
- rowHeights.push_back(lineHeight);
- }
int totalHeight = 0;
- for (size_t i = 1; i < rowHeights.size(); ++i) {
- totalHeight += rowHeights[i];
+ for (size_t i = 1; i < cachedHeights.size(); ++i) {
+ totalHeight += cachedHeights[i];
}
- int usableScrollHeight = usableHeight - rowHeights[0]; // remove header height
- int scrollStop = std::max(0, totalHeight - usableScrollHeight + rowHeights.back());
+ int usableScrollHeight = usableHeight - cachedHeights[0]; // remove header height
+ int scrollStop = std::max(0, totalHeight - usableScrollHeight + cachedHeights.back());
static float scrollY = 0.0f;
static uint32_t lastTime = 0, scrollStartDelay = 0, pauseStart = 0;
@@ -363,28 +337,109 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
int scrollOffset = static_cast(scrollY);
int yOffset = -scrollOffset + getTextPositions(display)[1];
- for (int separatorX = 0; separatorX <= (display->getStringWidth(headerStr) + 3); separatorX += 2) {
- display->setPixel(separatorX, yOffset + ((SCREEN_WIDTH > 128) ? 19 : 13));
+ for (int separatorX = 1; separatorX <= (display->getStringWidth(headerStr) + 2); separatorX += 2) {
+ display->setPixel(separatorX, yOffset + ((isHighResolution) ? 19 : 13));
}
// === Render visible lines ===
+ renderMessageContent(display, cachedLines, cachedHeights, x, yOffset, scrollBottom, emotes, numEmotes, isInverted, isBold);
+
+ // Draw header at the end to sort out overlapping elements
+ graphics::drawCommonHeader(display, x, y, titleStr);
+}
+
+std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth)
+{
+ std::vector lines;
+ lines.push_back(std::string(headerStr)); // Header line is always first
+
+ std::string line, word;
+ for (int i = 0; messageBuf[i]; ++i) {
+ char ch = messageBuf[i];
+ if ((unsigned char)messageBuf[i] == 0xE2 && (unsigned char)messageBuf[i + 1] == 0x80 &&
+ (unsigned char)messageBuf[i + 2] == 0x99) {
+ ch = '\''; // plain apostrophe
+ i += 2; // skip over the extra UTF-8 bytes
+ }
+ if (ch == '\n') {
+ if (!word.empty())
+ line += word;
+ if (!line.empty())
+ lines.push_back(line);
+ line.clear();
+ word.clear();
+ } else if (ch == ' ') {
+ line += word + ' ';
+ word.clear();
+ } else {
+ word += ch;
+ std::string test = line + word;
+ // Keep these lines for diagnostics
+ // LOG_INFO("Char: '%c' (0x%02X)", ch, (unsigned char)ch);
+ // LOG_INFO("Current String: %s", test.c_str());
+ if (display->getStringWidth(test.c_str()) > textWidth) {
+ if (!line.empty())
+ lines.push_back(line);
+ line = word;
+ word.clear();
+ }
+ }
+ }
+
+ if (!word.empty())
+ line += word;
+ if (!line.empty())
+ lines.push_back(line);
+
+ return lines;
+}
+
+std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes)
+{
+ std::vector rowHeights;
+
+ for (const auto &_line : lines) {
+ int lineHeight = FONT_HEIGHT_SMALL;
+ bool hasEmote = false;
+
+ for (int i = 0; i < numEmotes; ++i) {
+ const Emote &e = emotes[i];
+ if (_line.find(e.label) != std::string::npos) {
+ lineHeight = std::max(lineHeight, e.height);
+ hasEmote = true;
+ }
+ }
+
+ // Apply tighter spacing if no emotes on this line
+ if (!hasEmote) {
+ lineHeight -= 2; // reduce by 2px for tighter spacing
+ if (lineHeight < 8)
+ lineHeight = 8; // minimum safety
+ }
+
+ rowHeights.push_back(lineHeight);
+ }
+
+ return rowHeights;
+}
+
+void renderMessageContent(OLEDDisplay *display, const std::vector &lines, const std::vector &rowHeights, int x,
+ int yOffset, int scrollBottom, const Emote *emotes, int numEmotes, bool isInverted, bool isBold)
+{
for (size_t i = 0; i < lines.size(); ++i) {
int lineY = yOffset;
for (size_t j = 0; j < i; ++j)
lineY += rowHeights[j];
if (lineY > -rowHeights[i] && lineY < scrollBottom) {
if (i == 0 && isInverted) {
- display->drawString(x + 3, lineY, lines[i].c_str());
+ display->drawString(x, lineY, lines[i].c_str());
if (isBold)
- display->drawString(x + 4, lineY, lines[i].c_str());
+ display->drawString(x, lineY, lines[i].c_str());
} else {
drawStringWithEmotes(display, x, lineY, lines[i], emotes, numEmotes);
}
}
}
-
- // Draw header at the end to sort out overlapping elements
- graphics::drawCommonHeader(display, x, y, titleStr);
}
} // namespace MessageRenderer
diff --git a/src/graphics/draw/MessageRenderer.h b/src/graphics/draw/MessageRenderer.h
index d92b96014..c15a699f7 100644
--- a/src/graphics/draw/MessageRenderer.h
+++ b/src/graphics/draw/MessageRenderer.h
@@ -2,6 +2,8 @@
#include "OLEDDisplay.h"
#include "OLEDDisplayUi.h"
#include "graphics/emotes.h"
+#include
+#include
namespace graphics
{
@@ -14,5 +16,15 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
/// Draws the text message frame for displaying received messages
void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
+// Function to generate lines with word wrapping
+std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth);
+
+// Function to calculate heights for each line
+std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes);
+
+// Function to render the message content
+void renderMessageContent(OLEDDisplay *display, const std::vector &lines, const std::vector &rowHeights, int x,
+ int yOffset, int scrollBottom, const Emote *emotes, int numEmotes, bool isInverted, bool isBold);
+
} // namespace MessageRenderer
} // namespace graphics
diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp
index 13b71546e..3f47a3a09 100644
--- a/src/graphics/draw/NodeListRenderer.cpp
+++ b/src/graphics/draw/NodeListRenderer.cpp
@@ -80,7 +80,11 @@ const char *getCurrentModeTitle(int screenWidth)
case MODE_LAST_HEARD:
return "Last Heard";
case MODE_HOP_SIGNAL:
- return (screenWidth > 128) ? "Hops/Signal" : "Hops/Sig";
+#ifdef USE_EINK
+ return "Hops/Sig";
+#else
+ return (isHighResolution) ? "Hops/Signal" : "Hops/Sig";
+#endif
case MODE_DISTANCE:
return "Distance";
default:
@@ -94,50 +98,11 @@ unsigned long getModeCycleIntervalMs()
return 3000;
}
-// Calculate bearing between two lat/lon points
-float calculateBearing(double lat1, double lon1, double lat2, double lon2)
-{
- double dLon = (lon2 - lon1) * DEG_TO_RAD;
- double y = sin(dLon) * cos(lat2 * DEG_TO_RAD);
- double x = cos(lat1 * DEG_TO_RAD) * sin(lat2 * DEG_TO_RAD) - sin(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * cos(dLon);
- double bearing = atan2(y, x) * RAD_TO_DEG;
- return fmod(bearing + 360.0, 360.0);
-}
-
int calculateMaxScroll(int totalEntries, int visibleRows)
{
return std::max(0, (totalEntries - 1) / (visibleRows * 2));
}
-void retrieveAndSortNodes(std::vector &nodeList)
-{
- size_t numNodes = nodeDB->getNumMeshNodes();
- for (size_t i = 0; i < numNodes; i++) {
- meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
- if (!node || node->num == nodeDB->getNodeNum())
- continue;
-
- NodeEntry entry;
- entry.node = node;
- entry.sortValue = sinceLastSeen(node);
-
- nodeList.push_back(entry);
- }
-
- // Sort nodes: favorites first, then by last heard (most recent first)
- std::sort(nodeList.begin(), nodeList.end(), [](const NodeEntry &a, const NodeEntry &b) {
- bool aFav = a.node->is_favorite;
- bool bFav = b.node->is_favorite;
- if (aFav != bFav)
- return aFav;
- if (a.sortValue == 0 || a.sortValue == UINT32_MAX)
- return false;
- if (b.sortValue == 0 || b.sortValue == UINT32_MAX)
- return true;
- return a.sortValue < b.sortValue;
- });
-}
-
void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd)
{
int columnWidth = display->getWidth() / 2;
@@ -170,7 +135,7 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
{
bool isLeftCol = (x < SCREEN_WIDTH / 2);
- int timeOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7);
+ int timeOffset = (isHighResolution) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7);
const char *nodeName = getSafeNodeName(node);
@@ -191,9 +156,9 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
- display->drawString(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nodeName);
+ display->drawString(x + ((isHighResolution) ? 6 : 3), y, nodeName);
if (node->is_favorite) {
- if (SCREEN_WIDTH > 128) {
+ if (isHighResolution) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
@@ -212,8 +177,8 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
bool isLeftCol = (x < SCREEN_WIDTH / 2);
int nameMaxWidth = columnWidth - 25;
- int barsOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
- int hopOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
+ int barsOffset = (isHighResolution) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
+ int hopOffset = (isHighResolution) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
int barsXOffset = columnWidth - barsOffset;
@@ -222,9 +187,9 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
- display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName);
+ display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName);
if (node->is_favorite) {
- if (SCREEN_WIDTH > 128) {
+ if (isHighResolution) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
@@ -259,7 +224,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
{
bool isLeftCol = (x < SCREEN_WIDTH / 2);
- int nameMaxWidth = columnWidth - (SCREEN_WIDTH > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
+ int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
const char *nodeName = getSafeNodeName(node);
char distStr[10] = "";
@@ -314,9 +279,9 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
- display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName);
+ display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName);
if (node->is_favorite) {
- if (SCREEN_WIDTH > 128) {
+ if (isHighResolution) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
@@ -324,8 +289,8 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
}
if (strlen(distStr) > 0) {
- int offset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column)
- : (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column)
+ int offset = (isHighResolution) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column)
+ : (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column)
int rightEdge = x + columnWidth - offset;
int textWidth = display->getStringWidth(distStr);
display->drawString(rightEdge - textWidth, y, distStr);
@@ -354,15 +319,15 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
bool isLeftCol = (x < SCREEN_WIDTH / 2);
// Adjust max text width depending on column and screen width
- int nameMaxWidth = columnWidth - (SCREEN_WIDTH > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
+ int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
const char *nodeName = getSafeNodeName(node);
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
- display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName);
+ display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName);
if (node->is_favorite) {
- if (SCREEN_WIDTH > 128) {
+ if (isHighResolution) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
@@ -377,19 +342,21 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
return;
bool isLeftCol = (x < SCREEN_WIDTH / 2);
- int arrowXOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18);
+ int arrowXOffset = (isHighResolution) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18);
int centerX = x + columnWidth - arrowXOffset;
int centerY = y + FONT_HEIGHT_SMALL / 2;
double nodeLat = node->position.latitude_i * 1e-7;
double nodeLon = node->position.longitude_i * 1e-7;
- float bearingToNode = calculateBearing(userLat, userLon, nodeLat, nodeLon);
+ float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon);
+ float bearingToNode = RAD_TO_DEG * bearing;
float relativeBearing = fmod((bearingToNode - myHeading + 360), 360);
float angle = relativeBearing * DEG_TO_RAD;
-
// Shrink size by 2px
int size = FONT_HEIGHT_SMALL - 5;
+ CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearing);
+ /*
float halfSize = size / 2.0;
// Point of the arrow
@@ -414,6 +381,7 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
// Draw the chevron-style arrowhead
display->fillTriangle(tipX, tipY, leftX, leftY, notchX, notchY);
display->fillTriangle(tipX, tipY, notchX, notchY, rightX, rightY);
+ */
}
// =============================
@@ -436,19 +404,16 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
// Space below header
y += COMMON_HEADER_HEIGHT;
- // Fetch and display sorted node list
- std::vector nodeList;
- retrieveAndSortNodes(nodeList);
-
- int totalEntries = nodeList.size();
+ int totalEntries = nodeDB->getNumMeshNodes();
int totalRowsAvailable = (display->getHeight() - y) / rowYOffset;
-#ifdef USE_EINK
- totalRowsAvailable -= 1;
-#endif
+
int visibleNodeRows = totalRowsAvailable;
int totalColumns = 2;
int startIndex = scrollIndex * visibleNodeRows * totalColumns;
+ if (nodeDB->getMeshNodeByIndex(startIndex)->num == nodeDB->getNodeNum()) {
+ startIndex++; // skip own node
+ }
int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries);
int yOffset = 0;
@@ -460,10 +425,10 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
for (int i = startIndex; i < endIndex; ++i) {
int xPos = x + (col * columnWidth);
int yPos = y + yOffset;
- renderer(display, nodeList[i].node, xPos, yPos, columnWidth);
+ renderer(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth);
if (extras) {
- extras(display, nodeList[i].node, xPos, yPos, columnWidth, heading, lat, lon);
+ extras(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth, heading, lat, lon);
}
lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
@@ -533,7 +498,12 @@ void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
+#ifdef USE_EINK
+ const char *title = "Hops/Sig";
+#else
+
const char *title = "Hops/Signal";
+#endif
drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal);
}
@@ -548,22 +518,24 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
{
float heading = 0;
bool validHeading = false;
- double lat = 0;
- double lon = 0;
+ auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
+ double lat = DegD(ourNode->position.latitude_i);
+ double lon = DegD(ourNode->position.longitude_i);
+ if (!screen->ignoreCompass) {
#if HAS_GPS
- if (screen->hasHeading()) {
- heading = screen->getHeading(); // degrees
- validHeading = true;
- } else {
- heading = screen->estimatedHeading(lat, lon);
- validHeading = !isnan(heading);
- }
+ if (screen->hasHeading()) {
+ heading = screen->getHeading(); // degrees
+ validHeading = true;
+ } else {
+ heading = screen->estimatedHeading(lat, lon);
+ validHeading = !isnan(heading);
+ }
#endif
- if (!validHeading)
- return;
-
+ if (!validHeading)
+ return;
+ }
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon);
}
diff --git a/src/graphics/draw/NodeListRenderer.h b/src/graphics/draw/NodeListRenderer.h
index 63f0d1c69..ea8df8bd9 100644
--- a/src/graphics/draw/NodeListRenderer.h
+++ b/src/graphics/draw/NodeListRenderer.h
@@ -23,12 +23,6 @@ namespace NodeListRenderer
typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int);
typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double);
-// Node entry structure
-struct NodeEntry {
- meshtastic_NodeInfoLite *node;
- uint32_t sortValue;
-};
-
// Node list mode enumeration
enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 };
@@ -57,7 +51,6 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
// Utility functions
const char *getCurrentModeTitle(int screenWidth);
-void retrieveAndSortNodes(std::vector &nodeList);
const char *getSafeNodeName(meshtastic_NodeInfoLite *node);
void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields);
diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp
index ed5257012..4866b4060 100644
--- a/src/graphics/draw/NotificationRenderer.cpp
+++ b/src/graphics/draw/NotificationRenderer.cpp
@@ -31,6 +31,7 @@ int8_t NotificationRenderer::curSelected = 0;
char NotificationRenderer::alertBannerMessage[256] = {0};
uint32_t NotificationRenderer::alertBannerUntil = 0; // 0 is a special case meaning forever
uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are seelctable options
+const char **NotificationRenderer::optionsArrayPtr = nullptr;
std::function NotificationRenderer::alertBannerCallback = NULL;
bool NotificationRenderer::pauseBanner = false;
@@ -56,29 +57,22 @@ void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiStat
void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state)
{
- // Exit if no message is active or duration has passed
- if (!isOverlayBannerShowing())
- return;
-
- if (pauseBanner)
+ if (!isOverlayBannerShowing() || pauseBanner)
return;
// === Layout Configuration ===
- constexpr uint16_t padding = 5; // Padding around text inside the box
- constexpr uint16_t vPadding = 2; // Padding around text inside the box
- constexpr uint8_t lineSpacing = 1; // Extra space between lines
+ constexpr uint16_t hPadding = 5;
+ constexpr uint16_t vPadding = 2;
+ constexpr uint8_t lineSpacing = 1;
- // Search the message to determine if we need the bell added
bool needs_bell = (strstr(alertBannerMessage, "Alert Received") != nullptr);
- uint8_t firstOption = 0;
- uint8_t firstOptionToShow = 0;
-
// Setup font and alignment
display->setFont(FONT_SMALL);
- display->setTextAlignment(TEXT_ALIGN_LEFT); // We will manually center per line
- const int MAX_LINES = 24;
+ display->setTextAlignment(TEXT_ALIGN_LEFT);
+ constexpr int MAX_LINES = 5;
+ uint16_t optionWidths[alertBannerOptions] = {0};
uint16_t maxWidth = 0;
uint16_t arrowsWidth = display->getStringWidth("> <", 4, true);
uint16_t lineWidths[MAX_LINES] = {0};
@@ -86,30 +80,33 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
char *lineStarts[MAX_LINES + 1];
uint16_t lineCount = 0;
char lineBuffer[40] = {0};
- // pointer to the terminating null
+
+ // Parse lines
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
lineStarts[lineCount] = alertBannerMessage;
- // loop through lines finding \n characters
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
lineStarts[lineCount + 1] = std::find(lineStarts[lineCount], alertEnd, '\n');
lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount];
- if (lineStarts[lineCount + 1][0] == '\n') {
- lineStarts[lineCount + 1] += 1; // Move the start pointer beyond the \n
- }
+ if (lineStarts[lineCount + 1][0] == '\n')
+ lineStarts[lineCount + 1] += 1;
lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true);
- if (lineWidths[lineCount] > maxWidth) {
+ if (lineWidths[lineCount] > maxWidth)
maxWidth = lineWidths[lineCount];
- }
- if (alertBannerOptions > 0 && lineCount > 0 && lineWidths[lineCount] + arrowsWidth > maxWidth) {
- maxWidth = lineWidths[lineCount] + arrowsWidth;
- }
lineCount++;
- // if we are doing a selection, add extra width for arrows
}
+ // Measure option widths
+ for (int i = 0; i < alertBannerOptions; i++) {
+ optionWidths[i] = display->getStringWidth(optionsArrayPtr[i], strlen(optionsArrayPtr[i]), true);
+ if (optionWidths[i] > maxWidth)
+ maxWidth = optionWidths[i];
+ if (optionWidths[i] + arrowsWidth > maxWidth)
+ maxWidth = optionWidths[i] + arrowsWidth;
+ }
+
+ // Handle input
if (alertBannerOptions > 0) {
- // respond to input
if (inEvent == INPUT_BROKER_UP || inEvent == INPUT_BROKER_ALT_PRESS) {
curSelected--;
} else if (inEvent == INPUT_BROKER_DOWN || inEvent == INPUT_BROKER_USER_PRESS) {
@@ -120,113 +117,133 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
} else if ((inEvent == INPUT_BROKER_CANCEL || inEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) {
alertBannerMessage[0] = '\0';
}
+
if (curSelected == -1)
curSelected = alertBannerOptions - 1;
if (curSelected == alertBannerOptions)
curSelected = 0;
- // compare number of options to number of lines
- if (lineCount < alertBannerOptions)
- return;
- firstOption = lineCount - alertBannerOptions;
- if (curSelected > 1 && alertBannerOptions > 3) {
- firstOptionToShow = curSelected + firstOption - 1;
- // put the selected option in the middle
- } else {
- firstOptionToShow = firstOption;
- }
- } else { // not in an alert with a callback
- // TODO: check that at least a second has passed since the alert started
+ } else {
if (inEvent == INPUT_BROKER_SELECT || inEvent == INPUT_BROKER_ALT_LONG || inEvent == INPUT_BROKER_CANCEL) {
- alertBannerMessage[0] = '\0'; // end the alert early
+ alertBannerMessage[0] = '\0';
}
}
+
inEvent = INPUT_BROKER_NONE;
if (alertBannerMessage[0] == '\0')
return;
- // set width from longest line
- uint16_t boxWidth = padding * 2 + maxWidth;
+ // === Box Size Calculation ===
+ uint16_t boxWidth = hPadding * 2 + maxWidth;
if (needs_bell) {
- if (SCREEN_WIDTH > 128 && boxWidth <= 150) {
+ if (isHighResolution && boxWidth <= 150)
boxWidth += 26;
- }
- if (SCREEN_WIDTH <= 128 && boxWidth <= 100) {
+ if (!isHighResolution && boxWidth <= 100)
boxWidth += 20;
- }
- }
- // calculate max lines on screen? for now it's 4
- // set height from line count
- uint16_t boxHeight;
- if (lineCount <= 4) {
- boxHeight = vPadding * 2 + lineCount * FONT_HEIGHT_SMALL + (lineCount - 1) * lineSpacing;
- } else {
- boxHeight = vPadding * 2 + 4 * FONT_HEIGHT_SMALL + 4 * lineSpacing;
}
+ uint16_t totalLines = lineCount + alertBannerOptions;
+ uint16_t screenHeight = display->height();
+ uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
+ uint8_t visibleTotalLines = std::min(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight);
+ uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
+ uint16_t boxHeight = contentHeight + vPadding * 2;
+
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
- // === Draw background box ===
+
+ // === Draw Box ===
display->setColor(BLACK);
- display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Slightly oversized box
- display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); // Top Line
- display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); // Bottom Line
- display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); // Left Line
- display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); // Right Line
+ display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2);
+ display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
+ display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1);
+ display->fillRect(boxLeft - 2, boxTop, 1, boxHeight);
+ display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
display->setColor(WHITE);
- display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); // Border
+ display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
display->setColor(BLACK);
- display->fillRect(boxLeft, boxTop, 1, 1); // Top Left
- display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); // Top Right
- display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); // Bottom Left
- display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); // Bottom Right
+ display->fillRect(boxLeft, boxTop, 1, 1);
+ display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1);
+ display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
+ display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
display->setColor(WHITE);
- // === Draw each line centered in the box ===
+ // === Draw Content ===
int16_t lineY = boxTop + vPadding;
+ uint8_t linesShown = 0;
- for (int i = 0; i < lineCount; i++) {
- // is this line selected?
- // if so, start the buffer with -> and strncpy to the 4th location
- if (i < lineCount - alertBannerOptions || alertBannerOptions == 0) {
- strncpy(lineBuffer, lineStarts[i], 40);
- if (lineLengths[i] > 39)
- lineBuffer[39] = '\0';
- else
- lineBuffer[lineLengths[i]] = '\0';
- } else if (i >= firstOptionToShow && i < firstOptionToShow + 3) {
- if (i == curSelected + firstOption) {
- if (lineLengths[i] > 35)
- lineLengths[i] = 35;
- strncpy(lineBuffer, "> ", 3);
- strncpy(lineBuffer + 2, lineStarts[i], 36);
- strncpy(lineBuffer + lineLengths[i] + 2, " <", 3);
- lineLengths[i] += 4;
- lineWidths[i] += display->getStringWidth("> <", 4, true);
- if (lineLengths[i] > 35)
- lineBuffer[39] = '\0';
- else
- lineBuffer[lineLengths[i]] = '\0';
- } else {
- strncpy(lineBuffer, lineStarts[i], 40);
- if (lineLengths[i] > 39)
- lineBuffer[39] = '\0';
- else
- lineBuffer[lineLengths[i]] = '\0';
- }
- } else { // add break for the additional lines
- continue;
- }
+ for (int i = 0; i < lineCount && linesShown < visibleTotalLines; i++, linesShown++) {
+ strncpy(lineBuffer, lineStarts[i], 40);
+ lineBuffer[lineLengths[i] > 39 ? 39 : lineLengths[i]] = '\0';
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
-
if (needs_bell && i == 0) {
int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2;
display->drawXbm(textX - 10, bellY, 8, 8, bell_alert);
display->drawXbm(textX + lineWidths[i] + 2, bellY, 8, 8, bell_alert);
}
+ // Determine if this is a pop-up or a pick list
+ if (alertBannerOptions > 0) {
+ // Pick List
+ display->setColor(WHITE);
+ int background_yOffset = 1;
+ // Determine if we have low hanging characters
+ if (strchr(lineBuffer, 'p') || strchr(lineBuffer, 'g') || strchr(lineBuffer, 'y') || strchr(lineBuffer, 'j')) {
+ background_yOffset = -1;
+ }
+ display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset);
+ display->setColor(BLACK);
+ int yOffset = 3;
+ display->drawString(textX, lineY - yOffset, lineBuffer);
+ display->setColor(WHITE);
+ lineY += (effectiveLineHeight - 2 - background_yOffset);
+ } else {
+ // Pop-up
+ display->drawString(textX, lineY - 2, lineBuffer);
+ lineY += (effectiveLineHeight);
+ }
+ }
+
+ uint8_t firstOptionToShow = 0;
+ if (alertBannerOptions > 0) {
+ if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount)
+ firstOptionToShow = curSelected - 1;
+ else
+ firstOptionToShow = 0;
+ }
+
+ for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
+ if (i == curSelected) {
+ strncpy(lineBuffer, "> ", 3);
+ strncpy(lineBuffer + 2, optionsArrayPtr[i], 36);
+ strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3);
+ lineBuffer[39] = '\0';
+ } else {
+ strncpy(lineBuffer, optionsArrayPtr[i], 40);
+ lineBuffer[39] = '\0';
+ }
+
+ int16_t textX = boxLeft + (boxWidth - optionWidths[i] - (i == curSelected ? arrowsWidth : 0)) / 2;
display->drawString(textX, lineY, lineBuffer);
- lineY += FONT_HEIGHT_SMALL + lineSpacing;
+ lineY += effectiveLineHeight;
+ }
+
+ // === Scroll Bar (Thicker, inside box, not over title) ===
+ if (totalLines > visibleTotalLines) {
+ const uint8_t scrollBarWidth = 5;
+ const uint8_t scrollPadding = 2;
+
+ int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2;
+ int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; // start after title line
+ uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight;
+
+ float ratio = (float)visibleTotalLines / totalLines;
+ uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4);
+ float scrollRatio = (float)(firstOptionToShow + linesShown - visibleTotalLines) / (totalLines - visibleTotalLines);
+ uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight);
+
+ display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
+ display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
}
}
diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h
index 3ed931dc6..2ec5fd9ec 100644
--- a/src/graphics/draw/NotificationRenderer.h
+++ b/src/graphics/draw/NotificationRenderer.h
@@ -12,7 +12,8 @@ class NotificationRenderer
static char inEvent;
static int8_t curSelected;
static char alertBannerMessage[256];
- static uint32_t alertBannerUntil; // 0 is a special case meaning forever
+ static uint32_t alertBannerUntil; // 0 is a special case meaning forever
+ static const char **optionsArrayPtr;
static uint8_t alertBannerOptions; // last x lines are seelctable options
static std::function alertBannerCallback;
diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp
index a77d5b44b..1738a8246 100644
--- a/src/graphics/draw/UIRenderer.cpp
+++ b/src/graphics/draw/UIRenderer.cpp
@@ -18,6 +18,32 @@
#include
#include
+bool isAllowedPunctuation(char c)
+{
+ const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ ";
+ return allowed.find(c) != std::string::npos;
+}
+
+std::string sanitizeString(const std::string &input)
+{
+ std::string output;
+ bool inReplacement = false;
+
+ for (char c : input) {
+ if (std::isalnum(static_cast(c)) || isAllowedPunctuation(c)) {
+ output += c;
+ inReplacement = false;
+ } else {
+ if (!inReplacement) {
+ output += 0xbf; // ISO-8859-1 for inverted question mark
+ inReplacement = true;
+ }
+ }
+ }
+
+ return output;
+}
+
#if !MESHTASTIC_EXCLUDE_GPS
// External variables
@@ -38,7 +64,7 @@ NodeNum UIRenderer::currentFavoriteNodeNum = 0;
void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps)
{
// Draw satellite image
- if (SCREEN_WIDTH > 128) {
+ if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y - 2, imgSatellite_width, imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, y + 1, imgSatellite_width, imgSatellite_height, imgSatellite);
@@ -58,7 +84,7 @@ void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const mesht
} else {
snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites());
}
- if (SCREEN_WIDTH > 128) {
+ if (isHighResolution) {
display->drawString(x + 18, y, textString);
} else {
display->drawString(x + 11, y, textString);
@@ -163,46 +189,6 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y,
}
}
-void UIRenderer::drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer,
- const meshtastic::PowerStatus *powerStatus)
-{
- static const uint8_t powerBar[3] = {0x81, 0xBD, 0xBD};
- static const uint8_t lightning[8] = {0xA1, 0xA1, 0xA5, 0xAD, 0xB5, 0xA5, 0x85, 0x85};
-
- // Clear the bar area inside the battery image
- for (int i = 1; i < 14; i++) {
- imgBuffer[i] = 0x81;
- }
-
- // Fill with lightning or power bars
- if (powerStatus->getIsCharging()) {
- memcpy(imgBuffer + 3, lightning, 8);
- } else {
- for (int i = 0; i < 4; i++) {
- if (powerStatus->getBatteryChargePercent() >= 25 * i)
- memcpy(imgBuffer + 1 + (i * 3), powerBar, 3);
- }
- }
-
- // Slightly more conservative scaling based on screen width
- int scale = 1;
-
- if (SCREEN_WIDTH >= 200)
- scale = 2;
- if (SCREEN_WIDTH >= 300)
- scale = 2; // Do NOT go higher than 2
-
- // Draw scaled battery image (16 columns × 8 rows)
- for (int col = 0; col < 16; col++) {
- uint8_t colBits = imgBuffer[col];
- for (int row = 0; row < 8; row++) {
- if (colBits & (1 << row)) {
- display->fillRect(x + col * scale, y + row * scale, scale, scale);
- }
- }
- }
-}
-
// Draw nodes status
void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset,
bool show_total, String additional_words)
@@ -221,19 +207,19 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS)
- if (SCREEN_WIDTH > 128) {
+ if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display);
} else {
display->drawFastImage(x, y + 3, 8, 8, imgUser);
}
#else
- if (SCREEN_WIDTH > 128) {
+ if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display);
} else {
display->drawFastImage(x, y + 1, 8, 8, imgUser);
}
#endif
- int string_offset = (SCREEN_WIDTH > 128) ? 9 : 0;
+ int string_offset = (isHighResolution) ? 9 : 0;
display->drawString(x + 10 + string_offset, y - 2, usersString);
}
@@ -293,12 +279,14 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
// List of available macro Y positions in order, from top to bottom.
int line = 1; // which slot to use next
+ std::string usernameStr;
// === 1. Long Name (always try to show first) ===
const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr;
- if (username && line < 5) {
+ if (username) {
+ usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case
// Print node's long name (e.g. "Backpack Node")
- display->drawString(x, getTextPositions(display)[line++], username);
+ display->drawString(x, getTextPositions(display)[line++], usernameStr.c_str());
}
// === 2. Signal and Hops (combined on one line, if available) ===
@@ -456,8 +444,11 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
*/
float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
- if (!config.display.compass_north_top)
+ if (screen->ignoreCompass) {
+ myHeading = 0;
+ } else {
bearing -= myHeading;
+ }
display->drawCircle(compassX, compassY, compassRadius);
CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
@@ -476,7 +467,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
const int margin = 4;
// --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) -----------
#if defined(USE_EINK)
- const int iconSize = (SCREEN_WIDTH > 128) ? 16 : 8;
+ const int iconSize = (isHighResolution) ? 16 : 8;
const int navBarHeight = iconSize + 6;
#else
const int navBarHeight = 0;
@@ -497,8 +488,11 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
int compassY = yBelowContent + availableHeight / 2;
const auto &op = ourNode->position;
- float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180
- : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
+ float myHeading = 0;
+ if (!screen->ignoreCompass) {
+ myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180
+ : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
+ }
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
const auto &p = node->position;
@@ -507,7 +501,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
*/
float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
- if (!config.display.compass_north_top)
+ if (!screen->ignoreCompass)
bearing -= myHeading;
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing);
@@ -570,15 +564,15 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
} else {
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
}
- int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1;
- if (SCREEN_WIDTH > 128) {
+ int yOffset = (isHighResolution) ? 3 : 1;
+ if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
imgSatellite);
}
- int xOffset = (SCREEN_WIDTH > 128) ? 6 : 0;
+ int xOffset = (isHighResolution) ? 6 : 0;
display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine);
} else {
UIRenderer::drawGps(display, 0, getTextPositions(display)[line], gpsStatus);
@@ -602,17 +596,17 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
char chUtilPercentage[10];
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
- int chUtil_x = (SCREEN_WIDTH > 128) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5;
+ int chUtil_x = (isHighResolution) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5;
int chUtil_y = getTextPositions(display)[line] + 3;
- int chutil_bar_width = (SCREEN_WIDTH > 128) ? 100 : 50;
+ int chutil_bar_width = (isHighResolution) ? 100 : 50;
if (!config.bluetooth.enabled) {
- chutil_bar_width = (SCREEN_WIDTH > 128) ? 80 : 40;
+ chutil_bar_width = (isHighResolution) ? 80 : 40;
}
- int chutil_bar_height = (SCREEN_WIDTH > 128) ? 12 : 7;
- int extraoffset = (SCREEN_WIDTH > 128) ? 6 : 3;
+ int chutil_bar_height = (isHighResolution) ? 12 : 7;
+ int extraoffset = (isHighResolution) ? 6 : 3;
if (!config.bluetooth.enabled) {
- extraoffset = (SCREEN_WIDTH > 128) ? 6 : 1;
+ extraoffset = (isHighResolution) ? 6 : 1;
}
int chutil_percent = airTime->channelUtilizationPercent();
@@ -672,21 +666,20 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
// === Fourth & Fifth Rows: Node Identity ===
int textWidth = 0;
int nameX = 0;
- int yOffset = (SCREEN_WIDTH > 128) ? 0 : 5;
+ int yOffset = (isHighResolution) ? 0 : 5;
const char *longName = nullptr;
+ std::string longNameStr;
+
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) {
- longName = ourNode->user.long_name;
+ longNameStr = sanitizeString(ourNode->user.long_name);
}
- uint8_t dmac[6];
char shortnameble[35];
- getMacAddr(dmac);
- snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
snprintf(shortnameble, sizeof(shortnameble), "%s",
graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : "");
char combinedName[50];
- snprintf(combinedName, sizeof(combinedName), "%s (%s)", longName, shortnameble);
+ snprintf(combinedName, sizeof(combinedName), "%s (%s)", longNameStr.empty() ? "" : longNameStr.c_str(), shortnameble);
if (SCREEN_WIDTH - (display->getStringWidth(longName) + display->getStringWidth(shortnameble)) > 10) {
size_t len = strlen(combinedName);
if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) {
@@ -700,7 +693,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
// === LongName Centered ===
textWidth = display->getStringWidth(longName);
nameX = (SCREEN_WIDTH - textWidth) / 2;
- display->drawString(nameX, getTextPositions(display)[line++], longName);
+ display->drawString(nameX, getTextPositions(display)[line++], longNameStr.c_str());
// === ShortName Centered ===
textWidth = display->getStringWidth(shortnameble);
@@ -808,44 +801,42 @@ void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState
{
LOG_DEBUG("Draw screensaver overlay");
- EINK_ADD_FRAMEFLAG(display, COSMETIC); // Take the opportunity for a full-refresh
+ EINK_ADD_FRAMEFLAG(display, COSMETIC); // Full refresh for screensaver
// Config
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT);
const char *pauseText = "Screen Paused";
const char *idText = owner.short_name;
- const bool useId = haveGlyphs(idText); // This bool is used to hide the idText box if we can't render the short name
- constexpr uint16_t padding = 5;
+ const bool useId = haveGlyphs(idText);
+ constexpr uint8_t padding = 2;
constexpr uint8_t dividerGap = 1;
- constexpr uint8_t imprecision = 5; // How far the box origins can drift from center. Combat burn-in.
- // Dimensions
- const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true); // "true": handle utf8 chars
+ // Text widths
+ const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true);
const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText));
- const uint16_t boxWidth = padding + (useId ? idTextWidth + padding + padding : 0) + pauseTextWidth + padding;
- const uint16_t boxHeight = padding + FONT_HEIGHT_SMALL + padding;
+ const uint16_t boxWidth = padding + (useId ? idTextWidth + padding : 0) + pauseTextWidth + padding;
+ const uint16_t boxHeight = FONT_HEIGHT_SMALL + (padding * 2);
- // Position
- const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2) + random(-imprecision, imprecision + 1);
- // const int16_t boxRight = boxLeft + boxWidth - 1;
- const int16_t boxTop = (display->height() / 2) - (boxHeight / 2 + random(-imprecision, imprecision + 1));
- const int16_t boxBottom = boxTop + boxHeight - 1;
+ // Flush with bottom
+ const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
+ const int16_t boxTop = display->height() - boxHeight;
+ const int16_t boxBottom = display->height() - 1;
const int16_t idTextLeft = boxLeft + padding;
const int16_t idTextTop = boxTop + padding;
- const int16_t pauseTextLeft = boxLeft + (useId ? padding + idTextWidth + padding : 0) + padding;
+ const int16_t pauseTextLeft = boxLeft + (useId ? idTextWidth + (padding * 2) : 0) + padding;
const int16_t pauseTextTop = boxTop + padding;
const int16_t dividerX = boxLeft + padding + idTextWidth + padding;
- const int16_t dividerTop = boxTop + 1 + dividerGap;
- const int16_t dividerBottom = boxBottom - 1 - dividerGap;
+ const int16_t dividerTop = boxTop + dividerGap;
+ const int16_t dividerBottom = boxBottom - dividerGap;
// Draw: box
display->setColor(EINK_WHITE);
- display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Clear a slightly oversized area for the box
+ display->fillRect(boxLeft, boxTop, boxWidth, boxHeight);
display->setColor(EINK_BLACK);
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
- // Draw: Text
+ // Draw: text
if (useId)
display->drawString(idTextLeft, idTextTop, idText);
display->drawString(pauseTextLeft, pauseTextTop, pauseText);
@@ -920,15 +911,15 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
} else {
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
}
- int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1;
- if (SCREEN_WIDTH > 128) {
+ int yOffset = (isHighResolution) ? 3 : 1;
+ if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
imgSatellite);
}
- int xOffset = (SCREEN_WIDTH > 128) ? 6 : 0;
+ int xOffset = (isHighResolution) ? 6 : 0;
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
} else {
UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus);
@@ -941,15 +932,18 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
int32_t(gpsStatus->getAltitude()));
// === Determine Compass Heading ===
- float heading;
+ float heading = 0;
bool validHeading = false;
-
- if (screen->hasHeading()) {
- heading = radians(screen->getHeading());
+ if (screen->ignoreCompass) {
validHeading = true;
} else {
- heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7);
- validHeading = !isnan(heading);
+ if (screen->hasHeading()) {
+ heading = radians(screen->getHeading());
+ validHeading = true;
+ } else {
+ heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7);
+ validHeading = !isnan(heading);
+ }
}
// If GPS is off, no need to display these parts
@@ -1005,7 +999,9 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
display->drawCircle(compassX, compassY, compassRadius);
// "N" label
- float northAngle = -heading;
+ float northAngle = 0;
+ if (!config.display.compass_north_top)
+ northAngle = -heading;
float radius = compassRadius;
int16_t nX = compassX + (radius - 1) * sin(northAngle);
int16_t nY = compassY - (radius - 1) * cos(northAngle);
@@ -1046,7 +1042,9 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
display->drawCircle(compassX, compassY, compassRadius);
// "N" label
- float northAngle = -heading;
+ float northAngle = 0;
+ if (!config.display.compass_north_top)
+ northAngle = -heading;
float radius = compassRadius;
int16_t nX = compassX + (radius - 1) * sin(northAngle);
int16_t nY = compassY - (radius - 1) * cos(northAngle);
@@ -1114,18 +1112,6 @@ void UIRenderer::drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *sta
#endif
-// Function overlay for showing mute/buzzer modifiers etc.
-void UIRenderer::drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state)
-{
- // LOG_DEBUG("Draw function overlay");
- if (functionSymbol.begin() != functionSymbol.end()) {
- char buf[64];
- display->setFont(FONT_SMALL);
- snprintf(buf, sizeof(buf), "%s", functionSymbolString.c_str());
- display->drawString(SCREEN_WIDTH - display->getStringWidth(buf), SCREEN_HEIGHT - FONT_HEIGHT_SMALL, buf);
- }
-}
-
// Navigation bar overlay implementation
static int8_t lastFrameIndex = -1;
static uint32_t lastFrameChangeTime = 0;
@@ -1141,10 +1127,9 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
lastFrameChangeTime = millis();
}
- const bool useBigIcons = (SCREEN_WIDTH > 128);
- const int iconSize = useBigIcons ? 16 : 8;
- const int spacing = useBigIcons ? 8 : 4;
- const int bigOffset = useBigIcons ? 1 : 0;
+ const int iconSize = isHighResolution ? 16 : 8;
+ const int spacing = isHighResolution ? 8 : 4;
+ const int bigOffset = isHighResolution ? 1 : 0;
const size_t totalIcons = screen->indicatorIcons.size();
if (totalIcons == 0)
@@ -1158,14 +1143,35 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing;
const int xStart = (SCREEN_WIDTH - totalWidth) / 2;
- // Only show bar briefly after switching frames (unless on E-Ink)
+ // Only show bar briefly after switching frames
+ static uint32_t navBarLastShown = 0;
+ static bool cosmeticRefreshDone = false;
+
+ bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS;
+ int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT;
+
#if defined(USE_EINK)
- int y = SCREEN_HEIGHT - iconSize - 1;
-#else
- int y = SCREEN_HEIGHT - iconSize - 1;
- if (millis() - lastFrameChangeTime > ICON_DISPLAY_DURATION_MS) {
- y = SCREEN_HEIGHT;
+ static bool navBarPrevVisible = false;
+
+ if (navBarVisible && !navBarPrevVisible) {
+ EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when showing nav bar
+ cosmeticRefreshDone = false;
+ navBarLastShown = millis();
}
+
+ if (!navBarVisible && navBarPrevVisible) {
+ EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when hiding nav bar
+ navBarLastShown = millis(); // Mark when it disappeared
+ }
+
+ if (!navBarVisible && navBarLastShown != 0 && !cosmeticRefreshDone) {
+ if (millis() - navBarLastShown > 10000) { // 10s after hidden
+ EINK_ADD_FRAMEFLAG(display, COSMETIC); // One-time ghost cleanup
+ cosmeticRefreshDone = true;
+ }
+ }
+
+ navBarPrevVisible = navBarVisible;
#endif
// Pre-calculate bounding rect
@@ -1191,7 +1197,7 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
display->setColor(BLACK);
}
- if (useBigIcons) {
+ if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display);
} else {
display->drawXbm(x, y, iconSize, iconSize, icon);
diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h
index 21e4aef61..9e5e8c4b4 100644
--- a/src/graphics/draw/UIRenderer.h
+++ b/src/graphics/draw/UIRenderer.h
@@ -32,8 +32,6 @@ class UIRenderer
{
public:
// Common UI elements
- static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer,
- const meshtastic::PowerStatus *powerStatus);
static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus,
int node_offset = 0, bool show_total = true, String additional_words = "");
@@ -49,9 +47,6 @@ class UIRenderer
// Overlay and special screens
static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text);
- // Function overlay for showing mute/buzzer modifiers etc.
- static void drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
-
// Navigation bar overlay
static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state);
diff --git a/src/graphics/images.h b/src/graphics/images.h
index e9c2f00ea..c5865878a 100644
--- a/src/graphics/images.h
+++ b/src/graphics/images.h
@@ -23,9 +23,6 @@ const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03
0xf3, 0x3f, 0x33, 0x30, 0x33, 0x33, 0x33, 0x33, 0x03, 0x33, 0xff, 0x33,
0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f};
-// This image definition is here instead of images.h because it's modified dynamically by the drawBattery function
-static uint8_t imgBattery[16] = {0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0xE7, 0x3C};
-
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || ARCH_PORTDUINO) && \
!defined(DISPLAY_FORCE_SMALL_FONTS)
@@ -45,19 +42,15 @@ const uint8_t imgSF[] PROGMEM = {0xd2, 0xb7, 0xad, 0xbb, 0x92, 0x01, 0xfd, 0xfd,
// === Horizontal battery ===
// Basic battery design and all related pieces
-const unsigned char batteryBitmap_h[] PROGMEM = {
- 0b11111110, 0b00000000, 0b11110000, 0b00000111, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000,
- 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000,
- 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000,
- 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000,
- 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000,
- 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b11111110, 0b00000000, 0b11110000, 0b00000111};
+const unsigned char batteryBitmap_h_bottom[] PROGMEM = {
+ 0b00011110, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001,
+ 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
+ 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00011110, 0b00000000};
-// This is the left and right bars for the fill in
-const unsigned char batteryBitmap_sidegaps_h[] PROGMEM = {
- 0b11111111, 0b00001111, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
- 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
- 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b11111111, 0b00001111};
+const unsigned char batteryBitmap_h_top[] PROGMEM = {
+ 0b00111100, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000,
+ 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000,
+ 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b00111100, 0b00000000};
// Lightning Bolt
const unsigned char lightning_bolt_h[] PROGMEM = {
@@ -280,11 +273,16 @@ const uint8_t bluetoothdisabled[] PROGMEM = {0b11101100, 0b01010100, 0b01001100,
const uint8_t smallbulletpoint[] PROGMEM = {0b00000011, 0b00000011, 0b00000000, 0b00000000,
0b00000000, 0b00000000, 0b00000000, 0b00000000};
-// Clock
-#define icon_clock_width 8
-#define icon_clock_height 8
-const uint8_t icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101, 0b10101001,
- 0b10010001, 0b10000001, 0b01000010, 0b00111100};
+// Digital Clock
+#define digital_icon_clock_width 8
+#define digital_icon_clock_height 8
+const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101, 0b10101001,
+ 0b10010001, 0b10000001, 0b01000010, 0b00111100};
+// Analog Clock
+#define analog_icon_clock_width 8
+#define analog_icon_clock_height 8
+const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000,
+ 0b00100100, 0b01000010, 0b01000010, 0b11111111};
#include "img/icon.xbm"
static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning");
\ No newline at end of file
diff --git a/src/input/ButtonThread.cpp b/src/input/ButtonThread.cpp
index bc75e0a54..da9878fa4 100644
--- a/src/input/ButtonThread.cpp
+++ b/src/input/ButtonThread.cpp
@@ -27,28 +27,25 @@ ButtonThread::ButtonThread(const char *name) : OSThread(name)
_originName = name;
}
-bool ButtonThread::initButton(uint8_t pinNumber, bool activeLow, bool activePullup, uint32_t pullupSense, voidFuncPtr intRoutine,
- input_broker_event singlePress, input_broker_event longPress, uint16_t longPressTime,
- input_broker_event doublePress, input_broker_event longLongPress, uint16_t longLongPressTime,
- input_broker_event triplePress, input_broker_event shortLong, bool touchQuirk)
+bool ButtonThread::initButton(const ButtonConfig &config)
{
if (inputBroker)
inputBroker->registerSource(this);
- _longPressTime = longPressTime;
- _longLongPressTime = longLongPressTime;
- _pinNum = pinNumber;
- _activeLow = activeLow;
- _touchQuirk = touchQuirk;
- _intRoutine = intRoutine;
- _longLongPress = longLongPress;
+ _longPressTime = config.longPressTime;
+ _longLongPressTime = config.longLongPressTime;
+ _pinNum = config.pinNumber;
+ _activeLow = config.activeLow;
+ _touchQuirk = config.touchQuirk;
+ _intRoutine = config.intRoutine;
+ _longLongPress = config.longLongPress;
- userButton = OneButton(pinNumber, activeLow, activePullup);
+ userButton = OneButton(config.pinNumber, config.activeLow, config.activePullup);
- if (pullupSense != 0) {
- pinMode(pinNumber, pullupSense);
+ if (config.pullupSense != 0) {
+ pinMode(config.pinNumber, config.pullupSense);
}
- _singlePress = singlePress;
+ _singlePress = config.singlePress;
userButton.attachClick(
[](void *callerThread) -> void {
ButtonThread *thread = (ButtonThread *)callerThread;
@@ -56,8 +53,8 @@ bool ButtonThread::initButton(uint8_t pinNumber, bool activeLow, bool activePull
},
this);
- if (longPress != INPUT_BROKER_NONE) {
- _longPress = longPress;
+ if (config.longPress != INPUT_BROKER_NONE) {
+ _longPress = config.longPress;
userButton.attachLongPressStart(
[](void *callerThread) -> void {
ButtonThread *thread = (ButtonThread *)callerThread;
@@ -74,8 +71,8 @@ bool ButtonThread::initButton(uint8_t pinNumber, bool activeLow, bool activePull
this);
}
- if (doublePress != INPUT_BROKER_NONE) {
- _doublePress = doublePress;
+ if (config.doublePress != INPUT_BROKER_NONE) {
+ _doublePress = config.doublePress;
userButton.attachDoubleClick(
[](void *callerThread) -> void {
ButtonThread *thread = (ButtonThread *)callerThread;
@@ -84,8 +81,8 @@ bool ButtonThread::initButton(uint8_t pinNumber, bool activeLow, bool activePull
this);
}
- if (triplePress != INPUT_BROKER_NONE) {
- _triplePress = triplePress;
+ if (config.triplePress != INPUT_BROKER_NONE) {
+ _triplePress = config.triplePress;
userButton.attachMultiClick(
[](void *callerThread) -> void {
ButtonThread *thread = (ButtonThread *)callerThread;
@@ -94,8 +91,8 @@ bool ButtonThread::initButton(uint8_t pinNumber, bool activeLow, bool activePull
},
this);
}
- if (shortLong != INPUT_BROKER_NONE) {
- _shortLong = shortLong;
+ if (config.shortLong != INPUT_BROKER_NONE) {
+ _shortLong = config.shortLong;
}
userButton.setDebounceMs(1);
@@ -266,6 +263,11 @@ int32_t ButtonThread::runOnce()
break;
}
+
+ // doesn't handle BUTTON_EVENT_PRESSED_SCREEN BUTTON_EVENT_TOUCH_LONG_PRESSED BUTTON_EVENT_COMBO_SHORT_LONG
+ default: {
+ break;
+ }
}
}
btnEvent = BUTTON_EVENT_NONE;
diff --git a/src/input/ButtonThread.h b/src/input/ButtonThread.h
index 033f92b8b..949048de1 100644
--- a/src/input/ButtonThread.h
+++ b/src/input/ButtonThread.h
@@ -7,6 +7,26 @@
typedef void (*voidFuncPtr)(void);
+struct ButtonConfig {
+ uint8_t pinNumber;
+ bool activeLow = true;
+ bool activePullup = true;
+ uint32_t pullupSense = 0;
+ voidFuncPtr intRoutine = nullptr;
+ input_broker_event singlePress = INPUT_BROKER_NONE;
+ input_broker_event longPress = INPUT_BROKER_NONE;
+ uint16_t longPressTime = 500;
+ input_broker_event doublePress = INPUT_BROKER_NONE;
+ input_broker_event longLongPress = INPUT_BROKER_NONE;
+ uint16_t longLongPressTime = 5000;
+ input_broker_event triplePress = INPUT_BROKER_NONE;
+ input_broker_event shortLong = INPUT_BROKER_NONE;
+ bool touchQuirk = false;
+
+ // Constructor to set required parameter
+ ButtonConfig(uint8_t pin = 0) : pinNumber(pin) {}
+};
+
#ifndef BUTTON_CLICK_MS
#define BUTTON_CLICK_MS 250
#endif
@@ -28,12 +48,7 @@ class ButtonThread : public Observable, public concurrency::
public:
const char *_originName;
static const uint32_t c_holdOffTime = 30000; // hold off 30s after boot
- bool initButton(uint8_t pinNumber, bool activeLow, bool activePullup, uint32_t pullupSense, voidFuncPtr intRoutine,
- input_broker_event singlePress, input_broker_event longPress = INPUT_BROKER_NONE,
- uint16_t longPressTime = 500, input_broker_event doublePress = INPUT_BROKER_NONE,
- input_broker_event longLongPress = INPUT_BROKER_NONE, uint16_t longLongPressTime = 5000,
- input_broker_event triplePress = INPUT_BROKER_NONE, input_broker_event shortLong = INPUT_BROKER_NONE,
- bool touchQuirk = false);
+ bool initButton(const ButtonConfig &config);
enum ButtonEventType {
BUTTON_EVENT_NONE,
diff --git a/src/main.cpp b/src/main.cpp
index 2251241da..4b64a78ea 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -927,58 +927,81 @@ void setup()
LOG_DEBUG("Use GPIO%02d for button", settingsMap[userButtonPin]);
UserButtonThread = new ButtonThread("UserButton");
- if (screen)
- UserButtonThread->initButton(
- settingsMap[userButtonPin], true, true, INPUT_PULLUP, // pull up bias
- []() {
- UserButtonThread->userButton.tick();
- runASAP = true;
- BaseType_t higherWake = 0;
- mainDelay.interruptFromISR(&higherWake);
- },
- INPUT_BROKER_USER_PRESS, INPUT_BROKER_SELECT);
+ if (screen) {
+ ButtonConfig config;
+ config.pinNumber = (uint8_t)settingsMap[userButtonPin];
+ config.activeLow = true;
+ config.activePullup = true;
+ config.pullupSense = INPUT_PULLUP;
+ config.intRoutine = []() {
+ UserButtonThread->userButton.tick();
+ runASAP = true;
+ BaseType_t higherWake = 0;
+ mainDelay.interruptFromISR(&higherWake);
+ };
+ config.singlePress = INPUT_BROKER_USER_PRESS;
+ config.longPress = INPUT_BROKER_SELECT;
+ UserButtonThread->initButton(config);
+ }
}
#endif
#ifdef BUTTON_PIN_TOUCH
TouchButtonThread = new ButtonThread("BackButton");
- TouchButtonThread->initButton(
- BUTTON_PIN_TOUCH, true, true, pullup_sense,
- []() {
- TouchButtonThread->userButton.tick();
- runASAP = true;
- BaseType_t higherWake = 0;
- mainDelay.interruptFromISR(&higherWake);
- },
- INPUT_BROKER_NONE, INPUT_BROKER_BACK);
+ ButtonConfig touchConfig;
+ touchConfig.pinNumber = BUTTON_PIN_TOUCH;
+ touchConfig.activeLow = true;
+ touchConfig.activePullup = true;
+ touchConfig.pullupSense = pullup_sense;
+ touchConfig.intRoutine = []() {
+ TouchButtonThread->userButton.tick();
+ runASAP = true;
+ BaseType_t higherWake = 0;
+ mainDelay.interruptFromISR(&higherWake);
+ };
+ touchConfig.singlePress = INPUT_BROKER_NONE;
+ touchConfig.longPress = INPUT_BROKER_BACK;
+ TouchButtonThread->initButton(touchConfig);
#endif
#if defined(CANCEL_BUTTON_PIN)
// Buttons. Moved here cause we need NodeDB to be initialized
CancelButtonThread = new ButtonThread("CancelButton");
- CancelButtonThread->initButton(
- CANCEL_BUTTON_PIN, CANCEL_BUTTON_ACTIVE_LOW, CANCEL_BUTTON_ACTIVE_PULLUP, pullup_sense,
- []() {
- CancelButtonThread->userButton.tick();
- runASAP = true;
- BaseType_t higherWake = 0;
- mainDelay.interruptFromISR(&higherWake);
- },
- INPUT_BROKER_CANCEL, INPUT_BROKER_SHUTDOWN, 4000);
+ ButtonConfig cancelConfig;
+ cancelConfig.pinNumber = CANCEL_BUTTON_PIN;
+ cancelConfig.activeLow = CANCEL_BUTTON_ACTIVE_LOW;
+ cancelConfig.activePullup = CANCEL_BUTTON_ACTIVE_PULLUP;
+ cancelConfig.pullupSense = pullup_sense;
+ cancelConfig.intRoutine = []() {
+ CancelButtonThread->userButton.tick();
+ runASAP = true;
+ BaseType_t higherWake = 0;
+ mainDelay.interruptFromISR(&higherWake);
+ };
+ cancelConfig.singlePress = INPUT_BROKER_CANCEL;
+ cancelConfig.longPress = INPUT_BROKER_SHUTDOWN;
+ cancelConfig.longPressTime = 4000;
+ CancelButtonThread->initButton(cancelConfig);
#endif
#if defined(ALT_BUTTON_PIN)
// Buttons. Moved here cause we need NodeDB to be initialized
BackButtonThread = new ButtonThread("BackButton");
- BackButtonThread->initButton(
- ALT_BUTTON_PIN, ALT_BUTTON_ACTIVE_LOW, ALT_BUTTON_ACTIVE_PULLUP, pullup_sense,
- []() {
- BackButtonThread->userButton.tick();
- runASAP = true;
- BaseType_t higherWake = 0;
- mainDelay.interruptFromISR(&higherWake);
- },
- INPUT_BROKER_ALT_PRESS, INPUT_BROKER_ALT_LONG, 500);
+ ButtonConfig backConfig;
+ backConfig.pinNumber = ALT_BUTTON_PIN;
+ backConfig.activeLow = ALT_BUTTON_ACTIVE_LOW;
+ backConfig.activePullup = ALT_BUTTON_ACTIVE_PULLUP;
+ backConfig.pullupSense = pullup_sense;
+ backConfig.intRoutine = []() {
+ BackButtonThread->userButton.tick();
+ runASAP = true;
+ BaseType_t higherWake = 0;
+ mainDelay.interruptFromISR(&higherWake);
+ };
+ backConfig.singlePress = INPUT_BROKER_ALT_PRESS;
+ backConfig.longPress = INPUT_BROKER_ALT_LONG;
+ backConfig.longPressTime = 500;
+ BackButtonThread->initButton(backConfig);
#endif
#if defined(BUTTON_PIN)
@@ -997,27 +1020,42 @@ void setup()
// Buttons. Moved here cause we need NodeDB to be initialized
// If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP
UserButtonThread = new ButtonThread("UserButton");
- if (screen)
- UserButtonThread->initButton(
- _pinNum, BUTTON_ACTIVE_LOW, BUTTON_ACTIVE_PULLUP, pullup_sense,
- []() {
- UserButtonThread->userButton.tick();
- runASAP = true;
- BaseType_t higherWake = 0;
- mainDelay.interruptFromISR(&higherWake);
- },
- INPUT_BROKER_USER_PRESS, INPUT_BROKER_SELECT, 500, INPUT_BROKER_NONE, INPUT_BROKER_SHUTDOWN);
- else
- UserButtonThread->initButton(
- _pinNum, BUTTON_ACTIVE_LOW, BUTTON_ACTIVE_PULLUP, pullup_sense,
- []() {
- UserButtonThread->userButton.tick();
- runASAP = true;
- BaseType_t higherWake = 0;
- mainDelay.interruptFromISR(&higherWake);
- },
- INPUT_BROKER_USER_PRESS, INPUT_BROKER_SHUTDOWN, 5000, INPUT_BROKER_SEND_PING, INPUT_BROKER_NONE, 0,
- INPUT_BROKER_GPS_TOGGLE);
+ if (screen) {
+ ButtonConfig userConfig;
+ userConfig.pinNumber = (uint8_t)_pinNum;
+ userConfig.activeLow = BUTTON_ACTIVE_LOW;
+ userConfig.activePullup = BUTTON_ACTIVE_PULLUP;
+ userConfig.pullupSense = pullup_sense;
+ userConfig.intRoutine = []() {
+ UserButtonThread->userButton.tick();
+ runASAP = true;
+ BaseType_t higherWake = 0;
+ mainDelay.interruptFromISR(&higherWake);
+ };
+ userConfig.singlePress = INPUT_BROKER_USER_PRESS;
+ userConfig.longPress = INPUT_BROKER_SELECT;
+ userConfig.longPressTime = 500;
+ userConfig.longLongPress = INPUT_BROKER_SHUTDOWN;
+ UserButtonThread->initButton(userConfig);
+ } else {
+ ButtonConfig userConfigNoScreen;
+ userConfigNoScreen.pinNumber = (uint8_t)_pinNum;
+ userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW;
+ userConfigNoScreen.activePullup = BUTTON_ACTIVE_PULLUP;
+ userConfigNoScreen.pullupSense = pullup_sense;
+ userConfigNoScreen.intRoutine = []() {
+ UserButtonThread->userButton.tick();
+ runASAP = true;
+ BaseType_t higherWake = 0;
+ mainDelay.interruptFromISR(&higherWake);
+ };
+ userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS;
+ userConfigNoScreen.longPress = INPUT_BROKER_SHUTDOWN;
+ userConfigNoScreen.longPressTime = 5000;
+ userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING;
+ userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE;
+ UserButtonThread->initButton(userConfigNoScreen);
+ }
#endif
#endif
diff --git a/src/mesh/MeshModule.cpp b/src/mesh/MeshModule.cpp
index 62d3c82bc..c5748a560 100644
--- a/src/mesh/MeshModule.cpp
+++ b/src/mesh/MeshModule.cpp
@@ -244,10 +244,13 @@ void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to)
p->decoded.request_id = to.id;
}
-std::vector MeshModule::GetMeshModulesWithUIFrames()
+std::vector MeshModule::GetMeshModulesWithUIFrames(int startIndex)
{
-
std::vector modulesWithUIFrames;
+
+ // Fill with nullptr up to startIndex
+ modulesWithUIFrames.resize(startIndex, nullptr);
+
if (modules) {
for (auto i = modules->begin(); i != modules->end(); ++i) {
auto &pi = **i;
diff --git a/src/mesh/MeshModule.h b/src/mesh/MeshModule.h
index f08b8f49c..eda3f8881 100644
--- a/src/mesh/MeshModule.h
+++ b/src/mesh/MeshModule.h
@@ -75,7 +75,7 @@ class MeshModule
*/
static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO);
- static std::vector GetMeshModulesWithUIFrames();
+ static std::vector GetMeshModulesWithUIFrames(int startIndex);
static void observeUIEvents(Observer *observer);
static AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp,
meshtastic_AdminMessage *request,
diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp
index 3eb3a5173..9433cc75d 100644
--- a/src/mesh/NodeDB.cpp
+++ b/src/mesh/NodeDB.cpp
@@ -1582,6 +1582,7 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
// Mark the node's key as manually verified to indicate trustworthiness.
updateGUIforNode = info;
// powerFSM.trigger(EVENT_NODEDB_UPDATED); This event has been retired
+ sortMeshDB();
notifyObservers(true); // Force an update whether or not our node counts have changed
}
saveNodeDatabaseToDisk();
@@ -1685,6 +1686,31 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
info->has_hops_away = true;
info->hops_away = mp.hop_start - mp.hop_limit;
}
+ sortMeshDB();
+ }
+}
+
+void NodeDB::sortMeshDB()
+{
+ if (!Throttle::isWithinTimespanMs(lastSort, 1000 * 5)) {
+ lastSort = millis();
+ std::sort(meshNodes->begin(), meshNodes->end(), [](const meshtastic_NodeInfoLite &a, const meshtastic_NodeInfoLite &b) {
+ if (a.num == myNodeInfo.my_node_num) {
+ return true;
+ }
+ if (b.num == myNodeInfo.my_node_num) {
+ return false;
+ }
+ bool aFav = a.is_favorite;
+ bool bFav = b.is_favorite;
+ if (aFav != bFav)
+ return aFav;
+ if (a.last_heard == 0 || a.last_heard == UINT32_MAX)
+ return false;
+ if (b.last_heard == 0 || b.last_heard == UINT32_MAX)
+ return true;
+ return a.last_heard > b.last_heard;
+ });
}
}
diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h
index 90ca5aefd..b6e4d600b 100644
--- a/src/mesh/NodeDB.h
+++ b/src/mesh/NodeDB.h
@@ -282,6 +282,7 @@ class NodeDB
bool duplicateWarned = false;
uint32_t lastNodeDbSave = 0; // when we last saved our db to flash
uint32_t lastBackupAttempt = 0; // when we last tried a backup automatically or manually
+ uint32_t lastSort = 0; // When last sorted the nodeDB
/// Find a node in our DB, create an empty NodeInfoLite if missing
meshtastic_NodeInfoLite *getOrCreateMeshNode(NodeNum n);
@@ -310,6 +311,7 @@ class NodeDB
bool saveChannelsToDisk();
bool saveDeviceStateToDisk();
bool saveNodeDatabaseToDisk();
+ void sortMeshDB();
};
extern NodeDB *nodeDB;
diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp
index b24f3ca00..4d8d6ce4b 100644
--- a/src/modules/CannedMessageModule.cpp
+++ b/src/modules/CannedMessageModule.cpp
@@ -154,7 +154,7 @@ int CannedMessageModule::splitConfiguredMessages()
}
void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer)
{
- if (display->getWidth() > 128) {
+ if (graphics::isHighResolution) {
if (this->dest == NODENUM_BROADCAST) {
display->drawStringf(x, y, buffer, "To: Broadcast@%s", channels.getName(this->channel));
} else {
@@ -245,12 +245,15 @@ void CannedMessageModule::updateDestinationSelectionList()
}
}
+ /* As the nodeDB is sorted, can skip this step
// Sort by favorite, then last heard
std::sort(this->filteredNodes.begin(), this->filteredNodes.end(), [](const NodeEntry &a, const NodeEntry &b) {
if (a.node->is_favorite != b.node->is_favorite)
return a.node->is_favorite > b.node->is_favorite;
return a.lastHeard < b.lastHeard;
});
+ */
+
scrollIndex = 0; // Show first result at the top
destIndex = 0; // Highlight the first entry
if (nodesChanged && runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) {
@@ -387,6 +390,7 @@ bool CannedMessageModule::handleTabSwitch(const InputEvent *event)
// RESTORE THIS!
if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION)
updateDestinationSelectionList();
+ requestFocus();
UIFrameEvent e;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
@@ -986,6 +990,7 @@ int32_t CannedMessageModule::runOnce()
default:
// Only insert ASCII printable characters (32–126)
if (this->payload >= 32 && this->payload <= 126) {
+ requestFocus();
if (this->cursor == this->freetext.length()) {
this->freetext += (char)this->payload;
} else {
diff --git a/src/modules/KeyVerificationModule.cpp b/src/modules/KeyVerificationModule.cpp
index f5a9f2359..c0972c155 100644
--- a/src/modules/KeyVerificationModule.cpp
+++ b/src/modules/KeyVerificationModule.cpp
@@ -79,10 +79,10 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
memset(message, 0, sizeof(message));
sprintf(message, "Verification: \n");
generateVerificationCode(message + 15);
- sprintf(message + 24, "\nACCEPT\nREJECT");
+ static const char *optionsArray[] = {"ACCEPT", "REJECT"};
LOG_INFO("Hash1 matches!");
if (screen) {
- screen->showOverlayBanner(message, 30000, 2, [=](int selected) {
+ screen->showOverlayBanner(message, 30000, optionsArray, 2, [=](int selected) {
if (selected == 0) {
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
diff --git a/src/modules/SystemCommandsModule.cpp b/src/modules/SystemCommandsModule.cpp
index a6b01d68a..6a7da95af 100644
--- a/src/modules/SystemCommandsModule.cpp
+++ b/src/modules/SystemCommandsModule.cpp
@@ -100,9 +100,9 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
case INPUT_BROKER_SEND_PING:
service->refreshLocalMeshNode();
if (service->trySendPosition(NODENUM_BROADCAST, true)) {
- IF_SCREEN(screen->showOverlayBanner("Position\nUpdate Sent", 3000));
+ IF_SCREEN(screen->showOverlayBanner("Position\nSent", 3000));
} else {
- IF_SCREEN(screen->showOverlayBanner("Node Info\nUpdate Sent", 3000));
+ IF_SCREEN(screen->showOverlayBanner("Node Info\nSent", 3000));
}
return true;
// Power control
@@ -113,6 +113,10 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000;
// runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
return true;
+
+ default:
+ // No other input events handled here
+ break;
}
return false;
}
\ No newline at end of file
diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp
index 375d1e596..46a24a816 100644
--- a/src/modules/Telemetry/EnvironmentTelemetry.cpp
+++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp
@@ -30,7 +30,7 @@
namespace graphics
{
-extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr);
+extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only);
}
#if __has_include()
#include "Sensor/AHT10.h"
@@ -358,7 +358,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
int line = 1;
// === Set Title
- const char *titleStr = (SCREEN_WIDTH > 128) ? "Environment" : "Env.";
+ const char *titleStr = (graphics::isHighResolution) ? "Environment" : "Env.";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr);
diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp
index df1505226..a92013d01 100644
--- a/src/modules/Telemetry/PowerTelemetry.cpp
+++ b/src/modules/Telemetry/PowerTelemetry.cpp
@@ -24,7 +24,7 @@
namespace graphics
{
-extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr);
+extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only);
}
int32_t PowerTelemetryModule::runOnce()
@@ -115,7 +115,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s
int line = 1;
// === Set Title
- const char *titleStr = (SCREEN_WIDTH > 128) ? "Power Telem." : "Power";
+ const char *titleStr = (graphics::isHighResolution) ? "Power Telem." : "Power";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr);
diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp
index 578e7183a..cab668406 100644
--- a/src/modules/WaypointModule.cpp
+++ b/src/modules/WaypointModule.cpp
@@ -137,10 +137,14 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) {
const meshtastic_PositionLite &op = ourNode->position;
float myHeading;
- if (screen->hasHeading())
- myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians
- else
- myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
+ if (screen->ignoreCompass) {
+ myHeading = 0;
+ } else {
+ if (screen->hasHeading())
+ myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians
+ else
+ myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
+ }
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, (compassDiam / 2));
// Compass bearing to waypoint
@@ -148,7 +152,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(wp.latitude_i), DegD(wp.longitude_i));
// If the top of the compass is a static north then bearingToOther can be drawn on the compass directly
// If the top of the compass is not a static north we need adjust bearingToOther based on heading
- if (!config.display.compass_north_top)
+ if (!screen->ignoreCompass)
bearingToOther -= myHeading;
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther);
diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp
index fc8531298..29a9b6840 100644
--- a/src/serialization/MeshPacketSerializer.cpp
+++ b/src/serialization/MeshPacketSerializer.cpp
@@ -79,7 +79,8 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
msgPayload["relative_humidity"] = new JSONValue(decoded->variant.environment_metrics.relative_humidity);
}
if (decoded->variant.environment_metrics.has_barometric_pressure) {
- msgPayload["barometric_pressure"] = new JSONValue(decoded->variant.environment_metrics.barometric_pressure);
+ msgPayload["barometric_pressure"] =
+ new JSONValue(decoded->variant.environment_metrics.barometric_pressure);
}
if (decoded->variant.environment_metrics.has_gas_resistance) {
msgPayload["gas_resistance"] = new JSONValue(decoded->variant.environment_metrics.gas_resistance);
@@ -125,13 +126,16 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard);
}
if (decoded->variant.air_quality_metrics.has_pm10_environmental) {
- msgPayload["pm10_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental);
+ msgPayload["pm10_e"] =
+ new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental);
}
if (decoded->variant.air_quality_metrics.has_pm25_environmental) {
- msgPayload["pm25_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental);
+ msgPayload["pm25_e"] =
+ new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental);
}
if (decoded->variant.air_quality_metrics.has_pm100_environmental) {
- msgPayload["pm100_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental);
+ msgPayload["pm100_e"] =
+ new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental);
}
} else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
if (decoded->variant.power_metrics.has_ch1_voltage) {
diff --git a/variants/portduino/platformio.ini b/variants/portduino/platformio.ini
index 6da827508..5293b12b9 100644
--- a/variants/portduino/platformio.ini
+++ b/variants/portduino/platformio.ini
@@ -22,7 +22,6 @@ lib_deps =
${native_base.lib_deps}
${device-ui_base.lib_deps}
build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunction-sections -fdata-sections -Wl,--gc-sections
- -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1
-D RAM_SIZE=16384
-D USE_X11=1
-D HAS_TFT=1
@@ -51,7 +50,6 @@ lib_deps =
${device-ui_base.lib_deps}
board_level = extra
build_flags = ${native_base.build_flags} -Os -ffunction-sections -fdata-sections -Wl,--gc-sections
- -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1
-D RAM_SIZE=8192
-D USE_FRAMEBUFFER=1
-D LV_COLOR_DEPTH=32
@@ -81,7 +79,6 @@ lib_deps =
${device-ui_base.lib_deps}
board_level = extra
build_flags = ${native_base.build_flags} -O0 -fsanitize=address -lX11 -linput -lxkbcommon
- -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1
-D DEBUG_HEAP
-D RAM_SIZE=16384
-D USE_X11=1
diff --git a/variants/rak4631/variant.h b/variants/rak4631/variant.h
index cd8f46153..f5ec11ef2 100644
--- a/variants/rak4631/variant.h
+++ b/variants/rak4631/variant.h
@@ -88,8 +88,8 @@ static const uint8_t A7 = PIN_A7;
#define ADC_RESOLUTION 14
// Other pins
- #define WB_I2C1_SDA (13) // SENSOR_SLOT IO_SLOT
- #define WB_I2C1_SCL (14) // SENSOR_SLOT IO_SLOT
+#define WB_I2C1_SDA (13) // SENSOR_SLOT IO_SLOT
+#define WB_I2C1_SCL (14) // SENSOR_SLOT IO_SLOT
#define PIN_AREF (2)
#define PIN_NFC1 (9)