diff --git a/protobufs b/protobufs index 5ef7aec95..386fa53c1 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 5ef7aec9597c6f841152e63b84d9dd7608cdef81 +Subproject commit 386fa53c1596c8dfc547521f08df107f4cb3a275 diff --git a/src/commands.h b/src/commands.h index e0bfab330..603003e5c 100644 --- a/src/commands.h +++ b/src/commands.h @@ -13,5 +13,6 @@ enum class Cmd { START_FIRMWARE_UPDATE_SCREEN, STOP_BOOT_SCREEN, SHOW_PREV_FRAME, - SHOW_NEXT_FRAME + SHOW_NEXT_FRAME, + NOOP }; \ No newline at end of file diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index c8c9d8b74..067e4418f 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -69,6 +69,8 @@ using graphics::Emote; using graphics::emotes; using graphics::numEmotes; +extern uint16_t TFT_MESH; + #if HAS_WIFI && !defined(ARCH_PORTDUINO) #include "mesh/wifi/WiFiAPClient.h" #endif @@ -135,10 +137,66 @@ extern bool hasUnreadMessage; // 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 -// Called to trigger a banner with custom message and duration -void Screen::showOverlayBanner(const char *message, uint32_t durationMs, const char **optionsArrayPtr, uint8_t options, - std::function bannerCallback, int8_t InitialSelected) +void Screen::showSimpleBanner(const char *message, uint32_t durationMs) { + 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 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 bannerCallback) +{ + LOG_WARN("Show Number Picker"); #ifdef USE_EINK EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus #endif @@ -146,14 +204,16 @@ void Screen::showOverlayBanner(const char *message, uint32_t durationMs, const c strncpy(NotificationRenderer::alertBannerMessage, message, 255); NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; - NotificationRenderer::optionsArrayPtr = optionsArrayPtr; - NotificationRenderer::alertBannerOptions = options; NotificationRenderer::alertBannerCallback = bannerCallback; - NotificationRenderer::curSelected = InitialSelected; 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])); - setFastFramerate(); // Draw ASAP + ui->setTargetFPS(60); 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) { 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) dispdev = new SH1106Wire(address.address, -1, -1, geometry, (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); #else dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); - static_cast(dispdev)->setRGB(COLOR565(255, 255, 128)); + static_cast(dispdev)->setRGB(TFT_MESH); #endif #elif defined(USE_SSD1306) dispdev = new SSD1306Wire(address.address, -1, -1, geometry, @@ -386,9 +460,22 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) void Screen::setup() { + // === Enable display rendering === 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) === #ifdef AutoOLEDWire_h if (isAUTOOled) @@ -416,6 +503,14 @@ void Screen::setup() ui->disableAllIndicators(); // Disable page indicator dots ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance + // === Apply loaded brightness === +#if defined(ST7789_CS) + static_cast(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 === static OverlayCallback overlays[] = { graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame @@ -562,7 +657,7 @@ int32_t Screen::runOnce() if (displayHeight == 0) { displayHeight = dispdev->getHeight(); } - menuHandler::handleMenuSwitch(); + menuHandler::handleMenuSwitch(dispdev); // Show boot screen for first logo_timeout seconds, then switch to normal operation. // serialSinceMsec adjusts for additional serial wait time during nRF52 bootup @@ -595,7 +690,7 @@ int32_t Screen::runOnce() } #endif if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) { - showOverlayBanner("Rebooting...", 0); + showSimpleBanner("Rebooting...", 0); } // 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 setFrames(); break; + case Cmd::NOOP: + break; default: LOG_ERROR("Invalid screen cmd"); } @@ -785,8 +882,8 @@ void Screen::setFrames(FrameFocus focus) #if defined(DISPLAY_CLOCK_FRAME) fsi.positions.clock = numframes; - normalFrames[numframes++] = graphics::ClockRenderer::digitalWatchFace ? graphics::ClockRenderer::drawDigitalClockFrame - : &graphics::ClockRenderer::drawAnalogClockFrame; + normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame + : graphics::ClockRenderer::drawDigitalClockFrame; indicatorIcons.push_back(digital_icon_clock); #endif @@ -842,8 +939,8 @@ void Screen::setFrames(FrameFocus focus) } #if !defined(DISPLAY_CLOCK_FRAME) fsi.positions.clock = numframes; - normalFrames[numframes++] = graphics::ClockRenderer::digitalWatchFace ? graphics::ClockRenderer::drawDigitalClockFrame - : graphics::ClockRenderer::drawAnalogClockFrame; + normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame + : graphics::ClockRenderer::drawDigitalClockFrame; indicatorIcons.push_back(digital_icon_clock); #endif @@ -909,7 +1006,7 @@ void Screen::setFrames(FrameFocus focus) ui->disableAllIndicators(); // 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])); 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 ui->switchToFrame(fsi.positions.clock); break; + case FOCUS_SYSTEM: + ui->switchToFrame(fsi.positions.memory); + break; case FOCUS_PRESERVE: // 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 if (NotificationRenderer::isOverlayBannerShowing()) { NotificationRenderer::inEvent = event->inputEvent; - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, - NotificationRenderer::drawAlertBannerOverlay}; + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); setFastFramerate(); // Draw ASAP ui->update(); - menuHandler::handleMenuSwitch(); + menuHandler::handleMenuSwitch(dispdev); 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, // 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) { if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { menuHandler::homeBaseMenu(); -#if HAS_TFT } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) { - menuHandler::switchToMUIMenu(); -#else - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) { - menuHandler::BuzzerModeMenu(); -#endif + menuHandler::systemBaseMenu(); #if HAS_GPS } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) { menuHandler::positionBaseMenu(); diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index ac7d9aa69..a486f99f8 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -5,10 +5,26 @@ #include "detect/ScanI2C.h" #include "mesh/generated/meshtastic/config.pb.h" #include +#include #include #include #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 bannerCallback = nullptr; + int8_t InitialSelected = 0; + notificationTypeEnum notificationType = notificationTypeEnum::text_banner; +}; +} // namespace graphics #if !HAS_SCREEN #include "power.h" @@ -25,6 +41,7 @@ class Screen FOCUS_TEXTMESSAGE, FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus FOCUS_CLOCK, + FOCUS_SYSTEM, }; explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); @@ -39,10 +56,8 @@ class Screen void setFunctionSymbol(std::string) {} void removeFunctionSymbol(std::string) {} void startAlert(const char *) {} - void showOverlayBanner(const char *message, uint32_t durationMs = 3000, const char **optionsArrayPtr = nullptr, - uint8_t options = 0, std::function bannerCallback = NULL, int8_t InitialSelected = 0) - { - } + void showSimpleBanner(const char *message, uint32_t durationMs = 0) {} + void showOverlayBanner(BannerOverlayOptions) {} void setFrames(FrameFocus focus) {} void endAlert() {} }; @@ -199,6 +214,7 @@ class Screen : public concurrency::OSThread CallbackObserver(this, &Screen::handleAdminMessage); public: + OLEDDisplay *getDisplayDevice() { return dispdev; } explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); size_t frameCount = 0; // Total number of active frames ~Screen(); @@ -211,6 +227,7 @@ class Screen : public concurrency::OSThread FOCUS_TEXTMESSAGE, FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus FOCUS_CLOCK, + FOCUS_SYSTEM, }; // 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; OLEDDISPLAY_GEOMETRY geometry; - bool ignoreCompass = false; - bool isOverlayBannerShowing(); // 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); } - void showOverlayBanner(const char *message, uint32_t durationMs = 3000, const char **optionsArrayPtr = nullptr, - uint8_t options = 0, std::function bannerCallback = NULL, int8_t InitialSelected = 0); + void showSimpleBanner(const char *message, uint32_t durationMs = 0); + void showOverlayBanner(BannerOverlayOptions); + + void showNodePicker(const char *message, uint32_t durationMs, std::function bannerCallback); + void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function bannerCallback); void startFirmwareUpdateScreen() { @@ -325,6 +343,12 @@ class Screen : public concurrency::OSThread /// Stops showing the 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 static char customFontTableLookup(const uint8_t ch) { diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 07f2e5cde..9f2422748 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -343,4 +343,30 @@ const int *getTextPositions(OLEDDisplay *display) 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(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 diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h index 2e97052a8..b8d82795e 100644 --- a/src/graphics/SharedUIDisplay.h +++ b/src/graphics/SharedUIDisplay.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace graphics { @@ -52,4 +53,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti const int *getTextPositions(OLEDDisplay *display); +bool isAllowedPunctuation(char c); + +std::string sanitizeString(const std::string &input); + } // namespace graphics diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 92b2c3d02..3e9bafc6c 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -1,5 +1,6 @@ #include "configuration.h" #include "main.h" + #if ARCH_PORTDUINO #include "platform/portduino/PortduinoGlue.h" #endif @@ -14,8 +15,10 @@ extern SX1509 gpioExtender; #endif -#ifndef TFT_MESH -#define TFT_MESH COLOR565(0x67, 0xEA, 0x94) +#ifdef TFT_MESH_OVERRIDE +uint16_t TFT_MESH = TFT_MESH_OVERRIDE; +#else +uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94); #endif #if defined(ST7735S) diff --git a/src/graphics/draw/ClockRenderer.cpp b/src/graphics/draw/ClockRenderer.cpp index aa177078b..7ccb1c03c 100644 --- a/src/graphics/draw/ClockRenderer.cpp +++ b/src/graphics/draw/ClockRenderer.cpp @@ -21,7 +21,6 @@ namespace graphics namespace ClockRenderer { -bool digitalWatchFace = true; void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale) { diff --git a/src/graphics/draw/ClockRenderer.h b/src/graphics/draw/ClockRenderer.h index 9c3238b14..c8ba62868 100644 --- a/src/graphics/draw/ClockRenderer.h +++ b/src/graphics/draw/ClockRenderer.h @@ -11,8 +11,6 @@ class Screen; namespace ClockRenderer { -// Whether we are showing the digital watch face or the analog one -extern bool digitalWatchFace; // Clock frame functions void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); diff --git a/src/graphics/draw/CompassRenderer.cpp b/src/graphics/draw/CompassRenderer.cpp index 6d8051546..0e5a1d727 100644 --- a/src/graphics/draw/CompassRenderer.cpp +++ b/src/graphics/draw/CompassRenderer.cpp @@ -50,7 +50,7 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, radius += 4; } Point north(0, -radius); - if (!config.display.compass_north_top) + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) north.rotate(-myHeading); north.translate(compassX, compassY); diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 92cf49610..b1a901f99 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -501,7 +501,10 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int line = 1; const int barHeight = 6; const int labelX = x; - const int barsOffset = (isHighResolution) ? 24 : 0; + int barsOffset = (isHighResolution) ? 24 : 0; +#ifdef USE_EINK + barsOffset -= 12; +#endif const int barX = x + 40 + barsOffset; auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 9736cf9d1..3681532bb 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -8,14 +8,19 @@ #include "NodeDB.h" #include "buzz.h" #include "graphics/Screen.h" +#include "graphics/SharedUIDisplay.h" #include "graphics/draw/UIRenderer.h" #include "main.h" #include "modules/AdminModule.h" #include "modules/CannedMessageModule.h" +extern uint16_t TFT_MESH; + namespace graphics { menuHandler::screenMenus menuHandler::menuQueue = menu_none; +bool test_enabled = false; +uint8_t test_count = 0; void menuHandler::LoraRegionPicker(uint32_t duration) { @@ -44,72 +49,92 @@ void menuHandler::LoraRegionPicker(uint32_t duration) "PH_868", "PH_915", "ANZ_433"}; - screen->showOverlayBanner( - "Set the LoRa region", duration, optionsArray, 23, - [](int selected) -> void { - if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) { - config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected); - // This is needed as we wait til picking the LoRa region to generate keys for the first time. - if (!owner.is_licensed) { - bool keygenSuccess = false; - if (config.security.private_key.size == 32) { - // public key is derived from private, so this will always have the same result. - if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { - keygenSuccess = true; - } - } else { - LOG_INFO("Generate new PKI keys"); - crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Set the LoRa region"; + bannerOptions.durationMs = duration; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 23; + bannerOptions.InitialSelected = 0; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) { + config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected); + // This is needed as we wait til picking the LoRa region to generate keys for the first time. + if (!owner.is_licensed) { + bool keygenSuccess = false; + if (config.security.private_key.size == 32) { + // public key is derived from private, so this will always have the same result. + if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { keygenSuccess = true; } - if (keygenSuccess) { - config.security.public_key.size = 32; - config.security.private_key.size = 32; - owner.public_key.size = 32; - memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); - } + } else { + LOG_INFO("Generate new PKI keys"); + crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); + keygenSuccess = true; } - config.lora.tx_enabled = true; - initRegion(); - if (myRegion->dutyCycle < 100) { - config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit + if (keygenSuccess) { + config.security.public_key.size = 32; + config.security.private_key.size = 32; + owner.public_key.size = 32; + memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); } - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); } - }, - 0); + config.lora.tx_enabled = true; + initRegion(); + if (myRegion->dutyCycle < 100) { + config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit + } + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + screen->showOverlayBanner(bannerOptions); } void menuHandler::TwelveHourPicker() { static const char *optionsArray[] = {"Back", "12-hour", "24-hour"}; - screen->showOverlayBanner("Time Format", 30000, optionsArray, 3, [](int selected) -> void { - if (selected == 0) { + enum optionsNumbers { Back = 0, twelve = 1, twentyfour = 2 }; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Time Format"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Back) { menuHandler::menuQueue = menuHandler::clock_menu; - } else if (selected == 1) { + screen->runNow(); + } else if (selected == twelve) { config.display.use_12h_clock = true; } else { config.display.use_12h_clock = false; } service->reloadConfig(SEGMENT_CONFIG); - }); + }; + screen->showOverlayBanner(bannerOptions); } void menuHandler::ClockFacePicker() { static const char *optionsArray[] = {"Back", "Digital", "Analog"}; - screen->showOverlayBanner("Which Face?", 30000, optionsArray, 3, [](int selected) -> void { - if (selected == 0) { + enum optionsNumbers { Back = 0, Digital = 1, Analog = 2 }; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Which Face?"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Back) { menuHandler::menuQueue = menuHandler::clock_menu; - } else if (selected == 1) { - graphics::ClockRenderer::digitalWatchFace = true; + screen->runNow(); + } else if (selected == Digital) { + uiconfig.is_clockface_analog = false; + nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uiconfig); screen->setFrames(Screen::FOCUS_CLOCK); } else { - graphics::ClockRenderer::digitalWatchFace = false; + uiconfig.is_clockface_analog = true; + nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uiconfig); screen->setFrames(Screen::FOCUS_CLOCK); } - }); + }; + screen->showOverlayBanner(bannerOptions); } void menuHandler::TZPicker() @@ -133,9 +158,14 @@ void menuHandler::TZPicker() "AU/ACST", "AU/AEST", "Pacific/NZ"}; - screen->showOverlayBanner("Pick Timezone", 30000, optionsArray, 17, [](int selected) -> void { + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Pick Timezone"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 17; + bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 0) { menuHandler::menuQueue = menuHandler::clock_menu; + screen->runNow(); } else if (selected == 1) { // Hawaii strncpy(config.device.tzdef, "HST10", sizeof(config.device.tzdef)); } else if (selected == 2) { // Alaska @@ -175,27 +205,31 @@ void menuHandler::TZPicker() setenv("TZ", config.device.tzdef, 1); service->reloadConfig(SEGMENT_CONFIG); } - }); + }; + screen->showOverlayBanner(bannerOptions); } void menuHandler::clockMenu() { static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"}; - screen->showOverlayBanner("Clock Action", 30000, optionsArray, 4, [](int selected) -> void { - if (selected == 1) { + enum optionsNumbers { Back = 0, Clock = 1, Time = 2, Timezone = 3 }; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Clock Action"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 4; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Clock) { menuHandler::menuQueue = menuHandler::clock_face_picker; - screen->setInterval(0); - runASAP = true; - } else if (selected == 2) { + screen->runNow(); + } else if (selected == Time) { menuHandler::menuQueue = menuHandler::twelve_hour_picker; - screen->setInterval(0); - runASAP = true; - } else if (selected == 3) { + screen->runNow(); + } else if (selected == Timezone) { menuHandler::menuQueue = menuHandler::TZ_picker; - screen->setInterval(0); - runASAP = true; + screen->runNow(); } - }); + }; + screen->showOverlayBanner(bannerOptions); } void menuHandler::messageResponseMenu() @@ -203,6 +237,7 @@ void menuHandler::messageResponseMenu() static const char **optionsArrayPtr; int options; + enum optionsNumbers { Back = 0, Dismiss = 1, Preset = 2, Freetext = 3 }; if (kb_found) { static const char *optionsArray[] = {"Back", "Dismiss", "Reply via Preset", "Reply via Freetext"}; optionsArrayPtr = optionsArray; @@ -217,16 +252,20 @@ void menuHandler::messageResponseMenu() optionsArrayPtr = optionsArray; options = 5; #endif - screen->showOverlayBanner("Message Action", 30000, optionsArrayPtr, options, [](int selected) -> void { - if (selected == 1) { + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Message Action"; + bannerOptions.optionsArrayPtr = optionsArrayPtr; + bannerOptions.optionsCount = options; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Dismiss) { screen->dismissCurrentFrame(); - } else if (selected == 2) { + } else if (selected == Preset) { if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); } else { cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from); } - } else if (selected == 3) { + } else if (selected == Freetext) { if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); } else { @@ -241,51 +280,138 @@ void menuHandler::messageResponseMenu() audioThread->readAloud(msg); } #endif - }); + }; + screen->showOverlayBanner(bannerOptions); } void menuHandler::homeBaseMenu() { - int options; - static const char **optionsArrayPtr; + enum optionsNumbers { Back, Backlight, Position, Preset, Freetext, Bluetooth, Sleep }; + static const char *optionsArray[6] = {"Back"}; + static int optionsEnumArray[6] = {Back}; + int options = 1; + +#ifdef PIN_EINK_EN + optionsArray[options] = "Toggle Backlight"; + optionsEnumArray[options++] = Backlight; +#else + optionsArray[options] = "Sleep Screen"; + optionsEnumArray[options++] = Sleep; +#endif + + optionsArray[options] = "Send Position"; + optionsEnumArray[options++] = Position; + optionsArray[options] = "New Preset Msg"; + optionsEnumArray[options++] = Preset; if (kb_found) { -#ifdef PIN_EINK_EN - static const char *optionsArray[] = {"Back", "Toggle Backlight", "Send Position", "New Preset Msg", "New Freetext Msg"}; -#else - static const char *optionsArray[] = {"Back", "Sleep Screen", "Send Position", "New Preset Msg", "New Freetext Msg"}; -#endif - optionsArrayPtr = optionsArray; - options = 5; - } else { -#ifdef PIN_EINK_EN - static const char *optionsArray[] = {"Back", "Toggle Backlight", "Send Position", "New Preset Msg"}; -#else - static const char *optionsArray[] = {"Back", "Sleep Screen", "Send Position", "New Preset Msg"}; -#endif - optionsArrayPtr = optionsArray; - options = 4; + optionsArray[options] = "New Freetext Msg"; + optionsEnumArray[options++] = Freetext; } - screen->showOverlayBanner("Home Action", 30000, optionsArrayPtr, options, [](int selected) -> void { - if (selected == 1) { + optionsArray[options] = "Bluetooth Toggle"; + optionsEnumArray[options++] = Bluetooth; + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Home Action"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Backlight) { #ifdef PIN_EINK_EN if (digitalRead(PIN_EINK_EN) == HIGH) { digitalWrite(PIN_EINK_EN, LOW); } else { digitalWrite(PIN_EINK_EN, HIGH); } -#else - screen->setOn(false); #endif - } else if (selected == 2) { - InputEvent event = {.inputEvent = (input_broker_event)175, .kbchar = 175, .touchX = 0, .touchY = 0}; + } else if (selected == Sleep) { + screen->setOn(false); + } else if (selected == Position) { + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SEND_PING, .kbchar = 0, .touchX = 0, .touchY = 0}; inputBroker->injectInputEvent(&event); - } else if (selected == 3) { + } else if (selected == Preset) { cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); - } else if (selected == 4) { + } else if (selected == Freetext) { cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST); + } else if (selected == Bluetooth) { + InputEvent event = {.inputEvent = (input_broker_event)170, .kbchar = 170, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); } - }); + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::systemBaseMenu() +{ + + // Check if brightness is supported + bool hasSupportBrightness = false; +#if defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || HAS_TFT + hasSupportBrightness = true; +#endif + + enum optionsNumbers { Back, Beeps, Brightness, Reboot, Color, MUI, Test }; + static const char *optionsArray[6] = {"Back"}; + static int optionsEnumArray[6] = {Back}; + int options = 1; + + optionsArray[options] = "Beeps Action"; + optionsEnumArray[options++] = Beeps; + + if (hasSupportBrightness) { + optionsArray[options] = "Brightness"; + optionsEnumArray[options++] = Brightness; + } + + optionsArray[options] = "Reboot"; + optionsEnumArray[options++] = Reboot; + +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT + optionsArray[options] = "Screen Color"; + optionsEnumArray[options++] = Color; +#endif +#if HAS_TFT + optionsArray[options] = "Switch to MUI"; + optionsEnumArray[options++] = MUI; +#endif + if (test_enabled) { + optionsArray[options] = "Test Menu"; + optionsEnumArray[options++] = Test; + } + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "System Action"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Beeps) { + menuHandler::menuQueue = menuHandler::buzzermodemenupicker; + screen->runNow(); + } else if (selected == Brightness) { + menuHandler::menuQueue = menuHandler::brightness_picker; + screen->runNow(); + } else if (selected == Reboot) { + menuHandler::menuQueue = menuHandler::reboot_menu; + screen->runNow(); + } else if (selected == MUI) { + menuHandler::menuQueue = menuHandler::mui_picker; + screen->runNow(); + } else if (selected == Color) { + menuHandler::menuQueue = menuHandler::tftcolormenupicker; + screen->runNow(); + } else if (selected == Test) { + menuHandler::menuQueue = menuHandler::test_menu; + screen->runNow(); + } else if (selected == Back && !test_enabled) { + test_count++; + if (test_count > 4) { + test_enabled = true; + } + } + }; + screen->showOverlayBanner(bannerOptions); } void menuHandler::favoriteBaseMenu() @@ -294,21 +420,29 @@ void menuHandler::favoriteBaseMenu() static const char **optionsArrayPtr; if (kb_found) { - static const char *optionsArray[] = {"Back", "New Preset Msg", "New Freetext Msg"}; + static const char *optionsArray[] = {"Back", "New Preset Msg", "New Freetext Msg", "Remove Favorite"}; + optionsArrayPtr = optionsArray; + options = 4; + } else { + static const char *optionsArray[] = {"Back", "New Preset Msg", "Remove Favorite"}; optionsArrayPtr = optionsArray; options = 3; - } else { - static const char *optionsArray[] = {"Back", "New Preset Msg"}; - optionsArrayPtr = optionsArray; - options = 2; } - screen->showOverlayBanner("Favorites Action", 30000, optionsArrayPtr, options, [](int selected) -> void { + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Favorites Action"; + bannerOptions.optionsArrayPtr = optionsArrayPtr; + bannerOptions.optionsCount = options; + bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 1) { cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); - } else if (selected == 2) { + } else if (selected == 2 && kb_found) { cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); + } else if ((!kb_found && selected == 2) || (selected == 3 && kb_found)) { + menuHandler::menuQueue = menuHandler::remove_favorite; + screen->runNow(); } - }); + }; + screen->showOverlayBanner(bannerOptions); } void menuHandler::positionBaseMenu() @@ -325,127 +459,385 @@ void menuHandler::positionBaseMenu() optionsArrayPtr = optionsArray; options = 3; } - screen->showOverlayBanner("Position Action", 30000, optionsArrayPtr, options, [](int selected) -> void { + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Position Action"; + bannerOptions.optionsArrayPtr = optionsArrayPtr; + bannerOptions.optionsCount = options; + bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 1) { #if MESHTASTIC_EXCLUDE_GPS menuQueue = menu_none; #else menuQueue = gps_toggle_menu; + screen->runNow(); #endif } else if (selected == 2) { menuQueue = compass_point_north_menu; + screen->runNow(); } else if (selected == 3) { accelerometerThread->calibrate(30); } - }); + }; + screen->showOverlayBanner(bannerOptions); } void menuHandler::nodeListMenu() { - static const char *optionsArray[] = {"Back", "Reset NodeDB"}; - screen->showOverlayBanner("Node Action", 30000, optionsArray, 2, [](int selected) -> void { + static const char *optionsArray[] = {"Back", "Add Favorite", "Reset NodeDB"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Node Action"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 1) { + menuQueue = add_favorite; + screen->runNow(); + } else if (selected == 2) { menuQueue = reset_node_db_menu; + screen->runNow(); } - }); + }; + screen->showOverlayBanner(bannerOptions); } void menuHandler::resetNodeDBMenu() { static const char *optionsArray[] = {"Back", "Confirm"}; - screen->showOverlayBanner("Confirm Reset NodeDB", 30000, optionsArray, 2, [](int selected) -> void { + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Confirm Reset NodeDB"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 1) { disableBluetooth(); LOG_INFO("Initiate node-db reset"); nodeDB->resetNodes(); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); } - }); + }; + screen->showOverlayBanner(bannerOptions); } void menuHandler::compassNorthMenu() { static const char *optionsArray[] = {"Back", "Dynamic", "Fixed Ring", "Freeze Heading"}; - screen->showOverlayBanner("North Directions?", 30000, optionsArray, 4, [](int selected) -> void { + BannerOverlayOptions bannerOptions; + bannerOptions.message = "North Directions?"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 4; + bannerOptions.InitialSelected = uiconfig.compass_mode + 1; + bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 1) { - if (config.display.compass_north_top != false) { - config.display.compass_north_top = false; - service->reloadConfig(SEGMENT_CONFIG); + if (uiconfig.compass_mode != meshtastic_CompassMode_DYNAMIC) { + uiconfig.compass_mode = meshtastic_CompassMode_DYNAMIC; + nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, + &uiconfig); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); } - screen->ignoreCompass = false; - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); } else if (selected == 2) { - if (config.display.compass_north_top != true) { - config.display.compass_north_top = true; - service->reloadConfig(SEGMENT_CONFIG); + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) { + uiconfig.compass_mode = meshtastic_CompassMode_FIXED_RING; + nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, + &uiconfig); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); } - screen->ignoreCompass = false; - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); } else if (selected == 3) { - if (config.display.compass_north_top != true) { - config.display.compass_north_top = true; - service->reloadConfig(SEGMENT_CONFIG); + if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { + uiconfig.compass_mode = meshtastic_CompassMode_FREEZE_HEADING; + nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, + &uiconfig); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); } - screen->ignoreCompass = true; - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); } else if (selected == 0) { menuQueue = position_base_menu; + screen->runNow(); } - }); + }; + screen->showOverlayBanner(bannerOptions); } #if !MESHTASTIC_EXCLUDE_GPS void menuHandler::GPSToggleMenu() { static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; - screen->showOverlayBanner( - "Toggle GPS", 30000, optionsArray, 3, - [](int selected) -> void { - if (selected == 1) { - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; - playGPSEnableBeep(); - gps->enable(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 2) { - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; - playGPSDisableBeep(); - gps->disable(); - service->reloadConfig(SEGMENT_CONFIG); - } else { - menuQueue = position_base_menu; - } - }, - config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2); // set inital selection + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Toggle GPS"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; + playGPSEnableBeep(); + gps->enable(); + service->reloadConfig(SEGMENT_CONFIG); + } else if (selected == 2) { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; + playGPSDisableBeep(); + gps->disable(); + service->reloadConfig(SEGMENT_CONFIG); + } else { + menuQueue = position_base_menu; + screen->runNow(); + } + }; + bannerOptions.InitialSelected = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2; + screen->showOverlayBanner(bannerOptions); } #endif void menuHandler::BuzzerModeMenu() { static const char *optionsArray[] = {"All Enabled", "Disabled", "Notifications", "System Only"}; - screen->showOverlayBanner( - "Beep Action", 30000, optionsArray, 4, - [](int selected) -> void { - config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected; - service->reloadConfig(SEGMENT_CONFIG); - }, - config.device.buzzer_mode); + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Beep Action"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 4; + bannerOptions.bannerCallback = [](int selected) -> void { + config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected; + service->reloadConfig(SEGMENT_CONFIG); + }; + bannerOptions.InitialSelected = config.device.buzzer_mode; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::BrightnessPickerMenu() +{ + static const char *optionsArray[] = {"Back", "Low", "Medium", "High", "Very High"}; + + // Get current brightness level to set initial selection + int currentSelection = 1; // Default to Low + if (uiconfig.screen_brightness >= 255) { + currentSelection = 4; // Very High + } else if (uiconfig.screen_brightness >= 128) { + currentSelection = 3; // High + } else if (uiconfig.screen_brightness >= 64) { + currentSelection = 2; // Medium + } else { + currentSelection = 1; // Low + } + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Brightness"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 5; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { // Low + uiconfig.screen_brightness = 1; + } else if (selected == 2) { // Medium + uiconfig.screen_brightness = 64; + } else if (selected == 3) { // High + uiconfig.screen_brightness = 128; + } else if (selected == 4) { // Very High + uiconfig.screen_brightness = 255; + } + + if (selected != 0) { // Not "Back" + // Apply brightness immediately +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(HELTEC_VISION_MASTER_E213) || \ + defined(HELTEC_VISION_MASTER_E290) + // For HELTEC devices, use analogWrite to control backlight + analogWrite(VTFT_LEDA, uiconfig.screen_brightness); +#elif defined(ST7789_CS) + static_cast(screen->getDisplayDevice())->setDisplayBrightness(uiconfig.screen_brightness); +#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) + screen->getDisplayDevice()->setBrightness(uiconfig.screen_brightness); +#endif + + // Save to device + nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uiconfig); + + LOG_INFO("Screen brightness set to %d", uiconfig.screen_brightness); + } + }; + bannerOptions.InitialSelected = currentSelection; + screen->showOverlayBanner(bannerOptions); } void menuHandler::switchToMUIMenu() { static const char *optionsArray[] = {"Yes", "No"}; - screen->showOverlayBanner("Switch to MUI?", 30000, optionsArray, 2, [](int selected) -> void { + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Switch to MUI?"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 0) { config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; config.bluetooth.enabled = false; service->reloadConfig(SEGMENT_CONFIG); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) +{ + static const char *optionsArray[] = {"Back", "Default", "Meshtastic Green", "Yellow", "Red", "Orange", "Purple", "Teal", + "Pink", "White"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Select Screen Color"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 10; + bannerOptions.bannerCallback = [display](int selected) -> void { + uint8_t r = 0; + uint8_t g = 0; + uint8_t b = 0; + if (selected == 1) { + LOG_INFO("Setting color to system default or defined variant"); + // Given just before we set all these to zero, we will allow this to go through + } else if (selected == 2) { + LOG_INFO("Setting color to Meshtastic Green"); + r = 103; + g = 234; + b = 148; + } else if (selected == 3) { + LOG_INFO("Setting color to Yellow"); + r = 255; + g = 255; + b = 128; + } else if (selected == 4) { + LOG_INFO("Setting color to Red"); + r = 255; + g = 64; + b = 64; + } else if (selected == 5) { + LOG_INFO("Setting color to Orange"); + r = 255; + g = 160; + b = 20; + } else if (selected == 6) { + LOG_INFO("Setting color to Purple"); + r = 204; + g = 153; + b = 255; + } else if (selected == 7) { + LOG_INFO("Setting color to Teal"); + r = 64; + g = 224; + b = 208; + } else if (selected == 8) { + LOG_INFO("Setting color to Pink"); + r = 255; + g = 105; + b = 180; + } else if (selected == 9) { + LOG_INFO("Setting color to White"); + r = 255; + g = 255; + b = 255; + } + +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT + if (selected != 0) { + display->setColor(BLACK); + display->fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); + display->setColor(WHITE); + + if (r == 0 && g == 0 && b == 0) { +#ifdef TFT_MESH_OVERRIDE + TFT_MESH = TFT_MESH_OVERRIDE; +#else + TFT_MESH = COLOR565(0x67, 0xEA, 0x94); +#endif + } else { + TFT_MESH = COLOR565(r, g, b); + } + +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) + static_cast(screen->getDisplayDevice())->setRGB(TFT_MESH); +#endif + + screen->setFrames(graphics::Screen::FOCUS_SYSTEM); + if (r == 0 && g == 0 && b == 0) { + uiconfig.screen_rgb_color = 0; + } else { + uiconfig.screen_rgb_color = (r << 16) | (g << 8) | b; + } + LOG_INFO("Storing Value of %d to uiconfig.screen_rgb_color", uiconfig.screen_rgb_color); + nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uiconfig); + } +#endif + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::rebootMenu() +{ + static const char *optionsArray[] = {"Back", "Confirm"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Reboot Device?"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0)); + nodeDB->saveToDisk(); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::addFavoriteMenu() +{ + screen->showNodePicker("Node To Favorite", 30000, [](int nodenum) -> void { + LOG_WARN("Nodenum: %u", nodenum); + nodeDB->set_favorite(true, nodenum); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); }); } -void menuHandler::handleMenuSwitch() +void menuHandler::removeFavoriteMenu() { + + static const char *optionsArray[] = {"Back", "Yes"}; + BannerOverlayOptions bannerOptions; + std::string message = "Unfavorite This Node?\n"; + auto node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum); + if (node && node->has_user) { + message += sanitizeString(node->user.long_name).substr(0, 15); + } + bannerOptions.message = message.c_str(); + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + nodeDB->set_favorite(false, graphics::UIRenderer::currentFavoriteNodeNum); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::testMenu() +{ + + static const char *optionsArray[] = {"Back", "Number Picker"}; + BannerOverlayOptions bannerOptions; + std::string message = "Test to Run?\n"; + bannerOptions.message = message.c_str(); + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + menuQueue = number_test; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::numberTest() +{ + screen->showNumberPicker("Pick a number\n ", 30000, 4, + [](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); }); +} + +void menuHandler::handleMenuSwitch(OLEDDisplay *display) +{ + if (menuQueue != menu_none) + test_count = 0; switch (menuQueue) { case menu_none: break; @@ -478,6 +870,33 @@ void menuHandler::handleMenuSwitch() case reset_node_db_menu: resetNodeDBMenu(); break; + case buzzermodemenupicker: + BuzzerModeMenu(); + break; + case mui_picker: + switchToMUIMenu(); + break; + case tftcolormenupicker: + TFTColorPickerMenu(display); + break; + case brightness_picker: + BrightnessPickerMenu(); + break; + case reboot_menu: + rebootMenu(); + break; + case add_favorite: + addFavoriteMenu(); + break; + case remove_favorite: + removeFavoriteMenu(); + break; + case test_menu: + testMenu(); + break; + case number_test: + numberTest(); + break; } menuQueue = menu_none; } diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 5a5ee8bf6..09279b041 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -17,26 +17,43 @@ class menuHandler gps_toggle_menu, #endif compass_point_north_menu, - reset_node_db_menu + reset_node_db_menu, + buzzermodemenupicker, + mui_picker, + tftcolormenupicker, + brightness_picker, + reboot_menu, + add_favorite, + remove_favorite, + test_menu, + number_test }; static screenMenus menuQueue; static void LoraRegionPicker(uint32_t duration = 30000); - static void handleMenuSwitch(); + static void handleMenuSwitch(OLEDDisplay *display); static void clockMenu(); static void TZPicker(); static void TwelveHourPicker(); static void ClockFacePicker(); static void messageResponseMenu(); static void homeBaseMenu(); + static void systemBaseMenu(); static void favoriteBaseMenu(); static void positionBaseMenu(); static void compassNorthMenu(); static void GPSToggleMenu(); static void BuzzerModeMenu(); static void switchToMUIMenu(); + static void TFTColorPickerMenu(OLEDDisplay *display); static void nodeListMenu(); static void resetNodeDBMenu(); + static void BrightnessPickerMenu(); + static void rebootMenu(); + static void addFavoriteMenu(); + static void removeFavoriteMenu(); + static void testMenu(); + static void numberTest(); }; } // namespace graphics \ No newline at end of file diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index 3f47a3a09..d8746fb69 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -66,10 +66,10 @@ const char *getSafeNodeName(meshtastic_NodeInfoLite *node) strncpy(nodeName, name, sizeof(nodeName) - 1); nodeName[sizeof(nodeName) - 1] = '\0'; } else { - snprintf(nodeName, sizeof(nodeName), "%04X", (uint16_t)(node->num & 0xFFFF)); + snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF)); } } else { - strcpy(nodeName, "?"); + snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF)); } return nodeName; } @@ -522,7 +522,7 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, double lat = DegD(ourNode->position.latitude_i); double lon = DegD(ourNode->position.longitude_i); - if (!screen->ignoreCompass) { + if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { #if HAS_GPS if (screen->hasHeading()) { heading = screen->getHeading(); // degrees diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 4866b4060..3b682cc55 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -32,8 +32,21 @@ char NotificationRenderer::alertBannerMessage[256] = {0}; uint32_t NotificationRenderer::alertBannerUntil = 0; // 0 is a special case meaning forever uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are seelctable options const char **NotificationRenderer::optionsArrayPtr = nullptr; +const int *NotificationRenderer::optionsEnumPtr = nullptr; std::function NotificationRenderer::alertBannerCallback = NULL; bool NotificationRenderer::pauseBanner = false; +notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none; +uint32_t NotificationRenderer::numDigits = 0; +uint32_t NotificationRenderer::currentNumber = 0; + +uint32_t pow_of_10(uint32_t n) +{ + uint32_t ret = 1; + for (int i = 0; i < n; i++) { + ret *= 10; + } + return ret; +} // Used on boot when a certificate is being created void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) @@ -55,29 +68,214 @@ void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiStat } } -void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) +void NotificationRenderer::resetBanner() +{ + alertBannerMessage[0] = '\0'; + current_notification_type = notificationTypeEnum::none; + nodeDB->pause_sort(false); +} + +void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state) { if (!isOverlayBannerShowing() || pauseBanner) return; + switch (current_notification_type) { + case notificationTypeEnum::text_banner: + case notificationTypeEnum::selection_picker: + drawAlertBannerOverlay(display, state); + break; + case notificationTypeEnum::node_picker: + drawNodePicker(display, state); + break; + case notificationTypeEnum::number_picker: + drawNumberPicker(display, state); + break; + } +} + +void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + const char *lineStarts[MAX_LINES + 1] = {0}; + uint16_t lineCount = 0; + + // Parse lines + char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); + lineStarts[lineCount] = alertBannerMessage; + + // Find lines + while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { + lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); + if (lineStarts[lineCount + 1][0] == '\n') + lineStarts[lineCount + 1] += 1; + lineCount++; + } + // modulo to extract + uint8_t this_digit = (currentNumber % (pow_of_10(numDigits - curSelected))) / (pow_of_10(numDigits - curSelected - 1)); + // Handle input + if (inEvent == INPUT_BROKER_UP || inEvent == INPUT_BROKER_ALT_PRESS) { + if (this_digit == 9) { + currentNumber -= 9 * (pow_of_10(numDigits - curSelected - 1)); + } else { + currentNumber += (pow_of_10(numDigits - curSelected - 1)); + } + } else if (inEvent == INPUT_BROKER_DOWN || inEvent == INPUT_BROKER_USER_PRESS) { + if (this_digit == 0) { + currentNumber += 9 * (pow_of_10(numDigits - curSelected - 1)); + } else { + currentNumber -= (pow_of_10(numDigits - curSelected - 1)); + } + } else if (inEvent == INPUT_BROKER_SELECT || inEvent == INPUT_BROKER_RIGHT) { + curSelected++; + } else if (inEvent == INPUT_BROKER_LEFT) { + curSelected--; + } else if ((inEvent == INPUT_BROKER_CANCEL || inEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) { + resetBanner(); + } + if (curSelected == numDigits) { + resetBanner(); + alertBannerCallback(currentNumber); + } + + inEvent = INPUT_BROKER_NONE; + if (alertBannerMessage[0] == '\0') + return; + + uint16_t totalLines = lineCount + 2; + const char *linePointers[totalLines + 1] = {0}; // this is sort of a dynamic allocation + + // copy the linestarts to display to the linePointers holder + for (int i = 0; i < lineCount; i++) { + linePointers[i] = lineStarts[i]; + } + std::string digits = " "; + std::string arrowPointer = " "; + for (int i = 0; i < numDigits; i++) { + // Modulo minus modulo to return just the current number + digits += std::to_string((currentNumber % (pow_of_10(numDigits - i))) / (pow_of_10(numDigits - i - 1))) + " "; + if (curSelected == i) { + arrowPointer += "^ "; + } else { + arrowPointer += "_ "; + } + } + + linePointers[lineCount++] = digits.c_str(); + linePointers[lineCount++] = arrowPointer.c_str(); + + drawNotificationBox(display, state, linePointers, totalLines, 0); +} + +void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + static uint32_t selectedNodenum = 0; // === Layout Configuration === - constexpr uint16_t hPadding = 5; constexpr uint16_t vPadding = 2; - constexpr uint8_t lineSpacing = 1; + alertBannerOptions = nodeDB->getNumMeshNodes() - 1; - bool needs_bell = (strstr(alertBannerMessage, "Alert Received") != nullptr); + // let the box drawing function calculate the widths? - // Setup font and alignment - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); + const char *lineStarts[MAX_LINES + 1] = {0}; + uint16_t lineCount = 0; + + // Parse lines + char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); + lineStarts[lineCount] = alertBannerMessage; + + while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { + lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); + if (lineStarts[lineCount + 1][0] == '\n') + lineStarts[lineCount + 1] += 1; + lineCount++; + } + + // Handle input + if (inEvent == INPUT_BROKER_UP || inEvent == INPUT_BROKER_ALT_PRESS) { + curSelected--; + } else if (inEvent == INPUT_BROKER_DOWN || inEvent == INPUT_BROKER_USER_PRESS) { + curSelected++; + } else if (inEvent == INPUT_BROKER_SELECT) { + resetBanner(); + alertBannerCallback(selectedNodenum); + + } else if ((inEvent == INPUT_BROKER_CANCEL || inEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) { + resetBanner(); + } + + if (curSelected == -1) + curSelected = alertBannerOptions - 1; + if (curSelected == alertBannerOptions) + curSelected = 0; + + inEvent = INPUT_BROKER_NONE; + if (alertBannerMessage[0] == '\0') + return; + + uint16_t totalLines = lineCount + alertBannerOptions; + uint16_t screenHeight = display->height(); + uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; + uint8_t visibleTotalLines = std::min(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight); + uint8_t linesShown = lineCount; + const char *linePointers[visibleTotalLines + 1] = {0}; // this is sort of a dynamic allocation + + // copy the linestarts to display to the linePointers holder + for (int i = 0; i < lineCount; i++) { + linePointers[i] = lineStarts[i]; + } + char scratchLineBuffer[visibleTotalLines - lineCount][40]; + + uint8_t firstOptionToShow = 0; + if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) { + if (curSelected > alertBannerOptions - visibleTotalLines + lineCount) + firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount; + else + firstOptionToShow = curSelected - 1; + } else { + firstOptionToShow = 0; + } + int scratchLineNum = 0; + for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { + char temp_name[16] = {0}; + if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) { + std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name); + strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1); + + } else { + snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF)); + } + // make temp buffer for name + // fi + if (i == curSelected) { + selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num; + if (isHighResolution) { + strncpy(scratchLineBuffer[scratchLineNum], "> ", 3); + strncpy(scratchLineBuffer[scratchLineNum] + 2, temp_name, 36); + strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 2, " <", 3); + } else { + strncpy(scratchLineBuffer[scratchLineNum], ">", 2); + strncpy(scratchLineBuffer[scratchLineNum] + 1, temp_name, 37); + strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 1, "<", 2); + } + scratchLineBuffer[scratchLineNum][39] = '\0'; + } else { + strncpy(scratchLineBuffer[scratchLineNum], temp_name, 36); + } + linePointers[linesShown] = scratchLineBuffer[scratchLineNum++]; + } + drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow); +} + +void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + // === Layout Configuration === + constexpr uint16_t vPadding = 2; - constexpr int MAX_LINES = 5; uint16_t optionWidths[alertBannerOptions] = {0}; uint16_t maxWidth = 0; uint16_t arrowsWidth = display->getStringWidth("> <", 4, true); uint16_t lineWidths[MAX_LINES] = {0}; uint16_t lineLengths[MAX_LINES] = {0}; - char *lineStarts[MAX_LINES + 1]; + const char *lineStarts[MAX_LINES + 1] = {0}; uint16_t lineCount = 0; char lineBuffer[40] = {0}; @@ -86,7 +284,7 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp lineStarts[lineCount] = alertBannerMessage; while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { - lineStarts[lineCount + 1] = std::find(lineStarts[lineCount], alertEnd, '\n'); + lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount]; if (lineStarts[lineCount + 1][0] == '\n') lineStarts[lineCount + 1] += 1; @@ -112,10 +310,15 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp } else if (inEvent == INPUT_BROKER_DOWN || inEvent == INPUT_BROKER_USER_PRESS) { curSelected++; } else if (inEvent == INPUT_BROKER_SELECT) { - alertBannerCallback(curSelected); - alertBannerMessage[0] = '\0'; + if (optionsEnumPtr != nullptr) { + alertBannerCallback(optionsEnumPtr[curSelected]); + optionsEnumPtr = nullptr; + } else { + alertBannerCallback(curSelected); + } + resetBanner(); } else if ((inEvent == INPUT_BROKER_CANCEL || inEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) { - alertBannerMessage[0] = '\0'; + resetBanner(); } if (curSelected == -1) @@ -124,7 +327,7 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp curSelected = 0; } else { if (inEvent == INPUT_BROKER_SELECT || inEvent == INPUT_BROKER_ALT_LONG || inEvent == INPUT_BROKER_CANCEL) { - alertBannerMessage[0] = '\0'; + resetBanner(); } } @@ -132,7 +335,91 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp if (alertBannerMessage[0] == '\0') return; - // === Box Size Calculation === + uint16_t totalLines = lineCount + alertBannerOptions; + + uint16_t screenHeight = display->height(); + uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; + uint8_t visibleTotalLines = std::min(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight); + uint8_t linesShown = lineCount; + const char *linePointers[visibleTotalLines + 1] = {0}; // this is sort of a dynamic allocation + + // copy the linestarts to display to the linePointers holder + for (int i = 0; i < lineCount; i++) { + linePointers[i] = lineStarts[i]; + } + + uint8_t firstOptionToShow = 0; + if (alertBannerOptions > 0) { + if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) { + if (curSelected > alertBannerOptions - visibleTotalLines + lineCount) + firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount; + else + firstOptionToShow = curSelected - 1; + } else { + firstOptionToShow = 0; + } + } + + for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { + if (i == curSelected) { + if (isHighResolution) { + strncpy(lineBuffer, "> ", 3); + strncpy(lineBuffer + 2, optionsArrayPtr[i], 36); + strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3); + } else { + strncpy(lineBuffer, ">", 2); + strncpy(lineBuffer + 1, optionsArrayPtr[i], 37); + strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 1, "<", 2); + } + lineBuffer[39] = '\0'; + linePointers[linesShown] = lineBuffer; + } else { + linePointers[linesShown] = optionsArrayPtr[i]; + } + } + if (alertBannerOptions > 0) { + drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow, maxWidth); + } else { + drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow); + } +} + +void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[], + uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth) +{ + + bool is_picker = false; + uint16_t lineCount = 0; + // === Layout Configuration === + constexpr uint16_t hPadding = 5; + constexpr uint16_t vPadding = 2; + bool needs_bell = false; + uint16_t lineWidths[totalLines] = {0}; + uint16_t lineLengths[totalLines] = {0}; + + if (maxWidth != 0) + is_picker = true; + + // Setup font and alignment + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + while (lines[lineCount] != nullptr) { + auto newlinePointer = strchr(lines[lineCount], '\n'); + if (newlinePointer) + lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first + else // if the newline wasn't found, then pull string length from strlen + lineLengths[lineCount] = strlen(lines[lineCount]); + lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true); + if (!is_picker) { + needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr); + if (lineWidths[lineCount] > maxWidth) + maxWidth = lineWidths[lineCount]; + } + lineCount++; + } + // count lines + uint16_t boxWidth = hPadding * 2 + maxWidth; if (needs_bell) { if (isHighResolution && boxWidth <= 150) @@ -141,14 +428,19 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp boxWidth += 20; } - uint16_t totalLines = lineCount + alertBannerOptions; uint16_t screenHeight = display->height(); uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; - uint8_t visibleTotalLines = std::min(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight); + uint8_t visibleTotalLines = std::min(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight); uint16_t contentHeight = visibleTotalLines * effectiveLineHeight; uint16_t boxHeight = contentHeight + vPadding * 2; + if (visibleTotalLines == 1) { + boxHeight += (isHighResolution) ? 4 : 3; + } int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); + if (totalLines > visibleTotalLines) { + boxWidth += (isHighResolution) ? 4 : 2; + } int16_t boxTop = (display->height() / 2) - (boxHeight / 2); // === Draw Box === @@ -169,21 +461,18 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp // === Draw Content === int16_t lineY = boxTop + vPadding; - uint8_t linesShown = 0; - - for (int i = 0; i < lineCount && linesShown < visibleTotalLines; i++, linesShown++) { - strncpy(lineBuffer, lineStarts[i], 40); - lineBuffer[lineLengths[i] > 39 ? 39 : lineLengths[i]] = '\0'; - + for (int i = 0; i < lineCount; i++) { int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; if (needs_bell && i == 0) { int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2; display->drawXbm(textX - 10, bellY, 8, 8, bell_alert); display->drawXbm(textX + lineWidths[i] + 2, bellY, 8, 8, bell_alert); } - + char lineBuffer[lineLengths[i] + 1]; + strncpy(lineBuffer, lines[i], lineLengths[i]); + lineBuffer[lineLengths[i]] = '\0'; // Determine if this is a pop-up or a pick list - if (alertBannerOptions > 0) { + if (alertBannerOptions > 0 && i == 0) { // Pick List display->setColor(WHITE); int background_yOffset = 1; @@ -199,39 +488,14 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp lineY += (effectiveLineHeight - 2 - background_yOffset); } else { // Pop-up - display->drawString(textX, lineY - 2, lineBuffer); + display->drawString(textX, lineY, lineBuffer); lineY += (effectiveLineHeight); } } - uint8_t firstOptionToShow = 0; - if (alertBannerOptions > 0) { - if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) - firstOptionToShow = curSelected - 1; - else - firstOptionToShow = 0; - } - - for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { - if (i == curSelected) { - strncpy(lineBuffer, "> ", 3); - strncpy(lineBuffer + 2, optionsArrayPtr[i], 36); - strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3); - lineBuffer[39] = '\0'; - } else { - strncpy(lineBuffer, optionsArrayPtr[i], 40); - lineBuffer[39] = '\0'; - } - - int16_t textX = boxLeft + (boxWidth - optionWidths[i] - (i == curSelected ? arrowsWidth : 0)) / 2; - display->drawString(textX, lineY, lineBuffer); - lineY += effectiveLineHeight; - } - // === Scroll Bar (Thicker, inside box, not over title) === if (totalLines > visibleTotalLines) { const uint8_t scrollBarWidth = 5; - const uint8_t scrollPadding = 2; int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2; int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; // start after title line @@ -239,7 +503,7 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp float ratio = (float)visibleTotalLines / totalLines; uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4); - float scrollRatio = (float)(firstOptionToShow + linesShown - visibleTotalLines) / (totalLines - visibleTotalLines); + float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines); uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight); display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight); diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h index 2ec5fd9ec..97a404d11 100644 --- a/src/graphics/draw/NotificationRenderer.h +++ b/src/graphics/draw/NotificationRenderer.h @@ -2,6 +2,8 @@ #include "OLEDDisplay.h" #include "OLEDDisplayUi.h" +#include "graphics/Screen.h" +#define MAX_LINES 5 namespace graphics { @@ -14,16 +16,28 @@ class NotificationRenderer static char alertBannerMessage[256]; static uint32_t alertBannerUntil; // 0 is a special case meaning forever static const char **optionsArrayPtr; + static const int *optionsEnumPtr; static uint8_t alertBannerOptions; // last x lines are seelctable options static std::function alertBannerCallback; + static uint32_t numDigits; + static uint32_t currentNumber; static bool pauseBanner; + static void resetBanner(); + static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1], + uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0); + static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); static bool isOverlayBannerShowing(); + + static graphics::notificationTypeEnum current_notification_type; }; } // namespace graphics diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 9c3a9eabb..9be8b04f4 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -18,32 +18,6 @@ #include #include -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(c)) || isAllowedPunctuation(c)) { - output += c; - inReplacement = false; - } else { - if (!inReplacement) { - output += 0xbf; // ISO-8859-1 for inverted question mark - inReplacement = true; - } - } - } - - return output; -} - // External variables extern graphics::Screen *screen; @@ -443,7 +417,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); */ float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); - if (screen->ignoreCompass) { + if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { myHeading = 0; } else { bearing -= myHeading; @@ -488,7 +462,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st const auto &op = ourNode->position; float myHeading = 0; - if (!screen->ignoreCompass) { + if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); } @@ -500,7 +474,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); */ float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); - if (!screen->ignoreCompass) + if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) bearing -= myHeading; graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); @@ -600,7 +574,11 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta int chutil_bar_width = (isHighResolution) ? 100 : 50; if (!config.bluetooth.enabled) { +#if defined(USE_EINK) + chutil_bar_width = (isHighResolution) ? 50 : 30; +#else chutil_bar_width = (isHighResolution) ? 80 : 40; +#endif } int chutil_bar_height = (isHighResolution) ? 12 : 7; int extraoffset = (isHighResolution) ? 6 : 3; @@ -933,7 +911,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU // === Determine Compass Heading === float heading = 0; bool validHeading = false; - if (screen->ignoreCompass) { + if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { validHeading = true; } else { if (screen->hasHeading()) { @@ -999,7 +977,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU // "N" label float northAngle = 0; - if (!config.display.compass_north_top) + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) northAngle = -heading; float radius = compassRadius; int16_t nX = compassX + (radius - 1) * sin(northAngle); @@ -1042,7 +1020,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU // "N" label float northAngle = 0; - if (!config.display.compass_north_top) + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) northAngle = -heading; float radius = compassRadius; int16_t nX = compassX + (radius - 1) * sin(northAngle); @@ -1066,9 +1044,16 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { static const uint8_t xbm[] = USERPREFS_OEM_IMAGE_DATA; - display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, - USERPREFS_OEM_IMAGE_HEIGHT, xbm); + if (isHighResolution) { + display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, + y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, + USERPREFS_OEM_IMAGE_HEIGHT, xbm); + } else { + + display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, + y + (SCREEN_HEIGHT - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, + USERPREFS_OEM_IMAGE_HEIGHT, xbm); + } switch (USERPREFS_OEM_FONT_SIZE) { case 0: @@ -1084,7 +1069,9 @@ void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, O display->setTextAlignment(TEXT_ALIGN_LEFT); const char *title = USERPREFS_OEM_TEXT; - display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); + if (isHighResolution) { + display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); + } display->setFont(FONT_SMALL); // Draw region in upper left diff --git a/src/graphics/emotes.cpp b/src/graphics/emotes.cpp index 205d5c660..e1a105d20 100644 --- a/src/graphics/emotes.cpp +++ b/src/graphics/emotes.cpp @@ -11,11 +11,11 @@ const Emote emotes[] = { {"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, // 👎 Thumbs Down // --- Smileys (Multiple Unicode Aliases) --- - {"\U0001F60A", smiley, smiley_width, smiley_height}, // 😊 Smiling Face with Smiling Eyes - {"\U0001F600", smiley, smiley_width, smiley_height}, // 😀 Grinning Face - {"\U0001F642", smiley, smiley_width, smiley_height}, // 🙂 Slightly Smiling Face - {"\U0001F609", smiley, smiley_width, smiley_height}, // 😉 Winking Face - {"\U0001F601", smiley, smiley_width, smiley_height}, // 😁 Grinning Face with Smiling Eyes + {"\U0001F60A", Smiling_Eyes, Smiling_Eyes_width, Smiling_Eyes_height}, // 😊 Smiling Eyes + {"\U0001F600", Grinning, Grinning_width, Grinning_height}, // 😀 Grinning Face + {"\U0001F642", Slightly_Smiling, Slightly_Smiling_width, Slightly_Smiling_height}, // 🙂 Slightly Smiling Face + {"\U0001F609", Winking_Face, Winking_Face_width, Winking_Face_height}, // 😉 Winking Face + {"\U0001F601", Grinning_Smiling_Eyes, Grinning_Smiling_Eyes_width, Grinning_Smiling_Eyes_height}, // 😁 Grinning Smiling Eyes // --- Question/Alert --- {"\u2753", question, question_width, question_height}, // ❓ Question Mark @@ -23,10 +23,11 @@ const Emote emotes[] = { // --- Laughing Faces --- {"\U0001F602", haha, haha_width, haha_height}, // 😂 Face with Tears of Joy - {"\U0001F923", haha, haha_width, haha_height}, // 🤣 Rolling on the Floor Laughing - {"\U0001F606", haha, haha_width, haha_height}, // 😆 Smiling with Open Mouth and Closed Eyes - {"\U0001F605", haha, haha_width, haha_height}, // 😅 Smiling with Sweat - {"\U0001F604", haha, haha_width, haha_height}, // 😄 Grinning Face with Smiling Eyes + {"\U0001F923", ROFL, ROFL_width, ROFL_height}, // 🤣 Rolling on the Floor Laughing + {"\U0001F606", Smiling_Closed_Eyes, Smiling_Closed_Eyes_width, Smiling_Closed_Eyes_height}, // 😆 Smiling Closed Eyes + {"\U0001F605", haha, haha_width, haha_height}, // 😅 Smiling with Sweat + {"\U0001F604", Grinning_SmilingEyes2, Grinning_SmilingEyes2_width, + Grinning_SmilingEyes2_height}, // 😄 Grinning Face with Smiling Eyes // --- Gestures and People --- {"\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, }; -const unsigned char smiley[] PROGMEM = { - 0x00, 0xfe, 0x0f, 0x00, 0x80, 0x01, 0x30, 0x00, 0x40, 0x00, 0xc0, 0x00, 0x20, 0x00, 0x00, 0x01, 0x10, 0x00, 0x00, 0x02, - 0x08, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x10, 0x02, 0x0e, 0x0e, 0x10, 0x02, 0x09, 0x12, 0x10, - 0x01, 0x09, 0x12, 0x20, 0x01, 0x0f, 0x1e, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, - 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x81, 0x00, 0x20, 0x20, - 0x82, 0x00, 0x20, 0x10, 0x02, 0x01, 0x10, 0x10, 0x04, 0x02, 0x08, 0x08, 0x04, 0xfc, 0x07, 0x08, 0x08, 0x00, 0x00, 0x04, - 0x10, 0x00, 0x00, 0x02, 0x20, 0x00, 0x00, 0x01, 0x40, 0x00, 0xc0, 0x00, 0x80, 0x01, 0x30, 0x00, 0x00, 0xfe, 0x0f, 0x00}; +const unsigned char 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, 0xff, 0xff, 0xcf, 0xfc, 0xff, 0xff, 0xcf, + 0x7e, 0xf8, 0xc3, 0xdf, 0x3e, 0xf0, 0x81, 0xdf, 0xbf, 0xf7, 0xbd, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0x3f, 0xff, + 0x6f, 0xff, 0xdf, 0xfe, 0x6f, 0xff, 0xdf, 0xfe, 0x9f, 0xff, 0x3f, 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[] 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 = { 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 = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, - 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x1F, 0x3E, 0x00, 0x80, 0x03, 0x70, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0xC0, 0x00, 0xC2, 0x00, - 0x60, 0x00, 0x03, 0x00, 0x60, 0x00, 0xC1, 0x1F, 0x60, 0x80, 0x8F, 0x31, 0x30, 0x0E, 0x80, 0x31, 0x30, 0x10, 0x30, 0x1F, - 0x30, 0x08, 0x58, 0x00, 0x30, 0x04, 0x6C, 0x03, 0x60, 0x00, 0xF3, 0x01, 0x60, 0xC0, 0xFC, 0x01, 0x80, 0x38, 0xBF, 0x01, - 0xE0, 0xC5, 0xDF, 0x00, 0xB0, 0xF9, 0xEF, 0x00, 0x30, 0xF1, 0x73, 0x00, 0xB0, 0x1D, 0x3E, 0x00, 0xF0, 0xFD, 0x0F, 0x00, - 0xE0, 0xE0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -}; + 0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0x7f, 0xc0, 0xe0, 0xf9, 0xf3, 0xc0, + 0xf0, 0xfe, 0xef, 0xc1, 0x38, 0xff, 0x9f, 0xc3, 0xd8, 0xff, 0x7f, 0xc3, 0xfc, 0xf8, 0xe3, 0xc7, 0x7c, 0xf7, 0xdd, 0xcf, + 0xbe, 0xef, 0xbe, 0xcf, 0xfe, 0xff, 0xff, 0xcf, 0xef, 0xff, 0xff, 0xde, 0xe7, 0xff, 0xff, 0xdc, 0xeb, 0xff, 0xff, 0xda, + 0xed, 0xff, 0xff, 0xd6, 0xee, 0xff, 0xff, 0xce, 0x36, 0x00, 0x80, 0xcd, 0xb8, 0xff, 0xbf, 0xc3, 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 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 = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x0C, 0x9C, 0x01, 0x80, 0x17, 0x20, 0x01, 0x80, 0x26, 0x46, 0x02, 0x80, 0x44, 0x88, 0x02, 0xC0, 0x89, 0x8A, 0x02, - 0x40, 0x93, 0x8B, 0x02, 0x40, 0x26, 0x13, 0x00, 0x80, 0x44, 0x16, 0x00, 0xC0, 0x89, 0x24, 0x00, 0x40, 0x93, 0x60, 0x00, - 0x40, 0x26, 0x40, 0x00, 0x80, 0x0C, 0x80, 0x00, 0x00, 0x09, 0x80, 0x00, 0x00, 0x02, 0x80, 0x00, 0x40, 0x06, 0x80, 0x00, - 0x50, 0x0C, 0x80, 0x00, 0x50, 0x08, 0x40, 0x00, 0x90, 0x10, 0x20, 0x00, 0xB0, 0x21, 0x10, 0x00, 0x20, 0x47, 0x18, 0x00, - 0x40, 0x80, 0x0F, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -}; + 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x7f, 0xc0, 0x00, 0x00, 0xc0, 0xc1, 0x00, 0x00, 0x00, 0xc7, + 0x00, 0x00, 0x1e, 0xcc, 0x00, 0x00, 0x30, 0xc8, 0x00, 0x00, 0x60, 0xd8, 0x00, 0x08, 0xc0, 0xd0, 0x00, 0x1a, 0x81, 0xd1, + 0x00, 0x36, 0x03, 0xd3, 0x80, 0x6d, 0x06, 0xd2, 0x00, 0xdb, 0x0c, 0xc2, 0x80, 0xb6, 0x1d, 0xc0, 0x80, 0x6d, 0x1f, 0xc0, + 0x00, 0xdb, 0x3f, 0xc0, 0x00, 0xf6, 0x7f, 0xc0, 0x00, 0xfc, 0x7f, 0xc0, 0x08, 0xf8, 0x7f, 0xc0, 0x48, 0xf0, 0x7f, 0xc0, + 0x48, 0xe0, 0x7f, 0xc0, 0xc8, 0xc0, 0x3f, 0xc0, 0x98, 0x81, 0x1f, 0xc0, 0x10, 0x03, 0x00, 0xc0, 0x30, 0x0e, 0x00, 0xc0, + 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 = { - 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x3C, 0xFE, 0x1F, 0x0F, - 0xFE, 0xFE, 0xDF, 0x1F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, - 0x3E, 0xC0, 0x00, 0x1F, 0x1E, 0x00, 0x00, 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x08, 0x0E, 0x1C, 0x04, 0x00, 0x0E, 0x1C, 0x00, - 0x04, 0x0E, 0x1C, 0x08, 0x04, 0x0E, 0x1C, 0x08, 0x04, 0x04, 0x08, 0x08, 0x04, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x08, - 0x8C, 0x07, 0x70, 0x0C, 0x88, 0xFC, 0x4F, 0x04, 0x88, 0x01, 0x40, 0x04, 0x90, 0xFF, 0x7F, 0x02, 0x30, 0x03, 0x30, 0x03, - 0x60, 0x0E, 0x9C, 0x01, 0xC0, 0xF8, 0xC7, 0x00, 0x80, 0x01, 0x60, 0x00, 0x00, 0x0E, 0x1C, 0x00, 0x00, 0xF8, 0x07, 0x00, -}; + 0x00, 0x0c, 0x0c, 0xc0, 0x00, 0x02, 0x10, 0xc0, 0x00, 0x01, 0x20, 0xc0, 0xbc, 0x00, 0x40, 0xcf, 0xc2, 0x01, 0xe0, 0xd0, + 0x01, 0x01, 0x20, 0xe0, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0, + 0xc1, 0x3f, 0xff, 0xe0, 0xe1, 0xff, 0xff, 0xe1, 0xf2, 0xf3, 0xf3, 0xd3, 0xf4, 0xf1, 0xe3, 0xcb, 0xfc, 0xf1, 0xe3, 0xc7, + 0xf8, 0xf1, 0xe3, 0xc7, 0xf8, 0xf1, 0xe3, 0xc7, 0xf8, 0xfb, 0xf7, 0xc7, 0xf8, 0xff, 0xff, 0xc7, 0xf8, 0xff, 0xff, 0xc7, + 0x70, 0xf8, 0x8f, 0xc3, 0x70, 0x03, 0xb0, 0xc3, 0x70, 0xfe, 0xbf, 0xc3, 0x60, 0x00, 0x80, 0xc1, 0xc0, 0x00, 0xc0, 0xc0, + 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 = { 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 = { - 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x10, 0x03, 0xC0, 0x01, 0x38, 0x07, 0x7C, 0x0F, 0x38, 0x1F, 0x03, 0x30, 0x1E, - 0xFE, 0x01, 0xE0, 0x1F, 0x7E, 0x00, 0x80, 0x1F, 0x3C, 0x00, 0x00, 0x0F, 0x1C, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x06, - 0x08, 0x00, 0x00, 0x04, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x0E, 0x1C, 0x0C, - 0x0C, 0x18, 0x06, 0x0C, 0x0C, 0x1C, 0x06, 0x0C, 0x0C, 0x1C, 0x0E, 0x0C, 0x0C, 0x1C, 0x0E, 0x0C, 0x0C, 0x0C, 0x06, 0x0C, - 0x08, 0x00, 0x00, 0x06, 0x18, 0x02, 0x10, 0x06, 0x10, 0x0C, 0x0C, 0x03, 0x30, 0xF8, 0x07, 0x03, 0x60, 0xE0, 0x80, 0x01, - 0xC0, 0x00, 0xC0, 0x00, 0x80, 0x01, 0x70, 0x00, 0x00, 0x06, 0x1C, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, -}; + 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x01, 0x00, 0x00, 0xe0, 0x03, 0x00, 0x00, 0xf0, 0x0f, 0xfc, 0x0f, 0xfc, + 0x3f, 0xff, 0x3f, 0xff, 0xfe, 0xff, 0xff, 0xdf, 0xfe, 0xff, 0xff, 0xdf, 0xfe, 0xff, 0xff, 0xdf, 0xfc, 0xff, 0xff, 0xcf, + 0xfc, 0xff, 0xff, 0xcf, 0xf8, 0xff, 0xff, 0xc7, 0xf0, 0xff, 0xff, 0xc3, 0xf0, 0xff, 0xff, 0xc3, 0xf0, 0xf1, 0xe3, 0xc3, + 0xf0, 0xe7, 0xf9, 0xc3, 0xf0, 0xe7, 0xf9, 0xc3, 0xf0, 0xe3, 0xf1, 0xc3, 0xf0, 0xe3, 0xf1, 0xc3, 0xf0, 0xe7, 0xf9, 0xc3, + 0xf0, 0xff, 0xff, 0xc3, 0xe0, 0xfd, 0xef, 0xc1, 0xe0, 0xf3, 0xf3, 0xc1, 0xc0, 0x07, 0xf8, 0xc0, 0x80, 0x1f, 0x7e, 0xc0, + 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 = { 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 = { - 0x00, 0x1C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xEC, 0x01, 0x00, 0x00, 0x8C, 0x07, 0x00, 0x00, 0x0C, 0x06, 0x00, - 0x00, 0x24, 0x0C, 0x00, 0x00, 0x34, 0x08, 0x00, 0x00, 0x1F, 0x08, 0x00, 0xC0, 0x0F, 0x08, 0x00, 0xC0, 0x00, 0x3C, 0x00, - 0x60, 0x00, 0x7C, 0x00, 0x60, 0x00, 0xC6, 0x00, 0x20, 0x00, 0xCB, 0x00, 0xA0, 0xC7, 0xFF, 0x00, 0xE0, 0x7F, 0xF7, 0x00, - 0xF0, 0x18, 0xE3, 0x03, 0x78, 0x18, 0x41, 0x03, 0x6C, 0x9B, 0x5D, 0x06, 0x64, 0x9B, 0x5D, 0x04, 0x44, 0x1A, 0x41, 0x04, - 0x4C, 0xD8, 0x63, 0x06, 0xF8, 0xFC, 0x36, 0x06, 0xFE, 0x0F, 0x9C, 0x1F, 0x07, 0x03, 0xC0, 0x30, 0x03, 0x00, 0x78, 0x20, - 0x01, 0x00, 0x1F, 0x20, 0x03, 0xE0, 0x03, 0x20, 0x07, 0x7E, 0x04, 0x30, 0xFE, 0x0F, 0xFC, 0x1F, 0xF0, 0x00, 0xF0, 0x0F, -}; + 0x00, 0x1c, 0x00, 0xc0, 0x00, 0x7c, 0x00, 0xc0, 0x00, 0xfc, 0x00, 0xc0, 0x00, 0x7c, 0x03, 0xc0, 0x00, 0xbe, 0x03, 0xc0, + 0x00, 0xdf, 0x0f, 0xc0, 0x80, 0xcf, 0x0f, 0xc0, 0xc0, 0xf1, 0x0f, 0xc0, 0x60, 0xfc, 0x0f, 0xc0, 0x30, 0xff, 0x07, 0xc0, + 0x90, 0xff, 0x3b, 0xc0, 0xc0, 0xff, 0x7d, 0xc0, 0xf8, 0xff, 0xfc, 0xc0, 0xf8, 0x3f, 0xf0, 0xc0, 0x78, 0x88, 0xc0, 0xc0, + 0x20, 0xe3, 0x18, 0xc0, 0x98, 0xe7, 0xbc, 0xc1, 0x9c, 0x64, 0xa4, 0xc3, 0x9e, 0x64, 0xa4, 0xc7, 0xbe, 0xe4, 0xa4, 0xc7, + 0xbc, 0x27, 0xbc, 0xc7, 0x38, 0x03, 0xd9, 0xc3, 0x00, 0xf0, 0x63, 0xc0, 0xf8, 0xfc, 0x3f, 0xcf, 0xfc, 0xff, 0x87, 0xdf, + 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 = { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b11110000, diff --git a/src/graphics/emotes.h b/src/graphics/emotes.h index 5640ac04a..30b164cbc 100644 --- a/src/graphics/emotes.h +++ b/src/graphics/emotes.h @@ -22,9 +22,25 @@ extern const int numEmotes; extern const unsigned char thumbup[] PROGMEM; extern const unsigned char thumbdown[] PROGMEM; -#define smiley_height 30 -#define smiley_width 30 -extern const unsigned char smiley[] PROGMEM; +#define Smiling_Eyes_height 30 +#define Smiling_Eyes_width 30 +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_width 25 @@ -38,6 +54,18 @@ extern const unsigned char bang[] PROGMEM; #define haha_width 30 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_width 30 extern const unsigned char wave_icon[] PROGMEM; diff --git a/src/main.cpp b/src/main.cpp index 9e0985a3a..773145951 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1363,7 +1363,7 @@ void setup() if (!rIf->reconfigure()) { LOG_WARN("Reconfigure failed, rebooting"); if (screen) { - screen->showOverlayBanner("Rebooting..."); + screen->showSimpleBanner("Rebooting..."); } rebootAtMsec = millis() + 5000; } @@ -1436,6 +1436,9 @@ void setup() LOG_DEBUG("Free heap : %7d bytes", ESP.getFreeHeap()); LOG_DEBUG("Free PSRAM : %7d bytes", ESP.getFreePsram()); #endif + + // We manually run this to update the NodeStatus + nodeDB->notifyObservers(true); } #endif diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 79047cbd8..5630a4ea3 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -407,6 +407,7 @@ NodeDB::NodeDB() #endif } #endif + sortMeshDB(); saveToDisk(saveWhat); } @@ -1000,7 +1001,10 @@ void NodeDB::cleanupMeshDB() 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 { removed++; } @@ -1087,8 +1091,8 @@ LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t if (f) { LOG_INFO("Load %s", filename); pb_istream_t stream = {&readcb, &f, protoSize}; - - memset(dest_struct, 0, objSize); + if (fields != &meshtastic_NodeDatabase_msg) // contains a vector object + memset(dest_struct, 0, objSize); if (!pb_decode(&stream, fields, dest_struct)) { LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); 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); 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 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() { - if (!Throttle::isWithinTimespanMs(lastSort, 1000 * 5)) { + if (!sortingIsPaused && (lastSort == 0 || !Throttle::isWithinTimespanMs(lastSort, 1000 * 5))) { lastSort = millis(); - std::sort(meshNodes->begin(), meshNodes->begin() + numMeshNodes, - [](const meshtastic_NodeInfoLite &a, const meshtastic_NodeInfoLite &b) { - if (a.num == myNodeInfo.my_node_num && b.num == myNodeInfo.my_node_num) // in theory impossible - return false; - if (a.num == myNodeInfo.my_node_num) { - return true; - } - if (b.num == myNodeInfo.my_node_num) { - return false; - } - bool aFav = a.is_favorite; - bool bFav = b.is_favorite; - if (aFav != bFav) - return aFav; - return a.last_heard > b.last_heard; - }); + bool changed = true; + while (changed) { // dumb reverse bubble sort, but probably not bad for what we're doing + changed = false; + for (int i = numMeshNodes - 1; i > 0; i--) { // lowest case this should examine is i == 1 + if (meshNodes->at(i - 1).num == getNodeNum()) { + // noop + } else if (meshNodes->at(i).num == + getNodeNum()) { // in the oddball case our own node num is not at location 0, put it there + // 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)); + changed = true; + } else if (meshNodes->at(i).is_favorite && !meshNodes->at(i - 1).is_favorite) { + std::swap(meshNodes->at(i), meshNodes->at(i - 1)); + changed = true; + } 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); } } diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index b6e4d600b..845f42c76 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -191,6 +191,16 @@ class NodeDB */ 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 NodeNum getNodeNum() { return myNodeInfo.my_node_num; } @@ -208,9 +218,6 @@ class NodeDB 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 uint8_t getMeshNodeChannel(NodeNum n); @@ -278,6 +285,14 @@ class NodeDB bool restorePreferences(meshtastic_AdminMessage_BackupLocation location, 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: bool duplicateWarned = false; 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 meshtastic_NodeInfoLite *getOrCreateMeshNode(NodeNum n); - /// 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); - } + /* + * Internal boolean to track sorting paused + */ + bool sortingIsPaused = false; + + /// pick a provisional nodenum we hope no one is using + void pickNewNodeNum(); /// read our db from flash void loadFromDisk(); diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index aad7f5f06..12a586cd7 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -1190,7 +1190,7 @@ void AdminModule::reboot(int32_t seconds) { LOG_INFO("Reboot in %d seconds", seconds); if (screen) - screen->showOverlayBanner("Rebooting...", 0); // stays on screen + screen->showSimpleBanner("Rebooting...", 0); // stays on screen rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000); } diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 4d8d6ce4b..1ab4af02d 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -442,9 +442,13 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event return 1; } - // UP - if (isUp && destIndex > 0) { - destIndex--; + if (isUp) { + if (destIndex > 0) { + destIndex--; + } else if (totalEntries > 0) { + destIndex = totalEntries - 1; + } + if ((destIndex / columns) < scrollIndex) scrollIndex = destIndex / columns; else if ((destIndex / columns) >= (scrollIndex + visibleRows)) @@ -454,9 +458,14 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event return 1; } - // DOWN - if (isDown && destIndex + 1 < totalEntries) { - destIndex++; + if (isDown) { + if (destIndex + 1 < totalEntries) { + destIndex++; + } else if (totalEntries > 0) { + destIndex = 0; + scrollIndex = 0; + } + if ((destIndex / columns) >= (scrollIndex + visibleRows)) scrollIndex = (destIndex / columns) - visibleRows + 1; diff --git a/src/modules/KeyVerificationModule.cpp b/src/modules/KeyVerificationModule.cpp index c0972c155..408d29126 100644 --- a/src/modules/KeyVerificationModule.cpp +++ b/src/modules/KeyVerificationModule.cpp @@ -59,7 +59,7 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket & r->hash1.size == 0) { memcpy(hash2, r->hash2.bytes, 32); if (screen) - screen->showOverlayBanner("Enter Security Number", 30000); + screen->showSimpleBanner("Enter Security Number", 30000); meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->level = meshtastic_LogRecord_Level_WARNING; @@ -82,12 +82,19 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket & static const char *optionsArray[] = {"ACCEPT", "REJECT"}; LOG_INFO("Hash1 matches!"); 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) { auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; } - }); + }; + screen->showOverlayBanner(options); } meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->level = meshtastic_LogRecord_Level_WARNING; @@ -185,7 +192,7 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply() responsePacket->pki_encrypted = true; if (screen) { snprintf(message, 25, "Security Number \n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000); - screen->showOverlayBanner(message, 30000); + screen->showSimpleBanner(message, 30000); LOG_WARN("%s", message); } meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); @@ -255,7 +262,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber) sprintf(message, "Verification: \n"); generateVerificationCode(message + 15); // send the toPhone packet if (screen) { - screen->showOverlayBanner(message, 30000); + screen->showSimpleBanner(message, 30000); } meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->level = meshtastic_LogRecord_Level_WARNING; diff --git a/src/modules/SystemCommandsModule.cpp b/src/modules/SystemCommandsModule.cpp index 08c87ec64..ab9439b39 100644 --- a/src/modules/SystemCommandsModule.cpp +++ b/src/modules/SystemCommandsModule.cpp @@ -47,7 +47,7 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event) bool isMuted = externalNotificationModule->getMute(); externalNotificationModule->setMute(!isMuted); 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; // Bluetooth @@ -58,24 +58,24 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event) #if defined(ARDUINO_ARCH_NRF52) if (!config.bluetooth.enabled) { disableBluetooth(); - IF_SCREEN(screen->showOverlayBanner("Bluetooth OFF\nRebooting", 3000)); + IF_SCREEN(screen->showSimpleBanner("Bluetooth OFF\nRebooting", 3000)); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 2000; } else { - IF_SCREEN(screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000)); + IF_SCREEN(screen->showSimpleBanner("Bluetooth ON\nRebooting", 3000)); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; } #else if (!config.bluetooth.enabled) { disableBluetooth(); - IF_SCREEN(screen->showOverlayBanner("Bluetooth OFF", 3000)); + IF_SCREEN(screen->showSimpleBanner("Bluetooth OFF", 3000)); } else { - IF_SCREEN(screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000)); + IF_SCREEN(screen->showSimpleBanner("Bluetooth ON\nRebooting", 3000)); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; } #endif return 0; case INPUT_BROKER_MSG_REBOOT: - IF_SCREEN(screen->showOverlayBanner("Rebooting...", 0)); + IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0)); nodeDB->saveToDisk(); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; // runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; @@ -92,7 +92,7 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event) gps->toggleGpsMode(); const char *msg = (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 return true; @@ -100,15 +100,15 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event) case INPUT_BROKER_SEND_PING: service->refreshLocalMeshNode(); if (service->trySendPosition(NODENUM_BROADCAST, true)) { - IF_SCREEN(screen->showOverlayBanner("Position\nSent", 3000)); + IF_SCREEN(screen->showSimpleBanner("Position\nSent", 3000)); } else { - IF_SCREEN(screen->showOverlayBanner("Node Info\nSent", 3000)); + IF_SCREEN(screen->showSimpleBanner("Node Info\nSent", 3000)); } return true; // Power control case INPUT_BROKER_SHUTDOWN: LOG_ERROR("Shutting Down"); - IF_SCREEN(screen->showOverlayBanner("Shutting Down...")); + IF_SCREEN(screen->showSimpleBanner("Shutting Down...")); nodeDB->saveToDisk(); shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000; // runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 46a24a816..d1b10fa82 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -450,7 +450,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt if (isOwnTelemetry && bannerMsg && isCooldownOver) { 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 if (m.iaq > 200 && moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) { diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp index cab668406..aab3ed6bc 100644 --- a/src/modules/WaypointModule.cpp +++ b/src/modules/WaypointModule.cpp @@ -137,7 +137,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) { const meshtastic_PositionLite &op = ourNode->position; float myHeading; - if (screen->ignoreCompass) { + if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { myHeading = 0; } else { 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)); // 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 (!screen->ignoreCompass) + if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) bearingToOther -= myHeading; graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 3ab06695b..8f53c9229 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -9,6 +9,7 @@ #include "mesh/mesh-pb-constants.h" #include "sleep.h" #include +#include NimBLECharacteristic *fromNumCharacteristic; NimBLECharacteristic *BatteryCharacteristic; @@ -17,8 +18,36 @@ NimBLEServer *bleServer; 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 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 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) */ @@ -51,15 +80,16 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks { virtual void onWrite(NimBLECharacteristic *pCharacteristic) { - LOG_DEBUG("To Radio onwrite"); auto val = pCharacteristic->getValue(); if (memcmp(lastToRadio, val.data(), val.length()) != 0) { - LOG_DEBUG("New ToRadio packet"); - memcpy(lastToRadio, val.data(), val.length()); - bluetoothPhoneAPI->handleToRadio(val.data(), val.length()); - } else { - LOG_DEBUG("Drop dup ToRadio packet we just saw"); + if (bluetoothPhoneAPI->queue_size < 3) { + memcpy(lastToRadio, val.data(), val.length()); + std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex); + bluetoothPhoneAPI->nimble_queue.at(bluetoothPhoneAPI->queue_size) = val; + bluetoothPhoneAPI->queue_size++; + bluetoothPhoneAPI->setIntervalFromNow(0); + } } } }; @@ -68,12 +98,19 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks { virtual void onRead(NimBLECharacteristic *pCharacteristic) { - uint8_t fromRadioBytes[meshtastic_FromRadio_size]; - size_t numBytes = bluetoothPhoneAPI->getFromRadio(fromRadioBytes); - - std::string fromRadioByteString(fromRadioBytes, fromRadioBytes + numBytes); - + while (!bluetoothPhoneAPI->hasChecked) { + bluetoothPhoneAPI->setIntervalFromNow(0); + delay(20); + } + std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex); + std::string fromRadioByteString(bluetoothPhoneAPI->fromRadioBytes, + bluetoothPhoneAPI->fromRadioBytes + bluetoothPhoneAPI->numBytes); 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; } }; diff --git a/src/shutdown.h b/src/shutdown.h index 998944677..7e2120149 100644 --- a/src/shutdown.h +++ b/src/shutdown.h @@ -42,7 +42,7 @@ void powerCommandsCheck() #if defined(ARCH_ESP32) || defined(ARCH_NRF52) if (shutdownAtMsec && screen) { - screen->showOverlayBanner("Shutting Down...", 0); // stays on screen + screen->showSimpleBanner("Shutting Down...", 0); // stays on screen } #endif diff --git a/variants/heltec_mesh_node_t114/variant.h b/variants/heltec_mesh_node_t114/variant.h index 798c3538a..f4f0baf13 100644 --- a/variants/heltec_mesh_node_t114/variant.h +++ b/variants/heltec_mesh_node_t114/variant.h @@ -57,6 +57,9 @@ extern "C" { #define TFT_OFFSET_X 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 SCREEN_ROTATE // #define SCREEN_TRANSITION_FRAMERATE 5 diff --git a/variants/picomputer-s3/variant.h b/variants/picomputer-s3/variant.h index ff8faa6f4..8252e841c 100644 --- a/variants/picomputer-s3/variant.h +++ b/variants/picomputer-s3/variant.h @@ -49,7 +49,7 @@ #define SCREEN_TRANSITION_FRAMERATE 5 // 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 diff --git a/variants/tracksenger/internal/variant.h b/variants/tracksenger/internal/variant.h index 57ead848d..6f75ad0e2 100644 --- a/variants/tracksenger/internal/variant.h +++ b/variants/tracksenger/internal/variant.h @@ -72,7 +72,7 @@ #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // Picomputer gets a white on black display -#define TFT_MESH COLOR565(0xFF, 0xFF, 0xFF) +#define TFT_MESH_OVERRIDE COLOR565(255, 255, 255) // keyboard changes diff --git a/variants/tracksenger/lcd/variant.h b/variants/tracksenger/lcd/variant.h index ecf4e854e..843bf3924 100644 --- a/variants/tracksenger/lcd/variant.h +++ b/variants/tracksenger/lcd/variant.h @@ -96,7 +96,7 @@ #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // Picomputer gets a white on black display -#define TFT_MESH COLOR565(0xFF, 0xFF, 0xFF) +#define TFT_MESH_OVERRIDE COLOR565(255, 255, 255) // keyboard changes diff --git a/variants/tracksenger/oled/variant.h b/variants/tracksenger/oled/variant.h index 70f0f3209..85cc019c4 100644 --- a/variants/tracksenger/oled/variant.h +++ b/variants/tracksenger/oled/variant.h @@ -74,7 +74,7 @@ #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // Picomputer gets a white on black display -#define TFT_MESH COLOR565(0xFF, 0xFF, 0xFF) +#define TFT_MESH_OVERRIDE COLOR565(255, 255, 255) // keyboard changes