diff --git a/src/ButtonThread.cpp b/src/ButtonThread.cpp index a6840e5a1..c9b4be8a7 100644 --- a/src/ButtonThread.cpp +++ b/src/ButtonThread.cpp @@ -286,9 +286,12 @@ int32_t ButtonThread::runOnce() case BUTTON_EVENT_LONG_PRESSED: { LOG_BUTTON("Long press!"); powerFSM.trigger(EVENT_PRESS); + if (screen) { - screen->startAlert("Shutting down..."); + // Show shutdown message as a temporary overlay banner + screen->showOverlayBanner("Shutting Down..."); // Display for 3 seconds } + playBeep(); break; } diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index ee44d77cc..0029ed787 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -308,26 +308,43 @@ void Screen::showOverlayBanner(const String &message, uint32_t durationMs) { // Store the message and set the expiration timestamp alertBannerMessage = message; - alertBannerUntil = millis() + durationMs; + alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; } // Draws the overlay banner on screen, if still within display duration static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) { // Exit if no message is active or duration has passed - if (alertBannerMessage.length() == 0 || millis() > alertBannerUntil) return; + if (alertBannerMessage.length() == 0 || (alertBannerUntil != 0 && millis() > alertBannerUntil)) return; // === Layout Configuration === - constexpr uint16_t padding = 5; // Padding around the text + constexpr uint16_t padding = 5; // Padding around text inside the box + constexpr uint8_t lineSpacing = 1; // Extra space between lines // Setup font and alignment display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setTextAlignment(TEXT_ALIGN_LEFT); // We will manually center per line - // === Measure and position the box === - uint16_t textWidth = display->getStringWidth(alertBannerMessage.c_str(), alertBannerMessage.length(), true); - uint16_t boxWidth = padding * 2 + textWidth; - uint16_t boxHeight = FONT_HEIGHT_SMALL + padding * 2; + // === Split the message into lines (supports multi-line banners) === + std::vector lines; + int start = 0, newlineIdx; + while ((newlineIdx = alertBannerMessage.indexOf('\n', start)) != -1) { + lines.push_back(alertBannerMessage.substring(start, newlineIdx)); + start = newlineIdx + 1; + } + lines.push_back(alertBannerMessage.substring(start)); + + // === Measure text dimensions === + uint16_t maxWidth = 0; + std::vector lineWidths; + for (const auto& line : lines) { + uint16_t w = display->getStringWidth(line.c_str(), line.length(), true); + lineWidths.push_back(w); + if (w > maxWidth) maxWidth = w; + } + + uint16_t boxWidth = padding * 2 + maxWidth; + uint16_t boxHeight = padding * 2 + lines.size() * FONT_HEIGHT_SMALL + (lines.size() - 1) * lineSpacing; int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); int16_t boxTop = (display->height() / 2) - (boxHeight / 2); @@ -338,9 +355,16 @@ static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *sta display->setColor(WHITE); display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); // Border - // === Draw the text (twice for faux bold) === - display->drawString(boxLeft + padding, boxTop + padding, alertBannerMessage); - display->drawString(boxLeft + padding + 1, boxTop + padding, alertBannerMessage); // Faux bold effect + // === Draw each line centered in the box === + int16_t lineY = boxTop + padding; + for (size_t i = 0; i < lines.size(); ++i) { + int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; + + display->drawString(textX, lineY, lines[i]); + display->drawString(textX + 1, lineY, lines[i]); // Faux bold + + lineY += FONT_HEIGHT_SMALL + lineSpacing; + } } // draw overlay in bottom right corner of screen to show when notifications are muted or modifier key is active @@ -4199,11 +4223,17 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) setFrames(FOCUS_PRESERVE); // Stay on same frame, silently add/remove frames } else { // Incoming message - // setFrames(FOCUS_TEXTMESSAGE); // Focus on the new message devicestate.has_rx_text_message = true; // Needed to include the message frame hasUnreadMessage = true; // Enables mail icon in the header setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view forceDisplay(); // Forces screen redraw (this works in your codebase) + + // === Show banner: "New Message" followed by name on second line === + const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from); + if (node && node->has_user && node->user.long_name[0]) { + String name = String(node->user.long_name); + screen->showOverlayBanner("New Message\nfrom " + name, 3000); // Multiline banner + } } } diff --git a/src/main.cpp b/src/main.cpp index 9ef944e65..651f1ff16 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1226,9 +1226,12 @@ void setup() LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset"); config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; nodeDB->saveToDisk(SEGMENT_CONFIG); + if (!rIf->reconfigure()) { LOG_WARN("Reconfigure failed, rebooting"); - screen->startAlert("Rebooting..."); + if (screen) { + screen->showOverlayBanner("Rebooting..."); + } rebootAtMsec = millis() + 5000; } } diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 88109bc78..fcc70e39f 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -1096,7 +1096,7 @@ void AdminModule::handleGetDeviceUIConfig(const meshtastic_MeshPacket &req) void AdminModule::reboot(int32_t seconds) { LOG_INFO("Reboot in %d seconds", seconds); - screen->startAlert("Rebooting..."); + screen->showOverlayBanner("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 4700f122e..5e0645f24 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -463,52 +463,57 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) break; // mute (switch off/toggle) external notifications on fn+m case INPUT_BROKER_MSG_MUTE_TOGGLE: - if (moduleConfig.external_notification.enabled == true) { - if (externalNotificationModule->getMute()) { - externalNotificationModule->setMute(false); - showTemporaryMessage("Notifications \nEnabled"); - if (screen) - screen->removeFunctionSymbol("M"); // remove the mute symbol from the bottom right corner - } else { - externalNotificationModule->stopNow(); // this will turn off all GPIO and sounds and idle the loop - externalNotificationModule->setMute(true); - showTemporaryMessage("Notifications \nDisabled"); - if (screen) - screen->setFunctionSymbol("M"); // add the mute symbol to the bottom right corner + if (moduleConfig.external_notification.enabled == true) { + if (externalNotificationModule->getMute()) { + externalNotificationModule->setMute(false); + if (screen) { + screen->removeFunctionSymbol("M"); + screen->showOverlayBanner("Notifications\nEnabled", 3000); + } + } else { + externalNotificationModule->stopNow(); + externalNotificationModule->setMute(true); + if (screen) { + screen->setFunctionSymbol("M"); + screen->showOverlayBanner("Notifications\nDisabled", 3000); } } - break; - case INPUT_BROKER_MSG_GPS_TOGGLE: // toggle GPS like triple press does -#if !MESHTASTIC_EXCLUDE_GPS + } + break; + + case INPUT_BROKER_MSG_GPS_TOGGLE: + #if !MESHTASTIC_EXCLUDE_GPS if (gps != nullptr) { gps->toggleGpsMode(); } - if (screen) + if (screen) { screen->forceDisplay(); - showTemporaryMessage("GPS Toggled"); -#endif + screen->showOverlayBanner("GPS Toggled", 3000); + } + #endif break; - case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE: // toggle Bluetooth on/off + + case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE: if (config.bluetooth.enabled == true) { config.bluetooth.enabled = false; LOG_INFO("User toggled Bluetooth"); nodeDB->saveToDisk(); disableBluetooth(); - showTemporaryMessage("Bluetooth OFF"); - } else if (config.bluetooth.enabled == false) { + if (screen) screen->showOverlayBanner("Bluetooth OFF", 3000); + } else { config.bluetooth.enabled = true; LOG_INFO("User toggled Bluetooth"); nodeDB->saveToDisk(); rebootAtMsec = millis() + 2000; - showTemporaryMessage("Bluetooth ON\nReboot"); + if (screen) screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000); } break; - case INPUT_BROKER_MSG_SEND_PING: // fn+space send network ping like double press does + case INPUT_BROKER_MSG_SEND_PING: service->refreshLocalMeshNode(); if (service->trySendPosition(NODENUM_BROADCAST, true)) { - showTemporaryMessage("Position \nUpdate Sent"); + if (screen) screen->showOverlayBanner("Position\nUpdate Sent", 3000); } else { - showTemporaryMessage("Node Info \nUpdate Sent"); + if (screen) screen->showOverlayBanner("Node Info\nUpdate Sent", 3000); } break; case INPUT_BROKER_MSG_DISMISS_FRAME: // fn+del: dismiss screen frames like text or waypoint @@ -869,14 +874,13 @@ int32_t CannedMessageModule::runOnce() // handle fn+s for shutdown case INPUT_BROKER_MSG_SHUTDOWN: if (screen) - screen->startAlert("Shutting down..."); shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000; runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; break; // and fn+r for reboot case INPUT_BROKER_MSG_REBOOT: if (screen) - screen->startAlert("Rebooting..."); + screen->showOverlayBanner("Rebooting...", 0); // stays on screen rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; break; diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index c4581c6a1..d08cdb764 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -402,14 +402,24 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt entries.push_back("Prss: " + String(m.barometric_pressure, 0) + " hPa"); if (m.iaq != 0) { String aqi = "IAQ: " + String(m.iaq); + const char *bannerMsg = nullptr; // Default: no banner - if (m.iaq <= 50) aqi += " (Good)"; - else if (m.iaq <= 100) aqi += " (Moderate)"; - else if (m.iaq <= 150) aqi += " (Poor)"; - else if (m.iaq <= 200) aqi += " (Unhealthy)"; - else if (m.iaq <= 250) aqi += " (Very Unhealthy)"; - else if (m.iaq <= 350) aqi += " (Hazardous)"; - else aqi += " (Extreme)"; + if (m.iaq <= 25) aqi += " (Excellent)"; + else if (m.iaq <= 50) aqi += " (Good)"; + else if (m.iaq <= 100) aqi += " (Moderate)"; + else if (m.iaq <= 150) aqi += " (Poor)"; + else if (m.iaq <= 200) { + aqi += " (Unhealthy)"; + bannerMsg = "Unhealthy IAQ"; + } + else if (m.iaq <= 300) { + aqi += " (Very Unhealthy)"; + bannerMsg = "Very Unhealthy IAQ"; + } + else { + aqi += " (Hazardous)"; + bannerMsg = "Hazardous IAQ"; + } entries.push_back(aqi); @@ -418,14 +428,17 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt uint32_t now = millis(); bool isOwnTelemetry = lastMeasurementPacket->from == nodeDB->getNodeNum(); - bool isIAQAlert = m.iaq > 200 && (now - lastAlertTime > 60000); + bool isCooldownOver = (now - lastAlertTime > 60000); - if (isOwnTelemetry && isIAQAlert) { - LOG_INFO("drawFrame: IAQ %d (own) — showing banner", m.iaq); - screen->showOverlayBanner("Unhealthy IAQ Levels", 3000); // Always show banner - if (moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) { - playLongBeep(); // Only buzz if not muted + if (isOwnTelemetry && bannerMsg && isCooldownOver) { + LOG_INFO("drawFrame: IAQ %d (own) — showing banner: %s", m.iaq, bannerMsg); + screen->showOverlayBanner(bannerMsg, 3000); + + // Only buzz if IAQ is over 200 + if (m.iaq > 200 && moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) { + playLongBeep(); } + lastAlertTime = now; } } diff --git a/src/shutdown.h b/src/shutdown.h index f02cb7964..d585ddb30 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->startAlert("Shutting down..."); + screen->showOverlayBanner("Shutting Down...", 0); // stays on screen } #endif