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 "draw/ClockRenderer.h"
#include "draw/DebugRenderer.h"
#include "draw/MenuHandler.h"
#include "draw/MessageRenderer.h"
#include "draw/NodeListRenderer.h"
#include "draw/NotificationRenderer.h"
@@ -135,13 +136,17 @@ extern bool hasUnreadMessage;
// The banner appears in the center of the screen and disappears after the specified duration
// Called to trigger a banner with custom message and duration
void Screen::showOverlayBanner(const char *message, uint32_t durationMs, uint8_t options, std::function<void(int)> bannerCallback,
int8_t InitialSelected)
void Screen::showOverlayBanner(const char *message, uint32_t durationMs, const char **optionsArrayPtr, uint8_t options,
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
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
NotificationRenderer::optionsArrayPtr = optionsArrayPtr;
NotificationRenderer::alertBannerOptions = options;
NotificationRenderer::alertBannerCallback = bannerCallback;
NotificationRenderer::curSelected = InitialSelected;
@@ -152,6 +157,24 @@ void Screen::showOverlayBanner(const char *message, uint32_t durationMs, uint8_t
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)
{
uint8_t module_frame;
@@ -413,8 +436,7 @@ void Screen::setup()
// === Set custom overlay callbacks ===
static OverlayCallback overlays[] = {
graphics::UIRenderer::drawFunctionOverlay, // For mute/buzzer modifiers etc.
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
@@ -471,6 +493,7 @@ void Screen::setup()
// === Turn on display and trigger first draw ===
handleSetOn(true);
determineResolution(dispdev->height(), dispdev->width());
ui->update();
#ifndef USE_EINK
ui->update(); // Some SSD1306 clones drop the first draw, so run twice
@@ -557,6 +580,7 @@ int32_t Screen::runOnce()
if (displayHeight == 0) {
displayHeight = dispdev->getHeight();
}
menuHandler::handleMenuSwitch();
// Show boot screen for first logo_timeout seconds, then switch to normal operation.
// serialSinceMsec adjusts for additional serial wait time during nRF52 bootup
@@ -585,7 +609,7 @@ int32_t Screen::runOnce()
#ifndef DISABLE_WELCOME_UNSET
if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
LoraRegionPicker(0);
menuHandler::LoraRegionPicker(0);
}
#endif
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) {
@@ -768,32 +792,6 @@ void Screen::setFrames(FrameFocus focus)
indicatorIcons.clear();
size_t numframes = 0;
moduleFrames = MeshModule::GetMeshModulesWithUIFrames();
LOG_DEBUG("Show %d module frames", moduleFrames.size());
// put all of the module frames first.
// this is a little bit of a dirty hack; since we're going to call
// the same drawModuleFrame handler here for all of these module frames
// and then we'll just assume that the state->currentFrame value
// is the same offset into the moduleFrames vector
// so that we can invoke the module's callback
for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) {
// Draw the module frame, using the hack described above
normalFrames[numframes] = drawModuleFrame;
// Check if the module being drawn has requested focus
// We will honor this request later, if setFrames was triggered by a UIFrameEvent
MeshModule *m = *i;
if (m->isRequestingFocus())
fsi.positions.focusedModule = numframes;
if (m == waypointModule)
fsi.positions.waypoint = numframes;
indicatorIcons.push_back(icon_module);
numframes++;
}
LOG_DEBUG("Added modules. numframes: %d", numframes);
// If we have a critical fault, show it first
fsi.positions.fault = numframes;
@@ -807,7 +805,7 @@ void Screen::setFrames(FrameFocus focus)
fsi.positions.clock = numframes;
normalFrames[numframes++] = graphics::ClockRenderer::digitalWatchFace ? graphics::ClockRenderer::drawDigitalClockFrame
: &graphics::ClockRenderer::drawAnalogClockFrame;
indicatorIcons.push_back(icon_clock);
indicatorIcons.push_back(digital_icon_clock);
#endif
// Declare this early so its available in FOCUS_PRESERVE block
@@ -822,22 +820,27 @@ void Screen::setFrames(FrameFocus focus)
indicatorIcons.push_back(icon_mail);
#ifndef USE_EINK
fsi.positions.nodelist = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen;
indicatorIcons.push_back(icon_nodes);
#endif
// Show detailed node views only on E-Ink builds
#ifdef USE_EINK
fsi.positions.nodelist_lastheard = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen;
indicatorIcons.push_back(icon_nodes);
fsi.positions.nodelist_hopsignal = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen;
indicatorIcons.push_back(icon_signal);
fsi.positions.nodelist_distance = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen;
indicatorIcons.push_back(icon_distance);
#endif
#if HAS_GPS
fsi.positions.nodelist_bearings = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
indicatorIcons.push_back(icon_list);
@@ -857,8 +860,9 @@ void Screen::setFrames(FrameFocus focus)
}
#if !defined(DISPLAY_CLOCK_FRAME)
fsi.positions.clock = numframes;
normalFrames[numframes++] = graphics::ClockRenderer::drawDigitalClockFrame;
indicatorIcons.push_back(icon_clock);
normalFrames[numframes++] = graphics::ClockRenderer::digitalWatchFace ? graphics::ClockRenderer::drawDigitalClockFrame
: graphics::ClockRenderer::drawAnalogClockFrame;
indicatorIcons.push_back(digital_icon_clock);
#endif
// We don't show the node info of our node (if we have it yet - we should)
@@ -885,6 +889,36 @@ void Screen::setFrames(FrameFocus focus)
}
#endif
// Beware of what changes you make in this code!
// We pass numfames into GetMeshModulesWithUIFrames() which is highly important!
// Inside of that callback, goes over to MeshModule.cpp and we run
// modulesWithUIFrames.resize(startIndex, nullptr), to insert nullptr
// entries until we're ready to start building the matching entries.
// We are doing our best to keep the normalFrames vector
// and the moduleFrames vector in lock step.
moduleFrames = MeshModule::GetMeshModulesWithUIFrames(numframes);
LOG_DEBUG("Show %d module frames", moduleFrames.size());
for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) {
// Draw the module frame, using the hack described above
if (*i != nullptr) {
normalFrames[numframes] = drawModuleFrame;
// Check if the module being drawn has requested focus
// We will honor this request later, if setFrames was triggered by a UIFrameEvent
MeshModule *m = *i;
if (m && m->isRequestingFocus())
fsi.positions.focusedModule = numframes;
if (m && m == waypointModule)
fsi.positions.waypoint = numframes;
indicatorIcons.push_back(icon_module);
numframes++;
}
}
LOG_DEBUG("Added modules. numframes: %d", numframes);
fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE
this->frameCount = numframes; // ✅ Save frame count for use in custom overlay
LOG_DEBUG("Finished build frames. numframes: %d", numframes);
@@ -916,6 +950,11 @@ void Screen::setFrames(FrameFocus focus)
// If no module requested focus, will show the first frame instead
ui->switchToFrame(fsi.positions.focusedModule);
break;
case FOCUS_CLOCK:
// Whichever frame was marked by MeshModule::requestFocus(), if any
// If no module requested focus, will show the first frame instead
ui->switchToFrame(fsi.positions.clock);
break;
case FOCUS_PRESERVE:
// No more adjustment — force stay on same index
@@ -1204,6 +1243,8 @@ int Screen::handleInputEvent(const InputEvent *event)
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
setFastFramerate(); // Draw ASAP
ui->update();
menuHandler::handleMenuSwitch();
return 0;
}
/*
@@ -1229,7 +1270,7 @@ int Screen::handleInputEvent(const InputEvent *event)
// Ask any MeshModules if they're handling keyboard input right now
bool inputIntercepted = false;
for (MeshModule *module : moduleFrames) {
if (module->interceptingKeyboardInput())
if (module && module->interceptingKeyboardInput())
inputIntercepted = true;
}
@@ -1241,129 +1282,36 @@ int Screen::handleInputEvent(const InputEvent *event)
showNextFrame();
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
const char *banner_message;
int options;
if (kb_found) {
banner_message = "Action?\nBack\nSleep Screen\nNew Preset Msg\nNew Freetext Msg";
options = 4;
} else {
banner_message = "Action?\nBack\nSleep Screen\nNew Preset Msg";
options = 3;
}
showOverlayBanner(banner_message, 30000, options, [](int selected) -> void {
if (selected == 1) {
screen->setOn(false);
} else if (selected == 2) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
} else if (selected == 3) {
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST);
}
});
menuHandler::homeBaseMenu();
#if HAS_TFT
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) {
showOverlayBanner("Switch to MUI?\nYes\nNo", 30000, 2, [](int selected) -> void {
if (selected == 0) {
config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR;
config.bluetooth.enabled = false;
service->reloadConfig(SEGMENT_CONFIG);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
}
});
menuHandler::switchToMUIMenu();
#else
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) {
showOverlayBanner(
"Beeps Mode\nAll Enabled\nDisabled\nNotifications\nSystem Only", 30000, 4,
[](int selected) -> void {
config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected;
service->reloadConfig(SEGMENT_CONFIG);
},
config.device.buzzer_mode);
menuHandler::BuzzerModeMenu();
#endif
#if HAS_GPS
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) {
showOverlayBanner(
"Toggle GPS\nBack\nEnabled\nDisabled", 30000, 3,
[](int selected) -> void {
if (selected == 1) {
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED;
playGPSEnableBeep();
gps->enable();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 2) {
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED;
playGPSDisableBeep();
gps->disable();
service->reloadConfig(SEGMENT_CONFIG);
}
},
config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1
: 2); // set inital selection
menuHandler::positionBaseMenu();
#endif
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) {
TZPicker();
menuHandler::clockMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
LoraRegionPicker();
menuHandler::LoraRegionPicker();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage &&
devicestate.rx_text_message.from) {
const char *banner_message;
int options;
if (kb_found) {
banner_message = "Message Action?\nBack\nDismiss\nReply via Preset\nReply via Freetext";
options = 4;
} else {
banner_message = "Message Action?\nBack\nDismiss\nReply via Preset";
options = 3;
}
#ifdef HAS_I2S
banner_message = "Message Action?\nBack\nDismiss\nReply via Preset\nReply via Freetext\nRead Aloud";
options = 5;
#endif
showOverlayBanner(banner_message, 30000, options, [](int selected) -> void {
if (selected == 1) {
screen->dismissCurrentFrame();
} else if (selected == 2) {
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST,
devicestate.rx_text_message.channel);
} else {
cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
}
} else if (selected == 3) {
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST,
devicestate.rx_text_message.channel);
} else {
cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
}
}
#ifdef HAS_I2S
else if (selected == 4) {
const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes);
audioThread->readAloud(msg);
}
#endif
});
menuHandler::messageResponseMenu();
} else if (framesetInfo.positions.firstFavorite != 255 &&
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
const char *banner_message;
int options;
if (kb_found) {
banner_message = "Message Node?\nCancel\nNew Preset Msg\nNew Freetext Msg";
options = 3;
} else {
banner_message = "Message Node?\nCancel\nConfirm";
options = 2;
}
showOverlayBanner(banner_message, 30000, options, [](int selected) -> void {
if (selected == 1) {
cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
} else if (selected == 2) {
cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
}
});
menuHandler::favoriteBaseMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) {
menuHandler::nodeListMenu();
}
} else if (event->inputEvent == INPUT_BROKER_BACK) {
showPrevFrame();
@@ -1397,96 +1345,6 @@ bool Screen::isOverlayBannerShowing()
return NotificationRenderer::isOverlayBannerShowing();
}
void Screen::LoraRegionPicker(uint32_t duration)
{
showOverlayBanner(
"Set the LoRa "
"region\nBack\nUS\nEU_433\nEU_868\nCN\nJP\nANZ\nKR\nTW\nRU\nIN\nNZ_865\nTH\nLORA_24\nUA_433\nUA_868\nMY_433\nMY_"
"919\nSG_"
"923\nPH_433\nPH_868\nPH_915\nANZ_433",
duration, 23,
[](int selected) -> void {
if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) {
config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected);
// This is needed as we wait til picking the LoRa region to generate keys for the first time.
if (!owner.is_licensed) {
bool keygenSuccess = false;
if (config.security.private_key.size == 32) {
// public key is derived from private, so this will always have the same result.
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
keygenSuccess = true;
}
} else {
LOG_INFO("Generate new PKI keys");
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
keygenSuccess = true;
}
if (keygenSuccess) {
config.security.public_key.size = 32;
config.security.private_key.size = 32;
owner.public_key.size = 32;
memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
}
}
config.lora.tx_enabled = true;
initRegion();
if (myRegion->dutyCycle < 100) {
config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit
}
service->reloadConfig(SEGMENT_CONFIG);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
}
},
0);
}
void Screen::TZPicker()
{
showOverlayBanner(
"Pick "
"Timezone\nBack\nUS/Hawaii\nUS/Alaska\nUS/Pacific\nUS/Mountain\nUS/Central\nUS/Eastern\nUTC\nEU/Western\nEU/"
"Central\nEU/Eastern\nAsia/Kolkata\nAsia/Hong_Kong\nAU/AWST\nAU/ACST\nAU/AEST\nPacific/NZ",
30000, 17, [](int selected) -> void {
if (selected == 1) { // Hawaii
strncpy(config.device.tzdef, "HST10", sizeof(config.device.tzdef));
} else if (selected == 2) { // Alaska
strncpy(config.device.tzdef, "AKST9AKDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
} else if (selected == 3) { // Pacific
strncpy(config.device.tzdef, "PST8PDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
} else if (selected == 4) { // Mountain
strncpy(config.device.tzdef, "MST7MDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
} else if (selected == 5) { // Central
strncpy(config.device.tzdef, "CST6CDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
} else if (selected == 6) { // Eastern
strncpy(config.device.tzdef, "EST5EDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
} else if (selected == 7) { // UTC
strncpy(config.device.tzdef, "UTC", sizeof(config.device.tzdef));
} else if (selected == 8) { // EU/Western
strncpy(config.device.tzdef, "GMT0BST,M3.5.0/1,M10.5.0", sizeof(config.device.tzdef));
} else if (selected == 9) { // EU/Central
strncpy(config.device.tzdef, "CET-1CEST,M3.5.0,M10.5.0/3", sizeof(config.device.tzdef));
} else if (selected == 10) { // EU/Eastern
strncpy(config.device.tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4", sizeof(config.device.tzdef));
} else if (selected == 11) { // Asia/Kolkata
strncpy(config.device.tzdef, "IST-5:30", sizeof(config.device.tzdef));
} else if (selected == 12) { // China
strncpy(config.device.tzdef, "HKT-8", sizeof(config.device.tzdef));
} else if (selected == 13) { // AU/AWST
strncpy(config.device.tzdef, "AWST-8", sizeof(config.device.tzdef));
} else if (selected == 14) { // AU/ACST
strncpy(config.device.tzdef, "ACST-9:30ACDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef));
} else if (selected == 15) { // AU/AEST
strncpy(config.device.tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef));
} else if (selected == 16) { // NZ
strncpy(config.device.tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3", sizeof(config.device.tzdef));
}
if (selected != 0) {
setenv("TZ", config.device.tzdef, 1);
service->reloadConfig(SEGMENT_CONFIG);
}
});
}
} // namespace graphics
#else

View File

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

View File

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

View File

@@ -41,12 +41,14 @@ namespace graphics
// Shared state (declare inside namespace)
extern bool hasUnreadMessage;
extern bool isMuted;
extern bool isHighResolution;
void determineResolution(int16_t screenheight, int16_t screenwidth);
// Rounded highlight (used for inverted headers)
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r);
// Shared battery/time/mail header
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "");
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool battery_only = false);
const int *getTextPositions(OLEDDisplay *display);

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -2,6 +2,8 @@
#include "OLEDDisplay.h"
#include "OLEDDisplayUi.h"
#include "graphics/emotes.h"
#include <string>
#include <vector>
namespace graphics
{
@@ -14,5 +16,15 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
/// Draws the text message frame for displaying received messages
void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
// Function to generate lines with word wrapping
std::vector<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 graphics

View File

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

View File

@@ -23,12 +23,6 @@ namespace NodeListRenderer
typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int);
typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double);
// Node entry structure
struct NodeEntry {
meshtastic_NodeInfoLite *node;
uint32_t sortValue;
};
// Node list mode enumeration
enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 };
@@ -57,7 +51,6 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
// Utility functions
const char *getCurrentModeTitle(int screenWidth);
void retrieveAndSortNodes(std::vector<NodeEntry> &nodeList);
const char *getSafeNodeName(meshtastic_NodeInfoLite *node);
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};
uint32_t NotificationRenderer::alertBannerUntil = 0; // 0 is a special case meaning forever
uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are seelctable options
const char **NotificationRenderer::optionsArrayPtr = nullptr;
std::function<void(int)> NotificationRenderer::alertBannerCallback = NULL;
bool NotificationRenderer::pauseBanner = false;
@@ -56,60 +57,51 @@ void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiStat
void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state)
{
// Exit if no message is active or duration has passed
if (!isOverlayBannerShowing())
return;
if (pauseBanner)
if (!isOverlayBannerShowing() || pauseBanner)
return;
// === Layout Configuration ===
constexpr uint16_t padding = 5; // Padding around text inside the box
constexpr uint16_t vPadding = 2; // Padding around text inside the box
constexpr uint8_t lineSpacing = 1; // Extra space between lines
// 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;
constexpr uint16_t vPadding = 2;
// Setup font and alignment
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT); // We will manually center per line
const int MAX_LINES = 24;
display->setTextAlignment(TEXT_ALIGN_LEFT);
uint16_t optionWidths[alertBannerOptions] = {0};
uint16_t maxWidth = 0;
uint16_t arrowsWidth = display->getStringWidth("> <", 4, true);
uint16_t lineWidths[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;
char lineBuffer[40] = {0};
// pointer to the terminating null
// Parse lines
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
lineStarts[lineCount] = alertBannerMessage;
// loop through lines finding \n characters
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
lineStarts[lineCount + 1] = std::find(lineStarts[lineCount], alertEnd, '\n');
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount];
if (lineStarts[lineCount + 1][0] == '\n') {
lineStarts[lineCount + 1] += 1; // Move the start pointer beyond the \n
}
if (lineStarts[lineCount + 1][0] == '\n')
lineStarts[lineCount + 1] += 1;
lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true);
if (lineWidths[lineCount] > maxWidth) {
if (lineWidths[lineCount] > maxWidth)
maxWidth = lineWidths[lineCount];
}
if (alertBannerOptions > 0 && lineCount > 0 && lineWidths[lineCount] + arrowsWidth > maxWidth) {
maxWidth = lineWidths[lineCount] + arrowsWidth;
}
lineCount++;
// if we are doing a selection, add extra width for arrows
}
// Measure option widths
for (int i = 0; i < alertBannerOptions; i++) {
optionWidths[i] = display->getStringWidth(optionsArrayPtr[i], strlen(optionsArrayPtr[i]), true);
if (optionWidths[i] > maxWidth)
maxWidth = optionWidths[i];
if (optionWidths[i] + arrowsWidth > maxWidth)
maxWidth = optionWidths[i] + arrowsWidth;
}
// Handle input
if (alertBannerOptions > 0) {
// respond to input
if (inEvent == INPUT_BROKER_UP || inEvent == INPUT_BROKER_ALT_PRESS) {
curSelected--;
} else if (inEvent == INPUT_BROKER_DOWN || inEvent == INPUT_BROKER_USER_PRESS) {
@@ -120,113 +112,176 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
} else if ((inEvent == INPUT_BROKER_CANCEL || inEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) {
alertBannerMessage[0] = '\0';
}
if (curSelected == -1)
curSelected = alertBannerOptions - 1;
if (curSelected == alertBannerOptions)
curSelected = 0;
// compare number of options to number of lines
if (lineCount < alertBannerOptions)
return;
firstOption = lineCount - alertBannerOptions;
if (curSelected > 1 && alertBannerOptions > 3) {
firstOptionToShow = curSelected + firstOption - 1;
// put the selected option in the middle
} else {
firstOptionToShow = firstOption;
}
} else { // not in an alert with a callback
// TODO: check that at least a second has passed since the alert started
} else {
if (inEvent == INPUT_BROKER_SELECT || inEvent == INPUT_BROKER_ALT_LONG || inEvent == INPUT_BROKER_CANCEL) {
alertBannerMessage[0] = '\0'; // end the alert early
alertBannerMessage[0] = '\0';
}
}
inEvent = INPUT_BROKER_NONE;
if (alertBannerMessage[0] == '\0')
return;
// set width from longest line
uint16_t boxWidth = padding * 2 + maxWidth;
if (needs_bell) {
if (SCREEN_WIDTH > 128 && boxWidth <= 150) {
boxWidth += 26;
}
if (SCREEN_WIDTH <= 128 && boxWidth <= 100) {
boxWidth += 20;
uint16_t totalLines = lineCount + alertBannerOptions;
uint16_t screenHeight = display->height();
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
uint8_t visibleTotalLines = std::min<uint8_t>(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight);
uint8_t linesShown = lineCount;
const char *linePointers[visibleTotalLines]; // this is sort of a dynamic allocation
// 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
uint16_t boxHeight;
if (lineCount <= 4) {
boxHeight = vPadding * 2 + lineCount * FONT_HEIGHT_SMALL + (lineCount - 1) * lineSpacing;
for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
if (i == curSelected) {
strncpy(lineBuffer, "> ", 3);
strncpy(lineBuffer + 2, optionsArrayPtr[i], 36);
strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3);
lineBuffer[39] = '\0';
linePointers[linesShown] = lineBuffer;
} else {
linePointers[linesShown] = optionsArrayPtr[i];
}
}
if (alertBannerOptions > 0) {
drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow, maxWidth);
} 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 boxTop = (display->height() / 2) - (boxHeight / 2);
// === Draw background box ===
// === Draw Box ===
display->setColor(BLACK);
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Slightly oversized box
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); // Top Line
display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); // Bottom Line
display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); // Left Line
display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); // Right Line
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2);
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1);
display->fillRect(boxLeft - 2, boxTop, 1, boxHeight);
display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
display->setColor(WHITE);
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); // Border
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
display->setColor(BLACK);
display->fillRect(boxLeft, boxTop, 1, 1); // Top Left
display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); // Top Right
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); // Bottom Left
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); // Bottom Right
display->fillRect(boxLeft, boxTop, 1, 1);
display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1);
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
display->setColor(WHITE);
// === Draw each line centered in the box ===
// === Draw Content ===
int16_t lineY = boxTop + vPadding;
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;
if (needs_bell && i == 0) {
int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2;
display->drawXbm(textX - 10, bellY, 8, 8, bell_alert);
display->drawXbm(textX + lineWidths[i] + 2, bellY, 8, 8, bell_alert);
}
display->drawString(textX, lineY, lineBuffer);
lineY += FONT_HEIGHT_SMALL + lineSpacing;
// Determine if this is a pop-up or a pick list
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 "OLEDDisplayUi.h"
#define MAX_LINES 5
namespace graphics
{
@@ -12,13 +13,17 @@ class NotificationRenderer
static char inEvent;
static int8_t curSelected;
static char alertBannerMessage[256];
static uint32_t alertBannerUntil; // 0 is a special case meaning forever
static uint32_t alertBannerUntil; // 0 is a special case meaning forever
static const char **optionsArrayPtr;
static uint8_t alertBannerOptions; // last x lines are seelctable options
static std::function<void(int)> alertBannerCallback;
static bool pauseBanner;
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 drawSSLScreen(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 <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
// External variables
@@ -38,7 +64,7 @@ NodeNum UIRenderer::currentFavoriteNodeNum = 0;
void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps)
{
// Draw satellite image
if (SCREEN_WIDTH > 128) {
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y - 2, imgSatellite_width, imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, y + 1, imgSatellite_width, imgSatellite_height, imgSatellite);
@@ -58,7 +84,7 @@ void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const mesht
} else {
snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites());
}
if (SCREEN_WIDTH > 128) {
if (isHighResolution) {
display->drawString(x + 18, y, textString);
} else {
display->drawString(x + 11, y, textString);
@@ -163,46 +189,6 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y,
}
}
void UIRenderer::drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer,
const meshtastic::PowerStatus *powerStatus)
{
static const uint8_t powerBar[3] = {0x81, 0xBD, 0xBD};
static const uint8_t lightning[8] = {0xA1, 0xA1, 0xA5, 0xAD, 0xB5, 0xA5, 0x85, 0x85};
// Clear the bar area inside the battery image
for (int i = 1; i < 14; i++) {
imgBuffer[i] = 0x81;
}
// Fill with lightning or power bars
if (powerStatus->getIsCharging()) {
memcpy(imgBuffer + 3, lightning, 8);
} else {
for (int i = 0; i < 4; i++) {
if (powerStatus->getBatteryChargePercent() >= 25 * i)
memcpy(imgBuffer + 1 + (i * 3), powerBar, 3);
}
}
// Slightly more conservative scaling based on screen width
int scale = 1;
if (SCREEN_WIDTH >= 200)
scale = 2;
if (SCREEN_WIDTH >= 300)
scale = 2; // Do NOT go higher than 2
// Draw scaled battery image (16 columns × 8 rows)
for (int col = 0; col < 16; col++) {
uint8_t colBits = imgBuffer[col];
for (int row = 0; row < 8; row++) {
if (colBits & (1 << row)) {
display->fillRect(x + col * scale, y + row * scale, scale, scale);
}
}
}
}
// Draw nodes status
void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset,
bool show_total, String additional_words)
@@ -221,19 +207,19 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS)
if (SCREEN_WIDTH > 128) {
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display);
} else {
display->drawFastImage(x, y + 3, 8, 8, imgUser);
}
#else
if (SCREEN_WIDTH > 128) {
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display);
} else {
display->drawFastImage(x, y + 1, 8, 8, imgUser);
}
#endif
int string_offset = (SCREEN_WIDTH > 128) ? 9 : 0;
int string_offset = (isHighResolution) ? 9 : 0;
display->drawString(x + 10 + string_offset, y - 2, usersString);
}
@@ -293,12 +279,14 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
// List of available macro Y positions in order, from top to bottom.
int line = 1; // which slot to use next
std::string usernameStr;
// === 1. Long Name (always try to show first) ===
const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr;
if (username && line < 5) {
if (username) {
usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case
// Print node's long name (e.g. "Backpack Node")
display->drawString(x, getTextPositions(display)[line++], username);
display->drawString(x, getTextPositions(display)[line++], usernameStr.c_str());
}
// === 2. Signal and Hops (combined on one line, if available) ===
@@ -476,7 +464,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
const int margin = 4;
// --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) -----------
#if defined(USE_EINK)
const int iconSize = (SCREEN_WIDTH > 128) ? 16 : 8;
const int iconSize = (isHighResolution) ? 16 : 8;
const int navBarHeight = iconSize + 6;
#else
const int navBarHeight = 0;
@@ -570,15 +558,15 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
} else {
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
}
int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1;
if (SCREEN_WIDTH > 128) {
int yOffset = (isHighResolution) ? 3 : 1;
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
imgSatellite);
}
int xOffset = (SCREEN_WIDTH > 128) ? 6 : 0;
int xOffset = (isHighResolution) ? 6 : 0;
display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine);
} else {
UIRenderer::drawGps(display, 0, getTextPositions(display)[line], gpsStatus);
@@ -602,17 +590,17 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
char chUtilPercentage[10];
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
int chUtil_x = (SCREEN_WIDTH > 128) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5;
int chUtil_x = (isHighResolution) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5;
int chUtil_y = getTextPositions(display)[line] + 3;
int chutil_bar_width = (SCREEN_WIDTH > 128) ? 100 : 50;
int chutil_bar_width = (isHighResolution) ? 100 : 50;
if (!config.bluetooth.enabled) {
chutil_bar_width = (SCREEN_WIDTH > 128) ? 80 : 40;
chutil_bar_width = (isHighResolution) ? 80 : 40;
}
int chutil_bar_height = (SCREEN_WIDTH > 128) ? 12 : 7;
int extraoffset = (SCREEN_WIDTH > 128) ? 6 : 3;
int chutil_bar_height = (isHighResolution) ? 12 : 7;
int extraoffset = (isHighResolution) ? 6 : 3;
if (!config.bluetooth.enabled) {
extraoffset = (SCREEN_WIDTH > 128) ? 6 : 1;
extraoffset = (isHighResolution) ? 6 : 1;
}
int chutil_percent = airTime->channelUtilizationPercent();
@@ -672,21 +660,20 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
// === Fourth & Fifth Rows: Node Identity ===
int textWidth = 0;
int nameX = 0;
int yOffset = (SCREEN_WIDTH > 128) ? 0 : 5;
int yOffset = (isHighResolution) ? 0 : 5;
const char *longName = nullptr;
std::string longNameStr;
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) {
longName = ourNode->user.long_name;
longNameStr = sanitizeString(ourNode->user.long_name);
}
uint8_t dmac[6];
char shortnameble[35];
getMacAddr(dmac);
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
snprintf(shortnameble, sizeof(shortnameble), "%s",
graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : "");
char combinedName[50];
snprintf(combinedName, sizeof(combinedName), "%s (%s)", longName, shortnameble);
snprintf(combinedName, sizeof(combinedName), "%s (%s)", longNameStr.empty() ? "" : longNameStr.c_str(), shortnameble);
if (SCREEN_WIDTH - (display->getStringWidth(longName) + display->getStringWidth(shortnameble)) > 10) {
size_t len = strlen(combinedName);
if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) {
@@ -700,7 +687,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
// === LongName Centered ===
textWidth = display->getStringWidth(longName);
nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], longName);
display->drawString(nameX, getTextPositions(display)[line++], longNameStr.c_str());
// === ShortName Centered ===
textWidth = display->getStringWidth(shortnameble);
@@ -808,44 +795,42 @@ void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState
{
LOG_DEBUG("Draw screensaver overlay");
EINK_ADD_FRAMEFLAG(display, COSMETIC); // Take the opportunity for a full-refresh
EINK_ADD_FRAMEFLAG(display, COSMETIC); // Full refresh for screensaver
// Config
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT);
const char *pauseText = "Screen Paused";
const char *idText = owner.short_name;
const bool useId = haveGlyphs(idText); // This bool is used to hide the idText box if we can't render the short name
constexpr uint16_t padding = 5;
const bool useId = haveGlyphs(idText);
constexpr uint8_t padding = 2;
constexpr uint8_t dividerGap = 1;
constexpr uint8_t imprecision = 5; // How far the box origins can drift from center. Combat burn-in.
// Dimensions
const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true); // "true": handle utf8 chars
// Text widths
const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true);
const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText));
const uint16_t boxWidth = padding + (useId ? idTextWidth + padding + padding : 0) + pauseTextWidth + padding;
const uint16_t boxHeight = padding + FONT_HEIGHT_SMALL + padding;
const uint16_t boxWidth = padding + (useId ? idTextWidth + padding : 0) + pauseTextWidth + padding;
const uint16_t boxHeight = FONT_HEIGHT_SMALL + (padding * 2);
// Position
const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2) + random(-imprecision, imprecision + 1);
// const int16_t boxRight = boxLeft + boxWidth - 1;
const int16_t boxTop = (display->height() / 2) - (boxHeight / 2 + random(-imprecision, imprecision + 1));
const int16_t boxBottom = boxTop + boxHeight - 1;
// Flush with bottom
const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
const int16_t boxTop = display->height() - boxHeight;
const int16_t boxBottom = display->height() - 1;
const int16_t idTextLeft = boxLeft + padding;
const int16_t idTextTop = boxTop + padding;
const int16_t pauseTextLeft = boxLeft + (useId ? padding + idTextWidth + padding : 0) + padding;
const int16_t pauseTextLeft = boxLeft + (useId ? idTextWidth + (padding * 2) : 0) + padding;
const int16_t pauseTextTop = boxTop + padding;
const int16_t dividerX = boxLeft + padding + idTextWidth + padding;
const int16_t dividerTop = boxTop + 1 + dividerGap;
const int16_t dividerBottom = boxBottom - 1 - dividerGap;
const int16_t dividerTop = boxTop + dividerGap;
const int16_t dividerBottom = boxBottom - dividerGap;
// Draw: box
display->setColor(EINK_WHITE);
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Clear a slightly oversized area for the box
display->fillRect(boxLeft, boxTop, boxWidth, boxHeight);
display->setColor(EINK_BLACK);
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
// Draw: Text
// Draw: text
if (useId)
display->drawString(idTextLeft, idTextTop, idText);
display->drawString(pauseTextLeft, pauseTextTop, pauseText);
@@ -920,15 +905,15 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
} else {
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
}
int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1;
if (SCREEN_WIDTH > 128) {
int yOffset = (isHighResolution) ? 3 : 1;
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
imgSatellite);
}
int xOffset = (SCREEN_WIDTH > 128) ? 6 : 0;
int xOffset = (isHighResolution) ? 6 : 0;
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
} else {
UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus);
@@ -1114,18 +1099,6 @@ void UIRenderer::drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *sta
#endif
// Function overlay for showing mute/buzzer modifiers etc.
void UIRenderer::drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state)
{
// LOG_DEBUG("Draw function overlay");
if (functionSymbol.begin() != functionSymbol.end()) {
char buf[64];
display->setFont(FONT_SMALL);
snprintf(buf, sizeof(buf), "%s", functionSymbolString.c_str());
display->drawString(SCREEN_WIDTH - display->getStringWidth(buf), SCREEN_HEIGHT - FONT_HEIGHT_SMALL, buf);
}
}
// Navigation bar overlay implementation
static int8_t lastFrameIndex = -1;
static uint32_t lastFrameChangeTime = 0;
@@ -1141,10 +1114,9 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
lastFrameChangeTime = millis();
}
const bool useBigIcons = (SCREEN_WIDTH > 128);
const int iconSize = useBigIcons ? 16 : 8;
const int spacing = useBigIcons ? 8 : 4;
const int bigOffset = useBigIcons ? 1 : 0;
const int iconSize = isHighResolution ? 16 : 8;
const int spacing = isHighResolution ? 8 : 4;
const int bigOffset = isHighResolution ? 1 : 0;
const size_t totalIcons = screen->indicatorIcons.size();
if (totalIcons == 0)
@@ -1158,14 +1130,35 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing;
const int xStart = (SCREEN_WIDTH - totalWidth) / 2;
// Only show bar briefly after switching frames (unless on E-Ink)
// Only show bar briefly after switching frames
static uint32_t navBarLastShown = 0;
static bool cosmeticRefreshDone = false;
bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS;
int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT;
#if defined(USE_EINK)
int y = SCREEN_HEIGHT - iconSize - 1;
#else
int y = SCREEN_HEIGHT - iconSize - 1;
if (millis() - lastFrameChangeTime > ICON_DISPLAY_DURATION_MS) {
y = SCREEN_HEIGHT;
static bool navBarPrevVisible = false;
if (navBarVisible && !navBarPrevVisible) {
EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when showing nav bar
cosmeticRefreshDone = false;
navBarLastShown = millis();
}
if (!navBarVisible && navBarPrevVisible) {
EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when hiding nav bar
navBarLastShown = millis(); // Mark when it disappeared
}
if (!navBarVisible && navBarLastShown != 0 && !cosmeticRefreshDone) {
if (millis() - navBarLastShown > 10000) { // 10s after hidden
EINK_ADD_FRAMEFLAG(display, COSMETIC); // One-time ghost cleanup
cosmeticRefreshDone = true;
}
}
navBarPrevVisible = navBarVisible;
#endif
// Pre-calculate bounding rect
@@ -1191,7 +1184,7 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
display->setColor(BLACK);
}
if (useBigIcons) {
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display);
} else {
display->drawXbm(x, y, iconSize, iconSize, icon);

View File

@@ -32,8 +32,6 @@ class UIRenderer
{
public:
// Common UI elements
static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer,
const meshtastic::PowerStatus *powerStatus);
static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus,
int node_offset = 0, bool show_total = true, String additional_words = "");
@@ -49,9 +47,6 @@ class UIRenderer
// Overlay and special screens
static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text);
// Function overlay for showing mute/buzzer modifiers etc.
static void drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
// Navigation bar overlay
static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state);

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,
0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f};
// This image definition is here instead of images.h because it's modified dynamically by the drawBattery function
static uint8_t imgBattery[16] = {0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0xE7, 0x3C};
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || ARCH_PORTDUINO) && \
!defined(DISPLAY_FORCE_SMALL_FONTS)
@@ -45,19 +42,15 @@ const uint8_t imgSF[] PROGMEM = {0xd2, 0xb7, 0xad, 0xbb, 0x92, 0x01, 0xfd, 0xfd,
// === Horizontal battery ===
// Basic battery design and all related pieces
const unsigned char batteryBitmap_h[] PROGMEM = {
0b11111110, 0b00000000, 0b11110000, 0b00000111, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000,
0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000,
0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000,
0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000,
0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000,
0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b11111110, 0b00000000, 0b11110000, 0b00000111};
const unsigned char batteryBitmap_h_bottom[] PROGMEM = {
0b00011110, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001,
0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00011110, 0b00000000};
// This is the left and right bars for the fill in
const unsigned char batteryBitmap_sidegaps_h[] PROGMEM = {
0b11111111, 0b00001111, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b11111111, 0b00001111};
const unsigned char batteryBitmap_h_top[] PROGMEM = {
0b00111100, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000,
0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000,
0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b00111100, 0b00000000};
// Lightning Bolt
const unsigned char lightning_bolt_h[] PROGMEM = {
@@ -280,11 +273,16 @@ const uint8_t bluetoothdisabled[] PROGMEM = {0b11101100, 0b01010100, 0b01001100,
const uint8_t smallbulletpoint[] PROGMEM = {0b00000011, 0b00000011, 0b00000000, 0b00000000,
0b00000000, 0b00000000, 0b00000000, 0b00000000};
// Clock
#define icon_clock_width 8
#define icon_clock_height 8
const uint8_t icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101, 0b10101001,
0b10010001, 0b10000001, 0b01000010, 0b00111100};
// Digital Clock
#define digital_icon_clock_width 8
#define digital_icon_clock_height 8
const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101, 0b10101001,
0b10010001, 0b10000001, 0b01000010, 0b00111100};
// Analog Clock
#define analog_icon_clock_width 8
#define analog_icon_clock_height 8
const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000,
0b00100100, 0b01000010, 0b01000010, 0b11111111};
#include "img/icon.xbm"
static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning");

View File

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

View File

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

View File

@@ -75,7 +75,7 @@ class MeshModule
*/
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 AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp,
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.
updateGUIforNode = info;
// powerFSM.trigger(EVENT_NODEDB_UPDATED); This event has been retired
sortMeshDB();
notifyObservers(true); // Force an update whether or not our node counts have changed
}
saveNodeDatabaseToDisk();
@@ -1669,6 +1670,31 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
info->has_hops_away = true;
info->hops_away = mp.hop_start - mp.hop_limit;
}
sortMeshDB();
}
}
void NodeDB::sortMeshDB()
{
if (!Throttle::isWithinTimespanMs(lastSort, 1000 * 5)) {
lastSort = millis();
std::sort(meshNodes->begin(), meshNodes->end(), [](const meshtastic_NodeInfoLite &a, const meshtastic_NodeInfoLite &b) {
if (a.num == myNodeInfo.my_node_num) {
return true;
}
if (b.num == myNodeInfo.my_node_num) {
return false;
}
bool aFav = a.is_favorite;
bool bFav = b.is_favorite;
if (aFav != bFav)
return aFav;
if (a.last_heard == 0 || a.last_heard == UINT32_MAX)
return false;
if (b.last_heard == 0 || b.last_heard == UINT32_MAX)
return true;
return a.last_heard > b.last_heard;
});
}
}

View File

@@ -282,6 +282,7 @@ class NodeDB
bool duplicateWarned = false;
uint32_t lastNodeDbSave = 0; // when we last saved our db to flash
uint32_t lastBackupAttempt = 0; // when we last tried a backup automatically or manually
uint32_t lastSort = 0; // When last sorted the nodeDB
/// Find a node in our DB, create an empty NodeInfoLite if missing
meshtastic_NodeInfoLite *getOrCreateMeshNode(NodeNum n);
@@ -310,6 +311,7 @@ class NodeDB
bool saveChannelsToDisk();
bool saveDeviceStateToDisk();
bool saveNodeDatabaseToDisk();
void sortMeshDB();
};
extern NodeDB *nodeDB;

View File

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

View File

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

View File

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

View File

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

View File

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

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);
}
if (decoded->variant.environment_metrics.has_barometric_pressure) {
msgPayload["barometric_pressure"] = new JSONValue(decoded->variant.environment_metrics.barometric_pressure);
msgPayload["barometric_pressure"] =
new JSONValue(decoded->variant.environment_metrics.barometric_pressure);
}
if (decoded->variant.environment_metrics.has_gas_resistance) {
msgPayload["gas_resistance"] = new JSONValue(decoded->variant.environment_metrics.gas_resistance);
@@ -125,13 +126,16 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard);
}
if (decoded->variant.air_quality_metrics.has_pm10_environmental) {
msgPayload["pm10_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental);
msgPayload["pm10_e"] =
new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental);
}
if (decoded->variant.air_quality_metrics.has_pm25_environmental) {
msgPayload["pm25_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental);
msgPayload["pm25_e"] =
new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental);
}
if (decoded->variant.air_quality_metrics.has_pm100_environmental) {
msgPayload["pm100_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental);
msgPayload["pm100_e"] =
new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental);
}
} else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
if (decoded->variant.power_metrics.has_ch1_voltage) {

View File

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

View File

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