2.7 Miscellaneous Fixes - Week 1 (#7102)

* Update Favorite Node Message Options to unify against other screens

* Rebuild Horizontal Battery, Resolve overlap concerns

* Update positioning on Message frame and fix drawCommonHeader overlay

* Beginnings of creating isHighResolution bool

* Fixup determineResolution()

* Implement isHighResolution in place of SCREEN_WIDTH > 128 checks

* Line Spacing bound to isHighResolution

* Analog Clock for all

* Add AM/PM to Analog Clock if isHighResolution and not TWatch

* Simple Menu Queue, and add time menu

* Fix prompt string for 12/24 hour picker

* More menu banners into functions

* Fix Action Menu on Home frame

* Correct pop-up calculation size and continue to leverage isHighResolution

* Move menu bits to MenuHandler

* Plumb in the digital/analog picker

* Correct Clock Face Picker title

* Clock picker fixes

* Migrate the rest of the menus to MenuHandler.*

* Add compass menu and needle point option

* Minor fix for compass point menu

* Correct Home menu into typical format

* Fix emoji bounce, overlap, and missing commonHeader

* Sanitize long_names and removed unused variables

* Slightly better sanitizeString variation

* Resolved apostrophe being shown as upside down question mark

* Gotta keep height and width in expected order

* Remove Second Hand for Analog Clock on EInk displays

* Fix Clock menu option decision tree

* Improvements to Eink Navigation

* Pause Banner for Eink moved to bottom

* Updated working for 12-/24-hour menu and Added US/Arizona to timezone picker

* Add Adhoc Ping and resolve error with std::string sanitized

* Hide quick toggle as option is available within Action Menu, commented out for the moment

* Remove old battery icon and option, use drawCommonHeader throughout, re-add battery to Clock frames

* fix misc build warnings. NFC

* Update Analog Clock on EInk to show more digits

* Establish Action Menu on all node list screens, add NodeDB reset (with confirmation) option

* Add Toggle Backlight for EInk Displays

* Suppress action screen Full refresh for Eink

* Adjust drawBluetoothConnectedIcon on TWatch

* Maintain clock frame when switching between Clock Faces

* Move modules beyond the clock in navigation

* addressed the conflicts, and changed target branch to 2.7-MiscFixes-Week1

* cleanup, cheers

* Add AM/PM to low resolution clock also

* Small adjustments to AM/PM replacement across various devices

* Resolve dangling pointer issues with sanitize code

* Update comments for Screen.cpp related to module load change

* Trunk runs

* Update message caching to correct aged timestamp

* Menu wording adjustments

* Time Format wording

* Use all the rows on EInk since with autohide the navigation bar

* Finalize Time Format picker word change

* Retired drawFunctionOverlay code

No longer being used

* Actually honor the points-north setting

* Trunk

* Compressed action list

* Update no-op showOverlayBanner function

* trunk

* Correct T_Watch_S3 specific line

* Autosized Action menu per screen

* Finalize Autosized Action menu per screen

* Unify Message Titles

* Reorder Timezones to match expectations

* Adjust text location for pop-ups

* Revert "Actually honor the points-north setting"

This reverts commit 20988aa4fabb0975be644989d556fca7e1176680.

* Make NodeDB sort its internal vector when lastheard is updated. Don't sort in NodeListRenderer

* Update src/graphics/draw/NodeListRenderer.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/mesh/NodeDB.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Pass by reference -- Thanks Copilot!

* Throttle sorting just a touch

* Check more carefully for own node

* Eliminate some now-unneeded sorting

* Move function after include

* Putting Modules back to position 0 and some trunk checks found

* Add Scrollbar for Action menus

* Second attempt to move modules down the navigation bar

* Continue effort of moving modules in the navigation

* Canned Messages tweak

* Replicate Function + Space through the Menu System

* Move init button parameters into config struct (#7145)

* Remove bundling of web-ui from ESP32 devices (#7143)

* Fixed triple click GPS toggle bungle

* Move init button parameters into config struct

* Reapply "Actually honor the points-north setting"

This reverts commit 42c1967e7b3735ec9f5be8acd9582bc9edcbc78a.

* Actually do compass pointings correctly

* Tweak to node bearings

* Menu wording tweaks

* Get the compass_north_top logic right

* Don't jump frames after setting Compass

* Get rid of the extra bearingTo functions

* Don't blink Mail on EInk Clock Screens

* Actually set lat and long

* Calibrate

* Convert Radians to Degrees

* More degree vs radians fixes

* De-duplicate draw arrow function

* Don't advertise compass calibration without an accell thread.

---------

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