2.7 fixes w2 (#7148)

* Initial work on splitting notification renderer into components for reuse

* More progress

* Fix notification popup

* more fix, less crash

* Adjustments for OLED on keeping menus tidy, added Bluetooth Toggle to Home frame. Also widen the frame slightly if you have a scroll bar

* Small changes for EInk to not crowd elements

* Change System frame menu over to better match actions; added color picker for T114

* Fix build errors and add T190 for testing

* Logic gates are hard sometimes

* Screen Color Picker changes, defined Yellow as a Color.

* Additional colors and tuning

* Abandon std::sort in NodeDB, and associated fixes (#7175)

* Generate short name for nodes that don't have user yet

* Add reboot menu

* Sort fixes

* noop sort option to avoid infinite loop

* Refactor Overlay Banner

* Continuing work on Color Picker

* Add BaseUI menus to add and remove Favorited Nodes

* Create TFT_MESH_OVERRIDE for variants.h and defined colors

* Trigger a NodeStatus update at the end of setup() to get fresh data on display at boot.

* T114 defaults to White, Yellow is now bright Yellow

* Revert "T114 defaults to White, Yellow is now bright Yellow"

This reverts commit 8d05e17f11.

* Only show OEM text if not OLED

* Adjust OEM logo to maximize visible area

* Start plumbing in Color Picker changes

* Finished plumbing

* Fix warning

* Revert "Fix warning"

This reverts commit 2e8aecd52d.

* Fix display not fully redrawing

* T-Deck should get color too

* Emote Revamp

* Update emotes.cpp

* Poo Emote fix

* Trunk fix

* Add secret test menu and number picker

* Missed bits

* Save colors between reboots

* Save Clock Face election to protobuf

* Make reboot first, then settings

* Add padding for single line pop-ups

* Compass saving and faster menus

* Resolve build issue with Excluding GPS

* Resolve issue with memory bars on EInk

* Add brightness settings for supported screen (#7182)

* Add brightness menu.

* add loop destination selection.

* Bring back color (and sanity) to the menus!

* Trunk

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Jason P <applewiz@mac.com>
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
Co-authored-by: Wilson <m.tools@qq.com>
This commit is contained in:
Jonathan Bennett
2025-07-02 20:50:49 -05:00
committed by GitHub
parent 3fdefe8289
commit a6be2e46ed
35 changed files with 1441 additions and 422 deletions

View File

@@ -21,7 +21,6 @@ namespace graphics
namespace ClockRenderer
{
bool digitalWatchFace = true;
void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
{

View File

@@ -11,8 +11,6 @@ class Screen;
namespace ClockRenderer
{
// Whether we are showing the digital watch face or the analog one
extern bool digitalWatchFace;
// Clock frame functions
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);

View File

@@ -50,7 +50,7 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY,
radius += 4;
}
Point north(0, -radius);
if (!config.display.compass_north_top)
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
north.rotate(-myHeading);
north.translate(compassX, compassY);

View File

@@ -501,7 +501,10 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
int line = 1;
const int barHeight = 6;
const int labelX = x;
const int barsOffset = (isHighResolution) ? 24 : 0;
int barsOffset = (isHighResolution) ? 24 : 0;
#ifdef USE_EINK
barsOffset -= 12;
#endif
const int barX = x + 40 + barsOffset;
auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) {

View File

@@ -8,14 +8,19 @@
#include "NodeDB.h"
#include "buzz.h"
#include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/UIRenderer.h"
#include "main.h"
#include "modules/AdminModule.h"
#include "modules/CannedMessageModule.h"
extern uint16_t TFT_MESH;
namespace graphics
{
menuHandler::screenMenus menuHandler::menuQueue = menu_none;
bool test_enabled = false;
uint8_t test_count = 0;
void menuHandler::LoraRegionPicker(uint32_t duration)
{
@@ -44,72 +49,92 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
"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);
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Set the LoRa region";
bannerOptions.durationMs = duration;
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 23;
bannerOptions.InitialSelected = 0;
bannerOptions.bannerCallback = [](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;
}
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);
}
} else {
LOG_INFO("Generate new PKI keys");
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
keygenSuccess = true;
}
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
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);
}
service->reloadConfig(SEGMENT_CONFIG);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
}
},
0);
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);
}
};
screen->showOverlayBanner(bannerOptions);
}
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) {
enum optionsNumbers { Back = 0, twelve = 1, twentyfour = 2 };
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Time Format";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 3;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
menuHandler::menuQueue = menuHandler::clock_menu;
} else if (selected == 1) {
screen->runNow();
} else if (selected == twelve) {
config.display.use_12h_clock = true;
} else {
config.display.use_12h_clock = false;
}
service->reloadConfig(SEGMENT_CONFIG);
});
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::ClockFacePicker()
{
static const char *optionsArray[] = {"Back", "Digital", "Analog"};
screen->showOverlayBanner("Which Face?", 30000, optionsArray, 3, [](int selected) -> void {
if (selected == 0) {
enum optionsNumbers { Back = 0, Digital = 1, Analog = 2 };
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Which Face?";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 3;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
menuHandler::menuQueue = menuHandler::clock_menu;
} else if (selected == 1) {
graphics::ClockRenderer::digitalWatchFace = true;
screen->runNow();
} else if (selected == Digital) {
uiconfig.is_clockface_analog = false;
nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uiconfig);
screen->setFrames(Screen::FOCUS_CLOCK);
} else {
graphics::ClockRenderer::digitalWatchFace = false;
uiconfig.is_clockface_analog = true;
nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uiconfig);
screen->setFrames(Screen::FOCUS_CLOCK);
}
});
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::TZPicker()
@@ -133,9 +158,14 @@ void menuHandler::TZPicker()
"AU/ACST",
"AU/AEST",
"Pacific/NZ"};
screen->showOverlayBanner("Pick Timezone", 30000, optionsArray, 17, [](int selected) -> void {
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Pick Timezone";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 17;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 0) {
menuHandler::menuQueue = menuHandler::clock_menu;
screen->runNow();
} else if (selected == 1) { // Hawaii
strncpy(config.device.tzdef, "HST10", sizeof(config.device.tzdef));
} else if (selected == 2) { // Alaska
@@ -175,27 +205,31 @@ void menuHandler::TZPicker()
setenv("TZ", config.device.tzdef, 1);
service->reloadConfig(SEGMENT_CONFIG);
}
});
};
screen->showOverlayBanner(bannerOptions);
}
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) {
enum optionsNumbers { Back = 0, Clock = 1, Time = 2, Timezone = 3 };
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Clock Action";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 4;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Clock) {
menuHandler::menuQueue = menuHandler::clock_face_picker;
screen->setInterval(0);
runASAP = true;
} else if (selected == 2) {
screen->runNow();
} else if (selected == Time) {
menuHandler::menuQueue = menuHandler::twelve_hour_picker;
screen->setInterval(0);
runASAP = true;
} else if (selected == 3) {
screen->runNow();
} else if (selected == Timezone) {
menuHandler::menuQueue = menuHandler::TZ_picker;
screen->setInterval(0);
runASAP = true;
screen->runNow();
}
});
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::messageResponseMenu()
@@ -203,6 +237,7 @@ void menuHandler::messageResponseMenu()
static const char **optionsArrayPtr;
int options;
enum optionsNumbers { Back = 0, Dismiss = 1, Preset = 2, Freetext = 3 };
if (kb_found) {
static const char *optionsArray[] = {"Back", "Dismiss", "Reply via Preset", "Reply via Freetext"};
optionsArrayPtr = optionsArray;
@@ -217,16 +252,20 @@ void menuHandler::messageResponseMenu()
optionsArrayPtr = optionsArray;
options = 5;
#endif
screen->showOverlayBanner("Message Action", 30000, optionsArrayPtr, options, [](int selected) -> void {
if (selected == 1) {
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Message Action";
bannerOptions.optionsArrayPtr = optionsArrayPtr;
bannerOptions.optionsCount = options;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Dismiss) {
screen->dismissCurrentFrame();
} else if (selected == 2) {
} else if (selected == Preset) {
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) {
} else if (selected == Freetext) {
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
} else {
@@ -241,51 +280,138 @@ void menuHandler::messageResponseMenu()
audioThread->readAloud(msg);
}
#endif
});
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::homeBaseMenu()
{
int options;
static const char **optionsArrayPtr;
enum optionsNumbers { Back, Backlight, Position, Preset, Freetext, Bluetooth, Sleep };
static const char *optionsArray[6] = {"Back"};
static int optionsEnumArray[6] = {Back};
int options = 1;
#ifdef PIN_EINK_EN
optionsArray[options] = "Toggle Backlight";
optionsEnumArray[options++] = Backlight;
#else
optionsArray[options] = "Sleep Screen";
optionsEnumArray[options++] = Sleep;
#endif
optionsArray[options] = "Send Position";
optionsEnumArray[options++] = Position;
optionsArray[options] = "New Preset Msg";
optionsEnumArray[options++] = Preset;
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;
optionsArray[options] = "New Freetext Msg";
optionsEnumArray[options++] = Freetext;
}
screen->showOverlayBanner("Home Action", 30000, optionsArrayPtr, options, [](int selected) -> void {
if (selected == 1) {
optionsArray[options] = "Bluetooth Toggle";
optionsEnumArray[options++] = Bluetooth;
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Home Action";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Backlight) {
#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};
} else if (selected == Sleep) {
screen->setOn(false);
} else if (selected == Position) {
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SEND_PING, .kbchar = 0, .touchX = 0, .touchY = 0};
inputBroker->injectInputEvent(&event);
} else if (selected == 3) {
} else if (selected == Preset) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
} else if (selected == 4) {
} else if (selected == Freetext) {
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST);
} else if (selected == Bluetooth) {
InputEvent event = {.inputEvent = (input_broker_event)170, .kbchar = 170, .touchX = 0, .touchY = 0};
inputBroker->injectInputEvent(&event);
}
});
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::systemBaseMenu()
{
// Check if brightness is supported
bool hasSupportBrightness = false;
#if defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || HAS_TFT
hasSupportBrightness = true;
#endif
enum optionsNumbers { Back, Beeps, Brightness, Reboot, Color, MUI, Test };
static const char *optionsArray[6] = {"Back"};
static int optionsEnumArray[6] = {Back};
int options = 1;
optionsArray[options] = "Beeps Action";
optionsEnumArray[options++] = Beeps;
if (hasSupportBrightness) {
optionsArray[options] = "Brightness";
optionsEnumArray[options++] = Brightness;
}
optionsArray[options] = "Reboot";
optionsEnumArray[options++] = Reboot;
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT
optionsArray[options] = "Screen Color";
optionsEnumArray[options++] = Color;
#endif
#if HAS_TFT
optionsArray[options] = "Switch to MUI";
optionsEnumArray[options++] = MUI;
#endif
if (test_enabled) {
optionsArray[options] = "Test Menu";
optionsEnumArray[options++] = Test;
}
BannerOverlayOptions bannerOptions;
bannerOptions.message = "System Action";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = options;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Beeps) {
menuHandler::menuQueue = menuHandler::buzzermodemenupicker;
screen->runNow();
} else if (selected == Brightness) {
menuHandler::menuQueue = menuHandler::brightness_picker;
screen->runNow();
} else if (selected == Reboot) {
menuHandler::menuQueue = menuHandler::reboot_menu;
screen->runNow();
} else if (selected == MUI) {
menuHandler::menuQueue = menuHandler::mui_picker;
screen->runNow();
} else if (selected == Color) {
menuHandler::menuQueue = menuHandler::tftcolormenupicker;
screen->runNow();
} else if (selected == Test) {
menuHandler::menuQueue = menuHandler::test_menu;
screen->runNow();
} else if (selected == Back && !test_enabled) {
test_count++;
if (test_count > 4) {
test_enabled = true;
}
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::favoriteBaseMenu()
@@ -294,21 +420,29 @@ void menuHandler::favoriteBaseMenu()
static const char **optionsArrayPtr;
if (kb_found) {
static const char *optionsArray[] = {"Back", "New Preset Msg", "New Freetext Msg"};
static const char *optionsArray[] = {"Back", "New Preset Msg", "New Freetext Msg", "Remove Favorite"};
optionsArrayPtr = optionsArray;
options = 4;
} else {
static const char *optionsArray[] = {"Back", "New Preset Msg", "Remove Favorite"};
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 {
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Favorites Action";
bannerOptions.optionsArrayPtr = optionsArrayPtr;
bannerOptions.optionsCount = options;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1) {
cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
} else if (selected == 2) {
} else if (selected == 2 && kb_found) {
cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
} else if ((!kb_found && selected == 2) || (selected == 3 && kb_found)) {
menuHandler::menuQueue = menuHandler::remove_favorite;
screen->runNow();
}
});
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::positionBaseMenu()
@@ -325,127 +459,385 @@ void menuHandler::positionBaseMenu()
optionsArrayPtr = optionsArray;
options = 3;
}
screen->showOverlayBanner("Position Action", 30000, optionsArrayPtr, options, [](int selected) -> void {
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Position Action";
bannerOptions.optionsArrayPtr = optionsArrayPtr;
bannerOptions.optionsCount = options;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1) {
#if MESHTASTIC_EXCLUDE_GPS
menuQueue = menu_none;
#else
menuQueue = gps_toggle_menu;
screen->runNow();
#endif
} else if (selected == 2) {
menuQueue = compass_point_north_menu;
screen->runNow();
} else if (selected == 3) {
accelerometerThread->calibrate(30);
}
});
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::nodeListMenu()
{
static const char *optionsArray[] = {"Back", "Reset NodeDB"};
screen->showOverlayBanner("Node Action", 30000, optionsArray, 2, [](int selected) -> void {
static const char *optionsArray[] = {"Back", "Add Favorite", "Reset NodeDB"};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Node Action";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 3;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1) {
menuQueue = add_favorite;
screen->runNow();
} else if (selected == 2) {
menuQueue = reset_node_db_menu;
screen->runNow();
}
});
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::resetNodeDBMenu()
{
static const char *optionsArray[] = {"Back", "Confirm"};
screen->showOverlayBanner("Confirm Reset NodeDB", 30000, optionsArray, 2, [](int selected) -> void {
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Confirm Reset NodeDB";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1) {
disableBluetooth();
LOG_INFO("Initiate node-db reset");
nodeDB->resetNodes();
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
}
});
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::compassNorthMenu()
{
static const char *optionsArray[] = {"Back", "Dynamic", "Fixed Ring", "Freeze Heading"};
screen->showOverlayBanner("North Directions?", 30000, optionsArray, 4, [](int selected) -> void {
BannerOverlayOptions bannerOptions;
bannerOptions.message = "North Directions?";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 4;
bannerOptions.InitialSelected = uiconfig.compass_mode + 1;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1) {
if (config.display.compass_north_top != false) {
config.display.compass_north_top = false;
service->reloadConfig(SEGMENT_CONFIG);
if (uiconfig.compass_mode != meshtastic_CompassMode_DYNAMIC) {
uiconfig.compass_mode = meshtastic_CompassMode_DYNAMIC;
nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg,
&uiconfig);
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
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);
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) {
uiconfig.compass_mode = meshtastic_CompassMode_FIXED_RING;
nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg,
&uiconfig);
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
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);
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) {
uiconfig.compass_mode = meshtastic_CompassMode_FREEZE_HEADING;
nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg,
&uiconfig);
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
screen->ignoreCompass = true;
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
} else if (selected == 0) {
menuQueue = position_base_menu;
screen->runNow();
}
});
};
screen->showOverlayBanner(bannerOptions);
}
#if !MESHTASTIC_EXCLUDE_GPS
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
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Toggle GPS";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 3;
bannerOptions.bannerCallback = [](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;
screen->runNow();
}
};
bannerOptions.InitialSelected = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2;
screen->showOverlayBanner(bannerOptions);
}
#endif
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);
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Beep Action";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 4;
bannerOptions.bannerCallback = [](int selected) -> void {
config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected;
service->reloadConfig(SEGMENT_CONFIG);
};
bannerOptions.InitialSelected = config.device.buzzer_mode;
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::BrightnessPickerMenu()
{
static const char *optionsArray[] = {"Back", "Low", "Medium", "High", "Very High"};
// Get current brightness level to set initial selection
int currentSelection = 1; // Default to Low
if (uiconfig.screen_brightness >= 255) {
currentSelection = 4; // Very High
} else if (uiconfig.screen_brightness >= 128) {
currentSelection = 3; // High
} else if (uiconfig.screen_brightness >= 64) {
currentSelection = 2; // Medium
} else {
currentSelection = 1; // Low
}
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Brightness";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 5;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1) { // Low
uiconfig.screen_brightness = 1;
} else if (selected == 2) { // Medium
uiconfig.screen_brightness = 64;
} else if (selected == 3) { // High
uiconfig.screen_brightness = 128;
} else if (selected == 4) { // Very High
uiconfig.screen_brightness = 255;
}
if (selected != 0) { // Not "Back"
// Apply brightness immediately
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(HELTEC_VISION_MASTER_E213) || \
defined(HELTEC_VISION_MASTER_E290)
// For HELTEC devices, use analogWrite to control backlight
analogWrite(VTFT_LEDA, uiconfig.screen_brightness);
#elif defined(ST7789_CS)
static_cast<TFTDisplay *>(screen->getDisplayDevice())->setDisplayBrightness(uiconfig.screen_brightness);
#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
screen->getDisplayDevice()->setBrightness(uiconfig.screen_brightness);
#endif
// Save to device
nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uiconfig);
LOG_INFO("Screen brightness set to %d", uiconfig.screen_brightness);
}
};
bannerOptions.InitialSelected = currentSelection;
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::switchToMUIMenu()
{
static const char *optionsArray[] = {"Yes", "No"};
screen->showOverlayBanner("Switch to MUI?", 30000, optionsArray, 2, [](int selected) -> void {
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Switch to MUI?";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](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);
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
{
static const char *optionsArray[] = {"Back", "Default", "Meshtastic Green", "Yellow", "Red", "Orange", "Purple", "Teal",
"Pink", "White"};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Select Screen Color";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 10;
bannerOptions.bannerCallback = [display](int selected) -> void {
uint8_t r = 0;
uint8_t g = 0;
uint8_t b = 0;
if (selected == 1) {
LOG_INFO("Setting color to system default or defined variant");
// Given just before we set all these to zero, we will allow this to go through
} else if (selected == 2) {
LOG_INFO("Setting color to Meshtastic Green");
r = 103;
g = 234;
b = 148;
} else if (selected == 3) {
LOG_INFO("Setting color to Yellow");
r = 255;
g = 255;
b = 128;
} else if (selected == 4) {
LOG_INFO("Setting color to Red");
r = 255;
g = 64;
b = 64;
} else if (selected == 5) {
LOG_INFO("Setting color to Orange");
r = 255;
g = 160;
b = 20;
} else if (selected == 6) {
LOG_INFO("Setting color to Purple");
r = 204;
g = 153;
b = 255;
} else if (selected == 7) {
LOG_INFO("Setting color to Teal");
r = 64;
g = 224;
b = 208;
} else if (selected == 8) {
LOG_INFO("Setting color to Pink");
r = 255;
g = 105;
b = 180;
} else if (selected == 9) {
LOG_INFO("Setting color to White");
r = 255;
g = 255;
b = 255;
}
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT
if (selected != 0) {
display->setColor(BLACK);
display->fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
display->setColor(WHITE);
if (r == 0 && g == 0 && b == 0) {
#ifdef TFT_MESH_OVERRIDE
TFT_MESH = TFT_MESH_OVERRIDE;
#else
TFT_MESH = COLOR565(0x67, 0xEA, 0x94);
#endif
} else {
TFT_MESH = COLOR565(r, g, b);
}
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190)
static_cast<ST7789Spi *>(screen->getDisplayDevice())->setRGB(TFT_MESH);
#endif
screen->setFrames(graphics::Screen::FOCUS_SYSTEM);
if (r == 0 && g == 0 && b == 0) {
uiconfig.screen_rgb_color = 0;
} else {
uiconfig.screen_rgb_color = (r << 16) | (g << 8) | b;
}
LOG_INFO("Storing Value of %d to uiconfig.screen_rgb_color", uiconfig.screen_rgb_color);
nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uiconfig);
}
#endif
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::rebootMenu()
{
static const char *optionsArray[] = {"Back", "Confirm"};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Reboot Device?";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1) {
IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0));
nodeDB->saveToDisk();
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::addFavoriteMenu()
{
screen->showNodePicker("Node To Favorite", 30000, [](int nodenum) -> void {
LOG_WARN("Nodenum: %u", nodenum);
nodeDB->set_favorite(true, nodenum);
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
});
}
void menuHandler::handleMenuSwitch()
void menuHandler::removeFavoriteMenu()
{
static const char *optionsArray[] = {"Back", "Yes"};
BannerOverlayOptions bannerOptions;
std::string message = "Unfavorite This Node?\n";
auto node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum);
if (node && node->has_user) {
message += sanitizeString(node->user.long_name).substr(0, 15);
}
bannerOptions.message = message.c_str();
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1) {
nodeDB->set_favorite(false, graphics::UIRenderer::currentFavoriteNodeNum);
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::testMenu()
{
static const char *optionsArray[] = {"Back", "Number Picker"};
BannerOverlayOptions bannerOptions;
std::string message = "Test to Run?\n";
bannerOptions.message = message.c_str();
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1) {
menuQueue = number_test;
screen->runNow();
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::numberTest()
{
screen->showNumberPicker("Pick a number\n ", 30000, 4,
[](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); });
}
void menuHandler::handleMenuSwitch(OLEDDisplay *display)
{
if (menuQueue != menu_none)
test_count = 0;
switch (menuQueue) {
case menu_none:
break;
@@ -478,6 +870,33 @@ void menuHandler::handleMenuSwitch()
case reset_node_db_menu:
resetNodeDBMenu();
break;
case buzzermodemenupicker:
BuzzerModeMenu();
break;
case mui_picker:
switchToMUIMenu();
break;
case tftcolormenupicker:
TFTColorPickerMenu(display);
break;
case brightness_picker:
BrightnessPickerMenu();
break;
case reboot_menu:
rebootMenu();
break;
case add_favorite:
addFavoriteMenu();
break;
case remove_favorite:
removeFavoriteMenu();
break;
case test_menu:
testMenu();
break;
case number_test:
numberTest();
break;
}
menuQueue = menu_none;
}

View File

@@ -17,26 +17,43 @@ class menuHandler
gps_toggle_menu,
#endif
compass_point_north_menu,
reset_node_db_menu
reset_node_db_menu,
buzzermodemenupicker,
mui_picker,
tftcolormenupicker,
brightness_picker,
reboot_menu,
add_favorite,
remove_favorite,
test_menu,
number_test
};
static screenMenus menuQueue;
static void LoraRegionPicker(uint32_t duration = 30000);
static void handleMenuSwitch();
static void handleMenuSwitch(OLEDDisplay *display);
static void clockMenu();
static void TZPicker();
static void TwelveHourPicker();
static void ClockFacePicker();
static void messageResponseMenu();
static void homeBaseMenu();
static void systemBaseMenu();
static void favoriteBaseMenu();
static void positionBaseMenu();
static void compassNorthMenu();
static void GPSToggleMenu();
static void BuzzerModeMenu();
static void switchToMUIMenu();
static void TFTColorPickerMenu(OLEDDisplay *display);
static void nodeListMenu();
static void resetNodeDBMenu();
static void BrightnessPickerMenu();
static void rebootMenu();
static void addFavoriteMenu();
static void removeFavoriteMenu();
static void testMenu();
static void numberTest();
};
} // namespace graphics

View File

@@ -66,10 +66,10 @@ const char *getSafeNodeName(meshtastic_NodeInfoLite *node)
strncpy(nodeName, name, sizeof(nodeName) - 1);
nodeName[sizeof(nodeName) - 1] = '\0';
} else {
snprintf(nodeName, sizeof(nodeName), "%04X", (uint16_t)(node->num & 0xFFFF));
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
}
} else {
strcpy(nodeName, "?");
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
}
return nodeName;
}
@@ -522,7 +522,7 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
double lat = DegD(ourNode->position.latitude_i);
double lon = DegD(ourNode->position.longitude_i);
if (!screen->ignoreCompass) {
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) {
#if HAS_GPS
if (screen->hasHeading()) {
heading = screen->getHeading(); // degrees

View File

@@ -32,8 +32,21 @@ char NotificationRenderer::alertBannerMessage[256] = {0};
uint32_t NotificationRenderer::alertBannerUntil = 0; // 0 is a special case meaning forever
uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are seelctable options
const char **NotificationRenderer::optionsArrayPtr = nullptr;
const int *NotificationRenderer::optionsEnumPtr = nullptr;
std::function<void(int)> NotificationRenderer::alertBannerCallback = NULL;
bool NotificationRenderer::pauseBanner = false;
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
uint32_t NotificationRenderer::numDigits = 0;
uint32_t NotificationRenderer::currentNumber = 0;
uint32_t pow_of_10(uint32_t n)
{
uint32_t ret = 1;
for (int i = 0; i < n; i++) {
ret *= 10;
}
return ret;
}
// Used on boot when a certificate is being created
void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
@@ -55,29 +68,214 @@ void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiStat
}
}
void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state)
void NotificationRenderer::resetBanner()
{
alertBannerMessage[0] = '\0';
current_notification_type = notificationTypeEnum::none;
nodeDB->pause_sort(false);
}
void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state)
{
if (!isOverlayBannerShowing() || pauseBanner)
return;
switch (current_notification_type) {
case notificationTypeEnum::text_banner:
case notificationTypeEnum::selection_picker:
drawAlertBannerOverlay(display, state);
break;
case notificationTypeEnum::node_picker:
drawNodePicker(display, state);
break;
case notificationTypeEnum::number_picker:
drawNumberPicker(display, state);
break;
}
}
void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state)
{
const char *lineStarts[MAX_LINES + 1] = {0};
uint16_t lineCount = 0;
// Parse lines
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
lineStarts[lineCount] = alertBannerMessage;
// Find lines
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
if (lineStarts[lineCount + 1][0] == '\n')
lineStarts[lineCount + 1] += 1;
lineCount++;
}
// modulo to extract
uint8_t this_digit = (currentNumber % (pow_of_10(numDigits - curSelected))) / (pow_of_10(numDigits - curSelected - 1));
// Handle input
if (inEvent == INPUT_BROKER_UP || inEvent == INPUT_BROKER_ALT_PRESS) {
if (this_digit == 9) {
currentNumber -= 9 * (pow_of_10(numDigits - curSelected - 1));
} else {
currentNumber += (pow_of_10(numDigits - curSelected - 1));
}
} else if (inEvent == INPUT_BROKER_DOWN || inEvent == INPUT_BROKER_USER_PRESS) {
if (this_digit == 0) {
currentNumber += 9 * (pow_of_10(numDigits - curSelected - 1));
} else {
currentNumber -= (pow_of_10(numDigits - curSelected - 1));
}
} else if (inEvent == INPUT_BROKER_SELECT || inEvent == INPUT_BROKER_RIGHT) {
curSelected++;
} else if (inEvent == INPUT_BROKER_LEFT) {
curSelected--;
} else if ((inEvent == INPUT_BROKER_CANCEL || inEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) {
resetBanner();
}
if (curSelected == numDigits) {
resetBanner();
alertBannerCallback(currentNumber);
}
inEvent = INPUT_BROKER_NONE;
if (alertBannerMessage[0] == '\0')
return;
uint16_t totalLines = lineCount + 2;
const char *linePointers[totalLines + 1] = {0}; // this is sort of a dynamic allocation
// copy the linestarts to display to the linePointers holder
for (int i = 0; i < lineCount; i++) {
linePointers[i] = lineStarts[i];
}
std::string digits = " ";
std::string arrowPointer = " ";
for (int i = 0; i < numDigits; i++) {
// Modulo minus modulo to return just the current number
digits += std::to_string((currentNumber % (pow_of_10(numDigits - i))) / (pow_of_10(numDigits - i - 1))) + " ";
if (curSelected == i) {
arrowPointer += "^ ";
} else {
arrowPointer += "_ ";
}
}
linePointers[lineCount++] = digits.c_str();
linePointers[lineCount++] = arrowPointer.c_str();
drawNotificationBox(display, state, linePointers, totalLines, 0);
}
void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state)
{
static uint32_t selectedNodenum = 0;
// === Layout Configuration ===
constexpr uint16_t hPadding = 5;
constexpr uint16_t vPadding = 2;
constexpr uint8_t lineSpacing = 1;
alertBannerOptions = nodeDB->getNumMeshNodes() - 1;
bool needs_bell = (strstr(alertBannerMessage, "Alert Received") != nullptr);
// let the box drawing function calculate the widths?
// Setup font and alignment
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT);
const char *lineStarts[MAX_LINES + 1] = {0};
uint16_t lineCount = 0;
// Parse lines
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
lineStarts[lineCount] = alertBannerMessage;
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
if (lineStarts[lineCount + 1][0] == '\n')
lineStarts[lineCount + 1] += 1;
lineCount++;
}
// Handle input
if (inEvent == INPUT_BROKER_UP || inEvent == INPUT_BROKER_ALT_PRESS) {
curSelected--;
} else if (inEvent == INPUT_BROKER_DOWN || inEvent == INPUT_BROKER_USER_PRESS) {
curSelected++;
} else if (inEvent == INPUT_BROKER_SELECT) {
resetBanner();
alertBannerCallback(selectedNodenum);
} else if ((inEvent == INPUT_BROKER_CANCEL || inEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) {
resetBanner();
}
if (curSelected == -1)
curSelected = alertBannerOptions - 1;
if (curSelected == alertBannerOptions)
curSelected = 0;
inEvent = INPUT_BROKER_NONE;
if (alertBannerMessage[0] == '\0')
return;
uint16_t totalLines = lineCount + alertBannerOptions;
uint16_t screenHeight = display->height();
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
uint8_t visibleTotalLines = std::min<uint8_t>(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight);
uint8_t linesShown = lineCount;
const char *linePointers[visibleTotalLines + 1] = {0}; // this is sort of a dynamic allocation
// copy the linestarts to display to the linePointers holder
for (int i = 0; i < lineCount; i++) {
linePointers[i] = lineStarts[i];
}
char scratchLineBuffer[visibleTotalLines - lineCount][40];
uint8_t firstOptionToShow = 0;
if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) {
if (curSelected > alertBannerOptions - visibleTotalLines + lineCount)
firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount;
else
firstOptionToShow = curSelected - 1;
} else {
firstOptionToShow = 0;
}
int scratchLineNum = 0;
for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
char temp_name[16] = {0};
if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) {
std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name);
strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1);
} else {
snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF));
}
// make temp buffer for name
// fi
if (i == curSelected) {
selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num;
if (isHighResolution) {
strncpy(scratchLineBuffer[scratchLineNum], "> ", 3);
strncpy(scratchLineBuffer[scratchLineNum] + 2, temp_name, 36);
strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 2, " <", 3);
} else {
strncpy(scratchLineBuffer[scratchLineNum], ">", 2);
strncpy(scratchLineBuffer[scratchLineNum] + 1, temp_name, 37);
strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 1, "<", 2);
}
scratchLineBuffer[scratchLineNum][39] = '\0';
} else {
strncpy(scratchLineBuffer[scratchLineNum], temp_name, 36);
}
linePointers[linesShown] = scratchLineBuffer[scratchLineNum++];
}
drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow);
}
void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state)
{
// === Layout Configuration ===
constexpr uint16_t vPadding = 2;
constexpr int MAX_LINES = 5;
uint16_t optionWidths[alertBannerOptions] = {0};
uint16_t maxWidth = 0;
uint16_t arrowsWidth = display->getStringWidth("> <", 4, true);
uint16_t lineWidths[MAX_LINES] = {0};
uint16_t lineLengths[MAX_LINES] = {0};
char *lineStarts[MAX_LINES + 1];
const char *lineStarts[MAX_LINES + 1] = {0};
uint16_t lineCount = 0;
char lineBuffer[40] = {0};
@@ -86,7 +284,7 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
lineStarts[lineCount] = alertBannerMessage;
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
lineStarts[lineCount + 1] = std::find(lineStarts[lineCount], alertEnd, '\n');
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount];
if (lineStarts[lineCount + 1][0] == '\n')
lineStarts[lineCount + 1] += 1;
@@ -112,10 +310,15 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
} else if (inEvent == INPUT_BROKER_DOWN || inEvent == INPUT_BROKER_USER_PRESS) {
curSelected++;
} else if (inEvent == INPUT_BROKER_SELECT) {
alertBannerCallback(curSelected);
alertBannerMessage[0] = '\0';
if (optionsEnumPtr != nullptr) {
alertBannerCallback(optionsEnumPtr[curSelected]);
optionsEnumPtr = nullptr;
} else {
alertBannerCallback(curSelected);
}
resetBanner();
} else if ((inEvent == INPUT_BROKER_CANCEL || inEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) {
alertBannerMessage[0] = '\0';
resetBanner();
}
if (curSelected == -1)
@@ -124,7 +327,7 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
curSelected = 0;
} else {
if (inEvent == INPUT_BROKER_SELECT || inEvent == INPUT_BROKER_ALT_LONG || inEvent == INPUT_BROKER_CANCEL) {
alertBannerMessage[0] = '\0';
resetBanner();
}
}
@@ -132,7 +335,91 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
if (alertBannerMessage[0] == '\0')
return;
// === Box Size Calculation ===
uint16_t totalLines = lineCount + alertBannerOptions;
uint16_t screenHeight = display->height();
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
uint8_t visibleTotalLines = std::min<uint8_t>(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight);
uint8_t linesShown = lineCount;
const char *linePointers[visibleTotalLines + 1] = {0}; // this is sort of a dynamic allocation
// copy the linestarts to display to the linePointers holder
for (int i = 0; i < lineCount; i++) {
linePointers[i] = lineStarts[i];
}
uint8_t firstOptionToShow = 0;
if (alertBannerOptions > 0) {
if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) {
if (curSelected > alertBannerOptions - visibleTotalLines + lineCount)
firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount;
else
firstOptionToShow = curSelected - 1;
} else {
firstOptionToShow = 0;
}
}
for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
if (i == curSelected) {
if (isHighResolution) {
strncpy(lineBuffer, "> ", 3);
strncpy(lineBuffer + 2, optionsArrayPtr[i], 36);
strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3);
} else {
strncpy(lineBuffer, ">", 2);
strncpy(lineBuffer + 1, optionsArrayPtr[i], 37);
strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 1, "<", 2);
}
lineBuffer[39] = '\0';
linePointers[linesShown] = lineBuffer;
} else {
linePointers[linesShown] = optionsArrayPtr[i];
}
}
if (alertBannerOptions > 0) {
drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow, maxWidth);
} else {
drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow);
}
}
void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[],
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth)
{
bool is_picker = false;
uint16_t lineCount = 0;
// === Layout Configuration ===
constexpr uint16_t hPadding = 5;
constexpr uint16_t vPadding = 2;
bool needs_bell = false;
uint16_t lineWidths[totalLines] = {0};
uint16_t lineLengths[totalLines] = {0};
if (maxWidth != 0)
is_picker = true;
// Setup font and alignment
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT);
while (lines[lineCount] != nullptr) {
auto newlinePointer = strchr(lines[lineCount], '\n');
if (newlinePointer)
lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first
else // if the newline wasn't found, then pull string length from strlen
lineLengths[lineCount] = strlen(lines[lineCount]);
lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true);
if (!is_picker) {
needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr);
if (lineWidths[lineCount] > maxWidth)
maxWidth = lineWidths[lineCount];
}
lineCount++;
}
// count lines
uint16_t boxWidth = hPadding * 2 + maxWidth;
if (needs_bell) {
if (isHighResolution && boxWidth <= 150)
@@ -141,14 +428,19 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
boxWidth += 20;
}
uint16_t totalLines = lineCount + alertBannerOptions;
uint16_t screenHeight = display->height();
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
uint8_t visibleTotalLines = std::min<uint8_t>(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight);
uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight);
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
uint16_t boxHeight = contentHeight + vPadding * 2;
if (visibleTotalLines == 1) {
boxHeight += (isHighResolution) ? 4 : 3;
}
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
if (totalLines > visibleTotalLines) {
boxWidth += (isHighResolution) ? 4 : 2;
}
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
// === Draw Box ===
@@ -169,21 +461,18 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
// === Draw Content ===
int16_t lineY = boxTop + vPadding;
uint8_t linesShown = 0;
for (int i = 0; i < lineCount && linesShown < visibleTotalLines; i++, linesShown++) {
strncpy(lineBuffer, lineStarts[i], 40);
lineBuffer[lineLengths[i] > 39 ? 39 : lineLengths[i]] = '\0';
for (int i = 0; i < lineCount; i++) {
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
if (needs_bell && i == 0) {
int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2;
display->drawXbm(textX - 10, bellY, 8, 8, bell_alert);
display->drawXbm(textX + lineWidths[i] + 2, bellY, 8, 8, bell_alert);
}
char lineBuffer[lineLengths[i] + 1];
strncpy(lineBuffer, lines[i], lineLengths[i]);
lineBuffer[lineLengths[i]] = '\0';
// Determine if this is a pop-up or a pick list
if (alertBannerOptions > 0) {
if (alertBannerOptions > 0 && i == 0) {
// Pick List
display->setColor(WHITE);
int background_yOffset = 1;
@@ -199,39 +488,14 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
lineY += (effectiveLineHeight - 2 - background_yOffset);
} else {
// Pop-up
display->drawString(textX, lineY - 2, lineBuffer);
display->drawString(textX, lineY, lineBuffer);
lineY += (effectiveLineHeight);
}
}
uint8_t firstOptionToShow = 0;
if (alertBannerOptions > 0) {
if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount)
firstOptionToShow = curSelected - 1;
else
firstOptionToShow = 0;
}
for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
if (i == curSelected) {
strncpy(lineBuffer, "> ", 3);
strncpy(lineBuffer + 2, optionsArrayPtr[i], 36);
strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3);
lineBuffer[39] = '\0';
} else {
strncpy(lineBuffer, optionsArrayPtr[i], 40);
lineBuffer[39] = '\0';
}
int16_t textX = boxLeft + (boxWidth - optionWidths[i] - (i == curSelected ? arrowsWidth : 0)) / 2;
display->drawString(textX, lineY, lineBuffer);
lineY += 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
@@ -239,7 +503,7 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
float ratio = (float)visibleTotalLines / totalLines;
uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4);
float scrollRatio = (float)(firstOptionToShow + linesShown - visibleTotalLines) / (totalLines - visibleTotalLines);
float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines);
uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight);
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);

View File

@@ -2,6 +2,8 @@
#include "OLEDDisplay.h"
#include "OLEDDisplayUi.h"
#include "graphics/Screen.h"
#define MAX_LINES 5
namespace graphics
{
@@ -14,16 +16,28 @@ class NotificationRenderer
static char alertBannerMessage[256];
static uint32_t alertBannerUntil; // 0 is a special case meaning forever
static const char **optionsArrayPtr;
static const int *optionsEnumPtr;
static uint8_t alertBannerOptions; // last x lines are seelctable options
static std::function<void(int)> alertBannerCallback;
static uint32_t numDigits;
static uint32_t currentNumber;
static bool pauseBanner;
static void resetBanner();
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0);
static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
static bool isOverlayBannerShowing();
static graphics::notificationTypeEnum current_notification_type;
};
} // namespace graphics

View File

@@ -18,32 +18,6 @@
#include <RTC.h>
#include <cstring>
bool isAllowedPunctuation(char c)
{
const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ ";
return allowed.find(c) != std::string::npos;
}
std::string sanitizeString(const std::string &input)
{
std::string output;
bool inReplacement = false;
for (char c : input) {
if (std::isalnum(static_cast<unsigned char>(c)) || isAllowedPunctuation(c)) {
output += c;
inReplacement = false;
} else {
if (!inReplacement) {
output += 0xbf; // ISO-8859-1 for inverted question mark
inReplacement = true;
}
}
}
return output;
}
// External variables
extern graphics::Screen *screen;
@@ -443,7 +417,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
*/
float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
if (screen->ignoreCompass) {
if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) {
myHeading = 0;
} else {
bearing -= myHeading;
@@ -488,7 +462,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
const auto &op = ourNode->position;
float myHeading = 0;
if (!screen->ignoreCompass) {
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) {
myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180
: screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
}
@@ -500,7 +474,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
*/
float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
if (!screen->ignoreCompass)
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING)
bearing -= myHeading;
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing);
@@ -600,7 +574,11 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
int chutil_bar_width = (isHighResolution) ? 100 : 50;
if (!config.bluetooth.enabled) {
#if defined(USE_EINK)
chutil_bar_width = (isHighResolution) ? 50 : 30;
#else
chutil_bar_width = (isHighResolution) ? 80 : 40;
#endif
}
int chutil_bar_height = (isHighResolution) ? 12 : 7;
int extraoffset = (isHighResolution) ? 6 : 3;
@@ -933,7 +911,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
// === Determine Compass Heading ===
float heading = 0;
bool validHeading = false;
if (screen->ignoreCompass) {
if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) {
validHeading = true;
} else {
if (screen->hasHeading()) {
@@ -999,7 +977,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
// "N" label
float northAngle = 0;
if (!config.display.compass_north_top)
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
northAngle = -heading;
float radius = compassRadius;
int16_t nX = compassX + (radius - 1) * sin(northAngle);
@@ -1042,7 +1020,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
// "N" label
float northAngle = 0;
if (!config.display.compass_north_top)
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
northAngle = -heading;
float radius = compassRadius;
int16_t nX = compassX + (radius - 1) * sin(northAngle);
@@ -1066,9 +1044,16 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
static const uint8_t xbm[] = USERPREFS_OEM_IMAGE_DATA;
display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2,
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH,
USERPREFS_OEM_IMAGE_HEIGHT, xbm);
if (isHighResolution) {
display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2,
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH,
USERPREFS_OEM_IMAGE_HEIGHT, xbm);
} else {
display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2,
y + (SCREEN_HEIGHT - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH,
USERPREFS_OEM_IMAGE_HEIGHT, xbm);
}
switch (USERPREFS_OEM_FONT_SIZE) {
case 0:
@@ -1084,7 +1069,9 @@ void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, O
display->setTextAlignment(TEXT_ALIGN_LEFT);
const char *title = USERPREFS_OEM_TEXT;
display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title);
if (isHighResolution) {
display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title);
}
display->setFont(FONT_SMALL);
// Draw region in upper left