Compare commits

...

96 Commits

Author SHA1 Message Date
Jonathan Bennett
550c0796eb More progress 2025-06-26 13:03:52 -05:00
Jonathan Bennett
93d101d11a Initial work on splitting notification renderer into components for reuse 2025-06-26 01:09:31 -05:00
Jonathan Bennett
fc2fd5ebff Canned Messages tweak 2025-06-26 00:32:29 -05:00
Jason P
7265b5e6c6 Continue effort of moving modules in the navigation 2025-06-25 23:50:00 -05:00
Jason P
bf5c9f6263 Second attempt to move modules down the navigation bar 2025-06-25 23:30:16 -05:00
HarukiToreda
1736db6b56 Add Scrollbar for Action menus 2025-06-26 00:03:06 -04:00
HarukiToreda
3a5dc870e0 Putting Modules back to position 0 and some trunk checks found 2025-06-25 23:45:51 -04:00
Jonathan Bennett
11d307c609 Move function after include 2025-06-25 20:42:47 -05:00
Jason P
c090a7f6d5 Merge branch 'master' into 2.7-MiscFixes-Week1 2025-06-25 20:25:24 -05:00
Jason P
c8bfb61c8d Merge pull request #7137 from meshtastic/sort-nodes
Make NodeDB sort its internal vector when lastheard is updated. Don't…
2025-06-25 20:24:57 -05:00
Jonathan Bennett
dbc67973c6 Eliminate some now-unneeded sorting 2025-06-25 19:44:31 -05:00
Jonathan Bennett
3dd77ace85 Check more carefully for own node 2025-06-25 19:43:50 -05:00
Jonathan Bennett
e1b1e35a27 Throttle sorting just a touch 2025-06-25 19:43:36 -05:00
Jonathan Bennett
18098fb1cb Pass by reference -- Thanks Copilot! 2025-06-25 18:08:24 -05:00
Jonathan Bennett
b1e3353ceb Update src/mesh/NodeDB.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-25 18:07:02 -05:00
Jonathan Bennett
8fb1e0f874 Update src/graphics/draw/NodeListRenderer.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-25 18:06:52 -05:00
Jonathan Bennett
667ff17fdb Make NodeDB sort its internal vector when lastheard is updated. Don't sort in NodeListRenderer 2025-06-25 17:20:36 -05:00
Jonathan Bennett
42c1967e7b Revert "Actually honor the points-north setting"
This reverts commit 20988aa4fa.
2025-06-25 17:06:48 -05:00
Jason P
94258cfd1c Adjust text location for pop-ups 2025-06-25 16:32:36 -05:00
Jason P
7c297eff8f Reorder Timezones to match expectations 2025-06-25 16:13:00 -05:00
Jason P
c178396e20 Unify Message Titles 2025-06-25 16:02:10 -05:00
Jason P
caf4c3919c Finalize Autosized Action menu per screen 2025-06-25 15:40:27 -05:00
HarukiToreda
7d09bd981a Autosized Action menu per screen 2025-06-25 15:34:19 -04:00
Jason P
f046c1a68a Merge branch 'master' into 2.7-MiscFixes-Week1 2025-06-25 13:56:14 -05:00
Jason P
61f81ac758 Correct T_Watch_S3 specific line 2025-06-25 12:04:59 -05:00
Jonathan Bennett
9446f07c4d trunk 2025-06-25 11:28:45 -05:00
Jonathan Bennett
94904cb6a7 Update no-op showOverlayBanner function 2025-06-25 11:28:19 -05:00
HarukiToreda
646b370411 Compressed action list 2025-06-25 12:24:27 -04:00
Jonathan Bennett
b6bcee18b5 Trunk 2025-06-25 11:20:43 -05:00
Jonathan Bennett
20988aa4fa Actually honor the points-north setting 2025-06-25 11:17:04 -05:00
HarukiToreda
cab6707ca0 Retired drawFunctionOverlay code
No longer being used
2025-06-25 10:44:39 -04:00
Jason P
46ac9841d6 Finalize Time Format picker word change 2025-06-25 09:12:31 -05:00
Jason P
88ab198e0f Use all the rows on EInk since with autohide the navigation bar 2025-06-25 09:10:04 -05:00
Jason P
0c948a3fc0 Time Format wording 2025-06-25 08:25:41 -05:00
Jason P
17456d0618 Merge branch 'master' into 2.7-MiscFixes-Week1 2025-06-25 07:54:07 -05:00
Jason P
a395448170 Menu wording adjustments 2025-06-25 07:52:26 -05:00
Jason P
e6ba326876 Update message caching to correct aged timestamp 2025-06-25 07:11:21 -05:00
Jason P
a6cc4ab3fe Trunk runs 2025-06-24 22:57:27 -05:00
Jason P
d411fd99f0 Update comments for Screen.cpp related to module load change 2025-06-24 22:50:39 -05:00
Jason P
819f5a2fde Resolve dangling pointer issues with sanitize code 2025-06-24 22:33:27 -05:00
Jason P
ca34fe9a90 Small adjustments to AM/PM replacement across various devices 2025-06-24 21:59:55 -05:00
Jason P
137e7183c7 Merge pull request #7124 from csrutil/2.7-MiscFixes-Week1-message-cache
feat(bug): cache the lines and heights to reduce the overhead
2025-06-24 21:37:17 -05:00
Jason P
54fa39b2e9 Add AM/PM to low resolution clock also 2025-06-24 21:14:29 -05:00
csrutil
eca240373a cleanup, cheers 2025-06-25 09:08:30 +08:00
csrutil
0b1703a51a addressed the conflicts, and changed target branch to 2.7-MiscFixes-Week1 2025-06-25 08:53:03 +08:00
Jason P
653f6c2a85 Move modules beyond the clock in navigation 2025-06-24 17:24:20 -05:00
Jason P
7a285cf221 Maintain clock frame when switching between Clock Faces 2025-06-24 14:46:40 -05:00
Jason P
cea5cd171a Adjust drawBluetoothConnectedIcon on TWatch 2025-06-24 14:13:28 -05:00
HarukiToreda
c5e3bc841e Suppress action screen Full refresh for Eink 2025-06-24 14:52:43 -04:00
Jason P
ca7d2d7482 Add Toggle Backlight for EInk Displays 2025-06-24 13:42:28 -05:00
Jason P
7af31a88c0 Establish Action Menu on all node list screens, add NodeDB reset (with confirmation) option 2025-06-24 13:13:37 -05:00
Jason P
9f53df4f2e Update Analog Clock on EInk to show more digits 2025-06-24 11:06:10 -05:00
Thomas Göttgens
485fc7639e fix misc build warnings. NFC 2025-06-24 18:01:04 +02:00
Jason P
34f3800e2b Remove old battery icon and option, use drawCommonHeader throughout, re-add battery to Clock frames 2025-06-24 10:46:25 -05:00
Jason P
a3ed75c5c9 Hide quick toggle as option is available within Action Menu, commented out for the moment 2025-06-24 09:48:06 -05:00
Jason P
088143dbf3 Add Adhoc Ping and resolve error with std::string sanitized 2025-06-24 08:14:38 -05:00
Jason P
fecf80c39b Updated working for 12-/24-hour menu and Added US/Arizona to timezone picker 2025-06-24 07:00:07 -05:00
Jason P
7ef8067b87 Merge branch 'master' into 2.7-MiscFixes-Week1 2025-06-24 06:46:40 -05:00
HarukiToreda
9de5d170bf Pause Banner for Eink moved to bottom 2025-06-24 02:14:01 -04:00
HarukiToreda
3d28086f68 Improvements to Eink Navigation 2025-06-24 01:37:10 -04:00
Jason P
232d601b14 Fix Clock menu option decision tree 2025-06-23 23:50:14 -05:00
Jason P
36ee2cfda0 Remove Second Hand for Analog Clock on EInk displays 2025-06-23 23:43:22 -05:00
Jason P
56c1ba037a Gotta keep height and width in expected order 2025-06-23 23:15:20 -05:00
Jason P
ae9c062dc9 Resolved apostrophe being shown as upside down question mark 2025-06-23 22:32:39 -05:00
Jason P
6c5b947ad5 Slightly better sanitizeString variation 2025-06-23 16:21:32 -05:00
Jason P
f9bf7a1010 Sanitize long_names and removed unused variables 2025-06-23 15:58:43 -05:00
Jason P
c35610b04d Fix emoji bounce, overlap, and missing commonHeader 2025-06-23 15:16:28 -05:00
Jason P
0df1d49220 Merge branch 'master' into 2.7-MiscFixes-Week1 2025-06-23 15:02:07 -05:00
Jason P
0ba3170dfe Merge branch '2.7-MiscFixes-Week1' of https://github.com/meshtastic/firmware into 2.7-MiscFixes-Week1 2025-06-23 15:00:38 -05:00
Jason P
94b9684981 Correct Home menu into typical format 2025-06-23 15:00:31 -05:00
Jonathan Bennett
e0918ea448 Minor fix for compass point menu 2025-06-23 14:56:28 -05:00
Jonathan Bennett
4c0517c6f2 Add compass menu and needle point option 2025-06-23 14:50:17 -05:00
Jonathan Bennett
07cd16d2df Migrate the rest of the menus to MenuHandler.* 2025-06-23 13:16:47 -05:00
Jonathan Bennett
a33672db4f Clock picker fixes 2025-06-23 12:56:47 -05:00
Jason P
6088ab49eb Correct Clock Face Picker title 2025-06-23 11:46:55 -05:00
Jason P
7f8acf5658 Plumb in the digital/analog picker 2025-06-23 11:41:34 -05:00
Jonathan Bennett
99176a8388 Move menu bits to MenuHandler 2025-06-23 10:00:51 -05:00
Jason P
30e0972de5 Correct pop-up calculation size and continue to leverage isHighResolution 2025-06-23 09:32:45 -05:00
Jason P
6bd600a878 Fix Action Menu on Home frame 2025-06-23 07:56:23 -05:00
Jonathan Bennett
2f31ee5b6e More menu banners into functions 2025-06-23 01:02:41 -05:00
Jonathan Bennett
6a91741209 Fix prompt string for 12/24 hour picker 2025-06-23 00:29:32 -05:00
Jonathan Bennett
b55e763b29 Simple Menu Queue, and add time menu 2025-06-23 00:26:43 -05:00
Jason P
60acba877e Add AM/PM to Analog Clock if isHighResolution and not TWatch 2025-06-22 22:59:22 -05:00
Jason P
221988c665 Analog Clock for all 2025-06-22 22:26:20 -05:00
Jonathan Bennett
850d957931 Merge branch 'master' into 2.7-MiscFixes-Week1 2025-06-22 21:25:24 -05:00
Jason P
83248ce0d0 Merge branch 'master' into 2.7-MiscFixes-Week1 2025-06-22 20:17:47 -05:00
Jason P
bdc1df9f5c Line Spacing bound to isHighResolution 2025-06-22 20:17:33 -05:00
Jason P
2de08bebdc Implement isHighResolution in place of SCREEN_WIDTH > 128 checks 2025-06-22 19:54:47 -05:00
Jonathan Bennett
d3e56ea084 Fixup determineResolution() 2025-06-22 19:37:35 -05:00
Jason P
2f37204df2 Beginnings of creating isHighResolution bool 2025-06-22 18:56:07 -05:00
Jason P
791377b76b Update positioning on Message frame and fix drawCommonHeader overlay 2025-06-22 15:25:56 -05:00
Jason P
53d28f3a3a Merge branch 'master' into 2.7-MiscFixes-Week1 2025-06-22 14:46:53 -05:00
Ben Meadors
574cbe55c0 Merge branch 'master' into 2.7-MiscFixes-Week1 2025-06-22 05:53:35 -05:00
Jason P
f11b49863d Merge branch 'master' into 2.7-MiscFixes-Week1 2025-06-21 23:02:54 -05:00
Jason P
5ca5ee2846 Rebuild Horizontal Battery, Resolve overlap concerns 2025-06-21 22:51:50 -05:00
Jason P
766189212c Update Favorite Node Message Options to unify against other screens 2025-06-21 20:42:20 -05:00
32 changed files with 1266 additions and 728 deletions

View File

@@ -31,6 +31,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "TimeFormatters.h" #include "TimeFormatters.h"
#include "draw/ClockRenderer.h" #include "draw/ClockRenderer.h"
#include "draw/DebugRenderer.h" #include "draw/DebugRenderer.h"
#include "draw/MenuHandler.h"
#include "draw/MessageRenderer.h" #include "draw/MessageRenderer.h"
#include "draw/NodeListRenderer.h" #include "draw/NodeListRenderer.h"
#include "draw/NotificationRenderer.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 // 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 // Called to trigger a banner with custom message and duration
void Screen::showOverlayBanner(const char *message, uint32_t durationMs, uint8_t options, std::function<void(int)> bannerCallback, void Screen::showOverlayBanner(const char *message, uint32_t durationMs, const char **optionsArrayPtr, uint8_t options,
int8_t InitialSelected) std::function<void(int)> 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 // Store the message and set the expiration timestamp
strncpy(NotificationRenderer::alertBannerMessage, message, 255); strncpy(NotificationRenderer::alertBannerMessage, message, 255);
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
NotificationRenderer::optionsArrayPtr = optionsArrayPtr;
NotificationRenderer::alertBannerOptions = options; NotificationRenderer::alertBannerOptions = options;
NotificationRenderer::alertBannerCallback = bannerCallback; NotificationRenderer::alertBannerCallback = bannerCallback;
NotificationRenderer::curSelected = InitialSelected; NotificationRenderer::curSelected = InitialSelected;
@@ -152,6 +157,24 @@ void Screen::showOverlayBanner(const char *message, uint32_t durationMs, uint8_t
ui->update(); ui->update();
} }
// Called to trigger a banner with custom message and duration
void Screen::showNodePicker(const char *message, uint32_t durationMs, std::function<void(int)> bannerCallback)
{
#ifdef USE_EINK
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
#endif
// Store the message and set the expiration timestamp
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
NotificationRenderer::alertBannerCallback = bannerCallback;
NotificationRenderer::pauseBanner = false;
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawAlertBannerOverlay};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
setFastFramerate(); // Draw ASAP
ui->update();
}
static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
uint8_t module_frame; uint8_t module_frame;
@@ -413,8 +436,7 @@ void Screen::setup()
// === Set custom overlay callbacks === // === Set custom overlay callbacks ===
static OverlayCallback overlays[] = { 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])); ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
@@ -471,6 +493,7 @@ void Screen::setup()
// === Turn on display and trigger first draw === // === Turn on display and trigger first draw ===
handleSetOn(true); handleSetOn(true);
determineResolution(dispdev->height(), dispdev->width());
ui->update(); ui->update();
#ifndef USE_EINK #ifndef USE_EINK
ui->update(); // Some SSD1306 clones drop the first draw, so run twice ui->update(); // Some SSD1306 clones drop the first draw, so run twice
@@ -557,6 +580,7 @@ int32_t Screen::runOnce()
if (displayHeight == 0) { if (displayHeight == 0) {
displayHeight = dispdev->getHeight(); displayHeight = dispdev->getHeight();
} }
menuHandler::handleMenuSwitch();
// Show boot screen for first logo_timeout seconds, then switch to normal operation. // Show boot screen for first logo_timeout seconds, then switch to normal operation.
// serialSinceMsec adjusts for additional serial wait time during nRF52 bootup // serialSinceMsec adjusts for additional serial wait time during nRF52 bootup
@@ -585,7 +609,7 @@ int32_t Screen::runOnce()
#ifndef DISABLE_WELCOME_UNSET #ifndef DISABLE_WELCOME_UNSET
if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
LoraRegionPicker(0); menuHandler::LoraRegionPicker(0);
} }
#endif #endif
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) { if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) {
@@ -768,32 +792,6 @@ void Screen::setFrames(FrameFocus focus)
indicatorIcons.clear(); indicatorIcons.clear();
size_t numframes = 0; 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 // If we have a critical fault, show it first
fsi.positions.fault = numframes; fsi.positions.fault = numframes;
@@ -807,7 +805,7 @@ void Screen::setFrames(FrameFocus focus)
fsi.positions.clock = numframes; fsi.positions.clock = numframes;
normalFrames[numframes++] = graphics::ClockRenderer::digitalWatchFace ? graphics::ClockRenderer::drawDigitalClockFrame normalFrames[numframes++] = graphics::ClockRenderer::digitalWatchFace ? graphics::ClockRenderer::drawDigitalClockFrame
: &graphics::ClockRenderer::drawAnalogClockFrame; : &graphics::ClockRenderer::drawAnalogClockFrame;
indicatorIcons.push_back(icon_clock); indicatorIcons.push_back(digital_icon_clock);
#endif #endif
// Declare this early so its available in FOCUS_PRESERVE block // Declare this early so its available in FOCUS_PRESERVE block
@@ -822,22 +820,27 @@ void Screen::setFrames(FrameFocus focus)
indicatorIcons.push_back(icon_mail); indicatorIcons.push_back(icon_mail);
#ifndef USE_EINK #ifndef USE_EINK
fsi.positions.nodelist = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen; normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen;
indicatorIcons.push_back(icon_nodes); indicatorIcons.push_back(icon_nodes);
#endif #endif
// Show detailed node views only on E-Ink builds // Show detailed node views only on E-Ink builds
#ifdef USE_EINK #ifdef USE_EINK
fsi.positions.nodelist_lastheard = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen;
indicatorIcons.push_back(icon_nodes); indicatorIcons.push_back(icon_nodes);
fsi.positions.nodelist_hopsignal = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen; normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen;
indicatorIcons.push_back(icon_signal); indicatorIcons.push_back(icon_signal);
fsi.positions.nodelist_distance = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen;
indicatorIcons.push_back(icon_distance); indicatorIcons.push_back(icon_distance);
#endif #endif
#if HAS_GPS #if HAS_GPS
fsi.positions.nodelist_bearings = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
indicatorIcons.push_back(icon_list); indicatorIcons.push_back(icon_list);
@@ -857,8 +860,9 @@ void Screen::setFrames(FrameFocus focus)
} }
#if !defined(DISPLAY_CLOCK_FRAME) #if !defined(DISPLAY_CLOCK_FRAME)
fsi.positions.clock = numframes; fsi.positions.clock = numframes;
normalFrames[numframes++] = graphics::ClockRenderer::drawDigitalClockFrame; normalFrames[numframes++] = graphics::ClockRenderer::digitalWatchFace ? graphics::ClockRenderer::drawDigitalClockFrame
indicatorIcons.push_back(icon_clock); : graphics::ClockRenderer::drawAnalogClockFrame;
indicatorIcons.push_back(digital_icon_clock);
#endif #endif
// We don't show the node info of our node (if we have it yet - we should) // We don't show the node info of our node (if we have it yet - we should)
@@ -885,6 +889,36 @@ void Screen::setFrames(FrameFocus focus)
} }
#endif #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 fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE
this->frameCount = numframes; // ✅ Save frame count for use in custom overlay this->frameCount = numframes; // ✅ Save frame count for use in custom overlay
LOG_DEBUG("Finished build frames. numframes: %d", numframes); LOG_DEBUG("Finished build frames. numframes: %d", numframes);
@@ -916,6 +950,11 @@ void Screen::setFrames(FrameFocus focus)
// If no module requested focus, will show the first frame instead // If no module requested focus, will show the first frame instead
ui->switchToFrame(fsi.positions.focusedModule); ui->switchToFrame(fsi.positions.focusedModule);
break; 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: case FOCUS_PRESERVE:
// No more adjustment — force stay on same index // No more adjustment — force stay on same index
@@ -1204,6 +1243,8 @@ int Screen::handleInputEvent(const InputEvent *event)
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
setFastFramerate(); // Draw ASAP setFastFramerate(); // Draw ASAP
ui->update(); ui->update();
menuHandler::handleMenuSwitch();
return 0; return 0;
} }
/* /*
@@ -1229,7 +1270,7 @@ int Screen::handleInputEvent(const InputEvent *event)
// Ask any MeshModules if they're handling keyboard input right now // Ask any MeshModules if they're handling keyboard input right now
bool inputIntercepted = false; bool inputIntercepted = false;
for (MeshModule *module : moduleFrames) { for (MeshModule *module : moduleFrames) {
if (module->interceptingKeyboardInput()) if (module && module->interceptingKeyboardInput())
inputIntercepted = true; inputIntercepted = true;
} }
@@ -1241,129 +1282,36 @@ int Screen::handleInputEvent(const InputEvent *event)
showNextFrame(); showNextFrame();
} else if (event->inputEvent == INPUT_BROKER_SELECT) { } else if (event->inputEvent == INPUT_BROKER_SELECT) {
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
const char *banner_message; menuHandler::homeBaseMenu();
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);
}
});
#if HAS_TFT #if HAS_TFT
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) { } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) {
showOverlayBanner("Switch to MUI?\nYes\nNo", 30000, 2, [](int selected) -> void { menuHandler::switchToMUIMenu();
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);
}
});
#else #else
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) { } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) {
showOverlayBanner( menuHandler::BuzzerModeMenu();
"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);
#endif #endif
#if HAS_GPS #if HAS_GPS
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) { } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) {
showOverlayBanner( menuHandler::positionBaseMenu();
"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
#endif #endif
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) { } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) {
TZPicker(); menuHandler::clockMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) { } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
LoraRegionPicker(); menuHandler::LoraRegionPicker();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage && } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage &&
devicestate.rx_text_message.from) { devicestate.rx_text_message.from) {
const char *banner_message; menuHandler::messageResponseMenu();
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<const char *>(mp.decoded.payload.bytes);
audioThread->readAloud(msg);
}
#endif
});
} else if (framesetInfo.positions.firstFavorite != 255 && } else if (framesetInfo.positions.firstFavorite != 255 &&
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) { this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
const char *banner_message; menuHandler::favoriteBaseMenu();
int options; } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist ||
if (kb_found) { this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard ||
banner_message = "Message Node?\nCancel\nNew Preset Msg\nNew Freetext Msg"; this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
options = 3; this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance ||
} else { this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
banner_message = "Message Node?\nCancel\nConfirm"; this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) {
options = 2; menuHandler::nodeListMenu();
}
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);
}
});
} }
} else if (event->inputEvent == INPUT_BROKER_BACK) { } else if (event->inputEvent == INPUT_BROKER_BACK) {
showPrevFrame(); showPrevFrame();
@@ -1397,96 +1345,6 @@ bool Screen::isOverlayBannerShowing()
return NotificationRenderer::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 } // namespace graphics
#else #else

View File

@@ -24,6 +24,7 @@ class Screen
FOCUS_FAULT, FOCUS_FAULT,
FOCUS_TEXTMESSAGE, FOCUS_TEXTMESSAGE,
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus 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); explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
@@ -38,8 +39,8 @@ class Screen
void setFunctionSymbol(std::string) {} void setFunctionSymbol(std::string) {}
void removeFunctionSymbol(std::string) {} void removeFunctionSymbol(std::string) {}
void startAlert(const char *) {} void startAlert(const char *) {}
void showOverlayBanner(const char *message, uint32_t durationMs = 3000, uint8_t options = 0, void showOverlayBanner(const char *message, uint32_t durationMs = 3000, const char **optionsArrayPtr = nullptr,
std::function<void(int)> bannerCallback = NULL, int8_t InitialSelected = 0) uint8_t options = 0, std::function<void(int)> bannerCallback = NULL, int8_t InitialSelected = 0)
{ {
} }
void setFrames(FrameFocus focus) {} void setFrames(FrameFocus focus) {}
@@ -209,6 +210,7 @@ class Screen : public concurrency::OSThread
FOCUS_FAULT, FOCUS_FAULT,
FOCUS_TEXTMESSAGE, FOCUS_TEXTMESSAGE,
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus 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 // Regenerate the normal set of frames, focusing a specific frame if requested
@@ -286,8 +288,10 @@ class Screen : public concurrency::OSThread
enqueueCmd(cmd); enqueueCmd(cmd);
} }
void showOverlayBanner(const char *message, uint32_t durationMs = 3000, uint8_t options = 0, void showOverlayBanner(const char *message, uint32_t durationMs = 3000, const char **optionsArrayPtr = nullptr,
std::function<void(int)> bannerCallback = NULL, int8_t InitialSelected = 0); uint8_t options = 0, std::function<void(int)> bannerCallback = NULL, int8_t InitialSelected = 0);
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(int)> bannerCallback);
void startFirmwareUpdateScreen() void startFirmwareUpdateScreen()
{ {
@@ -602,8 +606,6 @@ class Screen : public concurrency::OSThread
void handleShowNextFrame(); void handleShowNextFrame();
void handleShowPrevFrame(); void handleShowPrevFrame();
void handleStartFirmwareUpdateScreen(); void handleStartFirmwareUpdateScreen();
void TZPicker();
void LoraRegionPicker(uint32_t duration = 30000);
// Info collected by setFrames method. // Info collected by setFrames method.
// Index location of specific frames. // Index location of specific frames.
@@ -612,7 +614,6 @@ class Screen : public concurrency::OSThread
struct FramesetInfo { struct FramesetInfo {
struct FramePositions { struct FramePositions {
uint8_t fault = 255; uint8_t fault = 255;
uint8_t textMessage = 255;
uint8_t waypoint = 255; uint8_t waypoint = 255;
uint8_t focusedModule = 255; uint8_t focusedModule = 255;
uint8_t log = 255; uint8_t log = 255;
@@ -622,6 +623,12 @@ class Screen : public concurrency::OSThread
uint8_t memory = 255; uint8_t memory = 255;
uint8_t gps = 255; uint8_t gps = 255;
uint8_t home = 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 clock = 255;
uint8_t firstFavorite = 255; uint8_t firstFavorite = 255;
uint8_t lastFavorite = 255; uint8_t lastFavorite = 255;
@@ -679,5 +686,6 @@ class Screen : public concurrency::OSThread
// Extern declarations for function symbols used in UIRenderer // Extern declarations for function symbols used in UIRenderer
extern std::vector<std::string> functionSymbol; extern std::vector<std::string> functionSymbol;
extern std::string functionSymbolString; extern std::string functionSymbolString;
extern graphics::Screen *screen;
#endif #endif

View File

@@ -10,9 +10,22 @@
namespace graphics 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 === // === Shared External State ===
bool hasUnreadMessage = false; bool hasUnreadMessage = false;
bool isMuted = false; bool isMuted = false;
bool isHighResolution = false;
// === Internal State === // === Internal State ===
bool isBoltVisibleShared = true; bool isBoltVisibleShared = true;
@@ -40,7 +53,7 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w,
// ************************* // *************************
// * Common Header Drawing * // * 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; constexpr int HEADER_OFFSET_Y = 1;
y += HEADER_OFFSET_Y; 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 screenW = display->getWidth();
const int screenH = display->getHeight(); const int screenH = display->getHeight();
const bool useBigIcons = (screenW > 128); if (!battery_only) {
// === Inverted Header Background ===
// === Inverted Header Background === if (isInverted) {
if (isInverted) { display->setColor(BLACK);
drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2); display->fillRect(0, 0, screenW, highlightHeight + 2);
display->setColor(BLACK); display->setColor(WHITE);
} else { drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
display->setColor(BLACK); display->setColor(BLACK);
display->fillRect(0, 0, screenW, highlightHeight + 3);
display->setColor(WHITE);
if (screenW > 128) {
display->drawLine(0, 20, screenW, 20);
} else { } 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 === // === Screen Title ===
display->setTextAlignment(TEXT_ALIGN_CENTER); display->setTextAlignment(TEXT_ALIGN_CENTER);
display->drawString(SCREEN_WIDTH / 2, y, titleStr); display->drawString(SCREEN_WIDTH / 2, y, titleStr);
if (config.display.heading_bold) { if (config.display.heading_bold) {
display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr); display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr);
}
} }
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
// === Battery State === // === Battery State ===
int chargePercent = powerStatus->getBatteryChargePercent(); int chargePercent = powerStatus->getBatteryChargePercent();
bool isCharging = powerStatus->getIsCharging() == meshtastic::OptionalBool::OptTrue; bool isCharging = powerStatus->getIsCharging() == meshtastic::OptionalBool::OptTrue;
if (chargePercent == 100) {
isCharging = false;
}
uint32_t now = millis(); uint32_t now = millis();
#ifndef USE_EINK #ifndef USE_EINK
@@ -93,20 +112,22 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
} }
#endif #endif
bool useHorizontalBattery = (screenW > 128 && screenW >= screenH); bool useHorizontalBattery = (isHighResolution && screenW >= screenH);
const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
// === Battery Icons === // === Battery Icons ===
if (useHorizontalBattery) { if (useHorizontalBattery) {
int batteryX = 2; int batteryX = 2;
int batteryY = HEADER_OFFSET_Y + 2; int batteryY = HEADER_OFFSET_Y + 3;
display->drawXbm(batteryX, batteryY, 29, 15, batteryBitmap_h); display->drawXbm(batteryX, batteryY, 9, 13, batteryBitmap_h_bottom);
display->drawXbm(batteryX + 9, batteryY, 9, 13, batteryBitmap_h_top);
if (isCharging && isBoltVisibleShared) 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 { else {
display->drawXbm(batteryX + 8, batteryY, 12, 15, batteryBitmap_sidegaps_h); display->drawLine(batteryX + 5, batteryY, batteryX + 10, batteryY);
int fillWidth = 24 * chargePercent / 100; display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12);
display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 13); int fillWidth = 14 * chargePercent / 100;
display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11);
} }
} else { } else {
int batteryX = 1; int batteryX = 1;
@@ -129,12 +150,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
char chargeStr[4]; char chargeStr[4];
snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent); snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent);
int chargeNumWidth = display->getStringWidth(chargeStr); int chargeNumWidth = display->getStringWidth(chargeStr);
const int batteryOffset = useHorizontalBattery ? 28 : 6; const int batteryOffset = useHorizontalBattery ? 19 : 9;
#ifdef USE_EINK const int percentX = x + batteryOffset;
const int percentX = x + xOffset + batteryOffset - 2;
#else
const int percentX = x + xOffset + batteryOffset;
#endif
display->drawString(percentX, textY, chargeStr); display->drawString(percentX, textY, chargeStr);
display->drawString(percentX + chargeNumWidth - 1, textY, "%"); display->drawString(percentX + chargeNumWidth - 1, textY, "%");
if (isBold) { 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 timeStrWidth = display->getStringWidth("12:34"); // Default alignment
int timeX = screenW - xOffset - timeStrWidth + 4; int timeX = screenW - xOffset - timeStrWidth + 4;
if (rtc_sec > 0) { if (rtc_sec > 0 && !battery_only) {
// === Build Time String === // === Build Time String ===
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
int hour = hms / SEC_PER_HOUR; 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); timeStrWidth = display->getStringWidth(timeStr);
timeX = screenW - xOffset - timeStrWidth + 4; timeX = screenW - xOffset - timeStrWidth + 3;
// === Show Mail or Mute Icon to the Left of Time === // === Show Mail or Mute Icon to the Left of Time ===
int iconRightEdge = timeX - 1; 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); display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
} }
} else if (isMuted) { } else if (isMuted) {
if (useBigIcons) { if (isHighResolution) {
int iconX = iconRightEdge - mute_symbol_big_width; int iconX = iconRightEdge - mute_symbol_big_width;
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
@@ -281,7 +298,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
display->drawXbm(iconX, iconY, mail_width, mail_height, mail); display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
} }
} else if (isMuted) { } else if (isMuted) {
if (useBigIcons) { if (isHighResolution) {
int iconX = iconRightEdge - mute_symbol_big_width; int iconX = iconRightEdge - mute_symbol_big_width;
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; 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); display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big);
@@ -300,7 +317,7 @@ const int *getTextPositions(OLEDDisplay *display)
{ {
static int textPositions[7]; // Static array that persists beyond function scope static int textPositions[7]; // Static array that persists beyond function scope
if (display->getHeight() > 64) { if (isHighResolution) {
textPositions[0] = textZeroLine; textPositions[0] = textZeroLine;
textPositions[1] = textFirstLine_medium; textPositions[1] = textFirstLine_medium;
textPositions[2] = textSecondLine_medium; textPositions[2] = textSecondLine_medium;

View File

@@ -41,12 +41,14 @@ namespace graphics
// Shared state (declare inside namespace) // Shared state (declare inside namespace)
extern bool hasUnreadMessage; extern bool hasUnreadMessage;
extern bool isMuted; extern bool isMuted;
extern bool isHighResolution;
void determineResolution(int16_t screenheight, int16_t screenwidth);
// Rounded highlight (used for inverted headers) // 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); void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r);
// Shared battery/time/mail header // 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); const int *getTextPositions(OLEDDisplay *display);

View File

@@ -21,6 +21,7 @@ namespace graphics
namespace ClockRenderer namespace ClockRenderer
{ {
bool digitalWatchFace = true;
void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale) 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); 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) void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale)
{ {
uint16_t segmentWidth = SEGMENT_WIDTH * 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); drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
} }
} }
*/
// Draw a digital clock // Draw a digital clock
void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
display->clear(); display->clear();
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
int line = 1; int line = 1;
// === Set Title, Blank for Clock
const char *titleStr = "";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr, true);
#ifdef T_WATCH_S3 #ifdef T_WATCH_S3
if (nimbleBluetooth && nimbleBluetooth->isConnected()) { 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 #endif
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone 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; float scale = 1.5;
#else #else
float scale = 0.75; float scale = 0.75;
if (SCREEN_WIDTH > 128) { if (isHighResolution) {
scale = 1.5; scale = 1.5;
} }
#endif #endif
@@ -276,17 +279,17 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
// draw seconds string // draw seconds string
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
int xOffset = (SCREEN_WIDTH > 128) ? 0 : -1; int xOffset = (isHighResolution) ? 0 : -1;
if (hour >= 10) { 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) { if (config.display.use_12h_clock) {
display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2, display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2,
isPM ? "pm" : "am"); isPM ? "pm" : "am");
} }
#ifndef USE_EINK #ifndef USE_EINK
xOffset = (SCREEN_WIDTH > 128) ? 18 : 10; xOffset = (isHighResolution) ? 18 : 10;
display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset, display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset,
secondString); secondString);
#endif #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) void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
display->setTextAlignment(TEXT_ALIGN_LEFT); 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 #ifdef T_WATCH_S3
if (nimbleBluetooth && nimbleBluetooth->isConnected()) { if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2); drawBluetoothConnectedIcon(display, display->getWidth() - 18, display->getHeight() - 14);
} }
#endif #endif
drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36,
graphics::ClockRenderer::digitalWatchFace, 1);
// clock face center coordinates // clock face center coordinates
int16_t centerX = display->getWidth() / 2; int16_t centerX = display->getWidth() / 2;
int16_t centerY = display->getHeight() / 2; int16_t centerY = display->getHeight() / 2;
// clock face radius // 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) // noon (0 deg) coordinates (outermost circle)
int16_t noonX = centerX; int16_t noonX = centerX;
@@ -338,10 +340,16 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
int16_t tickMarkOuterNoonY = secondHandNoonY; int16_t tickMarkOuterNoonY = secondHandNoonY;
// seconds tick mark inner y coordinate; (second nested circle) // 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) // 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 // minute hand y coordinate
int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4; int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4;
@@ -350,7 +358,10 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
int16_t hourStringNoonY = minuteHandNoonY + 18; int16_t hourStringNoonY = minuteHandNoonY + 18;
// hour hand radius and y coordinate // 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; int16_t hourHandNoonY = centerY - hourHandRadius;
display->setColor(OLEDDISPLAY_COLOR::WHITE); 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 minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % 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 degreesPerHour = 30;
int16_t degreesPerMinuteOrSecond = 6; int16_t degreesPerMinuteOrSecond = 6;
@@ -443,16 +467,32 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
double hourStringX = (sineAngleInRadians * (hourStringNoonY - centerY) + noonX) - hourStringXOffset; double hourStringX = (sineAngleInRadians * (hourStringNoonY - centerY) + noonX) - hourStringXOffset;
double hourStringY = (cosineAngleInRadians * (hourStringNoonY - centerY) + centerY) - hourStringYOffset; double hourStringY = (cosineAngleInRadians * (hourStringNoonY - centerY) + centerY) - hourStringYOffset;
#ifdef T_WATCH_S3
// draw hour number // draw hour number
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); 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) { if (angle % degreesPerMinuteOrSecond == 0) {
double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX; double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX;
double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY; double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY;
// draw minute tick mark if (isHighResolution) {
display->drawLine(startX, startY, endX, endY); // draw minute tick mark
display->drawLine(startX, startY, endX, endY);
}
} }
} }
@@ -461,9 +501,10 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// draw minute hand // draw minute hand
display->drawLine(centerX, centerY, minuteX, minuteY); display->drawLine(centerX, centerY, minuteX, minuteY);
#ifndef USE_EINK
// draw second hand // draw second hand
display->drawLine(centerX, centerY, secondX, secondY); display->drawLine(centerX, centerY, secondX, secondY);
#endif
} }
} }

View File

@@ -12,7 +12,7 @@ class Screen;
namespace ClockRenderer namespace ClockRenderer
{ {
// Whether we are showing the digital watch face or the analog one // Whether we are showing the digital watch face or the analog one
static bool digitalWatchFace = true; extern bool digitalWatchFace;
// Clock frame functions // Clock frame functions
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); 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); void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height);
// UI elements for clock displays // 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); void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y);
} // namespace ClockRenderer } // namespace ClockRenderer

View File

@@ -4,6 +4,7 @@
#include "configuration.h" #include "configuration.h"
#include "gps/GeoCoord.h" #include "gps/GeoCoord.h"
#include "graphics/ScreenFonts.h" #include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include <cmath> #include <cmath>
namespace graphics namespace graphics
@@ -45,7 +46,7 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY,
// This could draw a "N" indicator or north arrow // This could draw a "N" indicator or north arrow
// For now, we'll draw a simple north indicator // For now, we'll draw a simple north indicator
// const float radius = 17.0f; // const float radius = 17.0f;
if (display->width() > 128) { if (isHighResolution) {
radius += 4; radius += 4;
} }
Point north(0, -radius); Point north(0, -radius);
@@ -55,7 +56,7 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY,
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_CENTER); display->setTextAlignment(TEXT_ALIGN_CENTER);
display->setColor(BLACK); 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); display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6);
} else { } else {
display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6); display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6);

View File

@@ -67,21 +67,6 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
char channelStr[20]; char channelStr[20];
snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); 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 // Display nodes status
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus); 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; int line = 1;
// === Set Title // === Set Title
const char *titleStr = (SCREEN_WIDTH > 128) ? "LoRa Info" : "LoRa"; const char *titleStr = (isHighResolution) ? "LoRa Info" : "LoRa";
// === Header === // === Header ===
graphics::drawCommonHeader(display, x, y, titleStr); graphics::drawCommonHeader(display, x, y, titleStr);
@@ -444,12 +429,12 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
char chUtilPercentage[10]; char chUtilPercentage[10];
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); 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_y = getTextPositions(display)[line] + 3;
int chutil_bar_width = (SCREEN_WIDTH > 128) ? 100 : 50; int chutil_bar_width = (isHighResolution) ? 100 : 50;
int chutil_bar_height = (SCREEN_WIDTH > 128) ? 12 : 7; int chutil_bar_height = (isHighResolution) ? 12 : 7;
int extraoffset = (SCREEN_WIDTH > 128) ? 6 : 3; int extraoffset = (isHighResolution) ? 6 : 3;
int chutil_percent = airTime->channelUtilizationPercent(); int chutil_percent = airTime->channelUtilizationPercent();
int centerofscreen = SCREEN_WIDTH / 2; int centerofscreen = SCREEN_WIDTH / 2;
@@ -516,7 +501,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
int line = 1; int line = 1;
const int barHeight = 6; const int barHeight = 6;
const int labelX = x; const int labelX = x;
const int barsOffset = (SCREEN_WIDTH > 128) ? 24 : 0; const int barsOffset = (isHighResolution) ? 24 : 0;
const int barX = x + 40 + barsOffset; const int barX = x + 40 + barsOffset;
auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { 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; int percent = (used * 100) / total;
char combinedStr[24]; char combinedStr[24];
if (SCREEN_WIDTH > 128) { if (isHighResolution) {
snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024, snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024,
total / 1024); total / 1024);
} else { } else {

View File

@@ -0,0 +1,457 @@
#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<const char *>(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) {
service->refreshLocalMeshNode();
if (service->trySendPosition(NODENUM_BROADCAST, true)) {
LOG_INFO("Position Sent");
} else {
LOG_INFO("Node Info Sent");
}
} 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()
{
static const char *optionsArray[] = {"Back", "GPS Toggle", "Compass Point"};
screen->showOverlayBanner("Position Action", 30000, optionsArray, 3, [](int selected) -> void {
if (selected == 1) {
menuQueue = gps_toggle_menu;
} else if (selected == 2) {
menuQueue = compass_point_north_menu;
}
});
}
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", "Points Up", "Dynamic"};
screen->showOverlayBanner("Needle Direction?", 30000, optionsArray, 3, [](int selected) -> void {
if (selected == 1 && config.display.compass_north_top != true) {
config.display.compass_north_top = true;
service->reloadConfig(SEGMENT_CONFIG);
screen->setFrames();
} else if (selected == 2 && config.display.compass_north_top != false) {
config.display.compass_north_top = false;
service->reloadConfig(SEGMENT_CONFIG);
screen->setFrames();
} 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

View File

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

View File

@@ -56,6 +56,11 @@ namespace graphics
namespace MessageRenderer namespace MessageRenderer
{ {
// Simple cache based on text hash
static size_t cachedKey = 0;
static std::vector<std::string> cachedLines;
static std::vector<int> cachedHeights;
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount) void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount)
{ {
int cursorX = x; int cursorX = x;
@@ -225,6 +230,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
sender); sender);
} }
uint32_t now = millis();
#ifndef EXCLUDE_EMOJI #ifndef EXCLUDE_EMOJI
// === Bounce animation setup === // === Bounce animation setup ===
static uint32_t lastBounceTime = 0; 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 bounceRange = 2; // Max pixels to bounce up/down
const int bounceInterval = 10; // How quickly to change bounce direction (ms) const int bounceInterval = 10; // How quickly to change bounce direction (ms)
uint32_t now = millis();
if (now - lastBounceTime >= bounceInterval) { if (now - lastBounceTime >= bounceInterval) {
lastBounceTime = now; lastBounceTime = now;
bounceY = (bounceY + 1) % (bounceRange * 2); bounceY = (bounceY + 1) % (bounceRange * 2);
@@ -246,82 +251,51 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
display->drawString(x + 4, headerY, headerStr); display->drawString(x + 4, headerY, headerStr);
// Draw separator (same as scroll version) // Draw separator (same as scroll version)
for (int separatorX = 0; separatorX <= (display->getStringWidth(headerStr) + 3); separatorX += 2) { for (int separatorX = 1; separatorX <= (display->getStringWidth(headerStr) + 2); separatorX += 2) {
display->setPixel(separatorX, headerY + ((SCREEN_WIDTH > 128) ? 19 : 13)); display->setPixel(separatorX, headerY + ((isHighResolution) ? 19 : 13));
} }
// Center the emote below the header line + separator + nav // Center the emote below the header line + separator + nav
int remainingHeight = SCREEN_HEIGHT - (headerY + FONT_HEIGHT_SMALL) - navHeight; 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); 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; return;
} }
} }
#endif #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 === if (cachedKey != currentKey) {
std::vector<std::string> lines; LOG_INFO("Message cache key is misssed cachedKey=0x%0x, currentKey=0x%x", cachedKey, currentKey);
lines.push_back(std::string(headerStr)); // Header line is always first
std::string line, word; // Cache miss - regenerate lines and heights
for (int i = 0; messageBuf[i]; ++i) { cachedLines = generateLines(display, headerStr, messageBuf, textWidth);
char ch = messageBuf[i]; cachedHeights = calculateLineHeights(cachedLines, emotes);
if (ch == '\n') { cachedKey = currentKey;
if (!word.empty()) } else {
line += word; // Cache hit but update the header line with current time information
if (!line.empty()) cachedLines[0] = std::string(headerStr);
lines.push_back(line); // The header always has a fixed height since it doesn't contain emotes
line.clear(); // As per calculateLineHeights logic for lines without emotes:
word.clear(); cachedHeights[0] = FONT_HEIGHT_SMALL - 2;
} else if (ch == ' ') { if (cachedHeights[0] < 8)
line += word + ' '; cachedHeights[0] = 8; // minimum safety
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();
}
}
} }
if (!word.empty())
line += word;
if (!line.empty())
lines.push_back(line);
// === Scrolling logic === // === Scrolling logic ===
std::vector<int> 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; int totalHeight = 0;
for (size_t i = 1; i < rowHeights.size(); ++i) { for (size_t i = 1; i < cachedHeights.size(); ++i) {
totalHeight += rowHeights[i]; totalHeight += cachedHeights[i];
} }
int usableScrollHeight = usableHeight - rowHeights[0]; // remove header height int usableScrollHeight = usableHeight - cachedHeights[0]; // remove header height
int scrollStop = std::max(0, totalHeight - usableScrollHeight + rowHeights.back()); int scrollStop = std::max(0, totalHeight - usableScrollHeight + cachedHeights.back());
static float scrollY = 0.0f; static float scrollY = 0.0f;
static uint32_t lastTime = 0, scrollStartDelay = 0, pauseStart = 0; static uint32_t lastTime = 0, scrollStartDelay = 0, pauseStart = 0;
@@ -363,28 +337,109 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
int scrollOffset = static_cast<int>(scrollY); int scrollOffset = static_cast<int>(scrollY);
int yOffset = -scrollOffset + getTextPositions(display)[1]; int yOffset = -scrollOffset + getTextPositions(display)[1];
for (int separatorX = 0; separatorX <= (display->getStringWidth(headerStr) + 3); separatorX += 2) { for (int separatorX = 1; separatorX <= (display->getStringWidth(headerStr) + 2); separatorX += 2) {
display->setPixel(separatorX, yOffset + ((SCREEN_WIDTH > 128) ? 19 : 13)); display->setPixel(separatorX, yOffset + ((isHighResolution) ? 19 : 13));
} }
// === Render visible lines === // === 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<std::string> generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth)
{
std::vector<std::string> 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<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes)
{
std::vector<int> 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<std::string> &lines, const std::vector<int> &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) { for (size_t i = 0; i < lines.size(); ++i) {
int lineY = yOffset; int lineY = yOffset;
for (size_t j = 0; j < i; ++j) for (size_t j = 0; j < i; ++j)
lineY += rowHeights[j]; lineY += rowHeights[j];
if (lineY > -rowHeights[i] && lineY < scrollBottom) { if (lineY > -rowHeights[i] && lineY < scrollBottom) {
if (i == 0 && isInverted) { if (i == 0 && isInverted) {
display->drawString(x + 3, lineY, lines[i].c_str()); display->drawString(x, lineY, lines[i].c_str());
if (isBold) if (isBold)
display->drawString(x + 4, lineY, lines[i].c_str()); display->drawString(x, lineY, lines[i].c_str());
} else { } else {
drawStringWithEmotes(display, x, lineY, lines[i], emotes, numEmotes); 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 } // namespace MessageRenderer

View File

@@ -2,6 +2,8 @@
#include "OLEDDisplay.h" #include "OLEDDisplay.h"
#include "OLEDDisplayUi.h" #include "OLEDDisplayUi.h"
#include "graphics/emotes.h" #include "graphics/emotes.h"
#include <string>
#include <vector>
namespace graphics 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 /// Draws the text message frame for displaying received messages
void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
// Function to generate lines with word wrapping
std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth);
// Function to calculate heights for each line
std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes);
// Function to render the message content
void renderMessageContent(OLEDDisplay *display, const std::vector<std::string> &lines, const std::vector<int> &rowHeights, int x,
int yOffset, int scrollBottom, const Emote *emotes, int numEmotes, bool isInverted, bool isBold);
} // namespace MessageRenderer } // namespace MessageRenderer
} // namespace graphics } // namespace graphics

View File

@@ -80,7 +80,11 @@ const char *getCurrentModeTitle(int screenWidth)
case MODE_LAST_HEARD: case MODE_LAST_HEARD:
return "Last Heard"; return "Last Heard";
case MODE_HOP_SIGNAL: 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: case MODE_DISTANCE:
return "Distance"; return "Distance";
default: default:
@@ -109,35 +113,6 @@ int calculateMaxScroll(int totalEntries, int visibleRows)
return std::max(0, (totalEntries - 1) / (visibleRows * 2)); return std::max(0, (totalEntries - 1) / (visibleRows * 2));
} }
void retrieveAndSortNodes(std::vector<NodeEntry> &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) void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd)
{ {
int columnWidth = display->getWidth() / 2; int columnWidth = display->getWidth() / 2;
@@ -170,7 +145,7 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
{ {
bool isLeftCol = (x < SCREEN_WIDTH / 2); 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); const char *nodeName = getSafeNodeName(node);
@@ -191,9 +166,9 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); 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 (node->is_favorite) {
if (SCREEN_WIDTH > 128) { if (isHighResolution) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else { } else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
@@ -212,8 +187,8 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
bool isLeftCol = (x < SCREEN_WIDTH / 2); bool isLeftCol = (x < SCREEN_WIDTH / 2);
int nameMaxWidth = columnWidth - 25; int nameMaxWidth = columnWidth - 25;
int barsOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19); int barsOffset = (isHighResolution) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
int hopOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17); int hopOffset = (isHighResolution) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
int barsXOffset = columnWidth - barsOffset; int barsXOffset = columnWidth - barsOffset;
@@ -222,9 +197,9 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); 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 (node->is_favorite) {
if (SCREEN_WIDTH > 128) { if (isHighResolution) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else { } else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
@@ -259,7 +234,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
{ {
bool isLeftCol = (x < SCREEN_WIDTH / 2); 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); const char *nodeName = getSafeNodeName(node);
char distStr[10] = ""; char distStr[10] = "";
@@ -314,9 +289,9 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); 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 (node->is_favorite) {
if (SCREEN_WIDTH > 128) { if (isHighResolution) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else { } else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
@@ -324,8 +299,8 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
} }
if (strlen(distStr) > 0) { if (strlen(distStr) > 0) {
int offset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) // Offset for Wide 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) : (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column)
int rightEdge = x + columnWidth - offset; int rightEdge = x + columnWidth - offset;
int textWidth = display->getStringWidth(distStr); int textWidth = display->getStringWidth(distStr);
display->drawString(rightEdge - textWidth, y, distStr); display->drawString(rightEdge - textWidth, y, distStr);
@@ -354,15 +329,15 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
bool isLeftCol = (x < SCREEN_WIDTH / 2); bool isLeftCol = (x < SCREEN_WIDTH / 2);
// Adjust max text width depending on column and screen width // 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); const char *nodeName = getSafeNodeName(node);
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); 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 (node->is_favorite) {
if (SCREEN_WIDTH > 128) { if (isHighResolution) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else { } else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
@@ -377,7 +352,7 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
return; return;
bool isLeftCol = (x < SCREEN_WIDTH / 2); 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 centerX = x + columnWidth - arrowXOffset;
int centerY = y + FONT_HEIGHT_SMALL / 2; int centerY = y + FONT_HEIGHT_SMALL / 2;
@@ -436,19 +411,16 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
// Space below header // Space below header
y += COMMON_HEADER_HEIGHT; y += COMMON_HEADER_HEIGHT;
// Fetch and display sorted node list int totalEntries = nodeDB->getNumMeshNodes();
std::vector<NodeEntry> nodeList;
retrieveAndSortNodes(nodeList);
int totalEntries = nodeList.size();
int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; int totalRowsAvailable = (display->getHeight() - y) / rowYOffset;
#ifdef USE_EINK
totalRowsAvailable -= 1;
#endif
int visibleNodeRows = totalRowsAvailable; int visibleNodeRows = totalRowsAvailable;
int totalColumns = 2; int totalColumns = 2;
int startIndex = scrollIndex * visibleNodeRows * totalColumns; 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 endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries);
int yOffset = 0; int yOffset = 0;
@@ -460,10 +432,10 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
for (int i = startIndex; i < endIndex; ++i) { for (int i = startIndex; i < endIndex; ++i) {
int xPos = x + (col * columnWidth); int xPos = x + (col * columnWidth);
int yPos = y + yOffset; int yPos = y + yOffset;
renderer(display, nodeList[i].node, xPos, yPos, columnWidth); renderer(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth);
if (extras) { 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); lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
@@ -533,7 +505,12 @@ void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) 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"; const char *title = "Hops/Signal";
#endif
drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal); drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal);
} }

View File

@@ -23,12 +23,6 @@ namespace NodeListRenderer
typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); 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); 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 // Node list mode enumeration
enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 }; 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 // Utility functions
const char *getCurrentModeTitle(int screenWidth); const char *getCurrentModeTitle(int screenWidth);
void retrieveAndSortNodes(std::vector<NodeEntry> &nodeList);
const char *getSafeNodeName(meshtastic_NodeInfoLite *node); const char *getSafeNodeName(meshtastic_NodeInfoLite *node);
void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields);

View File

@@ -31,6 +31,7 @@ int8_t NotificationRenderer::curSelected = 0;
char NotificationRenderer::alertBannerMessage[256] = {0}; char NotificationRenderer::alertBannerMessage[256] = {0};
uint32_t NotificationRenderer::alertBannerUntil = 0; // 0 is a special case meaning forever uint32_t NotificationRenderer::alertBannerUntil = 0; // 0 is a special case meaning forever
uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are seelctable options uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are seelctable options
const char **NotificationRenderer::optionsArrayPtr = nullptr;
std::function<void(int)> NotificationRenderer::alertBannerCallback = NULL; std::function<void(int)> NotificationRenderer::alertBannerCallback = NULL;
bool NotificationRenderer::pauseBanner = false; bool NotificationRenderer::pauseBanner = false;
@@ -56,60 +57,51 @@ void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiStat
void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state)
{ {
// Exit if no message is active or duration has passed if (!isOverlayBannerShowing() || pauseBanner)
if (!isOverlayBannerShowing())
return;
if (pauseBanner)
return; return;
// === Layout Configuration === // === Layout Configuration ===
constexpr uint16_t padding = 5; // Padding around text inside the box constexpr uint16_t vPadding = 2;
constexpr uint16_t vPadding = 2; // Padding around text inside the box
constexpr uint8_t lineSpacing = 1; // Extra space between lines
// 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 // Setup font and alignment
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT); // We will manually center per line display->setTextAlignment(TEXT_ALIGN_LEFT);
const int MAX_LINES = 24;
uint16_t optionWidths[alertBannerOptions] = {0};
uint16_t maxWidth = 0; uint16_t maxWidth = 0;
uint16_t arrowsWidth = display->getStringWidth("> <", 4, true); uint16_t arrowsWidth = display->getStringWidth("> <", 4, true);
uint16_t lineWidths[MAX_LINES] = {0}; uint16_t lineWidths[MAX_LINES] = {0};
uint16_t lineLengths[MAX_LINES] = {0}; uint16_t lineLengths[MAX_LINES] = {0};
char *lineStarts[MAX_LINES + 1]; const char *lineStarts[MAX_LINES + 1] = {0};
uint16_t lineCount = 0; uint16_t lineCount = 0;
char lineBuffer[40] = {0}; char lineBuffer[40] = {0};
// pointer to the terminating null
// Parse lines
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
lineStarts[lineCount] = alertBannerMessage; lineStarts[lineCount] = alertBannerMessage;
// loop through lines finding \n characters
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
lineStarts[lineCount + 1] = std::find(lineStarts[lineCount], alertEnd, '\n'); lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount]; lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount];
if (lineStarts[lineCount + 1][0] == '\n') { if (lineStarts[lineCount + 1][0] == '\n')
lineStarts[lineCount + 1] += 1; // Move the start pointer beyond the \n lineStarts[lineCount + 1] += 1;
}
lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true); lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true);
if (lineWidths[lineCount] > maxWidth) { if (lineWidths[lineCount] > maxWidth)
maxWidth = lineWidths[lineCount]; maxWidth = lineWidths[lineCount];
}
if (alertBannerOptions > 0 && lineCount > 0 && lineWidths[lineCount] + arrowsWidth > maxWidth) {
maxWidth = lineWidths[lineCount] + arrowsWidth;
}
lineCount++; 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) { if (alertBannerOptions > 0) {
// respond to input
if (inEvent == INPUT_BROKER_UP || inEvent == INPUT_BROKER_ALT_PRESS) { if (inEvent == INPUT_BROKER_UP || inEvent == INPUT_BROKER_ALT_PRESS) {
curSelected--; curSelected--;
} else if (inEvent == INPUT_BROKER_DOWN || inEvent == INPUT_BROKER_USER_PRESS) { } else if (inEvent == INPUT_BROKER_DOWN || inEvent == INPUT_BROKER_USER_PRESS) {
@@ -120,113 +112,176 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
} else if ((inEvent == INPUT_BROKER_CANCEL || inEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) { } else if ((inEvent == INPUT_BROKER_CANCEL || inEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) {
alertBannerMessage[0] = '\0'; alertBannerMessage[0] = '\0';
} }
if (curSelected == -1) if (curSelected == -1)
curSelected = alertBannerOptions - 1; curSelected = alertBannerOptions - 1;
if (curSelected == alertBannerOptions) if (curSelected == alertBannerOptions)
curSelected = 0; curSelected = 0;
// compare number of options to number of lines } else {
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
if (inEvent == INPUT_BROKER_SELECT || inEvent == INPUT_BROKER_ALT_LONG || inEvent == INPUT_BROKER_CANCEL) { 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; inEvent = INPUT_BROKER_NONE;
if (alertBannerMessage[0] == '\0') if (alertBannerMessage[0] == '\0')
return; return;
// set width from longest line uint16_t totalLines = lineCount + alertBannerOptions;
uint16_t boxWidth = padding * 2 + maxWidth;
if (needs_bell) { uint16_t screenHeight = display->height();
if (SCREEN_WIDTH > 128 && boxWidth <= 150) { uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
boxWidth += 26; uint8_t visibleTotalLines = std::min<uint8_t>(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight);
} uint8_t linesShown = lineCount;
if (SCREEN_WIDTH <= 128 && boxWidth <= 100) { const char *linePointers[visibleTotalLines]; // this is sort of a dynamic allocation
boxWidth += 20;
// copy the linestarts to display to the linePointers holder
for (int i = 0; i < lineCount; i++) {
linePointers[i] = lineStarts[i];
}
uint8_t firstOptionToShow = 0;
if (alertBannerOptions > 0) {
if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) {
if (curSelected > alertBannerOptions - visibleTotalLines + lineCount)
firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount;
else
firstOptionToShow = curSelected - 1;
} else {
firstOptionToShow = 0;
} }
} }
// calculate max lines on screen? for now it's 4
// set height from line count for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
uint16_t boxHeight; if (i == curSelected) {
if (lineCount <= 4) { strncpy(lineBuffer, "> ", 3);
boxHeight = vPadding * 2 + lineCount * FONT_HEIGHT_SMALL + (lineCount - 1) * lineSpacing; strncpy(lineBuffer + 2, optionsArrayPtr[i], 36);
strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3);
lineBuffer[39] = '\0';
linePointers[linesShown] = lineBuffer;
} else {
linePointers[linesShown] = optionsArrayPtr[i];
}
}
if (alertBannerOptions > 0) {
drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow, maxWidth);
} else { } else {
boxHeight = vPadding * 2 + 4 * FONT_HEIGHT_SMALL + 4 * lineSpacing; drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow);
} }
}
void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[],
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth)
{
bool is_picker = false;
uint16_t lineCount = 0;
// === Layout Configuration ===
constexpr uint16_t hPadding = 5;
constexpr uint16_t vPadding = 2;
bool needs_bell = false;
uint16_t lineWidths[totalLines] = {0};
if (maxWidth != 0)
is_picker = true;
// seelction box
while (lineCount < totalLines) {
if (lines[lineCount] != nullptr) {
lineWidths[lineCount] = display->getStringWidth(lines[lineCount], strlen(lines[lineCount]), true);
if (!is_picker) {
needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr);
if (lineWidths[lineCount] > maxWidth)
maxWidth = lineWidths[lineCount];
}
lineCount++;
} else {
break;
}
}
// count lines
uint16_t boxWidth = hPadding * 2 + maxWidth;
if (needs_bell) {
if (isHighResolution && boxWidth <= 150)
boxWidth += 26;
if (!isHighResolution && boxWidth <= 100)
boxWidth += 20;
}
uint16_t screenHeight = display->height();
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (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 boxLeft = (display->width() / 2) - (boxWidth / 2);
int16_t boxTop = (display->height() / 2) - (boxHeight / 2); int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
// === Draw background box ===
// === Draw Box ===
display->setColor(BLACK); display->setColor(BLACK);
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Slightly oversized box display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2);
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); // Top Line display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); // Bottom Line display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1);
display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); // Left Line display->fillRect(boxLeft - 2, boxTop, 1, boxHeight);
display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); // Right Line display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
display->setColor(WHITE); display->setColor(WHITE);
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); // Border display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
display->setColor(BLACK); display->setColor(BLACK);
display->fillRect(boxLeft, boxTop, 1, 1); // Top Left display->fillRect(boxLeft, boxTop, 1, 1);
display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); // Top Right display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1);
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); // Bottom Left display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); // Bottom Right display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
display->setColor(WHITE); display->setColor(WHITE);
// === Draw each line centered in the box === // === Draw Content ===
int16_t lineY = boxTop + vPadding; int16_t lineY = boxTop + vPadding;
for (int i = 0; i < lineCount; i++) { 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;
}
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
if (needs_bell && i == 0) { if (needs_bell && i == 0) {
int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2; int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2;
display->drawXbm(textX - 10, bellY, 8, 8, bell_alert); display->drawXbm(textX - 10, bellY, 8, 8, bell_alert);
display->drawXbm(textX + lineWidths[i] + 2, bellY, 8, 8, bell_alert); display->drawXbm(textX + lineWidths[i] + 2, bellY, 8, 8, bell_alert);
} }
display->drawString(textX, lineY, lineBuffer); // Determine if this is a pop-up or a pick list
lineY += FONT_HEIGHT_SMALL + lineSpacing; if (alertBannerOptions > 0 && i == 0) {
// Pick List
display->setColor(WHITE);
int background_yOffset = 1;
// Determine if we have low hanging characters
if (strchr(lines[i], 'p') || strchr(lines[i], 'g') || strchr(lines[i], 'y') || strchr(lines[i], 'j')) {
background_yOffset = -1;
}
display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset);
display->setColor(BLACK);
int yOffset = 3;
display->drawString(textX, lineY - yOffset, lines[i]);
display->setColor(WHITE);
lineY += (effectiveLineHeight - 2 - background_yOffset);
} else {
// Pop-up
LOG_WARN("x%u y%u %s", textX, lineY, lines[i]);
display->drawString(textX, lineY, lines[i]);
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 + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines);
uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight);
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
} }
} }

View File

@@ -2,6 +2,7 @@
#include "OLEDDisplay.h" #include "OLEDDisplay.h"
#include "OLEDDisplayUi.h" #include "OLEDDisplayUi.h"
#define MAX_LINES 5
namespace graphics namespace graphics
{ {
@@ -12,13 +13,17 @@ class NotificationRenderer
static char inEvent; static char inEvent;
static int8_t curSelected; static int8_t curSelected;
static char alertBannerMessage[256]; 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 uint8_t alertBannerOptions; // last x lines are seelctable options
static std::function<void(int)> alertBannerCallback; static std::function<void(int)> alertBannerCallback;
static bool pauseBanner; static bool pauseBanner;
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0);
static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);

View File

@@ -18,6 +18,32 @@
#include <RTC.h> #include <RTC.h>
#include <cstring> #include <cstring>
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<unsigned char>(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 #if !MESHTASTIC_EXCLUDE_GPS
// External variables // 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) void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps)
{ {
// Draw satellite image // Draw satellite image
if (SCREEN_WIDTH > 128) { if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y - 2, imgSatellite_width, imgSatellite_height, imgSatellite, display); NodeListRenderer::drawScaledXBitmap16x16(x, y - 2, imgSatellite_width, imgSatellite_height, imgSatellite, display);
} else { } else {
display->drawXbm(x + 1, y + 1, imgSatellite_width, imgSatellite_height, imgSatellite); 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 { } else {
snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites()); snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites());
} }
if (SCREEN_WIDTH > 128) { if (isHighResolution) {
display->drawString(x + 18, y, textString); display->drawString(x + 18, y, textString);
} else { } else {
display->drawString(x + 11, y, textString); 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 // Draw nodes status
void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset, void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset,
bool show_total, String additional_words) 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(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS) !defined(DISPLAY_FORCE_SMALL_FONTS)
if (SCREEN_WIDTH > 128) { if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display);
} else { } else {
display->drawFastImage(x, y + 3, 8, 8, imgUser); display->drawFastImage(x, y + 3, 8, 8, imgUser);
} }
#else #else
if (SCREEN_WIDTH > 128) { if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display);
} else { } else {
display->drawFastImage(x, y + 1, 8, 8, imgUser); display->drawFastImage(x, y + 1, 8, 8, imgUser);
} }
#endif #endif
int string_offset = (SCREEN_WIDTH > 128) ? 9 : 0; int string_offset = (isHighResolution) ? 9 : 0;
display->drawString(x + 10 + string_offset, y - 2, usersString); 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. // List of available macro Y positions in order, from top to bottom.
int line = 1; // which slot to use next int line = 1; // which slot to use next
std::string usernameStr;
// === 1. Long Name (always try to show first) === // === 1. Long Name (always try to show first) ===
const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; 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") // 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) === // === 2. Signal and Hops (combined on one line, if available) ===
@@ -476,7 +464,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
const int margin = 4; const int margin = 4;
// --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) ----------- // --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) -----------
#if defined(USE_EINK) #if defined(USE_EINK)
const int iconSize = (SCREEN_WIDTH > 128) ? 16 : 8; const int iconSize = (isHighResolution) ? 16 : 8;
const int navBarHeight = iconSize + 6; const int navBarHeight = iconSize + 6;
#else #else
const int navBarHeight = 0; const int navBarHeight = 0;
@@ -570,15 +558,15 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
} else { } else {
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
} }
int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1; int yOffset = (isHighResolution) ? 3 : 1;
if (SCREEN_WIDTH > 128) { if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width, NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
imgSatellite_height, imgSatellite, display); imgSatellite_height, imgSatellite, display);
} else { } else {
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height, display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
imgSatellite); imgSatellite);
} }
int xOffset = (SCREEN_WIDTH > 128) ? 6 : 0; int xOffset = (isHighResolution) ? 6 : 0;
display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine); display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine);
} else { } else {
UIRenderer::drawGps(display, 0, getTextPositions(display)[line], gpsStatus); UIRenderer::drawGps(display, 0, getTextPositions(display)[line], gpsStatus);
@@ -602,17 +590,17 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
char chUtilPercentage[10]; char chUtilPercentage[10];
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); 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_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) { 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 chutil_bar_height = (isHighResolution) ? 12 : 7;
int extraoffset = (SCREEN_WIDTH > 128) ? 6 : 3; int extraoffset = (isHighResolution) ? 6 : 3;
if (!config.bluetooth.enabled) { if (!config.bluetooth.enabled) {
extraoffset = (SCREEN_WIDTH > 128) ? 6 : 1; extraoffset = (isHighResolution) ? 6 : 1;
} }
int chutil_percent = airTime->channelUtilizationPercent(); int chutil_percent = airTime->channelUtilizationPercent();
@@ -672,21 +660,20 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
// === Fourth & Fifth Rows: Node Identity === // === Fourth & Fifth Rows: Node Identity ===
int textWidth = 0; int textWidth = 0;
int nameX = 0; int nameX = 0;
int yOffset = (SCREEN_WIDTH > 128) ? 0 : 5; int yOffset = (isHighResolution) ? 0 : 5;
const char *longName = nullptr; const char *longName = nullptr;
std::string longNameStr;
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { 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]; char shortnameble[35];
getMacAddr(dmac);
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
snprintf(shortnameble, sizeof(shortnameble), "%s", snprintf(shortnameble, sizeof(shortnameble), "%s",
graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : "");
char combinedName[50]; 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) { if (SCREEN_WIDTH - (display->getStringWidth(longName) + display->getStringWidth(shortnameble)) > 10) {
size_t len = strlen(combinedName); size_t len = strlen(combinedName);
if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) { if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) {
@@ -700,7 +687,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
// === LongName Centered === // === LongName Centered ===
textWidth = display->getStringWidth(longName); textWidth = display->getStringWidth(longName);
nameX = (SCREEN_WIDTH - textWidth) / 2; nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], longName); display->drawString(nameX, getTextPositions(display)[line++], longNameStr.c_str());
// === ShortName Centered === // === ShortName Centered ===
textWidth = display->getStringWidth(shortnameble); textWidth = display->getStringWidth(shortnameble);
@@ -808,44 +795,42 @@ void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState
{ {
LOG_DEBUG("Draw screensaver overlay"); 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 // Config
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
const char *pauseText = "Screen Paused"; const char *pauseText = "Screen Paused";
const char *idText = owner.short_name; 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 const bool useId = haveGlyphs(idText);
constexpr uint16_t padding = 5; constexpr uint8_t padding = 2;
constexpr uint8_t dividerGap = 1; constexpr uint8_t dividerGap = 1;
constexpr uint8_t imprecision = 5; // How far the box origins can drift from center. Combat burn-in.
// Dimensions // Text widths
const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true); // "true": handle utf8 chars const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true);
const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText)); const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText));
const uint16_t boxWidth = padding + (useId ? idTextWidth + padding + padding : 0) + pauseTextWidth + padding; const uint16_t boxWidth = padding + (useId ? idTextWidth + padding : 0) + pauseTextWidth + padding;
const uint16_t boxHeight = padding + FONT_HEIGHT_SMALL + padding; const uint16_t boxHeight = FONT_HEIGHT_SMALL + (padding * 2);
// Position // Flush with bottom
const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2) + random(-imprecision, imprecision + 1); const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
// const int16_t boxRight = boxLeft + boxWidth - 1; const int16_t boxTop = display->height() - boxHeight;
const int16_t boxTop = (display->height() / 2) - (boxHeight / 2 + random(-imprecision, imprecision + 1)); const int16_t boxBottom = display->height() - 1;
const int16_t boxBottom = boxTop + boxHeight - 1;
const int16_t idTextLeft = boxLeft + padding; const int16_t idTextLeft = boxLeft + padding;
const int16_t idTextTop = boxTop + 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 pauseTextTop = boxTop + padding;
const int16_t dividerX = boxLeft + padding + idTextWidth + padding; const int16_t dividerX = boxLeft + padding + idTextWidth + padding;
const int16_t dividerTop = boxTop + 1 + dividerGap; const int16_t dividerTop = boxTop + dividerGap;
const int16_t dividerBottom = boxBottom - 1 - dividerGap; const int16_t dividerBottom = boxBottom - dividerGap;
// Draw: box // Draw: box
display->setColor(EINK_WHITE); 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->setColor(EINK_BLACK);
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
// Draw: Text // Draw: text
if (useId) if (useId)
display->drawString(idTextLeft, idTextTop, idText); display->drawString(idTextLeft, idTextTop, idText);
display->drawString(pauseTextLeft, pauseTextTop, pauseText); display->drawString(pauseTextLeft, pauseTextTop, pauseText);
@@ -920,15 +905,15 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
} else { } else {
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
} }
int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1; int yOffset = (isHighResolution) ? 3 : 1;
if (SCREEN_WIDTH > 128) { if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width, NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
imgSatellite_height, imgSatellite, display); imgSatellite_height, imgSatellite, display);
} else { } else {
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height, display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
imgSatellite); imgSatellite);
} }
int xOffset = (SCREEN_WIDTH > 128) ? 6 : 0; int xOffset = (isHighResolution) ? 6 : 0;
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine); display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
} else { } else {
UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus); UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus);
@@ -1114,18 +1099,6 @@ void UIRenderer::drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *sta
#endif #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 // Navigation bar overlay implementation
static int8_t lastFrameIndex = -1; static int8_t lastFrameIndex = -1;
static uint32_t lastFrameChangeTime = 0; static uint32_t lastFrameChangeTime = 0;
@@ -1141,10 +1114,9 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
lastFrameChangeTime = millis(); lastFrameChangeTime = millis();
} }
const bool useBigIcons = (SCREEN_WIDTH > 128); const int iconSize = isHighResolution ? 16 : 8;
const int iconSize = useBigIcons ? 16 : 8; const int spacing = isHighResolution ? 8 : 4;
const int spacing = useBigIcons ? 8 : 4; const int bigOffset = isHighResolution ? 1 : 0;
const int bigOffset = useBigIcons ? 1 : 0;
const size_t totalIcons = screen->indicatorIcons.size(); const size_t totalIcons = screen->indicatorIcons.size();
if (totalIcons == 0) if (totalIcons == 0)
@@ -1158,14 +1130,35 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing; const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing;
const int xStart = (SCREEN_WIDTH - totalWidth) / 2; 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) #if defined(USE_EINK)
int y = SCREEN_HEIGHT - iconSize - 1; static bool navBarPrevVisible = false;
#else
int y = SCREEN_HEIGHT - iconSize - 1; if (navBarVisible && !navBarPrevVisible) {
if (millis() - lastFrameChangeTime > ICON_DISPLAY_DURATION_MS) { EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when showing nav bar
y = SCREEN_HEIGHT; 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 #endif
// Pre-calculate bounding rect // Pre-calculate bounding rect
@@ -1191,7 +1184,7 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
display->setColor(BLACK); display->setColor(BLACK);
} }
if (useBigIcons) { if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display); NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display);
} else { } else {
display->drawXbm(x, y, iconSize, iconSize, icon); display->drawXbm(x, y, iconSize, iconSize, icon);

View File

@@ -32,8 +32,6 @@ class UIRenderer
{ {
public: public:
// Common UI elements // 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, 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 = ""); int node_offset = 0, bool show_total = true, String additional_words = "");
@@ -49,9 +47,6 @@ class UIRenderer
// Overlay and special screens // Overlay and special screens
static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text); 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 // Navigation bar overlay
static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state);

View File

@@ -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, 0xf3, 0x3f, 0x33, 0x30, 0x33, 0x33, 0x33, 0x33, 0x03, 0x33, 0xff, 0x33,
0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f}; 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) || \ #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(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || ARCH_PORTDUINO) && \
!defined(DISPLAY_FORCE_SMALL_FONTS) !defined(DISPLAY_FORCE_SMALL_FONTS)
@@ -45,19 +42,15 @@ const uint8_t imgSF[] PROGMEM = {0xd2, 0xb7, 0xad, 0xbb, 0x92, 0x01, 0xfd, 0xfd,
// === Horizontal battery === // === Horizontal battery ===
// Basic battery design and all related pieces // Basic battery design and all related pieces
const unsigned char batteryBitmap_h[] PROGMEM = { const unsigned char batteryBitmap_h_bottom[] PROGMEM = {
0b11111110, 0b00000000, 0b11110000, 0b00000111, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00011110, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001,
0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00011110, 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};
// This is the left and right bars for the fill in const unsigned char batteryBitmap_h_top[] PROGMEM = {
const unsigned char batteryBitmap_sidegaps_h[] PROGMEM = { 0b00111100, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000,
0b11111111, 0b00001111, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000,
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b00111100, 0b00000000};
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b11111111, 0b00001111};
// Lightning Bolt // Lightning Bolt
const unsigned char lightning_bolt_h[] PROGMEM = { 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, const uint8_t smallbulletpoint[] PROGMEM = {0b00000011, 0b00000011, 0b00000000, 0b00000000,
0b00000000, 0b00000000, 0b00000000, 0b00000000}; 0b00000000, 0b00000000, 0b00000000, 0b00000000};
// Clock // Digital Clock
#define icon_clock_width 8 #define digital_icon_clock_width 8
#define icon_clock_height 8 #define digital_icon_clock_height 8
const uint8_t icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101, 0b10101001, const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101, 0b10101001,
0b10010001, 0b10000001, 0b01000010, 0b00111100}; 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" #include "img/icon.xbm"
static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning"); static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning");

View File

@@ -266,6 +266,11 @@ int32_t ButtonThread::runOnce()
break; break;
} }
// doesn't handle BUTTON_EVENT_PRESSED_SCREEN BUTTON_EVENT_TOUCH_LONG_PRESSED BUTTON_EVENT_COMBO_SHORT_LONG
default: {
break;
}
} }
} }
btnEvent = BUTTON_EVENT_NONE; btnEvent = BUTTON_EVENT_NONE;

View File

@@ -244,10 +244,13 @@ void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to)
p->decoded.request_id = to.id; p->decoded.request_id = to.id;
} }
std::vector<MeshModule *> MeshModule::GetMeshModulesWithUIFrames() std::vector<MeshModule *> MeshModule::GetMeshModulesWithUIFrames(int startIndex)
{ {
std::vector<MeshModule *> modulesWithUIFrames; std::vector<MeshModule *> modulesWithUIFrames;
// Fill with nullptr up to startIndex
modulesWithUIFrames.resize(startIndex, nullptr);
if (modules) { if (modules) {
for (auto i = modules->begin(); i != modules->end(); ++i) { for (auto i = modules->begin(); i != modules->end(); ++i) {
auto &pi = **i; auto &pi = **i;

View File

@@ -75,7 +75,7 @@ class MeshModule
*/ */
static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO); static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO);
static std::vector<MeshModule *> GetMeshModulesWithUIFrames(); static std::vector<MeshModule *> GetMeshModulesWithUIFrames(int startIndex);
static void observeUIEvents(Observer<const UIFrameEvent *> *observer); static void observeUIEvents(Observer<const UIFrameEvent *> *observer);
static AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp, static AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp,
meshtastic_AdminMessage *request, meshtastic_AdminMessage *request,

View File

@@ -1566,6 +1566,7 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
// Mark the node's key as manually verified to indicate trustworthiness. // Mark the node's key as manually verified to indicate trustworthiness.
updateGUIforNode = info; updateGUIforNode = info;
// powerFSM.trigger(EVENT_NODEDB_UPDATED); This event has been retired // powerFSM.trigger(EVENT_NODEDB_UPDATED); This event has been retired
sortMeshDB();
notifyObservers(true); // Force an update whether or not our node counts have changed notifyObservers(true); // Force an update whether or not our node counts have changed
} }
saveNodeDatabaseToDisk(); saveNodeDatabaseToDisk();
@@ -1669,6 +1670,31 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
info->has_hops_away = true; info->has_hops_away = true;
info->hops_away = mp.hop_start - mp.hop_limit; 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;
});
} }
} }

View File

@@ -282,6 +282,7 @@ class NodeDB
bool duplicateWarned = false; bool duplicateWarned = false;
uint32_t lastNodeDbSave = 0; // when we last saved our db to flash 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 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 /// Find a node in our DB, create an empty NodeInfoLite if missing
meshtastic_NodeInfoLite *getOrCreateMeshNode(NodeNum n); meshtastic_NodeInfoLite *getOrCreateMeshNode(NodeNum n);
@@ -310,6 +311,7 @@ class NodeDB
bool saveChannelsToDisk(); bool saveChannelsToDisk();
bool saveDeviceStateToDisk(); bool saveDeviceStateToDisk();
bool saveNodeDatabaseToDisk(); bool saveNodeDatabaseToDisk();
void sortMeshDB();
}; };
extern NodeDB *nodeDB; extern NodeDB *nodeDB;

View File

@@ -154,7 +154,7 @@ int CannedMessageModule::splitConfiguredMessages()
} }
void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer) 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) { if (this->dest == NODENUM_BROADCAST) {
display->drawStringf(x, y, buffer, "To: Broadcast@%s", channels.getName(this->channel)); display->drawStringf(x, y, buffer, "To: Broadcast@%s", channels.getName(this->channel));
} else { } else {
@@ -245,12 +245,15 @@ void CannedMessageModule::updateDestinationSelectionList()
} }
} }
/* As the nodeDB is sorted, can skip this step
// Sort by favorite, then last heard // Sort by favorite, then last heard
std::sort(this->filteredNodes.begin(), this->filteredNodes.end(), [](const NodeEntry &a, const NodeEntry &b) { std::sort(this->filteredNodes.begin(), this->filteredNodes.end(), [](const NodeEntry &a, const NodeEntry &b) {
if (a.node->is_favorite != b.node->is_favorite) if (a.node->is_favorite != b.node->is_favorite)
return a.node->is_favorite > b.node->is_favorite; return a.node->is_favorite > b.node->is_favorite;
return a.lastHeard < b.lastHeard; return a.lastHeard < b.lastHeard;
}); });
*/
scrollIndex = 0; // Show first result at the top scrollIndex = 0; // Show first result at the top
destIndex = 0; // Highlight the first entry destIndex = 0; // Highlight the first entry
if (nodesChanged && runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { if (nodesChanged && runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) {
@@ -387,6 +390,7 @@ bool CannedMessageModule::handleTabSwitch(const InputEvent *event)
// RESTORE THIS! // RESTORE THIS!
if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION)
updateDestinationSelectionList(); updateDestinationSelectionList();
requestFocus();
UIFrameEvent e; UIFrameEvent e;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
@@ -986,6 +990,7 @@ int32_t CannedMessageModule::runOnce()
default: default:
// Only insert ASCII printable characters (32126) // Only insert ASCII printable characters (32126)
if (this->payload >= 32 && this->payload <= 126) { if (this->payload >= 32 && this->payload <= 126) {
requestFocus();
if (this->cursor == this->freetext.length()) { if (this->cursor == this->freetext.length()) {
this->freetext += (char)this->payload; this->freetext += (char)this->payload;
} else { } else {

View File

@@ -79,10 +79,10 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
memset(message, 0, sizeof(message)); memset(message, 0, sizeof(message));
sprintf(message, "Verification: \n"); sprintf(message, "Verification: \n");
generateVerificationCode(message + 15); generateVerificationCode(message + 15);
sprintf(message + 24, "\nACCEPT\nREJECT"); static const char *optionsArray[] = {"ACCEPT", "REJECT"};
LOG_INFO("Hash1 matches!"); LOG_INFO("Hash1 matches!");
if (screen) { if (screen) {
screen->showOverlayBanner(message, 30000, 2, [=](int selected) { screen->showOverlayBanner(message, 30000, optionsArray, 2, [=](int selected) {
if (selected == 0) { if (selected == 0) {
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;

View File

@@ -100,9 +100,9 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
case INPUT_BROKER_SEND_PING: case INPUT_BROKER_SEND_PING:
service->refreshLocalMeshNode(); service->refreshLocalMeshNode();
if (service->trySendPosition(NODENUM_BROADCAST, true)) { if (service->trySendPosition(NODENUM_BROADCAST, true)) {
IF_SCREEN(screen->showOverlayBanner("Position\nUpdate Sent", 3000)); IF_SCREEN(screen->showOverlayBanner("Position\nSent", 3000));
} else { } else {
IF_SCREEN(screen->showOverlayBanner("Node Info\nUpdate Sent", 3000)); IF_SCREEN(screen->showOverlayBanner("Node Info\nSent", 3000));
} }
return true; return true;
// Power control // Power control
@@ -113,6 +113,10 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000; shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000;
// runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; // runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
return true; return true;
default:
// No other input events handled here
break;
} }
return false; return false;
} }

View File

@@ -30,7 +30,7 @@
namespace graphics 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(<Adafruit_AHTX0.h>) #if __has_include(<Adafruit_AHTX0.h>)
#include "Sensor/AHT10.h" #include "Sensor/AHT10.h"
@@ -358,7 +358,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
int line = 1; int line = 1;
// === Set Title // === Set Title
const char *titleStr = (SCREEN_WIDTH > 128) ? "Environment" : "Env."; const char *titleStr = (graphics::isHighResolution) ? "Environment" : "Env.";
// === Header === // === Header ===
graphics::drawCommonHeader(display, x, y, titleStr); graphics::drawCommonHeader(display, x, y, titleStr);

View File

@@ -24,7 +24,7 @@
namespace graphics 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() int32_t PowerTelemetryModule::runOnce()
@@ -115,7 +115,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s
int line = 1; int line = 1;
// === Set Title // === Set Title
const char *titleStr = (SCREEN_WIDTH > 128) ? "Power Telem." : "Power"; const char *titleStr = (graphics::isHighResolution) ? "Power Telem." : "Power";
// === Header === // === Header ===
graphics::drawCommonHeader(display, x, y, titleStr); graphics::drawCommonHeader(display, x, y, titleStr);

View File

@@ -79,7 +79,8 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
msgPayload["relative_humidity"] = new JSONValue(decoded->variant.environment_metrics.relative_humidity); msgPayload["relative_humidity"] = new JSONValue(decoded->variant.environment_metrics.relative_humidity);
} }
if (decoded->variant.environment_metrics.has_barometric_pressure) { 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) { if (decoded->variant.environment_metrics.has_gas_resistance) {
msgPayload["gas_resistance"] = new JSONValue(decoded->variant.environment_metrics.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); msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard);
} }
if (decoded->variant.air_quality_metrics.has_pm10_environmental) { 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) { 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) { 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) { } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
if (decoded->variant.power_metrics.has_ch1_voltage) { if (decoded->variant.power_metrics.has_ch1_voltage) {

View File

@@ -22,7 +22,6 @@ lib_deps =
${native_base.lib_deps} ${native_base.lib_deps}
${device-ui_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 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 RAM_SIZE=16384
-D USE_X11=1 -D USE_X11=1
-D HAS_TFT=1 -D HAS_TFT=1
@@ -51,7 +50,6 @@ lib_deps =
${device-ui_base.lib_deps} ${device-ui_base.lib_deps}
board_level = extra board_level = extra
build_flags = ${native_base.build_flags} -Os -ffunction-sections -fdata-sections -Wl,--gc-sections build_flags = ${native_base.build_flags} -Os -ffunction-sections -fdata-sections -Wl,--gc-sections
-D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1
-D RAM_SIZE=8192 -D RAM_SIZE=8192
-D USE_FRAMEBUFFER=1 -D USE_FRAMEBUFFER=1
-D LV_COLOR_DEPTH=32 -D LV_COLOR_DEPTH=32
@@ -81,7 +79,6 @@ lib_deps =
${device-ui_base.lib_deps} ${device-ui_base.lib_deps}
board_level = extra board_level = extra
build_flags = ${native_base.build_flags} -O0 -fsanitize=address -lX11 -linput -lxkbcommon build_flags = ${native_base.build_flags} -O0 -fsanitize=address -lX11 -linput -lxkbcommon
-D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1
-D DEBUG_HEAP -D DEBUG_HEAP
-D RAM_SIZE=16384 -D RAM_SIZE=16384
-D USE_X11=1 -D USE_X11=1

View File

@@ -88,8 +88,8 @@ static const uint8_t A7 = PIN_A7;
#define ADC_RESOLUTION 14 #define ADC_RESOLUTION 14
// Other pins // Other pins
#define WB_I2C1_SDA (13) // SENSOR_SLOT IO_SLOT #define WB_I2C1_SDA (13) // SENSOR_SLOT IO_SLOT
#define WB_I2C1_SCL (14) // SENSOR_SLOT IO_SLOT #define WB_I2C1_SCL (14) // SENSOR_SLOT IO_SLOT
#define PIN_AREF (2) #define PIN_AREF (2)
#define PIN_NFC1 (9) #define PIN_NFC1 (9)