#ifdef MESHTASTIC_INCLUDE_INKHUD #include "./Events.h" #include "PowerFSM.h" #include "RTC.h" #include "buzz.h" #include "modules/ExternalNotificationModule.h" #include "modules/TextMessageModule.h" #include "sleep.h" #include "./Applet.h" #include "./SystemApplet.h" #include "graphics/niche/Utils/FlashData.h" using namespace NicheGraphics; namespace { // When touch long-press opens menu, some panels report a delayed release/tap // if the finger stays down briefly. Keep this long enough to cover that release. constexpr uint32_t TOUCH_MENU_OPEN_TAP_SUPPRESS_MS = 1200; inline void noteInkHUDUserInteraction() { // Keep power state and screen-timeout behavior in sync with InkHUD input activity. powerFSM.trigger(EVENT_INPUT); } } // namespace InkHUD::Events::Events() { // Get convenient references inkhud = InkHUD::getInstance(); settings = &inkhud->persistence->settings; } void InkHUD::Events::begin() { // Register our callbacks for the various events deepSleepObserver.observe(¬ifyDeepSleep); rebootObserver.observe(¬ifyReboot); textMessageObserver.observe(textMessageModule); #if !MESHTASTIC_EXCLUDE_ADMIN adminMessageObserver.observe((Observable *)adminModule); #endif #ifdef ARCH_ESP32 lightSleepObserver.observe(¬ifyLightSleep); #endif } void InkHUD::Events::onButtonShort() { noteInkHUDUserInteraction(); // Audio feedback (via buzzer) // Short tone playChirp(); // Cancel any beeping, buzzing, blinking // Some button handling suppressed if we are dismissing an external notification (see below) bool dismissedExt = dismissExternalNotification(); // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleInput) { consumer = sa; break; } } // If no system applet is handling input, default behavior instead is to cycle applets // or open menu if joystick is enabled if (consumer) { consumer->onButtonShortPress(); } else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module Applet *userConsumer = inkhud->getActiveApplet(); if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::BUTTON_SHORT)) userConsumer->onButtonShortPress(); else { if (!settings->joystick.enabled) inkhud->nextApplet(); else inkhud->openMenu(); } } } void InkHUD::Events::onButtonLong() { noteInkHUDUserInteraction(); // Audio feedback (via buzzer) // Slightly longer than playChirp playBoop(); // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleInput) { consumer = sa; break; } } // If no system applet is handling input, default behavior instead is to open the menu if (consumer) consumer->onButtonLongPress(); else { Applet *userConsumer = inkhud->getActiveApplet(); if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::BUTTON_LONG)) userConsumer->onButtonLongPress(); else inkhud->openMenu(); } } void InkHUD::Events::onExitShort() { // Preserve legacy behavior on non-touch builds: // EXIT input is only active when joystick mode is enabled. // Touch-capable builds intentionally bypass this joystick gate. if (!settings->joystick.enabled && !inkhud->hasTouchEnabledProvider()) { return; } noteInkHUDUserInteraction(); // Audio feedback (via buzzer) // Short tone playChirp(); // Cancel any beeping, buzzing, blinking // Some button handling suppressed if we are dismissing an external notification module (see below) bool dismissedExt = dismissExternalNotification(); // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleInput) { consumer = sa; break; } } // Always let active system applets consume EXIT/HOME input (menu close, keyboard cancel, etc), // including on touch-first nodes where joystick mode is disabled. if (consumer) { consumer->onExitShort(); return; } // Touch-capable InkHUD nodes use EXIT/HOME as a quick app switcher launcher. if (!dismissedExt && inkhud->hasTouchEnabledProvider()) { inkhud->openAppSwitcher(); return; } if (!dismissedExt) { Applet *userConsumer = inkhud->getActiveApplet(); if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_SHORT)) { userConsumer->onExitShort(); } else if (settings->joystick.enabled) { // Preserve existing joystick behavior when no applet handles EXIT. inkhud->nextTile(); } } } void InkHUD::Events::onExitLong() { // Preserve legacy behavior on non-touch builds: // EXIT input is only active when joystick mode is enabled. // Touch-capable builds intentionally bypass this joystick gate. if (!settings->joystick.enabled && !inkhud->hasTouchEnabledProvider()) { return; } noteInkHUDUserInteraction(); // Audio feedback (via buzzer) // Slightly longer than playChirp playBoop(); // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleInput) { consumer = sa; break; } } // Always allow system applets to consume EXIT/HOME long-press. if (consumer) { consumer->onExitLong(); } else { Applet *userConsumer = inkhud->getActiveApplet(); if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_LONG)) userConsumer->onExitLong(); // Nothing uses exit long yet } } void InkHUD::Events::onNavUp() { if (settings->joystick.enabled) onTouchNavUp(); } void InkHUD::Events::onNavDown() { if (settings->joystick.enabled) onTouchNavDown(); } void InkHUD::Events::onNavLeft() { if (settings->joystick.enabled) onTouchNavLeft(); } void InkHUD::Events::onNavRight() { if (settings->joystick.enabled) onTouchNavRight(); } void InkHUD::Events::onTouchNavUp() { noteInkHUDUserInteraction(); // Audio feedback (via buzzer) // Short tone playChirp(); // Cancel any beeping, buzzing, blinking // Some button handling suppressed if we are dismissing an external notification (see below) bool dismissedExt = dismissExternalNotification(); // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleInput) { consumer = sa; break; } } if (consumer) consumer->onNavUp(); else if (!dismissedExt) { Applet *userConsumer = inkhud->getActiveApplet(); if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_UP)) userConsumer->onNavUp(); } } void InkHUD::Events::onTouchNavDown() { noteInkHUDUserInteraction(); // Audio feedback (via buzzer) // Short tone playChirp(); // Cancel any beeping, buzzing, blinking // Some button handling suppressed if we are dismissing an external notification (see below) bool dismissedExt = dismissExternalNotification(); // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleInput) { consumer = sa; break; } } if (consumer) consumer->onNavDown(); else if (!dismissedExt) { Applet *userConsumer = inkhud->getActiveApplet(); if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_DOWN)) userConsumer->onNavDown(); } } void InkHUD::Events::onTouchNavLeft() { noteInkHUDUserInteraction(); // Audio feedback (via buzzer) // Short tone playChirp(); // Cancel any beeping, buzzing, blinking // Some button handling suppressed if we are dismissing an external notification (see below) bool dismissedExt = dismissExternalNotification(); // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleInput) { consumer = sa; break; } } // If no system applet is handling input, default behavior instead is to cycle applets if (consumer) consumer->onNavLeft(); else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module Applet *userConsumer = inkhud->getActiveApplet(); if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_LEFT)) userConsumer->onNavLeft(); else inkhud->prevApplet(); } } void InkHUD::Events::onTouchNavRight() { noteInkHUDUserInteraction(); // Audio feedback (via buzzer) // Short tone playChirp(); // Cancel any beeping, buzzing, blinking // Some button handling suppressed if we are dismissing an external notification (see below) bool dismissedExt = dismissExternalNotification(); // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleInput) { consumer = sa; break; } } // If no system applet is handling input, default behavior instead is to cycle applets if (consumer) consumer->onNavRight(); else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module Applet *userConsumer = inkhud->getActiveApplet(); if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_RIGHT)) userConsumer->onNavRight(); else inkhud->nextApplet(); } } void InkHUD::Events::onTouchTap(uint16_t x, uint16_t y, bool longPress) { const bool touchEnabledBuild = inkhud->hasTouchEnabledProvider(); // A long-press used to open the menu can be followed by a synthetic/queued tap at release. // Ignore that brief follow-up window so touch-opened menus do not auto-select an item. if (touchEnabledBuild && !longPress && suppressTouchTapUntilMs != 0) { if ((int32_t)(millis() - suppressTouchTapUntilMs) < 0) { noteInkHUDUserInteraction(); return; } suppressTouchTapUntilMs = 0; } // Give system applets (menu, keyboard, etc) first chance to consume direct touch input. for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleInput && sa->onTouchPoint(x, y, longPress)) { noteInkHUDUserInteraction(); return; } } // In split layouts, tapping a different tile changes focus. // Consume this tap so selection does not also trigger button fallback behavior. if (inkhud->selectTileAt(x, y)) { noteInkHUDUserInteraction(); return; } // Allow foreground user applet to consume direct touch input if it wants. Applet *userConsumer = inkhud->getActiveApplet(); if (userConsumer != nullptr && userConsumer->onTouchPoint(x, y, longPress)) { noteInkHUDUserInteraction(); return; } // Fallback to existing button semantics so non-touch-aware applets keep working unchanged. if (longPress) { onButtonLong(); // Only arm suppression if the long-press actually opened menu foreground. SystemApplet *menu = inkhud->getSystemApplet("Menu"); if (touchEnabledBuild && menu && menu->isForeground()) { suppressTouchTapUntilMs = millis() + TOUCH_MENU_OPEN_TAP_SUPPRESS_MS; } } else onButtonShort(); } void InkHUD::Events::onFreeText(char c) { noteInkHUDUserInteraction(); // Trigger the first system applet that wants to handle the new character for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleFreeText) { sa->onFreeText(c); break; } } } void InkHUD::Events::onFreeTextDone() { noteInkHUDUserInteraction(); // Trigger the first system applet that wants to handle it for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleFreeText) { sa->onFreeTextDone(); break; } } } void InkHUD::Events::onFreeTextCancel() { noteInkHUDUserInteraction(); // Trigger the first system applet that wants to handle it for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleFreeText) { sa->onFreeTextCancel(); break; } } } // Callback for deepSleepObserver // Returns 0 to signal that we agree to sleep now int InkHUD::Events::beforeDeepSleep(void *unused) { // If a previous display update is in progress, wait for it to complete. inkhud->awaitUpdate(); // Notify all applets that we're shutting down for (Applet *ua : inkhud->userApplets) { ua->onDeactivate(); ua->onShutdown(); } for (SystemApplet *sa : inkhud->systemApplets) { // Note: no onDeactivate. System applets are always active. sa->onShutdown(); } // User has successful executed a safe shutdown // We don't need to nag at boot anymore settings->tips.safeShutdownSeen = true; inkhud->persistence->saveSettings(); inkhud->persistence->saveLatestMessage(); // LogoApplet::onShutdown attempted to heal the display by drawing a "shutting down" screen twice, // then prepared a final powered-off screen for us, which shows device shortname. // We're updating to show that one now. inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, true, false); delay(1000); // Cooldown, before potentially yanking display power // InkHUD shutdown complete // Firmware shutdown continues for several seconds more; flash write still pending playShutdownMelody(); return 0; // We agree: deep sleep now } // Display an intermediate screen while configuration changes are applied void InkHUD::Events::applyingChanges() { // Bring the logo applet forward with a temporary message for (SystemApplet *sa : inkhud->systemApplets) { sa->onApplyingChanges(); } } // Callback for rebootObserver // Same as shutdown, without drawing the logoApplet // Makes sure we don't lose message history / InkHUD config int InkHUD::Events::beforeReboot(void *unused) { // Notify all applets that we're "shutting down" // They don't need to know that it's really a reboot for (Applet *a : inkhud->userApplets) { a->onDeactivate(); a->onShutdown(); } for (SystemApplet *sa : inkhud->systemApplets) { // Note: no onDeactivate. System applets are always active. sa->onReboot(); } // Save settings to flash, or erase if factory reset in progress if (!eraseOnReboot) { inkhud->persistence->saveSettings(); inkhud->persistence->saveLatestMessage(); } else { NicheGraphics::clearFlashData(); } // Note: no forceUpdate call here // We don't have any final screen to draw, although LogoApplet::onReboot did already display a "rebooting" screen return 0; // No special status to report. Ignored anyway by this Observable } // Callback when a new text message is received // Caches the most recently received message, for use by applets // Rx does not trigger a save to flash, however the data *will* be saved alongside other during shutdown, etc. // Note: this is intentionally separate from device-state message fields. int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet) { // Short circuit: don't store outgoing messages if (getFrom(packet) == nodeDB->getNodeNum()) return 0; // Determine whether the message is broadcast or a DM // Store this info to prevent confusion after a reboot // Avoids need to compare timestamps, because of situation where "future" messages block newly received, if time not set inkhud->persistence->latestMessage.wasBroadcast = isBroadcast(packet->to); // Pick the appropriate variable to store the message in MessageStore::Message *storedMessage = inkhud->persistence->latestMessage.wasBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm; // Store nodenum of the sender // Applets can use this to fetch user data from nodedb, if they want storedMessage->sender = packet->from; // Store the time (epoch seconds) when message received storedMessage->timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time // Store the channel // - (potentially) used to determine whether notification shows // - (potentially) used to determine which applet to focus storedMessage->channelIndex = packet->channel; // Store the text // Need to specify manually how many bytes, because source not null-terminated storedMessage->text = std::string(&packet->decoded.payload.bytes[0], &packet->decoded.payload.bytes[packet->decoded.payload.size]); return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) } int InkHUD::Events::onAdminMessage(AdminModule_ObserverData *data) { switch (data->request->which_payload_variant) { // Factory reset // Two possible messages. One preserves BLE bonds, other wipes. Both should clear InkHUD data. case meshtastic_AdminMessage_factory_reset_device_tag: case meshtastic_AdminMessage_factory_reset_config_tag: eraseOnReboot = true; *data->result = AdminMessageHandleResult::HANDLED; break; default: break; } return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) } #ifdef ARCH_ESP32 // Callback for lightSleepObserver // Make sure the display is not partway through an update when we begin light sleep // This is because some displays require active input from us to terminate the update process, and protect the panel hardware int InkHUD::Events::beforeLightSleep(void *unused) { inkhud->awaitUpdate(); return 0; // No special status to report. Ignored anyway by this Observable } #endif // Silence all ongoing beeping, blinking, buzzing, coming from the external notification module // Returns true if an external notification was active, and we dismissed it // Button handling changes depending on our result bool InkHUD::Events::dismissExternalNotification() { // Abort if not using external notifications if (!moduleConfig.external_notification.enabled) return false; // Abort if nothing to dismiss if (!externalNotificationModule->nagging()) return false; // Stop the beep buzz blink externalNotificationModule->stopNow(); // Inform that we did indeed dismiss an external notification return true; } #endif