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 8d05e17f11eb48c42460176317893a50abd2eeb2.

* 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 2e8aecd52d6f5b9058e0bde09b72ece43a5f3a48.

* 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
co-authored by GitHub Ben Meadors Jason P HarukiToreda Wilson
parent 3fdefe8289
commit a6be2e46ed
35 changed files with 1441 additions and 422 deletions
+2 -1
View File
@@ -13,5 +13,6 @@ enum class Cmd {
START_FIRMWARE_UPDATE_SCREEN, START_FIRMWARE_UPDATE_SCREEN,
STOP_BOOT_SCREEN, STOP_BOOT_SCREEN,
SHOW_PREV_FRAME, SHOW_PREV_FRAME,
SHOW_NEXT_FRAME SHOW_NEXT_FRAME,
NOOP
}; };
+120 -41
View File
@@ -69,6 +69,8 @@ using graphics::Emote;
using graphics::emotes; using graphics::emotes;
using graphics::numEmotes; using graphics::numEmotes;
extern uint16_t TFT_MESH;
#if HAS_WIFI && !defined(ARCH_PORTDUINO) #if HAS_WIFI && !defined(ARCH_PORTDUINO)
#include "mesh/wifi/WiFiAPClient.h" #include "mesh/wifi/WiFiAPClient.h"
#endif #endif
@@ -135,10 +137,66 @@ extern bool hasUnreadMessage;
// Displays a temporary centered banner message (e.g., warning, status, etc.) // Displays a temporary centered banner message (e.g., warning, status, etc.)
// 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 void Screen::showSimpleBanner(const char *message, uint32_t durationMs)
void Screen::showOverlayBanner(const char *message, uint32_t durationMs, const char **optionsArrayPtr, uint8_t options,
std::function<void(int)> bannerCallback, int8_t InitialSelected)
{ {
BannerOverlayOptions options;
options.message = message;
options.durationMs = durationMs;
options.notificationType = notificationTypeEnum::text_banner;
showOverlayBanner(options);
}
// Called to trigger a banner with custom message and duration
void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options)
{
#ifdef USE_EINK
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
#endif
// Store the message and set the expiration timestamp
strncpy(NotificationRenderer::alertBannerMessage, banner_overlay_options.message, 255);
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
NotificationRenderer::alertBannerUntil =
(banner_overlay_options.durationMs == 0) ? 0 : millis() + banner_overlay_options.durationMs;
NotificationRenderer::optionsArrayPtr = banner_overlay_options.optionsArrayPtr;
NotificationRenderer::optionsEnumPtr = banner_overlay_options.optionsEnumPtr;
NotificationRenderer::alertBannerOptions = banner_overlay_options.optionsCount;
NotificationRenderer::alertBannerCallback = banner_overlay_options.bannerCallback;
NotificationRenderer::curSelected = banner_overlay_options.InitialSelected;
NotificationRenderer::pauseBanner = false;
NotificationRenderer::current_notification_type = notificationTypeEnum::selection_picker;
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
ui->setTargetFPS(60);
ui->update();
}
// Called to trigger a banner with custom message and duration
void Screen::showNodePicker(const char *message, uint32_t durationMs, std::function<void(int)> bannerCallback)
{
#ifdef USE_EINK
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
#endif
nodeDB->pause_sort(true);
// Store the message and set the expiration timestamp
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
NotificationRenderer::alertBannerCallback = bannerCallback;
NotificationRenderer::pauseBanner = false;
NotificationRenderer::curSelected = 0;
NotificationRenderer::current_notification_type = notificationTypeEnum::node_picker;
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
ui->setTargetFPS(60);
ui->update();
}
// Called to trigger a banner with custom message and duration
void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits,
std::function<void(uint32_t)> bannerCallback)
{
LOG_WARN("Show Number Picker");
#ifdef USE_EINK #ifdef USE_EINK
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
#endif #endif
@@ -146,14 +204,16 @@ void Screen::showOverlayBanner(const char *message, uint32_t durationMs, const c
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::alertBannerCallback = bannerCallback; NotificationRenderer::alertBannerCallback = bannerCallback;
NotificationRenderer::curSelected = InitialSelected;
NotificationRenderer::pauseBanner = false; NotificationRenderer::pauseBanner = false;
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawAlertBannerOverlay}; NotificationRenderer::curSelected = 0;
NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker;
NotificationRenderer::numDigits = digits;
NotificationRenderer::currentNumber = 0;
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
setFastFramerate(); // Draw ASAP ui->setTargetFPS(60);
ui->update(); ui->update();
} }
@@ -230,6 +290,20 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
: concurrency::OSThread("Screen"), address_found(address), model(screenType), geometry(geometry), cmdQueue(32) : concurrency::OSThread("Screen"), address_found(address), model(screenType), geometry(geometry), cmdQueue(32)
{ {
graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES]; graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES];
LOG_INFO("Protobuf Value uiconfig.screen_rgb_color: %d", uiconfig.screen_rgb_color);
int32_t rawRGB = uiconfig.screen_rgb_color;
if (rawRGB > 0 && rawRGB <= 255255255) {
uint8_t r = (rawRGB >> 16) & 0xFF;
uint8_t g = (rawRGB >> 8) & 0xFF;
uint8_t b = rawRGB & 0xFF;
LOG_INFO("Values of r,g,b: %d, %d, %d", r, g, b);
if (r <= 255 && g <= 255 && b <= 255) {
TFT_MESH = COLOR565(r, g, b);
}
}
#if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64) #if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64)
dispdev = new SH1106Wire(address.address, -1, -1, geometry, dispdev = new SH1106Wire(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
@@ -239,7 +313,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
ST7789_MISO, ST7789_SCK); ST7789_MISO, ST7789_SCK);
#else #else
dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT);
static_cast<ST7789Spi *>(dispdev)->setRGB(COLOR565(255, 255, 128)); static_cast<ST7789Spi *>(dispdev)->setRGB(TFT_MESH);
#endif #endif
#elif defined(USE_SSD1306) #elif defined(USE_SSD1306)
dispdev = new SSD1306Wire(address.address, -1, -1, geometry, dispdev = new SSD1306Wire(address.address, -1, -1, geometry,
@@ -386,9 +460,22 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
void Screen::setup() void Screen::setup()
{ {
// === Enable display rendering === // === Enable display rendering ===
useDisplay = true; useDisplay = true;
// === Load saved brightness from UI config ===
// For OLED displays (SSD1306), default brightness is 255 if not set
if (uiconfig.screen_brightness == 0) {
#if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
brightness = 255; // Default for OLED
#else
brightness = BRIGHTNESS_DEFAULT;
#endif
} else {
brightness = uiconfig.screen_brightness;
}
// === Detect OLED subtype (if supported by board variant) === // === Detect OLED subtype (if supported by board variant) ===
#ifdef AutoOLEDWire_h #ifdef AutoOLEDWire_h
if (isAUTOOled) if (isAUTOOled)
@@ -416,6 +503,14 @@ void Screen::setup()
ui->disableAllIndicators(); // Disable page indicator dots ui->disableAllIndicators(); // Disable page indicator dots
ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance
// === Apply loaded brightness ===
#if defined(ST7789_CS)
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
dispdev->setBrightness(brightness);
#endif
LOG_INFO("Applied screen brightness: %d", brightness);
// === Set custom overlay callbacks === // === Set custom overlay callbacks ===
static OverlayCallback overlays[] = { static OverlayCallback overlays[] = {
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
@@ -562,7 +657,7 @@ int32_t Screen::runOnce()
if (displayHeight == 0) { if (displayHeight == 0) {
displayHeight = dispdev->getHeight(); displayHeight = dispdev->getHeight();
} }
menuHandler::handleMenuSwitch(); menuHandler::handleMenuSwitch(dispdev);
// 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
@@ -595,7 +690,7 @@ int32_t Screen::runOnce()
} }
#endif #endif
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) { if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) {
showOverlayBanner("Rebooting...", 0); showSimpleBanner("Rebooting...", 0);
} }
// Process incoming commands. // Process incoming commands.
@@ -642,6 +737,8 @@ int32_t Screen::runOnce()
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
setFrames(); setFrames();
break; break;
case Cmd::NOOP:
break;
default: default:
LOG_ERROR("Invalid screen cmd"); LOG_ERROR("Invalid screen cmd");
} }
@@ -785,8 +882,8 @@ 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::digitalWatchFace ? graphics::ClockRenderer::drawDigitalClockFrame normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
: &graphics::ClockRenderer::drawAnalogClockFrame; : graphics::ClockRenderer::drawDigitalClockFrame;
indicatorIcons.push_back(digital_icon_clock); indicatorIcons.push_back(digital_icon_clock);
#endif #endif
@@ -842,8 +939,8 @@ 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::digitalWatchFace ? graphics::ClockRenderer::drawDigitalClockFrame normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
: graphics::ClockRenderer::drawAnalogClockFrame; : graphics::ClockRenderer::drawDigitalClockFrame;
indicatorIcons.push_back(digital_icon_clock); indicatorIcons.push_back(digital_icon_clock);
#endif #endif
@@ -909,7 +1006,7 @@ void Screen::setFrames(FrameFocus focus)
ui->disableAllIndicators(); ui->disableAllIndicators();
// Add overlays: frame icons and alert banner) // Add overlays: frame icons and alert banner)
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawAlertBannerOverlay}; static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list
@@ -937,6 +1034,9 @@ 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.clock); ui->switchToFrame(fsi.positions.clock);
break; break;
case FOCUS_SYSTEM:
ui->switchToFrame(fsi.positions.memory);
break;
case FOCUS_PRESERVE: case FOCUS_PRESERVE:
// No more adjustment — force stay on same index // No more adjustment — force stay on same index
@@ -1180,7 +1280,7 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
} }
} }
screen->showOverlayBanner(banner, 3000); screen->showSimpleBanner(banner, 3000);
} }
} }
@@ -1220,30 +1320,14 @@ int Screen::handleInputEvent(const InputEvent *event)
#endif #endif
if (NotificationRenderer::isOverlayBannerShowing()) { if (NotificationRenderer::isOverlayBannerShowing()) {
NotificationRenderer::inEvent = event->inputEvent; NotificationRenderer::inEvent = event->inputEvent;
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
NotificationRenderer::drawAlertBannerOverlay};
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(); menuHandler::handleMenuSwitch(dispdev);
return 0; return 0;
} }
/*
#if defined(DISPLAY_CLOCK_FRAME)
// For the T-Watch, intercept touches to the 'toggle digital/analog watch face' button
uint8_t watchFaceFrame = error_code ? 1 : 0;
if (this->ui->getUiState()->currentFrame == watchFaceFrame && event->touchX >= 204 && event->touchX <= 240 &&
event->touchY >= 204 && event->touchY <= 240) {
screen->digitalWatchFace = !screen->digitalWatchFace;
setFrames();
return 0;
}
#endif
*/
// Use left or right input from a keyboard to move between frames, // Use left or right input from a keyboard to move between frames,
// so long as a mesh module isn't using these events for some other purpose // so long as a mesh module isn't using these events for some other purpose
@@ -1265,13 +1349,8 @@ int Screen::handleInputEvent(const InputEvent *event)
} 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) {
menuHandler::homeBaseMenu(); menuHandler::homeBaseMenu();
#if HAS_TFT
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) { } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) {
menuHandler::switchToMUIMenu(); menuHandler::systemBaseMenu();
#else
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) {
menuHandler::BuzzerModeMenu();
#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) {
menuHandler::positionBaseMenu(); menuHandler::positionBaseMenu();
+32 -8
View File
@@ -5,10 +5,26 @@
#include "detect/ScanI2C.h" #include "detect/ScanI2C.h"
#include "mesh/generated/meshtastic/config.pb.h" #include "mesh/generated/meshtastic/config.pb.h"
#include <OLEDDisplay.h> #include <OLEDDisplay.h>
#include <functional>
#include <string> #include <string>
#include <vector> #include <vector>
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) #define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
namespace graphics
{
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker };
struct BannerOverlayOptions {
const char *message;
uint32_t durationMs = 30000;
const char **optionsArrayPtr = nullptr;
const int *optionsEnumPtr = nullptr;
uint8_t optionsCount = 0;
std::function<void(int)> bannerCallback = nullptr;
int8_t InitialSelected = 0;
notificationTypeEnum notificationType = notificationTypeEnum::text_banner;
};
} // namespace graphics
#if !HAS_SCREEN #if !HAS_SCREEN
#include "power.h" #include "power.h"
@@ -25,6 +41,7 @@ class Screen
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, FOCUS_CLOCK,
FOCUS_SYSTEM,
}; };
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
@@ -39,10 +56,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, const char **optionsArrayPtr = nullptr, void showSimpleBanner(const char *message, uint32_t durationMs = 0) {}
uint8_t options = 0, std::function<void(int)> bannerCallback = NULL, int8_t InitialSelected = 0) void showOverlayBanner(BannerOverlayOptions) {}
{
}
void setFrames(FrameFocus focus) {} void setFrames(FrameFocus focus) {}
void endAlert() {} void endAlert() {}
}; };
@@ -199,6 +214,7 @@ class Screen : public concurrency::OSThread
CallbackObserver<Screen, AdminModule_ObserverData *>(this, &Screen::handleAdminMessage); CallbackObserver<Screen, AdminModule_ObserverData *>(this, &Screen::handleAdminMessage);
public: public:
OLEDDisplay *getDisplayDevice() { return dispdev; }
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
size_t frameCount = 0; // Total number of active frames size_t frameCount = 0; // Total number of active frames
~Screen(); ~Screen();
@@ -211,6 +227,7 @@ class Screen : public concurrency::OSThread
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, FOCUS_CLOCK,
FOCUS_SYSTEM,
}; };
// Regenerate the normal set of frames, focusing a specific frame if requested // Regenerate the normal set of frames, focusing a specific frame if requested
@@ -225,8 +242,6 @@ 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
@@ -290,8 +305,11 @@ class Screen : public concurrency::OSThread
enqueueCmd(cmd); enqueueCmd(cmd);
} }
void showOverlayBanner(const char *message, uint32_t durationMs = 3000, const char **optionsArrayPtr = nullptr, void showSimpleBanner(const char *message, uint32_t durationMs = 0);
uint8_t options = 0, std::function<void(int)> bannerCallback = NULL, int8_t InitialSelected = 0); void showOverlayBanner(BannerOverlayOptions);
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(int)> bannerCallback);
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback);
void startFirmwareUpdateScreen() void startFirmwareUpdateScreen()
{ {
@@ -325,6 +343,12 @@ class Screen : public concurrency::OSThread
/// Stops showing the boot screen. /// Stops showing the boot screen.
void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); } void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); }
void runNow()
{
setFastFramerate();
enqueueCmd(ScreenCmd{.cmd = Cmd::NOOP});
}
/// Overrides the default utf8 character conversion, to replace empty space with question marks /// Overrides the default utf8 character conversion, to replace empty space with question marks
static char customFontTableLookup(const uint8_t ch) static char customFontTableLookup(const uint8_t ch)
{ {
+26
View File
@@ -343,4 +343,30 @@ const int *getTextPositions(OLEDDisplay *display)
return textPositions; return textPositions;
} }
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;
}
} // namespace graphics } // namespace graphics
+5
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <OLEDDisplay.h> #include <OLEDDisplay.h>
#include <string>
namespace graphics namespace graphics
{ {
@@ -52,4 +53,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
const int *getTextPositions(OLEDDisplay *display); const int *getTextPositions(OLEDDisplay *display);
bool isAllowedPunctuation(char c);
std::string sanitizeString(const std::string &input);
} // namespace graphics } // namespace graphics
+5 -2
View File
@@ -1,5 +1,6 @@
#include "configuration.h" #include "configuration.h"
#include "main.h" #include "main.h"
#if ARCH_PORTDUINO #if ARCH_PORTDUINO
#include "platform/portduino/PortduinoGlue.h" #include "platform/portduino/PortduinoGlue.h"
#endif #endif
@@ -14,8 +15,10 @@
extern SX1509 gpioExtender; extern SX1509 gpioExtender;
#endif #endif
#ifndef TFT_MESH #ifdef TFT_MESH_OVERRIDE
#define TFT_MESH COLOR565(0x67, 0xEA, 0x94) uint16_t TFT_MESH = TFT_MESH_OVERRIDE;
#else
uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94);
#endif #endif
#if defined(ST7735S) #if defined(ST7735S)
-1
View File
@@ -21,7 +21,6 @@ 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)
{ {
-2
View File
@@ -11,8 +11,6 @@ class Screen;
namespace ClockRenderer namespace ClockRenderer
{ {
// Whether we are showing the digital watch face or the analog one
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);
+1 -1
View File
@@ -50,7 +50,7 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY,
radius += 4; radius += 4;
} }
Point north(0, -radius); Point north(0, -radius);
if (!config.display.compass_north_top) if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
north.rotate(-myHeading); north.rotate(-myHeading);
north.translate(compassX, compassY); north.translate(compassX, compassY);
+4 -1
View File
@@ -501,7 +501,10 @@ 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 = (isHighResolution) ? 24 : 0; int barsOffset = (isHighResolution) ? 24 : 0;
#ifdef USE_EINK
barsOffset -= 12;
#endif
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) {
+562 -143
View File
@@ -8,14 +8,19 @@
#include "NodeDB.h" #include "NodeDB.h"
#include "buzz.h" #include "buzz.h"
#include "graphics/Screen.h" #include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/UIRenderer.h" #include "graphics/draw/UIRenderer.h"
#include "main.h" #include "main.h"
#include "modules/AdminModule.h" #include "modules/AdminModule.h"
#include "modules/CannedMessageModule.h" #include "modules/CannedMessageModule.h"
extern uint16_t TFT_MESH;
namespace graphics namespace graphics
{ {
menuHandler::screenMenus menuHandler::menuQueue = menu_none; menuHandler::screenMenus menuHandler::menuQueue = menu_none;
bool test_enabled = false;
uint8_t test_count = 0;
void menuHandler::LoraRegionPicker(uint32_t duration) void menuHandler::LoraRegionPicker(uint32_t duration)
{ {
@@ -44,72 +49,92 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
"PH_868", "PH_868",
"PH_915", "PH_915",
"ANZ_433"}; "ANZ_433"};
screen->showOverlayBanner( BannerOverlayOptions bannerOptions;
"Set the LoRa region", duration, optionsArray, 23, bannerOptions.message = "Set the LoRa region";
[](int selected) -> void { bannerOptions.durationMs = duration;
if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) { bannerOptions.optionsArrayPtr = optionsArray;
config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected); bannerOptions.optionsCount = 23;
// This is needed as we wait til picking the LoRa region to generate keys for the first time. bannerOptions.InitialSelected = 0;
if (!owner.is_licensed) { bannerOptions.bannerCallback = [](int selected) -> void {
bool keygenSuccess = false; if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) {
if (config.security.private_key.size == 32) { config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected);
// public key is derived from private, so this will always have the same result. // This is needed as we wait til picking the LoRa region to generate keys for the first time.
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { if (!owner.is_licensed) {
keygenSuccess = true; bool keygenSuccess = false;
} if (config.security.private_key.size == 32) {
} else { // public key is derived from private, so this will always have the same result.
LOG_INFO("Generate new PKI keys"); if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
keygenSuccess = true; keygenSuccess = true;
} }
if (keygenSuccess) { } else {
config.security.public_key.size = 32; LOG_INFO("Generate new PKI keys");
config.security.private_key.size = 32; crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
owner.public_key.size = 32; keygenSuccess = true;
memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
}
} }
config.lora.tx_enabled = true; if (keygenSuccess) {
initRegion(); config.security.public_key.size = 32;
if (myRegion->dutyCycle < 100) { config.security.private_key.size = 32;
config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit 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);
} }
}, config.lora.tx_enabled = true;
0); 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() void menuHandler::TwelveHourPicker()
{ {
static const char *optionsArray[] = {"Back", "12-hour", "24-hour"}; static const char *optionsArray[] = {"Back", "12-hour", "24-hour"};
screen->showOverlayBanner("Time Format", 30000, optionsArray, 3, [](int selected) -> void { enum optionsNumbers { Back = 0, twelve = 1, twentyfour = 2 };
if (selected == 0) { 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; menuHandler::menuQueue = menuHandler::clock_menu;
} else if (selected == 1) { screen->runNow();
} else if (selected == twelve) {
config.display.use_12h_clock = true; config.display.use_12h_clock = true;
} else { } else {
config.display.use_12h_clock = false; config.display.use_12h_clock = false;
} }
service->reloadConfig(SEGMENT_CONFIG); service->reloadConfig(SEGMENT_CONFIG);
}); };
screen->showOverlayBanner(bannerOptions);
} }
void menuHandler::ClockFacePicker() void menuHandler::ClockFacePicker()
{ {
static const char *optionsArray[] = {"Back", "Digital", "Analog"}; static const char *optionsArray[] = {"Back", "Digital", "Analog"};
screen->showOverlayBanner("Which Face?", 30000, optionsArray, 3, [](int selected) -> void { enum optionsNumbers { Back = 0, Digital = 1, Analog = 2 };
if (selected == 0) { 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; menuHandler::menuQueue = menuHandler::clock_menu;
} else if (selected == 1) { screen->runNow();
graphics::ClockRenderer::digitalWatchFace = true; } 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); screen->setFrames(Screen::FOCUS_CLOCK);
} else { } 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->setFrames(Screen::FOCUS_CLOCK);
} }
}); };
screen->showOverlayBanner(bannerOptions);
} }
void menuHandler::TZPicker() void menuHandler::TZPicker()
@@ -133,9 +158,14 @@ void menuHandler::TZPicker()
"AU/ACST", "AU/ACST",
"AU/AEST", "AU/AEST",
"Pacific/NZ"}; "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) { if (selected == 0) {
menuHandler::menuQueue = menuHandler::clock_menu; menuHandler::menuQueue = menuHandler::clock_menu;
screen->runNow();
} else if (selected == 1) { // Hawaii } else if (selected == 1) { // Hawaii
strncpy(config.device.tzdef, "HST10", sizeof(config.device.tzdef)); strncpy(config.device.tzdef, "HST10", sizeof(config.device.tzdef));
} else if (selected == 2) { // Alaska } else if (selected == 2) { // Alaska
@@ -175,27 +205,31 @@ void menuHandler::TZPicker()
setenv("TZ", config.device.tzdef, 1); setenv("TZ", config.device.tzdef, 1);
service->reloadConfig(SEGMENT_CONFIG); service->reloadConfig(SEGMENT_CONFIG);
} }
}); };
screen->showOverlayBanner(bannerOptions);
} }
void menuHandler::clockMenu() void menuHandler::clockMenu()
{ {
static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"}; static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"};
screen->showOverlayBanner("Clock Action", 30000, optionsArray, 4, [](int selected) -> void { enum optionsNumbers { Back = 0, Clock = 1, Time = 2, Timezone = 3 };
if (selected == 1) { 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; menuHandler::menuQueue = menuHandler::clock_face_picker;
screen->setInterval(0); screen->runNow();
runASAP = true; } else if (selected == Time) {
} else if (selected == 2) {
menuHandler::menuQueue = menuHandler::twelve_hour_picker; menuHandler::menuQueue = menuHandler::twelve_hour_picker;
screen->setInterval(0); screen->runNow();
runASAP = true; } else if (selected == Timezone) {
} else if (selected == 3) {
menuHandler::menuQueue = menuHandler::TZ_picker; menuHandler::menuQueue = menuHandler::TZ_picker;
screen->setInterval(0); screen->runNow();
runASAP = true;
} }
}); };
screen->showOverlayBanner(bannerOptions);
} }
void menuHandler::messageResponseMenu() void menuHandler::messageResponseMenu()
@@ -203,6 +237,7 @@ void menuHandler::messageResponseMenu()
static const char **optionsArrayPtr; static const char **optionsArrayPtr;
int options; int options;
enum optionsNumbers { Back = 0, Dismiss = 1, Preset = 2, Freetext = 3 };
if (kb_found) { if (kb_found) {
static const char *optionsArray[] = {"Back", "Dismiss", "Reply via Preset", "Reply via Freetext"}; static const char *optionsArray[] = {"Back", "Dismiss", "Reply via Preset", "Reply via Freetext"};
optionsArrayPtr = optionsArray; optionsArrayPtr = optionsArray;
@@ -217,16 +252,20 @@ void menuHandler::messageResponseMenu()
optionsArrayPtr = optionsArray; optionsArrayPtr = optionsArray;
options = 5; options = 5;
#endif #endif
screen->showOverlayBanner("Message Action", 30000, optionsArrayPtr, options, [](int selected) -> void { BannerOverlayOptions bannerOptions;
if (selected == 1) { bannerOptions.message = "Message Action";
bannerOptions.optionsArrayPtr = optionsArrayPtr;
bannerOptions.optionsCount = options;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Dismiss) {
screen->dismissCurrentFrame(); screen->dismissCurrentFrame();
} else if (selected == 2) { } else if (selected == Preset) {
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
} else { } else {
cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from); cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
} }
} else if (selected == 3) { } else if (selected == Freetext) {
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
} else { } else {
@@ -241,51 +280,138 @@ void menuHandler::messageResponseMenu()
audioThread->readAloud(msg); audioThread->readAloud(msg);
} }
#endif #endif
}); };
screen->showOverlayBanner(bannerOptions);
} }
void menuHandler::homeBaseMenu() void menuHandler::homeBaseMenu()
{ {
int options; enum optionsNumbers { Back, Backlight, Position, Preset, Freetext, Bluetooth, Sleep };
static const char **optionsArrayPtr;
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) { if (kb_found) {
#ifdef PIN_EINK_EN optionsArray[options] = "New Freetext Msg";
static const char *optionsArray[] = {"Back", "Toggle Backlight", "Send Position", "New Preset Msg", "New Freetext Msg"}; optionsEnumArray[options++] = Freetext;
#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 { optionsArray[options] = "Bluetooth Toggle";
if (selected == 1) { 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 #ifdef PIN_EINK_EN
if (digitalRead(PIN_EINK_EN) == HIGH) { if (digitalRead(PIN_EINK_EN) == HIGH) {
digitalWrite(PIN_EINK_EN, LOW); digitalWrite(PIN_EINK_EN, LOW);
} else { } else {
digitalWrite(PIN_EINK_EN, HIGH); digitalWrite(PIN_EINK_EN, HIGH);
} }
#else
screen->setOn(false);
#endif #endif
} else if (selected == 2) { } else if (selected == Sleep) {
InputEvent event = {.inputEvent = (input_broker_event)175, .kbchar = 175, .touchX = 0, .touchY = 0}; 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); inputBroker->injectInputEvent(&event);
} else if (selected == 3) { } else if (selected == Preset) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
} else if (selected == 4) { } else if (selected == Freetext) {
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST); 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() void menuHandler::favoriteBaseMenu()
@@ -294,21 +420,29 @@ void menuHandler::favoriteBaseMenu()
static const char **optionsArrayPtr; static const char **optionsArrayPtr;
if (kb_found) { 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; optionsArrayPtr = optionsArray;
options = 3; 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) { if (selected == 1) {
cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
} else if (selected == 2) { } else if (selected == 2 && kb_found) {
cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); 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() void menuHandler::positionBaseMenu()
@@ -325,127 +459,385 @@ void menuHandler::positionBaseMenu()
optionsArrayPtr = optionsArray; optionsArrayPtr = optionsArray;
options = 3; 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 (selected == 1) {
#if MESHTASTIC_EXCLUDE_GPS #if MESHTASTIC_EXCLUDE_GPS
menuQueue = menu_none; menuQueue = menu_none;
#else #else
menuQueue = gps_toggle_menu; menuQueue = gps_toggle_menu;
screen->runNow();
#endif #endif
} else if (selected == 2) { } else if (selected == 2) {
menuQueue = compass_point_north_menu; menuQueue = compass_point_north_menu;
screen->runNow();
} else if (selected == 3) { } else if (selected == 3) {
accelerometerThread->calibrate(30); accelerometerThread->calibrate(30);
} }
}); };
screen->showOverlayBanner(bannerOptions);
} }
void menuHandler::nodeListMenu() void menuHandler::nodeListMenu()
{ {
static const char *optionsArray[] = {"Back", "Reset NodeDB"}; static const char *optionsArray[] = {"Back", "Add Favorite", "Reset NodeDB"};
screen->showOverlayBanner("Node Action", 30000, optionsArray, 2, [](int selected) -> void { BannerOverlayOptions bannerOptions;
bannerOptions.message = "Node Action";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 3;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1) { if (selected == 1) {
menuQueue = add_favorite;
screen->runNow();
} else if (selected == 2) {
menuQueue = reset_node_db_menu; menuQueue = reset_node_db_menu;
screen->runNow();
} }
}); };
screen->showOverlayBanner(bannerOptions);
} }
void menuHandler::resetNodeDBMenu() void menuHandler::resetNodeDBMenu()
{ {
static const char *optionsArray[] = {"Back", "Confirm"}; 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) { if (selected == 1) {
disableBluetooth(); disableBluetooth();
LOG_INFO("Initiate node-db reset"); LOG_INFO("Initiate node-db reset");
nodeDB->resetNodes(); nodeDB->resetNodes();
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
} }
}); };
screen->showOverlayBanner(bannerOptions);
} }
void menuHandler::compassNorthMenu() void menuHandler::compassNorthMenu()
{ {
static const char *optionsArray[] = {"Back", "Dynamic", "Fixed Ring", "Freeze Heading"}; 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 (selected == 1) {
if (config.display.compass_north_top != false) { if (uiconfig.compass_mode != meshtastic_CompassMode_DYNAMIC) {
config.display.compass_north_top = false; uiconfig.compass_mode = meshtastic_CompassMode_DYNAMIC;
service->reloadConfig(SEGMENT_CONFIG); 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) { } else if (selected == 2) {
if (config.display.compass_north_top != true) { if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) {
config.display.compass_north_top = true; uiconfig.compass_mode = meshtastic_CompassMode_FIXED_RING;
service->reloadConfig(SEGMENT_CONFIG); 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) { } else if (selected == 3) {
if (config.display.compass_north_top != true) { if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) {
config.display.compass_north_top = true; uiconfig.compass_mode = meshtastic_CompassMode_FREEZE_HEADING;
service->reloadConfig(SEGMENT_CONFIG); 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) { } else if (selected == 0) {
menuQueue = position_base_menu; menuQueue = position_base_menu;
screen->runNow();
} }
}); };
screen->showOverlayBanner(bannerOptions);
} }
#if !MESHTASTIC_EXCLUDE_GPS #if !MESHTASTIC_EXCLUDE_GPS
void menuHandler::GPSToggleMenu() void menuHandler::GPSToggleMenu()
{ {
static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; static const char *optionsArray[] = {"Back", "Enabled", "Disabled"};
screen->showOverlayBanner( BannerOverlayOptions bannerOptions;
"Toggle GPS", 30000, optionsArray, 3, bannerOptions.message = "Toggle GPS";
[](int selected) -> void { bannerOptions.optionsArrayPtr = optionsArray;
if (selected == 1) { bannerOptions.optionsCount = 3;
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; bannerOptions.bannerCallback = [](int selected) -> void {
playGPSEnableBeep(); if (selected == 1) {
gps->enable(); config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED;
service->reloadConfig(SEGMENT_CONFIG); playGPSEnableBeep();
} else if (selected == 2) { gps->enable();
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; service->reloadConfig(SEGMENT_CONFIG);
playGPSDisableBeep(); } else if (selected == 2) {
gps->disable(); config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED;
service->reloadConfig(SEGMENT_CONFIG); playGPSDisableBeep();
} else { gps->disable();
menuQueue = position_base_menu; service->reloadConfig(SEGMENT_CONFIG);
} } else {
}, menuQueue = position_base_menu;
config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2); // set inital selection screen->runNow();
}
};
bannerOptions.InitialSelected = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2;
screen->showOverlayBanner(bannerOptions);
} }
#endif #endif
void menuHandler::BuzzerModeMenu() void menuHandler::BuzzerModeMenu()
{ {
static const char *optionsArray[] = {"All Enabled", "Disabled", "Notifications", "System Only"}; static const char *optionsArray[] = {"All Enabled", "Disabled", "Notifications", "System Only"};
screen->showOverlayBanner( BannerOverlayOptions bannerOptions;
"Beep Action", 30000, optionsArray, 4, bannerOptions.message = "Beep Action";
[](int selected) -> void { bannerOptions.optionsArrayPtr = optionsArray;
config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected; bannerOptions.optionsCount = 4;
service->reloadConfig(SEGMENT_CONFIG); bannerOptions.bannerCallback = [](int selected) -> void {
}, config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected;
config.device.buzzer_mode); 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() void menuHandler::switchToMUIMenu()
{ {
static const char *optionsArray[] = {"Yes", "No"}; 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) { if (selected == 0) {
config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR;
config.bluetooth.enabled = false; config.bluetooth.enabled = false;
service->reloadConfig(SEGMENT_CONFIG); service->reloadConfig(SEGMENT_CONFIG);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); 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) { switch (menuQueue) {
case menu_none: case menu_none:
break; break;
@@ -478,6 +870,33 @@ void menuHandler::handleMenuSwitch()
case reset_node_db_menu: case reset_node_db_menu:
resetNodeDBMenu(); resetNodeDBMenu();
break; 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; menuQueue = menu_none;
} }
+19 -2
View File
@@ -17,26 +17,43 @@ class menuHandler
gps_toggle_menu, gps_toggle_menu,
#endif #endif
compass_point_north_menu, 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 screenMenus menuQueue;
static void LoraRegionPicker(uint32_t duration = 30000); static void LoraRegionPicker(uint32_t duration = 30000);
static void handleMenuSwitch(); static void handleMenuSwitch(OLEDDisplay *display);
static void clockMenu(); static void clockMenu();
static void TZPicker(); static void TZPicker();
static void TwelveHourPicker(); static void TwelveHourPicker();
static void ClockFacePicker(); static void ClockFacePicker();
static void messageResponseMenu(); static void messageResponseMenu();
static void homeBaseMenu(); static void homeBaseMenu();
static void systemBaseMenu();
static void favoriteBaseMenu(); static void favoriteBaseMenu();
static void positionBaseMenu(); static void positionBaseMenu();
static void compassNorthMenu(); static void compassNorthMenu();
static void GPSToggleMenu(); static void GPSToggleMenu();
static void BuzzerModeMenu(); static void BuzzerModeMenu();
static void switchToMUIMenu(); static void switchToMUIMenu();
static void TFTColorPickerMenu(OLEDDisplay *display);
static void nodeListMenu(); static void nodeListMenu();
static void resetNodeDBMenu(); static void resetNodeDBMenu();
static void BrightnessPickerMenu();
static void rebootMenu();
static void addFavoriteMenu();
static void removeFavoriteMenu();
static void testMenu();
static void numberTest();
}; };
} // namespace graphics } // namespace graphics
+3 -3
View File
@@ -66,10 +66,10 @@ const char *getSafeNodeName(meshtastic_NodeInfoLite *node)
strncpy(nodeName, name, sizeof(nodeName) - 1); strncpy(nodeName, name, sizeof(nodeName) - 1);
nodeName[sizeof(nodeName) - 1] = '\0'; nodeName[sizeof(nodeName) - 1] = '\0';
} else { } else {
snprintf(nodeName, sizeof(nodeName), "%04X", (uint16_t)(node->num & 0xFFFF)); snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
} }
} else { } else {
strcpy(nodeName, "?"); snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
} }
return nodeName; return nodeName;
} }
@@ -522,7 +522,7 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
double lat = DegD(ourNode->position.latitude_i); double lat = DegD(ourNode->position.latitude_i);
double lon = DegD(ourNode->position.longitude_i); double lon = DegD(ourNode->position.longitude_i);
if (!screen->ignoreCompass) { if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) {
#if HAS_GPS #if HAS_GPS
if (screen->hasHeading()) { if (screen->hasHeading()) {
heading = screen->getHeading(); // degrees heading = screen->getHeading(); // degrees
+316 -52
View File
@@ -32,8 +32,21 @@ 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; const char **NotificationRenderer::optionsArrayPtr = nullptr;
const int *NotificationRenderer::optionsEnumPtr = nullptr;
std::function<void(int)> NotificationRenderer::alertBannerCallback = NULL; std::function<void(int)> NotificationRenderer::alertBannerCallback = NULL;
bool NotificationRenderer::pauseBanner = false; 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 // Used on boot when a certificate is being created
void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) 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) if (!isOverlayBannerShowing() || pauseBanner)
return; 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 === // === Layout Configuration ===
constexpr uint16_t hPadding = 5;
constexpr uint16_t vPadding = 2; 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 const char *lineStarts[MAX_LINES + 1] = {0};
display->setFont(FONT_SMALL); uint16_t lineCount = 0;
display->setTextAlignment(TEXT_ALIGN_LEFT);
// 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 optionWidths[alertBannerOptions] = {0};
uint16_t maxWidth = 0; uint16_t maxWidth = 0;
uint16_t arrowsWidth = display->getStringWidth("> <", 4, true); uint16_t arrowsWidth = display->getStringWidth("> <", 4, true);
uint16_t lineWidths[MAX_LINES] = {0}; uint16_t lineWidths[MAX_LINES] = {0};
uint16_t lineLengths[MAX_LINES] = {0}; uint16_t lineLengths[MAX_LINES] = {0};
char *lineStarts[MAX_LINES + 1]; const char *lineStarts[MAX_LINES + 1] = {0};
uint16_t lineCount = 0; uint16_t lineCount = 0;
char lineBuffer[40] = {0}; char lineBuffer[40] = {0};
@@ -86,7 +284,7 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
lineStarts[lineCount] = alertBannerMessage; lineStarts[lineCount] = alertBannerMessage;
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
lineStarts[lineCount + 1] = std::find(lineStarts[lineCount], alertEnd, '\n'); lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount]; lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount];
if (lineStarts[lineCount + 1][0] == '\n') if (lineStarts[lineCount + 1][0] == '\n')
lineStarts[lineCount + 1] += 1; 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) { } else if (inEvent == INPUT_BROKER_DOWN || inEvent == INPUT_BROKER_USER_PRESS) {
curSelected++; curSelected++;
} else if (inEvent == INPUT_BROKER_SELECT) { } else if (inEvent == INPUT_BROKER_SELECT) {
alertBannerCallback(curSelected); if (optionsEnumPtr != nullptr) {
alertBannerMessage[0] = '\0'; alertBannerCallback(optionsEnumPtr[curSelected]);
optionsEnumPtr = nullptr;
} else {
alertBannerCallback(curSelected);
}
resetBanner();
} 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'; resetBanner();
} }
if (curSelected == -1) if (curSelected == -1)
@@ -124,7 +327,7 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
curSelected = 0; curSelected = 0;
} else { } else {
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'; resetBanner();
} }
} }
@@ -132,7 +335,91 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
if (alertBannerMessage[0] == '\0') if (alertBannerMessage[0] == '\0')
return; 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; uint16_t boxWidth = hPadding * 2 + maxWidth;
if (needs_bell) { if (needs_bell) {
if (isHighResolution && boxWidth <= 150) if (isHighResolution && boxWidth <= 150)
@@ -141,14 +428,19 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
boxWidth += 20; boxWidth += 20;
} }
uint16_t totalLines = lineCount + alertBannerOptions;
uint16_t screenHeight = display->height(); uint16_t screenHeight = display->height();
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; 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 contentHeight = visibleTotalLines * effectiveLineHeight;
uint16_t boxHeight = contentHeight + vPadding * 2; uint16_t boxHeight = contentHeight + vPadding * 2;
if (visibleTotalLines == 1) {
boxHeight += (isHighResolution) ? 4 : 3;
}
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
if (totalLines > visibleTotalLines) {
boxWidth += (isHighResolution) ? 4 : 2;
}
int16_t boxTop = (display->height() / 2) - (boxHeight / 2); int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
// === Draw Box === // === Draw Box ===
@@ -169,21 +461,18 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
// === Draw Content === // === 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++) {
strncpy(lineBuffer, lineStarts[i], 40);
lineBuffer[lineLengths[i] > 39 ? 39 : lineLengths[i]] = '\0';
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);
} }
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 // Determine if this is a pop-up or a pick list
if (alertBannerOptions > 0) { if (alertBannerOptions > 0 && i == 0) {
// Pick List // Pick List
display->setColor(WHITE); display->setColor(WHITE);
int background_yOffset = 1; int background_yOffset = 1;
@@ -199,39 +488,14 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
lineY += (effectiveLineHeight - 2 - background_yOffset); lineY += (effectiveLineHeight - 2 - background_yOffset);
} else { } else {
// Pop-up // Pop-up
display->drawString(textX, lineY - 2, lineBuffer); display->drawString(textX, lineY, lineBuffer);
lineY += (effectiveLineHeight); 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) === // === Scroll Bar (Thicker, inside box, not over title) ===
if (totalLines > visibleTotalLines) { if (totalLines > visibleTotalLines) {
const uint8_t scrollBarWidth = 5; const uint8_t scrollBarWidth = 5;
const uint8_t scrollPadding = 2;
int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2; int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2;
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; // start after title line 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; float ratio = (float)visibleTotalLines / totalLines;
uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4); 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); uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight);
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight); display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
+14
View File
@@ -2,6 +2,8 @@
#include "OLEDDisplay.h" #include "OLEDDisplay.h"
#include "OLEDDisplayUi.h" #include "OLEDDisplayUi.h"
#include "graphics/Screen.h"
#define MAX_LINES 5
namespace graphics namespace graphics
{ {
@@ -14,16 +16,28 @@ class NotificationRenderer
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 const char **optionsArrayPtr;
static const int *optionsEnumPtr;
static uint8_t alertBannerOptions; // last x lines are seelctable options static uint8_t alertBannerOptions; // last x lines are seelctable options
static std::function<void(int)> alertBannerCallback; static std::function<void(int)> alertBannerCallback;
static uint32_t numDigits;
static uint32_t currentNumber;
static bool pauseBanner; static bool pauseBanner;
static void resetBanner();
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawAlertBannerOverlay(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 drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
static bool isOverlayBannerShowing(); static bool isOverlayBannerShowing();
static graphics::notificationTypeEnum current_notification_type;
}; };
} // namespace graphics } // namespace graphics
+23 -36
View File
@@ -18,32 +18,6 @@
#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;
}
// External variables // External variables
extern graphics::Screen *screen; 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)); 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 (screen->ignoreCompass) { if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) {
myHeading = 0; myHeading = 0;
} else { } else {
bearing -= myHeading; bearing -= myHeading;
@@ -488,7 +462,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
const auto &op = ourNode->position; const auto &op = ourNode->position;
float myHeading = 0; float myHeading = 0;
if (!screen->ignoreCompass) { if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) {
myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180
: screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); : 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)); 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 (!screen->ignoreCompass) if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING)
bearing -= myHeading; bearing -= myHeading;
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); 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; int chutil_bar_width = (isHighResolution) ? 100 : 50;
if (!config.bluetooth.enabled) { if (!config.bluetooth.enabled) {
#if defined(USE_EINK)
chutil_bar_width = (isHighResolution) ? 50 : 30;
#else
chutil_bar_width = (isHighResolution) ? 80 : 40; chutil_bar_width = (isHighResolution) ? 80 : 40;
#endif
} }
int chutil_bar_height = (isHighResolution) ? 12 : 7; int chutil_bar_height = (isHighResolution) ? 12 : 7;
int extraoffset = (isHighResolution) ? 6 : 3; int extraoffset = (isHighResolution) ? 6 : 3;
@@ -933,7 +911,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
// === Determine Compass Heading === // === Determine Compass Heading ===
float heading = 0; float heading = 0;
bool validHeading = false; bool validHeading = false;
if (screen->ignoreCompass) { if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) {
validHeading = true; validHeading = true;
} else { } else {
if (screen->hasHeading()) { if (screen->hasHeading()) {
@@ -999,7 +977,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
// "N" label // "N" label
float northAngle = 0; float northAngle = 0;
if (!config.display.compass_north_top) if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
northAngle = -heading; northAngle = -heading;
float radius = compassRadius; float radius = compassRadius;
int16_t nX = compassX + (radius - 1) * sin(northAngle); int16_t nX = compassX + (radius - 1) * sin(northAngle);
@@ -1042,7 +1020,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
// "N" label // "N" label
float northAngle = 0; float northAngle = 0;
if (!config.display.compass_north_top) if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
northAngle = -heading; northAngle = -heading;
float radius = compassRadius; float radius = compassRadius;
int16_t nX = compassX + (radius - 1) * sin(northAngle); 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) 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; static const uint8_t xbm[] = USERPREFS_OEM_IMAGE_DATA;
display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, if (isHighResolution) {
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2,
USERPREFS_OEM_IMAGE_HEIGHT, xbm); 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) { switch (USERPREFS_OEM_FONT_SIZE) {
case 0: case 0:
@@ -1084,7 +1069,9 @@ void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, O
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
const char *title = USERPREFS_OEM_TEXT; 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); display->setFont(FONT_SMALL);
// Draw region in upper left // Draw region in upper left
+103 -51
View File
@@ -11,11 +11,11 @@ const Emote emotes[] = {
{"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, // 👎 Thumbs Down {"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, // 👎 Thumbs Down
// --- Smileys (Multiple Unicode Aliases) --- // --- Smileys (Multiple Unicode Aliases) ---
{"\U0001F60A", smiley, smiley_width, smiley_height}, // 😊 Smiling Face with Smiling Eyes {"\U0001F60A", Smiling_Eyes, Smiling_Eyes_width, Smiling_Eyes_height}, // 😊 Smiling Eyes
{"\U0001F600", smiley, smiley_width, smiley_height}, // 😀 Grinning Face {"\U0001F600", Grinning, Grinning_width, Grinning_height}, // 😀 Grinning Face
{"\U0001F642", smiley, smiley_width, smiley_height}, // 🙂 Slightly Smiling Face {"\U0001F642", Slightly_Smiling, Slightly_Smiling_width, Slightly_Smiling_height}, // 🙂 Slightly Smiling Face
{"\U0001F609", smiley, smiley_width, smiley_height}, // 😉 Winking Face {"\U0001F609", Winking_Face, Winking_Face_width, Winking_Face_height}, // 😉 Winking Face
{"\U0001F601", smiley, smiley_width, smiley_height}, // 😁 Grinning Face with Smiling Eyes {"\U0001F601", Grinning_Smiling_Eyes, Grinning_Smiling_Eyes_width, Grinning_Smiling_Eyes_height}, // 😁 Grinning Smiling Eyes
// --- Question/Alert --- // --- Question/Alert ---
{"\u2753", question, question_width, question_height}, // ❓ Question Mark {"\u2753", question, question_width, question_height}, // ❓ Question Mark
@@ -23,10 +23,11 @@ const Emote emotes[] = {
// --- Laughing Faces --- // --- Laughing Faces ---
{"\U0001F602", haha, haha_width, haha_height}, // 😂 Face with Tears of Joy {"\U0001F602", haha, haha_width, haha_height}, // 😂 Face with Tears of Joy
{"\U0001F923", haha, haha_width, haha_height}, // 🤣 Rolling on the Floor Laughing {"\U0001F923", ROFL, ROFL_width, ROFL_height}, // 🤣 Rolling on the Floor Laughing
{"\U0001F606", haha, haha_width, haha_height}, // 😆 Smiling with Open Mouth and Closed Eyes {"\U0001F606", Smiling_Closed_Eyes, Smiling_Closed_Eyes_width, Smiling_Closed_Eyes_height}, // 😆 Smiling Closed Eyes
{"\U0001F605", haha, haha_width, haha_height}, // 😅 Smiling with Sweat {"\U0001F605", haha, haha_width, haha_height}, // 😅 Smiling with Sweat
{"\U0001F604", haha, haha_width, haha_height}, // 😄 Grinning Face with Smiling Eyes {"\U0001F604", Grinning_SmilingEyes2, Grinning_SmilingEyes2_width,
Grinning_SmilingEyes2_height}, // 😄 Grinning Face with Smiling Eyes
// --- Gestures and People --- // --- Gestures and People ---
{"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height}, // 👋 Waving Hand {"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height}, // 👋 Waving Hand
@@ -78,13 +79,45 @@ const unsigned char thumbdown[] PROGMEM = {
0x80, 0x09, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x80, 0x09, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00,
}; };
const unsigned char smiley[] PROGMEM = { const unsigned char Smiling_Eyes[] PROGMEM = {
0x00, 0xfe, 0x0f, 0x00, 0x80, 0x01, 0x30, 0x00, 0x40, 0x00, 0xc0, 0x00, 0x20, 0x00, 0x00, 0x01, 0x10, 0x00, 0x00, 0x02, 0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0xff, 0xc0, 0xe0, 0xff, 0xff, 0xc1,
0x08, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x10, 0x02, 0x0e, 0x0e, 0x10, 0x02, 0x09, 0x12, 0x10, 0xf0, 0xff, 0xff, 0xc3, 0xf8, 0xff, 0xff, 0xc7, 0xf8, 0xff, 0xff, 0xcf, 0xfc, 0xff, 0xff, 0xcf, 0xfc, 0xff, 0xff, 0xcf,
0x01, 0x09, 0x12, 0x20, 0x01, 0x0f, 0x1e, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x7e, 0xf8, 0xc3, 0xdf, 0x3e, 0xf0, 0x81, 0xdf, 0xbf, 0xf7, 0xbd, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0x3f, 0xff,
0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x81, 0x00, 0x20, 0x20, 0x6f, 0xff, 0xdf, 0xfe, 0x6f, 0xff, 0xdf, 0xfe, 0x9f, 0xff, 0x3f, 0xff, 0xfe, 0xff, 0xff, 0xdf, 0x7e, 0xff, 0xdf, 0xdf,
0x82, 0x00, 0x20, 0x10, 0x02, 0x01, 0x10, 0x10, 0x04, 0x02, 0x08, 0x08, 0x04, 0xfc, 0x07, 0x08, 0x08, 0x00, 0x00, 0x04, 0x7c, 0xff, 0xdf, 0xcf, 0xfc, 0xfe, 0xef, 0xcf, 0xf8, 0xf9, 0xf7, 0xc7, 0xf8, 0x03, 0xf8, 0xc7, 0xf0, 0xff, 0xff, 0xc3,
0x10, 0x00, 0x00, 0x02, 0x20, 0x00, 0x00, 0x01, 0x40, 0x00, 0xc0, 0x00, 0x80, 0x01, 0x30, 0x00, 0x00, 0xfe, 0x0f, 0x00}; 0xe0, 0xff, 0xff, 0xc1, 0xc0, 0xff, 0xff, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x07, 0xc0};
const unsigned char Grinning[] PROGMEM = {
0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0xff, 0xc0, 0xe0, 0xff, 0xff, 0xc1,
0xf0, 0xff, 0xff, 0xc3, 0xf8, 0xff, 0xff, 0xc7, 0xf8, 0xff, 0xff, 0xcf, 0xfc, 0xf9, 0xf3, 0xcf, 0xfc, 0xf0, 0xe1, 0xcf,
0xfe, 0xf0, 0xe1, 0xdf, 0xfe, 0xf0, 0xe1, 0xdf, 0xff, 0xf0, 0xe1, 0xff, 0xff, 0xf9, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0x80, 0xff, 0xbe, 0xff, 0xbf, 0xdf, 0x7e, 0x00, 0xc0, 0xdf,
0x7c, 0x00, 0xc0, 0xcf, 0xfc, 0x00, 0xe0, 0xcf, 0xf8, 0x01, 0xf0, 0xc7, 0xf8, 0x03, 0xf8, 0xc7, 0xf0, 0xff, 0xff, 0xc3,
0xe0, 0xff, 0xff, 0xc1, 0xc0, 0xff, 0xff, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x03, 0xc0};
const unsigned char Slightly_Smiling[] PROGMEM = {
0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0xff, 0xc0, 0xe0, 0xff, 0xff, 0xc1,
0xf0, 0xff, 0xff, 0xc3, 0xf8, 0xff, 0xff, 0xc7, 0xf8, 0xff, 0xff, 0xcf, 0xfc, 0xf9, 0xf3, 0xcf, 0xfc, 0xf0, 0xe1, 0xcf,
0xfe, 0xf0, 0xe1, 0xdf, 0xfe, 0xf0, 0xe1, 0xdf, 0xff, 0xf0, 0xe1, 0xff, 0xff, 0xf9, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xdf, 0x7e, 0xff, 0xdf, 0xdf,
0x7c, 0xff, 0xdf, 0xcf, 0xfc, 0xfe, 0xef, 0xcf, 0xf8, 0xf9, 0xf7, 0xc7, 0xf8, 0x03, 0xf8, 0xc7, 0xf0, 0xff, 0xff, 0xc3,
0xe0, 0xff, 0xff, 0xc1, 0xc0, 0xff, 0xff, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x03, 0xc0};
const unsigned char Winking_Face[] PROGMEM = {
0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0xff, 0xc0, 0xe0, 0xff, 0xff, 0xc1,
0xf0, 0xf0, 0xff, 0xc3, 0x78, 0xef, 0xc3, 0xc7, 0xb8, 0xdf, 0xbd, 0xcf, 0xfc, 0xf9, 0x7f, 0xcf, 0xfc, 0xf0, 0xff, 0xcf,
0xfe, 0xf0, 0xc3, 0xdf, 0xfe, 0xf0, 0x81, 0xdf, 0xff, 0xf0, 0xbf, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xdf, 0x7e, 0xff, 0xdf, 0xdf,
0x7c, 0xff, 0xdf, 0xcf, 0xfc, 0xfe, 0xef, 0xcf, 0xf8, 0xf9, 0xf7, 0xc7, 0xf8, 0x03, 0xf8, 0xc7, 0xf0, 0xff, 0xff, 0xc3,
0xe0, 0xff, 0xff, 0xc1, 0xc0, 0xff, 0xff, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x07, 0xc0};
const unsigned char Grinning_Smiling_Eyes[] PROGMEM = {
0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0xff, 0xc0, 0xe0, 0xff, 0xff, 0xc1,
0xf0, 0xff, 0xff, 0xc3, 0xf8, 0xff, 0xff, 0xc7, 0xf8, 0xff, 0xff, 0xcf, 0xfc, 0xf8, 0xe3, 0xcf, 0x7c, 0xf7, 0xdd, 0xcf,
0xbe, 0xef, 0xbe, 0xdf, 0xbe, 0xef, 0xbe, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0xff, 0x5e, 0x55, 0x55, 0xdf, 0x5e, 0x55, 0x55, 0xdf,
0x3c, 0x00, 0x80, 0xcf, 0x7c, 0x55, 0xd5, 0xcf, 0xf8, 0x54, 0xe5, 0xc7, 0xf8, 0x03, 0xf8, 0xc7, 0xf0, 0xff, 0xff, 0xc3,
0xe0, 0xff, 0xff, 0xc1, 0xc0, 0xff, 0xff, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x03, 0xc0};
const unsigned char question[] PROGMEM = { const unsigned char question[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x07, 0x00,
@@ -104,31 +137,52 @@ const unsigned char bang[] PROGMEM = {
}; };
const unsigned char haha[] PROGMEM = { const unsigned char haha[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0x7f, 0xc0, 0xe0, 0xf9, 0xf3, 0xc0,
0x00, 0xFC, 0x0F, 0x00, 0x00, 0x1F, 0x3E, 0x00, 0x80, 0x03, 0x70, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0xC0, 0x00, 0xC2, 0x00, 0xf0, 0xfe, 0xef, 0xc1, 0x38, 0xff, 0x9f, 0xc3, 0xd8, 0xff, 0x7f, 0xc3, 0xfc, 0xf8, 0xe3, 0xc7, 0x7c, 0xf7, 0xdd, 0xcf,
0x60, 0x00, 0x03, 0x00, 0x60, 0x00, 0xC1, 0x1F, 0x60, 0x80, 0x8F, 0x31, 0x30, 0x0E, 0x80, 0x31, 0x30, 0x10, 0x30, 0x1F, 0xbe, 0xef, 0xbe, 0xcf, 0xfe, 0xff, 0xff, 0xcf, 0xef, 0xff, 0xff, 0xde, 0xe7, 0xff, 0xff, 0xdc, 0xeb, 0xff, 0xff, 0xda,
0x30, 0x08, 0x58, 0x00, 0x30, 0x04, 0x6C, 0x03, 0x60, 0x00, 0xF3, 0x01, 0x60, 0xC0, 0xFC, 0x01, 0x80, 0x38, 0xBF, 0x01, 0xed, 0xff, 0xff, 0xd6, 0xee, 0xff, 0xff, 0xce, 0x36, 0x00, 0x80, 0xcd, 0xb8, 0xff, 0xbf, 0xc3, 0x7e, 0x00, 0xc0, 0xdf,
0xE0, 0xC5, 0xDF, 0x00, 0xB0, 0xF9, 0xEF, 0x00, 0x30, 0xF1, 0x73, 0x00, 0xB0, 0x1D, 0x3E, 0x00, 0xF0, 0xFD, 0x0F, 0x00, 0x7c, 0x00, 0xc0, 0xcf, 0xfc, 0x00, 0xe0, 0xcf, 0xf8, 0x01, 0xf0, 0xc7, 0xf8, 0x03, 0xf8, 0xc7, 0xf0, 0xff, 0xff, 0xc3,
0xE0, 0xE0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xff, 0xff, 0xc1, 0xc0, 0xff, 0xff, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x03, 0xc0};
};
const unsigned char ROFL[] PROGMEM = {
0x00, 0x00, 0x00, 0xc0, 0x00, 0xfc, 0x07, 0xc0, 0x00, 0xff, 0x1f, 0xc0, 0x80, 0xff, 0x7f, 0xc0, 0xc0, 0xff, 0xff, 0xc0,
0xe0, 0x9f, 0xff, 0xc1, 0xf0, 0x9f, 0xff, 0xc0, 0xf8, 0x9f, 0x7f, 0xcb, 0xf8, 0x9f, 0xbf, 0xcb, 0xfc, 0x9f, 0xdf, 0xdb,
0xfc, 0x1f, 0x08, 0xdc, 0xfe, 0x1f, 0xf8, 0xfe, 0xfe, 0xff, 0xff, 0xfe, 0x1e, 0xf0, 0x7f, 0xfe, 0x1e, 0xf0, 0xbf, 0xfe,
0xfe, 0xf3, 0xdf, 0xfe, 0xfe, 0xf3, 0x6f, 0xfe, 0xfe, 0xf3, 0x37, 0xfe, 0xfe, 0xeb, 0x1b, 0xfe, 0xfc, 0xef, 0x0d, 0xde,
0xfc, 0xe7, 0x06, 0xcf, 0xf8, 0x6b, 0x83, 0xcf, 0xf8, 0x0d, 0xc0, 0xc7, 0xf0, 0xed, 0xff, 0xc7, 0xe0, 0xee, 0xff, 0xc3,
0xc0, 0xee, 0xff, 0xc1, 0x80, 0xee, 0xff, 0xc0, 0x00, 0xe6, 0x3f, 0xc0, 0x00, 0xf0, 0x0f, 0xc0, 0x00, 0x00, 0x00, 0xc0};
const unsigned char Smiling_Closed_Eyes[] PROGMEM = {
0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0xff, 0xc0, 0xe0, 0xff, 0xff, 0xc1,
0xf0, 0xff, 0xff, 0xc3, 0xf8, 0xff, 0xff, 0xc7, 0xf8, 0xff, 0xff, 0xcf, 0x7c, 0xfe, 0xcf, 0xcf, 0xfc, 0xfc, 0xe7, 0xcf,
0xfe, 0xf9, 0xf3, 0xdf, 0xfe, 0xf3, 0xf9, 0xdf, 0xff, 0xf9, 0xf3, 0xff, 0xff, 0xfc, 0xe7, 0xff, 0x7f, 0xfe, 0xcf, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0x80, 0xff, 0xbe, 0xff, 0xbf, 0xdf, 0x7e, 0x00, 0xc0, 0xdf,
0x7c, 0x00, 0xc0, 0xcf, 0xfc, 0x00, 0xe0, 0xcf, 0xf8, 0x01, 0xf0, 0xc7, 0xf8, 0x03, 0xf8, 0xc7, 0xf0, 0xff, 0xff, 0xc3,
0xe0, 0xff, 0xff, 0xc1, 0xc0, 0xff, 0xff, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x03, 0xc0};
const unsigned char Grinning_SmilingEyes2[] PROGMEM = {
0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0x7f, 0xc0, 0xe0, 0xff, 0xff, 0xc0,
0xf0, 0xff, 0xff, 0xc1, 0xf8, 0xff, 0xff, 0xc3, 0xf8, 0xff, 0xff, 0xc3, 0xfc, 0xf8, 0xe3, 0xc7, 0x7c, 0xf7, 0xdd, 0xc7,
0xbe, 0xef, 0xbe, 0xcf, 0xfe, 0xff, 0xff, 0xcf, 0xff, 0xff, 0xff, 0xdf, 0xff, 0xff, 0xff, 0xdf, 0xff, 0xff, 0xff, 0xdf,
0xff, 0xff, 0xff, 0xdf, 0xff, 0xff, 0xff, 0xdf, 0x3f, 0x00, 0x80, 0xdf, 0xbe, 0xff, 0xbf, 0xcf, 0x7e, 0x00, 0xc0, 0xcf,
0x7c, 0x00, 0xc0, 0xc7, 0xfc, 0x00, 0xe0, 0xc7, 0xf8, 0x01, 0xf0, 0xc3, 0xf8, 0x03, 0xf8, 0xc3, 0xf0, 0xff, 0xff, 0xc1,
0xe0, 0xff, 0xff, 0xc0, 0xc0, 0xff, 0x7f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x03, 0xc0};
const unsigned char wave_icon[] PROGMEM = { const unsigned char wave_icon[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x7f, 0xc0, 0x00, 0x00, 0xc0, 0xc1, 0x00, 0x00, 0x00, 0xc7,
0x00, 0x0C, 0x9C, 0x01, 0x80, 0x17, 0x20, 0x01, 0x80, 0x26, 0x46, 0x02, 0x80, 0x44, 0x88, 0x02, 0xC0, 0x89, 0x8A, 0x02, 0x00, 0x00, 0x1e, 0xcc, 0x00, 0x00, 0x30, 0xc8, 0x00, 0x00, 0x60, 0xd8, 0x00, 0x08, 0xc0, 0xd0, 0x00, 0x1a, 0x81, 0xd1,
0x40, 0x93, 0x8B, 0x02, 0x40, 0x26, 0x13, 0x00, 0x80, 0x44, 0x16, 0x00, 0xC0, 0x89, 0x24, 0x00, 0x40, 0x93, 0x60, 0x00, 0x00, 0x36, 0x03, 0xd3, 0x80, 0x6d, 0x06, 0xd2, 0x00, 0xdb, 0x0c, 0xc2, 0x80, 0xb6, 0x1d, 0xc0, 0x80, 0x6d, 0x1f, 0xc0,
0x40, 0x26, 0x40, 0x00, 0x80, 0x0C, 0x80, 0x00, 0x00, 0x09, 0x80, 0x00, 0x00, 0x02, 0x80, 0x00, 0x40, 0x06, 0x80, 0x00, 0x00, 0xdb, 0x3f, 0xc0, 0x00, 0xf6, 0x7f, 0xc0, 0x00, 0xfc, 0x7f, 0xc0, 0x08, 0xf8, 0x7f, 0xc0, 0x48, 0xf0, 0x7f, 0xc0,
0x50, 0x0C, 0x80, 0x00, 0x50, 0x08, 0x40, 0x00, 0x90, 0x10, 0x20, 0x00, 0xB0, 0x21, 0x10, 0x00, 0x20, 0x47, 0x18, 0x00, 0x48, 0xe0, 0x7f, 0xc0, 0xc8, 0xc0, 0x3f, 0xc0, 0x98, 0x81, 0x1f, 0xc0, 0x10, 0x03, 0x00, 0xc0, 0x30, 0x0e, 0x00, 0xc0,
0x40, 0x80, 0x0F, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x38, 0x00, 0xc0, 0xe0, 0x00, 0x00, 0xc0, 0x80, 0x07, 0x00, 0xc0, 0x00, 0x1e, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0};
};
const unsigned char cowboy[] PROGMEM = { const unsigned char cowboy[] PROGMEM = {
0x00, 0xF0, 0x03, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x3C, 0xFE, 0x1F, 0x0F, 0x00, 0x0c, 0x0c, 0xc0, 0x00, 0x02, 0x10, 0xc0, 0x00, 0x01, 0x20, 0xc0, 0xbc, 0x00, 0x40, 0xcf, 0xc2, 0x01, 0xe0, 0xd0,
0xFE, 0xFE, 0xDF, 0x1F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0x01, 0x01, 0x20, 0xe0, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0,
0x3E, 0xC0, 0x00, 0x1F, 0x1E, 0x00, 0x00, 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x08, 0x0E, 0x1C, 0x04, 0x00, 0x0E, 0x1C, 0x00, 0xc1, 0x3f, 0xff, 0xe0, 0xe1, 0xff, 0xff, 0xe1, 0xf2, 0xf3, 0xf3, 0xd3, 0xf4, 0xf1, 0xe3, 0xcb, 0xfc, 0xf1, 0xe3, 0xc7,
0x04, 0x0E, 0x1C, 0x08, 0x04, 0x0E, 0x1C, 0x08, 0x04, 0x04, 0x08, 0x08, 0x04, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x08, 0xf8, 0xf1, 0xe3, 0xc7, 0xf8, 0xf1, 0xe3, 0xc7, 0xf8, 0xfb, 0xf7, 0xc7, 0xf8, 0xff, 0xff, 0xc7, 0xf8, 0xff, 0xff, 0xc7,
0x8C, 0x07, 0x70, 0x0C, 0x88, 0xFC, 0x4F, 0x04, 0x88, 0x01, 0x40, 0x04, 0x90, 0xFF, 0x7F, 0x02, 0x30, 0x03, 0x30, 0x03, 0x70, 0xf8, 0x8f, 0xc3, 0x70, 0x03, 0xb0, 0xc3, 0x70, 0xfe, 0xbf, 0xc3, 0x60, 0x00, 0x80, 0xc1, 0xc0, 0x00, 0xc0, 0xc0,
0x60, 0x0E, 0x9C, 0x01, 0xC0, 0xF8, 0xC7, 0x00, 0x80, 0x01, 0x60, 0x00, 0x00, 0x0E, 0x1C, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x80, 0x01, 0x60, 0xc0, 0x00, 0x07, 0x38, 0xc0, 0x00, 0xfe, 0x1f, 0xc0, 0x00, 0xf0, 0x03, 0xc0, 0x00, 0x00, 0x00, 0xc0};
};
const unsigned char deadmau5[] PROGMEM = { const unsigned char deadmau5[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x07, 0x00,
@@ -181,13 +235,12 @@ const unsigned char fog[] PROGMEM = {
}; };
const unsigned char devil[] PROGMEM = { const unsigned char devil[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x10, 0x03, 0xC0, 0x01, 0x38, 0x07, 0x7C, 0x0F, 0x38, 0x1F, 0x03, 0x30, 0x1E, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x01, 0x00, 0x00, 0xe0, 0x03, 0x00, 0x00, 0xf0, 0x0f, 0xfc, 0x0f, 0xfc,
0xFE, 0x01, 0xE0, 0x1F, 0x7E, 0x00, 0x80, 0x1F, 0x3C, 0x00, 0x00, 0x0F, 0x1C, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x06, 0x3f, 0xff, 0x3f, 0xff, 0xfe, 0xff, 0xff, 0xdf, 0xfe, 0xff, 0xff, 0xdf, 0xfe, 0xff, 0xff, 0xdf, 0xfc, 0xff, 0xff, 0xcf,
0x08, 0x00, 0x00, 0x04, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x0E, 0x1C, 0x0C, 0xfc, 0xff, 0xff, 0xcf, 0xf8, 0xff, 0xff, 0xc7, 0xf0, 0xff, 0xff, 0xc3, 0xf0, 0xff, 0xff, 0xc3, 0xf0, 0xf1, 0xe3, 0xc3,
0x0C, 0x18, 0x06, 0x0C, 0x0C, 0x1C, 0x06, 0x0C, 0x0C, 0x1C, 0x0E, 0x0C, 0x0C, 0x1C, 0x0E, 0x0C, 0x0C, 0x0C, 0x06, 0x0C, 0xf0, 0xe7, 0xf9, 0xc3, 0xf0, 0xe7, 0xf9, 0xc3, 0xf0, 0xe3, 0xf1, 0xc3, 0xf0, 0xe3, 0xf1, 0xc3, 0xf0, 0xe7, 0xf9, 0xc3,
0x08, 0x00, 0x00, 0x06, 0x18, 0x02, 0x10, 0x06, 0x10, 0x0C, 0x0C, 0x03, 0x30, 0xF8, 0x07, 0x03, 0x60, 0xE0, 0x80, 0x01, 0xf0, 0xff, 0xff, 0xc3, 0xe0, 0xfd, 0xef, 0xc1, 0xe0, 0xf3, 0xf3, 0xc1, 0xc0, 0x07, 0xf8, 0xc0, 0x80, 0x1f, 0x7e, 0xc0,
0xC0, 0x00, 0xC0, 0x00, 0x80, 0x01, 0x70, 0x00, 0x00, 0x06, 0x1C, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x03, 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0};
};
const unsigned char heart[] PROGMEM = { const unsigned char heart[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xF0, 0x00, 0xF8, 0x0F, 0xFC, 0x07, 0xFC, 0x1F, 0x06, 0x0E, 0xFE, 0x3F, 0x03, 0x18, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xF0, 0x00, 0xF8, 0x0F, 0xFC, 0x07, 0xFC, 0x1F, 0x06, 0x0E, 0xFE, 0x3F, 0x03, 0x18,
@@ -199,13 +252,12 @@ const unsigned char heart[] PROGMEM = {
}; };
const unsigned char poo[] PROGMEM = { const unsigned char poo[] PROGMEM = {
0x00, 0x1C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xEC, 0x01, 0x00, 0x00, 0x8C, 0x07, 0x00, 0x00, 0x0C, 0x06, 0x00, 0x00, 0x1c, 0x00, 0xc0, 0x00, 0x7c, 0x00, 0xc0, 0x00, 0xfc, 0x00, 0xc0, 0x00, 0x7c, 0x03, 0xc0, 0x00, 0xbe, 0x03, 0xc0,
0x00, 0x24, 0x0C, 0x00, 0x00, 0x34, 0x08, 0x00, 0x00, 0x1F, 0x08, 0x00, 0xC0, 0x0F, 0x08, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x00, 0xdf, 0x0f, 0xc0, 0x80, 0xcf, 0x0f, 0xc0, 0xc0, 0xf1, 0x0f, 0xc0, 0x60, 0xfc, 0x0f, 0xc0, 0x30, 0xff, 0x07, 0xc0,
0x60, 0x00, 0x7C, 0x00, 0x60, 0x00, 0xC6, 0x00, 0x20, 0x00, 0xCB, 0x00, 0xA0, 0xC7, 0xFF, 0x00, 0xE0, 0x7F, 0xF7, 0x00, 0x90, 0xff, 0x3b, 0xc0, 0xc0, 0xff, 0x7d, 0xc0, 0xf8, 0xff, 0xfc, 0xc0, 0xf8, 0x3f, 0xf0, 0xc0, 0x78, 0x88, 0xc0, 0xc0,
0xF0, 0x18, 0xE3, 0x03, 0x78, 0x18, 0x41, 0x03, 0x6C, 0x9B, 0x5D, 0x06, 0x64, 0x9B, 0x5D, 0x04, 0x44, 0x1A, 0x41, 0x04, 0x20, 0xe3, 0x18, 0xc0, 0x98, 0xe7, 0xbc, 0xc1, 0x9c, 0x64, 0xa4, 0xc3, 0x9e, 0x64, 0xa4, 0xc7, 0xbe, 0xe4, 0xa4, 0xc7,
0x4C, 0xD8, 0x63, 0x06, 0xF8, 0xFC, 0x36, 0x06, 0xFE, 0x0F, 0x9C, 0x1F, 0x07, 0x03, 0xC0, 0x30, 0x03, 0x00, 0x78, 0x20, 0xbc, 0x27, 0xbc, 0xc7, 0x38, 0x03, 0xd9, 0xc3, 0x00, 0xf0, 0x63, 0xc0, 0xf8, 0xfc, 0x3f, 0xcf, 0xfc, 0xff, 0x87, 0xdf,
0x01, 0x00, 0x1F, 0x20, 0x03, 0xE0, 0x03, 0x20, 0x07, 0x7E, 0x04, 0x30, 0xFE, 0x0F, 0xFC, 0x1F, 0xF0, 0x00, 0xF0, 0x0F, 0xfe, 0xff, 0xe0, 0xdf, 0xfc, 0x1f, 0xfe, 0xdf, 0xf8, 0x07, 0xf8, 0xcf, 0xf0, 0x03, 0xe0, 0xc7, 0x00, 0x00, 0x00, 0xc0};
};
const unsigned char bell_icon[] PROGMEM = { const unsigned char bell_icon[] PROGMEM = {
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b11110000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b11110000,
+31 -3
View File
@@ -22,9 +22,25 @@ extern const int numEmotes;
extern const unsigned char thumbup[] PROGMEM; extern const unsigned char thumbup[] PROGMEM;
extern const unsigned char thumbdown[] PROGMEM; extern const unsigned char thumbdown[] PROGMEM;
#define smiley_height 30 #define Smiling_Eyes_height 30
#define smiley_width 30 #define Smiling_Eyes_width 30
extern const unsigned char smiley[] PROGMEM; extern const unsigned char Smiling_Eyes[] PROGMEM;
#define Grinning_height 30
#define Grinning_width 30
extern const unsigned char Grinning[] PROGMEM;
#define Slightly_Smiling_height 30
#define Slightly_Smiling_width 30
extern const unsigned char Slightly_Smiling[] PROGMEM;
#define Winking_Face_height 30
#define Winking_Face_width 30
extern const unsigned char Winking_Face[] PROGMEM;
#define Grinning_Smiling_Eyes_height 30
#define Grinning_Smiling_Eyes_width 30
extern const unsigned char Grinning_Smiling_Eyes[] PROGMEM;
#define question_height 25 #define question_height 25
#define question_width 25 #define question_width 25
@@ -38,6 +54,18 @@ extern const unsigned char bang[] PROGMEM;
#define haha_width 30 #define haha_width 30
extern const unsigned char haha[] PROGMEM; extern const unsigned char haha[] PROGMEM;
#define ROFL_height 30
#define ROFL_width 30
extern const unsigned char ROFL[] PROGMEM;
#define Smiling_Closed_Eyes_height 30
#define Smiling_Closed_Eyes_width 30
extern const unsigned char Smiling_Closed_Eyes[] PROGMEM;
#define Grinning_SmilingEyes2_height 30
#define Grinning_SmilingEyes2_width 30
extern const unsigned char Grinning_SmilingEyes2[] PROGMEM;
#define wave_icon_height 30 #define wave_icon_height 30
#define wave_icon_width 30 #define wave_icon_width 30
extern const unsigned char wave_icon[] PROGMEM; extern const unsigned char wave_icon[] PROGMEM;
+4 -1
View File
@@ -1363,7 +1363,7 @@ void setup()
if (!rIf->reconfigure()) { if (!rIf->reconfigure()) {
LOG_WARN("Reconfigure failed, rebooting"); LOG_WARN("Reconfigure failed, rebooting");
if (screen) { if (screen) {
screen->showOverlayBanner("Rebooting..."); screen->showSimpleBanner("Rebooting...");
} }
rebootAtMsec = millis() + 5000; rebootAtMsec = millis() + 5000;
} }
@@ -1436,6 +1436,9 @@ void setup()
LOG_DEBUG("Free heap : %7d bytes", ESP.getFreeHeap()); LOG_DEBUG("Free heap : %7d bytes", ESP.getFreeHeap());
LOG_DEBUG("Free PSRAM : %7d bytes", ESP.getFreePsram()); LOG_DEBUG("Free PSRAM : %7d bytes", ESP.getFreePsram());
#endif #endif
// We manually run this to update the NodeStatus
nodeDB->notifyObservers(true);
} }
#endif #endif
+47 -21
View File
@@ -407,6 +407,7 @@ NodeDB::NodeDB()
#endif #endif
} }
#endif #endif
sortMeshDB();
saveToDisk(saveWhat); saveToDisk(saveWhat);
} }
@@ -1000,7 +1001,10 @@ void NodeDB::cleanupMeshDB()
meshNodes->at(i).user.public_key.size = 0; meshNodes->at(i).user.public_key.size = 0;
} }
} }
meshNodes->at(newPos++) = meshNodes->at(i); if (newPos != i)
meshNodes->at(newPos++) = meshNodes->at(i);
else
newPos++;
} else { } else {
removed++; removed++;
} }
@@ -1087,8 +1091,8 @@ LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t
if (f) { if (f) {
LOG_INFO("Load %s", filename); LOG_INFO("Load %s", filename);
pb_istream_t stream = {&readcb, &f, protoSize}; pb_istream_t stream = {&readcb, &f, protoSize};
if (fields != &meshtastic_NodeDatabase_msg) // contains a vector object
memset(dest_struct, 0, objSize); memset(dest_struct, 0, objSize);
if (!pb_decode(&stream, fields, dest_struct)) { if (!pb_decode(&stream, fields, dest_struct)) {
LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream));
state = LoadFileResult::DECODE_FAILED; state = LoadFileResult::DECODE_FAILED;
@@ -1156,7 +1160,7 @@ void NodeDB::loadFromDisk()
LOG_WARN("Node count %d exceeds MAX_NUM_NODES %d, truncating", numMeshNodes, MAX_NUM_NODES); LOG_WARN("Node count %d exceeds MAX_NUM_NODES %d, truncating", numMeshNodes, MAX_NUM_NODES);
numMeshNodes = MAX_NUM_NODES; numMeshNodes = MAX_NUM_NODES;
} }
meshNodes->resize(MAX_NUM_NODES + 1); // The rp2040, rp2035, and maybe other targets, have a problem doing a sort() when full meshNodes->resize(MAX_NUM_NODES);
// static DeviceState scratch; We no longer read into a tempbuf because this structure is 15KB of valuable RAM // static DeviceState scratch; We no longer read into a tempbuf because this structure is 15KB of valuable RAM
state = loadProto(deviceStateFileName, meshtastic_DeviceState_size, sizeof(meshtastic_DeviceState), state = loadProto(deviceStateFileName, meshtastic_DeviceState_size, sizeof(meshtastic_DeviceState),
@@ -1690,26 +1694,48 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
} }
} }
void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId)
{
meshtastic_NodeInfoLite *lite = getMeshNode(nodeId);
if (lite && lite->is_favorite != is_favorite) {
lite->is_favorite = is_favorite;
sortMeshDB();
saveNodeDatabaseToDisk();
}
}
void NodeDB::pause_sort(bool paused)
{
sortingIsPaused = paused;
}
void NodeDB::sortMeshDB() void NodeDB::sortMeshDB()
{ {
if (!Throttle::isWithinTimespanMs(lastSort, 1000 * 5)) { if (!sortingIsPaused && (lastSort == 0 || !Throttle::isWithinTimespanMs(lastSort, 1000 * 5))) {
lastSort = millis(); lastSort = millis();
std::sort(meshNodes->begin(), meshNodes->begin() + numMeshNodes, bool changed = true;
[](const meshtastic_NodeInfoLite &a, const meshtastic_NodeInfoLite &b) { while (changed) { // dumb reverse bubble sort, but probably not bad for what we're doing
if (a.num == myNodeInfo.my_node_num && b.num == myNodeInfo.my_node_num) // in theory impossible changed = false;
return false; for (int i = numMeshNodes - 1; i > 0; i--) { // lowest case this should examine is i == 1
if (a.num == myNodeInfo.my_node_num) { if (meshNodes->at(i - 1).num == getNodeNum()) {
return true; // noop
} } else if (meshNodes->at(i).num ==
if (b.num == myNodeInfo.my_node_num) { getNodeNum()) { // in the oddball case our own node num is not at location 0, put it there
return false; // TODO: Look for at(i-1) also matching own node num, and throw the DB in the trash
} std::swap(meshNodes->at(i), meshNodes->at(i - 1));
bool aFav = a.is_favorite; changed = true;
bool bFav = b.is_favorite; } else if (meshNodes->at(i).is_favorite && !meshNodes->at(i - 1).is_favorite) {
if (aFav != bFav) std::swap(meshNodes->at(i), meshNodes->at(i - 1));
return aFav; changed = true;
return a.last_heard > b.last_heard; } else if (!meshNodes->at(i).is_favorite && meshNodes->at(i - 1).is_favorite) {
}); // noop
} else if (meshNodes->at(i).last_heard > meshNodes->at(i - 1).last_heard) {
std::swap(meshNodes->at(i), meshNodes->at(i - 1));
changed = true;
}
}
}
LOG_INFO("Sort took %u milliseconds", millis() - lastSort);
} }
} }
+25 -10
View File
@@ -191,6 +191,16 @@ class NodeDB
*/ */
bool updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex = 0); bool updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex = 0);
/*
* Sets a node either favorite or unfavorite
*/
void set_favorite(bool is_favorite, uint32_t nodeId);
/**
* Other functions like the node picker can request a pause in the node sorting
*/
void pause_sort(bool paused);
/// @return our node number /// @return our node number
NodeNum getNodeNum() { return myNodeInfo.my_node_num; } NodeNum getNodeNum() { return myNodeInfo.my_node_num; }
@@ -208,9 +218,6 @@ class NodeDB
their denial?) their denial?)
*/ */
/// pick a provisional nodenum we hope no one is using
void pickNewNodeNum();
// get channel channel index we heard a nodeNum on, defaults to 0 if not found // get channel channel index we heard a nodeNum on, defaults to 0 if not found
uint8_t getMeshNodeChannel(NodeNum n); uint8_t getMeshNodeChannel(NodeNum n);
@@ -278,6 +285,14 @@ class NodeDB
bool restorePreferences(meshtastic_AdminMessage_BackupLocation location, bool restorePreferences(meshtastic_AdminMessage_BackupLocation location,
int restoreWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); int restoreWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS);
/// Notify observers of changes to the DB
void notifyObservers(bool forceUpdate = false)
{
// Notify observers of the current node state
const meshtastic::NodeStatus status = meshtastic::NodeStatus(getNumOnlineMeshNodes(), getNumMeshNodes(), forceUpdate);
newStatus.notifyObservers(&status);
}
private: private:
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
@@ -286,13 +301,13 @@ class 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);
/// Notify observers of changes to the DB /*
void notifyObservers(bool forceUpdate = false) * Internal boolean to track sorting paused
{ */
// Notify observers of the current node state bool sortingIsPaused = false;
const meshtastic::NodeStatus status = meshtastic::NodeStatus(getNumOnlineMeshNodes(), getNumMeshNodes(), forceUpdate);
newStatus.notifyObservers(&status); /// pick a provisional nodenum we hope no one is using
} void pickNewNodeNum();
/// read our db from flash /// read our db from flash
void loadFromDisk(); void loadFromDisk();
+1 -1
View File
@@ -1190,7 +1190,7 @@ void AdminModule::reboot(int32_t seconds)
{ {
LOG_INFO("Reboot in %d seconds", seconds); LOG_INFO("Reboot in %d seconds", seconds);
if (screen) if (screen)
screen->showOverlayBanner("Rebooting...", 0); // stays on screen screen->showSimpleBanner("Rebooting...", 0); // stays on screen
rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000); rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000);
} }
+15 -6
View File
@@ -442,9 +442,13 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event
return 1; return 1;
} }
// UP if (isUp) {
if (isUp && destIndex > 0) { if (destIndex > 0) {
destIndex--; destIndex--;
} else if (totalEntries > 0) {
destIndex = totalEntries - 1;
}
if ((destIndex / columns) < scrollIndex) if ((destIndex / columns) < scrollIndex)
scrollIndex = destIndex / columns; scrollIndex = destIndex / columns;
else if ((destIndex / columns) >= (scrollIndex + visibleRows)) else if ((destIndex / columns) >= (scrollIndex + visibleRows))
@@ -454,9 +458,14 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event
return 1; return 1;
} }
// DOWN if (isDown) {
if (isDown && destIndex + 1 < totalEntries) { if (destIndex + 1 < totalEntries) {
destIndex++; destIndex++;
} else if (totalEntries > 0) {
destIndex = 0;
scrollIndex = 0;
}
if ((destIndex / columns) >= (scrollIndex + visibleRows)) if ((destIndex / columns) >= (scrollIndex + visibleRows))
scrollIndex = (destIndex / columns) - visibleRows + 1; scrollIndex = (destIndex / columns) - visibleRows + 1;
+12 -5
View File
@@ -59,7 +59,7 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
r->hash1.size == 0) { r->hash1.size == 0) {
memcpy(hash2, r->hash2.bytes, 32); memcpy(hash2, r->hash2.bytes, 32);
if (screen) if (screen)
screen->showOverlayBanner("Enter Security Number", 30000); screen->showSimpleBanner("Enter Security Number", 30000);
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
cn->level = meshtastic_LogRecord_Level_WARNING; cn->level = meshtastic_LogRecord_Level_WARNING;
@@ -82,12 +82,19 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
static const char *optionsArray[] = {"ACCEPT", "REJECT"}; static const char *optionsArray[] = {"ACCEPT", "REJECT"};
LOG_INFO("Hash1 matches!"); LOG_INFO("Hash1 matches!");
if (screen) { if (screen) {
screen->showOverlayBanner(message, 30000, optionsArray, 2, [=](int selected) { graphics::BannerOverlayOptions options;
options.message = message;
options.durationMs = 30000;
options.optionsArrayPtr = optionsArray;
options.optionsCount = 2;
options.notificationType = graphics::notificationTypeEnum::selection_picker;
options.bannerCallback = [=](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;
} }
}); };
screen->showOverlayBanner(options);
} }
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
cn->level = meshtastic_LogRecord_Level_WARNING; cn->level = meshtastic_LogRecord_Level_WARNING;
@@ -185,7 +192,7 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
responsePacket->pki_encrypted = true; responsePacket->pki_encrypted = true;
if (screen) { if (screen) {
snprintf(message, 25, "Security Number \n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000); snprintf(message, 25, "Security Number \n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000);
screen->showOverlayBanner(message, 30000); screen->showSimpleBanner(message, 30000);
LOG_WARN("%s", message); LOG_WARN("%s", message);
} }
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
@@ -255,7 +262,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
sprintf(message, "Verification: \n"); sprintf(message, "Verification: \n");
generateVerificationCode(message + 15); // send the toPhone packet generateVerificationCode(message + 15); // send the toPhone packet
if (screen) { if (screen) {
screen->showOverlayBanner(message, 30000); screen->showSimpleBanner(message, 30000);
} }
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
cn->level = meshtastic_LogRecord_Level_WARNING; cn->level = meshtastic_LogRecord_Level_WARNING;
+10 -10
View File
@@ -47,7 +47,7 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
bool isMuted = externalNotificationModule->getMute(); bool isMuted = externalNotificationModule->getMute();
externalNotificationModule->setMute(!isMuted); externalNotificationModule->setMute(!isMuted);
IF_SCREEN(graphics::isMuted = !isMuted; if (!isMuted) externalNotificationModule->stopNow(); IF_SCREEN(graphics::isMuted = !isMuted; if (!isMuted) externalNotificationModule->stopNow();
screen->showOverlayBanner(isMuted ? "Notifications\nEnabled" : "Notifications\nDisabled", 3000);) screen->showSimpleBanner(isMuted ? "Notifications\nEnabled" : "Notifications\nDisabled", 3000);)
} }
return 0; return 0;
// Bluetooth // Bluetooth
@@ -58,24 +58,24 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
#if defined(ARDUINO_ARCH_NRF52) #if defined(ARDUINO_ARCH_NRF52)
if (!config.bluetooth.enabled) { if (!config.bluetooth.enabled) {
disableBluetooth(); disableBluetooth();
IF_SCREEN(screen->showOverlayBanner("Bluetooth OFF\nRebooting", 3000)); IF_SCREEN(screen->showSimpleBanner("Bluetooth OFF\nRebooting", 3000));
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 2000; rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 2000;
} else { } else {
IF_SCREEN(screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000)); IF_SCREEN(screen->showSimpleBanner("Bluetooth ON\nRebooting", 3000));
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
} }
#else #else
if (!config.bluetooth.enabled) { if (!config.bluetooth.enabled) {
disableBluetooth(); disableBluetooth();
IF_SCREEN(screen->showOverlayBanner("Bluetooth OFF", 3000)); IF_SCREEN(screen->showSimpleBanner("Bluetooth OFF", 3000));
} else { } else {
IF_SCREEN(screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000)); IF_SCREEN(screen->showSimpleBanner("Bluetooth ON\nRebooting", 3000));
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
} }
#endif #endif
return 0; return 0;
case INPUT_BROKER_MSG_REBOOT: case INPUT_BROKER_MSG_REBOOT:
IF_SCREEN(screen->showOverlayBanner("Rebooting...", 0)); IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0));
nodeDB->saveToDisk(); nodeDB->saveToDisk();
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
// runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; // runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
@@ -92,7 +92,7 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
gps->toggleGpsMode(); gps->toggleGpsMode();
const char *msg = const char *msg =
(config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) ? "GPS Enabled" : "GPS Disabled"; (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) ? "GPS Enabled" : "GPS Disabled";
IF_SCREEN(screen->forceDisplay(); screen->showOverlayBanner(msg, 3000);) IF_SCREEN(screen->forceDisplay(); screen->showSimpleBanner(msg, 3000);)
} }
#endif #endif
return true; return true;
@@ -100,15 +100,15 @@ 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\nSent", 3000)); IF_SCREEN(screen->showSimpleBanner("Position\nSent", 3000));
} else { } else {
IF_SCREEN(screen->showOverlayBanner("Node Info\nSent", 3000)); IF_SCREEN(screen->showSimpleBanner("Node Info\nSent", 3000));
} }
return true; return true;
// Power control // Power control
case INPUT_BROKER_SHUTDOWN: case INPUT_BROKER_SHUTDOWN:
LOG_ERROR("Shutting Down"); LOG_ERROR("Shutting Down");
IF_SCREEN(screen->showOverlayBanner("Shutting Down...")); IF_SCREEN(screen->showSimpleBanner("Shutting Down..."));
nodeDB->saveToDisk(); nodeDB->saveToDisk();
shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000; shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000;
// runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; // runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
@@ -450,7 +450,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
if (isOwnTelemetry && bannerMsg && isCooldownOver) { if (isOwnTelemetry && bannerMsg && isCooldownOver) {
LOG_INFO("drawFrame: IAQ %d (own) — showing banner: %s", m.iaq, bannerMsg); LOG_INFO("drawFrame: IAQ %d (own) — showing banner: %s", m.iaq, bannerMsg);
screen->showOverlayBanner(bannerMsg, 3000); screen->showSimpleBanner(bannerMsg, 3000);
// Only buzz if IAQ is over 200 // Only buzz if IAQ is over 200
if (m.iaq > 200 && moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) { if (m.iaq > 200 && moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) {
+2 -2
View File
@@ -137,7 +137,7 @@ 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->ignoreCompass) { if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) {
myHeading = 0; myHeading = 0;
} else { } else {
if (screen->hasHeading()) if (screen->hasHeading())
@@ -152,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 (!screen->ignoreCompass) if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING)
bearingToOther -= myHeading; bearingToOther -= myHeading;
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther);
+49 -12
View File
@@ -9,6 +9,7 @@
#include "mesh/mesh-pb-constants.h" #include "mesh/mesh-pb-constants.h"
#include "sleep.h" #include "sleep.h"
#include <NimBLEDevice.h> #include <NimBLEDevice.h>
#include <mutex>
NimBLECharacteristic *fromNumCharacteristic; NimBLECharacteristic *fromNumCharacteristic;
NimBLECharacteristic *BatteryCharacteristic; NimBLECharacteristic *BatteryCharacteristic;
@@ -17,8 +18,36 @@ NimBLEServer *bleServer;
static bool passkeyShowing; static bool passkeyShowing;
class BluetoothPhoneAPI : public PhoneAPI class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
{ {
public:
BluetoothPhoneAPI() : concurrency::OSThread("NimbleBluetooth") { nimble_queue.resize(3); }
std::vector<NimBLEAttValue> nimble_queue;
std::mutex nimble_mutex;
uint8_t queue_size = 0;
bool has_fromRadio = false;
uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0};
size_t numBytes = 0;
bool hasChecked = false;
protected:
virtual int32_t runOnce() override
{
std::lock_guard<std::mutex> guard(nimble_mutex);
if (queue_size > 0) {
for (uint8_t i = 0; i < queue_size; i++) {
handleToRadio(nimble_queue.at(i).data(), nimble_queue.at(i).length());
}
LOG_WARN("Queue_size %u", queue_size);
queue_size = 0;
}
if (hasChecked == false) {
numBytes = getFromRadio(fromRadioBytes);
hasChecked = true;
}
return 100;
}
/** /**
* Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies) * Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies)
*/ */
@@ -51,15 +80,16 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
{ {
virtual void onWrite(NimBLECharacteristic *pCharacteristic) virtual void onWrite(NimBLECharacteristic *pCharacteristic)
{ {
LOG_DEBUG("To Radio onwrite");
auto val = pCharacteristic->getValue(); auto val = pCharacteristic->getValue();
if (memcmp(lastToRadio, val.data(), val.length()) != 0) { if (memcmp(lastToRadio, val.data(), val.length()) != 0) {
LOG_DEBUG("New ToRadio packet"); if (bluetoothPhoneAPI->queue_size < 3) {
memcpy(lastToRadio, val.data(), val.length()); memcpy(lastToRadio, val.data(), val.length());
bluetoothPhoneAPI->handleToRadio(val.data(), val.length()); std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->nimble_mutex);
} else { bluetoothPhoneAPI->nimble_queue.at(bluetoothPhoneAPI->queue_size) = val;
LOG_DEBUG("Drop dup ToRadio packet we just saw"); bluetoothPhoneAPI->queue_size++;
bluetoothPhoneAPI->setIntervalFromNow(0);
}
} }
} }
}; };
@@ -68,12 +98,19 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
{ {
virtual void onRead(NimBLECharacteristic *pCharacteristic) virtual void onRead(NimBLECharacteristic *pCharacteristic)
{ {
uint8_t fromRadioBytes[meshtastic_FromRadio_size]; while (!bluetoothPhoneAPI->hasChecked) {
size_t numBytes = bluetoothPhoneAPI->getFromRadio(fromRadioBytes); bluetoothPhoneAPI->setIntervalFromNow(0);
delay(20);
std::string fromRadioByteString(fromRadioBytes, fromRadioBytes + numBytes); }
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->nimble_mutex);
std::string fromRadioByteString(bluetoothPhoneAPI->fromRadioBytes,
bluetoothPhoneAPI->fromRadioBytes + bluetoothPhoneAPI->numBytes);
pCharacteristic->setValue(fromRadioByteString); pCharacteristic->setValue(fromRadioByteString);
if (bluetoothPhoneAPI->numBytes != 0) // if we did send something, queue it up right away to reload
bluetoothPhoneAPI->setIntervalFromNow(0);
bluetoothPhoneAPI->numBytes = 0;
bluetoothPhoneAPI->hasChecked = false;
} }
}; };
+1 -1
View File
@@ -42,7 +42,7 @@ void powerCommandsCheck()
#if defined(ARCH_ESP32) || defined(ARCH_NRF52) #if defined(ARCH_ESP32) || defined(ARCH_NRF52)
if (shutdownAtMsec && screen) { if (shutdownAtMsec && screen) {
screen->showOverlayBanner("Shutting Down...", 0); // stays on screen screen->showSimpleBanner("Shutting Down...", 0); // stays on screen
} }
#endif #endif
+3
View File
@@ -57,6 +57,9 @@ extern "C" {
#define TFT_OFFSET_X 0 #define TFT_OFFSET_X 0
#define TFT_OFFSET_Y 0 #define TFT_OFFSET_Y 0
// T114 gets a muted yellow on black display
#define TFT_MESH_OVERRIDE COLOR565(255, 255, 128)
// #define TFT_OFFSET_ROTATION 0 // #define TFT_OFFSET_ROTATION 0
// #define SCREEN_ROTATE // #define SCREEN_ROTATE
// #define SCREEN_TRANSITION_FRAMERATE 5 // #define SCREEN_TRANSITION_FRAMERATE 5
+1 -1
View File
@@ -49,7 +49,7 @@
#define SCREEN_TRANSITION_FRAMERATE 5 #define SCREEN_TRANSITION_FRAMERATE 5
// Picomputer gets a white on black display // Picomputer gets a white on black display
#define TFT_MESH COLOR565(0xFF, 0xFF, 0xFF) #define TFT_MESH_OVERRIDE COLOR565(255, 255, 255)
#define CANNED_MESSAGE_MODULE_ENABLE 1 #define CANNED_MESSAGE_MODULE_ENABLE 1
+1 -1
View File
@@ -72,7 +72,7 @@
#define SX126X_DIO3_TCXO_VOLTAGE 1.8 #define SX126X_DIO3_TCXO_VOLTAGE 1.8
// Picomputer gets a white on black display // Picomputer gets a white on black display
#define TFT_MESH COLOR565(0xFF, 0xFF, 0xFF) #define TFT_MESH_OVERRIDE COLOR565(255, 255, 255)
// keyboard changes // keyboard changes
+1 -1
View File
@@ -96,7 +96,7 @@
#define SX126X_DIO3_TCXO_VOLTAGE 1.8 #define SX126X_DIO3_TCXO_VOLTAGE 1.8
// Picomputer gets a white on black display // Picomputer gets a white on black display
#define TFT_MESH COLOR565(0xFF, 0xFF, 0xFF) #define TFT_MESH_OVERRIDE COLOR565(255, 255, 255)
// keyboard changes // keyboard changes
+1 -1
View File
@@ -74,7 +74,7 @@
#define SX126X_DIO3_TCXO_VOLTAGE 1.8 #define SX126X_DIO3_TCXO_VOLTAGE 1.8
// Picomputer gets a white on black display // Picomputer gets a white on black display
#define TFT_MESH COLOR565(0xFF, 0xFF, 0xFF) #define TFT_MESH_OVERRIDE COLOR565(255, 255, 255)
// keyboard changes // keyboard changes