#ifdef MESHTASTIC_INCLUDE_INKHUD #include "./InkHUD.h" #include "./Applet.h" #include "./Events.h" #include "./Persistence.h" #include "./Renderer.h" #include "./SystemApplet.h" #include "./Tile.h" #include "./WindowManager.h" #include "FSCommon.h" #include "MessageStore.h" #include "SPILock.h" #include "concurrency/LockGuard.h" using namespace NicheGraphics; // Get or create the singleton InkHUD::InkHUD *InkHUD::InkHUD::getInstance() { // Create the singleton instance of our class, if not yet done static InkHUD *instance = nullptr; if (!instance) { instance = new InkHUD; instance->persistence = new Persistence; instance->windowManager = new WindowManager; instance->renderer = new Renderer; instance->events = new Events; } return instance; } // Connect the (fully set-up) E-Ink driver to InkHUD // Should happen in your variant's nicheGraphics.h file, before InkHUD::begin is called void InkHUD::InkHUD::setDriver(Drivers::EInk *driver) { renderer->setDriver(driver); } // Set the target number of FAST display updates in a row, before a FULL update is used for display health // This value applies only to updates with an UNSPECIFIED update type // If explicitly requested FAST updates exceed this target, the stressMultiplier parameter determines how many // subsequent FULL updates will be performed, in an attempt to restore the display's health void InkHUD::InkHUD::setDisplayResilience(uint8_t fastPerFull, float stressMultiplier) { renderer->setDisplayResilience(fastPerFull, stressMultiplier); } // Register a user applet with InkHUD // A variant's nicheGraphics.h file should instantiate your chosen applets, then pass them to this method // Passing an applet to this method is all that is required to make it available to the user in your InkHUD build void InkHUD::InkHUD::addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile) { windowManager->addApplet(name, a, defaultActive, defaultAutoshow, onTile); } void InkHUD::InkHUD::notifyApplyingChanges() { if (events) { events->applyingChanges(); } } // One-time migration from the old per-channel InkHUD message files (/NicheGraphics/ch*.msgs) // to the firmware-wide MessageStore format (/Messages_default.msgs). // Only runs when the new store loaded empty, meaning this is the first boot after the format change. // Old files are deleted once migrated. static void migrateOldInkHUDMessages() { #ifdef FSCom bool migrated = false; constexpr uint8_t MAX_CHANNELS = 8; constexpr uint32_t OLD_MAX_MSG_SIZE = 250; for (uint8_t ch = 0; ch < MAX_CHANNELS; ch++) { std::string path = "/NicheGraphics/ch"; path += std::to_string(ch); path += ".msgs"; spiLock->lock(); bool exists = FSCom.exists(path.c_str()); spiLock->unlock(); if (!exists) continue; concurrency::LockGuard guard(spiLock); auto f = FSCom.open(path.c_str(), FILE_O_READ); if (!f || f.size() == 0) { if (f) f.close(); FSCom.remove(path.c_str()); continue; } uint8_t count = 0; f.readBytes(reinterpret_cast(&count), 1); std::vector channelMsgs; for (uint8_t i = 0; i < count; i++) { StoredMessage sm; f.readBytes(reinterpret_cast(&sm.timestamp), sizeof(sm.timestamp)); f.readBytes(reinterpret_cast(&sm.sender), sizeof(sm.sender)); f.readBytes(reinterpret_cast(&sm.channelIndex), sizeof(sm.channelIndex)); char textBuf[OLD_MAX_MSG_SIZE + 1] = {}; uint32_t textLen = 0; char c; while (textLen < OLD_MAX_MSG_SIZE) { if (f.readBytes(&c, 1) != 1) break; if (c == '\0') break; textBuf[textLen++] = c; } sm.dest = NODENUM_BROADCAST; sm.type = MessageType::BROADCAST; sm.isBootRelative = false; sm.ackStatus = AckStatus::ACKED; size_t storedLen = (textLen >= MAX_MESSAGE_SIZE) ? MAX_MESSAGE_SIZE - 1 : textLen; sm.textOffset = MessageStore::storeText(textBuf, storedLen); sm.textLength = static_cast(storedLen); channelMsgs.push_back(sm); } // Old format stored newest-first (push_front); insert oldest-first for correct chronological order for (int i = static_cast(channelMsgs.size()) - 1; i >= 0; i--) messageStore.addLiveMessage(channelMsgs[i]); if (!channelMsgs.empty()) migrated = true; f.close(); FSCom.remove(path.c_str()); LOG_INFO("Migrated %u messages from %s", static_cast(count), path.c_str()); } // Delete the old latest.msgs; the latestMessage cache will be re-derived from migrated channel messages spiLock->lock(); if (FSCom.exists("/NicheGraphics/latest.msgs")) FSCom.remove("/NicheGraphics/latest.msgs"); spiLock->unlock(); if (migrated) { LOG_INFO("InkHUD message migration complete, saving to new format"); messageStore.saveToFlash(); } #endif } // Start InkHUD! // Call this only after you have configured InkHUD void InkHUD::InkHUD::begin() { persistence->loadSettings(); messageStore.loadFromFlash(); // Load persisted messages before deriving latestMessage cache if (messageStore.getLiveMessages().empty()) migrateOldInkHUDMessages(); // First boot after format change: import old per-channel files persistence->loadLatestMessage(); windowManager->begin(); events->begin(); renderer->begin(); // LogoApplet shows boot screen here } void InkHUD::InkHUD::setTouchEnabledProvider(TouchEnabledProvider provider) { touchEnabledProvider = provider; } bool InkHUD::InkHUD::hasTouchEnabledProvider() const { return touchEnabledProvider != nullptr; } bool InkHUD::InkHUD::isTouchEnabled() const { if (!touchEnabledProvider) return true; return touchEnabledProvider(); } // Call this when your user button gets a short press // Should be connected to an input source in nicheGraphics.h (NicheGraphics::Inputs::TwoButton?) void InkHUD::InkHUD::shortpress() { events->onButtonShort(); } // Call this when your user button gets a long press // Should be connected to an input source in nicheGraphics.h (NicheGraphics::Inputs::TwoButton?) void InkHUD::InkHUD::longpress() { events->onButtonLong(); } // Call this when your exit button gets a short press void InkHUD::InkHUD::exitShort() { events->onExitShort(); } // Call this when your exit button gets a long press void InkHUD::InkHUD::exitLong() { events->onExitLong(); } // Call this when your joystick gets an up input void InkHUD::InkHUD::navUp() { switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { case 1: // 90 deg events->onNavLeft(); break; case 2: // 180 deg events->onNavDown(); break; case 3: // 270 deg events->onNavRight(); break; default: // 0 deg events->onNavUp(); break; } } // Call this when your joystick gets a down input void InkHUD::InkHUD::navDown() { switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { case 1: // 90 deg events->onNavRight(); break; case 2: // 180 deg events->onNavUp(); break; case 3: // 270 deg events->onNavLeft(); break; default: // 0 deg events->onNavDown(); break; } } // Call this when your joystick gets a left input void InkHUD::InkHUD::navLeft() { switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { case 1: // 90 deg events->onNavDown(); break; case 2: // 180 deg events->onNavRight(); break; case 3: // 270 deg events->onNavUp(); break; default: // 0 deg events->onNavLeft(); break; } } // Call this when your joystick gets a right input void InkHUD::InkHUD::navRight() { switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { case 1: // 90 deg events->onNavUp(); break; case 2: // 180 deg events->onNavLeft(); break; case 3: // 270 deg events->onNavDown(); break; default: // 0 deg events->onNavRight(); break; } } // Call this when touch input needs joystick-like up navigation independent of joystick-enabled mode void InkHUD::InkHUD::touchNavUp() { switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { case 1: // 90 deg events->onTouchNavLeft(); break; case 2: // 180 deg events->onTouchNavDown(); break; case 3: // 270 deg events->onTouchNavRight(); break; default: // 0 deg events->onTouchNavUp(); break; } } // Call this when touch input needs joystick-like down navigation independent of joystick-enabled mode void InkHUD::InkHUD::touchNavDown() { switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { case 1: // 90 deg events->onTouchNavRight(); break; case 2: // 180 deg events->onTouchNavUp(); break; case 3: // 270 deg events->onTouchNavLeft(); break; default: // 0 deg events->onTouchNavDown(); break; } } void InkHUD::InkHUD::touchTap(uint16_t x, uint16_t y) { events->onTouchTap(x, y, false); } void InkHUD::InkHUD::touchLongPress(uint16_t x, uint16_t y) { events->onTouchTap(x, y, true); } // Call this for keyboard input // The Keyboard Applet also calls this void InkHUD::InkHUD::freeText(char c) { events->onFreeText(c); } // Call this to complete a freetext input void InkHUD::InkHUD::freeTextDone() { events->onFreeTextDone(); } // Call this to cancel a freetext input void InkHUD::InkHUD::freeTextCancel() { events->onFreeTextCancel(); } // Cycle the next user applet to the foreground // Only activated applets are cycled // If user has a multi-applet layout, the applets will cycle on the "focused tile" void InkHUD::InkHUD::nextApplet() { windowManager->nextApplet(); } // Cycle the previous user applet to the foreground // Only activated applets are cycled // If user has a multi-applet layout, the applets will cycle on the "focused tile" void InkHUD::InkHUD::prevApplet() { windowManager->prevApplet(); } // Returns the currently active applet InkHUD::Applet *InkHUD::InkHUD::getActiveApplet() { return windowManager->getActiveApplet(); } // Show the menu (on the the focused tile) // The applet previously displayed there will be restored once the menu closes void InkHUD::InkHUD::openMenu() { windowManager->openMenu(); } // Show touch-friendly app switcher (on the focused tile) void InkHUD::InkHUD::openAppSwitcher() { windowManager->openAppSwitcher(); } // Bring AlignStick applet to the foreground void InkHUD::InkHUD::openAlignStick() { windowManager->openAlignStick(); } // Open the on-screen keyboard void InkHUD::InkHUD::openKeyboard() { windowManager->openKeyboard(); } // Close the on-screen keyboard void InkHUD::InkHUD::closeKeyboard() { windowManager->closeKeyboard(); } // In layouts where multiple applets are shown at once, change which tile is focused // The focused tile in the one which cycles applets on button short press, and displays menu on long press void InkHUD::InkHUD::nextTile() { windowManager->nextTile(); } // In layouts where multiple applets are shown at once, change which tile is focused // The focused tile in the one which cycles applets on button short press, and displays menu on long press void InkHUD::InkHUD::prevTile() { windowManager->prevTile(); } bool InkHUD::InkHUD::showApplet(uint8_t appletIndex) { return windowManager->showApplet(appletIndex); } bool InkHUD::InkHUD::selectTileAt(uint16_t x, uint16_t y) { return windowManager->selectTileAt(x, y); } // Rotate the display image by 90 degrees void InkHUD::InkHUD::rotate() { windowManager->rotate(); } // rotate the joystick in 90 degree increments void InkHUD::InkHUD::rotateJoystick(uint8_t angle) { persistence->settings.joystick.alignment += angle; persistence->settings.joystick.alignment %= 4; } // Show / hide the battery indicator in top-right void InkHUD::InkHUD::toggleBatteryIcon() { windowManager->toggleBatteryIcon(); } // An applet asking for the display to be updated // This does not occur immediately // Instead, rendering is scheduled ASAP, for the next Renderer::runOnce call // This allows multiple applets to observe the same event, and then share the same opportunity to update // Applets should requestUpdate, whether or not they are currently displayed ("foreground") // This is because they *might* be automatically brought to foreground by WindowManager::autoshow void InkHUD::InkHUD::requestUpdate() { renderer->requestUpdate(); } // Demand that the display be updated // Ignores all diplomacy: // - the display *will* update // - the specified update type *will* be used // If the all parameter is true, the whole screen buffer is cleared and re-rendered // If the async parameter is false, code flow is blocked while the update takes place void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool all, bool async) { renderer->forceUpdate(type, all, async); } // Wait for any in-progress display update to complete before continuing void InkHUD::InkHUD::awaitUpdate() { renderer->awaitUpdate(); } // Ask the window manager to potentially bring a different user applet to foreground // An applet will be brought to foreground if it has just received new and relevant info // For Example: AllMessagesApplet has just received a new text message // Permission for this autoshow behavior is granted by the user, on an applet-by-applet basis // If autoshow brings an applet to foreground, an InkHUD notification will not be generated for the same event void InkHUD::InkHUD::autoshow() { windowManager->autoshow(); } // Tell the window manager that the Persistence::Settings value for applet activation has changed, // and that it should reconfigure accordingly. // This is triggered at boot, or when the user enables / disabled applets via the on-screen menu void InkHUD::InkHUD::updateAppletSelection() { windowManager->changeActivatedApplets(); } // Tell the window manager that the Persistence::Settings value for layout or rotation has changed, // and that it should reconfigure accordingly. // This is triggered at boot, or by rotate / layout options in the on-screen menu void InkHUD::InkHUD::updateLayout() { windowManager->changeLayout(); } // Width of the display, in the context of the current rotation uint16_t InkHUD::InkHUD::width() { return renderer->width(); } // Height of the display, in the context of the current rotation uint16_t InkHUD::InkHUD::height() { return renderer->height(); } // A collection of any user tiles which do not have a valid user applet // This can occur in various situations, such as when a user enables fewer applets than their layout has tiles // The tiles (and which regions the occupy) are private information of the window manager // The renderer needs to know which regions (if any) are empty, // in order to fill them with a "placeholder" pattern. // -- There may be a tidier way to accomplish this -- std::vector InkHUD::InkHUD::getEmptyTiles() { return windowManager->getEmptyTiles(); } // Get a system applet by its name // This isn't particularly elegant, but it does avoid: // - passing around a big set of references // - having two sets of references (systemApplet vector for iteration) InkHUD::SystemApplet *InkHUD::InkHUD::getSystemApplet(const char *name) { for (SystemApplet *sa : systemApplets) { if (strcmp(name, sa->name) == 0) return sa; } assert(false); // Invalid name } // Place a pixel into the image buffer // The x and y coordinates are in the context of the current display rotation // - Applets pass "relative" pixels to tiles // - Tiles pass translated pixels to this method // - this methods (Renderer) places rotated pixels into the image buffer // This method provides the final formatting step required. The image buffer is suitable for writing to display void InkHUD::InkHUD::drawPixel(int16_t x, int16_t y, Color c) { renderer->handlePixel(x, y, c); } #endif