mirror of
https://github.com/meshtastic/firmware.git
synced 2025-06-08 22:22:05 +00:00
Notification banners implemented
This commit is contained in:
parent
581021031c
commit
14752caee5
@ -286,9 +286,12 @@ int32_t ButtonThread::runOnce()
|
|||||||
case BUTTON_EVENT_LONG_PRESSED: {
|
case BUTTON_EVENT_LONG_PRESSED: {
|
||||||
LOG_BUTTON("Long press!");
|
LOG_BUTTON("Long press!");
|
||||||
powerFSM.trigger(EVENT_PRESS);
|
powerFSM.trigger(EVENT_PRESS);
|
||||||
|
|
||||||
if (screen) {
|
if (screen) {
|
||||||
screen->startAlert("Shutting down...");
|
// Show shutdown message as a temporary overlay banner
|
||||||
|
screen->showOverlayBanner("Shutting Down..."); // Display for 3 seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
playBeep();
|
playBeep();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -308,26 +308,43 @@ void Screen::showOverlayBanner(const String &message, uint32_t durationMs)
|
|||||||
{
|
{
|
||||||
// Store the message and set the expiration timestamp
|
// Store the message and set the expiration timestamp
|
||||||
alertBannerMessage = message;
|
alertBannerMessage = message;
|
||||||
alertBannerUntil = millis() + durationMs;
|
alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draws the overlay banner on screen, if still within display duration
|
// Draws the overlay banner on screen, if still within display duration
|
||||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state)
|
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||||
{
|
{
|
||||||
// Exit if no message is active or duration has passed
|
// 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 ===
|
// === 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
|
// Setup font and alignment
|
||||||
display->setFont(FONT_SMALL);
|
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 ===
|
// === Split the message into lines (supports multi-line banners) ===
|
||||||
uint16_t textWidth = display->getStringWidth(alertBannerMessage.c_str(), alertBannerMessage.length(), true);
|
std::vector<String> lines;
|
||||||
uint16_t boxWidth = padding * 2 + textWidth;
|
int start = 0, newlineIdx;
|
||||||
uint16_t boxHeight = FONT_HEIGHT_SMALL + padding * 2;
|
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<uint16_t> 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 boxLeft = (display->width() / 2) - (boxWidth / 2);
|
||||||
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
|
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
|
||||||
@ -338,9 +355,16 @@ static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *sta
|
|||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); // Border
|
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); // Border
|
||||||
|
|
||||||
// === Draw the text (twice for faux bold) ===
|
// === Draw each line centered in the box ===
|
||||||
display->drawString(boxLeft + padding, boxTop + padding, alertBannerMessage);
|
int16_t lineY = boxTop + padding;
|
||||||
display->drawString(boxLeft + padding + 1, boxTop + padding, alertBannerMessage); // Faux bold effect
|
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
|
// 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
|
setFrames(FOCUS_PRESERVE); // Stay on same frame, silently add/remove frames
|
||||||
} else {
|
} else {
|
||||||
// Incoming message
|
// Incoming message
|
||||||
// setFrames(FOCUS_TEXTMESSAGE); // Focus on the new message
|
|
||||||
devicestate.has_rx_text_message = true; // Needed to include the message frame
|
devicestate.has_rx_text_message = true; // Needed to include the message frame
|
||||||
hasUnreadMessage = true; // Enables mail icon in the header
|
hasUnreadMessage = true; // Enables mail icon in the header
|
||||||
setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view
|
setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view
|
||||||
forceDisplay(); // Forces screen redraw (this works in your codebase)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1226,9 +1226,12 @@ void setup()
|
|||||||
LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset");
|
LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset");
|
||||||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
||||||
nodeDB->saveToDisk(SEGMENT_CONFIG);
|
nodeDB->saveToDisk(SEGMENT_CONFIG);
|
||||||
|
|
||||||
if (!rIf->reconfigure()) {
|
if (!rIf->reconfigure()) {
|
||||||
LOG_WARN("Reconfigure failed, rebooting");
|
LOG_WARN("Reconfigure failed, rebooting");
|
||||||
screen->startAlert("Rebooting...");
|
if (screen) {
|
||||||
|
screen->showOverlayBanner("Rebooting...");
|
||||||
|
}
|
||||||
rebootAtMsec = millis() + 5000;
|
rebootAtMsec = millis() + 5000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1096,7 +1096,7 @@ void AdminModule::handleGetDeviceUIConfig(const meshtastic_MeshPacket &req)
|
|||||||
void AdminModule::reboot(int32_t seconds)
|
void AdminModule::reboot(int32_t seconds)
|
||||||
{
|
{
|
||||||
LOG_INFO("Reboot in %d seconds", 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);
|
rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -466,49 +466,54 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event)
|
|||||||
if (moduleConfig.external_notification.enabled == true) {
|
if (moduleConfig.external_notification.enabled == true) {
|
||||||
if (externalNotificationModule->getMute()) {
|
if (externalNotificationModule->getMute()) {
|
||||||
externalNotificationModule->setMute(false);
|
externalNotificationModule->setMute(false);
|
||||||
showTemporaryMessage("Notifications \nEnabled");
|
if (screen) {
|
||||||
if (screen)
|
screen->removeFunctionSymbol("M");
|
||||||
screen->removeFunctionSymbol("M"); // remove the mute symbol from the bottom right corner
|
screen->showOverlayBanner("Notifications\nEnabled", 3000);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
externalNotificationModule->stopNow(); // this will turn off all GPIO and sounds and idle the loop
|
externalNotificationModule->stopNow();
|
||||||
externalNotificationModule->setMute(true);
|
externalNotificationModule->setMute(true);
|
||||||
showTemporaryMessage("Notifications \nDisabled");
|
if (screen) {
|
||||||
if (screen)
|
screen->setFunctionSymbol("M");
|
||||||
screen->setFunctionSymbol("M"); // add the mute symbol to the bottom right corner
|
screen->showOverlayBanner("Notifications\nDisabled", 3000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case INPUT_BROKER_MSG_GPS_TOGGLE: // toggle GPS like triple press does
|
|
||||||
#if !MESHTASTIC_EXCLUDE_GPS
|
case INPUT_BROKER_MSG_GPS_TOGGLE:
|
||||||
|
#if !MESHTASTIC_EXCLUDE_GPS
|
||||||
if (gps != nullptr) {
|
if (gps != nullptr) {
|
||||||
gps->toggleGpsMode();
|
gps->toggleGpsMode();
|
||||||
}
|
}
|
||||||
if (screen)
|
if (screen) {
|
||||||
screen->forceDisplay();
|
screen->forceDisplay();
|
||||||
showTemporaryMessage("GPS Toggled");
|
screen->showOverlayBanner("GPS Toggled", 3000);
|
||||||
#endif
|
}
|
||||||
|
#endif
|
||||||
break;
|
break;
|
||||||
case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE: // toggle Bluetooth on/off
|
|
||||||
|
case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE:
|
||||||
if (config.bluetooth.enabled == true) {
|
if (config.bluetooth.enabled == true) {
|
||||||
config.bluetooth.enabled = false;
|
config.bluetooth.enabled = false;
|
||||||
LOG_INFO("User toggled Bluetooth");
|
LOG_INFO("User toggled Bluetooth");
|
||||||
nodeDB->saveToDisk();
|
nodeDB->saveToDisk();
|
||||||
disableBluetooth();
|
disableBluetooth();
|
||||||
showTemporaryMessage("Bluetooth OFF");
|
if (screen) screen->showOverlayBanner("Bluetooth OFF", 3000);
|
||||||
} else if (config.bluetooth.enabled == false) {
|
} else {
|
||||||
config.bluetooth.enabled = true;
|
config.bluetooth.enabled = true;
|
||||||
LOG_INFO("User toggled Bluetooth");
|
LOG_INFO("User toggled Bluetooth");
|
||||||
nodeDB->saveToDisk();
|
nodeDB->saveToDisk();
|
||||||
rebootAtMsec = millis() + 2000;
|
rebootAtMsec = millis() + 2000;
|
||||||
showTemporaryMessage("Bluetooth ON\nReboot");
|
if (screen) screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case INPUT_BROKER_MSG_SEND_PING: // fn+space send network ping like double press does
|
case INPUT_BROKER_MSG_SEND_PING:
|
||||||
service->refreshLocalMeshNode();
|
service->refreshLocalMeshNode();
|
||||||
if (service->trySendPosition(NODENUM_BROADCAST, true)) {
|
if (service->trySendPosition(NODENUM_BROADCAST, true)) {
|
||||||
showTemporaryMessage("Position \nUpdate Sent");
|
if (screen) screen->showOverlayBanner("Position\nUpdate Sent", 3000);
|
||||||
} else {
|
} else {
|
||||||
showTemporaryMessage("Node Info \nUpdate Sent");
|
if (screen) screen->showOverlayBanner("Node Info\nUpdate Sent", 3000);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case INPUT_BROKER_MSG_DISMISS_FRAME: // fn+del: dismiss screen frames like text or waypoint
|
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
|
// handle fn+s for shutdown
|
||||||
case INPUT_BROKER_MSG_SHUTDOWN:
|
case INPUT_BROKER_MSG_SHUTDOWN:
|
||||||
if (screen)
|
if (screen)
|
||||||
screen->startAlert("Shutting down...");
|
|
||||||
shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000;
|
shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000;
|
||||||
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||||
break;
|
break;
|
||||||
// and fn+r for reboot
|
// and fn+r for reboot
|
||||||
case INPUT_BROKER_MSG_REBOOT:
|
case INPUT_BROKER_MSG_REBOOT:
|
||||||
if (screen)
|
if (screen)
|
||||||
screen->startAlert("Rebooting...");
|
screen->showOverlayBanner("Rebooting...", 0); // stays on screen
|
||||||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
||||||
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||||
break;
|
break;
|
||||||
|
@ -402,14 +402,24 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
|
|||||||
entries.push_back("Prss: " + String(m.barometric_pressure, 0) + " hPa");
|
entries.push_back("Prss: " + String(m.barometric_pressure, 0) + " hPa");
|
||||||
if (m.iaq != 0) {
|
if (m.iaq != 0) {
|
||||||
String aqi = "IAQ: " + String(m.iaq);
|
String aqi = "IAQ: " + String(m.iaq);
|
||||||
|
const char *bannerMsg = nullptr; // Default: no banner
|
||||||
|
|
||||||
if (m.iaq <= 50) aqi += " (Good)";
|
if (m.iaq <= 25) aqi += " (Excellent)";
|
||||||
|
else if (m.iaq <= 50) aqi += " (Good)";
|
||||||
else if (m.iaq <= 100) aqi += " (Moderate)";
|
else if (m.iaq <= 100) aqi += " (Moderate)";
|
||||||
else if (m.iaq <= 150) aqi += " (Poor)";
|
else if (m.iaq <= 150) aqi += " (Poor)";
|
||||||
else if (m.iaq <= 200) aqi += " (Unhealthy)";
|
else if (m.iaq <= 200) {
|
||||||
else if (m.iaq <= 250) aqi += " (Very Unhealthy)";
|
aqi += " (Unhealthy)";
|
||||||
else if (m.iaq <= 350) aqi += " (Hazardous)";
|
bannerMsg = "Unhealthy IAQ";
|
||||||
else aqi += " (Extreme)";
|
}
|
||||||
|
else if (m.iaq <= 300) {
|
||||||
|
aqi += " (Very Unhealthy)";
|
||||||
|
bannerMsg = "Very Unhealthy IAQ";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
aqi += " (Hazardous)";
|
||||||
|
bannerMsg = "Hazardous IAQ";
|
||||||
|
}
|
||||||
|
|
||||||
entries.push_back(aqi);
|
entries.push_back(aqi);
|
||||||
|
|
||||||
@ -418,14 +428,17 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
|
|||||||
uint32_t now = millis();
|
uint32_t now = millis();
|
||||||
|
|
||||||
bool isOwnTelemetry = lastMeasurementPacket->from == nodeDB->getNodeNum();
|
bool isOwnTelemetry = lastMeasurementPacket->from == nodeDB->getNodeNum();
|
||||||
bool isIAQAlert = m.iaq > 200 && (now - lastAlertTime > 60000);
|
bool isCooldownOver = (now - lastAlertTime > 60000);
|
||||||
|
|
||||||
if (isOwnTelemetry && isIAQAlert) {
|
if (isOwnTelemetry && bannerMsg && isCooldownOver) {
|
||||||
LOG_INFO("drawFrame: IAQ %d (own) — showing banner", m.iaq);
|
LOG_INFO("drawFrame: IAQ %d (own) — showing banner: %s", m.iaq, bannerMsg);
|
||||||
screen->showOverlayBanner("Unhealthy IAQ Levels", 3000); // Always show banner
|
screen->showOverlayBanner(bannerMsg, 3000);
|
||||||
if (moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) {
|
|
||||||
playLongBeep(); // Only buzz if not muted
|
// Only buzz if IAQ is over 200
|
||||||
|
if (m.iaq > 200 && moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) {
|
||||||
|
playLongBeep();
|
||||||
}
|
}
|
||||||
|
|
||||||
lastAlertTime = now;
|
lastAlertTime = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ void powerCommandsCheck()
|
|||||||
|
|
||||||
#if defined(ARCH_ESP32) || defined(ARCH_NRF52)
|
#if defined(ARCH_ESP32) || defined(ARCH_NRF52)
|
||||||
if (shutdownAtMsec) {
|
if (shutdownAtMsec) {
|
||||||
screen->startAlert("Shutting down...");
|
screen->showOverlayBanner("Shutting Down...", 0); // stays on screen
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user